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:
Dhananjaya99
2026-10-18 12:46:31 +05:30
parent c27db06880
commit e7b1f270a5
202 changed files with 99906 additions and 1894 deletions
+20 -2
View File
@@ -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