/** * Where a diagnostic order has got to, mirroring the server's two machines. * * TWO PROGRESSIONS, because the work is genuinely different: a specimen is collected and sent, a * study is scheduled and performed. The ends are shared -- every diagnostic order is placed, comes * back, and is looked at -- and only the middle differs. * * FORWARD ONLY, and the client does not enforce that -- the server does. What this file is for is * knowing which move to OFFER, so the screen never shows a button whose only outcome is a 409. * * `resulted` and `reviewed` are deliberately absent from what staff may choose. `resulted` is a * consequence of a result existing rather than a claim a person makes, and is stamped where the * result is recorded. `reviewed` is a clinician's act -- "has anybody looked at this?" is the * question that state answers, and a receptionist answering it answers it wrongly and permanently. */ export type DiagnosticOrderKind = "laboratory" | "imaging"; export const LABORATORY_PROGRESSION = [ "ordered", "collected", "sent", "resulted", "reviewed", ] as const; export const IMAGING_PROGRESSION = [ "ordered", "scheduled", "performed", "resulted", "reviewed", ] as const; export type DiagnosticOrderState = | (typeof LABORATORY_PROGRESSION)[number] | (typeof IMAGING_PROGRESSION)[number]; const ALL_STATES: readonly string[] = [ ...new Set([...LABORATORY_PROGRESSION, ...IMAGING_PROGRESSION]), ]; export const isDiagnosticOrderState = (value: unknown): value is DiagnosticOrderState => typeof value === "string" && ALL_STATES.includes(value); export const progressionFor = (kind: DiagnosticOrderKind): readonly DiagnosticOrderState[] => (kind === "imaging" ? IMAGING_PROGRESSION : LABORATORY_PROGRESSION); /** * Return whether the caller can withdraw an order: it is untouched and they placed it. The server * refuses anyone but the ordering practitioner. */ export const canWithdrawDiagnosticOrder = ( state: DiagnosticOrderState | undefined, alreadyWithdrawn: boolean, orderedByCaller: boolean, ): boolean => orderedByCaller && !alreadyWithdrawn && (state === undefined || state === "ordered"); /** What a person reading the queue should see, rather than the wire value. */ export const DIAGNOSTIC_ORDER_STATE_LABELS: Record = { ordered: "Ordered", collected: "Collected", sent: "Sent to lab", scheduled: "Scheduled", performed: "Performed", resulted: "Result received", reviewed: "Reviewed", }; /** The states staff may move an order to, per machine, in the order they happen. */ const STAFF_ADVANCEABLE: Record = { laboratory: ["collected", "sent"], imaging: ["scheduled", "performed"], }; /** * The moves worth offering for an order in this state. * * THE MACHINE COMES FROM THE ORDER'S KIND, never from the state alone. `ordered` is shared by both, * so a screen that guessed from the state would offer "collected" on an X-ray. * * Skipping forward is legitimate and is offered: a practice whose own lab runs the test never passes * through `sent`. Going backwards is not, because the states record things that happened. * * An order with NO state, or of no recognised kind, gets nothing. Both mean an order this system did * not stamp, and offering a move for one would be offering to advance something we do not understand. */ export const nextStatesFor = ( kind: DiagnosticOrderKind | undefined, state: DiagnosticOrderState | undefined, ): DiagnosticOrderState[] => { if (!kind || !state) return []; const progression = progressionFor(kind); const rank = progression.indexOf(state); if (rank === -1) return []; return STAFF_ADVANCEABLE[kind].filter((candidate) => progression.indexOf(candidate) > rank); }; /** * WHICH KIND OF WORK THIS HAT IS HERE FOR. * * A LENS, NOT A FENCE, and the difference is the whole design. Both diagnostic workspaces resolve to * the same `diagnostics` permission, so this decides what a tech is SHOWN, never what they may * reach: the same person can switch workspace and see the other kind, and should be able to, because * the section is what the practice actually granted. Anything stronger means two permission sections * and a server that filters the queue itself. */ export const lensFor = (workspace?: string): DiagnosticOrderKind | undefined => { if (workspace === "LabTech") return "laboratory"; if (workspace === "ImagingTech") return "imaging"; return undefined; }; /** * WHOSE WORKFLOW THE BENCH QUEUE IS, ASKED IN ONE PLACE. * * Permissions do access; workspaces do workflow. `diagnostics` says whether a person may REACH the * queue, and an Administrator holds it because somebody has to grant it -- which is not a reason for * a bench worklist to open their rail above the practice they administer. This is the other half of * the question, and it is deliberately derived from `lensFor` rather than written as a second list: * a hat that has a lens over the queue is a hat whose job the queue is. * * ONE EXPRESSION, TWO READERS -- the rail that offers the item and the resolver that decides where a * workspace lands. They already disagreed once about this queue, and a rail that offers what the * landing does not reach (or the reverse) is the same drift wearing different clothes. * * IT DOES NOT NARROW THE PERMISSION AND MUST NOT. An administrator who types the URL is still * admitted by the route and by the API; what changes is only whether the rail offers it to them. */ export const workspaceWorksTheDiagnosticQueue = (workspace?: string): boolean => lensFor(workspace) !== undefined; /** * WHOSE WORKFLOW THE BENCH IS, and it is narrower than the queue above. * * The bench is worked by SPECIMEN, and an imaging order never produces one, so a specimen-shaped * worklist has nothing to show a radiology technologist. Their hat keeps the order queue until an * imaging department of their own exists. * * DERIVED FROM THE SAME LENS for the reason its sibling records: a hat whose lens is laboratory is a * hat whose job the bench is, and a second list of workspace strings is how two answers to one * question start to differ. One expression, two readers - the rail and the landing resolver. */ export const workspaceWorksTheBench = (workspace?: string): boolean => lensFor(workspace) === "laboratory"; /** * WHAT THE QUEUE IS CALLED, DERIVED FROM THE LENS RATHER THAN FROM THE HAT. * * The rail switched on the hat and the page did not, so an imaging technologist opened the one * destination their workspace has and read **Lab orders** under a row that said **Imaging** - one * fact written in two places, disagreeing in the same viewport, on the first and only screen that * hat sees. * * A SECOND TERNARY WOULD HAVE BEEN THE SAME DEFECT WITH ONE MORE SITE, so this is one answer both * callers read. And it keys on `lensFor` rather than on the workspace directly, which is the * stronger version of the same idea: the heading then names exactly what the rows underneath it * were filtered to. A heading and a filter that read different predicates can disagree about what * is on the screen; these cannot. * * THERE IS A THIRD READER AND NEITHER HAT DESCRIBES THEM. An administrator reaches this queue by * holding `diagnostics` rather than by wearing a hat, so `lensFor` gives them no lens and they see * BOTH kinds - which is why they get the product's own name for both, the one the chart already * uses. Before this they got whichever branch the `else` happened to be, and it named half of what * they were looking at. */ export interface DiagnosticsQueueLabel { heading: string; subtitle: string; } export const diagnosticsQueueLabel = (workspace?: string): DiagnosticsQueueLabel => { switch (lensFor(workspace)) { case "imaging": return { heading: "Imaging", subtitle: "Studies this practice is carrying out, and where each one has got to.", }; case "laboratory": return { heading: "Lab orders", subtitle: "Track orders, send-outs and returned results.", }; default: return { heading: "Tests and imaging", subtitle: "Tests and imaging this practice has ordered, and where each one has got to.", }; } };