feat: add vendor optional fields and configuration hooks
- Implemented VendorOptionalFields component to handle optional vendor details (mobile, email, address) with validation. - Created useCustomerConfig hook to load optional customer field settings from Sales configuration. - Developed useProcurementConfig hook to manage Procurement configuration flags. - Introduced accessConfigApi for managing access configuration settings. - Added openingBalancesApi for handling opening balances operations. - Created procurementConfigApi for managing procurement configuration settings. - Implemented nav-routes for permission-based routing. - Developed PDF generation for purchase orders with dynamic vendor and item details. - Added purchase-order tax helpers for tax calculations and formatting. - Defined types for opening balances and related operations.
This commit is contained in:
@@ -12,6 +12,9 @@ import { ApiResult, ProblemDetails } from "@/types/common"
|
||||
|
||||
const API_BASE = "/api/v1"
|
||||
|
||||
/** Fired on window whenever the API answers 403, so the auth context can re-read the person's permissions. */
|
||||
export const FORBIDDEN_EVENT = "erp:forbidden"
|
||||
|
||||
/** Normalized RFC 7807 error (docs/11-BACKEND-PHASE1.md §1.8) thrown on any non-2xx response. */
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
@@ -36,7 +39,7 @@ export interface RequestOptions extends Omit<RequestInit, "body"> {
|
||||
ifMatch?: string
|
||||
/** Sent as Idempotency-Key for transactional POSTs (e.g. GRN confirm, docs/11 §1.6). */
|
||||
idempotencyKey?: string
|
||||
/** Sent as X-XSRF-TOKEN. Only the [ValidateCsrf] actions on AuthController need it (docs/11 §2.0). */
|
||||
/** Sent as X-XSRF-TOKEN. Now sent automatically on every POST/PUT/PATCH/DELETE (the API checks all of them); kept so a caller can force it. */
|
||||
csrf?: boolean
|
||||
}
|
||||
|
||||
@@ -78,7 +81,11 @@ function redirectToLogin() {
|
||||
|
||||
async function rawRequest(path: string, options: RequestOptions = {}, isRetry = false): Promise<Response> {
|
||||
const { body, ifMatch, idempotencyKey, csrf, headers, ...rest } = options
|
||||
const csrfToken = csrf ? readCsrfToken() : null
|
||||
// The API double-submit-checks every state-changing request that rides on the session cookie, so send the token
|
||||
// on all of them (it is just echoing a cookie of our own origin).
|
||||
const method = (rest.method ?? "GET").toUpperCase()
|
||||
const stateChanging = method !== "GET" && method !== "HEAD" && method !== "OPTIONS"
|
||||
const csrfToken = csrf || stateChanging ? readCsrfToken() : null
|
||||
// Multipart uploads (attendance files, staff documents) pass a FormData body — the
|
||||
// browser sets its own Content-Type (with boundary), and it must never be JSON-encoded.
|
||||
const isFormData = typeof FormData !== "undefined" && body instanceof FormData
|
||||
@@ -105,7 +112,18 @@ async function rawRequest(path: string, options: RequestOptions = {}, isRetry =
|
||||
redirectToLogin()
|
||||
}
|
||||
|
||||
// A session older than the CSRF check has no XSRF-TOKEN cookie (or it rotated): renewing the session re-issues it.
|
||||
// Peek at a clone so the normal error path below still reads the body.
|
||||
if (response.status === 403 && !isRetry && !path.startsWith("/auth/")) {
|
||||
const peek = await response.clone().json().catch(() => null) as ProblemDetails | null
|
||||
if (peek?.code === "CSRF_TOKEN_MISMATCH" && (await refreshSession())) return rawRequest(path, options, true)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403 && typeof window !== "undefined") {
|
||||
// The role may have changed under this tab: let the auth context re-read what the person can still do.
|
||||
window.dispatchEvent(new CustomEvent(FORBIDDEN_EVENT))
|
||||
}
|
||||
let problem: ProblemDetails
|
||||
try {
|
||||
problem = (await response.json()) as ProblemDetails
|
||||
|
||||
Reference in New Issue
Block a user