# 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 in `11-BACKEND-PHASE1.md`; business rules are in `10-BACKEND-PHASE1.md`. Record work in `Frontend/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/`. --- ## 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 are conceptual; RBAC is not enforced in Phase 1). ```mermaid flowchart TD START([Replenishment need]):::trigger ALERT[/Reorder alert raised
available at or below reorder point/]:::system START --> REQ[Create Requisition
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
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
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
source and destination]:::wh OPS -->|Adjust| AD1[Create adjustment + reason code
Inventory controller]:::controller OPS -->|Count| CN1[Create count - cycle or full
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
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.ts` is **deleted**; every `lib/api/*.ts` module 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`.** `itemType` now 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 `categoryId` **and** `subCategoryId`; the old `subCategoryId ?? categoryId` collapse lost the parent. The server rejects a mismatched pair with 422. Subcategories are their own resource — `Category.parentId` and `?tree=true` are gone. - **Colour hex-packing stays frontend-only.** There is no value table server-side, so `"Red|#EF4444"` + `encodeColorValue`/`decodeColorValue`/`isColorCategory` have 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 no `DELETE` endpoints on any master (FR-MD-08); the lists show a Status column and Deactivate/Activate. - **`initialQty` is gone** from the builder — the Item contract has no such field and there is no initial-receipt flow. Stock arrives via a GRN. - **Product Configuration** (`app/dashboard/products/settings`, `GET`/`PUT /product-config`) — only **3** of the original design's ~13 toggles exist. `subcategoriesEnabled`/`brandsEnabled` are server-enforced (`CONFIG_DISABLED`); **`itemTypesEnabled` is 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 — a `SKU_DUPLICATE` on 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`/`DELETE` for a GRN; receipts are corrected by reversing documents (FR-X-05). - **RFQ invited-vendors is not persisted** — `POST /rfqs` validates `vendorIds` then 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 `CreateGrnLineInput` has no such field (only `batch`). 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`. - 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_CONFLICT` via `ETag`/`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 `400` `ValidationProblemDetails`, map `errors` to fields. - On `409` / `422` domain errors, read `code` and show a specific message (e.g. `STOCK_NEGATIVE_BLOCKED` → "Not enough available stock"). Keep a `code → message` map in `lib/` 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 domain `code` message 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`.*