Revert "Refactor API modules to remove mock implementations and integrate real endpoints"

This reverts commit
This commit is contained in:
2026-07-15 10:27:38 +05:30
parent 0415794473
commit cb9fd7dfa8
27 changed files with 2811 additions and 416 deletions
@@ -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)