- Implemented `GET /dashboard/stats` in `DashboardController` to provide aggregate counts for stock, GRN, and procurement. - Created `DashboardStatsDto` and `WarehouseValuationDto` to structure the response data. - Developed `DashboardService` to fetch and compute necessary statistics from the database. - Added `IDashboardService` interface for service abstraction. - Introduced API client methods in `dashboard.ts` for frontend consumption of the new endpoint. - Defined TypeScript types for dashboard data in `dashboard.ts` to ensure type safety in the frontend. - Updated UI components in the frontend to reflect changes in the dashboard, including styling adjustments and removal of unused icons.
66 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.
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.