This commit includes the temporary Next.js build cache files, public brand logos, and project configuration added in this change.
176 lines
6.7 KiB
TypeScript
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}`;
|
|
}
|