Revert "Refactor API modules to remove mock implementations and integrate real endpoints"
This reverts commit
This commit is contained in:
@@ -115,7 +115,7 @@ function NewPurchaseOrderContent() {
|
||||
setVendorId(rfqVendorId)
|
||||
setLines(
|
||||
rfq.lines.map((l): DraftLine => {
|
||||
const cell = comparison.rows.find((row) => row.itemId === l.itemId)?.quotes.find((c) => c.vendorId === rfqVendorId)
|
||||
const cell = comparison.lines.find((cl) => cl.itemId === l.itemId)?.cells.find((c) => c.vendorId === rfqVendorId)
|
||||
return {
|
||||
key: newKey(),
|
||||
itemId: l.itemId,
|
||||
|
||||
@@ -97,6 +97,7 @@ export default function RequisitionsListPage() {
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-3 text-sm">Doc No</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Status</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Lines</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Requested by</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Created</TableHead>
|
||||
</TableRow>
|
||||
@@ -112,6 +113,7 @@ export default function RequisitionsListPage() {
|
||||
<TableCell className="px-3 py-3.5">
|
||||
<RequisitionStatusBadge status={r.status} />
|
||||
</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{r.lineCount}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">#{r.requestedBy}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{new Date(r.createdAt).toLocaleString()}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -67,10 +67,13 @@ export default function RfqDetailPage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rfqId])
|
||||
|
||||
// The backend derives `comparison.vendorIds` from vendors who have actually quoted
|
||||
// (RfqService.GetComparisonAsync) — invited-but-not-yet-quoted vendors aren't
|
||||
// persisted anywhere, so "pending" vendors can no longer be listed here.
|
||||
const quotedVendorIds = useMemo(() => new Set(comparison?.vendorIds ?? []), [comparison])
|
||||
const quotedVendorIds = useMemo(() => {
|
||||
const set = new Set<number>()
|
||||
for (const line of comparison?.lines ?? []) for (const cell of line.cells) set.add(cell.vendorId)
|
||||
return set
|
||||
}, [comparison])
|
||||
|
||||
const pendingVendors = useMemo(() => (rfq ? rfq.vendorIds.filter((id) => !quotedVendorIds.has(id)) : []), [rfq, quotedVendorIds])
|
||||
|
||||
function itemFor(itemId: number) {
|
||||
return items.find((i) => i.itemId === itemId)
|
||||
@@ -79,14 +82,6 @@ export default function RfqDetailPage() {
|
||||
return vendors.find((v) => v.vendorId === vendorId)
|
||||
}
|
||||
|
||||
// The backend doesn't persist which vendors were "invited" — any active vendor that
|
||||
// hasn't already quoted can be offered a quotation (AddQuotationAsync only checks the
|
||||
// vendor exists and hasn't already quoted this RFQ, not that it was invited).
|
||||
const quotableVendors = useMemo(
|
||||
() => vendors.filter((v) => v.status === "Active" && !quotedVendorIds.has(v.vendorId)),
|
||||
[vendors, quotedVendorIds]
|
||||
)
|
||||
|
||||
function selectQuoteVendor(vendorId: number | null) {
|
||||
setQuoteVendorId(vendorId)
|
||||
setQuoteFormError(null)
|
||||
@@ -161,9 +156,8 @@ export default function RfqDetailPage() {
|
||||
<RfqStatusBadge status={rfq.status} />
|
||||
</div>
|
||||
<p className="text-base text-muted-foreground">
|
||||
From Requisition #{rfq.requisitionId}
|
||||
{quotedVendorIds.size > 0 &&
|
||||
` — Quoted: ${[...quotedVendorIds].map((id) => vendorFor(id)?.code ?? `#${id}`).join(", ")}`}
|
||||
{rfq.requisitionId ? `From Requisition #${rfq.requisitionId} — ` : ""}
|
||||
Invited: {rfq.vendorIds.map((id) => vendorFor(id)?.code ?? `#${id}`).join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,7 +191,7 @@ export default function RfqDetailPage() {
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-base font-semibold text-foreground">Vendor comparison</h2>
|
||||
{comparison.rows.every((r) => r.quotes.length === 0) ? (
|
||||
{comparison.lines.every((l) => l.cells.length === 0) ? (
|
||||
<p className="text-base text-muted-foreground">No quotations recorded yet.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -205,19 +199,19 @@ export default function RfqDetailPage() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-3 text-sm">Item</TableHead>
|
||||
{comparison.vendorIds.map((vid) => (
|
||||
{rfq.vendorIds.map((vid) => (
|
||||
<TableHead key={vid} className="h-12 px-3 text-sm">{vendorFor(vid)?.code ?? `#${vid}`}</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{comparison.rows.map((row) => {
|
||||
const item = itemFor(row.itemId)
|
||||
{comparison.lines.map((line) => {
|
||||
const item = itemFor(line.itemId)
|
||||
return (
|
||||
<TableRow key={row.itemId}>
|
||||
<TableCell className="px-3 py-3.5">{item ? `${item.sku} — ${item.name}` : `Item #${row.itemId}`}</TableCell>
|
||||
{comparison.vendorIds.map((vid) => {
|
||||
const cell = row.quotes.find((c) => c.vendorId === vid)
|
||||
<TableRow key={line.itemId}>
|
||||
<TableCell className="px-3 py-3.5">{item ? `${item.sku} — ${item.name}` : `Item #${line.itemId}`}</TableCell>
|
||||
{rfq.vendorIds.map((vid) => {
|
||||
const cell = line.cells.find((c) => c.vendorId === vid)
|
||||
return (
|
||||
<TableCell key={vid} className="px-3 py-3.5">
|
||||
{cell ? (
|
||||
@@ -254,7 +248,7 @@ export default function RfqDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{quotableVendors.length > 0 && (
|
||||
{pendingVendors.length > 0 && (
|
||||
<div className="flex flex-col gap-3 rounded-xl border p-5">
|
||||
<h2 className="text-base font-semibold text-foreground">Record a quotation</h2>
|
||||
|
||||
@@ -262,12 +256,12 @@ export default function RfqDetailPage() {
|
||||
<Label className="text-base">Vendor</Label>
|
||||
<Select<number | null> value={quoteVendorId} onValueChange={selectQuoteVendor}>
|
||||
<SelectTrigger className="h-11! w-full text-base">
|
||||
<SelectValue placeholder="Select a vendor" />
|
||||
<SelectValue placeholder="Select an invited vendor" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{quotableVendors.map((v) => (
|
||||
<SelectItem key={v.vendorId} value={v.vendorId} className="text-base">
|
||||
{v.code} — {v.name}
|
||||
{pendingVendors.map((vid) => (
|
||||
<SelectItem key={vid} value={vid} className="text-base">
|
||||
{vendorFor(vid)?.code ?? `#${vid}`} — {vendorFor(vid)?.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { itemsApi } from "@/lib/api/items"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { validateRfqLine } from "@/lib/validations/procurement"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CreateRfqLineInput, RequisitionSummary } from "@/types/procurement"
|
||||
import { CreateRfqLineInput } from "@/types/procurement"
|
||||
import { ItemListItem, Vendor } from "@/types/master-data"
|
||||
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
@@ -44,17 +44,13 @@ function emptyLine(): DraftLine {
|
||||
function NewRfqContent() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const requisitionIdParam = Number(searchParams.get("requisitionId")) || null
|
||||
const requisitionId = Number(searchParams.get("requisitionId")) || null
|
||||
|
||||
const [items, setItems] = useState<ItemListItem[] | null>(null)
|
||||
const [vendors, setVendors] = useState<Vendor[] | null>(null)
|
||||
const [requisitions, setRequisitions] = useState<RequisitionSummary[] | null>(null)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
const [requisitionLoading, setRequisitionLoading] = useState(!!requisitionIdParam)
|
||||
const [requisitionLoading, setRequisitionLoading] = useState(!!requisitionId)
|
||||
|
||||
// Backend requires a requisitionId to create an RFQ (CreateRfqRequest.RequisitionId
|
||||
// is [Required]) — if one wasn't passed in via query param, the user must pick one.
|
||||
const [requisitionId, setRequisitionId] = useState<number | null>(requisitionIdParam)
|
||||
const [vendorIds, setVendorIds] = useState<Set<number>>(new Set())
|
||||
const [lines, setLines] = useState<DraftLine[]>([emptyLine()])
|
||||
const [lineErrors, setLineErrors] = useState<Record<string, Record<string, string>>>({})
|
||||
@@ -63,46 +59,24 @@ function NewRfqContent() {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
itemsApi.list({ pageSize: 200, status: "Active" }),
|
||||
vendorsApi.list({ pageSize: 200, status: "Active" }),
|
||||
requisitionsApi.list({ status: "Submitted", pageSize: 200 }),
|
||||
])
|
||||
.then(([it, ve, req]) => {
|
||||
Promise.all([itemsApi.list({ pageSize: 200, status: "Active" }), vendorsApi.list({ pageSize: 200, status: "Active" })])
|
||||
.then(([it, ve]) => {
|
||||
setItems(it.items)
|
||||
setVendors(ve.items)
|
||||
setRequisitions(req.items)
|
||||
})
|
||||
.catch((err) => setLoadError(errorMessage(err)))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!requisitionIdParam) return
|
||||
if (!requisitionId) return
|
||||
requisitionsApi
|
||||
.get(requisitionIdParam)
|
||||
.get(requisitionId)
|
||||
.then((req) => {
|
||||
setLines(req.lines.map((l): DraftLine => ({ key: newKey(), itemId: l.itemId, qty: String(l.qty) })))
|
||||
})
|
||||
.catch((err) => setHeaderError(errorMessage(err)))
|
||||
.finally(() => setRequisitionLoading(false))
|
||||
}, [requisitionIdParam])
|
||||
|
||||
function selectRequisition(id: number | null) {
|
||||
setRequisitionId(id)
|
||||
setHeaderError(null)
|
||||
if (!id) {
|
||||
setLines([emptyLine()])
|
||||
return
|
||||
}
|
||||
setRequisitionLoading(true)
|
||||
requisitionsApi
|
||||
.get(id)
|
||||
.then((req) => {
|
||||
setLines(req.lines.map((l): DraftLine => ({ key: newKey(), itemId: l.itemId, qty: String(l.qty) })))
|
||||
})
|
||||
.catch((err) => setHeaderError(errorMessage(err)))
|
||||
.finally(() => setRequisitionLoading(false))
|
||||
}
|
||||
}, [requisitionId])
|
||||
|
||||
function toggleVendor(vendorId: number) {
|
||||
setVendorIds((prev) => {
|
||||
@@ -125,10 +99,6 @@ function NewRfqContent() {
|
||||
setHeaderError(null)
|
||||
setSubmitError(null)
|
||||
|
||||
if (!requisitionId) {
|
||||
setHeaderError("Select a requisition to raise this RFQ against.")
|
||||
return
|
||||
}
|
||||
if (vendorIds.size === 0) {
|
||||
setHeaderError("Select at least one vendor to invite.")
|
||||
return
|
||||
@@ -192,24 +162,6 @@ function NewRfqContent() {
|
||||
|
||||
{!loading && (
|
||||
<>
|
||||
{!requisitionIdParam && (
|
||||
<div className="flex flex-col gap-2 sm:w-96">
|
||||
<Label className="text-base">Requisition</Label>
|
||||
<Select<number | null> value={requisitionId} onValueChange={selectRequisition}>
|
||||
<SelectTrigger className="h-11! w-full text-base">
|
||||
<SelectValue placeholder="Select a submitted requisition" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(requisitions ?? []).map((r) => (
|
||||
<SelectItem key={r.requisitionId} value={r.requisitionId} className="text-base">
|
||||
{r.docNo}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Invite vendors</Label>
|
||||
<div className="grid grid-cols-1 gap-1 rounded-xl border p-3 sm:grid-cols-2">
|
||||
|
||||
@@ -5,8 +5,10 @@ import Link from "next/link"
|
||||
import { FileText, Plus } from "lucide-react"
|
||||
|
||||
import { rfqsApi } from "@/lib/api/rfqs"
|
||||
import { vendorsApi } from "@/lib/api/vendors"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { RfqSummary } from "@/types/procurement"
|
||||
import { Vendor } from "@/types/master-data"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
@@ -15,15 +17,22 @@ import { RfqStatusBadge } from "@/components/procurement/status-badges"
|
||||
|
||||
export default function RfqsListPage() {
|
||||
const [rfqs, setRfqs] = useState<RfqSummary[] | null>(null)
|
||||
const [vendors, setVendors] = useState<Vendor[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
rfqsApi
|
||||
.list()
|
||||
.then((r) => setRfqs(r.items))
|
||||
Promise.all([rfqsApi.list(), vendorsApi.list({ pageSize: 200 })])
|
||||
.then(([r, v]) => {
|
||||
setRfqs(r.items)
|
||||
setVendors(v.items)
|
||||
})
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}, [])
|
||||
|
||||
function vendorNames(vendorIds: number[]) {
|
||||
return vendorIds.map((id) => vendors.find((v) => v.vendorId === id)?.code ?? `#${id}`).join(", ")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -66,6 +75,7 @@ export default function RfqsListPage() {
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-3 text-sm">Doc No</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Requisition</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Vendors invited</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Status</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Created</TableHead>
|
||||
</TableRow>
|
||||
@@ -78,7 +88,8 @@ export default function RfqsListPage() {
|
||||
{r.docNo}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="px-3 py-3.5">#{r.requisitionId}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{r.requisitionId ? `#${r.requisitionId}` : <span className="text-muted-foreground">—</span>}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{vendorNames(r.vendorIds)}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">
|
||||
<RfqStatusBadge status={r.status} />
|
||||
</TableCell>
|
||||
|
||||
@@ -66,7 +66,6 @@ export default function EditGrnPage() {
|
||||
const grnId = Number(params.id)
|
||||
|
||||
const [grn, setGrn] = useState<Grn | null>(null)
|
||||
const [etag, setEtag] = useState<string | null>(null)
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
|
||||
const [items, setItems] = useState<ItemListItem[] | null>(null)
|
||||
const [uoms, setUoms] = useState<Uom[] | null>(null)
|
||||
@@ -84,19 +83,18 @@ export default function EditGrnPage() {
|
||||
useEffect(() => {
|
||||
if (!Number.isFinite(grnId)) return
|
||||
Promise.all([
|
||||
grnsApi.getWithETag(grnId),
|
||||
grnsApi.get(grnId),
|
||||
warehousesApi.list(),
|
||||
itemsApi.list({ pageSize: 200, status: "Active" }),
|
||||
uomsApi.list(),
|
||||
])
|
||||
.then(([{ data: g, etag: tag }, wh, it, uo]) => {
|
||||
.then(([g, wh, it, uo]) => {
|
||||
if (g.status !== "Draft") {
|
||||
setLoadError(`${g.docNo} is ${g.status.toLowerCase()} and can no longer be edited.`)
|
||||
setGrn(g)
|
||||
return
|
||||
}
|
||||
setGrn(g)
|
||||
setEtag(tag)
|
||||
setWarehouses(wh.items)
|
||||
setItems(it.items)
|
||||
setUoms(uo.items)
|
||||
@@ -190,23 +188,14 @@ export default function EditGrnPage() {
|
||||
}
|
||||
})
|
||||
|
||||
if (!etag) {
|
||||
setSubmitError("Missing concurrency token — reload the page and try again.")
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const updated = await grnsApi.update(
|
||||
grn.grnId,
|
||||
{
|
||||
poId: grn.poId,
|
||||
vendorId: grn.vendorId,
|
||||
warehouseId: warehouseId as number,
|
||||
lines: payloadLines,
|
||||
},
|
||||
etag
|
||||
)
|
||||
const updated = await grnsApi.update(grn.grnId, {
|
||||
poId: grn.poId,
|
||||
vendorId: grn.vendorId,
|
||||
warehouseId: warehouseId as number,
|
||||
lines: payloadLines,
|
||||
})
|
||||
toast.success("GRN updated", `${updated.docNo} saved.`)
|
||||
router.push(`/dashboard/receiving/grn/${updated.grnId}`)
|
||||
} catch (err) {
|
||||
|
||||
@@ -40,14 +40,9 @@ export default function NewWastagePage() {
|
||||
const [result, setResult] = useState<StockAdjustment | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
warehousesApi.list(),
|
||||
itemsApi.list({ pageSize: 200, status: "Active" }),
|
||||
reasonCodesApi.list("Adjustment"),
|
||||
wastageReasonCodeIds(),
|
||||
])
|
||||
.then(([wh, it, rc, wastageIdList]) => {
|
||||
const wastageIds = new Set(wastageIdList)
|
||||
Promise.all([warehousesApi.list(), itemsApi.list({ pageSize: 200, status: "Active" }), reasonCodesApi.list("Adjustment")])
|
||||
.then(([wh, it, rc]) => {
|
||||
const wastageIds = new Set(wastageReasonCodeIds())
|
||||
setWarehouses(wh.items)
|
||||
setItems(it.items)
|
||||
setReasonCodes(rc.items.filter((r) => wastageIds.has(r.reasonCodeId)))
|
||||
|
||||
@@ -31,9 +31,9 @@ export default function WastagePage() {
|
||||
const [reasonCodeId, setReasonCodeId] = useState<number | "All">("All")
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([reasonCodesApi.list("Adjustment"), itemsApi.list({ pageSize: 200 }), warehousesApi.list(), wastageReasonCodeIds()])
|
||||
.then(([rc, it, wh, wastageIdList]) => {
|
||||
const wastageIds = new Set(wastageIdList)
|
||||
Promise.all([reasonCodesApi.list("Adjustment"), itemsApi.list({ pageSize: 200 }), warehousesApi.list()])
|
||||
.then(([rc, it, wh]) => {
|
||||
const wastageIds = new Set(wastageReasonCodeIds())
|
||||
setReasonCodes(rc.items.filter((r) => wastageIds.has(r.reasonCodeId)))
|
||||
setItems(it.items)
|
||||
setWarehouses(wh.items)
|
||||
|
||||
Reference in New Issue
Block a user