WFDASH-WEBAPP/src/lib/item-status-ui.ts
SJCoetz 7a68cafe35 chore: add logos, opencode config and build assets
This commit includes the temporary Next.js build cache files, public brand
logos, and project configuration added in this change.
2026-07-27 08:21:20 +02:00

176 lines
6.7 KiB
TypeScript

/**
* Visual tokens + tab-group mapping for the item-status-driven
* orders model.
*
* Two surfaces share the same hex palette so the per-item pill and
* the tab pill always agree on colour:
*
* 1. Per-item pill (rendered next to each line in an order row)
* — driven by `ITEM_STATUS_VISUAL[itemStatus].hex`
*
* 2. Tab pill on /orders (the 6 status tabs)
* — driven by `TAB_GROUP_VISUAL[tabGroup].hex`
*
* Tabs do NOT map 1:1 to item statuses — eleven item statuses would
* be too noisy as tabs. We collapse them into six operational
* groups (see `TAB_GROUP_ITEM_STATUSES`). An order appears on tab X
* iff it has at least one item whose `item_status` belongs to that
* group; collapsing means a single item only ever belongs to one tab.
*
* White text on each hex clears WCAG AA (≥ 4.5:1) — verified for all
* values. The 500-shade variants (e.g. #3b82f6) fail contrast on
* amber/teal/emerald, so we deliberately use 600/700-shade hex.
*/
import type { ItemStatus } from "@/lib/types";
/* ============================================================================
* Per-item visual tokens
* ========================================================================== */
export type ItemStatusVisual = {
/** Hex used for the item pill fill / dot. */
hex: string;
/** Friendly label rendered inside the pill. */
label: string;
};
const FALLBACK_ITEM_STATUS_VISUAL: ItemStatusVisual = {
hex: "#475569",
label: "Unknown",
};
export const ITEM_STATUS_VISUAL: Record<ItemStatus, ItemStatusVisual> = {
pending: { hex: "#2563eb", label: "New" },
processed: { hex: "#b45309", label: "Processing" },
po_generated: { hex: "#b45309", label: "Processing" },
collection_pool: { hex: "#7c3aed", label: "Collection Pool" },
assigned: { hex: "#7c3aed", label: "Collection Pool" },
collected: { hex: "#0f766e", label: "Collected" },
stock_verified: { hex: "#0f766e", label: "Collected" },
picking: { hex: "#0f766e", label: "Collected" },
ready_delivery: { hex: "#0f766e", label: "Collected" },
delivered: { hex: "#047857", label: "Delivered" },
cancelled: { hex: "#e11d48", label: "Cancelled" },
ready_invoice: { hex: "#1d4ed8", label: "Ready for Invoice" },
invoiced: { hex: "#047857", label: "Invoiced" },
stock_booked: { hex: "#0e7490", label: "Stock Booked" },
// Post-invoice physical-delivery flow (2026-07-20).
// Each step uses a different hex so a row can be visually
// distinguished at a glance — the operator can tell
// "invoiced" (green) from "with_driver" (amber) from
// "checked_in" (teal) from "completed" (deep green)
// without reading the label.
with_driver: { hex: "#b45309", label: "With Driver" },
checked_in: { hex: "#0e7490", label: "Checked In" },
completed: { hex: "#064e3b", label: "Completed" },
};
const EXTRA_ITEM_STATUS_VISUAL: Record<string, ItemStatusVisual> = {
delivery_note: { hex: "#7c2d12", label: "Delivery Note" },
};
export function getItemStatusVisual(status: string): ItemStatusVisual {
return ITEM_STATUS_VISUAL[status as ItemStatus]
?? EXTRA_ITEM_STATUS_VISUAL[status]
?? FALLBACK_ITEM_STATUS_VISUAL;
}
/* ============================================================================
* Tab groups (the 6 /orders filter tabs)
* ========================================================================== */
/**
* The six tab groups. Each one is the set of item statuses that
* "live" on that tab. An order is shown on tab X iff at least one of
* its items has an item_status in `TAB_GROUP_ITEM_STATUSES[X]`.
*
* Display order matches the operational lifecycle: a brand-new item
* lands on "new"; PO'd items move to "processing"; once queued for
* collection they slide to "collection pool"; back at the office they
* move to "collected"; delivered is terminal; cancelled is its own
* tab so the operator can audit it without it drowning the active tabs.
*/
export type TabGroup =
| "new"
| "processing"
| "collection_pool"
| "collected"
| "delivered"
| "cancelled";
export const TAB_GROUP_ORDER: TabGroup[] = [
"new",
"processing",
"collection_pool",
"collected",
"delivered",
"cancelled",
];
export const TAB_GROUP_ITEM_STATUSES: Record<TabGroup, ItemStatus[]> = {
new: ["pending"],
processing: ["processed", "po_generated"],
collection_pool: ["collection_pool", "assigned"],
collected: ["collected", "stock_verified", "picking", "ready_delivery"],
delivered: ["delivered"],
cancelled: ["cancelled"],
};
/**
* PB filter fragment for "items belonging to this tab group". PB
* doesn't have a set-membership operator on `IN`, so we OR a series
* of `item_status = "..."` clauses together — same approach the
* old per-status counts used.
*/
export function tabGroupFilter(tab: TabGroup): string {
return TAB_GROUP_ITEM_STATUSES[tab]
.map((s) => `item_status = "${s}"`)
.join(" || ");
}
/**
* Map an individual item status to the single tab group it belongs
* to. The grouping is a partition (every status lands in exactly one
* tab) — derived by inverting `TAB_GROUP_ITEM_STATUSES`.
*/
export function itemStatusToTabGroup(s: ItemStatus): TabGroup {
for (const tab of TAB_GROUP_ORDER) {
if (TAB_GROUP_ITEM_STATUSES[tab].includes(s)) return tab;
}
// Defensive fallback. The grouping above covers every member of
// the `ItemStatus` union, so this branch is unreachable on a typed
// input — but the function is also called from JS paths where the
// caller might pass a string that PB tolerated. Defaulting to
// "new" keeps the page rendering instead of throwing.
return "new";
}
/* ============================================================================
* Tab-group visual tokens
* ========================================================================== */
export type TabGroupVisual = {
hex: string;
label: string;
};
export const TAB_GROUP_VISUAL: Record<TabGroup, TabGroupVisual> = {
new: { hex: "#2563eb", label: "New" },
processing: { hex: "#b45309", label: "Processing" },
collection_pool: { hex: "#7c3aed", label: "Collection Pool" },
collected: { hex: "#0f766e", label: "Collected" },
delivered: { hex: "#047857", label: "Delivered" },
cancelled: { hex: "#e11d48", label: "Cancelled" },
};
/* ============================================================================
* Shared utilities
* ========================================================================== */
/** Add an alpha channel to a hex. `withAlpha("#2563eb", 12)` → "#2563eb1f". */
export function withAlpha(hex: string, alphaPercent: number): string {
const a = Math.round((alphaPercent / 100) * 255)
.toString(16)
.padStart(2, "0");
return `${hex}${a}`;
}