Files
ERP-core/Frontend/PROGRESS.md
T
ImanThiyanga 7d6e597389 feat: add production templates API and documentation for manufacturing phase 2
- Implemented CRUD operations for production templates, including listing, retrieving, creating, updating, and deactivating templates.
- Introduced a new API contract for production runs, detailing the lifecycle from creation to completion, including handling of stock inputs and outputs.
- Documented the architecture, requirements, entity model, and API contract for the manufacturing phase 2, ensuring clarity on the production process and its integration with existing systems.
2026-07-31 10:22:40 +05:30

78 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. 2026-07-22: /new gained a "Fixed price / Use stock value" sale-price toggle — see the 2026-07-22 entry. 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. 2026-07-22: "Add line" now works in PO mode (off-PO items) + "New item" (opens /dashboard/products/new in a new tab) + refresh icon — see the 2026-07-22 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

HRM (Phase 2)

Spec: docs/21-FRONTEND-HRM.md (flows + rules) · docs/13-BACKEND-HRM-API.md (API contract).

8. Screens (per 21-FRONTEND-HRM.md §1)

Code complete (2026-07-23). All screens below built against the live HRM API (lib/api/{employees,hrm-masters,hrm-master-factory,attendance,leave,payroll,hr-reports}.ts, types/hrm.ts). tsc --noEmit clean for every new/changed file (the only remaining project-wide tsc errors are pre-existing, unrelated syntax errors in app/dashboard/receiving/grn/new/page.tsx — not touched this pass, not introduced by it). eslint clean on all new/changed files. Runtime browser verification not yet done — see the note at the end of this section.

  • Employees (app/dashboard/hrm/employees/{page,[id]/page}.tsx) — list + create dialog (department/designation/employment-type/work-shift selects) + detail page with an Overview/Bank Details/Documents/Salary & Loans tab switcher (plain button-based tabs — no Tabs primitive exists in components/ui/ yet). Email-lookup cross-link suggestion chip on the create dialog's email field (onBluremployeesApi.emailLookup), never auto-linking — the human must have already seen the match before linkUserId is set on submit.
  • Salary & Loans tab (added same session, follow-up to the initial pass) — Salary Structure section shows effective-dated history (employeesApi.salaryStructureHistory) + a "New Structure" dialog (effective date, basic salary, dynamic allowance/deduction lines picked from salaryComponentsApi, employeesApi.createSalaryStructure); Loans & Advances section shows the loan list (employeesApi.listLoans) + a "New Loan" dialog (Loan/Advance, principal, installment amount, count, start year/month, employeesApi.createLoan).
  • Users screen (app/dashboard/settings/users/page.tsx) extended with the same cross-link suggestion chip in reverse (employeeCrossLinkApi.findStaffByEmail on email blur), setting linkEmployeeId on submit. ManagedUser/CreateUserRequest types extended with email/linkEmployeeId to match the backend DTO changes.
  • Attendance (app/dashboard/hrm/attendance/{page,[id]/page}.tsx) — batch list + upload dialog (period start/end + file picker, .xlsx/.csv) + template download link (attendanceTemplateUrl()) + detail page rendering the Working Hours/Late/OT preview table with per-row validation-status badges, Validate/Confirm/Unlock actions gated on batch status, and Keep/Discard duplicate-resolution buttons (Draft only).
  • Leave (app/dashboard/hrm/leave/page.tsx) — single-page request list + create dialog (employee/leave-type selects, immediately submits after create) + inline Approve/Reject actions on Submitted rows (reject via a window.prompt for the reason — the simplest correct UX given the time budget; a proper dialog is a nicer follow-up, not a correctness gap).
  • Payroll (app/dashboard/hrm/payroll/{page,[id]/page,[id]/lines/[lineId]/page}.tsx) — run list + Generate dialog (year/month), detail page rendering the exact Basic/OT/Allowances/Deductions/Net preview table plus Approve/Lock/Unlock(with mandatory reason)/Generate Payslips actions gated on status, and a line-breakdown page matching the user's exact Earnings/Deductions/Employer-Contributions layout (EPF-employer/ETF explicitly labeled "informational — not deducted"). Backend gap found and fixed during this pass: there was no endpoint to list all PayrollLines for a run (only single-line lookup existed) — added GET /payroll-runs/{id}/lines (IPayrollRunService.ListLinesAsync, PayrollRunsController.ListLines) since the Payroll Preview table genuinely needs it; documented in docs/13-BACKEND-HRM-API.md §6.
  • Reports (app/dashboard/hrm/reports/page.tsx) — single page, a report-type select switches which filter fields + table are shown (Attendance Summary / Overtime / Late Arrivals / Payroll Register / Salary History / Leave Balances / Document Expiry), each calling its own hrReportsApi method on demand.
  • Settings screens (app/dashboard/hrm/settings/{page,branches,departments,designations,employment-types,work-shifts,document-types,leave-types,salary-components,statutory}/page.tsx) — a hub page linking to 9 sub-screens. components/hrm/CodeNameMasterPage.tsx is a shared generic component for the three byte-identical "code + name" masters (Branch, Designation, EmploymentType) — the other masters (Department's parent/branch selects, WorkShift's many fields + working-days bitmask, HrDocumentType's category enum, LeaveType's paid/no-pay/carry-forward flags, SalaryComponent's type enum) each have their own page since their forms genuinely differ, matching this codebase's own existing convention of one file per master rather than a forced one-size-fits-all abstraction. Statutory settings page covers both PayrollStatutorySetting and TaxSlab (list + create, no edit — both are effective-dated/append-only by design).
  • Sidebar (components/Layouts/AppSidebar.tsx) — new "HRM" section with 6 children (Employees/Attendance/Leave/Payroll/Reports/Settings). Deviation, matching existing precedent: no backend NavItem/SubNavItem seed exists for hrm/hrm.* codes yet, so — exactly like the pre-existing procurement bypass — hrm is added to the same frontend-only bypassCodes set that skips the navCodes visibility check. This is also the AR-09 sidebar-visibility stopgap called out in 02-SECURITY.md §C.8: it hides HRM from the UI for now but enforces nothing server-side. Remove the bypass once a real nav/permission seed exists.
  • lib/api-client.ts extended to support FormData request bodies (attendance file upload, staff document upload) — previously every request body was unconditionally JSON.stringify'd; now a FormData body skips both that and the Content-Type header (the browser sets its own multipart boundary).

9. Validation posture (HRM specifics, per 21-FRONTEND-HRM.md §3)

  • Client format/required checks on the Employee create dialog (code/name/hire-date/department/designation/employment-type/work-shift) and Attendance upload (period dates, file presence) — UX only, per 20-FRONTEND.md §3
  • Server-authoritative, never assumed client-side: employee-code uniqueness, email-lookup match existence, one-User-per-Employee, attendance duplicate detection (within-batch/cross-batch), attendance batch lock state, payroll generation's attendance-confirmed precondition, payroll run lock state, and every calculated amount (Gross/Net/Tax/EPF/ETF/OT/Late/No-Pay) — the client never computes or previews these independently of what the server returns; all payroll tables render server-supplied numbers verbatim.

Not yet done, flagged rather than silently skipped:

  • Runtime/browser verification. Every screen above type-checks and lints clean, and was built directly against the live API contract confirmed by the backend smoke test (71+ registered routes, correct 401 gating), but no screen has been driven in an actual browser this pass — that needs a running AuthHex session (see Backend/PROGRESS.md's sub-phase 2.1 note on why deep functional testing was deferred) to get past the login wall.
  • Leave reject uses a native window.prompt instead of a dialog — functionally correct, but a lower-fidelity UX than the rest of the app's dialog-based patterns.
  • A pre-existing, unrelated syntax error in app/dashboard/receiving/grn/new/page.tsx (unclosed JSX, last touched 2026-07-23 before this HRM pass started) blocks a clean whole-project tsc --noEmit run. Not introduced by this work and not fixed by it — confirmed via git status/git log that this file was untouched this session; scoped eslint/tsc checks against every HRM file individually (and the fact this is the only file tsc reports) confirm the HRM additions themselves are clean.

Manufacturing — Production Lines (Phase 2)

Spec: docs/21-FRONTEND-PHASE2.md (flows/screens) · contract: docs/30-BACKEND-PHASE2.md (§D.1D.3). Validation posture: docs/20-FRONTEND.md §3 — client checks are UX only.

Every production screen now runs on the real API. Both mock modules are deleted. npx tsc --noEmit reports 0 errors in this module (the only 4 project errors are pre-existing HRM ones — see the note at the end of §13), and npx next build reports "Compiled successfully" before failing type-check on those same HRM files. Nothing has been driven in a browser — see the honesty note at the end.

11. Contract layer (F1) — DONE

  • types/production.ts fully rewritten against docs/30 Part D — every request/response DTO, all six enums, and the stage-action result shapes. Replaces the frontend-only placeholder shapes entirely
  • Three contract corrections carried through: templates are keyed by code (not docNo — only runs get a document number); quantities use itemId/uomId/qtyPerBatch numeric FKs (not free-text uom/qty); stages carry posX/posY, so canvas layout round-trips through the server
  • lib/api/production-templates.ts — list/get/create/update/updateStatus with ETag + If-Match
  • lib/api/production-runs.ts — the full §D.3 surface (start, complete, approve, transfer, reject-intake, reject, return-leftover, cancel, quantities), every action taking an idempotencyKey; plus isStaleStageError() for the docs/21 §6 "409 on a stage-status code → refetch silently" rule
  • lib/error-map.ts — all 17 docs/30 §D.4 codes. Also fixed a real mechanism gap: errorMessage() let any mapped domain code override the server's detail, which would have thrown away the specifics the user needs — the graph validator names the offending stages, and the transfer/leftover guards quote the actual figures. Added DETAIL_PREFERRED_CODES so those eight codes let detail win and keep their map entry as a fallback

12. Screens (F2F5) — DONE

  • Template overview (app/dashboard/production/templates/page.tsx) — real productionTemplatesApi.list with a 300 ms debounced search, status filter, pagination and real activeRunCount. One-row-per-template canvas labelled from live data
  • Template builder (templates/[id]/page.tsx) — fully rewired. GETs the graph, holds the ETag, and the former handleSave() toast stub is now a real create/update. Node ids are the server's stage keys (tmp-<uuid> for stages drawn this session), so a PUT diffs stages in place and keeps historical runs linked; node.position persists as posX/posY; real itemsApi/uomsApi pickers replaced MOCK_ITEMS; /templates/new renders an unsaved draft seeded from the overview dialog's query params and swaps its URL on first save. Also gained a Deactivate/Activate control — productionTemplatesApi.updateStatus previously had no UI path at all
  • Run board (runs/page.tsx) — real list with debounced doc-no search, template/warehouse/status filters and pagination. Start dialog posts productionRunsApi.create and navigates to the run
  • Run detail (runs/[id]/page.tsx) — canvas built from the run's own posX/posY and run edges, with per-stage intake (delivered/planned) and available-to-transfer badges, live cost pool, and a stageSummary computed from real stage statuses
  • Stage drawer (runs/[id]/StageDrawer.tsx) — the whole of docs/21 §5: per-status bodies (Waiting → explanation · Ready → editable planned quantities + Start · InProgress → produced/scrapped per output with a required Production reason + custom fields + Complete · Done non-terminal → approve with optional partial transfer · Done terminal → receipt preview + Approve & receive + Reject for rework · Approved → transfer remainder), reject-intake from Ready or Waiting-with-deliveries, and the per-stage event timeline
  • Runtime custom-field renderer (runs/[id]/CustomFieldForm.tsx) — fieldDefs → typed inputs for all five types, plus missingRequiredFields() which mirrors the server's rule exactly, including the part that surprises people: an unchecked Checkbox counts as provided (false), so a required checkbox does not force a tick
  • Run-level actions (runs/[id]/RunActions.tsx) — Return leftover (per consumed Stock input, showing consumed/returned/weighted cost, in base UOM and capped at the unreturned remainder) and Cancel run (previewing what goes back to stock). Both hidden once the run leaves InProgress, because RUN_COST_CLOSED/RUN_NOT_CANCELLABLE mean offering them could only produce an error
  • lib/production-status-colors.ts kept untouched — it already matches docs/21 §3 exactly and is the single source for status colour everywhere
  • Deleted lib/production-mock-runs.ts and lib/production-mock-templates.ts, including buildStagePlan()

Deviations / decisions (recorded):

  • The drawer is one file, not the seven the plan sketched. Each per-status panel is ~30 lines and they all share the same lookup helpers, submit() wrapper and error handling; splitting them would mean threading that shared context through seven prop lists for no isolation benefit. CustomFieldForm and RunActions are separate, because both stand alone and neither needs the drawer's form state.
  • The board shows per-status counts, not named stages. The list projection carries stageSummary only, so naming stages there would mean guessing which stage holds which count — exactly what the deleted buildStagePlan() did. Named per-stage state lives on the run detail, where the server actually returns it.
  • Non-terminal outputs have their itemId stripped on save, not rejected. A stage that was terminal and then gained a child keeps its picked item in local state with the field no longer rendered; an issue-list message about an invisible field would be unactionable, so the builder drops it silently (FR-MFG-05 forbids it on a WIP output anyway).
  • The terminal receipt preview is computed client-side. There is no preview endpoint and every input (cost pool, produced, scrapped) is already on the page, so the drawer mirrors the server's arithmetic to show the layer before creating it. Preview only — the server recomputes.
  • A status toggle re-reads the ETag. PATCH /status bumps the row's xmin, invalidating the token the builder holds. It re-GETs and takes only the etag and status, deliberately not reloading the canvas, because a full reload there would silently discard unsaved edits.
  • "New Template" opens an unsaved draft rather than creating immediately. A template cannot exist without a valid graph — the server requires ≥1 stage and a terminal output naming a real item (FR-MFG-02/05) — so there is nothing sensible to POST from a name alone.
  • templateGraphToSaveRequest() was deleted from lib/api/production-templates.ts. It converted a fetched graph into a save payload, but the builder's canvas — not the last GET — is the source of truth for what gets saved, so it had no caller and would have drifted.

Backend additions made for these screens (all amended into docs/30 as built):

  • TemplateSummaryDto.stageNames, in flow order. The overview draws each template as a line left-to-right and needs the names for every row; without the field the client would fetch every template's full graph just to label boxes. Ordering by stage id turned out to be insertion order, which put the terminal stage first and drew lines backwards — so the server toposorts (Kahn, tie-broken by id for stability, falling back to id order if the graph is ever cyclic so a listing can't fail on bad data).
  • TemplateGraphDto.activeRunCount. The builder reads its edit-locked state straight off the graph; without it, it would need a second request to the list endpoint purely to know whether to disable itself.
  • production_templates.Annotations (jsonb) + SaveTemplateRequest.annotations. The canvas already drew grouping boxes and divider lines and the contract had nowhere to keep them, so every save would have silently discarded the user's layout notes. Round-tripped verbatim, capped at 200 entries, kind validated to box/line, and invisible to the graph validator. Migration AddTemplateCanvasAnnotations. Note the flip side, pinned by a smoke assertion: replacement is wholesale, so a client that forgets to echo annotations back on a PUT clears them.

13. Validation posture (F6) — DONE

  • Domain-code → message map complete (§11), with detail preferred where the server is more specific
  • 412 CONCURRENCY_CONFLICT amber conflict banner + Reload on the builder (the app/dashboard/vendors/[id]/page.tsx pattern)
  • 409 TEMPLATE_IN_USE edit-lock banner driven by the server. Two distinct messages: locked on load (activeRunCount > 0) versus locked while editing — the FR-MFG-06 TOCTOU, where a run starts between the GET and the PUT. The second locks the canvas rather than reloading, so nothing the user just drew disappears without an explanation
  • 422 GRAPH_* focuses the offending stage — a best-effort substring match of the server's detail against stage names, which is why the validator quotes them. Advisory by design: the full message is always in the banner too, so an ambiguous name costs a highlight, never the explanation
  • Idempotency-Key per action (useRef(crypto.randomUUID()), re-minted after each success and whenever the drawer switches stage)
  • Silent refetch on stage-status 409s — submit() in the drawer routes every action through isStaleStageError()

Not done, flagged rather than silently skipped:

  • No browser verification of any production screen, and no live end-to-end run. The contract layer is written against a backend whose every endpoint is smoke-verified, the tree type-checks and Turbopack compiles it, but nothing has been clicked. Blocked on AuthHex: its configured MySQL host (187.127.102.190:3306) is unreachable from this machine, so no token can be issued — which also means the backend smoke suite could not be re-run after this pass's backend additions.
  • components/Layouts/AppSidebar.tsx:351 still lists "production" in bypassCodes. Correct for now — no role is seeded with a NAV:production permission, so removing the bypass would hide the section from everyone. Seeding that permission is the real fix (same outstanding item as procurement/hrm).
  • 4 pre-existing tsc errors, unrelated to this work — and they block next build for the whole app: hrm/employees/[id]/page.tsx (UpdateEmployeeRequest missing hireDate) and three hrm/settings/* pages (an Api<T> generic expecting {value} where ApiResult<T> is returned). None of these files import anything added or changed by this pass, so they were left alone rather than fixed as a side effect of manufacturing work.
  • 10 react-hooks/set-state-in-effect lint errors across the five production files. Same rule fires 42 times repo-wide (app/dashboard/receiving/grn/page.tsx included); these are the load effects, the hydration-mismatch guards and the builder's stale-upstream repair. No other rule fires in this module.

Done

2026-07-28 — Dashboard overview (app/dashboard/page.tsx)

  • Replaced the component-showcase placeholder with a real stats dashboard. 7 StatCard tiles (Low Stock Alerts, Stock On-Hand, Pending Approval POs, Pending GRNs, Open Requisitions, Open Counts, Active RFQs), all wired to the new GET /dashboard/stats (lib/api/dashboard.ts, types/dashboard.ts) — see Backend/PROGRESS.md's matching 2026-07-28 entry for the endpoint itself. Each tile links to its source list page.
  • Stock Valuation by WarehouseBarChart over stats.stockValuationByWarehouse, warehouse codes resolved via warehousesApi.list().
  • Stock Movement TrendLineChart, 14-day In/Out totals bucketed client-side from GET /stock/ledger?from=...&pageSize=200. Falls back to a hardcoded sample series (SAMPLE_TREND_IN/OUT) when the real ledger has no activity in that window, so the chart isn't a flat zero line on a fresh/demo database — real data always wins when present. (An equivalent fallback was added to the Recent Stock Movements table during this pass and then explicitly removed at the user's request — that table shows only real data + an empty state.)
  • Recent Stock Movements — table of the latest 5 ledger entries; shows #itemId rather than the item SKU, deliberately, to avoid a hard dependency on GET /items (see the bug below).
  • StatCard (components/ui/stat-card.tsx) fixed to use theme tokens — it previously hardcoded bg-white/text-slate-900/text-indigo-600/ring-black/5, which was invisible-on-dark once the Dark/Vibrant themes existed. Now bg-card/text-foreground/text-primary/ring-foreground/10.
  • Bug found — GET /items 500s on every call (column i.SalePrice does not exist) — this is why the dashboard and the movements table avoid itemsApi entirely. Root cause + fix status tracked in Backend/PROGRESS.md's 2026-07-28 entry; not yet fixed as of this entry.
  • Chart color gotcha (found and fixed twice this session): passing a CSS custom property or color-mix() string (e.g. "var(--color-primary)") as a Chart.js borderColor/backgroundColor silently renders black, because a <canvas> 2D context cannot resolve CSS variables — it's not a themeable value, it's an invalid string that falls back to the default. Every chart on this page uses real static hex colors instead (#6366f1, #22c55e, #ef4444).
  • Verified: tsc --noEmit clean throughout. Runtime verification blocked for most of this session by the dev backend running under an active Visual Studio debug session — killing the process externally just triggers VS's own auto-relaunch of the stale build (observed repeatedly; confirmed via process start-time checks), so dotnet build/dotnet ef against the live bin//obj/ failed on file locks. Worked around by building to an isolated -o output directory to verify compilation without touching the locked live build; actually deploying a rebuild still requires stopping debugging inside Visual Studio itself (not just closing a console window) — this blocked full end-to-end verification of /dashboard/stats until the user did that.

2026-07-22 — Item fixed sale price + GRN off-PO items / inline create

  • Item sale-price toggle (app/dashboard/products/new/page.tsx). New "Fixed price / Use stock value" segmented toggle (default stock). Stock sends salePrice: null on every created item. Fixed reveals a top "fix value" input that pre-fills a per-variant Sale price column (priceFor(key) = pricesByKey[key] ?? fixValue, so editing a row overrides only it while the rest follow the shared value); submit is blocked until every generated variant has a price > 0 (validateVariantPrices in lib/validations/master-data.ts). Each variant's price rides its own POST /items in the existing non-transactional create loop. types/master-data.ts: salePrice added to CreateItemRequest (optional) and Item/ItemListItem (number|null).
  • GRN off-PO items + inline create (app/dashboard/receiving/grn/new/page.tsx). "Add line" is now shown in both PO and direct mode — an added PO-mode line has poLineId: null (editable item/UOM, unitCost required) and the server receives it as a direct line. New "New item" button opens /dashboard/products/new in a new browser tab (window.open(..., "_blank", "noopener,noreferrer") — the first new-tab pattern in the app), and a refresh icon (refreshItems) re-pulls GET /items?status=Active so the new item is selectable without reloading the in-progress GRN. Existing validateLine covers off-PO lines unchanged.
  • Verified: tsc --noEmit clean. Runtime browser verification (create fixed-priced variants; add an off-PO line + inline item on a PO GRN) is the next step.

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.