Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/components/ward-management/coordinator/shortlist-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,23 @@ function capacityLine(unit: Unit) {
}

/**
* Task 6A: a Form 3B honestly carries no `dueAt` — the Mental Health Act imposes no
* post-examination deadline (clinician-confirmed). For that case this states the form and the
* real elapsed ED time via the existing `elapsedLabel` (never a new formatter), worded as time
* IN the department rather than time left against anything, so it can never be misread as a
* statutory countdown the way a bare number next to a form code could be.
* Neither a Form 1A nor a Form 3B carries a `dueAt` in this model (see `LegalForm`'s own doc
* comment in ward-model.ts). For that case this states the form and the real elapsed ED time via
* the existing `elapsedLabel` (never a new formatter), worded as time IN the department rather
* than time left against anything, so it can never be misread as a statutory countdown the way a
* bare number next to a form code could be.
*
* The wording is deliberately "no deadline recorded", not "no statutory deadline". It reports
* what THIS RECORD holds, which is all we can verify. "No statutory deadline" asserts what the
* Mental Health Act requires, and that is a legal claim this prototype is not entitled to make in
* either direction — asserting an absence is the same overreach as asserting the seven-day figure
* that was deleted on 2026-08-23.
*/
function legalFormLine(movement: Movement, now: Instant) {
if (!movement.legalForm) return "No legal form recorded for this movement";
const named = `Form ${movement.legalForm.code} (${movement.legalForm.label})`;
if (movement.legalForm.dueAt === undefined) {
return `${named} — no statutory deadline; ${elapsedLabel(movement, now)} in the emergency department`;
return `${named} — no deadline recorded; ${elapsedLabel(movement, now)} in the emergency department`;
}
const remaining = minutesUntil(movement.legalForm.dueAt, now);
return remaining < 0
Expand Down
34 changes: 26 additions & 8 deletions src/components/ward-management/ward-derivations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,18 +314,36 @@ export type InboxItem = {
/**
* Every item here is computed from real movement fields — nothing is authored.
*
* RULING (Task 8): each category uses `.filter()`, never `.find()`. Measured against the real
* fixture at `NOW_ANCHOR`, five movements carry a breached statutory deadline, one has reached
* the parallel-referral cap, and two have transport accepted but not departed — a `.find()`-based
* inbox reported exactly one of each regardless, understating a legal breach count by four. This
* is the coordinator's work list, not a report: every qualifying movement gets its own row.
* RULING (Task 8): each category uses `.filter()`, never `.find()`. A `.find()`-based inbox
* reported exactly one item per category regardless of how many movements qualified, silently
* understating the coordinator's work list.
*
* Re-measured against the real fixture at `NOW_ANCHOR` on 2026-08-23: **zero** movements carry a
* breached legal deadline, one has reached the parallel-referral cap, and two have transport
* accepted but not departed. The legal category is empty because the 2026-08-23 product-owner
* correction removed every `dueAt` from Forms 1A and 3B (see `LegalForm`'s own doc comment in
* ward-model.ts), and the only deadlines left in this fixture — the transport/transfer forms 4A
* and 4C — are not currently in the past. An earlier version of this comment claimed five
* movements carried a breached statutory deadline; that number described the deleted fabrication
* and is not true of any figure in this model.
*
* The `.filter()` shape stays regardless, for two reasons: the transport category alone still
* qualifies two movements today, so `.find()` would still understate the list; and the legal
* category is dormant rather than removed, so it must count correctly the moment a form that
* legitimately carries a deadline falls due. This is the coordinator's work list, not a report:
* every qualifying movement gets its own row.
*/
export function buildActionInbox(movements: Movement[], now: Instant): InboxItem[] {
const items: InboxItem[] = [];

// A form with no `dueAt` (Task 6A: a Form 3B honestly carries none — the Mental Health Act
// imposes no post-examination deadline) is never breached and contributes nothing here.
// `undefined` must never reach `clockState`'s arithmetic.
// A form with no `dueAt` is never breached and contributes nothing here; `undefined` must never
// reach `clockState`'s arithmetic. As of the 2026-08-23 product-owner correction that is every
// Form 1A and every Form 3B in this model — the record carries no deadline for them. Stated
// that way deliberately: what this model holds is a fact about the record, whereas what the
// Mental Health Act does or does not require is a legal claim this prototype is not entitled to
// make in either direction. The question was settled for the 3B by the clinician (Task 6A:
// "It is just counting how long they have been in ED determining priority. So counting up") and
// for the 1A by the product owner on 2026-08-23. See `LegalForm`'s doc comment in ward-model.ts.
const breachedLegal = movements.filter(
(movement) => movement.legalForm?.dueAt !== undefined && clockState(movement.legalForm.dueAt, now) === "breached",
);
Expand Down
3 changes: 1 addition & 2 deletions src/components/ward-management/ward-flow-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EVENT_ROLE, type WardFlowEvent, type WardFlowRole } from "@/components/ward-management/ward-flow-events";
import { FORM_1A_REFERRAL_EXPIRY_MINUTES, PARALLEL_REFERRAL_CAP } from "@/components/ward-management/ward-model";
import { PARALLEL_REFERRAL_CAP } from "@/components/ward-management/ward-model";
import type { Movement, MovementStage, Rejection, Unit } from "@/components/ward-management/ward-model";
import { wardMovements } from "@/components/ward-management/ward-movements";
import { allEmergencyDepartments, allUnits } from "@/components/ward-management/ward-sites";
Expand Down Expand Up @@ -142,7 +142,6 @@ export function wardFlowReducer(state: WardFlowState, event: WardFlowEvent): War
code: "1A",
label: "Referral for examination",
kind: "examination",
dueAt: event.now + FORM_1A_REFERRAL_EXPIRY_MINUTES,
}
: undefined,
statusChanges: [],
Expand Down
14 changes: 10 additions & 4 deletions src/components/ward-management/ward-management-console.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,21 @@ import styles from "./ward-management.module.css";

/**
* The "label (code) · …" line for a legal form, shared by the readiness card and the legal
* panel below. Task 6A: a Form 3B honestly carries no `dueAt` (the Mental Health Act imposes no
* post-examination deadline) — this states that absence explicitly rather than ever formatting
* an undefined instant, which is how "due NaN:NaN" would ship.
* panel below. Neither a Form 1A nor a Form 3B carries a `dueAt` in this model (see `LegalForm`'s
* own doc comment in ward-model.ts) — this states that absence explicitly rather than ever
* formatting an undefined instant, which is how "due NaN:NaN" would ship.
*
* The wording is deliberately "no deadline recorded", not "no statutory deadline". It reports
* what THIS RECORD holds, which is all we can verify. "No statutory deadline" asserts what the
* Mental Health Act requires, and that is a legal claim this prototype is not entitled to make in
* either direction — asserting an absence is the same overreach as asserting the seven-day figure
* that was deleted on 2026-08-23.
*/
function legalFormReadinessLine(legalForm: LegalForm): string {
const named = `${legalForm.label} (${legalForm.code})`;
return legalForm.dueAt !== undefined
? `${named} · due ${formatInstant(legalForm.dueAt)}`
: `${named} · no statutory deadline`;
: `${named} · no deadline recorded`;
}

const stageIcons = {
Expand Down
36 changes: 19 additions & 17 deletions src/components/ward-management/ward-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,30 @@ export type DeclineReason = (typeof DECLINE_REASONS)[number];
/** Referring to more than three units at once spams wards and erodes trust between services. */
export const PARALLEL_REFERRAL_CAP = 3;

/**
* A Form 1A referral must carry an explicit expiry. In this synthetic model a
* newly raised referral uses the Mental Health Act's seven-day outer limit;
* production form capture must always use the expiry recorded on the actual
* approved form rather than treating this demo default as clinical advice.
*/
export const FORM_1A_REFERRAL_EXPIRY_MINUTES = 7 * 24 * 60;

export type LegalStatus =
"Voluntary" | "Referred for psychiatric examination" | "Detained awaiting examination" | "Involuntary inpatient";

/**
* The legal clock and the ED clock are different clocks (spec "Model changes this phase
* requires", `Movement.formedAt`). `dueAt` is the legal clock — the Mental Health Act deadline
* this specific form carries, when it has one. A Form 1A ("referral for examination") always
* carries a real statutory examination window, so it always carries a `dueAt`. A Form 3B
* ("inpatient treatment order") has no equivalent post-examination deadline in the Act — put to
* the clinician directly, his answer was that the post-examination clock "is just counting how
* long they have been in ED determining priority. So counting up," i.e. it is not a legal
* countdown at all (Task 6A). So `dueAt` is optional, and a 3B is authored — and produced by the
* reducer — without one. Never substitute a fallback number for an absent `dueAt`, and never let
* an absent `dueAt` read as "clear" or "not yet due"; render its absence explicitly.
* requires", `Movement.formedAt`). `dueAt`, when present, is the legal clock — a statutory
* deadline a specific form carries. Task 6A first established that a Form 3B ("inpatient
* treatment order") has no such deadline: put to the clinician directly, his answer was that
* the post-examination clock "is just counting how long they have been in ED determining
* priority. So counting up," i.e. not a legal countdown at all. This model briefly gave a Form
* 1A ("referral for examination") an authored `dueAt` on the strength of an unverified figure
* an earlier agent wrote into this file from its own recollection, not from the clinician.
* Asked directly on 2026-08-23, the product owner's instruction was narrower than a corrected
* figure — "please can you leave the legal part and just start a clock once the patient arrives
* to ED. Keep it simple for now" — so as of that date **neither a Form 1A nor a Form 3B carries
* a `dueAt` in this model.** (The transport/transfer forms — 4A, "Transport order"; 4C,
* "Transfer between authorised hospitals" — are a different question, out of scope for this
* correction, and still carry real `dueAt` figures unrelated to the examination timeline this
* comment is about.) The field stays optional (never required) precisely so a form can honestly
* carry none, the same shape Task 6A gave a 3B and this now gives a 1A too. Never substitute a
* fallback number for an absent `dueAt`, never let an absent `dueAt` read as "clear" or "not yet
* due" — render its absence explicitly — and never reintroduce a `dueAt` on a 1A or 3B without a
* figure that traces back to the clinician or product owner by name and date, not to an
* assistant's recollection of the Mental Health Act.
*/
export type LegalForm = {
code: string;
Expand Down
21 changes: 13 additions & 8 deletions src/components/ward-management/ward-movements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { NOW_ANCHOR, allEmergencyDepartments, allUnits } from "@/components/ward
/**
* Hand-authored movements covering the states volume alone cannot guarantee: three declines
* with nowhere eligible left, a status change mid-referral, a movement that never completed,
* two legal-form breaches, every stage in the pathway at least once, and the older-adult and
* specialling pressure that is normal — not exceptional — on a busy metro night.
* every stage in the pathway at least once, and the older-adult and specialling pressure that
* is normal — not exceptional — on a busy metro night. (Earlier revisions of this fixture
* authored two Form 1A "legal-form breaches" here; the 2026-08-23 product-owner correction
* removed every `dueAt` from every Form 1A, so no legal-form breach exists in this fixture any
* longer — see `LegalForm`'s doc comment in `ward-model.ts`.)
*/
const seededMovements: Movement[] = [
{
Expand All @@ -19,7 +22,7 @@ const seededMovements: Movement[] = [
sex: "Female",
specialling: false,
legalStatus: "Referred for psychiatric examination",
legalForm: { code: "1A", label: "Referral for examination", kind: "examination", dueAt: NOW_ANCHOR - 15 },
legalForm: { code: "1A", label: "Referral for examination", kind: "examination" },
statusChanges: [],
stage: "placement_requested",
owner: "ED mental health team",
Expand Down Expand Up @@ -110,7 +113,7 @@ const seededMovements: Movement[] = [
sex: "Female",
specialling: false,
legalStatus: "Detained awaiting examination",
legalForm: { code: "1A", label: "Referral for examination", kind: "examination", dueAt: NOW_ANCHOR - 40 },
legalForm: { code: "1A", label: "Referral for examination", kind: "examination" },
statusChanges: [],
stage: "handover_ready",
owner: "ED mental health team",
Expand Down Expand Up @@ -295,7 +298,7 @@ const seededMovements: Movement[] = [
sex: "Female",
specialling: false,
legalStatus: "Detained awaiting examination",
legalForm: { code: "1A", label: "Referral for examination", kind: "examination", dueAt: NOW_ANCHOR + 260 },
legalForm: { code: "1A", label: "Referral for examination", kind: "examination" },
statusChanges: [
{ at: NOW_ANCHOR - 40, from: "Voluntary", to: "Detained awaiting examination", by: "Duty psychiatrist" },
],
Expand Down Expand Up @@ -342,7 +345,7 @@ const seededMovements: Movement[] = [
sex: "Female",
specialling: true,
legalStatus: "Referred for psychiatric examination",
legalForm: { code: "1A", label: "Referral for examination", kind: "examination", dueAt: NOW_ANCHOR + 200 },
legalForm: { code: "1A", label: "Referral for examination", kind: "examination" },
statusChanges: [],
stage: "placement_requested",
owner: "ED mental health team",
Expand Down Expand Up @@ -436,7 +439,7 @@ const seededMovements: Movement[] = [
sex: "Female",
specialling: false,
legalStatus: "Referred for psychiatric examination",
legalForm: { code: "1A", label: "Referral for examination", kind: "examination", dueAt: NOW_ANCHOR + 220 },
legalForm: { code: "1A", label: "Referral for examination", kind: "examination" },
statusChanges: [],
stage: "bed_held",
owner: "Flow coordinator",
Expand Down Expand Up @@ -617,13 +620,15 @@ function routineMovements(count: number, startIndex: number): Movement[] {
sex,
specialling: index % 11 === 0,
legalStatus: index % 3 === 0 ? "Referred for psychiatric examination" : "Voluntary",
// 2026-08-23: no Form 1A in this model carries a dueAt (see LegalForm's own doc comment
// in ward-model.ts) — the product owner's instruction was to drop the legal countdown
// entirely, not to derive a corrected one, so this generator authors none.
legalForm:
index % 3 === 0
? {
code: "1A",
label: "Referral for examination",
kind: "examination" as const,
dueAt: NOW_ANCHOR + (((index * 53) % 400) - 60),
}
: undefined,
statusChanges: [],
Expand Down
42 changes: 21 additions & 21 deletions tests/ui-ward-coordinator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,28 +265,28 @@ test.describe("Ward Flow coordinator screen", () => {
const firstRowScore = await firstRow.getAttribute("data-score");
await expect(firstRow).toContainText(`Operational ${firstRowScore}`);

// The breach line is the row a coordinator must not miss, and it must be able to vanish
// silently for neither direction (Task 5 review Important 3).
// The breach line used to be the row a coordinator must not miss (Task 5 review Important
// 3, then Task 6A fix round 1, then the clinician's "Bed need confirmed" factor added
// 2026-08-22 — see this test's history for how WF-017/WF-009/WF-303 used to be pinned here).
//
// Task 6A fix round 1 corrected two errors this comment used to carry (WF-017 as "first
// row", and "Form 2A" instead of "1A"). The clinician's "Bed need confirmed" factor, added
// 2026-08-22, moves the goalposts a second time: a movement whose examination outcome is
// recorded as `inpatient_order` now outranks one nobody has assessed at all, inside the
// same tier. WF-009 and WF-017 both carry that confirmed need and both carry a Form 3B with
// no `dueAt` (Task 6A deleted the fabricated one, so a 3B can never be breached) — they now
// rank rows 1 and 2 ahead of WF-303, which carries the breached Form 1A but no confirmed
// need. So `firstRow`/`secondRow` can no longer be used to prove the breach line renders —
// by design, neither of the top two rows has one to render — and the breach-line assertion
// is pinned by id instead, the same pattern the "shows a failing gate" test below already
// uses for WF-017/WF-009 for the identical reason (a fixture fact tied to a specific
// movement, not to whichever row currently ranks first).
await expect(firstRow).not.toContainText("passed its deadline");
const secondRow = rows.nth(1);
await expect(secondRow).not.toContainText("passed its deadline");
// Fixture assumption: WF-303 carries a breached Form 1A and no confirmed bed need, so it
// must still show the breach line — it just no longer does so from row 1 or 2.
const breachedRow = queue.locator('[data-testid="ward-queue-row-WF-303"]');
await expect(breachedRow).toContainText("passed its deadline");
// 2026-08-23 correction: put to the product owner directly, the instruction was to drop the
// legal countdown from this model entirely, not to get its deadline figure right — "please
// can you leave the legal part and just start a clock once the patient arrives to ED. Keep
// it simple for now." Neither a Form 1A nor a Form 3B carries a `dueAt` any longer (WF-303,
// the one movement this suite used to pin as "the genuine breach", now carries none — see
// `LegalForm`'s own doc comment in ward-model.ts). This supersedes ruling F17's requirement
// that the assertion be satisfied by a genuine breach: there is no longer such a thing as a
// legal breach for 1A/3B to prove, so the whole-page absence below replaces the old
// firstRow/secondRow/breachedRow pin rather than repointing it at a different movement.
// (Repointing at `ED_ACCESS_TARGET_MINUTES` instead was considered and rejected: measured
// against this fixture at `NOW_ANCHOR`, the longest current wait is under it, so no movement
// genuinely exceeds that target either — asserting one did would be a second fabrication of
// the exact kind this correction exists to remove.) This is a whole-page check, not a
// 1A/3B-scoped one, because the string itself is the thing that must not appear — on today's
// fixture the only other legal-form kinds, the transport/transfer forms 4A/4C (out of scope
// for this correction, still carrying a real `dueAt`), are not currently due in the past
// either, so the assertion is true for the whole page, not merely for 1A/3B rows.
await expect(queue).not.toContainText("passed its deadline");

// Selecting a movement drives the rest of the screen.
await firstRow.click();
Expand Down
Loading
Loading