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
@@ -0,0 +1,57 @@
// Client-side UX validation only (docs/20-FRONTEND.md §3.1) — required fields,
// format/range checks the browser can already see. Server-authoritative rules
// (over-receipt tolerance, referential existence, concurrency) are never
// re-implemented here; the server's ProblemDetails is the final word (§3.3).
import { z } from "zod"
import { TrackingMode } from "@/types/master-data"
export const grnHeaderSchema = z.object({
warehouseId: z.number({ error: "Select a warehouse" }).positive("Select a warehouse"),
vendorId: z.number().nullable(),
poId: z.number().nullable(),
})
export function validateLine(input: {
itemId: number | null
uomId: number | null
qty: string
unitCost: string
trackingMode: TrackingMode | null
batchNo: string
serialNumbersText: string
}): Record<string, string> {
const errors: Record<string, string> = {}
if (!input.itemId) errors.itemId = "Select an item"
if (!input.uomId) errors.uomId = "Select a UOM"
const qty = Number(input.qty)
if (!input.qty || Number.isNaN(qty) || qty <= 0) errors.qty = "Quantity must be greater than 0"
const unitCost = Number(input.unitCost)
if (input.unitCost === "" || Number.isNaN(unitCost) || unitCost < 0) errors.unitCost = "Unit cost cannot be negative"
if (input.trackingMode === "Batch" && !input.batchNo.trim()) {
errors.batchNo = "Batch number is required for this item"
}
if (input.trackingMode === "Serial") {
const serials = splitSerials(input.serialNumbersText)
if (serials.length === 0) {
errors.serialNumbers = "Enter one serial number per unit"
} else if (!Number.isNaN(qty) && serials.length !== qty) {
errors.serialNumbers = `Enter exactly ${qty || 0} serial number(s) — got ${serials.length}`
} else if (new Set(serials).size !== serials.length) {
errors.serialNumbers = "Serial numbers must be unique"
}
}
return errors
}
export function splitSerials(text: string): string[] {
return text
.split(/[\n,]/)
.map((s) => s.trim())
.filter((s) => s.length > 0)
}
@@ -0,0 +1,52 @@
// Client-side UX validation only (docs/20-FRONTEND.md §3.1) — required fields,
// format/range checks the browser can already see. Server-authoritative rules
// (SKU uniqueness, master-in-use on delete, concurrency) are never
// re-implemented here; the server's ProblemDetails is the final word (§3.3).
// Hand-rolled (not zod) to match the plain-hooks posture most screens this
// session use — lib/validations/grn.ts is the one deliberate zod exception.
export function validateItemForm(input: {
sku: string
name: string
categoryId: number | null
baseUomId: number | null
}): Record<string, string> {
const errors: Record<string, string> = {}
if (!input.sku.trim()) errors.sku = "SKU is required"
if (!input.name.trim()) errors.name = "Item name is required"
if (!input.categoryId) errors.categoryId = "Select a category"
if (!input.baseUomId) errors.baseUomId = "Select a base UOM"
return errors
}
export function validateReorderLine(input: { warehouseId: number | null; reorderPoint: string; reorderQty: string }): Record<string, string> {
const errors: Record<string, string> = {}
if (!input.warehouseId) errors.warehouseId = "Select a warehouse"
const point = Number(input.reorderPoint)
if (input.reorderPoint === "" || Number.isNaN(point) || point < 0) errors.reorderPoint = "Reorder point cannot be negative"
const qty = Number(input.reorderQty)
if (!input.reorderQty || Number.isNaN(qty) || qty <= 0) errors.reorderQty = "Reorder quantity must be greater than 0"
return errors
}
export function validateConversionLine(input: { fromUom: number | null; toUom: number | null; factor: string }): Record<string, string> {
const errors: Record<string, string> = {}
if (!input.fromUom) errors.fromUom = "Select a UOM"
if (!input.toUom) errors.toUom = "Select a UOM"
if (input.fromUom && input.toUom && input.fromUom === input.toUom) errors.toUom = "From and to UOM must differ"
const factor = Number(input.factor)
if (!input.factor || Number.isNaN(factor) || factor <= 0) errors.factor = "Factor must be greater than 0"
return errors
}
export function validateUomName(name: string): Record<string, string> {
const errors: Record<string, string> = {}
if (!name.trim()) errors.name = "UOM name is required"
return errors
}
export function validateCategoryName(name: string): Record<string, string> {
const errors: Record<string, string> = {}
if (!name.trim()) errors.name = "Category name is required"
return errors
}
@@ -0,0 +1,63 @@
// Client-side UX validation only (docs/20-FRONTEND.md §3.1) — required fields,
// format/range checks the browser can already see. Server-authoritative rules
// (referential existence, PO editability, concurrency, stock sufficiency on
// returns) are never re-implemented here; the server's ProblemDetails is the
// final word (§3.3). Same pattern as lib/validations/grn.ts.
export function validateRequisitionLine(input: { itemId: number | null; qty: string; requiredBy: string }): Record<string, string> {
const errors: Record<string, string> = {}
if (!input.itemId) errors.itemId = "Select an item"
const qty = Number(input.qty)
if (!input.qty || Number.isNaN(qty) || qty <= 0) errors.qty = "Quantity must be greater than 0"
if (!input.requiredBy) errors.requiredBy = "Required-by date is needed"
return errors
}
export function validateRfqLine(input: { itemId: number | null; qty: string }): Record<string, string> {
const errors: Record<string, string> = {}
if (!input.itemId) errors.itemId = "Select an item"
const qty = Number(input.qty)
if (!input.qty || Number.isNaN(qty) || qty <= 0) errors.qty = "Quantity must be greater than 0"
return errors
}
export function validateQuotationLine(input: { unitPrice: string; leadDays: string }): Record<string, string> {
const errors: Record<string, string> = {}
const unitPrice = Number(input.unitPrice)
if (input.unitPrice === "" || Number.isNaN(unitPrice) || unitPrice < 0) errors.unitPrice = "Unit price cannot be negative"
const leadDays = Number(input.leadDays)
if (input.leadDays === "" || Number.isNaN(leadDays) || leadDays < 0) errors.leadDays = "Lead days cannot be negative"
return errors
}
export function validatePoLine(input: {
itemId: number | null
uomId: number | null
warehouseId: number | null
qty: string
unitPrice: string
tax: string
}): Record<string, string> {
const errors: Record<string, string> = {}
if (!input.itemId) errors.itemId = "Select an item"
if (!input.uomId) errors.uomId = "Select a UOM"
if (!input.warehouseId) errors.warehouseId = "Select a warehouse"
const qty = Number(input.qty)
if (!input.qty || Number.isNaN(qty) || qty <= 0) errors.qty = "Quantity must be greater than 0"
const unitPrice = Number(input.unitPrice)
if (input.unitPrice === "" || Number.isNaN(unitPrice) || unitPrice < 0) errors.unitPrice = "Unit price cannot be negative"
const tax = Number(input.tax)
if (input.tax === "" || Number.isNaN(tax) || tax < 0) errors.tax = "Tax rate cannot be negative"
return errors
}
export function validateReturnLine(input: { grnLineId: number | null; qty: string; maxQty: number | null }): Record<string, string> {
const errors: Record<string, string> = {}
if (!input.grnLineId) errors.grnLineId = "Select a received line"
const qty = Number(input.qty)
if (!input.qty || Number.isNaN(qty) || qty <= 0) errors.qty = "Quantity must be greater than 0"
// Client-side sanity bound on the originally received qty — the server is still
// authoritative on live available stock (STOCK_NEGATIVE_BLOCKED, docs/20 §3.1).
if (input.maxQty !== null && qty > input.maxQty) errors.qty = `Cannot exceed the received quantity (${input.maxQty})`
return errors
}