9.8 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. |
| 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/) against NEXT_PUBLIC_API_BASE_URL; all calls go through it. No scattered fetch() in components. |
| Types | TS types in types/ mirror the API DTOs in 11-BACKEND-PHASE1.md. When the contract changes, update these first. |
| Auth | Store the bearer token from /auth/login; attach Authorization: Bearer <token> in the API client. |
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).
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 |
| 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 |
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 (
itemType,trackingMode,countType,holdStatus).
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.