status-taxonomy.ts
Language TypeScript9.9 KB226 lines
/*** Plinth product-status taxonomy: the judgment layer.** Plinth has no notion of "open work." It has statuses. Whether a status means Meridian* owes the next move, the client owes it, a vendor owes it, or the line is done* is a judgment that differs per status, and no system can tell you.** 'ours' Meridian owes the next move. The only class that counts as load.* 'client' Blocked on the client. A follow-up list, not workload. Aging here* measures client responsiveness, not our capacity.* 'vendor' At the printer or in transit. Delivery risk, not load.* 'parked' Deliberately paused.* 'terminal' Done, cancelled, or refunded.** These are judgements read from status names. The 'ours' vs 'client' calls* are the ones to confirm with the people who run production: that boundary* decides whether a number describes the team's backlog or the client's.*/export type WorkClass = 'ours' | 'client' | 'vendor' | 'parked' | 'terminal'export interface StatusRule {id: numbername: stringclass: WorkClass/** Design-queue statuses roll up into the designer-load number. */design?: booleannote?: string}/** catalog: "Product", the granular, truthful layer. */export const PRODUCT_STATUSES: StatusRule[] = [{ id: 176, name: 'File Check', class: 'ours', design: true, note: 'The designer queue.' },{ id: 144, name: 'Design Requested', class: 'ours', design: true },{ id: 101, name: 'Proof In Progress', class: 'ours', design: true },{ id: 120, name: 'Revisions In Progress', class: 'ours', design: true },{ id: 115, name: 'Reproof Needed', class: 'ours', design: true },{ id: 136, name: 'Files Resubmitted', class: 'ours', design: true, note: 'Back in the queue; needs re-review.' },{ id: 138, name: 'Waiting On Files', class: 'client' },{ id: 161, name: 'Proof With Client', class: 'client' },{ id: 122, name: 'Question For Client', class: 'client' },{ id: 175, name: 'Proof Signed Off', class: 'ours', note: 'Approved, not yet moved on. Should be near zero.' },{ id: 145, name: 'Cleared For Print', class: 'ours' },{ id: 179, name: 'On Press', class: 'vendor' },{ id: 182, name: 'At Partner Shop', class: 'vendor' },{ id: 112, name: 'Partner Dispatched', class: 'vendor' },{ id: 126, name: 'Packed', class: 'ours' },{ id: 103, name: 'Payment Due', class: 'client' },{ id: 143, name: 'Line Cancel Requested', class: 'ours', note: 'Needs a decision.' },{ id: 139, name: 'Line Cancelled', class: 'terminal' },{ id: 194, name: 'Line Refunded', class: 'terminal' },{ id: 157, name: 'Signed Off With Edits (Retired)', class: 'terminal', note: 'Marked for deletion by Plinth.' },]/*** catalog: "Order", the coarser layer.** Kept separate from the product catalog on purpose. The two use different ids* for the same label ("File Check" is 121 here and 176 there) and they do not* contain the same set: `Proof Out` (159) exists only at order level. Merging* them would let an order-level status fall through the product classifier* and be counted as "our move" when it is actually sitting with the client.*/export const ORDER_STATUSES: StatusRule[] = [{ id: 134, name: 'Received', class: 'ours' },{ id: 171, name: 'Allocated', class: 'ours' },{ id: 121, name: 'File Check', class: 'ours', design: true },{ id: 181, name: 'Design Requested', class: 'ours', design: true },{ id: 195, name: 'Proof In Progress', class: 'ours', design: true },{ id: 186, name: 'Revisions In Progress', class: 'ours', design: true },{ id: 159, name: 'Proof Out', class: 'client', note: 'Waiting on client approval. NOT team load.' },{ id: 167, name: 'Waiting On Files', class: 'client' },{ id: 163, name: 'Payment Due', class: 'client' },{ id: 168, name: 'Proof Signed Off', class: 'ours', note: 'Approved, not yet moved on.' },{ id: 185, name: 'Cleared For Print', class: 'ours' },{ id: 177, name: 'Rush Print', class: 'ours' },{ id: 140, name: 'Rush Prep', class: 'ours' },{ id: 111, name: 'PO In Review', class: 'ours' },{ id: 137, name: 'Payment Error', class: 'ours', note: 'Revenue at risk.' },{ id: 106, name: 'Cancel Requested', class: 'ours', note: 'Needs a decision.' },{ id: 108, name: 'Packed', class: 'ours' },{ id: 125, name: 'On Press', class: 'vendor' },{ id: 117, name: 'PO Dispatched', class: 'vendor' },{ id: 133, name: 'Dispatched', class: 'vendor' },{ id: 162, name: 'Part Dispatched', class: 'vendor' },{ id: 193, name: 'Awaiting Collection', class: 'vendor' },{ id: 135, name: 'On Hold', class: 'parked' },{ id: 189, name: 'Delivered', class: 'terminal', note: 'Graveyard: thousands of records, none ever exit.' },{ id: 129, name: 'Cancelled', class: 'terminal' },{ id: 166, name: 'Refunded', class: 'terminal' },{ id: 191, name: 'Claim Opened', class: 'ours' },{ id: 131, name: 'Claim Settled', class: 'terminal' },{ id: 158, name: 'PO Paid', class: 'ours' },{ id: 196, name: 'PO Payment Due', class: 'client' },{ id: 107, name: 'Legacy Review (retired)', class: 'terminal' },{ id: 174, name: 'Legacy Processing (retired)', class: 'terminal' },]const orderById = new Map(ORDER_STATUSES.map((s) => [s.id, s]))const orderByName = new Map(ORDER_STATUSES.map((s) => [s.name.toLowerCase(), s]))function resolveOrder(statusName: string, statusId?: number | null) {if (typeof statusId === 'number') {const hit = orderById.get(statusId)if (hit) return hit}return orderByName.get(statusName.trim().toLowerCase())}export function classifyOrder(statusName: string, statusId?: number | null): WorkClass {return resolveOrder(statusName, statusId)?.class ?? 'ours'}export function isOrderDesignQueue(statusName: string, statusId?: number | null): boolean {return resolveOrder(statusName, statusId)?.design ?? false}export function isKnownOrderStatus(statusName: string, statusId?: number | null): boolean {return resolveOrder(statusName, statusId) !== undefined}const byId = new Map(PRODUCT_STATUSES.map((s) => [s.id, s]))const byName = new Map(PRODUCT_STATUSES.map((s) => [s.name.toLowerCase(), s]))/*** Resolve a status rule, preferring the ID.** `line_status_code` matters more than it looks: status names are duplicated* across the two catalogs. "File Check"* is id 121 as an Order status and id 176 as a Product status; "On Press" is* 125 and 179; "Proof Signed Off" is 168 and 175. Matching on the name alone cannot* tell those apart, so a name-keyed lookup is silently one wrong field away* from classifying an order-level status against product-level rules.** The name fallback stays for records that carry no id.*/function resolve(statusName: string, statusId?: number | null) {if (typeof statusId === 'number') {const hit = byId.get(statusId)if (hit) return hit}return byName.get(statusName.trim().toLowerCase())}/** Unknown statuses classify as 'ours' so a new one is noticed, not hidden. */export function classifyProduct(statusName: string, statusId?: number | null): WorkClass {return resolve(statusName, statusId)?.class ?? 'ours'}export function isDesignQueue(statusName: string, statusId?: number | null): boolean {return resolve(statusName, statusId)?.design ?? false}/*** Order statuses past the point where the line has left the building.** A line is ours when the product status says ours and the order has not moved* past Part Dispatched. So Part Dispatched is still* eligible and these two are the first that are not; Delivered, Cancelled* and Refunded are caught by their `terminal` class instead of being listed* again here.*/const PAST_FULFILMENT = new Set([133, // Dispatched193, // Awaiting Collection])/*** Has this order moved beyond the point where anybody here still owes it a move?** Unknown statuses answer false: the line keeps showing. Same direction as* `classifyProduct`'s unknown default: a status nobody has classified yet should* be noticed on the page, not quietly filtered off it.*/export function isOrderPastFulfilment(statusName: string, statusId?: number | null): boolean {const rule = resolveOrder(statusName, statusId)if (!rule) return falsereturn rule.class === 'terminal' || PAST_FULFILMENT.has(rule.id)}/*** The one classifier for an order line. Product status decides, unless the* order has shipped, in which case the line is done whatever the product row* says. Every count on the board goes through this, so the counts agree with* the product status the Status column shows.** Neither field alone is right:** - Order status alone hides real design work. An order sitting On Press* with a product line still in File Check is somebody's job today.* - Product status alone invents work that is already out of the door. The* product row parks at its last value when an order ships, so many* design-stage product lines sit on orders already shipped or delivered.* Counting those makes the team look behind when it is not.** So: the product row says what the work is, and the order says whether the* work is still here, with the cutoff at Part Dispatched.*/export function classifyLine(line: {productStatus: stringproductStatusId?: number | nullorderStatus: stringorderStatusId?: number | null}): WorkClass {if (isOrderPastFulfilment(line.orderStatus, line.orderStatusId)) return 'terminal'return classifyProduct(line.productStatus, line.productStatusId)}/** Does this line's design-queue claim still stand? Same gate. */export function isLineDesignQueue(line: {productStatus: stringproductStatusId?: number | nullorderStatus: stringorderStatusId?: number | null}): boolean {if (isOrderPastFulfilment(line.orderStatus, line.orderStatusId)) return falsereturn isDesignQueue(line.productStatus, line.productStatusId)}export const CLASS_LABELS: Record<WorkClass, string> = {ours: 'Our move',client: 'Waiting on client',vendor: 'At vendor / in transit',parked: 'Parked',terminal: 'Closed',}