WFDASH-WEBAPP/src/lib/stock-status.ts

98 lines
3.2 KiB
TypeScript
Raw Normal View History

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);
}