- Added loginerps.png.bak (1134 KB) as a backup for the login image. - Added whithoutbackground.png.bak (3455 KB) as a backup for the image without background.
26 KiB
20 · FRONTEND — Phase 1 (Inventory & Supply Chain)
Authoritative for: frontend user-flows, the rules to follow the existing architecture, and the validation posture. Navigation: you arrived from
00-CORE.md. The API contract this UI consumes is in11-BACKEND-PHASE1.md; business rules are in10-BACKEND-PHASE1.md. Record work inFrontend/PROGRESS.md. Note: the Frontend project is already initialized (Next.js, App Router, TypeScript). This document does not scaffold structure — it governs how to build within what exists.
1. Stack & architecture rules
| Concern | Rule |
|---|---|
| Framework | Next.js App Router + TypeScript. Work within the existing structure; do not restructure or introduce a competing router. |
| Styling / UI | Tailwind CSS for all styling — no CSS modules, styled-components, or inline style objects. UI primitives come from shadcn/ui (components/ui/, built on @base-ui/react + class-variance-authority); use/extend existing components there before adding a new one, and generate new primitives via the shadcn CLI to keep the pattern consistent. |
| State / forms | Plain React hooks (useState, useReducer, custom hooks). No form/state library. |
| Validation | Dependency-free (hand-rolled helpers). See §3. |
| API access | A single typed fetch client (lib/api-client.ts) against a same-origin /api/v1; all calls go through it. No scattered fetch() in components. |
| Transport | The API is reached through a Next rewrites() proxy (next.config.ts: /api/* → BACKEND_ORIGIN, default http://localhost:5224). Same-origin by construction, so there is no CORS on the backend and none is needed. BACKEND_ORIGIN is server-side only — deliberately not NEXT_PUBLIC_*, since the browser only ever talks to the Next server. |
| Types | TS types in types/ mirror the API DTOs in 11-BACKEND-PHASE1.md. When the contract changes, update these first. |
| Auth | httpOnly cookie session — there is no bearer token to store. POST /auth/login sets erp_at/erp_rt/XSRF-TOKEN; the client just sends credentials: "include". JS cannot read erp_at by design. proxy.ts guards /dashboard/* on the cookie's presence only (it cannot validate an RS256 JWT at the edge) — the API remains the authority. lib/auth-session.ts caches the user profile in localStorage for display only, because there is no GET /auth/me; it is not a credential. |
Principles:
- The API contract is the source of truth. The UI adapts to
11-BACKEND-PHASE1.md, never the reverse. - Keep components thin; put data-fetching and derived state in hooks, request/response shaping in the API client.
- One typed client method per endpoint, returning the DTO type from
types/.
1.1 Design tokens (colour theme)
All colour comes from CSS custom properties in app/globals.css (:root, .dark, .vibrant), consumed as Tailwind utilities (bg-primary, text-primary, bg-secondary, …) via the @theme inline mapping — never hardcode a hex/oklch value in a component. To restyle the brand accent, change the token values in globals.css; every component that uses bg-primary/text-primary/ring picks it up automatically across all three themes (light/dark/vibrant).
2026-09-10 — brand accent switched to crimson.
--primary(and--ring,--chart-1,--sidebar-primary,--sidebar-accent) changed from blue/violet to a crimson-red (oklch(0.586 0.222 17.585)light/vibrant,oklch(0.645 0.215 16.439)dark) to match the reference brand look, purely a token-value change — no component structure, layout, or architecture changed.components/ui/button.tsx'sdefaultvariant now readsbg-primary/text-primary-foreground(was hardcodedbg-black/text-white), so this is the first theme change to actually flow through the shared Button rather than being fought by a one-off override. The Login screen (app/login/page.tsx) was updated to stop overriding the button withbg-foregroundand instead take the shareddefaultvariant, and its "Forgot password?" / "Create an account" links now usetext-primaryinstead oftext-foregroundto read as accent links. Same date:components/ui/select.tsx(SelectItem) andcomponents/ui/combobox.tsxhad a hardcodedviolet-*highlight/check-mark color that ignored the theme entirely — fixed to token-driven colours, then aligned with the sidebar look (below).
2026-09-10 — dropdown/select/combobox highlight matches the sidebar's active-item colour.
DropdownMenuItem/DropdownMenuSubTrigger/DropdownMenuCheckboxItem/DropdownMenuRadioItem(dropdown-menu.tsx),SelectItem(select.tsx), and the combobox item (combobox.tsx) all switched their highlighted/focused state frombg-accent/text-accent-foreground(a separate teal token, unrelated to the brand colour) tobg-secondary/text-secondary-foreground— the same soft-crimson-tint token the sidebar's active nav item already uses for its background/text. Result: hovering or keyboard-focusing any menu, select, or combobox row now highlights in the same soft-crimson tone as the active sidebar item, in every theme (light/dark/vibrant), since both read the same token pair.--accentitself is untouched and still used elsewhere (e.g.ghostbutton hover) as a neutral highlight.
2026-09-10 — theme-toggle "Vibrant" segment matches the sidebar/brand colour.
components/theme-toggle.tsx's active "Vibrant" (monitor icon) segment was hardcoded tobg-linear-to-br from-indigo-500 to-violet-600— the only spot in the toggle not driven by a token, so it kept showing indigo/violet regardless of the brand colour. Changed tofrom-sidebar-primary to-primarywithtext-primary-foreground, so the active pill now renders a crimson gradient matching the sidebar's active-item colour, and will track any future--primary/--sidebar-primarychange automatically.
2026-09-10 — Hexa ERP logo re-coloured, background removed.
public/images/hexa-erp-logo.png(used by bothAppSidebar.tsx's header andapp/login/page.tsx) was a raster image with a baked-in near-white background and blue/navy/gray brand colours — not something a CSS/token change can touch, since it's pixels, not markup. Reprocessed with a one-off Node/sharpscript (not committed — this was an asset edit, not a code change) that made the near-white background fully transparent (with soft alpha at the antialiased edges, not a hard cutout) and remapped the blue/navy hues to the same crimson used by--primary/--sidebar-primary, while leaving the "ERP" wordmark and its hexagon a neutral gray. The original is kept alongside ashexa-erp-logo.png.bakin case the brand mark itself needs to be redone by a designer later. If the logo is ever replaced, prefer sourcing a transparent-background asset directly rather than re-running the extraction script, which is a best-effort colour/alpha heuristic, not a general tool.
2026-09-10 — dashboard stat cards sized down.
components/ui/stat-card.tsx(the top row of the dashboard: Low Stock Alerts, Stock On-Hand, etc.) had generous spacing (p-5,gap-3,text-baselabel,size-9icon,text-xlvalue) that read as oversized once the cards' third row (delta/trend, both optional props the dashboard never passes) was empty — most of a card's height was whitespace. Tightened top-4,gap-2,text-smlabel,size-7icon,text-lgvalue,text-xsdelta row — same content, same props/behaviour, just denser.StatCardhas exactly one call site (app/dashboard/page.tsx), so this didn't need reconciling against any other screen. Same date: the grid wrapping those cards went fromgrid-cols-1 sm:grid-cols-2 lg:grid-cols-4(wrapped to two rows) togrid-cols-2 sm:grid-cols-4 lg:grid-cols-7(all 7 in one row), then the "Pending Approval POs" card was removed from the dashboard entirely (app/dashboard/page.tsx) and the grid settled atgrid-cols-2 sm:grid-cols-3 lg:grid-cols-6for the remaining 6 — a deliberate content + layout call, not a bug fix.stats.pendingApprovalPurchaseOrdersand theDashboardStatstype are untouched (backend/API contract, out of scope for a UI-only change) — only the card and its now-unusedClockicon import were removed from the page.
2026-09-10 — the two dashboard chart panels sit side by side. "Stock Valuation by Warehouse" (bar chart) and "Stock Movement Trend" (line chart) on
app/dashboard/page.tsxwere two full-widthrounded-2xlpanels stacked vertically. Wrapped both in agrid grid-cols-1 lg:grid-cols-2 gap-4container so they sit in one row atlgand above, and stack on narrower screens — each panel's own markup/content is unchanged, only the wrapping grid was added.
2026-09-10 — "Stock Valuation by Warehouse" bar colour matches the brand crimson. The bar dataset's
backgroundColorwas a hardcoded indigo#6366f1, left over from before the crimson theme change and not updated with it. Changed to#E11D48(the same crimson as--primary).components/ui/bar-chart.tsx/line-chart.tsxare thin Chart.js wrappers that take a plain CSS colour string per dataset — Canvas rendering can't reliably resolve the app'soklch()custom properties, so (matching the Line chart's existing "In"/"Out" green/red, also literal hex) the call site passes a literal hex kept in sync with the token by hand rather than avar(--primary)reference. The Line chart's green ("In") / red ("Out") were left as-is: those are semantic (stock direction), not brand colour, and forcing them to crimson would remove the in/out distinction.
2026-09-10 — login-page illustration's UI-mockup panels recoloured to crimson.
public/images/loginerps.png(light mode) andpublic/images/whithoutbackground.png(dark mode,app/login/page.tsx's two side-by-side hero images) are photorealistic scenes — two people at a table plus three floating UI-mockup panels ("Inventory Management", "Order Processing", "Integrated Analytics") in blue/teal. Unlike the flat-vector logo, a global hue-shift here would also discolour the people (navy suits/hair share the same blue hue range as the panel chrome — verified by direct HSL sampling, not assumption), so the recolour script is restricted to three hand-verified pixel rectangles covering just the panels, and only remaps hue 180-230° pixels with real saturation/lightness (spread ≥ 40,s ≥ 0.25,0.2 ≤ l ≤ 0.85) to crimson, blending by saturation so text/borders/antialiasing aren't speckled. Greens (the "Delivered" status pill, success ring, checkmarks) are left alone — same semantic-colour reasoning as the dashboard charts, above.whithoutbackground.pngadditionally got the same white-to-transparent treatment as the logo (it turned out to be a flat white background despite the filename, not real alpha) in the same pass. Both originals are kept asloginerps.png.bak/whithoutbackground.png.bak. This is a best-effort raster edit (not a general tool, and not committed as a script) — if a designer ever redoes this illustration, recolour it at the source rather than re-running the heuristic.
2. User flows
The flow below is the end-to-end Phase-1 journey: replenishment need → procurement → receiving → QC hold → stock available → stock operations, with the reorder loop closing back. Colour = functional role. Roles now drive real sidebar visibility (GET /auth/me's navCodes, see docs/10 C.8/docs/11 §2.0.1, admin screens at /dashboard/settings/roles and /dashboard/settings/users) but per-endpoint RBAC is still not enforced — this remains a UI-level filter only.
Roles screen (2026-07-18):
codeis never typed by an operator — it's derived client-side fromname(uppercased, non-alphanumeric →_) and shown read-only, on both create and edit. The Create Role dialog also includes the permission checkbox tree (components/auth/RolePermissionTree.tsx, fed byGET /nav), so creating a role and assigning its sidebar permissions is one Save action; the detail page (/dashboard/settings/roles/[id]) remains for later edits. The Create User dialog's "User type" is a<Select>populated fromGET /users/user-types(proxying AuthHex'slistUserTypes), defaulting to the sole existing type — operators never type an AuthHex UserType GUID by hand.
flowchart TD
START([Replenishment need]):::trigger
ALERT[/Reorder alert raised<br/>available at or below reorder point/]:::system
START --> REQ[Create Requisition<br/>Procurement officer]:::proc
ALERT --> REQ
REQ --> SUBREQ[Submit requisition]:::proc
SUBREQ --> RFQDEC{RFQ needed?}:::decision
RFQDEC -->|Yes| RFQ[Create RFQ and send to vendors]:::proc
RFQ --> QUOTE[Record vendor quotations]:::proc
QUOTE --> COMPARE[Compare and select vendor]:::proc
COMPARE --> PO[Create Purchase Order]:::proc
RFQDEC -->|No| PO
PO --> APPRDEC{approvalRequired?}:::decision
APPRDEC -->|No - Phase 1 default| AUTOAPP[[Auto-approved on creation]]:::system
APPRDEC -->|Yes| PENDING[Pending approval]:::proc
PENDING --> APPROVE{Approver decision}:::decision
APPROVE -->|Reject| REJECTED([PO rejected]):::endp
APPROVE -->|Approve| APPROVED[PO Approved and issued to vendor]:::proc
AUTOAPP --> APPROVED
APPROVED --> ARRIVE([Goods arrive from vendor]):::trigger
ARRIVE --> GRN[Create GRN against PO<br/>Storekeeper]:::wh
GRN --> ENTER[Enter qty, bin, batch/serial]:::wh
ENTER --> TOLDEC{Over-receipt beyond tolerance?}:::decision
TOLDEC -->|Yes| BLOCKREC([Blocked / warn]):::endp
TOLDEC -->|No| HOLDDEC{Inspection required?}:::decision
HOLDDEC -->|Yes| RECVHOLD[Receive as OnHold]:::wh
HOLDDEC -->|No| RECVOK[Receive as Available]:::wh
RECVHOLD --> CONFIRM[Confirm GRN]:::wh
RECVOK --> CONFIRM
CONFIRM --> FIFO[[Create FIFO layer + post stock ledger + update PO qtyReceived]]:::system
FIFO --> HELDQ{Line on hold?}:::decision
HELDQ -->|Yes| INSPECT[QC inspection<br/>Inventory controller]:::controller
INSPECT --> INSPDEC{Pass inspection?}:::decision
INSPDEC -->|Release| AVAIL
INSPDEC -->|Reject| PRET[Create Purchase Return]:::proc
PRET --> RETPOST[[Post outbound ledger]]:::system
RETPOST --> RETEND([Returned to vendor]):::endp
HELDQ -->|No| AVAIL([Stock Available]):::stock
AVAIL --> OPS{Stock operation}:::decision
OPS -->|Transfer| TR1[Create transfer<br/>source and destination]:::wh
OPS -->|Adjust| AD1[Create adjustment + reason code<br/>Inventory controller]:::controller
OPS -->|Count| CN1[Create count - cycle or full<br/>Storekeeper]:::wh
TR1 --> TRDISP[Dispatch]:::wh
TRDISP --> TRAVAIL{Available covers qty?}:::decision
TRAVAIL -->|No| TRBLOCK([Negative-stock block]):::endp
TRAVAIL -->|Yes| TRIT[[Consume source layers, status In-Transit]]:::system
TRIT --> TRREC[Receive at destination]:::wh
TRREC --> TRDONE[[Create destination layer at inherited cost - cost-preserving]]:::system
TRDONE --> AVAIL
AD1 --> ADDEC{Decrease exceeds available?}:::decision
ADDEC -->|Yes| ADBLOCK([Negative-stock block]):::endp
ADDEC -->|No| ADPOST[[Auto-post: FIFO consume/create + ledger]]:::system
ADPOST --> AVAIL
CN1 --> CNSNAP[[Snapshot system qty]]:::system
CNSNAP --> CNCOUNT[Enter counted qty]:::wh
CNCOUNT --> CNVAR[[Compute variance]]:::system
CNVAR --> CNPOST[Post count<br/>Inventory controller]:::controller
CNPOST --> CNADJ[[Variance adjustment + ledger]]:::system
CNADJ --> AVAIL
AVAIL -.->|monitor available vs reorder point| ALERT
classDef trigger fill:#fff2cc,stroke:#d6b656,color:#333;
classDef proc fill:#d5e8d4,stroke:#82b366,color:#333;
classDef wh fill:#ffe6cc,stroke:#d79b00,color:#333;
classDef controller fill:#e1d5e7,stroke:#9673a6,color:#333;
classDef system fill:#dae8fc,stroke:#6c8ebf,color:#333;
classDef stock fill:#d5e8d4,stroke:#2d7d2d,color:#173d17;
classDef decision fill:#f8f4d0,stroke:#b0a133,color:#333;
classDef endp fill:#f8cecc,stroke:#b85450,color:#333;
2.1 Flows → API mapping
Each screen calls the endpoints in 11-BACKEND-PHASE1.md. System steps (blue) are performed server-side — the UI only triggers them and renders the result.
| Flow | Screens | Key endpoints |
|---|---|---|
| Login | Login | POST /auth/login (sets the session cookies), POST /auth/logout |
| Replenishment | Reorder alerts | GET /stock/reorder-alerts, POST /stock/reorder-alerts/{itemId}/requisition |
| Procurement | Requisition, RFQ, PO | POST /requisitions, /rfqs, /rfqs/{id}/quotations, GET /rfqs/{id}/comparison, POST /purchase-orders, PUT /purchase-orders/{id} |
| Receiving | GRN | POST /grns, POST /grns/{id}/confirm, POST /grns/{id}/lines/{lineId}/release |
| Returns | Purchase return | POST /purchase-returns |
| Stock enquiry | Stock views | GET /stock/on-hand, /stock/ledger, /stock/valuation |
| Transfer | Transfer | POST /stock-transfers, /dispatch, /receive |
| Adjustment | Adjustment | POST /stock-adjustments |
| Count | Count | POST /stock-counts, PUT /stock-counts/{id}/counts, POST /stock-counts/{id}/post |
2.2 Master data screens (supporting, outside the core flow)
Vendors, Items, Categories, Subcategories, UOM, Warehouses, Brands, and Item Types are supporting master-data CRUD screens the flow above depends on but doesn't itself route through, so they're intentionally absent from the diagram/table. List screens follow one pagination convention: page/pageSize/q/sortOrder params, page size 5, debounced search, Previous/Next controls.
2026-07-17 — the frontend is connected to the real API.
lib/api/mock-data.tsis deleted; everylib/api/*.tsmodule calls the backend. The drift listed here previously has been reconciled — what follows records the decisions so they are not re-litigated.
Brand (app/dashboard/products/brands), Item Type (app/dashboard/products/item-types, formerly "Variant Category") and Subcategory (app/dashboard/products/categories/[id]) are real backend entities (docs/11 §2.3/2.6/2.7). The item builder on /dashboard/products/new reads the Item Type list live from GET /item-types: checking a type (Color, Size, or any custom one added inline from that same page) reveals a value-entry section for it, and one Item is created per combination across however many types are checked, with a client-generated SKU. See Frontend/PROGRESS.md for the history.
2.2.1 Resolved contract decisions (backend is authoritative — §1)
variantCategoriesApi→itemTypesApi(GET /item-types);variantCategoryId→itemTypeId.Item.itemType→stockNature.itemTypenow means a Color/Size dimension master — a different concept (docs/11 §2.7). The item-detail label reads "Stock nature".- Both category FKs travel. The builder sends
categoryIdandsubCategoryId; the oldsubCategoryId ?? categoryIdcollapse lost the parent. The server rejects a mismatched pair with 422. Subcategories are their own resource —Category.parentIdand?tree=trueare gone. - Colour hex-packing stays frontend-only. There is no value table server-side, so
"Red|#EF4444"+encodeColorValue/decodeColorValue/isColorCategoryhave nothing to reconcile against. Kept as-is. - SKU generation stays client-side (
buildVariantSku) and is the only record of which colour/size an item is; the server only uniqueness-checks it. Nothing can query items by colour — accepted (docs/10 Part C.9). remove()→updateStatus(id, "Inactive")everywhere. There are noDELETEendpoints on any master (FR-MD-08); the lists show a Status column and Deactivate/Activate.initialQtyis gone from the builder — the Item contract has no such field and there is no initial-receipt flow. Stock arrives via a GRN.- Sale-price toggle on the builder (2026-07-22). A "Fixed price / Use stock value" toggle sits on
/dashboard/products/new. Use stock value (default) sendssalePrice: nullon every created item (sold at FIFO value). Fixed price reveals a top "fix value" input that pre-fills a per-variantSale pricecolumn; each row is editable, and submit is blocked until every generated variant has a price> 0. The toggle is frontend-only — the contract is just the nullablesalePricefield (docs/11 §2.1); it is Sales-only and never affects GRN/costing. The non-transactional create loop still applies — each variant'ssalePricerides its ownPOST /items. - Product Configuration (
app/dashboard/products/settings,GET/PUT /product-config) — only 3 of the original design's ~13 toggles exist.subcategoriesEnabled/brandsEnabledare server-enforced (CONFIG_DISABLED);itemTypesEnabledis advisory and this app is what honours it (it hides the builder's type section). The UI states that distinction on the screen rather than implying a guarantee. - Non-transactional create loop: the builder's per-row
itemsApi.create()has no transaction — aSKU_DUPLICATEon row 7 of 12 leaves 6 items created. The error message now says how many landed rather than implying nothing happened. A transactional bulk-create endpoint would be the real fix. - GRN edit/delete removed — the API has no
PUT/DELETEfor a GRN; receipts are corrected by reversing documents (FR-X-05). - GRN off-PO items + inline item create (2026-07-22). On
/dashboard/receiving/grn/new, "Add line" is available in both PO and direct mode — an added line in PO mode haspoLineId: nulland receives an item not on the PO (editable item/UOM dropdowns,unitCostrequired). A "New item" button opens/dashboard/products/newin a new browser tab (window.open, the first such pattern in the app), and a refresh icon re-pullsGET /items?status=Activeso the newly created item is selectable without reloading the screen and losing the in-progress GRN. Server treats off-PO lines as direct receipts (no over-receipt check) —docs/11 §4.1. - RFQ invited-vendors is not persisted —
POST /rfqsvalidatesvendorIdsthen discards them, so the list/detail screens show quotations received instead of vendors invited. - Known gap — serial numbers: FR-GRN-04 requires capturing serials on receipt, but
CreateGrnLineInputhas no such field (onlybatch). The UI does not collect them rather than silently discarding them. Needs a backend change to honour the requirement.
3. Validation posture (read carefully)
Validate on the client for UX; never trust the client. The server is the authority. The browser can be bypassed (devtools, direct API calls, replays), so client checks exist only to give fast feedback and reduce round-trips — they are never the enforcement point.
3.1 Two categories
Client-side (UX only — safe to check locally): purely input-level facts the browser already has.
- Required fields present.
- Format: SKU pattern, numeric fields numeric, date format, positive integers.
- Range/bounds:
qty > 0,unitPrice >= 0,factor > 0. ItemsalePriceis a client-supplied number — the builder requires> 0per variant in fixed mode; the server only checks>= 0on a supplied value (docs/11 §2.1). - Simple cross-field input rules: transfer
destWarehouseId != srcWarehouseId. - Enum membership via constrained dropdowns (
stockNature— ex-itemType,trackingMode,countType,holdStatus). Note the Item Type dropdown is not in this category: it's server data (GET /item-types), not an enum.
Server-authoritative (client MUST NOT assume — only the server can judge): anything depending on current server state.
- Stock availability / negative-stock block (depends on live ledger) —
STOCK_NEGATIVE_BLOCKED. - FIFO layer sufficiency on any issue.
- Over/under-receipt tolerance vs PO open quantity —
OVER_RECEIPT_TOLERANCE. - SKU uniqueness —
SKU_DUPLICATE. - Batch expiry / on-hold issuability —
EXPIRED_BATCH_BLOCKED,ONHOLD_NOT_ISSUABLE. - PO editability (status-dependent) —
PO_NOT_EDITABLE. - Referential existence/active status of item/vendor/warehouse/bin.
- Concurrency (stale edit) —
CONCURRENCY_CONFLICTviaETag/If-Match. - Reason code required/valid —
REASON_CODE_REQUIRED.
Rule of thumb: if answering "is this allowed?" requires knowing the current stock, a document's status, or another user's change, it is server-authoritative. Do not gate submission on a client-side guess about it, and do not show it as "valid" until the server confirms.
3.2 Handling server responses
- Always send the request and handle the outcome; the server response is the truth.
- On
400ValidationProblemDetails, maperrorsto fields. - On
409/422domain errors, readcodeand show a specific message (e.g.STOCK_NEGATIVE_BLOCKED→ "Not enough available stock"). Keep acode → messagemap inlib/so messages are consistent. - On
412(CONCURRENCY_CONFLICT), tell the user the record changed and refetch before retrying. - Never silently swallow a
ProblemDetails; surface it.
3.3 What not to do
- Don't disable the submit button based on a client assumption about stock, availability, or status.
- Don't reimplement FIFO/tolerance/negative-stock logic in the browser — you cannot see the live ledger.
- Don't trust quantities computed client-side for posting; send inputs and let the server compute costed movements.
4. Error & empty states
- Every list screen handles loading, empty, and error states explicitly.
- Surface the API client's normalized error (from
ProblemDetails) with the domaincodemessage where present. - For transactional actions (GRN confirm, transfer dispatch/receive, adjustment, count post), show the server's returned side effects (created/consumed layers, ledger refs) as confirmation rather than assuming success.
End of 20-FRONTEND.md. API contract: 11-BACKEND-PHASE1.md. Record work: Frontend/PROGRESS.md.