Dev #7

Merged
ImanThiyanga merged 14 commits from Dev into production 2026-07-15 10:08:08 +00:00
24 changed files with 59 additions and 655 deletions
Showing only changes of commit 0e4bcf174b - Show all commits
+8 -4
View File
@@ -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<T>` (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<T>` — 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
@@ -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))
@@ -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))
+1 -2
View File
@@ -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))
-98
View File
@@ -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<string, string[]>
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<RequestInit, "body"> {
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<T> {
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<Response> {
const { body, ifMatch, idempotencyKey, headers, ...rest } = options
const token = getAccessToken()
const finalHeaders: Record<string, string> = {
Accept: "application/json",
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(ifMatch ? { "If-Match": ifMatch } : {}),
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
...((headers as Record<string, string> | 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<T>(path: string, options?: RequestOptions): Promise<T> {
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<T>(path: string, options?: RequestOptions): Promise<ApiResult<T>> {
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}` : ""
}
+1 -21
View File
@@ -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<PagedResponse<Category>>("/categories")
// },
// tree() {
// return apiRequest<CategoryTreeNode[]>("/categories?tree=true")
// },
// create(request: CreateCategoryRequest) {
// return apiRequest<Category>("/categories", { method: "POST", body: request })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
function buildTree(categories: Category[]): CategoryTreeNode[] {
const nodes = new Map<number, CategoryTreeNode>(categories.map((c) => [c.categoryId, { ...c, children: [] }]))
const roots: CategoryTreeNode[] = []
+3 -62
View File
@@ -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<PagedResponse<GrnSummary>>(`/grns${buildQuery(params)}`)
// },
//
// get(grnId: number) {
// return apiRequest<Grn>(`/grns/${grnId}`)
// },
//
// async create(request: CreateGrnRequest) {
// const { data } = await apiRequestWithETag<Grn>("/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<Grn>(`/grns/${grnId}`, { method: "PUT", body: request, ifMatch })
// return data
// },
//
// remove(grnId: number) {
// return apiRequest<void>(`/grns/${grnId}`, { method: "DELETE" })
// },
//
// confirm(grnId: number, idempotencyKey?: string) {
// return apiRequest<ConfirmGrnResponse>(`/grns/${grnId}/confirm`, {
// method: "POST",
// body: {},
// idempotencyKey,
// })
// },
//
// releaseLine(grnId: number, grnLineId: number, action: ReleaseAction) {
// return apiRequest<ReleaseGrnLineResponse>(`/grns/${grnId}/lines/${grnLineId}/release`, {
// method: "POST",
// body: { action },
// })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface ListGrnsParams {
page?: number
pageSize?: number
+2 -44
View File
@@ -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<PagedResponse<ItemListItem>>(`/items${buildQuery(params)}`)
// },
// get(itemId: number) {
// return apiRequestWithETag<Item>(`/items/${itemId}`)
// },
// create(request: CreateItemRequest) {
// return apiRequestWithETag<Item>("/items", { method: "POST", body: request })
// },
// update(itemId: number, request: UpdateItemRequest, ifMatch: string) {
// return apiRequestWithETag<Item>(`/items/${itemId}`, { method: "PUT", body: request, ifMatch })
// },
// updateStatus(itemId: number, status: EntityStatus) {
// return apiRequest<void>(`/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<UpdateUomConversionsResponse>(`/items/${itemId}/uom-conversions`, { method: "PUT", body: request })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface ListItemsParams {
page?: number
pageSize?: number
+3 -6
View File
@@ -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"
+2 -41
View File
@@ -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<PagedResponse<PurchaseOrderSummary>>(`/purchase-orders${buildQuery(params)}`)
// },
// get(poId: number) {
// return apiRequest<PurchaseOrder>(`/purchase-orders/${poId}`)
// },
// getWithETag(poId: number) {
// return apiRequestWithETag<PurchaseOrder>(`/purchase-orders/${poId}`)
// },
// create(request: CreatePurchaseOrderRequest) {
// return apiRequestWithETag<PurchaseOrder>("/purchase-orders", { method: "POST", body: request })
// },
// update(poId: number, request: UpdatePurchaseOrderRequest, ifMatch: string) {
// return apiRequestWithETag<PurchaseOrder>(`/purchase-orders/${poId}`, { method: "PUT", body: request, ifMatch })
// },
// cancel(poId: number, request: CancelPurchaseOrderRequest) {
// return apiRequest<PurchaseOrder>(`/purchase-orders/${poId}/cancel`, { method: "POST", body: request })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface ListPurchaseOrdersParams {
page?: number
pageSize?: number
@@ -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<PagedResponse<PurchaseReturnSummary>>("/purchase-returns")
// },
// get(returnId: number) {
// return apiRequest<PurchaseReturn>(`/purchase-returns/${returnId}`)
// },
// create(request: CreatePurchaseReturnRequest) {
// return apiRequest<PurchaseReturn>("/purchase-returns", { method: "POST", body: request })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
function toSummary(r: PurchaseReturn): PurchaseReturnSummary {
return {
returnId: r.returnId,
+1 -13
View File
@@ -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<PagedResponse<ReasonCode>>(`/reason-codes${buildQuery({ context })}`)
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export const reasonCodesApi = {
list(context?: ReasonCodeContext): Promise<PagedResponse<ReasonCode>> {
const items = mockReasonCodes.filter((r) => !context || r.context === context)
+1 -30
View File
@@ -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<PagedResponse<RequisitionSummary>>(`/requisitions${buildQuery(params)}`)
// },
// get(requisitionId: number) {
// return apiRequest<Requisition>(`/requisitions/${requisitionId}`)
// },
// create(request: CreateRequisitionRequest) {
// return apiRequest<Requisition>("/requisitions", { method: "POST", body: request })
// },
// submit(requisitionId: number) {
// return apiRequest<Requisition>(`/requisitions/${requisitionId}/submit`, { method: "POST", body: {} })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface ListRequisitionsParams {
page?: number
pageSize?: number
+1 -27
View File
@@ -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<PagedResponse<RfqSummary>>("/rfqs")
// },
// get(rfqId: number) {
// return apiRequest<Rfq>(`/rfqs/${rfqId}`)
// },
// create(request: CreateRfqRequest) {
// return apiRequest<Rfq>("/rfqs", { method: "POST", body: request })
// },
// addQuotation(rfqId: number, request: CreateQuotationRequest) {
// return apiRequest<Quotation>(`/rfqs/${rfqId}/quotations`, { method: "POST", body: request })
// },
// comparison(rfqId: number) {
// return apiRequest<RfqComparison>(`/rfqs/${rfqId}/comparison`)
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
function toSummary(r: Rfq): RfqSummary {
return {
rfqId: r.rfqId,
@@ -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<PagedResponse<StockAdjustmentSummary>>(`/stock-adjustments${buildQuery(params)}`)
// },
// get(adjustmentId: number) {
// return apiRequest<StockAdjustment>(`/stock-adjustments/${adjustmentId}`)
// },
// create(request: CreateAdjustmentRequest) {
// return apiRequest<StockAdjustment>("/stock-adjustments", { method: "POST", body: request })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface ListAdjustmentsParams {
page?: number
pageSize?: number
+1 -35
View File
@@ -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<PagedResponse<StockCountSummary>>(`/stock-counts${buildQuery(params)}`)
// },
// get(countId: number) {
// return apiRequest<StockCount>(`/stock-counts/${countId}`)
// },
// create(request: CreateCountRequest) {
// return apiRequest<StockCount>("/stock-counts", { method: "POST", body: request })
// },
// enterCounts(countId: number, request: EnterCountsRequest) {
// return apiRequest<EnterCountsResponse>(`/stock-counts/${countId}/counts`, { method: "PUT", body: request })
// },
// post(countId: number) {
// return apiRequest<PostCountResponse>(`/stock-counts/${countId}/post`, { method: "POST", body: {} })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface ListCountsParams {
page?: number
pageSize?: number
+1 -40
View File
@@ -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<PagedResponse<StockTransferSummary>>(`/stock-transfers${buildQuery(params)}`)
// },
// get(transferId: number) {
// return apiRequest<StockTransfer>(`/stock-transfers/${transferId}`)
// },
// create(request: CreateTransferRequest) {
// return apiRequest<StockTransfer>("/stock-transfers", { method: "POST", body: request })
// },
// dispatch(transferId: number) {
// return apiRequest<DispatchTransferResponse>(`/stock-transfers/${transferId}/dispatch`, { method: "POST", body: {} })
// },
// receive(transferId: number, lines: ReceiveTransferLineInput[]) {
// return apiRequest<ReceiveTransferResponse>(`/stock-transfers/${transferId}/receive`, {
// method: "POST",
// body: { lines },
// })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface ListTransfersParams {
page?: number
pageSize?: number
+1 -44
View File
@@ -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<OnHand>(`/stock/on-hand${buildQuery({ itemId, warehouseId })}`)
// },
//
// ledger(params: LedgerQuery) {
// return apiRequest<PagedResponse<LedgerEntry>>(`/stock/ledger${buildQuery(params)}`)
// },
//
// valuation(itemId: number, warehouseId: number) {
// return apiRequest<Valuation>(`/stock/valuation${buildQuery({ itemId, warehouseId })}`)
// },
//
// reorderAlerts(warehouseId?: number) {
// return apiRequest<PagedResponse<ReorderAlert>>(`/stock/reorder-alerts${buildQuery({ warehouseId })}`)
// },
//
// createReorderRequisition(itemId: number, warehouseId: number) {
// return apiRequest<ReorderRequisitionResponse>(
// `/stock/reorder-alerts/${itemId}/requisition${buildQuery({ warehouseId })}`,
// { method: "POST", body: {} }
// )
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface LedgerQuery {
itemId?: number
warehouseId?: number
+1 -18
View File
@@ -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<PagedResponse<Uom>>("/uoms")
// },
// create(request: CreateUomRequest) {
// return apiRequest<Uom>("/uoms", { method: "POST", body: request })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export const uomsApi = {
list(): Promise<PagedResponse<Uom>> {
return mockDelay({
+4 -49
View File
@@ -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<PagedResponse<Vendor>>(`/vendors${buildQuery(params)}`)
// },
// get(vendorId: number) {
// return apiRequestWithETag<Vendor>(`/vendors/${vendorId}`)
// },
// create(request: CreateVendorRequest) {
// return apiRequestWithETag<Vendor>("/vendors", { method: "POST", body: request })
// },
// update(vendorId: number, request: UpdateVendorRequest, ifMatch: string) {
// return apiRequestWithETag<Vendor>(`/vendors/${vendorId}`, { method: "PUT", body: request, ifMatch })
// },
// updateStatus(vendorId: number, status: EntityStatus) {
// return apiRequest<void>(`/vendors/${vendorId}/status`, { method: "PATCH", body: { status } })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface ListVendorsParams {
page?: number
pageSize?: number
+1 -39
View File
@@ -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<PagedResponse<Warehouse>>("/warehouses")
// },
//
// create(request: CreateWarehouseRequest) {
// return apiRequest<Warehouse>("/warehouses", { method: "POST", body: request })
// },
//
// listBins(warehouseId: number) {
// return apiRequest<PagedResponse<Bin>>(`/warehouses/${warehouseId}/bins`)
// },
//
// createBin(warehouseId: number, request: CreateBinRequest) {
// return apiRequest<Bin>(`/warehouses/${warehouseId}/bins`, { method: "POST", body: request })
// },
// }
// --- Mock implementation (UI-only review) --------------------------------------
export interface CreateWarehouseRequest {
code: string
name: string
-16
View File
@@ -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)
}
+16 -11
View File
@@ -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<string, string[]>
}
const CODE_MESSAGES: Record<string, string> = {
OVER_RECEIPT_TOLERANCE: "This quantity exceeds the purchase order's open quantity beyond the allowed tolerance.",
@@ -21,21 +26,21 @@ const CODE_MESSAGES: Record<string, string> = {
}
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<string, string> | null {
if (error instanceof ApiError && error.errors) {
const out: Record<string, string> = {}
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<string, string> = {}
for (const [field, messages] of Object.entries(errors)) {
out[field] = messages[0]
}
return null
return out
}
+7 -1
View File
@@ -12,7 +12,13 @@ export interface PagedResponse<T> {
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<T> {
data: T
etag: string | null
}
/** RFC 7807 ProblemDetails shape (docs/11-BACKEND-PHASE1.md §1.8). */
export interface ProblemDetails {
type?: string
title: string