serial numbers

This commit is contained in:
2026-10-07 14:00:32 +05:30
committed by ImanThiyanga
parent 3abcfa6e6f
commit a788349138
89 changed files with 15320 additions and 216 deletions
+89
View File
@@ -0,0 +1,89 @@
// Item tracking endpoints — pickers (/tracking), serial register (/serials), warranty register
// (/warranties) and batch stock (/stock/batches). docs/ITEM_TRACKING_PLAN.md.
import { apiRequest, buildQuery } from "@/lib/api-client"
import { PagedResponse } from "@/types/common"
import {
AvailableBatch,
AvailableSerial,
BatchStock,
BatchSuggestion,
SerialDetail,
SerialStatus,
SerialSummary,
TrackingSettings,
WarrantyRegistration,
WarrantyStatus,
} from "@/types/tracking"
export interface SerialListParams {
q?: string
itemId?: number
warehouseId?: number
status?: SerialStatus
page?: number
pageSize?: number
}
export interface WarrantyListParams {
q?: string
customerId?: number
itemId?: number
serialNo?: string
status?: WarrantyStatus
expiringWithinDays?: number
expired?: boolean
page?: number
pageSize?: number
}
export interface BatchStockParams {
q?: string
itemId?: number
warehouseId?: number
expiringWithinDays?: number
page?: number
pageSize?: number
}
export const trackingApi = {
settings() {
return apiRequest<TrackingSettings>("/tracking/settings")
},
/** Next free serial numbers for an item under a prefix (auto-generate on GRN). */
nextSerialNumbers(itemId: number, prefix: string, count: number) {
return apiRequest<string[]>(`/tracking/serials/next-numbers${buildQuery({ itemId, prefix, count })}`)
},
availableSerials(itemId: number, warehouseId: number, q?: string) {
return apiRequest<AvailableSerial[]>(`/tracking/serials/available${buildQuery({ itemId, warehouseId, q })}`)
},
availableBatches(itemId: number, warehouseId: number) {
return apiRequest<AvailableBatch[]>(`/tracking/batches/available${buildQuery({ itemId, warehouseId })}`)
},
suggestBatches(itemId: number, warehouseId: number, qty: number) {
return apiRequest<BatchSuggestion[]>(`/tracking/batches/suggest${buildQuery({ itemId, warehouseId, qty })}`)
},
listSerials(params: SerialListParams = {}) {
return apiRequest<PagedResponse<SerialSummary>>(`/serials${buildQuery(params)}`)
},
getSerial(serialId: number) {
return apiRequest<SerialDetail>(`/serials/${serialId}`)
},
lookupSerial(serialNo: string) {
return apiRequest<SerialDetail[]>(`/serials/lookup${buildQuery({ serialNo })}`)
},
listWarranties(params: WarrantyListParams = {}) {
return apiRequest<PagedResponse<WarrantyRegistration>>(`/warranties${buildQuery(params)}`)
},
listBatchStock(params: BatchStockParams = {}) {
return apiRequest<PagedResponse<BatchStock>>(`/stock/batches${buildQuery(params)}`)
},
}
+15
View File
@@ -0,0 +1,15 @@
import type { SuggestTagTone } from "@/components/ui/suggest-input"
import type { TrackingMode } from "@/types/master-data"
/**
* Highlighted tracking tag for an item in a dropdown: "Serial" or "Batch". A Serial item gets no
* tag while serial numbers are switched off in Product Configuration — it is plain stock then.
*/
export function trackingTags(
trackingMode: TrackingMode,
serialNumbersEnabled: boolean | null,
): { label: string; tone: SuggestTagTone }[] {
if (trackingMode === "Serial" && serialNumbersEnabled !== false) return [{ label: "Serial", tone: "serial" }]
if (trackingMode === "Batch") return [{ label: "Batch", tone: "batch" }]
return []
}
+14 -5
View File
@@ -3,7 +3,7 @@
// (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 { Warranty } from "@/types/master-data"
import { TrackingMode, Warranty } from "@/types/master-data"
export const grnHeaderSchema = z.object({
warehouseId: z.number({ error: "Select a warehouse" }).positive("Select a warehouse"),
@@ -18,6 +18,9 @@ export function validateLine(input: {
discountPct: string
vatPct: string
warranty: Warranty | null
trackingMode: TrackingMode | null
batchNo: string
/** Per-unit numbers: serial numbers for a Serial item, else warranty numbers for a Warranty item. */
warrantyNumbers: string[]
}): Record<string, string> {
const errors: Record<string, string> = {}
@@ -38,14 +41,20 @@ export function validateLine(input: {
if (input.vatPct !== "" && (Number.isNaN(vatPct) || vatPct < 0 || vatPct > 100))
errors.vatPct = "VAT must be 0–100%"
if (input.warranty === "Warranty") {
if (input.trackingMode === "Batch" && !input.batchNo.trim()) errors.batchNo = "Enter the batch number"
const serial = input.trackingMode === "Serial"
if (serial && !Number.isNaN(qty) && !Number.isInteger(qty)) errors.qty = "Serial-tracked items are received in whole units"
if (serial || input.warranty === "Warranty") {
const label = serial ? "serial number" : "warranty number"
const numbers = input.warrantyNumbers.map((s) => s.trim()).filter((s) => s.length > 0)
if (numbers.length === 0) {
errors.warrantyNumbers = "Enter one warranty number per unit"
errors.warrantyNumbers = `Enter one ${label} per unit`
} else if (!Number.isNaN(qty) && numbers.length !== qty) {
errors.warrantyNumbers = `Enter exactly ${qty || 0} warranty number(s) — got ${numbers.length}`
errors.warrantyNumbers = `Enter exactly ${qty || 0} ${label}(s) — got ${numbers.length}`
} else if (new Set(numbers.map((s) => s.toLowerCase())).size !== numbers.length) {
errors.warrantyNumbers = "Warranty numbers must be unique"
errors.warrantyNumbers = `${serial ? "Serial" : "Warranty"} numbers must be unique`
}
}