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
@@ -0,0 +1,21 @@
// Access configuration. Singleton - no id. The default role is what a first sign-in (or a person with no role) receives.
import { apiRequestWithETag } from "@/lib/api-client"
import { ApiResult } from "@/types/common"
export interface AccessConfig {
defaultRoleId: number | null
defaultRoleName: string | null
updatedAt: string | null
updatedBy: number | null
}
export const accessConfigApi = {
get(): Promise<ApiResult<AccessConfig>> {
return apiRequestWithETag<AccessConfig>("/access-config")
},
/** `null` clears the default (people without a role then have no permissions at all). The administrator role is refused. */
update(defaultRoleId: number | null, ifMatch: string): Promise<ApiResult<AccessConfig>> {
return apiRequestWithETag<AccessConfig>("/access-config", { method: "PUT", body: { defaultRoleId }, ifMatch })
},
}
+5
View File
@@ -27,6 +27,11 @@ export interface ListGrnsParams {
vendorId?: number
warehouseId?: number
branchId?: number
/** Created on or after this day (YYYY-MM-DD). */
from?: string
/** Created on or before this day (YYYY-MM-DD). */
to?: string
/** `created` (default, newest first) | `vendor` | `total` | `status`; a leading `-` flips the direction. */
sort?: string
}
@@ -0,0 +1,40 @@
// Opening balances. post() creates ONE journal in the General Ledger (a debit per cash/bank account, the
// offsetting credit to "Opening Balance Equity") and can be done only once: a second call is 409
// OPENING_BALANCE_ALREADY_POSTED.
import { apiRequest } from "@/lib/api-client"
import {
CashBankOpeningState,
OpeningBalanceRun,
OpeningStockRun,
OpeningStockRunSummary,
PostCashBankOpeningRequest,
PostOpeningStockRequest,
} from "@/types/opening-balances"
export const openingBalancesApi = {
/** The cash/bank accounts to enter amounts for — or the posted record, once it has been done. */
cashBank(): Promise<CashBankOpeningState> {
return apiRequest<CashBankOpeningState>("/opening-balances/cash-bank")
},
postCashBank(request: PostCashBankOpeningRequest): Promise<OpeningBalanceRun> {
return apiRequest<OpeningBalanceRun>("/opening-balances/cash-bank", { method: "POST", body: request })
},
/** Opening-stock runs posted so far, newest first. */
stockRuns(): Promise<OpeningStockRunSummary[]> {
return apiRequest<OpeningStockRunSummary[]>("/opening-balances/stock")
},
stockRun(runId: number): Promise<OpeningStockRun> {
return apiRequest<OpeningStockRun>(`/opening-balances/stock/${runId}`)
},
/**
* Creates a FIFO layer per line and ONE journal (Dr Inventory / Cr Opening Balance Equity). 409 when an item
* already has stock movements in that warehouse; untracked, stocked items only.
*/
postStock(request: PostOpeningStockRequest): Promise<OpeningStockRun> {
return apiRequest<OpeningStockRun>("/opening-balances/stock", { method: "POST", body: request })
},
}
@@ -0,0 +1,22 @@
// Procurement configuration. Singleton - no id. Controls the optional vendor contact / address
// fields and whether purchase orders carry tax. The server enforces every flag too: a value sent
// for a switched-off field returns 422 CONFIG_DISABLED, so the UI hides what is off.
import { apiRequest, apiRequestWithETag } from "@/lib/api-client"
import { ApiResult } from "@/types/common"
import { ProcurementConfig, UpdateProcurementConfigRequest } from "@/types/procurement"
export const procurementConfigApi = {
get(): Promise<ApiResult<ProcurementConfig>> {
return apiRequestWithETag<ProcurementConfig>("/procurement-config")
},
/** Full replacement — every flag is required. */
update(request: UpdateProcurementConfigRequest, ifMatch: string): Promise<ApiResult<ProcurementConfig>> {
return apiRequestWithETag<ProcurementConfig>("/procurement-config", { method: "PUT", body: request, ifMatch })
},
}
/** Read-only helper for screens that only need the flags. */
export function procurementConfig(): Promise<ProcurementConfig> {
return apiRequest<ProcurementConfig>("/procurement-config")
}
@@ -18,13 +18,18 @@ export interface ListPurchaseOrdersParams {
q?: string
status?: PurchaseOrderStatus
vendorId?: number
/** Created on or after this day (YYYY-MM-DD). */
from?: string
/** Created on or before this day (YYYY-MM-DD). */
to?: string
/** `created` (default, newest first) | `vendor` | `total` | `status`; a leading `-` flips the direction. */
sort?: string
}
/** Editable/deletable only while Draft (FR-PROC-05, revised — submitting locks the PO).
/** Editable/deletable only while Draft or Rejected (FR-PROC-05, revised — submitting locks the PO; a rejected one is reopened for revision).
* The server is authoritative (409 PO_NOT_EDITABLE otherwise); this only drives UI affordances. */
export function isPoEditable(status: PurchaseOrderStatus): boolean {
return status === "Draft"
return status === "Draft" || status === "Rejected"
}
export const purchaseOrdersApi = {
+2
View File
@@ -21,6 +21,8 @@ export interface ListUsersParams {
q?: string
status?: EntityStatus
roleCode?: string
/** true = only administrators (users whose role is the administrator role, whatever it is called). */
isAdmin?: boolean
}
export const usersApi = {