feat: add warehouse and bin management API with mock data implementation

- Implemented warehouses API with methods for listing, creating, and managing bins.
- Added wastage API to handle stock write-offs and integrate with stock adjustments.
- Created auth token management for storing and retrieving access tokens.
- Developed error mapping for consistent user-facing error messages.
- Introduced client-side validations for GRN, master data, and procurement processes.
- Defined common types for pagination, problem details, and various master data entities.
- Established procurement and stock management types to support frontend functionality.
This commit is contained in:
2026-07-13 17:11:40 +05:30
parent 4e84a15db7
commit 7ac30bb454
79 changed files with 14042 additions and 44 deletions
+98
View File
@@ -0,0 +1,98 @@
// 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}` : ""
}