- Added a new Ledgers sidebar section for statutory-format financial reports and cash/bank-account management. - Introduced dedicated GL client for API interactions, handling response envelopes and error management. - Developed report screens for Trial Balance, Balance Sheet, General Ledger, Profit & Loss, Cash Flow, Budget vs Actual, and a new Tax Report. - Implemented CSV download functionality alongside existing PDF downloads for all report screens. - Separated Cash and Bank accounts into distinct tables/endpoints, with updated create forms and unified list view. - Created a new Accounts section for Cheque Management, moving Cash/Bank Accounts from the Ledgers section. - Updated RBAC navigation to include new permissions and sub-navigation items for the added features. - Ensured compliance with GL's updated API contract, including renaming fields and adjusting response shapes. - Addressed various bugs and presentation issues, enhancing user experience across the new module.
99 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
8. General Ledger (Ledgers + Accounts sections)
Two sidebar sections (
app/dashboard/ledgers/*,app/dashboard/accounts/*), sourced entirely from the external General Ledger service via ERPCore's generic proxy (docs/12-GENERAL-LEDGER-INTEGRATION.md). Full detail, decisions, and known gaps:docs/21-GENERAL-LEDGER-FRONTEND.md.
- Ledgers: reports hub + 7 report screens (Trial Balance, Balance Sheet, General Ledger, Profit & Loss, Cash Flow, Budget vs Actual, Tax Report) — statutory-format header/table, PDF and CSV download via the same endpoint with
outputFormat=Pdf/Csv - Accounts: hub + Cash/Bank Accounts — unified list (GL's own server-side
accountTypeunion + client-side text search) + create (Cash/Bank toggle; GL account is now auto-created server-side, no picker — see 2026-07-31 (6) below). Moved here from Ledgers (2026-07-31 (5)) - Cash/Bank Accounts — edit: not built, GL has no
GET/PUTby id for either table to build it against (list shows a disabled Edit affordance with an explanatory tooltip instead of a broken form) - Accounts: Cheque Books — list/filter, create (auto-generates every leaf), drill-down to a book's own pages list, per-page details/issue/status-update in a modal
- Accounts: Received Cheques — list/filter, create, per-row details/status-update in a modal
- Sidebar "Ledgers" (7 sub-items) + new "Accounts" (3 sub-items) nav items (
components/Layouts/AppSidebar.tsx) + header title mappings (components/Layouts/Header.tsx) - Dedicated GL fetch client (
lib/api/general-ledger.ts) — GL's envelope differs from ERPCore's ownProblemDetails, so this does not reuselib/api-client.ts; now also covers Cheque Management (chequeBooksApi/chequePagesApi/receivedChequesApi)
2026-07-30 — GL's 2026-07-22 backend revision built out (large pass). Five reports restructured (Trial Balance flattened, Profit & Loss → nested named sections with a Gross Profit subtotal, Cash Flow → a real structured statement replacing the four
StatCards), a new CSV export on all seven reports (components/reports/DownloadCsvButton.tsx), a brand-new Tax Report screen (Income Tax Computation, collapsible optional-adjustments panel, payable/refundable sign-dependent final row), and Cash/Bank accounts split into two real GL endpoints (POST /bank-accountsvsPOST /cash-accounts, unifiedGET /bank-accounts?accountType=) with a two-choice create-form toggle and a Cash Account Type picker that can create a new type on the fly. Extractedcomponents/reports/{ReportSection,ReportSubtotal}.tsx— shared by Profit & Loss and Cash Flow rather than duplicating the "bordered section + bold subtotal" markup twice. Three response shapes (ProfitAndLossResponse/CashFlowResponse/TaxSummaryResponse) are inferred where GL's own reference doesn't spell out every field verbatim — flagged intypes/general-ledger.ts's own comments anddocs/21-GENERAL-LEDGER-FRONTEND.md, same posture as the original inferredBankAccountshape. Backend: migrationAddTaxReportNavSeedadds the 8th sidebar sub-item + its permission row. Verified:tsc --noEmitclean,eslintclean across every touched file,npm run buildsucceeds with all 9/dashboard/ledgers/*routes (incl./tax-report),dotnet buildclean. Not done: live smoke test against a running GL instance (still no instance available this session) — the three inferred response shapes are the highest-value thing to verify first.
2026-07-20, same-day fixes (user-reported): (1) General Ledger report was wrongly calling
GET /accountsto populate an account picker — GL documentsaccountIdon this report as a raw id, not a code-lookup value, so the picker is gone; the screen now only ever calls/reports, enteringaccountIddirectly and reading the account's code/name for display off the report's own returned rows instead. (2)ReportType/ReportOutputFormat/GlAccountTypeIdconverted from string/numeric literal unions to real TS enums. (3) Fixed a UI-only bug where a selected<Select>(Bank Account create's GL-account picker, Budget vs Actual's budget picker) displayed the raw numeric value instead of its label after selection — the underlying value sent to the server was always correct;@base-ui/react/select'sSelect.Valueneeds an explicitlabelprop per<SelectItem>(separate fromchildren) to resolve display text, which neither picker was passing. Fixed at the two call sites, not the sharedcomponents/ui/select.tsxprimitive (out of scope — other numeric-valued<Select>s elsewhere in the app likely share this latent bug; flagged indocs/21-GENERAL-LEDGER-FRONTEND.md§4 for whoever next touches one). Verified:tsc --noEmitandeslintclean.2026-07-20 (2) —
react-hooks/set-state-in-effecterrors resolved, Ledgers pages only (scope confirmed with the user — this is not an app-wide lint pass; the same error is pre-existing on ~35 other files elsewhere in the app, left untouched). All 7 report/list screens calledsetStatesynchronously as the first statement of a data-fetching effect (clearing stale results before the async call) — flagged as an error, not just a warning, by this project's current eslint config. Fixed with React's own "adjust state during render" pattern instead of an effect: each page now tracks the key it last loaded for (asOfDate/period/accountId/budgetId) in a small extra piece of state, and resets the result/error state during render when that key changes (before the effect below ever runs) rather than synchronously inside the effect. Behavior is unchanged — stale results still clear the instant a filter changes.bank-accounts/page.tsx's mount-onlyload()had a redundantsetError(null)(state already startsnull; nothing else ever recallsload()), removed outright rather than worked around. Verified:npx eslint app/dashboard/ledgersproduces zero output,tsc --noEmitclean,npm run buildsucceeds.2026-07-31 — Fixed a live runtime crash on Cash Flow: GL omits empty list/section fields entirely instead of sending
[]/{lines:[],total:0}. User-reported error clicking into the page:TypeError: Cannot read properties of undefined (reading 'map')atbucketOperatingLines→report.nonCashAdjustments.map(...), confirming the exact riskCashFlowResponsehad been flagged with since it was built (inferred shape, never verified live). Root cause: GL's serializer drops a list/section property from the JSON body altogether when there's nothing to report for the period, rather than emitting an empty array/zero-totalled object. Fixed defensively incash-flow/page.tsx(?? []onnonCashAdjustments/workingCapitalChanges, a newactivitySectionLines()helper + optional chaining forinvestingActivities/financingActivities/their.total) and, proactively, inprofit-and-loss/page.tsx(isEmptycheck and every section's.totalaccess) sinceProfitAndLossResponseshares the identical nested-section shape and was equally exposed — not yet crashed on, but certain to under the same conditions (a section with nothing posted for the period).types/general-ledger.ts'sCashFlowResponse/ProfitAndLossResponsefields updated from required to optional to match, with comments pointing back at this confirmed-live behavior. Verified:tsc --noEmit/eslintclean on all touched files;npm run build's TypeScript step fails, but only on a pre-existing, unrelatedapp/dashboard/hrm/employees/[id]/page.tsxerror present before this pass — out of scope per standing instruction to keep fixes scoped to Ledgers. Tax Report'sTaxSummaryResponseis the one remaining inferred shape not yet defensively hardened or live-verified — same class of risk, flagged for the next time that screen is touched.2026-07-31 (2) — Corrected against GL's own authoritative API reference (
04_API_Reference_And_Scenarios.md, user-supplied): Cash Flow's shape was fundamentally wrong, not just missing defensive guards; Tax Report was missing five real fields. With the actual GL API reference in hand (not inference), checked every report's response shape against it: Trial Balance, Balance Sheet, General Ledger, Profit & Loss, and Budget vs Actual all match exactly, confirming those five were built correctly. Two did not: (1)CashFlowResponsedoesn't havenetEarnings/nonCashAdjustments/workingCapitalChanges/netCashFromOperationsas flat top-level fields at all — everything genuinely nests underoperatingActivities({ profitForPeriod, nonCashAdjustments[], workingCapitalChanges[], netCashFromOperatingActivities }), andinvestingActivities/financingActivitieseach carry their own differently-named total (netCashFromInvestingActivities/netCashFromFinancingActivities), not a sharedtotal. This — not just "the field might be missing" — was the real cause of the crash fixed in the previous entry; the previous fix's defensive?? []guards were correct in spirit but pointed at the wrong (nonexistent) top-level fields, so the page would have kept rendering an empty operating-activities section forever even without crashing. Rewrotecash-flow/page.tsxandCashFlowResponse/addedCashFlowOperatingActivities/CashFlowInvestingActivities/CashFlowFinancingActivitiestotypes/general-ledger.tsto match the confirmed contract exactly; also caught thatworkingCapitalChanges[]entries usechangeAmount, notamount. (2)TaxSummaryResponse/the Tax Report'sROWStable were missingnonDeductibleExpenses,corporateIncomeTax,apitCredit,whtCredit, andquarterlyTaxPaymentsentirely — real GL-computed figures that were silently never rendered, not just a wrong guess at a field name. Added all five in their correct position in the confirmed row order (profitBeforeTax→balanceTaxPayable). Verified:tsc --noEmit/eslintclean on every touched file.2026-07-31 (3) — Balance Sheet regrouped into a proper LKAS Statement of Financial Position layout (user-reported).
BalanceSheetRow's shape was already correct (confirmed against GL's reference above), but the flat one-table rendering made a rollup total visually indistinguishable from the leaf amounts it already sums — e.g. "Cash and Bank"'s balance already includes "Petty Cash"/"Main Operating Bank Account"/"Savings Bank Account" beneath it, but every row read the same weight (onlydepth===0did any, subtle, bolding), inviting a user to double-count by adding up everything they see. Rewrotebalance-sheet/page.tsx: rows now group byaccountTypeinto ASSETS/LIABILITIES/EQUITY sections, each ending in a bold "Total {Section Name}" row (summed from that section's depth-0 rows only — a depth-0 row's balance already rolls up its own descendants, so summing depth-0 rows avoids double-counting), any row with a deeper row immediately following it is bolded as a rollup regardless of its own depth (not just the very top level), and a final "Total Liabilities and Equity" row for the standard balance-check. One quirk handled explicitly: GL's synthetic "Current Year Earnings" balancing row is documented to always carrydepth: 1even though it's a peer Equity entry, not a child of whatever real account happens to precede it — a neweffectiveDepth()helper special-cases it to 0 so it isn't mis-rendered as nested under (and excluded from the total alongside) an unrelated account. Manually verified the new grouping/summing logic against the actual numbers from the reported screenshot: Total Assets (5,880,466.50) = Total Liabilities (2,025,000.00) + Total Equity (3,855,466.50), exact match. Verified:tsc --noEmit/eslintclean.2026-07-31 (4) — Superseded by GL's own retrofit:
BalanceSheetis a genuinely different, classified response shape now, not just a re-grouping of the same flat array. GL's own API reference (user-supplied) documents a 2026-07-31 backend retrofit: the flat recursive-rollup array ({depth, lineItem, accountType, balance}, what entry (3) above regrouped client-side) is replaced entirely by a pre-classified nested object —{ asOfDate, nonCurrentAssets: {lines[], total}, currentAssets: {lines[], total}, unclassifiedAssets: {lines[], total}, totalAssets, equity: {lines[], total}, nonCurrentLiabilities: {lines[], total}, currentLiabilities: {lines[], total}, unclassifiedLiabilities: {lines[], total}, totalEquityAndLiabilities }, driven by a newaccounts.balance_sheet_classificationtag GL now maintains server-side. This means entry (3)'s client-side grouping/rollup logic (effectiveDepth,sectionTotal, thedepth-based rollup-bolding) is entirely obsolete — GL now does the Non-Current/Current classification itself, the frontend just renders the sections it's given. ReplacedBalanceSheetRowwithBalanceSheetLine/BalanceSheetSection/BalanceSheetResponseintypes/general-ledger.ts(every section marked optional, same defensive posture adopted forCashFlowResponse/ProfitAndLossResponseafter the Cash Flow crash, since this exact shape isn't live-verified against this frontend yet) and rewrotebalance-sheet/page.tsxfrom scratch to consume it. Also changed the layout to match a user-supplied reference Statement of Financial Position image (a real classified SOFP: Non-Current Assets/Current Assets each their own subtotaled block, then Equity and Liabilities the same way, ending in a Total Assets vs Total Equity-and-Liabilities check) — rather than inventing new one-off markup for this, reused the sameReportSection/ReportSubtotalshared components Profit & Loss and Cash Flow already use (oneReportSectionper GL-provided section, aReportSubtotalfor each side's grand total), keeping Balance Sheet visually and structurally consistent with the rest of the Ledgers screens rather than a bespoke table. Account codes are deliberately not shown per line (the reference template shows plain line-item names only). Verified:tsc --noEmit/eslintclean; grepped the codebase to confirm no lingering references to the removedBalanceSheetRow/flat shape.2026-07-31 (5) — New "Accounts" nav section: Cheque Management built out, Cash/Bank Accounts moved under it. New Cheque Management module (
04_API_Reference_And_Scenarios.md, Module: Cheque Management — added to GL 2026-07-30, beyond its original plan): two independent sub-areas, Cheque Books/Pages (cheques issued from this company's own supply) and Received Cheques (cheques received from others, deliberately unlinked to any cheque book). AddedPayeeType/ReceivedFromType/ChequeBookStatus/ChequePageIssueStatus/ChequePageStatusAction/ReceivedChequeStatus/ReceivedChequeStatusActionenums andChequeBook/ChequePage/ReceivedCheque(+ their create/status-update request types) totypes/general-ledger.ts, andchequeBooksApi/chequePagesApi/receivedChequesApitolib/api/general-ledger.ts.branchId/companyId/payeeId/voucherId/referenceIdare GL's own documented "loose references" (no Branch/Company/Customer/Supplier table exists in that service) — taken as plain numeric inputs, not picker dropdowns, matching GL's stated design rather than fabricating master data that doesn't exist.Cheque Books (
app/dashboard/accounts/cheque-books/{page,new,[chequeBookNo]/page}.tsx): list with a status filter, create form (bank account picker restricted toBank-type accounts only — GL's own module note says cheque books are bank-account-only, never cash-account), and a book-detail page showing every leaf (GET /cheque-books/{chequeBookNo}?expand=pages) — clicking a leaf openscomponents/accounts/ChequePageDialog.tsx, a modal with read-only details plus status-appropriate actions (Unused→ Issue/Cancel/Void;Issued→ Clear/Bounce/Cancel; terminal statuses → read-only), each action revealing only the fields that specific transition actually needs (e.g. Clear asks forclearedDate, Cancel asks forcancelReason, Bounce/Void need nothing beyond an optionalperformedBy). A modal was chosen over a second-level page for the leaf-details view (left open in the request) so working through several leaves in one book doesn't lose the list's scroll position/context each time.Received Cheques (
app/dashboard/accounts/received-cheques/{page,new/page}.tsx+components/accounts/ReceivedChequeDialog.tsx): same list-then-modal shape — status filter, create form, and a details/status-update modal (Received→ Deposit/Cancel;Deposited→ Clear/Return), Deposit asking for a bank-account picker + date, the rest needing nothing beyond an optional note.2026-07-31 (6) — Two user-reported fixes:
glAccountCoderemoved from Cash/Bank Account creation (further GL retrofit), and the three GL create-form pages widened to fill the page. (1) GL's reference now documents thatPOST /bank-accounts/POST /cash-accountsno longer acceptglAccountCode— the backing GL account (aBank/Cashroot, plus a type-header node for Cash) is always found-or-created server-side, never caller-selected. Removed the field fromCreateBankAccountRequest/CreateCashAccountRequest, deleted the "GL account"Selectand itsglAccountsApi.list()fetch frombank-accounts/new/page.tsxoutright, and dropped the check fromvalidateBankAccountForm. Typed the create response as a newCreateCashOrBankAccountResponse(glAccountnested, confirmed from GL's doc) so the success toast can surface the auto-generated GL account code. The Cash/Bank list page is untouched — GL's list endpoint still returns a flatglAccountIdper row, still resolved viaglAccountsApi.list()there. (2)bank-accounts/new,cheque-books/new, andreceived-cheques/neweach wrapped their form in amax-w-lgcard, leaving roughly half of any normal desktop screen blank. Dropped themax-w-lgcap (now full-width, matching the un-capped card convention every report page already uses) and replaced the vertical one-field-per-row stacking (plus scattered ad-hocgrid grid-cols-2pairs) with one consistentgrid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3wrapper per form. Left the two modals (ChequePageDialog/ReceivedChequeDialog) at their existing fixed width on purpose — the complaint was about full-page create forms, not dialogs. Verified:tsc --noEmit/eslintclean on every touched file;npm run build's Turbopack compile succeeds, its TypeScript step fails only on the same pre-existing, unrelatedapp/dashboard/hrm/employees/[id]/page.tsxerror noted in earlier entries.Cash/Bank Accounts moved from Ledgers to the new Accounts section (user-requested), since it's the same kind of "operational account bookkeeping" as cheques, not a statutory report —
app/dashboard/ledgers/bank-accounts/*relocated verbatim toapp/dashboard/accounts/bank-accounts/*(internal links updated, no behavior change), removed from the Ledgers hub's card grid.Backend: migration
AddAccountsNavSeedaddsNavItemaccounts(id 12) and two newSubNavItem/Permissionpairs (accounts.cheque-books,accounts.received-cheques), and re-homes the existing Cash/Bank AccountsSubNavItem/Permission(ids 15/26) from Ledgers to Accounts viaUpdateData(newCode/Href/NavItemId) rather than delete-and-recreate — keeps the same ids so any role already granted that permission doesn't silently lose it just because the section it lives under changed. Applied to the live database this session (dotnet ef database update).Fields not explicitly spelled out verbatim in GL's reference (its own numeric-id column names for
ChequeBook/ChequePage, andReceivedCheque's JSON id field) are built from the request-body field names GL does document plus this project's consistent<entity>Idconvention, flagged intypes/general-ledger.ts's comments —chequeNo/chequeBookNo(both explicitly documented as the identifying route values) are used for keys/URLs throughout instead, sidestepping the guess entirely wherever possible. Not done: live smoke test against a running GL instance — this entire module is unverified against real Cheque Management data. Verified:tsc --noEmit/eslintclean on every touched file;npm run buildcompiles successfully (Turbopack), its full-project TypeScript check still blocked only by the pre-existing, unrelatedhrm/employees/[id]error.2026-07-20 (3) — General Ledger report corrected again:
accountIddropped entirely, not just made direct-entry. The GL service's own contract changed (confirmed against its updated docs):GeneralLedger'saccountIdis now optional, and the omitted case is the real General Ledger (every postable account together, each with its own running balance, sorted byaccountCodethenentryDate) — supplyingaccountIdis a separate "Account Ledger" (single account + descendants) mode this page doesn't use. Superseding the same-day entry above: the numeric Account ID input is gone,reportsApi.generalLedger()dropped theaccountIdparameter, and the page now fetches onperiodStart/periodEndalone with sensible defaults — auto-fetching on page load like every other report screen (this also resolves the earlier-reported "no network call when landing on the page," which was the now-removed account-required gate). Result rows are grouped into per-account sections in the table (a header row whereveraccountCodechanges), matching the API's per-account running-balance reset. No frontend change was needed for the same-dayBalanceSheetresponse addition (a synthetic"Current Year Earnings"equity row) — the existing generic row renderer already displays whatever rows come back. Verified:tsc --noEmitclean,npx eslint app/dashboard/ledgers lib/api/general-ledger.tsproduces zero output,npm run buildsucceeds.
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.