This commit adds the complete v2/v3 stock management functionality: - add new stock-specific item statuses and update the status visual map - implement full stock purchase API suite (create, list, retrieve, send, receive) - reorganize sidebar navigation to group stock-related pages together - add stock item CRUD APIs, status utilities, low stock filtering, and surplus booking logic - add database migration for the `stock_items.target_qty` field - update type definitions and add comprehensive tests for all new stock components
97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
import type { StockItem } from "./types";
|
|
|
|
/**
|
|
* Stock status levels — used by the /stock list to render the
|
|
* reorder-alert badge and by the API to flag low-stock rows.
|
|
*
|
|
* The "inactive" state is orthogonal: an inactive item is always
|
|
* surfaced as `inactive` regardless of its current/min qty, so
|
|
* the operator can spot soft-deleted items that the rest of the
|
|
* app would otherwise treat as "low stock" or "out".
|
|
*/
|
|
export type StockStatus = "ok" | "low" | "out" | "inactive";
|
|
|
|
/**
|
|
* Reorder-alert thresholds.
|
|
*
|
|
* current_qty === 0 → "out" (red)
|
|
* 0 < current_qty <= min_qty → "low" (amber)
|
|
* current_qty > min_qty → "ok" (green)
|
|
* current_qty < 0 → "out" (red; treat as a bug
|
|
* rather than clamp)
|
|
* is_active === false → "inactive" (grey overlay)
|
|
*
|
|
* Pure, no side effects. Called by both the list page (to pick the
|
|
* badge colour) and the API filter (to gate `?low_stock=true`).
|
|
*/
|
|
export function getStockStatus(
|
|
currentQty: number | null | undefined,
|
|
minQty: number | null | undefined,
|
|
isActive: boolean | null | undefined,
|
|
): StockStatus {
|
|
if (isActive === false) return "inactive";
|
|
const current = Number(currentQty ?? 0);
|
|
const min = Number(minQty ?? 0);
|
|
if (current <= 0) return "out";
|
|
if (current <= min) return "low";
|
|
return "ok";
|
|
}
|
|
|
|
/**
|
|
* Tailwind-friendly colour tokens for each status. Centralised so
|
|
* the list table, the detail header, and the badge helper all
|
|
* render the same palette — the operator's eye learns one set of
|
|
* cues.
|
|
*/
|
|
export const STOCK_STATUS_STYLES: Record<
|
|
StockStatus,
|
|
{ label: string; hex: string; ring: string; text: string }
|
|
> = {
|
|
ok: {
|
|
label: "In stock",
|
|
hex: "#047857", // emerald-700 — matches CompleteBadge
|
|
ring: "rgba(4, 120, 87, 0.30)",
|
|
text: "text-emerald-700 dark:text-emerald-400",
|
|
},
|
|
low: {
|
|
label: "Low stock",
|
|
hex: "#b45309", // amber-700 — high contrast on light surfaces
|
|
ring: "rgba(180, 83, 9, 0.30)",
|
|
text: "text-amber-700 dark:text-amber-400",
|
|
},
|
|
out: {
|
|
label: "Out of stock",
|
|
hex: "#b91c1c", // red-700
|
|
ring: "rgba(185, 28, 28, 0.30)",
|
|
text: "text-red-700 dark:text-red-400",
|
|
},
|
|
inactive: {
|
|
label: "Inactive",
|
|
hex: "#475569", // slate-600
|
|
ring: "rgba(71, 85, 105, 0.30)",
|
|
text: "text-slate-600 dark:text-slate-400",
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Convenience helper for the list-page filter: true when the row
|
|
* should be surfaced by `?low_stock=true`. Mirrors the list badge
|
|
* (red + amber), but EXCLUDES inactive rows — the operator asked
|
|
* "what needs reordering" not "what's soft-deleted".
|
|
*/
|
|
export function isLowStock(
|
|
currentQty: number | null | undefined,
|
|
minQty: number | null | undefined,
|
|
isActive: boolean | null | undefined,
|
|
): boolean {
|
|
if (isActive === false) return false;
|
|
return getStockStatus(currentQty, minQty, isActive) !== "ok";
|
|
}
|
|
|
|
/**
|
|
* Apply `?low_stock=true` semantics to a StockItem record. Used by
|
|
* the list API as the gate after the PB fetch.
|
|
*/
|
|
export function isLowStockItem(item: Pick<StockItem, "current_qty" | "min_qty" | "is_active">): boolean {
|
|
return isLowStock(item.current_qty, item.min_qty, item.is_active);
|
|
}
|