diff --git a/Frontend/PROGRESS.md b/Frontend/PROGRESS.md index 8e8075d..3bdc995 100644 --- a/Frontend/PROGRESS.md +++ b/Frontend/PROGRESS.md @@ -5,14 +5,18 @@ Spec: `docs/20-FRONTEND.md` (flows + rules) · `docs/11-BACKEND-PHASE1.md` (API 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 -- [x] `NEXT_PUBLIC_API_BASE_URL` wired (`.env.local` / `.env.local.example`, alongside the existing `NEXT_PUBLIC_AUTH_API_BASE_URL`) — points at `Backend/ERPCore`'s https profile (`7112`) -- [x] Typed API client / fetch wrapper (`lib/api-client.ts`: `apiRequest`/`apiRequestWithETag`/`buildQuery`) + bearer token handling (`lib/auth-token.ts`, degrades gracefully — no login endpoint wired yet, see §1) -- [x] Shared TS types mirroring API DTOs (`types/common.ts`, `types/master-data.ts`, `types/procurement.ts`, `types/grn.ts`) — built only for the subset GRN needs (Item/Warehouse/Bin/Vendor/Uom/PurchaseOrder/Grn) +- [x] `NEXT_PUBLIC_API_BASE_URL` wired (`.env.local` / `.env.local.example`, alongside the existing `NEXT_PUBLIC_AUTH_API_BASE_URL`) — currently unused now that the fetch client is gone (see 2026-07-15 note below) +- [ ] Typed API client / fetch wrapper — **removed 2026-07-15** (`lib/api-client.ts` + `lib/auth-token.ts` deleted per explicit user request: "delete all api connections"). No live code path in the frontend makes an HTTP call to any backend anymore — every screen is 100% mock data (`lib/api/mock-data.ts`). +- [x] Shared TS types mirroring API DTOs (`types/common.ts`, `types/master-data.ts`, `types/procurement.ts`, `types/grn.ts`) — built only for the subset GRN needs (Item/Warehouse/Bin/Vendor/Uom/PurchaseOrder/Grn); `types/common.ts` now also carries `ApiResult` (moved here 2026-07-15 when `lib/api-client.ts` was deleted, since it's a plain data envelope, not fetch-specific) - [~] Client validation helpers (`lib/validations/grn.ts`) — **deviation**: uses `zod` (already a project dependency, used by `lib/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) -- [x] `ProblemDetails` normalizer (`ApiError` in `lib/api-client.ts`) + `code → message` map (`lib/error-map.ts`) +- [x] `code → message` map (`lib/error-map.ts`) — `errorMessage`/`fieldErrors` duck-type any `{ code, detail, errors }`-shaped rejection (the `ApiError` class they used to check via `instanceof` no longer exists); this also fixed a latent bug where the mock layer's plain `Error`-plus-`.code` rejects never matched the old `instanceof ApiError` check, so `CODE_MESSAGES` silently never applied to any mock error > **Scope note:** this foundation was built alongside the GRN feature and only covers the endpoints GRN consumes (items, warehouses/bins, vendors, uoms, purchase-orders, grns). Other master-data/procurement API methods still need their own `lib/api/*.ts` files when those screens are built. +> **2026-07-15 — fetch infrastructure deleted outright (not just reverted to mock).** Following an earlier same-week pass that wired every `lib/api/*.ts` module to real `fetch` calls (then reverted via `git revert --no-commit` at the user's request — see `Backend`-adjacent history if relevant later), the user asked to go further and delete the underlying connection mechanism entirely, not just leave it unused. Deleted `lib/api-client.ts` (`apiRequest`/`apiRequestWithETag`/`buildQuery`/`ApiError`) and `lib/auth-token.ts` (bearer-token storage) as files. Follow-on fixes this required: (1) `ApiResult` — used by `items.ts`/`purchase-orders.ts`/`vendors.ts` for their mock ETag pattern — moved into `types/common.ts`; (2) `lib/error-map.ts` rewritten to duck-type instead of `instanceof ApiError`; (3) three detail pages (`vendors/[id]`, `products/[id]`, `procurement/purchase-orders/[id]`) had their `err instanceof ApiError ? err.code : (err as {code?:string})?.code` conflict-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 15 `lib/api/*.ts` files, since they referenced a now-deleted module. `tsc --noEmit`/`eslint` clean (same pre-existing `login/page.tsx` error and established `set-state-in-effect` pattern 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, `requisitionId` required 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 since `lib/api-client.ts`/`lib/auth-token.ts` no longer exist. + ## 1. Auth - [~] Login screen — UI built (`app/login`); not yet wired to `POST /auth/login` / token storage - [~] Forgot password — add email screen — UI built (`app/login/forgot`); not yet wired to API diff --git a/Frontend/erp-system/app/dashboard/procurement/purchase-orders/[id]/page.tsx b/Frontend/erp-system/app/dashboard/procurement/purchase-orders/[id]/page.tsx index 8b0be76..a205486 100644 --- a/Frontend/erp-system/app/dashboard/procurement/purchase-orders/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/procurement/purchase-orders/[id]/page.tsx @@ -10,7 +10,6 @@ import { warehousesApi } from "@/lib/api/warehouses" import { itemsApi } from "@/lib/api/items" import { uomsApi } from "@/lib/api/uoms" import { vendorsApi } from "@/lib/api/vendors" -import { ApiError } from "@/lib/api-client" import { errorMessage } from "@/lib/error-map" import { validatePoLine } from "@/lib/validations/procurement" import { cn } from "@/lib/utils" @@ -172,7 +171,7 @@ export default function PurchaseOrderDetailPage() { setLines(toDraftLines(result.data)) toast.success("Purchase order saved", `${result.data.docNo} updated (FR-PROC-05, edit-while-open).`) } catch (err) { - const code = err instanceof ApiError ? err.code : (err as { code?: string })?.code + const code = (err as { code?: string })?.code if (code === "CONCURRENCY_CONFLICT") { setConflict(true) setSaveError(errorMessage(err)) diff --git a/Frontend/erp-system/app/dashboard/products/[id]/page.tsx b/Frontend/erp-system/app/dashboard/products/[id]/page.tsx index a319060..94cd29d 100644 --- a/Frontend/erp-system/app/dashboard/products/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/products/[id]/page.tsx @@ -10,7 +10,6 @@ import { categoriesApi } from "@/lib/api/categories" import { uomsApi } from "@/lib/api/uoms" import { vendorsApi } from "@/lib/api/vendors" import { warehousesApi } from "@/lib/api/warehouses" -import { ApiError } from "@/lib/api-client" import { errorMessage, fieldErrors } from "@/lib/error-map" import { validateConversionLine, validateItemForm, validateReorderLine } from "@/lib/validations/master-data" import { cn } from "@/lib/utils" @@ -147,7 +146,7 @@ export default function ItemDetailPage() { setEtag(result.etag) toast.success("Item saved", `${result.data.sku} — ${result.data.name}`) } catch (err) { - const code = err instanceof ApiError ? err.code : (err as { code?: string })?.code + const code = (err as { code?: string })?.code if (code === "CONCURRENCY_CONFLICT") { setConflict(true) setSaveError(errorMessage(err)) diff --git a/Frontend/erp-system/app/dashboard/vendors/[id]/page.tsx b/Frontend/erp-system/app/dashboard/vendors/[id]/page.tsx index 6e3e256..b04f997 100644 --- a/Frontend/erp-system/app/dashboard/vendors/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/vendors/[id]/page.tsx @@ -6,7 +6,6 @@ import Link from "next/link" import { AlertTriangle, ArrowLeft, Save } from "lucide-react" import { vendorsApi } from "@/lib/api/vendors" -import { ApiError } from "@/lib/api-client" import { errorMessage, fieldErrors } from "@/lib/error-map" import { cn } from "@/lib/utils" import { Vendor } from "@/types/master-data" @@ -78,7 +77,7 @@ export default function VendorDetailPage() { setEtag(result.etag) toast.success("Vendor saved", `${result.data.code} — ${result.data.name}`) } catch (err) { - const code = err instanceof ApiError ? err.code : (err as { code?: string })?.code + const code = (err as { code?: string })?.code if (code === "CONCURRENCY_CONFLICT") { setConflict(true) setSaveError(errorMessage(err)) diff --git a/Frontend/erp-system/lib/api-client.ts b/Frontend/erp-system/lib/api-client.ts deleted file mode 100644 index f0e3098..0000000 --- a/Frontend/erp-system/lib/api-client.ts +++ /dev/null @@ -1,98 +0,0 @@ -// Single typed fetch client for the ERPCore API (docs/20-FRONTEND.md §1: "A single -// typed fetch client against NEXT_PUBLIC_API_BASE_URL; all calls go through it. No -// scattered fetch() in components."). Per-endpoint methods live in lib/api/*.ts. -import { ProblemDetails } from "@/types/common" -import { getAccessToken } from "@/lib/auth-token" - -const API_BASE = `${process.env.NEXT_PUBLIC_API_BASE_URL ?? ""}/api/v1` - -/** Normalized RFC 7807 error (docs/11-BACKEND-PHASE1.md §1.8) thrown on any non-2xx response. */ -export class ApiError extends Error { - status: number - code?: string - detail?: string - errors?: Record - traceId?: string - - constructor(problem: ProblemDetails) { - super(problem.title || "Request failed") - this.status = problem.status - this.code = problem.code - this.detail = problem.detail - this.errors = problem.errors - this.traceId = problem.traceId - } -} - -export interface RequestOptions extends Omit { - body?: unknown - /** Sent as If-Match for concurrency-guarded PUT/PATCH (docs/11 §1.6). */ - ifMatch?: string - /** Sent as Idempotency-Key for transactional POSTs (e.g. GRN confirm, docs/11 §1.6). */ - idempotencyKey?: string -} - -export interface ApiResult { - data: T - /** Response ETag header, when the resource is mutable (docs/11 §1.6). */ - etag: string | null -} - -async function rawRequest(path: string, options: RequestOptions = {}): Promise { - const { body, ifMatch, idempotencyKey, headers, ...rest } = options - const token = getAccessToken() - - const finalHeaders: Record = { - Accept: "application/json", - ...(body !== undefined ? { "Content-Type": "application/json" } : {}), - ...(token ? { Authorization: `Bearer ${token}` } : {}), - ...(ifMatch ? { "If-Match": ifMatch } : {}), - ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}), - ...((headers as Record | undefined) ?? {}), - } - - const response = await fetch(`${API_BASE}${path}`, { - ...rest, - headers: finalHeaders, - body: body !== undefined ? JSON.stringify(body) : undefined, - }) - - if (!response.ok) { - let problem: ProblemDetails - try { - problem = (await response.json()) as ProblemDetails - } catch { - problem = { title: response.statusText || "Request failed", status: response.status } - } - if (!problem.status) problem.status = response.status - throw new ApiError(problem) - } - - return response -} - -/** Fire a request and decode the JSON body only (no ETag needed). */ -export async function apiRequest(path: string, options?: RequestOptions): Promise { - const response = await rawRequest(path, options) - if (response.status === 204) return undefined as T - return (await response.json()) as T -} - -/** Fire a request and also surface the ETag header, for resources that support If-Match. */ -export async function apiRequestWithETag(path: string, options?: RequestOptions): Promise> { - const response = await rawRequest(path, options) - const etag = response.headers.get("ETag") - const data = response.status === 204 ? (undefined as T) : ((await response.json()) as T) - return { data, etag } -} - -/** Build a `?a=1&b=2` query string, dropping null/undefined/empty values. */ -export function buildQuery(params: object): string { - const search = new URLSearchParams() - for (const [key, value] of Object.entries(params) as [string, string | number | boolean | null | undefined][]) { - if (value === null || value === undefined || value === "") continue - search.set(key, String(value)) - } - const qs = search.toString() - return qs ? `?${qs}` : "" -} diff --git a/Frontend/erp-system/lib/api/categories.ts b/Frontend/erp-system/lib/api/categories.ts index 6602cc3..45c473a 100644 --- a/Frontend/erp-system/lib/api/categories.ts +++ b/Frontend/erp-system/lib/api/categories.ts @@ -1,29 +1,9 @@ // One typed client method per Category endpoint (docs/11-BACKEND-PHASE1.md §2.3, FR-MD-04). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts) so these screens -// can be reviewed without a running backend. Restore the commented block and -// delete the mock block once Backend/PROGRESS.md §1 (Master Data) exists. +// In-memory sample data (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { Category, CategoryTreeNode, CreateCategoryRequest } from "@/types/master-data" import { allocateCategoryId, mockCategories, mockDelay } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest } from "@/lib/api-client" -// -// export const categoriesApi = { -// list() { -// return apiRequest>("/categories") -// }, -// tree() { -// return apiRequest("/categories?tree=true") -// }, -// create(request: CreateCategoryRequest) { -// return apiRequest("/categories", { method: "POST", body: request }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- function buildTree(categories: Category[]): CategoryTreeNode[] { const nodes = new Map(categories.map((c) => [c.categoryId, { ...c, children: [] }])) const roots: CategoryTreeNode[] = [] diff --git a/Frontend/erp-system/lib/api/grns.ts b/Frontend/erp-system/lib/api/grns.ts index 8021ced..78757ec 100644 --- a/Frontend/erp-system/lib/api/grns.ts +++ b/Frontend/erp-system/lib/api/grns.ts @@ -1,12 +1,7 @@ // One typed client method per GRN endpoint (docs/11-BACKEND-PHASE1.md §4). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with an in-memory mock store (lib/api/mock-data.ts) so the GRN -// screens (list/create/confirm/release) can be reviewed end-to-end without a -// running backend. Restore the commented block and delete the mock block once -// Backend/PROGRESS.md §3/§4 (GRN + Stock Core) exist. Note GET /grns and -// GET /grns/{id} are not yet in docs/11-BACKEND-PHASE1.md §4 — see the note in -// Frontend/PROGRESS.md §4. +// In-memory mock store (lib/api/mock-data.ts) — no backend API calls. Note +// GET /grns and GET /grns/{id} are not in docs/11-BACKEND-PHASE1.md §4 — see +// the note in Frontend/PROGRESS.md §4. import { PagedResponse } from "@/types/common" import { ConfirmGrnResponse, @@ -20,60 +15,6 @@ import { } from "@/types/grn" import { allocateGrnId, allocateGrnLineId, mockDelay, mockGrns, mockPurchaseOrders, receiveLayer } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, apiRequestWithETag, buildQuery } from "@/lib/api-client" -// -// export interface ListGrnsParams { -// page?: number -// pageSize?: number -// q?: string -// status?: GrnStatus -// poId?: number -// warehouseId?: number -// } -// -// export const grnsApi = { -// list(params: ListGrnsParams = {}) { -// return apiRequest>(`/grns${buildQuery(params)}`) -// }, -// -// get(grnId: number) { -// return apiRequest(`/grns/${grnId}`) -// }, -// -// async create(request: CreateGrnRequest) { -// const { data } = await apiRequestWithETag("/grns", { method: "POST", body: request }) -// return data -// }, -// -// // Draft-only — a GRN with createdLayers/ledger postings (Confirmed/Closed) is -// // immutable per docs/11 §4. -// async update(grnId: number, request: CreateGrnRequest, ifMatch: string) { -// const { data } = await apiRequestWithETag(`/grns/${grnId}`, { method: "PUT", body: request, ifMatch }) -// return data -// }, -// -// remove(grnId: number) { -// return apiRequest(`/grns/${grnId}`, { method: "DELETE" }) -// }, -// -// confirm(grnId: number, idempotencyKey?: string) { -// return apiRequest(`/grns/${grnId}/confirm`, { -// method: "POST", -// body: {}, -// idempotencyKey, -// }) -// }, -// -// releaseLine(grnId: number, grnLineId: number, action: ReleaseAction) { -// return apiRequest(`/grns/${grnId}/lines/${grnLineId}/release`, { -// method: "POST", -// body: { action }, -// }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface ListGrnsParams { page?: number pageSize?: number diff --git a/Frontend/erp-system/lib/api/items.ts b/Frontend/erp-system/lib/api/items.ts index b1816bf..c31e1e9 100644 --- a/Frontend/erp-system/lib/api/items.ts +++ b/Frontend/erp-system/lib/api/items.ts @@ -1,12 +1,7 @@ // One typed client method per Item endpoint (docs/11-BACKEND-PHASE1.md §2.1, FR-MD-01/05/08). // `list` also backs the GRN/PO/Requisition/RFQ item pickers built in earlier sessions. -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts) so these screens -// can be reviewed without a running backend. Restore the commented block and -// delete the mock block once Backend/PROGRESS.md §1 (Master Data) exists. -import { ApiResult } from "@/lib/api-client" -import { EntityStatus, PagedResponse } from "@/types/common" +// In-memory sample data (lib/api/mock-data.ts) — no backend API calls. +import { ApiResult, EntityStatus, PagedResponse } from "@/types/common" import { CreateItemRequest, Item, @@ -26,43 +21,6 @@ import { mockItems, } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, apiRequestWithETag, buildQuery } from "@/lib/api-client" -// -// export interface ListItemsParams { -// page?: number -// pageSize?: number -// q?: string -// status?: EntityStatus -// categoryId?: number -// trackingMode?: TrackingMode -// } -// -// export const itemsApi = { -// list(params: ListItemsParams = {}) { -// return apiRequest>(`/items${buildQuery(params)}`) -// }, -// get(itemId: number) { -// return apiRequestWithETag(`/items/${itemId}`) -// }, -// create(request: CreateItemRequest) { -// return apiRequestWithETag("/items", { method: "POST", body: request }) -// }, -// update(itemId: number, request: UpdateItemRequest, ifMatch: string) { -// return apiRequestWithETag(`/items/${itemId}`, { method: "PUT", body: request, ifMatch }) -// }, -// updateStatus(itemId: number, status: EntityStatus) { -// return apiRequest(`/items/${itemId}/status`, { method: "PATCH", body: { status } }) -// }, -// updateReorder(itemId: number, request: UpdateItemReorderRequest) { -// return apiRequest<{ settings: UpdateItemReorderRequest["settings"] }>(`/items/${itemId}/reorder`, { method: "PUT", body: request }) -// }, -// updateUomConversions(itemId: number, request: UpdateUomConversionsRequest) { -// return apiRequest(`/items/${itemId}/uom-conversions`, { method: "PUT", body: request }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface ListItemsParams { page?: number pageSize?: number diff --git a/Frontend/erp-system/lib/api/mock-data.ts b/Frontend/erp-system/lib/api/mock-data.ts index 46b46cd..fb35ae8 100644 --- a/Frontend/erp-system/lib/api/mock-data.ts +++ b/Frontend/erp-system/lib/api/mock-data.ts @@ -1,9 +1,6 @@ -// Temporary in-memory sample data so the GRN screens can be reviewed as pure UI -// without a running backend. Shapes mirror docs/11-BACKEND-PHASE1.md exactly. -// -// This file (and the "MOCK" blocks in the sibling lib/api/*.ts files) is meant -// to be deleted once the real GRN backend exists — the original fetch-based -// implementations are left commented out in each file for that switch-back. +// In-memory sample data backing every lib/api/*.ts module — the app has no +// fetch-based backend connection (lib/api-client.ts and lib/auth-token.ts were +// removed). Shapes mirror docs/11-BACKEND-PHASE1.md. import { Bin, Category, Item, Uom, Vendor, Warehouse } from "@/types/master-data" import { PurchaseOrder, Quotation, Requisition, Rfq, PurchaseReturn } from "@/types/procurement" import { Grn } from "@/types/grn" diff --git a/Frontend/erp-system/lib/api/purchase-orders.ts b/Frontend/erp-system/lib/api/purchase-orders.ts index 50ea6fa..791d2b5 100644 --- a/Frontend/erp-system/lib/api/purchase-orders.ts +++ b/Frontend/erp-system/lib/api/purchase-orders.ts @@ -1,13 +1,7 @@ // One typed client method per Purchase Order endpoint (docs/11-BACKEND-PHASE1.md §3.3, // FR-PROC-03..07). `get`/`list` also back the GRN "against a PO" picker. -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts) so the GRN and -// Procurement screens can be reviewed without a running backend. Restore the -// commented block and delete the mock block once Backend/PROGRESS.md §2/§3/§4 -// (Procurement + GRN + Stock Core) exist. -import { ApiResult } from "@/lib/api-client" -import { PagedResponse } from "@/types/common" +// In-memory sample data (lib/api/mock-data.ts) — no backend API calls. +import { ApiResult, PagedResponse } from "@/types/common" import { CancelPurchaseOrderRequest, CreatePurchaseOrderRequest, @@ -25,39 +19,6 @@ import { mockPurchaseOrders, } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, apiRequestWithETag, buildQuery } from "@/lib/api-client" -// -// export interface ListPurchaseOrdersParams { -// page?: number -// pageSize?: number -// q?: string -// status?: PurchaseOrderStatus -// vendorId?: number -// } -// -// export const purchaseOrdersApi = { -// list(params: ListPurchaseOrdersParams = {}) { -// return apiRequest>(`/purchase-orders${buildQuery(params)}`) -// }, -// get(poId: number) { -// return apiRequest(`/purchase-orders/${poId}`) -// }, -// getWithETag(poId: number) { -// return apiRequestWithETag(`/purchase-orders/${poId}`) -// }, -// create(request: CreatePurchaseOrderRequest) { -// return apiRequestWithETag("/purchase-orders", { method: "POST", body: request }) -// }, -// update(poId: number, request: UpdatePurchaseOrderRequest, ifMatch: string) { -// return apiRequestWithETag(`/purchase-orders/${poId}`, { method: "PUT", body: request, ifMatch }) -// }, -// cancel(poId: number, request: CancelPurchaseOrderRequest) { -// return apiRequest(`/purchase-orders/${poId}/cancel`, { method: "POST", body: request }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface ListPurchaseOrdersParams { page?: number pageSize?: number diff --git a/Frontend/erp-system/lib/api/purchase-returns.ts b/Frontend/erp-system/lib/api/purchase-returns.ts index 248262c..10fb0a1 100644 --- a/Frontend/erp-system/lib/api/purchase-returns.ts +++ b/Frontend/erp-system/lib/api/purchase-returns.ts @@ -1,9 +1,5 @@ // One typed client method per Purchase Return endpoint (docs/11-BACKEND-PHASE1.md §3.4, FR-PROC-08). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts) so the Procurement -// screens can be reviewed without a running backend. Restore the commented block -// and delete the mock block once Backend/PROGRESS.md §2 (Procurement) exists. +// In-memory sample data (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { CreatePurchaseReturnRequest, PurchaseReturn, PurchaseReturnSummary } from "@/types/procurement" import { @@ -15,22 +11,6 @@ import { postLedgerEntry, } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest } from "@/lib/api-client" -// -// export const purchaseReturnsApi = { -// list() { -// return apiRequest>("/purchase-returns") -// }, -// get(returnId: number) { -// return apiRequest(`/purchase-returns/${returnId}`) -// }, -// create(request: CreatePurchaseReturnRequest) { -// return apiRequest("/purchase-returns", { method: "POST", body: request }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- function toSummary(r: PurchaseReturn): PurchaseReturnSummary { return { returnId: r.returnId, diff --git a/Frontend/erp-system/lib/api/reason-codes.ts b/Frontend/erp-system/lib/api/reason-codes.ts index 0274f38..bd1c301 100644 --- a/Frontend/erp-system/lib/api/reason-codes.ts +++ b/Frontend/erp-system/lib/api/reason-codes.ts @@ -1,21 +1,9 @@ // One typed client method for reference data (docs/11-BACKEND-PHASE1.md §6). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts). +// In-memory sample data (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { ReasonCode, ReasonCodeContext } from "@/types/stock" import { mockDelay, mockReasonCodes } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, buildQuery } from "@/lib/api-client" -// -// export const reasonCodesApi = { -// list(context?: ReasonCodeContext) { -// return apiRequest>(`/reason-codes${buildQuery({ context })}`) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export const reasonCodesApi = { list(context?: ReasonCodeContext): Promise> { const items = mockReasonCodes.filter((r) => !context || r.context === context) diff --git a/Frontend/erp-system/lib/api/requisitions.ts b/Frontend/erp-system/lib/api/requisitions.ts index 9442ff3..01a9954 100644 --- a/Frontend/erp-system/lib/api/requisitions.ts +++ b/Frontend/erp-system/lib/api/requisitions.ts @@ -1,38 +1,9 @@ // One typed client method per Requisition endpoint (docs/11-BACKEND-PHASE1.md §3.1, FR-PROC-01). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts) so the Procurement -// screens can be reviewed without a running backend. Restore the commented block -// and delete the mock block once Backend/PROGRESS.md §2 (Procurement) exists. +// In-memory sample data (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { CreateRequisitionRequest, Requisition, RequisitionStatus, RequisitionSummary } from "@/types/procurement" import { allocateReqLineId, allocateRequisitionId, mockDelay, mockRequisitions } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, buildQuery } from "@/lib/api-client" -// -// export interface ListRequisitionsParams { -// page?: number -// pageSize?: number -// status?: RequisitionStatus -// } -// -// export const requisitionsApi = { -// list(params: ListRequisitionsParams = {}) { -// return apiRequest>(`/requisitions${buildQuery(params)}`) -// }, -// get(requisitionId: number) { -// return apiRequest(`/requisitions/${requisitionId}`) -// }, -// create(request: CreateRequisitionRequest) { -// return apiRequest("/requisitions", { method: "POST", body: request }) -// }, -// submit(requisitionId: number) { -// return apiRequest(`/requisitions/${requisitionId}/submit`, { method: "POST", body: {} }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface ListRequisitionsParams { page?: number pageSize?: number diff --git a/Frontend/erp-system/lib/api/rfqs.ts b/Frontend/erp-system/lib/api/rfqs.ts index b7380f7..e58416d 100644 --- a/Frontend/erp-system/lib/api/rfqs.ts +++ b/Frontend/erp-system/lib/api/rfqs.ts @@ -1,9 +1,5 @@ // One typed client method per RFQ/Quotation endpoint (docs/11-BACKEND-PHASE1.md §3.2, FR-PROC-02). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts) so the Procurement -// screens can be reviewed without a running backend. Restore the commented block -// and delete the mock block once Backend/PROGRESS.md §2 (Procurement) exists. +// In-memory sample data (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { CreateQuotationRequest, @@ -23,28 +19,6 @@ import { mockRfqs, } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest } from "@/lib/api-client" -// -// export const rfqsApi = { -// list() { -// return apiRequest>("/rfqs") -// }, -// get(rfqId: number) { -// return apiRequest(`/rfqs/${rfqId}`) -// }, -// create(request: CreateRfqRequest) { -// return apiRequest("/rfqs", { method: "POST", body: request }) -// }, -// addQuotation(rfqId: number, request: CreateQuotationRequest) { -// return apiRequest(`/rfqs/${rfqId}/quotations`, { method: "POST", body: request }) -// }, -// comparison(rfqId: number) { -// return apiRequest(`/rfqs/${rfqId}/comparison`) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- function toSummary(r: Rfq): RfqSummary { return { rfqId: r.rfqId, diff --git a/Frontend/erp-system/lib/api/stock-adjustments.ts b/Frontend/erp-system/lib/api/stock-adjustments.ts index b48f118..d29a2fd 100644 --- a/Frontend/erp-system/lib/api/stock-adjustments.ts +++ b/Frontend/erp-system/lib/api/stock-adjustments.ts @@ -1,11 +1,5 @@ // One typed client method per adjustment endpoint (docs/11-BACKEND-PHASE1.md §5.5). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with the in-memory Stock Core (lib/api/mock-data.ts). Restore the -// commented block and delete the mock block once Backend/PROGRESS.md §5 -// (stock transactions) exists. GET /stock-adjustments and GET -// /stock-adjustments/{id} are not documented in docs/11 §5.5 — same -// assumed-extension deviation as GRN (see Frontend/PROGRESS.md §5). +// In-memory Stock Core (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { AdjustmentStatus, CreateAdjustmentRequest, StockAdjustment, StockAdjustmentSummary } from "@/types/stock" import { @@ -19,28 +13,6 @@ import { receiveLayer, } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, buildQuery } from "@/lib/api-client" -// -// export interface ListAdjustmentsParams { -// page?: number -// pageSize?: number -// warehouseId?: number -// } -// -// export const stockAdjustmentsApi = { -// list(params: ListAdjustmentsParams = {}) { -// return apiRequest>(`/stock-adjustments${buildQuery(params)}`) -// }, -// get(adjustmentId: number) { -// return apiRequest(`/stock-adjustments/${adjustmentId}`) -// }, -// create(request: CreateAdjustmentRequest) { -// return apiRequest("/stock-adjustments", { method: "POST", body: request }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface ListAdjustmentsParams { page?: number pageSize?: number diff --git a/Frontend/erp-system/lib/api/stock-counts.ts b/Frontend/erp-system/lib/api/stock-counts.ts index a2427f6..21f280e 100644 --- a/Frontend/erp-system/lib/api/stock-counts.ts +++ b/Frontend/erp-system/lib/api/stock-counts.ts @@ -1,11 +1,5 @@ // One typed client method per count endpoint (docs/11-BACKEND-PHASE1.md §5.6). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with the in-memory Stock Core (lib/api/mock-data.ts). Restore the -// commented block and delete the mock block once Backend/PROGRESS.md §5 -// (stock transactions) exists. GET /stock-counts and GET /stock-counts/{id} -// are not documented in docs/11 §5.6 — same assumed-extension deviation as -// GRN (see Frontend/PROGRESS.md §5). +// In-memory Stock Core (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { CountStatus, @@ -31,34 +25,6 @@ import { receiveLayer, } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, buildQuery } from "@/lib/api-client" -// -// export interface ListCountsParams { -// page?: number -// pageSize?: number -// warehouseId?: number -// } -// -// export const stockCountsApi = { -// list(params: ListCountsParams = {}) { -// return apiRequest>(`/stock-counts${buildQuery(params)}`) -// }, -// get(countId: number) { -// return apiRequest(`/stock-counts/${countId}`) -// }, -// create(request: CreateCountRequest) { -// return apiRequest("/stock-counts", { method: "POST", body: request }) -// }, -// enterCounts(countId: number, request: EnterCountsRequest) { -// return apiRequest(`/stock-counts/${countId}/counts`, { method: "PUT", body: request }) -// }, -// post(countId: number) { -// return apiRequest(`/stock-counts/${countId}/post`, { method: "POST", body: {} }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface ListCountsParams { page?: number pageSize?: number diff --git a/Frontend/erp-system/lib/api/stock-transfers.ts b/Frontend/erp-system/lib/api/stock-transfers.ts index b6c1552..80e515a 100644 --- a/Frontend/erp-system/lib/api/stock-transfers.ts +++ b/Frontend/erp-system/lib/api/stock-transfers.ts @@ -1,11 +1,5 @@ // One typed client method per transfer endpoint (docs/11-BACKEND-PHASE1.md §5.4). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with the in-memory Stock Core (lib/api/mock-data.ts). Restore the -// commented block and delete the mock block once Backend/PROGRESS.md §5 -// (stock transactions) exists. GET /stock-transfers and GET /stock-transfers/{id} -// are not documented in docs/11 §5.4 — same assumed-extension deviation as GRN -// (see Frontend/PROGRESS.md §5). +// In-memory Stock Core (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { CreateTransferRequest, @@ -27,39 +21,6 @@ import { receiveLayer, } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, buildQuery } from "@/lib/api-client" -// -// export interface ListTransfersParams { -// page?: number -// pageSize?: number -// status?: TransferStatus -// srcWarehouseId?: number -// destWarehouseId?: number -// } -// -// export const stockTransfersApi = { -// list(params: ListTransfersParams = {}) { -// return apiRequest>(`/stock-transfers${buildQuery(params)}`) -// }, -// get(transferId: number) { -// return apiRequest(`/stock-transfers/${transferId}`) -// }, -// create(request: CreateTransferRequest) { -// return apiRequest("/stock-transfers", { method: "POST", body: request }) -// }, -// dispatch(transferId: number) { -// return apiRequest(`/stock-transfers/${transferId}/dispatch`, { method: "POST", body: {} }) -// }, -// receive(transferId: number, lines: ReceiveTransferLineInput[]) { -// return apiRequest(`/stock-transfers/${transferId}/receive`, { -// method: "POST", -// body: { lines }, -// }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface ListTransfersParams { page?: number pageSize?: number diff --git a/Frontend/erp-system/lib/api/stock.ts b/Frontend/erp-system/lib/api/stock.ts index 58d31be..130983b 100644 --- a/Frontend/erp-system/lib/api/stock.ts +++ b/Frontend/erp-system/lib/api/stock.ts @@ -1,10 +1,5 @@ // One typed client method per stock-enquiry endpoint (docs/11-BACKEND-PHASE1.md §5.1-5.3, §5.7). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with the in-memory Stock Core (lib/api/mock-data.ts) so the Stock -// Management screens can be reviewed without a running backend. Restore the -// commented block and delete the mock block once Backend/PROGRESS.md §4 -// (Stock Core) exists. +// In-memory Stock Core (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { LedgerEntry, OnHand, ReorderAlert, ReorderRequisitionResponse, Valuation } from "@/types/stock" import { @@ -19,44 +14,6 @@ import { mockStockLedger, } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, buildQuery } from "@/lib/api-client" -// -// export interface LedgerQuery { -// itemId?: number -// warehouseId?: number -// from?: string -// to?: string -// page?: number -// pageSize?: number -// } -// -// export const stockApi = { -// onHand(itemId: number, warehouseId: number) { -// return apiRequest(`/stock/on-hand${buildQuery({ itemId, warehouseId })}`) -// }, -// -// ledger(params: LedgerQuery) { -// return apiRequest>(`/stock/ledger${buildQuery(params)}`) -// }, -// -// valuation(itemId: number, warehouseId: number) { -// return apiRequest(`/stock/valuation${buildQuery({ itemId, warehouseId })}`) -// }, -// -// reorderAlerts(warehouseId?: number) { -// return apiRequest>(`/stock/reorder-alerts${buildQuery({ warehouseId })}`) -// }, -// -// createReorderRequisition(itemId: number, warehouseId: number) { -// return apiRequest( -// `/stock/reorder-alerts/${itemId}/requisition${buildQuery({ warehouseId })}`, -// { method: "POST", body: {} } -// ) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface LedgerQuery { itemId?: number warehouseId?: number diff --git a/Frontend/erp-system/lib/api/uoms.ts b/Frontend/erp-system/lib/api/uoms.ts index c6c33b3..318c6c8 100644 --- a/Frontend/erp-system/lib/api/uoms.ts +++ b/Frontend/erp-system/lib/api/uoms.ts @@ -1,27 +1,10 @@ // One typed client method per UOM endpoint (docs/11-BACKEND-PHASE1.md §2.2, FR-MD-02). // `list` also backs the GRN/PO line UOM picker built in earlier sessions. -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts) so these screens -// can be reviewed without a running backend. Restore the commented block and -// delete the mock block once Backend/PROGRESS.md §1 (Master Data) exists. +// In-memory sample data (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { CreateUomRequest, Uom } from "@/types/master-data" import { allocateUomId, mockDelay, mockUoms } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest } from "@/lib/api-client" -// -// export const uomsApi = { -// list() { -// return apiRequest>("/uoms") -// }, -// create(request: CreateUomRequest) { -// return apiRequest("/uoms", { method: "POST", body: request }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export const uomsApi = { list(): Promise> { return mockDelay({ diff --git a/Frontend/erp-system/lib/api/vendors.ts b/Frontend/erp-system/lib/api/vendors.ts index 360c20e..6f06743 100644 --- a/Frontend/erp-system/lib/api/vendors.ts +++ b/Frontend/erp-system/lib/api/vendors.ts @@ -1,14 +1,8 @@ // One typed client method per vendor (supplier) endpoint (docs/11-BACKEND-PHASE1.md -// §2.4, FR-MD-06). "GET/PUT/PATCH follow the Item pattern" per the doc — ETag/If-Match -// on update, PATCH status for deactivate (masters are deactivated, not hard-deleted, -// FR-MD-08). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts) so these screens can -// be reviewed without a running backend. Restore the commented block and delete -// the mock block once Backend/PROGRESS.md §1 (Master Data) exists. -import { ApiResult } from "@/lib/api-client" -import { EntityStatus, PagedResponse } from "@/types/common" +// §2.4, FR-MD-06). ETag/If-Match on update, PATCH status for deactivate (masters +// are deactivated, not hard-deleted, FR-MD-08). In-memory sample data +// (lib/api/mock-data.ts) — no backend API calls. +import { ApiResult, EntityStatus, PagedResponse } from "@/types/common" import { Vendor } from "@/types/master-data" import { allocateVendorId, @@ -19,45 +13,6 @@ import { mockVendors, } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest, apiRequestWithETag, buildQuery } from "@/lib/api-client" -// -// export interface ListVendorsParams { -// page?: number -// pageSize?: number -// q?: string -// status?: EntityStatus -// } -// -// export interface CreateVendorRequest { -// code: string -// name: string -// terms?: string | null -// taxReg?: string | null -// currency: string -// } -// -// export type UpdateVendorRequest = CreateVendorRequest -// -// export const vendorsApi = { -// list(params: ListVendorsParams = {}) { -// return apiRequest>(`/vendors${buildQuery(params)}`) -// }, -// get(vendorId: number) { -// return apiRequestWithETag(`/vendors/${vendorId}`) -// }, -// create(request: CreateVendorRequest) { -// return apiRequestWithETag("/vendors", { method: "POST", body: request }) -// }, -// update(vendorId: number, request: UpdateVendorRequest, ifMatch: string) { -// return apiRequestWithETag(`/vendors/${vendorId}`, { method: "PUT", body: request, ifMatch }) -// }, -// updateStatus(vendorId: number, status: EntityStatus) { -// return apiRequest(`/vendors/${vendorId}/status`, { method: "PATCH", body: { status } }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface ListVendorsParams { page?: number pageSize?: number diff --git a/Frontend/erp-system/lib/api/warehouses.ts b/Frontend/erp-system/lib/api/warehouses.ts index 8f130ec..546acd2 100644 --- a/Frontend/erp-system/lib/api/warehouses.ts +++ b/Frontend/erp-system/lib/api/warehouses.ts @@ -1,47 +1,9 @@ // One typed client method per warehouse/bin endpoint (docs/11-BACKEND-PHASE1.md §2.5, -// FR-MD-07/FR-WH-01). -// -// UI-ONLY MODE: the real fetch-based implementation is commented out below and -// replaced with in-memory sample data (lib/api/mock-data.ts) so the GRN/Stock/ -// Warehouse screens can be reviewed without a running backend. Restore the -// commented block and delete the mock block once Backend/PROGRESS.md §1 -// (Master Data) exists. +// FR-MD-07/FR-WH-01). In-memory sample data (lib/api/mock-data.ts) — no backend API calls. import { PagedResponse } from "@/types/common" import { Bin, Warehouse } from "@/types/master-data" import { allocateBinId, allocateWarehouseId, mockBins, mockDelay, mockWarehouses } from "@/lib/api/mock-data" -// --- Real implementation (restore when the backend exists) -------------------- -// import { apiRequest } from "@/lib/api-client" -// -// export interface CreateWarehouseRequest { -// code: string -// name: string -// } -// -// export interface CreateBinRequest { -// code: string -// binType?: string | null -// } -// -// export const warehousesApi = { -// list() { -// return apiRequest>("/warehouses") -// }, -// -// create(request: CreateWarehouseRequest) { -// return apiRequest("/warehouses", { method: "POST", body: request }) -// }, -// -// listBins(warehouseId: number) { -// return apiRequest>(`/warehouses/${warehouseId}/bins`) -// }, -// -// createBin(warehouseId: number, request: CreateBinRequest) { -// return apiRequest(`/warehouses/${warehouseId}/bins`, { method: "POST", body: request }) -// }, -// } - -// --- Mock implementation (UI-only review) -------------------------------------- export interface CreateWarehouseRequest { code: string name: string diff --git a/Frontend/erp-system/lib/auth-token.ts b/Frontend/erp-system/lib/auth-token.ts deleted file mode 100644 index 56a9b3c..0000000 --- a/Frontend/erp-system/lib/auth-token.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Bearer-token storage for the ERPCore API client (docs/20-FRONTEND.md §1). -// Login (POST /auth/login) is not wired yet — reads simply return null until it is, -// and the API client omits the Authorization header in that case. - -const STORAGE_KEY = "erpcore.accessToken" - -export function getAccessToken(): string | null { - if (typeof window === "undefined") return null - return window.localStorage.getItem(STORAGE_KEY) -} - -export function setAccessToken(token: string | null) { - if (typeof window === "undefined") return - if (token) window.localStorage.setItem(STORAGE_KEY, token) - else window.localStorage.removeItem(STORAGE_KEY) -} diff --git a/Frontend/erp-system/lib/error-map.ts b/Frontend/erp-system/lib/error-map.ts index 87a4528..4e72ac2 100644 --- a/Frontend/erp-system/lib/error-map.ts +++ b/Frontend/erp-system/lib/error-map.ts @@ -1,7 +1,12 @@ // code -> user-facing message (docs/20-FRONTEND.md §3.2: "Keep a code -> message // map in lib/ so messages are consistent"). Falls back to the ProblemDetails // title/detail from the server when a code isn't in the map. -import { ApiError } from "@/lib/api-client" + +interface ApiErrorLike { + code?: string + detail?: string + errors?: Record +} const CODE_MESSAGES: Record = { OVER_RECEIPT_TOLERANCE: "This quantity exceeds the purchase order's open quantity beyond the allowed tolerance.", @@ -21,21 +26,21 @@ const CODE_MESSAGES: Record = { } export function errorMessage(error: unknown): string { - if (error instanceof ApiError) { - if (error.code && CODE_MESSAGES[error.code]) return CODE_MESSAGES[error.code] - return error.detail || error.message || "Something went wrong." + if (error && typeof error === "object") { + const e = error as ApiErrorLike + if (e.code && CODE_MESSAGES[e.code]) return CODE_MESSAGES[e.code] + if (e.detail) return e.detail } if (error instanceof Error) return error.message return "Something went wrong." } export function fieldErrors(error: unknown): Record | null { - if (error instanceof ApiError && error.errors) { - const out: Record = {} - for (const [field, messages] of Object.entries(error.errors)) { - out[field] = messages[0] - } - return out + const errors = error && typeof error === "object" ? (error as ApiErrorLike).errors : undefined + if (!errors) return null + const out: Record = {} + for (const [field, messages] of Object.entries(errors)) { + out[field] = messages[0] } - return null + return out } diff --git a/Frontend/erp-system/types/common.ts b/Frontend/erp-system/types/common.ts index 9c614bd..52c4f6b 100644 --- a/Frontend/erp-system/types/common.ts +++ b/Frontend/erp-system/types/common.ts @@ -12,7 +12,13 @@ export interface PagedResponse { pagination: PaginationMeta } -/** RFC 7807 ProblemDetails, normalized by lib/api-client.ts. */ +/** A resource plus its concurrency token (docs/11 §1.6 ETag/If-Match), e.g. `vendorsApi.get()`. */ +export interface ApiResult { + data: T + etag: string | null +} + +/** RFC 7807 ProblemDetails shape (docs/11-BACKEND-PHASE1.md §1.8). */ export interface ProblemDetails { type?: string title: string