Files
ERP-core/Frontend/PROGRESS.md
T
ImanThiyanga f02c89b3cb feat(procurement): enhance purchase order and GRN functionalities
- Updated purchase order descriptions for clarity on draft and submission processes.
- Implemented submit and delete functionalities for draft purchase orders, allowing users to manage their orders more effectively.
- Added discount and VAT fields to GRN lines, enabling better cost tracking and reporting.
- Enhanced validation for GRN lines to ensure discount and VAT percentages are within acceptable ranges.
- Updated API to support new functionalities, including submitting and deleting purchase orders.
- Improved UI components for better user experience in managing purchase orders and GRNs.
- Documented changes in security and backend phase documentation to reflect new processes and requirements.
2026-07-21 10:08:32 +05:30

54 KiB
Raw Blame History

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

  • Transport: same-origin Next rewrites() proxy (next.config.ts, /api/*BACKEND_ORIGIN, default http://localhost:5224). BACKEND_ORIGIN in .env.local / .env.local.examplenot NEXT_PUBLIC_*; the browser never sees the backend URL. Chosen over backend CORS because it makes the API same-origin, so CORS and the Secure/SameSite=Strict session cookies stop being problems at all. (.gitignore's .env* was silently swallowing the example file — added a !.env.local.example negation.)
  • Typed API client rebuilt (lib/api-client.ts, 2026-07-17) — recovered the pre-deletion version from git (0e4bcf1^) and adapted: relative /api/v1 base, credentials: "include" (never present before), ApiResult/ProblemDetails imported from @/types/common rather than redeclared, readCsrfToken() for the eight [ValidateCsrf] auth actions. ApiError, apiRequest, apiRequestWithETag, buildQuery, ifMatch/idempotencyKey all carried over.
  • Route guard (proxy.ts — Next 16's rename of middleware.ts; the old name still works but warns). Redirects /dashboard/* to /login?next=… when the erp_at cookie is absent. Presence check only — the cookie is httpOnly and the JWT is RS256, so the edge cannot validate it; the API stays the authority.
  • Auth (lib/api/auth.ts, lib/auth-session.ts, types/auth.ts) — real login/logout. No token is stored: the session is httpOnly cookies. lib/auth-session.ts caches the user profile in localStorage for the Header, because there is no GET /auth/me and the user object only arrives in the login response. It is display data, not a credential.
  • Shared TS types mirroring API DTOs (types/{common,master-data,procurement,grn,stock,auth}.ts) — reconciled field-by-field against the live schemas 2026-07-17; see the entry below for what had drifted.
  • [~] Client validation helpers (lib/validations/grn.ts) — deviation: uses zod (already a project dependency, used by lib/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)
  • code → message map (lib/error-map.ts) — errorMessage/fieldErrors duck-type any { code, detail, errors }-shaped rejection. Fixed 2026-07-17: generic framework codes (conflict/not_found/validation_error) were shadowing the server's specific detail, so a duplicate brand showed "This action conflicts with the record's current state." instead of "A brand named 'bosch' already exists." Generic codes now lose to detail; specific domain codes still win.

⚠️ The 2026-07-15 note below is HISTORY, not current state. The fetch infrastructure was rebuilt on 2026-07-17 and lib/api/mock-data.ts is deleted — see "2026-07-17 — connected to the real API" at the bottom of this file. Its parting advice (re-verify the RFQ contract; expect to rebuild the client from scratch) was followed and proved correct.

2026-07-15 — fetch infrastructure deleted outright (not just reverted to mock). Following an earlier same-week pass that wired every lib/api/*.ts module to real fetch calls (then reverted via git revert --no-commit at the user's request — see Backend-adjacent history if relevant later), the user asked to go further and delete the underlying connection mechanism entirely, not just leave it unused. Deleted lib/api-client.ts (apiRequest/apiRequestWithETag/buildQuery/ApiError) and lib/auth-token.ts (bearer-token storage) as files. Follow-on fixes this required: (1) ApiResult<T> — used by items.ts/purchase-orders.ts/vendors.ts for their mock ETag pattern — moved into types/common.ts; (2) lib/error-map.ts rewritten to duck-type instead of instanceof ApiError; (3) three detail pages (vendors/[id], products/[id], procurement/purchase-orders/[id]) had their err instanceof ApiError ? err.code : (err as {code?:string})?.code conflict-detection simplified to the duck-typed form only. Also stripped the now-dangling commented-out "real implementation" blocks (// import { apiRequest... } from "@/lib/api-client" etc.) from all 15 lib/api/*.ts files, since they referenced a now-deleted module. tsc --noEmit/eslint clean (same pre-existing login/page.tsx error and established set-state-in-effect pattern only — confirmed unchanged by this pass).

If real backend integration is attempted again, note two things found during the reverted pass: the RFQ backend contract had drifted from this file's assumed shapes (Backend/ERPCore/Dtos/Procurement/RfqDtos.cs/RfqService.cs — no persisted invited-vendor list, requisitionId required on create, different comparison DTO field names) — re-verify against the actual backend rather than trusting old assumptions; and a typed fetch client + ETag/error-normalization layer will need to be rebuilt from scratch since lib/api-client.ts/lib/auth-token.ts no longer exist.

1. Auth

  • Login screen — wired 2026-07-17 to POST /auth/login. Previously it console.log'd the plaintext password and pushed to /dashboard unconditionally; any schema-valid input "logged in". Now: real call, session cookies, server errors surfaced, ?next= honoured (same-origin paths only — an absolute URL there would be an open redirect).
  • Route guard (proxy.ts) + real logout in components/Layouts/Header.tsx — the Header no longer hardcodes john52martinez@gmail.com, and "Log out" is a real POST /auth/logout rather than a <Link href="/login">.
  • [~] 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

2. Master Data screens

  • [~] Items (app/dashboard/products list + filters, /new create, /[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-15, rebuilt twice this session — see the two same-day entries below for the full history; current state: /new is now Category → Subcategory (always visible, disabled with "No subcategories" when the category has none) → Brand → a checkbox-driven variant builder sourced live from variantCategoriesApi, with no SKU/Name/Description/Default vendor/Tax class/Item type/Tracking mode/Base UOM fields left on the form at all (Item type/tracking mode/base UOM are now fixed constants — "Stocked"/"None"/uomId 1 — baked into the submit call, not user-facing). Checking a category (Color, Size, or any custom one) reveals its value-entry UI; Color gets a native color picker + a required name field (stored internally as "name|hex", decoded everywhere it's displayed/used for SKU) while every other category is free-text chips. A flat table (one column per checked category + SKU + Quantity) generalizes to any number of active categories via a cartesian-product useMemo, replacing the earlier hardcoded 2-column Color×Size matrix. Submitting loops itemsApi.create() once per combination; SKU is <CategoryCode>-<value1Code>-<value2Code>...; item name is <Brand> <Category> - <value1>/<value2>.... Added optional brandId/initialQty to Item/CreateItemRequest/ItemListItem (types/master-data.ts) — deviation: neither field is in the documented Item DTO (docs/11-BACKEND-PHASE1.md §2.1); initialQty is captured but not wired into the Stock Core ledger (informational only, no warehouse/GRN behind it).
  • [~] UOM + conversions (app/dashboard/products/uoms flat list + create dialog; conversions edited inline on the Item detail page via PUT /items/{itemId}/uom-conversions) — FR-MD-02/03
  • [~] Categories (app/dashboard/products/categories indented tree view + create dialog with parent picker) — FR-MD-04. 2026-07-15: added debounced search + Previous/Next pagination (categoriesApi.list() now takes page/pageSize/q/sortOrder, page size 5), matching the Vendor list's pagination pattern.
  • [~] Brands (app/dashboard/products/brands list + create/edit dialog + delete) — not a documented FR/endpoint; lib/api/brands.ts treats it as a standalone name-only master, same shape as Categories, since Item has no brandId in the doc. 2026-07-15: added the same debounced search + pagination as Categories; Item/CreateItemRequest/ItemListItem gained brandId so the new-item variant builder (above) can attach a brand.
  • [~] Variant Categories (app/dashboard/products/variants list + create/edit dialog + delete) — frontend-only, not a documented FR/endpoint. A flat, name-only master list of variant dimensions (seeded with "Color", "Size") that the Item /new variant builder now genuinely drives from (see the Items bullet above) — checking a category there shows its value-entry UI, and a "+" on that same page can create a brand-new category (e.g. "Material") inline via variantCategoriesApi.create, which then also shows up back here. Values themselves (Red, Blue, S, M...) are still not managed on this page — only entered per-Item on /new — so variant_values (the individual Red/Blue/S/M records) still isn't a real backend entity; flag to whoever owns the backend contract if that should change. New types/master-data.ts (VariantCategory/CreateVariantCategoryRequest/UpdateVariantCategoryRequest), lib/api/variants.ts (variantCategoriesApi), lib/validations/master-data.ts (validateVariantCategoryName). Sidebar gained a "Variant" entry under Products (components/Layouts/AppSidebar.tsx).
  • [~] Vendors (app/dashboard/vendors list + 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's ApiResult<T> was built earlier but unused until now).
  • [~] Warehouses + Bins (app/dashboard/warehouse list + create-warehouse dialog, [id] bin list + create-bin dialog) — FR-WH-01/FR-MD-07. Frontend-only (see note below); no ETag handling since there's no edit/delete yet, only create.
  • [~] Item reorder settings — edited inline on the Item detail page (per-warehouse point/qty rows) via PUT /items/{itemId}/reorder — FR-MD-05

3. Procurement screens

  • [~] Requisition (app/dashboard/procurement/requisitions list, /new create, /[id] detail + Submit) — FR-PROC-01
  • [~] RFQ + quotations + comparison view (.../rfqs list, /new create with vendor multi-invite, /[id] detail: lines, comparison matrix, record-quotation form, "Create PO from vendor") — FR-PROC-02
  • [~] Purchase Order (.../purchase-orders list, /new create — Save as draft or Create & submit (auto-approve), prefillable from a Requisition or an RFQ+vendor quotation via query params — /[id] detail: Draft is editable (ETag/If-Match) with Submit + Delete; a submitted/open PO is read-only with Cancel (with reason)) — FR-PROC-03..07. 2026-07-20: rewired to the draft lifecycle — isPoEditable is now Draft-only, submit/remove added to lib/api/purchase-orders.ts, saveAsDraft on the create request. See the 2026-07-20 entry.
  • [~] Purchase Return (.../purchase-returns list, /new create against a Confirmed/Closed GRN's lines) — FR-PROC-08; also reachable from a Rejected GRN line via a "Create Return" button on the GRN detail page

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, discount % / VAT % (with live after-discount/after-VAT line total + a per-row PO-price variance hint and a document total), hold status, and batch (batchNo+expiryDate) or serial-number-list capture driven by the item's trackingMode. 2026-07-20: discount/VAT/variance added — see the 2026-07-20 entry.
  • [~] GRN confirm (app/dashboard/receiving/grn/[id]/page.tsx) — renders returned createdLayers/ledgerRefs/poStatus as a confirmation panel (20-FRONTEND §4); sends a stable Idempotency-Key per 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

⚠️ The two notes below are HISTORY (2026-07-13). The GRN backend exists and these screens call it as of 2026-07-17; GET /grns + GET /grns/{id} are real, and GRN edit/delete were removed because the API has no PUT/DELETE. The FIFO engine they describe as living in mock-data.ts is deleted — the server owns it.

[~] not [x], by design (at the time): these screens are built against the documented+planned contract in docs/11-BACKEND-PHASE1.md §4, but no GRN backend existed yet (this was frontend-only work; see the deviation below).

UI-only / mock-data mode (temporary): lib/api/{grns,purchase-orders,warehouses,items,vendors,uoms}.ts currently return in-memory sample data (lib/api/mock-data.ts) instead of calling the real API, so the three screens are fully browsable/demoable (list → create against a mocked PO or as a direct receipt → detail → confirm → release/reject) without a running backend. Each lib/api/*.ts file keeps the real fetch-based implementation commented out directly above the mock block — switch back by deleting the mock block, uncommenting the real block, and deleting lib/api/mock-data.ts once the GRN backend exists. npm run dev + tsc --noEmit + eslint are clean (aside from the pre-existing, unrelated app/login/page.tsx resolver-typing error and the two react-hooks/set-state-in-effect warnings shared with hooks/use-mobile.ts).

Deviation — GET /grns and GET /grns/{id}: the API doc only specifies POST /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, so lib/api/grns.ts (grnsApi.list/grnsApi.get) and types/grn.ts assume these two GET endpoints will exist once the backend is built — flag this to whoever implements Backend/PROGRESS.md §3 so docs/11-BACKEND-PHASE1.md gets 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/transfers list, /new create, /[id] dispatch → receive) — cost-preserving per line (FR-STK-06)
  • [~] Adjustment (.../stock/adjustments list, /new create) — reason code mandatory, auto-posts on submit (no separate confirm step, matching FR-STK-07)
  • [~] Count (.../stock/counts list, /new create, /[id] enter counts → post) — posting creates a linked variance adjustment
  • [~] Reorder alerts (.../stock/reorder-alerts) — items ≤ reorder point, one-click "Create requisition"
  • [~] Wastage (.../stock/wastage report, /new record) — 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.ts is a frontend-only lens over stockAdjustmentsApi + the shared mock ledger (filters to reason codes DMG/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)

⚠️ HISTORY (2026-07-13). The Stock Core backend exists and these screens call it as of 2026-07-17; the list endpoints assumed here (GET /stock-transfers, /stock-adjustments, /stock-counts, on-hand list) were all added for real. The in-memory Stock Core described below is deleted.

[~] not [x], by design (at the time) — 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. Unlike Receiving, though, this pass wired a genuine in-memory Stock Core (lib/api/mock-data.ts: mockStockLayers, mockStockLedger, receiveLayer/consumeFifo/postLedgerEntry/computeOnHand) that GRN confirm now posts through too (previously GRN confirm fabricated a throwaway response; it now creates real layers/ledger entries and accrues PO qtyReceived/status) — so Enquiry/Ledger/Valuation reflect what actually happened in the session, and FIFO consumption (transfers, adjustment decreases, count variance decreases) genuinely throws a STOCK_NEGATIVE_BLOCKED-style error when stock is insufficient. tsc --noEmit and eslint are clean across all new/changed files (only the pre-existing login/page.tsx error and the established set-state-in-effect warnings remain, same as §4).

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 beyond docs/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 that iterates known item/warehouse pairs and calls the (documented) on-hand computation per pair; a real backend would want a proper list endpoint instead. Flag all of these to whoever implements Backend/PROGRESS.md §4/§5 (Stock Core + stock transactions).

Simplifications (mock-data limitations, not spec decisions): StockLayer has no per-bin field (matches the real ER model, docs/10 Part C.5 — only StockLedger carries bin_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's available (dispatch already reduced the source layer's qtyRemaining) — 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 ProblemDetails incl. domain codes; map to fields/messages — lib/error-map.ts (errorMessage/fieldErrors), used by GRN create/detail
  • 412 conflict → prompt refetch before retry — apiRequestWithETag surfaces 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. via error-map.ts rather 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/poStatus from the response

Done

2026-07-20 — PO draft lifecycle + GRN discount/VAT/price-override (backend + frontend, same pass)

  • PO draft lifecycle. lib/api/purchase-orders.ts: isPoEditable narrowed to status === "Draft" (was the three-status exclusion); added submit(poId) and remove(poId). types/procurement.ts: saveAsDraft? on CreatePurchaseOrderRequest; UpdatePurchaseOrderRequest now Omits it. /new: the single "Create PO" button split into Save as draft / Create & submit. /[id]: Draft shows the editable line grid + Submit + Delete draft; an issued-but-open PO (Approved/PartiallyReceived) is read-only with Cancel PO (with reason, disabled once receipts exist); closed/cancelled are terminal. The old Cancel button was gated on isPoEditable — that would have shown Cancel only for Draft, so the affordances were re-split into editable (Draft) vs cancellable (Approved/PartiallyReceived).
  • GRN discount/VAT/variance. types/grn.ts: discountPct/vatPct on CreateGrnLineInput; poUnitPrice/discountPct/netUnitCost/vatPct/vatAmount/lineTotal/priceVariance on GrnLine. /new: per-line Disc %/VAT % inputs, a client mirror of the server arithmetic (computeLine, display-only — server stays authoritative) driving a live Line total + document total, and a PO-price variance hint when the entered unit cost differs from the prefilled PO price. /[id]: renders the full cost breakdown (unit cost + variance, disc %, net cost, received value, VAT, line total) and a stock-value/VAT/document-total footer. lib/validations/grn.ts: 0100 range checks on the two percentages.
  • Deliberately not touched: the item picker already showed sku — name (the request's stated need); multi-GRN-per-PO and adding a non-PO item to a PO GRN already worked. Vendor stays PO-derived (non-selectable) for a PO-based GRN — selecting a different vendor than the PO's would be wrong.
  • Select trigger showed the id, not the label (global fix). Base UI's Select.Value renders the raw selected value unless the Select.Root is given an items map — the popup items unmount when closed, so their text isn't available to the trigger (confirmed in @base-ui/react's resolveSelectedLabel, which finds items by value and only falls back to stringifying the value when none is supplied). Fixed once in the shared wrapper (components/ui/select.tsx): Select now walks its own SelectItem children and derives the items array automatically, so all ~60 <SelectValue> call sites across 26 files show the selected label without any per-site change. tsc/eslint clean; verified against Base UI's label-resolution source.
  • Procurement sidebar submenu. The sidebar builds submenus from backend-seeded SubNavItem rows filtered by GET /auth/me's navCodes; only Products and Settings had children, so Purchase Orders had no sidebar section (only reachable via the Procurement hub card). Added a children array to the Procurement nav entry (components/Layouts/AppSidebar.tsx) — Requisitions, RFQs, Purchase Orders, Purchase Returns — matching new backend sub-nav codes. Also found the Admin role (RoleId 2) was never granted NAV:procurement at all, so the whole Procurement branch was hidden for it; granted the parent + 4 children. Verified: /auth/me for Admin now returns all five procurement codes → submenu renders. Stale PO hub-card copy ("freely editable while open") updated to the draft/submit wording.
  • Verified: tsc --noEmit clean; eslint unchanged from baseline (7 pre-existing set-state-in-effect on the PO/GRN screens before and after — 0 new issues, confirmed by stashing and re-counting). Runtime browser verification is the next step in this pass.

2026-07-17 — connected to the real API (mock-data.ts deleted)

The app now talks to ERPCore. Every lib/api/*.ts module calls the backend; lib/api/mock-data.ts is gone. This is the pass the 2026-07-15 note anticipated.

Transport + auth

  • Same-origin Next rewrites() proxy rather than backend CORS (see §0). The backend has no CORS and now needs none.
  • Rebuilt lib/api-client.ts from git show 0e4bcf1^; added credentials: "include".
  • New proxy.ts route guard, lib/api/auth.ts, lib/auth-session.ts, types/auth.ts. Login/logout are real.
  • Fixed the long-standing app/login/page.tsx resolver type errorlib/validations.ts used z.preprocess, which widens the schema's input type to unknown, so zodResolver produced a Resolver<{email: unknown}> that could not satisfy useForm<LoginValues>. Form fields always yield strings (RHF defaults them to ""), so the null-coercion it guarded against cannot happen. tsc --noEmit is now fully clean — the first time in this file's history.

Two real bugs found by driving the browser (both fixed, both invisible to unit-level checks)

  1. Logout didn't log you out. AuthHex returns user.userId: null on login, so the Header could not supply the userId that POST /auth/logout required; the call was skipped and erp_at survived. Fixed backend-side (userId optional, resolved from the token claim, cookies always cleared). Verified: cookies now [] after logout.
  2. Generic error codes shadowed the server's message. errorMessage() checked CODE_MESSAGES[code] before detail, so a duplicate brand showed "This action conflicts with the record's current state." instead of "A brand named 'bosch' already exists." Generic codes (conflict/not_found/validation_error) now lose to detail.

Contract drift reconciled (types were rewritten field-by-field against the live OpenAPI, not assumed):

  • itemTypestockNature; ItemType is now the Color/Size master. variants.tsitem-types.ts; the screen moved to /dashboard/products/item-types.
  • RfqComparison was {lines[].cells[]} in this app but {rows[].quotes[]} on the server, and cells carry quotationId. Rfq has no vendorIds/createdAt; StockTransfer/StockCount had createdBy/createdAt the DTOs never returned (added server-side rather than dropping the columns); ReasonCodeContext had "CountVariance" where the server says "Count"; EnterCounts returns the whole CountDto, not {lines}; PostCountResponse.adjustmentId is nullable; createReorderRequisition returns a full Requisition, not {qty}.
  • remove()updateStatus(id, "Inactive") on brands/categories/item-types, each with a Status column and Deactivate/Activate (no DELETE exists — FR-MD-08).
  • New app/dashboard/products/categories/[id] for subcategories (their own resource now); new app/dashboard/products/settings for Product Configuration (added the shadcn switch primitive via the CLI).

Features deliberately removed rather than left lying

  • initialQty and the builder's Quantity column — no such field on the Item contract and no initial-receipt flow; stock arrives via a GRN. It never worked under the mock either.
  • GRN edit/delete + the grn/[id]/edit route — the API has no PUT/DELETE for a GRN (FR-X-05).
  • RFQ "vendors invited" — not persisted server-side. The screens show quotations received; the quote form offers any active vendor instead of "invited but pending".
  • Serial capture on GRNCreateGrnLineInput has no serial field despite FR-GRN-04 (priority M). Not collected rather than silently discarded. Flagged in Backend/PROGRESS.md + docs/11 §4.

Fixed while rewiring: the builder hardcoded baseUomId: 1, which only worked because the mock seeded that id — against a real DB it is a 422 or, worse, the wrong unit. It now adopts the first real UOM and says so when none exists. The per-row create loop still has no transaction, but the error now reports how many items landed before the failure instead of implying none did.

Verified end-to-end in a real browser (Playwright), not just typechecked — 17/17 then 9/9 on a recheck: guard redirect + ?next= round-trip; login → cookies (erp_at httpOnly) → real user in Header; brand created via the UI; duplicate → server 409 with its own message; product-config screen reads the singleton; item-types shows the seeded Color/Size; logout clears cookies. Plus, through the page's own session: cross-FK guard 422 ("Subcategory 5 belongs to category 10, not 11"), item created with both categoryId and subCategoryId + brandId, conversions present on the detail, CONFIG_DISABLED 422 with the same item succeeding without the gated field and pre-existing items still readable, and a stale If-Match412 CONCURRENCY_CONFLICT. Test data was removed afterwards; the dev DB is back to empty masters.

The DB is near-empty and that is now visible. The mock silently supplied warehouses, UOMs, reason codes and sample POs. Screens are blank until that data is created — correct behaviour, not a regression, but a dev seed would make the app pleasant to open. lib/api/mock-data.ts's FIFO engine (receiveLayer/consumeFifo/postLedgerEntry/computeOnHand) is gone with it: the browser no longer does inventory maths — the server does.

Not yet exercised against real data: GRN confirm → Stock Enquiry/Ledger/Valuation, transfers, counts and the wastage report. They compile and are wired, but proving the FIFO chain needs a warehouse + PO + receipt seeded first. That is the highest-value next verification.

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 select primitive (npx shadcn add select) — wasn't in components/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 in docs/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 --noEmit clean for all new/edited files (one pre-existing, unrelated error remains in app/login/page.tsx); eslint clean aside from two react-hooks/set-state-in-effect warnings matching an already-existing pattern in hooks/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.ts gained a real in-memory Stock Core: mockStockLayers/mockStockLedger + receiveLayer/consumeFifo/postLedgerEntry/computeOnHand/lastKnownCost helpers, plus mockItemReorders and mockReasonCodes seed data. lib/api/grns.ts's confirm() was refactored to post through these helpers instead of fabricating a response, and now also accrues PO qtyReceived/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 as components/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 --noEmit and eslint clean (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. Filters mockStockAdjustments to loss-type reason codes, flattens to per-item WastageRecords, and computes cost per record from matching outbound mockStockLedger entries.
  • 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 existing stockAdjustmentsApi.create). Added a "Wastage" card to the Stock hub and header-title mappings.
  • Verified: tsc --noEmit clean (same pre-existing login/page.tsx error only); eslint clean aside from one more instance of the already-established set-state-in-effect pattern.

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.ts gained create/get/createBin (previously list/listBins only, read-only) — duplicate-code validation mirrors the real SKU_DUPLICATE-style 400 pattern used elsewhere. mock-data.ts gained allocateWarehouseId/allocateBinId.
  • Screens: app/dashboard/warehouse (list + "New Warehouse" Dialog form) and app/dashboard/warehouse/[id] (bin list + "New Bin" Dialog form) — used components/ui/dialog.tsx instead 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 real new/ 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 under app/dashboard/warehouse/* instead so it gets the dashboard chrome (sidebar/header) for free, consistent with every other screen this session.
  • Verified: tsc --noEmit clean (same pre-existing login/page.tsx error only); eslint clean aside from one exhaustive-deps warning (not an error) on [id]/page.tsx's loadBins helper.

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.ts extended from list-only to get/create/update/updateStatus. This is the first screen to exercise the ETag/If-Match/412 pattern: mock-data.ts gained a per-vendor concurrency-token map (getVendorVersion/bumpVendorVersion/initVendorVersion, standing in for the real backend's xmin — the public Vendor type has no version field of its own since it travels as an HTTP ETag header, not a body field) so update() genuinely rejects a stale If-Match with CONCURRENCY_CONFLICT, matching docs/11 §1.6 and 20-FRONTEND.md §3.2.
  • Screens: app/dashboard/vendors (list, search + status filter, "New Vendor" dialog) and app/dashboard/vendors/[id] (full edit form using the real apiRequestWithETag-shaped ApiResult<T>, a dedicated conflict banner with "Reload before retrying" per the 412 UX rule rather than a generic toast, and an Activate/Deactivate toggle via PATCH status, FR-MD-08 — deactivate, not hard-delete). Sidebar "Vendors" entry + header-title mapping added.
  • Verified: tsc --noEmit clean (same pre-existing login/page.tsx error only); eslint clean aside from the same established set-state-in-effect pattern 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 by page/pageSize properly, 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.ts grew 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 — mirrors docs/11-BACKEND-PHASE1.md §3 request/response JSON exactly (no deliveryDate field on PO lines, since the documented POST /purchase-orders example doesn't carry one despite FR-PROC-03's prose — contract-over-prose per docs/20-FRONTEND.md §1).
  • lib/api/mock-data.ts: added mockRequisitions/mockRfqs/mockQuotations/mockPurchaseReturns + allocators, a PO concurrency-token map (getPoVersion/bumpPoVersion/initPoVersion, same out-of-band ETag pattern as vendors), and consumeLayerByGrnLine — a new consumption path deliberately separate from consumeFifo: a Purchase Return disposes of the exact layer its GRN line created (often OnHold/Rejected, which consumeFifo's hold filter would otherwise skip), not "the oldest open layer for this item/warehouse". Seeded Requisition #210 to match the existing mockPurchaseOrders[0].requisitionId so 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.ts extended from list/get-only (its original GRN-support scope) to full create/update/cancel; added getWithETag/isPoEditable without touching the existing plain get() 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 into mockRequisitions, 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 Checkbox pattern/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 to Rejected lines on the GRN detail page, matching the SRS flow diagram's Reject→Purchase Return step). New components/procurement/status-badges.tsx (same fixed-width badge convention as Receiving/Stock) and lib/validations/procurement.ts (zod-free hand-rolled, matching the GRN validation file's style, not its zod deviation).
  • 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 existed at the time (Backend/PROGRESS.md §2 unchanged). Superseded 2026-07-17 — the Procurement backend exists and these screens now call it; several assumed shapes turned out wrong (see the 2026-07-17 entry).
  • Verified: tsc --noEmit clean (same pre-existing login/page.tsx error only); eslint clean aside from the same established set-state-in-effect pattern used throughout this session (confirmed it also fires on the pre-existing grn/page.tsx/vendors/page.tsx/hooks/use-mobile.ts — not a regression); npm run build compiles 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 full Item type — SKU/name/description/category/baseUom/defaultVendor/type/trackingMode/taxClass/status plus reorder: ItemReorderSetting[] (matches the documented GET /items/{itemId} example inline) and conversions: UomConversion[] (deviation: the doc's example response only shows reorder, and conversions are otherwise reachable only via PUT /items/{itemId}/uom-conversions with no matching GET — embedding them on the full resource, like the assumed GET /grns/GET /grns/{id} reads elsewhere in this app, lets the Item detail screen show current conversions before editing). Also added Category/CategoryTreeNode, CreateUomRequest, CreateCategoryRequest, and Item create/update/reorder/conversion request types (docs/11 §2.1-2.3).
  • lib/api/mock-data.ts: mockItems changed storage shape from ItemListItem[] to full Item[] (only mock-data.ts and lib/api/items.ts touched it directly, confirmed by grep, so no other call site broke) — lib/api/items.ts's list() now maps down to ItemListItem, same "full record → mapped summary" pattern as mockPurchaseOrdersPurchaseOrderSummary. Added a per-item concurrency-token map (getItemVersion/bumpItemVersion/initItemVersion, same out-of-band ETag pattern as vendors/POs), mockCategories seeded 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/createtree() builds the nested structure client-side from the flat list, since the mock has no separate tree-storage concept). lib/api/items.ts grew from list-only (its original GRN-picker scope) to full get/create/update/updateStatus/updateReorder/updateUomConversions; lib/api/uoms.ts gained create.
  • Screens: Items (app/dashboard/productsreused 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, /new create, /[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 posting PUT /items/{itemId}/reorder, and a UOM Conversions row-editor posting PUT /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.ts added (hand-rolled, matching the GRN validation file's style, not its zod deviation). Header title mappings added for all /dashboard/products/* routes.
  • Housekeeping: removed app/dashboard/vendors/view vendors/ — confirmed byte-for-byte identical to vendors/[id]/page.tsx and 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. Left app/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 --noEmit clean after clearing a stale .next type cache that still referenced the just-deleted view vendors route (same pre-existing login/page.tsx error only remains); eslint clean aside from the same established set-state-in-effect pattern; npm run build compiles 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-15 — Categories/Brands pagination, Item variant builder (Category→Subcategory→Brand→Color/Size), Variant Categories master (frontend-only; no backend changes)

  • Pagination: categoriesApi.list()/brandsApi.list() (lib/api/categories.ts/lib/api/brands.ts) changed from returning everything on one page to real page/pageSize/q/sortOrder filtering+slicing (page size 5), matching the Vendor list's existing pattern. Both list screens gained debounced search + Previous/Next controls with a "Showing XY of Z" caption. Follow-on fix: the new/page.tsx item-create form and anywhere else fetching the full category/brand list for a <Select> had to be updated to pass { pageSize: 200 } explicitly, since the new default of 5 would otherwise silently truncate those dropdowns.
  • Item variant builder (app/dashboard/products/new/page.tsx): added a Category (top-level, parentId === null) → Subcategory (children of the chosen category) → Brand picker ahead of the existing Base UOM/vendor/type/tracking fields, plus a "Variants" panel — free-text Color and Size chip inputs ("Add Color"/"Add Size" buttons) build a matrix table (rows = colors, columns = sizes; each cell shows an auto-generated SKU <CategoryOrSubcategoryCode>-<Color>-<Size>, e.g. FAS-RED-S, plus an editable Quantity). Submitting with variants present loops itemsApi.create() once per Color×Size cell and redirects to the Items list; submitting with no variants added falls back to the original single-item create/redirect-to-detail behavior unchanged. types/master-data.ts: added optional brandId/initialQty to Item/CreateItemRequest/ItemListItemdeviation, neither is in the documented Item DTO (docs/11-BACKEND-PHASE1.md §2.1); initialQty is captured per variant but not wired into the Stock Core ledger/GRN — informational only until a real "initial receipt" flow exists.
  • Variant Categories master (app/dashboard/products/variants/page.tsx, new sidebar entry "Variant" under Products): landed, after several discarded iterations mid-session (a Color/Size quick-select wired into the item builder, a hex-color-picker + Color/Size matrix table, a two-table Colors/Sizes toggle — all removed per follow-up user feedback), on a plain name-only CRUD list of Variant Categories (seeded "Color", "Size"; e.g. "Material" can be added), same list/create/edit/delete shape as Categories/Brands. New types/master-data.ts (VariantCategory/CreateVariantCategoryRequest/UpdateVariantCategoryRequest), new lib/api/variants.ts (variantCategoriesApi), lib/validations/master-data.ts gained validateVariantCategoryName. Not a documented FR/endpoint — flag to whoever owns the backend contract if per-category values (the actual Red/Blue/S/M list) should become a real variant_categories/variant_values entity rather than staying a UI-only name list feeding the item builder's free-text chips.
  • Verified: tsc --noEmit clean throughout every step (same pre-existing login/page.tsx resolver-typing error only); each UI change was screenshotted end-to-end via a headless Playwright session against the dev server (pagination Prev/Next + counts, category/subcategory/brand selection, color/size chip entry → matrix table → generated SKUs, submit → created items appearing in the Items list, variant-category create/edit/delete) with console --errors checked clean at every step.

2026-07-15 (continued) — New Item form pared down + variant builder generalized to dynamic Variant Categories (frontend-only; no backend changes)

  • Same-day follow-up, superseding parts of the entry above — the Item variant builder went through several more rounds of user-driven refinement after the initial Color/Size-hardcoded version landed:
    1. Field removal: SKU, Name, Description, Default vendor, Tax class, Item type, and Tracking mode were all removed from /new's UI on request. Since there's no manual SKU/Name anymore, the form now always operates in variant mode (the old "no variants → fall back to single-item create" branch is gone) — Item type/Tracking mode/Base UOM became fixed constants ("Stocked"/"None"/uomId 1) baked into every itemsApi.create() call instead of user-facing fields. New validateVariantItemForm (lib/validations/master-data.ts) replaced the old validateItemForm call on this page (that function is still used, unchanged, by the Item edit page at /[id], which keeps its SKU/Name fields — this removal is /new-only).
    2. Base UOM removed (a separate follow-up ask) — same treatment, folded into the DEFAULT_BASE_UOM_ID = 1 constant above.
    3. Subcategory made unconditional — previously hidden entirely when the selected category had no children; now always rendered, just disabled with a "No subcategories" placeholder in that case.
    4. Color/Size hardcoding replaced with dynamic Variant Categories: the builder now fetches variantCategoriesApi.list() and renders one checkbox per category (Color, Size, or any custom one); checking a box reveals its value-entry section instead of two fixed Color/Size blocks. The variant table generalized from the old 2-column Color×Size matrix to a flat table with one column per checked category + SKU + Quantity, built via a generic cartesian-product useMemo over however many categories are active (1, 2, or more) — buildVariantSku now takes an array of value labels instead of two fixed color/size params.
    5. Inline "add variant category": a "+" icon button next to the checkboxes opens an inline name field that calls variantCategoriesApi.create() directly from /new, appends the result to the in-memory list, and auto-checks it — so a brand-new dimension (e.g. "Material") can be added without leaving the Item form, and it also then appears on /dashboard/products/variants.
    6. Color gets a real color picker: for whichever checked category is literally named "Color" (case-insensitive), the free-text input is replaced with a native <input type="color"> swatch picker plus a required "Color name" text field — picking red alone isn't enough, a name is mandatory too. The pair is encoded as a single string "<name>|<hex>" in valuesByCategory (helpers encodeColorValue/decodeColorValue/partLabel in new/page.tsx) so the existing generic value-list plumbing didn't need a parallel data shape; every place that displays or SKU-generates from a color value decodes it back to just the name (the hex only ever drives the swatch dot next to chips and table cells) — so SKUs read HAR-CRI (from "Crimson"), never HAR-EF4.
  • Verified: tsc --noEmit clean after every step (same pre-existing login/page.tsx error only, confirmed unchanged throughout). Each change was driven end-to-end through a headless Playwright session against the dev server and screenshotted — field removal, subcategory always-visible + disabled state, checkbox show/hide of category builders, cartesian flat table with 2+ active categories, inline category creation followed by its builder appearing immediately, and the color picker + name → chip swatch → table swatch → final SKU/item name chain — with console --errors clean at every step and at least one full create-and-redirect-to-Items-list confirmed per major change.