Files
ERP-core/docs/20-FRONTEND.md
2026-07-17 14:27:51 +05:30

15 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 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).

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.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)

  • variantCategoriesApiitemTypesApi (GET /item-types); variantCategoryIditemTypeId.
  • Item.itemTypestockNature. 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 persistedPOST /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 uniquenessSKU_DUPLICATE.
  • Batch expiry / on-hold issuabilityEXPIRED_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/validREASON_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.