- 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.
78 KiB
Frontend — PROGRESS (Phase 1: Inventory & Supply Chain)
Legend: [ ] not started · [~] in progress · [x] done
Spec: docs/20-FRONTEND.md (flows + rules) · docs/11-BACKEND-PHASE1.md (API contract)
Convention: docs/01-DOC-GUIDE.md §6. Update this file in the same commit as the code. When ticking [x], append a short note + any deviation.
0. Foundation
- Transport: same-origin Next
rewrites()proxy (next.config.ts,/api/*→BACKEND_ORIGIN, defaulthttp://localhost:5224).BACKEND_ORIGINin.env.local/.env.local.example— notNEXT_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.examplenegation.) - Typed API client rebuilt (
lib/api-client.ts, 2026-07-17) — recovered the pre-deletion version from git (0e4bcf1^) and adapted: relative/api/v1base,credentials: "include"(never present before),ApiResult/ProblemDetailsimported from@/types/commonrather than redeclared,readCsrfToken()for the eight[ValidateCsrf]auth actions.ApiError,apiRequest,apiRequestWithETag,buildQuery,ifMatch/idempotencyKeyall carried over. - Route guard (
proxy.ts— Next 16's rename ofmiddleware.ts; the old name still works but warns). Redirects/dashboard/*to/login?next=…when theerp_atcookie 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.tscaches the user profile in localStorage for the Header, because there is noGET /auth/meand 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: useszod(already a project dependency, used bylib/validations.ts/login), not hand-rolled, to stay consistent with the codebase's existing pattern rather than introduce a second validation approach. UX-only; server remains authoritative (docs/20-FRONTEND.md §3) code → messagemap (lib/error-map.ts) —errorMessage/fieldErrorsduck-type any{ code, detail, errors }-shaped rejection. Fixed 2026-07-17: generic framework codes (conflict/not_found/validation_error) were shadowing the server's specificdetail, 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 todetail; 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.tsis 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/*.tsmodule to realfetchcalls (then reverted viagit revert --no-commitat the user's request — seeBackend-adjacent history if relevant later), the user asked to go further and delete the underlying connection mechanism entirely, not just leave it unused. Deletedlib/api-client.ts(apiRequest/apiRequestWithETag/buildQuery/ApiError) andlib/auth-token.ts(bearer-token storage) as files. Follow-on fixes this required: (1)ApiResult<T>— used byitems.ts/purchase-orders.ts/vendors.tsfor their mock ETag pattern — moved intotypes/common.ts; (2)lib/error-map.tsrewritten to duck-type instead ofinstanceof ApiError; (3) three detail pages (vendors/[id],products/[id],procurement/purchase-orders/[id]) had theirerr instanceof ApiError ? err.code : (err as {code?:string})?.codeconflict-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 15lib/api/*.tsfiles, since they referenced a now-deleted module.tsc --noEmit/eslintclean (same pre-existinglogin/page.tsxerror and establishedset-state-in-effectpattern 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,requisitionIdrequired 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 sincelib/api-client.ts/lib/auth-token.tsno longer exist.
1. Auth
- Login screen — wired 2026-07-17 to
POST /auth/login. Previously itconsole.log'd the plaintext password and pushed to/dashboardunconditionally; 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 incomponents/Layouts/Header.tsx— the Header no longer hardcodesjohn52martinez@gmail.com, and "Log out" is a realPOST /auth/logoutrather 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/productslist + filters,/newcreate,/[id]full edit + ETag/If-Match with 412-conflict handling + activate/deactivate) — FR-MD-01/08. 2026-07-22:/newgained 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:/newis now Category → Subcategory (always visible, disabled with "No subcategories" when the category has none) → Brand → a checkbox-driven variant builder sourced live fromvariantCategoriesApi, 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-productuseMemo, replacing the earlier hardcoded 2-column Color×Size matrix. Submitting loopsitemsApi.create()once per combination; SKU is<CategoryCode>-<value1Code>-<value2Code>...; item name is<Brand> <Category> - <value1>/<value2>.... Added optionalbrandId/initialQtytoItem/CreateItemRequest/ItemListItem(types/master-data.ts) — deviation: neither field is in the documented Item DTO (docs/11-BACKEND-PHASE1.md§2.1);initialQtyis captured but not wired into the Stock Core ledger (informational only, no warehouse/GRN behind it). - [~] UOM + conversions (
app/dashboard/products/uomsflat list + create dialog; conversions edited inline on the Item detail page viaPUT /items/{itemId}/uom-conversions) — FR-MD-02/03 - [~] Categories (
app/dashboard/products/categoriesindented tree view + create dialog with parent picker) — FR-MD-04. 2026-07-15: added debounced search + Previous/Next pagination (categoriesApi.list()now takespage/pageSize/q/sortOrder, page size 5), matching the Vendor list's pagination pattern. - [~] Brands (
app/dashboard/products/brandslist + create/edit dialog + delete) — not a documented FR/endpoint;lib/api/brands.tstreats it as a standalone name-only master, same shape as Categories, since Item has nobrandIdin the doc. 2026-07-15: added the same debounced search + pagination as Categories;Item/CreateItemRequest/ItemListItemgainedbrandIdso the new-item variant builder (above) can attach a brand. - [~] Variant Categories (
app/dashboard/products/variantslist + 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/newvariant 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 viavariantCategoriesApi.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— sovariant_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. Newtypes/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/vendorslist + search/status-filter + create dialog,[id]full edit + ETag/If-Match with 412-conflict handling + activate/deactivate) — FR-MD-06. First screen this session to exercise the ETag/If-Match/412 pattern end-to-end (lib/api-client.ts'sApiResult<T>was built earlier but unused until now). - [~] Warehouses + Bins (
app/dashboard/warehouselist + 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/requisitionslist,/newcreate,/[id]detail + Submit) — FR-PROC-01 - [~] RFQ + quotations + comparison view (
.../rfqslist,/newcreate with vendor multi-invite,/[id]detail: lines, comparison matrix, record-quotation form, "Create PO from vendor") — FR-PROC-02 - [~] Purchase Order (
.../purchase-orderslist,/newcreate — 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 —isPoEditableis nowDraft-only,submit/removeadded tolib/api/purchase-orders.ts,saveAsDrafton the create request. See the 2026-07-20 entry. - [~] Purchase Return (
.../purchase-returnslist,/newcreate against a Confirmed/Closed GRN's lines) — FR-PROC-08; also reachable from aRejectedGRN line via a "Create Return" button on the GRN detail page
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'strackingMode. 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/newin a new tab) + refresh icon — see the 2026-07-22 entry. - [~] GRN confirm (
app/dashboard/receiving/grn/[id]/page.tsx) — renders returnedcreatedLayers/ledgerRefs/poStatusas a confirmation panel (20-FRONTEND §4); sends a stableIdempotency-Keyper detail-page session - [~] Inspection hold release / reject — Release/Reject buttons per on-hold line, shown once the GRN is
Confirmed - Sidebar: added "Receiving" nav entry (
components/Layouts/AppSidebar.tsx) →/dashboard/receiving/grn
⚠️ 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 noPUT/DELETE. The FIFO engine they describe as living inmock-data.tsis deleted — the server owns it.
[~]not[x], by design (at the time): these screens are built against the documented+planned contract indocs/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}.tscurrently 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. Eachlib/api/*.tsfile keeps the realfetch-based implementation commented out directly above the mock block — switch back by deleting the mock block, uncommenting the real block, and deletinglib/api/mock-data.tsonce the GRN backend exists.npm run dev+tsc --noEmit+eslintare clean (aside from the pre-existing, unrelatedapp/login/page.tsxresolver-typing error and the tworeact-hooks/set-state-in-effectwarnings shared withhooks/use-mobile.ts).Deviation —
GET /grnsandGET /grns/{id}: the API doc only specifiesPOST /grns,POST /grns/{id}/confirm,POST /grns/{id}/lines/{id}/release(no list/detail read). A list screen and a confirm/release screen both need to reload a GRN, solib/api/grns.ts(grnsApi.list/grnsApi.get) andtypes/grn.tsassume these two GET endpoints will exist once the backend is built — flag this to whoever implementsBackend/PROGRESS.md§3 sodocs/11-BACKEND-PHASE1.mdgets the corresponding doc update.
5. Stock screens
- [~] Stock hub (
app/dashboard/stock/page.tsx) — card grid linking to all 7 areas below - [~] Stock enquiry (
.../stock/enquiry) — onHand/available/onHold/inTransit/reserved, search by SKU/name + warehouse filter, links to Valuation per row - [~] Ledger view (
.../stock/ledger) — filterable by item/warehouse/date range, paginated - [~] Valuation view (
.../stock/valuation) — item+warehouse picker (also reachable via?itemId=&warehouseId=from Enquiry), FIFO layer breakdown + totals - [~] Transfer (
.../stock/transferslist,/newcreate,/[id]dispatch → receive) — cost-preserving per line (FR-STK-06) - [~] Adjustment (
.../stock/adjustmentslist,/newcreate) — reason code mandatory, auto-posts on submit (no separate confirm step, matching FR-STK-07) - [~] Count (
.../stock/countslist,/newcreate,/[id]enter counts → post) — posting creates a linked variance adjustment - [~] Reorder alerts (
.../stock/reorder-alerts) — items ≤ reorder point, one-click "Create requisition" - [~] Wastage (
.../stock/wastagereport,/newrecord) — not a documented endpoint or SRS document type: damage/theft-loss/expiry write-offs are modeled as Stock Adjustments with a loss-type reason code (FR-STK-07);lib/api/wastage.tsis a frontend-only lens overstockAdjustmentsApi+ the shared mock ledger (filters to reason codesDMG/LOSS/EXPWO, flattens adjustment lines to per-item wastage records, sums matching outbound ledger entries for cost). No new backend concept — confirmed with the user before building (asked whether "Wastage" meant this vs. a distinct document type). - Sidebar: added "Stock" nav entry (
components/Layouts/AppSidebar.tsx) →/dashboard/stock; top-bar titles mapped per route (components/Layouts/Header.tsx)
⚠️ 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 POqtyReceived/status) — so Enquiry/Ledger/Valuation reflect what actually happened in the session, and FIFO consumption (transfers, adjustment decreases, count variance decreases) genuinely throws aSTOCK_NEGATIVE_BLOCKED-style error when stock is insufficient.tsc --noEmitandeslintare clean across all new/changed files (only the pre-existinglogin/page.tsxerror and the establishedset-state-in-effectwarnings 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 beyonddocs/11-BACKEND-PHASE1.md§5.4-5.6, which document only the transactional POSTs/PUT.stockApi.onHandList()(used by the Enquiry screen) is also not a documented endpoint — it's a frontend-only convenience 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 implementsBackend/PROGRESS.md§4/§5 (Stock Core + stock transactions).Simplifications (mock-data limitations, not spec decisions):
StockLayerhas no per-bin field (matches the real ER model, docs/10 Part C.5 — onlyStockLedgercarriesbin_id), so Count lines don't attempt bin-level snapshotting. Transfers don't expose batch selection in the create UI (FIFO picks layers regardless of batch). Adjustment increases always cost at "last known cost" for that item/warehouse (FR-STK-07); there's no landed-cost/manual-cost override. In-transit quantity is shown for visibility at the destination warehouse only and is not subtracted a second time from the source'savailable(dispatch already reduced the source layer'sqtyRemaining) — the docs'available = onHand − onHold − reserved − inTransit(out)formula is ambiguous on this point given dispatch semantics; this was a judgment call, noted here for whoever builds the real backend to confirm or correct.
6. Validation posture (20-FRONTEND §3)
- [~] Client format/required/range checks on all forms — done for GRN create (
lib/validations/grn.ts); not yet done for other forms - Surface server
ProblemDetailsincl. domain codes; map to fields/messages —lib/error-map.ts(errorMessage/fieldErrors), used by GRN create/detail 412conflict → prompt refetch before retry —apiRequestWithETagsurfaces the ETag but no screen edits a GRN yet (GRN has no PUT), so untested in practice- No client-side gating on stock/availability/status (server-authoritative) — GRN create always submits to the server and surfaces
OVER_RECEIPT_TOLERANCE/etc. viaerror-map.tsrather than pre-blocking
7. UX states
- [~] Loading / empty / error states on every list — done for the GRN list/create/detail screens; other screens still unbuilt
- Transactional actions show server-returned side effects as confirmation — GRN confirm renders
createdLayers/ledgerRefs/poStatusfrom the response
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 --noEmitclean for every new/changed file (the only remaining project-wide tsc errors are pre-existing, unrelated syntax errors inapp/dashboard/receiving/grn/new/page.tsx— not touched this pass, not introduced by it).eslintclean 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 — noTabsprimitive exists incomponents/ui/yet). Email-lookup cross-link suggestion chip on the create dialog's email field (onBlur→employeesApi.emailLookup), never auto-linking — the human must have already seen the match beforelinkUserIdis 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 fromsalaryComponentsApi,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.findStaffByEmailon email blur), settinglinkEmployeeIdon submit.ManagedUser/CreateUserRequesttypes extended withemail/linkEmployeeIdto 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 awindow.promptfor 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 onstatus, 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 allPayrollLines for a run (only single-line lookup existed) — addedGET /payroll-runs/{id}/lines(IPayrollRunService.ListLinesAsync,PayrollRunsController.ListLines) since the Payroll Preview table genuinely needs it; documented indocs/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 ownhrReportsApimethod 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.tsxis 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 bothPayrollStatutorySettingandTaxSlab(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 backendNavItem/SubNavItemseed exists forhrm/hrm.*codes yet, so — exactly like the pre-existingprocurementbypass —hrmis added to the same frontend-onlybypassCodesset that skips thenavCodesvisibility check. This is also the AR-09 sidebar-visibility stopgap called out in02-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.tsextended to supportFormDatarequest bodies (attendance file upload, staff document upload) — previously every request body was unconditionallyJSON.stringify'd; now aFormDatabody skips both that and theContent-Typeheader (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.promptinstead 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-projecttsc --noEmitrun. Not introduced by this work and not fixed by it — confirmed viagit status/git logthat this file was untouched this session; scopedeslint/tscchecks against every HRM file individually (and the fact this is the only filetscreports) 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.1–D.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 --noEmitreports 0 errors in this module (the only 4 project errors are pre-existing HRM ones — see the note at the end of §13), andnpx next buildreports "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.tsfully 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(notdocNo— only runs get a document number); quantities useitemId/uomId/qtyPerBatchnumeric FKs (not free-text uom/qty); stages carryposX/posY, so canvas layout round-trips through the server lib/api/production-templates.ts— list/get/create/update/updateStatus with ETag +If-Matchlib/api/production-runs.ts— the full §D.3 surface (start, complete, approve, transfer, reject-intake, reject, return-leftover, cancel, quantities), every action taking anidempotencyKey; plusisStaleStageError()for the docs/21 §6 "409 on a stage-status code → refetch silently" rulelib/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'sdetail, 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. AddedDETAIL_PREFERRED_CODESso those eight codes letdetailwin and keep their map entry as a fallback
12. Screens (F2–F5) — DONE
- Template overview (
app/dashboard/production/templates/page.tsx) — realproductionTemplatesApi.listwith a 300 ms debounced search, status filter, pagination and realactiveRunCount. 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 formerhandleSave()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.positionpersists asposX/posY; realitemsApi/uomsApipickers replacedMOCK_ITEMS;/templates/newrenders 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.updateStatuspreviously 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 postsproductionRunsApi.createand navigates to the run - Run detail (
runs/[id]/page.tsx) — canvas built from the run's ownposX/posYand run edges, with per-stage intake (delivered/planned) and available-to-transfer badges, live cost pool, and astageSummarycomputed 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, plusmissingRequiredFields()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, becauseRUN_COST_CLOSED/RUN_NOT_CANCELLABLEmean offering them could only produce an error lib/production-status-colors.tskept untouched — it already matches docs/21 §3 exactly and is the single source for status colour everywhere- Deleted
lib/production-mock-runs.tsandlib/production-mock-templates.ts, includingbuildStagePlan()
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.CustomFieldFormandRunActionsare 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
stageSummaryonly, so naming stages there would mean guessing which stage holds which count — exactly what the deletedbuildStagePlan()did. Named per-stage state lives on the run detail, where the server actually returns it. - Non-terminal outputs have their
itemIdstripped 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 /statusbumps the row'sxmin, 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 fromlib/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,kindvalidated tobox/line, and invisible to the graph validator. MigrationAddTemplateCanvasAnnotations. Note the flip side, pinned by a smoke assertion: replacement is wholesale, so a client that forgets to echoannotationsback on a PUT clears them.
13. Validation posture (F6) — DONE
- Domain-code → message map complete (§11), with
detailpreferred where the server is more specific 412 CONCURRENCY_CONFLICTamber conflict banner + Reload on the builder (theapp/dashboard/vendors/[id]/page.tsxpattern)409 TEMPLATE_IN_USEedit-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 explanation422 GRAPH_*focuses the offending stage — a best-effort substring match of the server'sdetailagainst 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 explanationIdempotency-Keyper 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 throughisStaleStageError()
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:351still lists"production"inbypassCodes. Correct for now — no role is seeded with aNAV:productionpermission, so removing the bypass would hide the section from everyone. Seeding that permission is the real fix (same outstanding item asprocurement/hrm).- 4 pre-existing
tscerrors, unrelated to this work — and they blocknext buildfor the whole app:hrm/employees/[id]/page.tsx(UpdateEmployeeRequestmissinghireDate) and threehrm/settings/*pages (anApi<T>generic expecting{value}whereApiResult<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-effectlint errors across the five production files. Same rule fires 42 times repo-wide (app/dashboard/receiving/grn/page.tsxincluded); 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
StatCardtiles (Low Stock Alerts, Stock On-Hand, Pending Approval POs, Pending GRNs, Open Requisitions, Open Counts, Active RFQs), all wired to the newGET /dashboard/stats(lib/api/dashboard.ts,types/dashboard.ts) — seeBackend/PROGRESS.md's matching 2026-07-28 entry for the endpoint itself. Each tile links to its source list page. - Stock Valuation by Warehouse —
BarChartoverstats.stockValuationByWarehouse, warehouse codes resolved viawarehousesApi.list(). - Stock Movement Trend —
LineChart, 14-day In/Out totals bucketed client-side fromGET /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
#itemIdrather than the item SKU, deliberately, to avoid a hard dependency onGET /items(see the bug below). StatCard(components/ui/stat-card.tsx) fixed to use theme tokens — it previously hardcodedbg-white/text-slate-900/text-indigo-600/ring-black/5, which was invisible-on-dark once the Dark/Vibrant themes existed. Nowbg-card/text-foreground/text-primary/ring-foreground/10.- Bug found —
GET /items500s on every call (column i.SalePrice does not exist) — this is why the dashboard and the movements table avoiditemsApientirely. Root cause + fix status tracked inBackend/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.jsborderColor/backgroundColorsilently 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 --noEmitclean 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), sodotnet build/dotnet efagainst the livebin//obj/failed on file locks. Worked around by building to an isolated-ooutput 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/statsuntil 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 sendssalePrice: nullon 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(validateVariantPricesinlib/validations/master-data.ts). Each variant's price rides its ownPOST /itemsin the existing non-transactional create loop.types/master-data.ts:salePriceadded toCreateItemRequest(optional) andItem/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 haspoLineId: null(editable item/UOM,unitCostrequired) and the server receives it as a direct line. New "New item" button opens/dashboard/products/newin a new browser tab (window.open(..., "_blank", "noopener,noreferrer")— the first new-tab pattern in the app), and a refresh icon (refreshItems) re-pullsGET /items?status=Activeso the new item is selectable without reloading the in-progress GRN. ExistingvalidateLinecovers off-PO lines unchanged. - Verified:
tsc --noEmitclean. 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:isPoEditablenarrowed tostatus === "Draft"(was the three-status exclusion); addedsubmit(poId)andremove(poId).types/procurement.ts:saveAsDraft?onCreatePurchaseOrderRequest;UpdatePurchaseOrderRequestnowOmits 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 onisPoEditable— that would have shown Cancel only for Draft, so the affordances were re-split intoeditable(Draft) vscancellable(Approved/PartiallyReceived). - GRN discount/VAT/variance.
types/grn.ts:discountPct/vatPctonCreateGrnLineInput;poUnitPrice/discountPct/netUnitCost/vatPct/vatAmount/lineTotal/priceVarianceonGrnLine./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: 0–100 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.Valuerenders the raw selected value unless theSelect.Rootis given anitemsmap — the popup items unmount when closed, so their text isn't available to the trigger (confirmed in@base-ui/react'sresolveSelectedLabel, whichfindsitemsby value and only falls back to stringifying the value when none is supplied). Fixed once in the shared wrapper (components/ui/select.tsx):Selectnow walks its ownSelectItemchildren and derives theitemsarray automatically, so all ~60<SelectValue>call sites across 26 files show the selected label without any per-site change.tsc/eslintclean; verified against Base UI's label-resolution source. - Procurement sidebar submenu. The sidebar builds submenus from backend-seeded
SubNavItemrows filtered byGET /auth/me'snavCodes; only Products and Settings had children, so Purchase Orders had no sidebar section (only reachable via the Procurement hub card). Added achildrenarray 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 grantedNAV:procurementat all, so the whole Procurement branch was hidden for it; granted the parent + 4 children. Verified:/auth/mefor 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 --noEmitclean;eslintunchanged from baseline (7 pre-existingset-state-in-effecton 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.tsfromgit show 0e4bcf1^; addedcredentials: "include". - New
proxy.tsroute guard,lib/api/auth.ts,lib/auth-session.ts,types/auth.ts. Login/logout are real. - Fixed the long-standing
app/login/page.tsxresolver type error —lib/validations.tsusedz.preprocess, which widens the schema's input type tounknown, sozodResolverproduced aResolver<{email: unknown}>that could not satisfyuseForm<LoginValues>. Form fields always yield strings (RHF defaults them to""), so the null-coercion it guarded against cannot happen.tsc --noEmitis 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)
- Logout didn't log you out. AuthHex returns
user.userId: nullon login, so the Header could not supply theuserIdthatPOST /auth/logoutrequired; the call was skipped anderp_atsurvived. Fixed backend-side (userIdoptional, resolved from the token claim, cookies always cleared). Verified: cookies now[]after logout. - Generic error codes shadowed the server's message.
errorMessage()checkedCODE_MESSAGES[code]beforedetail, 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 todetail.
Contract drift reconciled (types were rewritten field-by-field against the live OpenAPI, not assumed):
itemType→stockNature;ItemTypeis now the Color/Size master.variants.ts→item-types.ts; the screen moved to/dashboard/products/item-types.RfqComparisonwas{lines[].cells[]}in this app but{rows[].quotes[]}on the server, and cells carryquotationId.Rfqhas novendorIds/createdAt;StockTransfer/StockCounthadcreatedBy/createdAtthe DTOs never returned (added server-side rather than dropping the columns);ReasonCodeContexthad"CountVariance"where the server says"Count";EnterCountsreturns the wholeCountDto, not{lines};PostCountResponse.adjustmentIdis nullable;createReorderRequisitionreturns a fullRequisition, not{qty}.remove()→updateStatus(id, "Inactive")on brands/categories/item-types, each with a Status column and Deactivate/Activate (noDELETEexists — FR-MD-08).- New
app/dashboard/products/categories/[id]for subcategories (their own resource now); newapp/dashboard/products/settingsfor Product Configuration (added the shadcnswitchprimitive via the CLI).
Features deliberately removed rather than left lying
initialQtyand 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]/editroute — the API has noPUT/DELETEfor 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 GRN —
CreateGrnLineInputhas no serial field despite FR-GRN-04 (priority M). Not collected rather than silently discarded. Flagged inBackend/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-Match → 412 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
selectprimitive (npx shadcn add select) — wasn't incomponents/ui/yet; needed for PO/vendor/warehouse/bin/item/hold-status pickers. - Screens: GRN list, GRN create (PO-based + direct receipt, batch/serial capture by
trackingMode), GRN detail (confirm + release/reject). Sidebar nav entry added. - This was explicitly frontend-only (user interrupted an initial backend+frontend plan and asked for frontend only). No GRN backend exists —
Backend/PROGRESS.md§3/§4 are unchanged. The screens are built against the contract indocs/11-BACKEND-PHASE1.md§4 plus two assumed-but-undocumented endpoints (GET /grns,GET /grns/{id}, see §4 note above); none of it is runnable end-to-end yet. - Verified:
tsc --noEmitclean for all new/edited files (one pre-existing, unrelated error remains inapp/login/page.tsx);eslintclean aside from tworeact-hooks/set-state-in-effectwarnings matching an already-existing pattern inhooks/use-mobile.ts; all three routes confirmed rendering (200, correct content, no error boundary) via SSR against the dev server.
2026-07-13 — Stock Management screens (frontend-only; no backend changes)
types/stock.ts: full DTO set for on-hand, ledger, valuation, transfers, adjustments, counts, reorder alerts (docs/11 §5).lib/api/mock-data.tsgained a real in-memory Stock Core:mockStockLayers/mockStockLedger+receiveLayer/consumeFifo/postLedgerEntry/computeOnHand/lastKnownCosthelpers, plusmockItemReordersandmockReasonCodesseed data.lib/api/grns.ts'sconfirm()was refactored to post through these helpers instead of fabricating a response, and now also accrues POqtyReceived/recomputes PO status — so GRN and Stock screens are genuinely connected this session.- New API modules:
lib/api/stock.ts(on-hand/ledger/valuation/reorder-alerts),stock-transfers.ts,stock-adjustments.ts,stock-counts.ts,reason-codes.ts— same commented-real-block + active-mock-block pattern as the GRN modules. - Screens: hub, Enquiry, Ledger, Valuation, Transfers (list/new/detail with dispatch+receive), Adjustments (list/new, auto-post), Counts (list/new/detail with enter-counts+post), Reorder Alerts. New shared badge set
components/stock/status-badges.tsx(same fixed-size red/green/yellow convention ascomponents/receiving/status-badges.tsx). Sidebar + header-title mappings added. - Same posture as the GRN pass:
[~]not[x], frontend built ahead of a nonexistent Stock Core backend, deviations/simplifications recorded in the §5 note above.tsc --noEmitandeslintclean (only the same pre-existing/established issues as the GRN pass).
2026-07-13 — Wastage screens (frontend-only; no backend changes)
lib/api/wastage.ts: no new backend concept — confirmed with the user that "Wastage" should be a focused UI lens over the just-built Stock Adjustments (damage/theft-loss/expiry write-off reason codes), not a distinct document type. FiltersmockStockAdjustmentsto loss-type reason codes, flattens to per-itemWastageRecords, and computes cost per record from matching outboundmockStockLedgerentries.- Screens:
.../stock/wastage(report — totals cards, warehouse/reason filters, per-item table) and.../stock/wastage/new(single-line record form, reason dropdown restricted to wastage-type codes, posts via the existingstockAdjustmentsApi.create). Added a "Wastage" card to the Stock hub and header-title mappings. - Verified:
tsc --noEmitclean (same pre-existinglogin/page.tsxerror only);eslintclean aside from one more instance of the already-establishedset-state-in-effectpattern.
2026-07-13 — Warehouse Management screens (frontend-only; no backend changes)
- Scope, per user selection out of the four FR-WH sub-areas offered (Warehouses & Bins / Stock Locator / Batch & Serial / Putaway): Warehouses & Bins master data only (FR-WH-01, FR-MD-07). The other three (bin-level Stock Locator, Batch/Serial tracking, Putaway) were not built — flagged here so a future pass knows they're still open, not forgotten.
lib/api/warehouses.tsgainedcreate/get/createBin(previously list/listBins only, read-only) — duplicate-code validation mirrors the realSKU_DUPLICATE-style 400 pattern used elsewhere.mock-data.tsgainedallocateWarehouseId/allocateBinId.- Screens:
app/dashboard/warehouse(list + "New Warehouse"Dialogform) andapp/dashboard/warehouse/[id](bin list + "New Bin"Dialogform) — usedcomponents/ui/dialog.tsxinstead of a full page for these two-field creates, since a whole page felt heavy for that. Sidebar "Warehouses" entry + header-title mapping added. - Housekeeping: removed two stray duplicate route folders (
app/dashboard/receiving/grn/create new GRN/,.../view GRN/) that were byte-for-byte copies of the realnew/and[id]/GRN pages under garbled folder names — almost certainly an IDE artifact from an earlier malformed file-open path, not intentional work (confirmed untracked in git before removing). Also noted, but deliberately left alone:app/warehouse/*,components/warehouse/,lib/warehouse/are pre-existing empty scaffold folders (no files at all) from initial project setup — Warehouse Management was built underapp/dashboard/warehouse/*instead so it gets the dashboard chrome (sidebar/header) for free, consistent with every other screen this session. - Verified:
tsc --noEmitclean (same pre-existinglogin/page.tsxerror only);eslintclean aside from oneexhaustive-depswarning (not an error) on[id]/page.tsx'sloadBinshelper.
2026-07-13 — Vendor (Supplier) management screens (frontend-only; no backend changes)
- Confirmed with the user first: "supplier/shop" has no distinct "Shop" entity in the SRS/docs — scoped this to the documented Vendor master (FR-MD-06, docs/11 §2.4), "supplier" being the standard ERP synonym.
lib/api/vendors.tsextended from list-only toget/create/update/updateStatus. This is the first screen to exercise the ETag/If-Match/412 pattern:mock-data.tsgained a per-vendor concurrency-token map (getVendorVersion/bumpVendorVersion/initVendorVersion, standing in for the real backend'sxmin— the publicVendortype has no version field of its own since it travels as an HTTPETagheader, not a body field) soupdate()genuinely rejects a staleIf-MatchwithCONCURRENCY_CONFLICT, matchingdocs/11 §1.6and20-FRONTEND.md §3.2.- Screens:
app/dashboard/vendors(list, search + status filter, "New Vendor" dialog) andapp/dashboard/vendors/[id](full edit form using the realapiRequestWithETag-shapedApiResult<T>, a dedicated conflict banner with "Reload before retrying" per the 412 UX rule rather than a generic toast, and an Activate/Deactivate toggle viaPATCH status, FR-MD-08 — deactivate, not hard-delete). Sidebar "Vendors" entry + header-title mapping added. - Verified:
tsc --noEmitclean (same pre-existinglogin/page.tsxerror only);eslintclean aside from the same establishedset-state-in-effectpattern used throughout this session. - Follow-up (same day):
vendorsApi.list()didn't actually paginate (always returned page 1 / all matches, same latent gap the GRN list had before its own pagination pass) — fixed to slice bypage/pageSizeproperly, added the same Previous/Next pagination controls used on the GRN and Stock list screens, and seeded 8 more sample vendors so there's something real to page through.
2026-07-13 — Procurement screens: Requisition → RFQ → PO → Purchase Return (frontend-only; no backend changes)
types/procurement.tsgrew from a GRN-support subset (PO read types only) to the full §3 DTO set: Requisition/ReqLine, Rfq/RfqLine/Quotation/RfqComparison, PO create/update/cancel request types, PurchaseReturn/PurchaseReturnLine — mirrorsdocs/11-BACKEND-PHASE1.md§3 request/response JSON exactly (nodeliveryDatefield on PO lines, since the documentedPOST /purchase-ordersexample doesn't carry one despite FR-PROC-03's prose — contract-over-prose perdocs/20-FRONTEND.md§1).lib/api/mock-data.ts: addedmockRequisitions/mockRfqs/mockQuotations/mockPurchaseReturns+ allocators, a PO concurrency-token map (getPoVersion/bumpPoVersion/initPoVersion, same out-of-band ETag pattern as vendors), andconsumeLayerByGrnLine— a new consumption path deliberately separate fromconsumeFifo: a Purchase Return disposes of the exact layer its GRN line created (oftenOnHold/Rejected, whichconsumeFifo's hold filter would otherwise skip), not "the oldest open layer for this item/warehouse". Seeded Requisition #210 to match the existingmockPurchaseOrders[0].requisitionIdso the two screens cross-reference.- New API modules:
lib/api/requisitions.ts,lib/api/rfqs.ts(create/addQuotation/comparison — comparison is computed client-side from recorded quotations),lib/api/purchase-returns.ts.lib/api/purchase-orders.tsextended from list/get-only (its original GRN-support scope) to full create/update/cancel; addedgetWithETag/isPoEditablewithout touching the existing plainget()GRN's create-flow already depends on, so no existing call site broke. - Wiring, not just new screens:
stockApi.createReorderRequisition(Stock → Reorder Alerts, built in an earlier session) previously fabricated a response with no backing record; it now pushes a real row intomockRequisitions, so a reorder-triggered requisition genuinely shows up in the new Requisitions list — same "connect the mock modules together" posture as GRN confirm → Stock Core. - Screens: Requisition (list/new/detail+Submit), RFQ (list/new with checkbox vendor multi-invite reusing the Stock Count's
Checkboxpattern/new with quotation-recording form + vendor-by-line comparison table/"Create PO from vendor"), Purchase Order (list/new — accepts?requisitionId=or?rfqId=&vendorId=to prefill lines and pricing/detail with inline edit-while-open using the vendor[id]page's ETag+412-conflict-banner pattern, plus a Cancel-with-reason flow client-disabled when any line has receipts), Purchase Return (list/new — pick a Confirmed/Closed GRN, checkbox+qty its lines; also reachable via a new "Create Return" button next toRejectedlines on the GRN detail page, matching the SRS flow diagram's Reject→Purchase Return step). Newcomponents/procurement/status-badges.tsx(same fixed-width badge convention as Receiving/Stock) andlib/validations/procurement.ts(zod-free hand-rolled, matching the GRN validation file's style, not itszoddeviation). - Sidebar: added "Procurement" nav entry between Vendors and Receiving; header title mappings added for all new routes.
- Same
[~]posture as every other module this session: built against the documented+assumed Procurement contract (docs/11-BACKEND-PHASE1.md§3), no Procurement backend 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 --noEmitclean (same pre-existinglogin/page.tsxerror only);eslintclean aside from the same establishedset-state-in-effectpattern used throughout this session (confirmed it also fires on the pre-existinggrn/page.tsx/vendors/page.tsx/hooks/use-mobile.ts— not a regression);npm run buildcompiles successfully via Turbopack (same pre-existing login type-check failure blocks the full build, unrelated). All 12 new routes plus the GRN-detail "Create Return" link (including three query-param-prefilled variants) confirmed rendering 200 with no error boundary via SSR against the dev server.
2026-07-13 — Master Data screens: Items, UOM, Categories, Reorder settings (frontend-only; no backend changes)
types/master-data.ts:ItemListItem(the GRN/PO/Requisition/RFQ item-picker subset built in earlier sessions) is now a derived view of a new fullItemtype — SKU/name/description/category/baseUom/defaultVendor/type/trackingMode/taxClass/status plusreorder: ItemReorderSetting[](matches the documentedGET /items/{itemId}example inline) andconversions: UomConversion[](deviation: the doc's example response only showsreorder, and conversions are otherwise reachable only viaPUT /items/{itemId}/uom-conversionswith no matching GET — embedding them on the full resource, like the assumedGET /grns/GET /grns/{id}reads elsewhere in this app, lets the Item detail screen show current conversions before editing). Also addedCategory/CategoryTreeNode,CreateUomRequest,CreateCategoryRequest, and Item create/update/reorder/conversion request types (docs/11 §2.1-2.3).lib/api/mock-data.ts:mockItemschanged storage shape fromItemListItem[]to fullItem[](onlymock-data.tsandlib/api/items.tstouched it directly, confirmed by grep, so no other call site broke) —lib/api/items.ts'slist()now maps down toItemListItem, same "full record → mapped summary" pattern asmockPurchaseOrders→PurchaseOrderSummary. Added a per-item concurrency-token map (getItemVersion/bumpItemVersion/initItemVersion, same out-of-band ETag pattern as vendors/POs),mockCategoriesseeded with a 2-root/1-child tree matching the category IDs the existing sample items already reference (12 "Fasteners" under 3 "Hardware"; 20 "Power Tools"), and a UOM id allocator.- New API modules:
lib/api/categories.ts(list/tree/create—tree()builds the nested structure client-side from the flat list, since the mock has no separate tree-storage concept).lib/api/items.tsgrew from list-only (its original GRN-picker scope) to fullget/create/update/updateStatus/updateReorder/updateUomConversions;lib/api/uoms.tsgainedcreate. - Screens: Items (
app/dashboard/products— reused the pre-existing "Products" sidebar entry and stub route rather than adding a new nav item, since it was already wired to an empty placeholder page; list has search + category/tracking-mode/status filters + pagination,/newcreate,/[id]detail combining three independently-saved sections in one page — basic info with ETag/If-Match + 412-conflict banner mirroring the Vendor[id]page, a Reorder Settings row-editor postingPUT /items/{itemId}/reorder, and a UOM Conversions row-editor postingPUT /items/{itemId}/uom-conversions— matching how the API groups these as sub-resources of Item rather than separate top-level screens). UOM (app/dashboard/products/uoms— flat list + create dialog, same shape as the Warehouses list). Categories (app/dashboard/products/categories— indented recursive tree view + create dialog with a parent picker).lib/validations/master-data.tsadded (hand-rolled, matching the GRN validation file's style, not itszoddeviation). Header title mappings added for all/dashboard/products/*routes. - Housekeeping: removed
app/dashboard/vendors/view vendors/— confirmed byte-for-byte identical tovendors/[id]/page.tsxand untracked in git, same IDE-artifact pattern (malformed file-open path) as the garbled GRN duplicate folders removed in the Warehouse Management pass; noted here rather than silently dropped. Leftapp/dashboard/receiving/grn/[id]/edit/alone — it's untracked too but is a distinct, non-duplicate GRN-edit screen, not an artifact. - Same
[~]posture as every other module this session: built against the documented+assumed Master Data contract (docs/11-BACKEND-PHASE1.md§2), no Master Data backend exists (Backend/PROGRESS.md§1 unchanged). - Verified:
tsc --noEmitclean after clearing a stale.nexttype cache that still referenced the just-deletedview vendorsroute (same pre-existinglogin/page.tsxerror only remains);eslintclean aside from the same establishedset-state-in-effectpattern;npm run buildcompiles successfully via Turbopack (same pre-existing login type-check failure, unrelated). All 5 new/changed routes confirmed rendering 200 with no error boundary against the dev server (one false-alarm 500 during testing traced to an unrelated stale process already bound to port 3000, not this code — retested clean on the actual dev server port).
2026-07-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 realpage/pageSize/q/sortOrderfiltering+slicing (page size 5), matching the Vendor list's existing pattern. Both list screens gained debounced search + Previous/Next controls with a "Showing X–Y of Z" caption. Follow-on fix: thenew/page.tsxitem-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 loopsitemsApi.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 optionalbrandId/initialQtytoItem/CreateItemRequest/ItemListItem— deviation, neither is in the documented Item DTO (docs/11-BACKEND-PHASE1.md§2.1);initialQtyis 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. Newtypes/master-data.ts(VariantCategory/CreateVariantCategoryRequest/UpdateVariantCategoryRequest), newlib/api/variants.ts(variantCategoriesApi),lib/validations/master-data.tsgainedvalidateVariantCategoryName. 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 realvariant_categories/variant_valuesentity rather than staying a UI-only name list feeding the item builder's free-text chips. - Verified:
tsc --noEmitclean throughout every step (same pre-existinglogin/page.tsxresolver-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) withconsole --errorschecked 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:
- 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 everyitemsApi.create()call instead of user-facing fields. NewvalidateVariantItemForm(lib/validations/master-data.ts) replaced the oldvalidateItemFormcall 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). - Base UOM removed (a separate follow-up ask) — same treatment, folded into the
DEFAULT_BASE_UOM_ID = 1constant above. - 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.
- 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-productuseMemoover however many categories are active (1, 2, or more) —buildVariantSkunow takes an array of value labels instead of two fixed color/size params. - 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. - 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>"invaluesByCategory(helpersencodeColorValue/decodeColorValue/partLabelinnew/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 readHAR-CRI(from "Crimson"), neverHAR-EF4.
- Field removal: SKU, Name, Description, Default vendor, Tax class, Item type, and Tracking mode were all removed from
- Verified:
tsc --noEmitclean after every step (same pre-existinglogin/page.tsxerror 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 — withconsole --errorsclean at every step and at least one full create-and-redirect-to-Items-list confirmed per major change.