- Updated stock-transfers.ts to replace mock data with API requests for stock transfers. - Refactored stock.ts to utilize API calls for stock inquiries instead of mock data. - Modified uoms.ts to implement real API requests for unit of measure operations. - Transitioned vendors.ts to use actual API endpoints for vendor management, removing mock data handling. - Updated warehouses.ts to replace mock implementations with real API calls for warehouse and bin management. - Refactored wastage.ts to utilize stock adjustments and reason codes APIs, removing mock data dependencies. - Adjusted procurement.ts to align with backend DTO shapes, ensuring consistency with planned backend structures.
36 KiB
Frontend — PROGRESS (Phase 1: Inventory & Supply Chain)
Legend: [ ] not started · [~] in progress · [x] done
Spec: docs/20-FRONTEND.md (flows + rules) · docs/11-BACKEND-PHASE1.md (API contract)
Convention: docs/01-DOC-GUIDE.md §6. Update this file in the same commit as the code. When ticking [x], append a short note + any deviation.
0. Foundation
NEXT_PUBLIC_API_BASE_URLwired (.env.local/.env.local.example, alongside the existingNEXT_PUBLIC_AUTH_API_BASE_URL) — points atBackend/ERPCore's https profile (7112)- Typed API client / fetch wrapper (
lib/api-client.ts:apiRequest/apiRequestWithETag/buildQuery) + bearer token handling (lib/auth-token.ts, degrades gracefully — no login endpoint wired yet, see §1) - Shared TS types mirroring API DTOs (
types/common.ts,types/master-data.ts,types/procurement.ts,types/grn.ts) — built only for the subset GRN needs (Item/Warehouse/Bin/Vendor/Uom/PurchaseOrder/Grn) - [~] Client validation helpers (
lib/validations/grn.ts) — deviation: useszod(already a project dependency, used bylib/validations.ts/login), not hand-rolled, to stay consistent with the codebase's existing pattern rather than introduce a second validation approach. UX-only; server remains authoritative (docs/20-FRONTEND.md §3) ProblemDetailsnormalizer (ApiErrorinlib/api-client.ts) +code → messagemap (lib/error-map.ts)
Scope note: this foundation was built alongside the GRN feature and only covers the endpoints GRN consumes (items, warehouses/bins, vendors, uoms, purchase-orders, grns). Other master-data/procurement API methods still need their own
lib/api/*.tsfiles when those screens are built.
2026-07-14 — mock data removed,
lib/api/mock-data.tsdeleted. Everylib/api/*.tsfile's commented-out realfetchblock was restored and the in-memory mock block deleted, per user request. Master Data + Procurement modules (categories,uoms,warehouses,items,vendors,requisitions,rfqs,purchase-orders) now call the live backend (Backend/ERPCore§1/§2, both implemented + smoke-tested). GRN/Stock/Purchase-Return/Reason-Code modules (grns,stock,stock-transfers,stock-adjustments,stock-counts,purchase-returns,reason-codes,wastage) also now call real (documented-or-assumed) endpoint paths, but no backend controller exists for any of them yet (Backend/PROGRESS.md§3/§4/§5 are unstarted, and Purchase Return is explicitly deferred) — those calls will 404 against a running backend until that work happens. This was a deliberate tradeoff the user confirmed explicitly (see the twoAskUserQuestionexchanges this session) rather than silently faking data.
1. Auth
- [~] Login screen — UI built (
app/login); not yet wired toPOST /auth/login/ token storage - [~] Forgot password — add email screen — UI built (
app/login/forgot); not yet wired to API - [~] Forgot password — verify OTP screen — UI built (
app/login/forgot/otp); not yet wired to API - [~] Forgot password — change password screen — UI built (
app/login/forgot/reset); not yet wired to API
2026-07-14: left untouched during the mock-data removal pass — there was never a mock
lib/api/auth.tsto begin with (these screens simply don't call anything yet), andBackend/PROGRESS.md§6 confirms noPOST /auth/logincontroller exists (ICurrentUser/JWT validation are wired, but there's no token issuer). Nothing to wire until that lands.
2. Master Data screens
- Items (
app/dashboard/productslist + filters,/newcreate,/[id]full edit + ETag/If-Match with 412-conflict handling + activate/deactivate) — FR-MD-01/08. Reused the pre-existing "Products" sidebar entry/stub route rather than adding a new nav item. 2026-07-14: wired to the live backend (lib/api/items.ts), no more mock data. - UOM + conversions (
app/dashboard/products/uomsflat list + create dialog; conversions edited inline on the Item detail page viaPUT /items/{itemId}/uom-conversions) — FR-MD-02/03. 2026-07-14: wired to the live backend. Note: the realItemDetailDtodoes not include aconversionsfield (onlyReorder) — the Item detail page's conversion editor now round-trips purely throughPUT /items/{itemId}/uom-conversions's own request/response, not the GET response. - Categories (
app/dashboard/products/categoriesindented tree view + create dialog with parent picker) — FR-MD-04. 2026-07-14: wired to the live backend. - Vendors (
app/dashboard/vendorslist + search/status-filter + create dialog,[id]full edit + ETag/If-Match with 412-conflict handling + activate/deactivate) — FR-MD-06. First screen this session to exercise the ETag/If-Match/412 pattern end-to-end (lib/api-client.ts'sApiResult<T>was built earlier but unused until now). 2026-07-14: wired to the live backend. - Warehouses + Bins (
app/dashboard/warehouselist + create-warehouse dialog,[id]bin list + create-bin dialog) — FR-WH-01/FR-MD-07. 2026-07-14: wired to the live backend; addedwarehousesApi.get()(the page needed it but the original stub never had it) and wrapped the realGET /warehouses/{id}/bins(IReadOnlyList<BinDto>, not paged) into a synthetic single-pagePagedResponse<Bin>so existing.items-based call sites didn't need touching. - Item reorder settings — edited inline on the Item detail page (per-warehouse point/qty rows) via
PUT /items/{itemId}/reorder— FR-MD-05. 2026-07-14: wired to the live backend.
3. Procurement screens
- Requisition (
app/dashboard/procurement/requisitionslist,/newcreate,/[id]detail + Submit) — FR-PROC-01. 2026-07-14: wired to the live backend; droppedRequisitionSummary.lineCount(the realRequisitionSummaryDtodoesn't return it) from the list screen. - RFQ + quotations + comparison view (
.../rfqslist,/newcreate with vendor multi-invite,/[id]detail: lines, comparison matrix, record-quotation form, "Create PO from vendor") — FR-PROC-02. 2026-07-14: wired to the live backend, with real contract mismatches found and fixed (see the dedicated deviation note below — this one needed real rework, not just an API-client swap). - Purchase Order (
.../purchase-orderslist,/newcreate — auto-approved, prefillable from a Requisition or an RFQ+vendor quotation via query params —/[id]detail: edit-while-open with ETag/If-Match, Cancel with reason) — FR-PROC-03..07. 2026-07-14: wired to the live backend. - [~] Purchase Return (
.../purchase-returnslist,/newcreate against a Confirmed/Closed GRN's lines) — FR-PROC-08; also reachable from aRejectedGRN line via a "Create Return" button on the GRN detail page. Still[~]: the API client now calls real endpoint paths, butBackend/PROGRESS.md§2 explicitly defers Purchase Return until GRN + Stock Core exist — these calls 404 against a live backend today.
2026-07-14 — RFQ real-backend contract mismatches found while removing mock data (
Backend/ERPCore/Dtos/Procurement/RfqDtos.cs/RfqService.csvs. the frontend's speculative types):
- The backend does not persist which vendors were invited to an RFQ (
RfqService.MapRfqnever sets a vendor list) —Rfq/RfqSummaryno longer carryvendorIds.types/procurement.tsand the[id]/list pages were updated to stop relying on it; the RFQ detail page now shows "Quoted: ..." (derived fromRfqComparison.vendorIds, i.e. vendors who have actually submitted a quotation) instead of "Invited: ...".CreateRfqRequest.RequisitionIdis[Required]server-side, not optional as the frontend assumed —app/dashboard/procurement/rfqs/new/page.tsxnow requires picking a Submitted requisition (a picker was added for the case where one wasn't passed in via?requisitionId=) before an RFQ can be created.RfqComparisonDto's real shape is{ rfqId, vendorIds, rows: [{ itemId, qty, quotes: [{ vendorId, quotationId, unitPrice, leadDays }] }] }— the frontend's assumed{ lines: [{ cells }] }naming was wrong;types/procurement.ts(RfqComparisonRow/RfqComparisonCell) and both consuming pages (RFQ detail, PO-from-RFQ prefill) were corrected to match.- The "Record a quotation" vendor picker on the RFQ detail page now offers any active vendor who hasn't already quoted (matching what
AddQuotationAsyncactually validates — vendor exists + hasn't already quoted, not "was invited") rather than a now-nonexistent "pending invited vendors" list.GET /rfqs(list) still does not exist onRfqsController(onlyGET /rfqs/{id}) —rfqsApi.list()calls it anyway per the user's "remove all mock data" instruction, so the RFQ list screen 404s until that endpoint is added. Flagged here for whoever picks upBackend/PROGRESS.md§2.- Also fixed while cross-checking DTOs:
ReqLine.requiredByis nullable (DateOnly?server-side, not a mandatory string), andRequisitionSummarynever had alineCountfield (removed from the requisitions list column and the RFQ picker label).
4. Receiving screens
- [~] GRN list (
app/dashboard/receiving/grn/page.tsx) — loading/empty/error states, links to detail - [~] GRN create (
app/dashboard/receiving/grn/new/page.tsx) — "Against PO" (picks an Approved/PartiallyReceived PO, prefills open lines) and "Direct receipt" (vendor + manual lines) modes; per-line bin, qty, unit cost, hold status, and batch (batchNo+expiryDate) or serial-number-list capture driven by the item'strackingMode - [~] GRN confirm (
app/dashboard/receiving/grn/[id]/page.tsx) — renders returnedcreatedLayers/ledgerRefs/poStatusas a confirmation panel (20-FRONTEND §4); sends a stableIdempotency-Keyper detail-page session - [~] Inspection hold release / reject — Release/Reject buttons per on-hold line, shown once the GRN is
Confirmed - Sidebar: added "Receiving" nav entry (
components/Layouts/AppSidebar.tsx) →/dashboard/receiving/grn
[~]not[x], by design: these screens are built against the documented+planned contract indocs/11-BACKEND-PHASE1.md§4, but no GRN backend exists yet (this was frontend-only work; see the deviation below).2026-07-14 — mock data removed:
lib/api/grns.tsnow calls real endpoint paths (apiRequest/apiRequestWithETagagainst/grns) instead of an in-memory store. There is still noGrnsControlleron the backend (Backend/PROGRESS.md§3 unstarted), so every call here 404s against a running backend — this was a deliberate, user-confirmed tradeoff (seeAskUserQuestionexchange this session), not an oversight. Also addedgrnsApi.getWithETag()(the assumedGET /grns/{id}didn't have an ETag-returning variant, butapp/dashboard/receiving/grn/[id]/edit/page.tsx'supdate()call needs anIf-Matchtoken to send — matching the same patternpurchase-orders.tsalready uses forget/getWithETag).Deviation —
GET /grnsandGET /grns/{id}: the API doc only specifiesPOST /grns,POST /grns/{id}/confirm,POST /grns/{id}/lines/{id}/release(no list/detail read). A list screen and a confirm/release screen both need to reload a GRN, solib/api/grns.ts(grnsApi.list/grnsApi.get) andtypes/grn.tsassume these two GET endpoints will exist once the backend is built — flag this to whoever implementsBackend/PROGRESS.md§3 sodocs/11-BACKEND-PHASE1.mdgets the corresponding doc update.
5. Stock screens
- [~] Stock hub (
app/dashboard/stock/page.tsx) — card grid linking to all 7 areas below - [~] Stock enquiry (
.../stock/enquiry) — onHand/available/onHold/inTransit/reserved, search by SKU/name + warehouse filter, links to Valuation per row - [~] Ledger view (
.../stock/ledger) — filterable by item/warehouse/date range, paginated - [~] Valuation view (
.../stock/valuation) — item+warehouse picker (also reachable via?itemId=&warehouseId=from Enquiry), FIFO layer breakdown + totals - [~] Transfer (
.../stock/transferslist,/newcreate,/[id]dispatch → receive) — cost-preserving per line (FR-STK-06) - [~] Adjustment (
.../stock/adjustmentslist,/newcreate) — reason code mandatory, auto-posts on submit (no separate confirm step, matching FR-STK-07) - [~] Count (
.../stock/countslist,/newcreate,/[id]enter counts → post) — posting creates a linked variance adjustment - [~] Reorder alerts (
.../stock/reorder-alerts) — items ≤ reorder point, one-click "Create requisition" - [~] Wastage (
.../stock/wastagereport,/newrecord) — not a documented endpoint or SRS document type: damage/theft-loss/expiry write-offs are modeled as Stock Adjustments with a loss-type reason code (FR-STK-07);lib/api/wastage.tsis a frontend-only lens overstockAdjustmentsApi+ the shared mock ledger (filters to reason codesDMG/LOSS/EXPWO, flattens adjustment lines to per-item wastage records, sums matching outbound ledger entries for cost). No new backend concept — confirmed with the user before building (asked whether "Wastage" meant this vs. a distinct document type). - Sidebar: added "Stock" nav entry (
components/Layouts/AppSidebar.tsx) →/dashboard/stock; top-bar titles mapped per route (components/Layouts/Header.tsx)
[~]not[x], by design — same posture as §4 Receiving: built frontend-only (user request), against the documented+assumed Stock Core contract (docs/11-BACKEND-PHASE1.md§5), with no real backend.2026-07-14 — mock data removed: the in-memory Stock Core (
mockStockLayers/mockStockLedger/receiveLayer/consumeFifo/etc., previously inlib/api/mock-data.ts) is gone.stock.ts/stock-transfers.ts/stock-adjustments.ts/stock-counts.ts/reason-codes.tsnow call real endpoint paths;stockApi.onHandList()was rewritten to compose real (itemsApi.list×warehousesApi.list×stockApi.onHandper pair) calls instead of iterating a mock-derived key set.wastage.tswas rewritten the same way — it now callsreasonCodesApi.list()/stockAdjustmentsApi.list()+get()/stockApi.ledger()instead of reading mock arrays directly, sowastageReasonCodeIds()andwastageApi.list()are nowasync(both call sites inapp/dashboard/stock/wastage/{page,new/page}.tsxwere updated accordingly). None of §5's backend exists yet (Backend/PROGRESS.md§4/§5 unstarted), so every one of these calls 404s against a running backend — a deliberate, user-confirmed tradeoff (seeAskUserQuestionexchange this session), not an oversight.tsc --noEmitandeslintare clean (only the pre-existinglogin/page.tsxerror and the establishedset-state-in-effectpattern remain, unchanged from before this pass).Deviations (same pattern as GRN, see §4):
GET/detail list endpoints for transfers/adjustments/counts (lib/api/stock-transfers.ts,stock-adjustments.ts,stock-counts.ts) are assumed extensions beyonddocs/11-BACKEND-PHASE1.md§5.4-5.6, which document only the transactional POSTs/PUT.stockApi.onHandList()(used by the Enquiry screen) is also not a documented endpoint — it's a frontend-only convenience; a real backend would want a proper list endpoint instead. Flag all of these to whoever implementsBackend/PROGRESS.md§4/§5 (Stock Core + stock transactions).Simplifications (mock-data limitations, not spec decisions):
StockLayerhas no per-bin field (matches the real ER model, docs/10 Part C.5 — onlyStockLedgercarriesbin_id), so Count lines don't attempt bin-level snapshotting. Transfers don't expose batch selection in the create UI (FIFO picks layers regardless of batch). Adjustment increases always cost at "last known cost" for that item/warehouse (FR-STK-07); there's no landed-cost/manual-cost override. In-transit quantity is shown for visibility at the destination warehouse only and is not subtracted a second time from the source'savailable(dispatch already reduced the source layer'sqtyRemaining) — the docs'available = onHand − onHold − reserved − inTransit(out)formula is ambiguous on this point given dispatch semantics; this was a judgment call, noted here for whoever builds the real backend to confirm or correct.
6. Validation posture (20-FRONTEND §3)
- [~] Client format/required/range checks on all forms — done for GRN create (
lib/validations/grn.ts); not yet done for other forms - Surface server
ProblemDetailsincl. domain codes; map to fields/messages —lib/error-map.ts(errorMessage/fieldErrors), used by GRN create/detail 412conflict → prompt refetch before retry —apiRequestWithETagsurfaces the ETag but no screen edits a GRN yet (GRN has no PUT), so untested in practice- No client-side gating on stock/availability/status (server-authoritative) — GRN create always submits to the server and surfaces
OVER_RECEIPT_TOLERANCE/etc. viaerror-map.tsrather than pre-blocking
7. UX states
- [~] Loading / empty / error states on every list — done for the GRN list/create/detail screens; other screens still unbuilt
- Transactional actions show server-returned side effects as confirmation — GRN confirm renders
createdLayers/ledgerRefs/poStatusfrom the response
Done
2026-07-13 — GRN screens + frontend foundation (frontend-only; no backend changes)
- Foundation:
lib/api-client.ts,lib/error-map.ts,lib/auth-token.ts,types/{common,master-data,procurement,grn}.ts,lib/api/{grns,purchase-orders,warehouses,items,vendors,uoms}.ts— scoped to what the GRN flow needs, not the full API surface. - Added the shadcn
selectprimitive (npx shadcn add select) — wasn't incomponents/ui/yet; needed for PO/vendor/warehouse/bin/item/hold-status pickers. - Screens: GRN list, GRN create (PO-based + direct receipt, batch/serial capture by
trackingMode), GRN detail (confirm + release/reject). Sidebar nav entry added. - This was explicitly frontend-only (user interrupted an initial backend+frontend plan and asked for frontend only). No GRN backend exists —
Backend/PROGRESS.md§3/§4 are unchanged. The screens are built against the contract indocs/11-BACKEND-PHASE1.md§4 plus two assumed-but-undocumented endpoints (GET /grns,GET /grns/{id}, see §4 note above); none of it is runnable end-to-end yet. - Verified:
tsc --noEmitclean for all new/edited files (one pre-existing, unrelated error remains inapp/login/page.tsx);eslintclean aside from tworeact-hooks/set-state-in-effectwarnings matching an already-existing pattern inhooks/use-mobile.ts; all three routes confirmed rendering (200, correct content, no error boundary) via SSR against the dev server.
2026-07-13 — Stock Management screens (frontend-only; no backend changes)
types/stock.ts: full DTO set for on-hand, ledger, valuation, transfers, adjustments, counts, reorder alerts (docs/11 §5).lib/api/mock-data.tsgained a real in-memory Stock Core:mockStockLayers/mockStockLedger+receiveLayer/consumeFifo/postLedgerEntry/computeOnHand/lastKnownCosthelpers, plusmockItemReordersandmockReasonCodesseed data.lib/api/grns.ts'sconfirm()was refactored to post through these helpers instead of fabricating a response, and now also accrues POqtyReceived/recomputes PO status — so GRN and Stock screens are genuinely connected this session.- New API modules:
lib/api/stock.ts(on-hand/ledger/valuation/reorder-alerts),stock-transfers.ts,stock-adjustments.ts,stock-counts.ts,reason-codes.ts— same commented-real-block + active-mock-block pattern as the GRN modules. - Screens: hub, Enquiry, Ledger, Valuation, Transfers (list/new/detail with dispatch+receive), Adjustments (list/new, auto-post), Counts (list/new/detail with enter-counts+post), Reorder Alerts. New shared badge set
components/stock/status-badges.tsx(same fixed-size red/green/yellow convention ascomponents/receiving/status-badges.tsx). Sidebar + header-title mappings added. - Same posture as the GRN pass:
[~]not[x], frontend built ahead of a nonexistent Stock Core backend, deviations/simplifications recorded in the §5 note above.tsc --noEmitandeslintclean (only the same pre-existing/established issues as the GRN pass).
2026-07-13 — Wastage screens (frontend-only; no backend changes)
lib/api/wastage.ts: no new backend concept — confirmed with the user that "Wastage" should be a focused UI lens over the just-built Stock Adjustments (damage/theft-loss/expiry write-off reason codes), not a distinct document type. FiltersmockStockAdjustmentsto loss-type reason codes, flattens to per-itemWastageRecords, and computes cost per record from matching outboundmockStockLedgerentries.- Screens:
.../stock/wastage(report — totals cards, warehouse/reason filters, per-item table) and.../stock/wastage/new(single-line record form, reason dropdown restricted to wastage-type codes, posts via the existingstockAdjustmentsApi.create). Added a "Wastage" card to the Stock hub and header-title mappings. - Verified:
tsc --noEmitclean (same pre-existinglogin/page.tsxerror only);eslintclean aside from one more instance of the already-establishedset-state-in-effectpattern.
2026-07-13 — Warehouse Management screens (frontend-only; no backend changes)
- Scope, per user selection out of the four FR-WH sub-areas offered (Warehouses & Bins / Stock Locator / Batch & Serial / Putaway): Warehouses & Bins master data only (FR-WH-01, FR-MD-07). The other three (bin-level Stock Locator, Batch/Serial tracking, Putaway) were not built — flagged here so a future pass knows they're still open, not forgotten.
lib/api/warehouses.tsgainedcreate/get/createBin(previously list/listBins only, read-only) — duplicate-code validation mirrors the realSKU_DUPLICATE-style 400 pattern used elsewhere.mock-data.tsgainedallocateWarehouseId/allocateBinId.- Screens:
app/dashboard/warehouse(list + "New Warehouse"Dialogform) andapp/dashboard/warehouse/[id](bin list + "New Bin"Dialogform) — usedcomponents/ui/dialog.tsxinstead of a full page for these two-field creates, since a whole page felt heavy for that. Sidebar "Warehouses" entry + header-title mapping added. - Housekeeping: removed two stray duplicate route folders (
app/dashboard/receiving/grn/create new GRN/,.../view GRN/) that were byte-for-byte copies of the realnew/and[id]/GRN pages under garbled folder names — almost certainly an IDE artifact from an earlier malformed file-open path, not intentional work (confirmed untracked in git before removing). Also noted, but deliberately left alone:app/warehouse/*,components/warehouse/,lib/warehouse/are pre-existing empty scaffold folders (no files at all) from initial project setup — Warehouse Management was built underapp/dashboard/warehouse/*instead so it gets the dashboard chrome (sidebar/header) for free, consistent with every other screen this session. - Verified:
tsc --noEmitclean (same pre-existinglogin/page.tsxerror only);eslintclean aside from oneexhaustive-depswarning (not an error) on[id]/page.tsx'sloadBinshelper.
2026-07-13 — Vendor (Supplier) management screens (frontend-only; no backend changes)
- Confirmed with the user first: "supplier/shop" has no distinct "Shop" entity in the SRS/docs — scoped this to the documented Vendor master (FR-MD-06, docs/11 §2.4), "supplier" being the standard ERP synonym.
lib/api/vendors.tsextended from list-only toget/create/update/updateStatus. This is the first screen to exercise the ETag/If-Match/412 pattern:mock-data.tsgained a per-vendor concurrency-token map (getVendorVersion/bumpVendorVersion/initVendorVersion, standing in for the real backend'sxmin— the publicVendortype has no version field of its own since it travels as an HTTPETagheader, not a body field) soupdate()genuinely rejects a staleIf-MatchwithCONCURRENCY_CONFLICT, matchingdocs/11 §1.6and20-FRONTEND.md §3.2.- Screens:
app/dashboard/vendors(list, search + status filter, "New Vendor" dialog) andapp/dashboard/vendors/[id](full edit form using the realapiRequestWithETag-shapedApiResult<T>, a dedicated conflict banner with "Reload before retrying" per the 412 UX rule rather than a generic toast, and an Activate/Deactivate toggle viaPATCH status, FR-MD-08 — deactivate, not hard-delete). Sidebar "Vendors" entry + header-title mapping added. - Verified:
tsc --noEmitclean (same pre-existinglogin/page.tsxerror only);eslintclean aside from the same establishedset-state-in-effectpattern used throughout this session. - Follow-up (same day):
vendorsApi.list()didn't actually paginate (always returned page 1 / all matches, same latent gap the GRN list had before its own pagination pass) — fixed to slice bypage/pageSizeproperly, added the same Previous/Next pagination controls used on the GRN and Stock list screens, and seeded 8 more sample vendors so there's something real to page through.
2026-07-13 — Procurement screens: Requisition → RFQ → PO → Purchase Return (frontend-only; no backend changes)
types/procurement.tsgrew from a GRN-support subset (PO read types only) to the full §3 DTO set: Requisition/ReqLine, Rfq/RfqLine/Quotation/RfqComparison, PO create/update/cancel request types, PurchaseReturn/PurchaseReturnLine — mirrorsdocs/11-BACKEND-PHASE1.md§3 request/response JSON exactly (nodeliveryDatefield on PO lines, since the documentedPOST /purchase-ordersexample doesn't carry one despite FR-PROC-03's prose — contract-over-prose perdocs/20-FRONTEND.md§1).lib/api/mock-data.ts: addedmockRequisitions/mockRfqs/mockQuotations/mockPurchaseReturns+ allocators, a PO concurrency-token map (getPoVersion/bumpPoVersion/initPoVersion, same out-of-band ETag pattern as vendors), andconsumeLayerByGrnLine— a new consumption path deliberately separate fromconsumeFifo: a Purchase Return disposes of the exact layer its GRN line created (oftenOnHold/Rejected, whichconsumeFifo's hold filter would otherwise skip), not "the oldest open layer for this item/warehouse". Seeded Requisition #210 to match the existingmockPurchaseOrders[0].requisitionIdso the two screens cross-reference.- New API modules:
lib/api/requisitions.ts,lib/api/rfqs.ts(create/addQuotation/comparison — comparison is computed client-side from recorded quotations),lib/api/purchase-returns.ts.lib/api/purchase-orders.tsextended from list/get-only (its original GRN-support scope) to full create/update/cancel; addedgetWithETag/isPoEditablewithout touching the existing plainget()GRN's create-flow already depends on, so no existing call site broke. - Wiring, not just new screens:
stockApi.createReorderRequisition(Stock → Reorder Alerts, built in an earlier session) previously fabricated a response with no backing record; it now pushes a real row intomockRequisitions, so a reorder-triggered requisition genuinely shows up in the new Requisitions list — same "connect the mock modules together" posture as GRN confirm → Stock Core. - Screens: Requisition (list/new/detail+Submit), RFQ (list/new with checkbox vendor multi-invite reusing the Stock Count's
Checkboxpattern/new with quotation-recording form + vendor-by-line comparison table/"Create PO from vendor"), Purchase Order (list/new — accepts?requisitionId=or?rfqId=&vendorId=to prefill lines and pricing/detail with inline edit-while-open using the vendor[id]page's ETag+412-conflict-banner pattern, plus a Cancel-with-reason flow client-disabled when any line has receipts), Purchase Return (list/new — pick a Confirmed/Closed GRN, checkbox+qty its lines; also reachable via a new "Create Return" button next toRejectedlines on the GRN detail page, matching the SRS flow diagram's Reject→Purchase Return step). Newcomponents/procurement/status-badges.tsx(same fixed-width badge convention as Receiving/Stock) andlib/validations/procurement.ts(zod-free hand-rolled, matching the GRN validation file's style, not itszoddeviation). - Sidebar: added "Procurement" nav entry between Vendors and Receiving; header title mappings added for all new routes.
- Same
[~]posture as every other module this session: built against the documented+assumed Procurement contract (docs/11-BACKEND-PHASE1.md§3), no Procurement backend exists (Backend/PROGRESS.md§2 unchanged). - Verified:
tsc --noEmitclean (same pre-existinglogin/page.tsxerror only);eslintclean aside from the same establishedset-state-in-effectpattern used throughout this session (confirmed it also fires on the pre-existinggrn/page.tsx/vendors/page.tsx/hooks/use-mobile.ts— not a regression);npm run buildcompiles successfully via Turbopack (same pre-existing login type-check failure blocks the full build, unrelated). All 12 new routes plus the GRN-detail "Create Return" link (including three query-param-prefilled variants) confirmed rendering 200 with no error boundary via SSR against the dev server.
2026-07-13 — Master Data screens: Items, UOM, Categories, Reorder settings (frontend-only; no backend changes)
types/master-data.ts:ItemListItem(the GRN/PO/Requisition/RFQ item-picker subset built in earlier sessions) is now a derived view of a new fullItemtype — SKU/name/description/category/baseUom/defaultVendor/type/trackingMode/taxClass/status plusreorder: ItemReorderSetting[](matches the documentedGET /items/{itemId}example inline) andconversions: UomConversion[](deviation: the doc's example response only showsreorder, and conversions are otherwise reachable only viaPUT /items/{itemId}/uom-conversionswith no matching GET — embedding them on the full resource, like the assumedGET /grns/GET /grns/{id}reads elsewhere in this app, lets the Item detail screen show current conversions before editing). Also addedCategory/CategoryTreeNode,CreateUomRequest,CreateCategoryRequest, and Item create/update/reorder/conversion request types (docs/11 §2.1-2.3).lib/api/mock-data.ts:mockItemschanged storage shape fromItemListItem[]to fullItem[](onlymock-data.tsandlib/api/items.tstouched it directly, confirmed by grep, so no other call site broke) —lib/api/items.ts'slist()now maps down toItemListItem, same "full record → mapped summary" pattern asmockPurchaseOrders→PurchaseOrderSummary. Added a per-item concurrency-token map (getItemVersion/bumpItemVersion/initItemVersion, same out-of-band ETag pattern as vendors/POs),mockCategoriesseeded with a 2-root/1-child tree matching the category IDs the existing sample items already reference (12 "Fasteners" under 3 "Hardware"; 20 "Power Tools"), and a UOM id allocator.- New API modules:
lib/api/categories.ts(list/tree/create—tree()builds the nested structure client-side from the flat list, since the mock has no separate tree-storage concept).lib/api/items.tsgrew from list-only (its original GRN-picker scope) to fullget/create/update/updateStatus/updateReorder/updateUomConversions;lib/api/uoms.tsgainedcreate. - Screens: Items (
app/dashboard/products— reused the pre-existing "Products" sidebar entry and stub route rather than adding a new nav item, since it was already wired to an empty placeholder page; list has search + category/tracking-mode/status filters + pagination,/newcreate,/[id]detail combining three independently-saved sections in one page — basic info with ETag/If-Match + 412-conflict banner mirroring the Vendor[id]page, a Reorder Settings row-editor postingPUT /items/{itemId}/reorder, and a UOM Conversions row-editor postingPUT /items/{itemId}/uom-conversions— matching how the API groups these as sub-resources of Item rather than separate top-level screens). UOM (app/dashboard/products/uoms— flat list + create dialog, same shape as the Warehouses list). Categories (app/dashboard/products/categories— indented recursive tree view + create dialog with a parent picker).lib/validations/master-data.tsadded (hand-rolled, matching the GRN validation file's style, not itszoddeviation). Header title mappings added for all/dashboard/products/*routes. - Housekeeping: removed
app/dashboard/vendors/view vendors/— confirmed byte-for-byte identical tovendors/[id]/page.tsxand untracked in git, same IDE-artifact pattern (malformed file-open path) as the garbled GRN duplicate folders removed in the Warehouse Management pass; noted here rather than silently dropped. Leftapp/dashboard/receiving/grn/[id]/edit/alone — it's untracked too but is a distinct, non-duplicate GRN-edit screen, not an artifact. - Same
[~]posture as every other module this session: built against the documented+assumed Master Data contract (docs/11-BACKEND-PHASE1.md§2), no Master Data backend exists (Backend/PROGRESS.md§1 unchanged). - Verified:
tsc --noEmitclean after clearing a stale.nexttype cache that still referenced the just-deletedview vendorsroute (same pre-existinglogin/page.tsxerror only remains);eslintclean aside from the same establishedset-state-in-effectpattern;npm run buildcompiles successfully via Turbopack (same pre-existing login type-check failure, unrelated). All 5 new/changed routes confirmed rendering 200 with no error boundary against the dev server (one false-alarm 500 during testing traced to an unrelated stale process already bound to port 3000, not this code — retested clean on the actual dev server port).
2026-07-14 — Mock data removed everywhere; Master Data + Procurement wired to the live backend
- User asked to integrate the (now-built) backend into the frontend, frontend-only, no mock data. Confirmed scope first: the backend only has controllers for Master Data (
Backend/PROGRESS.md§1) and Procurement minus returns (§2) — GRN (§3), Stock Core (§4), Stock Transactions (§5), and Auth (§6) have no controllers at all yet. User's explicit instruction after that was still "remove mock data in all" — so everylib/api/*.tsmodule was switched to realfetchcalls, accepting that GRN/Stock/Purchase-Return/Reason-Code calls will 404 against a live backend until that work exists (not silently left mocked). - Deleted
lib/api/mock-data.tsentirely and restored the real-fetchimplementation in every one of:categories,uoms,warehouses(added missingget(); wrapped the real non-pagedGET /warehouses/{id}/binsarray response into a syntheticPagedResponse<Bin>so existing.itemscall sites kept working),items,vendors,requisitions,purchase-orders,purchase-returns,reason-codes,grns(addedgetWithETag()for the GRN edit page'sIf-Match),stock,stock-transfers,stock-adjustments,stock-counts.wastage.tshad no prior real-mode block (it's a frontend-only lens with no documented endpoint of its own) — rewrote it to compose the now-realreasonCodesApi/stockAdjustmentsApi/stockApicalls instead of reading mock arrays directly; its two exports becameasyncas a result, and both call sites (app/dashboard/stock/wastage/{page,new/page}.tsx) were updated. - Did not blindly trust the frontend's pre-written "real implementation" comments — cross-checked every Master Data/Procurement DTO against the actual
Backend/ERPCore/Dtos/**/*.csand controllers before wiring, since those blocks were written speculatively before/alongside the real backend and had drifted in the RFQ case (see the dedicated §3 deviation note above): the backend doesn't persist RFQ-invited vendors,requisitionIdis required (not optional) to create an RFQ,RfqComparisonDtousesrows/quotes/quotationId(notlines/cells), andRequisitionSummaryDtohas nolineCount. Fixedtypes/procurement.tsand the RFQ list/detail/new pages plus the PO-from-RFQ prefill accordingly, rather than shipping types that would silently beundefinedat runtime. - Left
app/login/*untouched — no mock auth existed to remove, and there's still noPOST /auth/logincontroller to wire to. - Verified:
tsc --noEmitclean (only the pre-existing, unrelatedlogin/page.tsxresolver-typing error remains — confirmed pre-existing viagithistory, not introduced here).eslintshows the same establishedreact-hooks/set-state-in-effect/static-componentspattern as before, confirmed unchanged by spot-checking it also fires on files untouched this session (app/login/forgot/reset/page.tsx). Did not start the backend/Postgres or click through the UI live in this pass — verification was type-check + lint only; the Master Data/Procurement screens should be smoke-tested against a runningdotnet run+ Postgres before considering this "done" in practice.