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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user