Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 11 Nov 2026, Held clinical review fixes (PR #1621)

Changed on top of `main` at `5b08a5e`: Home `/ward-flow` raised 575.4 to 575.7; referrals `/ward-flow/referrals` raised 583.6 to 583.9; ward `/ward-flow/ward/rph-adult-secure` raised 613.8 to 614.1; capacity `/ward-flow/capacity` raised 654.3 to 654.7. Each raise is under 1 KB, within the 5 KB per-screen allowance in HOW-WE-WORK section 6.

Measured on hosted CI (production build of the PR head `eda932a`, merged with main): Home 575.4 KB, referrals 583.6 KB, ward 613.8 KB, capacity 654.4 KB, each over its limit after rounding. Main alone measured 575.2, 583.5, 613.6 and 654.1. Each new limit is the measurement plus about 0.3.

Why it changed: the shared bed-forecast code that these routes load grew by 0.1 to 0.3 KB gzipped. Planned admissions now skip a patient who already holds a named bed (`patientsHoldingANamedBed`), and the ED long-wait check moved into one shared `isLongWait` helper (D-145 item 1).

Trim tried: the forecast is already part of each route's first load and the added code is the fix itself, so lazy loading would save nothing. No other safe trim was found.

Later the same day (PR head `65ea28a`, merged with main `c339231`): ED `/ward-flow/ed` raised 535.4 to 535.7. Hosted CI measured 535.4 KB against main alone 535.2 KB; the same shared `isLongWait` helper and named-bed check account for the 0.2 KB. Same trim reasoning as above.
22 changes: 11 additions & 11 deletions docs/ward-flow/journey/MAP-INVENTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The route is selective. Its action notes and the additional command models have source review evidence; source digests detect later edits. This does not establish runtime or clinical acceptance.

## 193 CURRENT ENGINE EVENTS
Source-contract SHA256: e73d1cd81542d3f2030a3f69d3b7b5dc096d8695d24f6ec6a605e18616914ab6
Source-contract SHA256: 058aeabf4009259104c6d772f6126c1c9d7fb94286b54b256eda1dd711025423
- RECORD_ADMISSION_CARE — diagram-reviewed; event roles: ward, coordinator, community
Diagram action notes reviewed against the recorded source revision. Source digests detect subsequent edits; this is not runtime acceptance.
Permission source: src/components/ward-management/ward-flow-event-roles.ts:20
Expand Down Expand Up @@ -1693,7 +1693,7 @@ Source-contract SHA256: e73d1cd81542d3f2030a3f69d3b7b5dc096d8695d24f6ec6a605e186
- BOOK_PLANNED_ADMISSION — modelled; event roles: coordinator, bed_manager, ward
Reviewed command model: New PA-* record in booked state, holding no bed.
Permission source: src/components/ward-management/ward-flow-event-roles.ts:613
Event/dispatch reference: src/components/ward-management/capacity/planned-admissions-reducer.ts:74
Event/dispatch reference: src/components/ward-management/capacity/planned-admissions-reducer.ts:78
Event/dispatch reference: src/components/ward-management/ward-flow-reducer.ts:2340
Subject: A future planned admission
Before: No booking for this linked patient, or an initials-only booking to create
Expand All @@ -1710,14 +1710,14 @@ Source-contract SHA256: e73d1cd81542d3f2030a3f69d3b7b5dc096d8695d24f6ec6a605e186
effects: Stores a required same-ward authorisation override. No bed count or admission changes until conversion; initials invoke the typed-text persistence boundary.
tests: tests/ward-planned-admissions.test.ts
tests: tests/ward-audit24-1-planned-booking-authorised.test.ts
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:299; SHA256 fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:263; SHA256 fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:334; SHA256 d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:298; SHA256 d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df
Reviewed source: src/components/ward-management/ward-flow-events.ts:2431; SHA256 378044cce3b2c98c2c24f4e5bb6ab27f7c361f02a7fa8ab59fa36a77388c84bf
Reviewed source: src/components/ward-management/ward-access-state.ts:659; SHA256 924e9e6ab48fddbf423e5cf988cc09b75df3ace0ce4f4465f379b143957e3d07
- CHANGE_PLANNED_ADMISSION — modelled; event roles: coordinator, bed_manager, ward
Reviewed command model: Still booked, with revised fields, changedAt and incremented changeCount.
Permission source: src/components/ward-management/ward-flow-event-roles.ts:614
Event/dispatch reference: src/components/ward-management/capacity/planned-admissions-reducer.ts:388
Event/dispatch reference: src/components/ward-management/capacity/planned-admissions-reducer.ts:423
Event/dispatch reference: src/components/ward-management/ward-flow-reducer.ts:2345
Subject: An existing booked planned admission
Before: booked plan with current ward/date/stay/reason/legal status/gender
Expand All @@ -1735,14 +1735,14 @@ Source-contract SHA256: e73d1cd81542d3f2030a3f69d3b7b5dc096d8695d24f6ec6a605e186
effects: Replaces planning fields, records changedAt/count and record-derived linked gender. No bed is held or released.
tests: tests/ward-planned-admissions.test.ts
tests: tests/ward-audit24-1-planned-booking-authorised.test.ts
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:383; SHA256 fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:263; SHA256 fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:418; SHA256 d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:298; SHA256 d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df
Reviewed source: src/components/ward-management/ward-flow-events.ts:2488; SHA256 378044cce3b2c98c2c24f4e5bb6ab27f7c361f02a7fa8ab59fa36a77388c84bf
Reviewed source: src/components/ward-management/ward-access-state.ts:659; SHA256 924e9e6ab48fddbf423e5cf988cc09b75df3ace0ce4f4465f379b143957e3d07
- CANCEL_PLANNED_ADMISSION — modelled; event roles: coordinator, bed_manager, ward
Reviewed command model: cancelled with cancel reason and time.
Permission source: src/components/ward-management/ward-flow-event-roles.ts:615
Event/dispatch reference: src/components/ward-management/capacity/planned-admissions-reducer.ts:464
Event/dispatch reference: src/components/ward-management/capacity/planned-admissions-reducer.ts:499
Event/dispatch reference: src/components/ward-management/ward-flow-reducer.ts:2350
Subject: One booked planned admission
Before: booked
Expand All @@ -1755,13 +1755,13 @@ Source-contract SHA256: e73d1cd81542d3f2030a3f69d3b7b5dc096d8695d24f6ec6a605e186
effects: No capacity refund occurs because booking held no bed. Cancelled plans no longer count as pending arrivals.
effects: The step can be taken back by UNDO_RELEASE_STEP within the Undo window, filed against the booking.
tests: tests/ward-planned-admissions.test.ts
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:459; SHA256 fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:494; SHA256 d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df
Reviewed source: src/components/ward-management/ward-flow-events.ts:2488; SHA256 378044cce3b2c98c2c24f4e5bb6ab27f7c361f02a7fa8ab59fa36a77388c84bf
Reviewed source: src/components/ward-management/ward-access-state.ts:659; SHA256 924e9e6ab48fddbf423e5cf988cc09b75df3ace0ce4f4465f379b143957e3d07
- CONVERT_PLANNED_ADMISSION — modelled; event roles: ward, coordinator
Reviewed command model: Plan arrived and linked to a new occupied admission.
Permission source: src/components/ward-management/ward-flow-event-roles.ts:616
Event/dispatch reference: src/components/ward-management/capacity/planned-admissions-reducer.ts:488
Event/dispatch reference: src/components/ward-management/capacity/planned-admissions-reducer.ts:523
Event/dispatch reference: src/components/ward-management/ward-flow-reducer.ts:743
Subject: A booked plan and its receiving ward
Before: booked plan, no current linked bed/journey, ready eligible capacity
Expand All @@ -1777,7 +1777,7 @@ Source-contract SHA256: e73d1cd81542d3f2030a3f69d3b7b5dc096d8695d24f6ec6a605e186
effects: Marks plan arrived with convertedAt/admissionId; does not create a numbered bed assignment or admit merely because its date passed.
tests: tests/ward-planned-admissions.test.ts
tests: tests/ward-planned-arrival-closed-bed.test.ts
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:483; SHA256 fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3
Reviewed source: src/components/ward-management/capacity/planned-admissions-reducer.ts:518; SHA256 d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df
Reviewed source: src/components/ward-management/ward-flow-events.ts:2516; SHA256 378044cce3b2c98c2c24f4e5bb6ab27f7c361f02a7fa8ab59fa36a77388c84bf
Reviewed source: src/components/ward-management/ward-access-state.ts:659; SHA256 924e9e6ab48fddbf423e5cf988cc09b75df3ace0ce4f4465f379b143957e3d07
- RECORD_SUPPORT_NOTIFICATION — modelled; event roles: ward, coordinator
Expand Down
24 changes: 12 additions & 12 deletions docs/ward-flow/journey/event-dispositions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2310,13 +2310,13 @@
"sources": [
{
"path": "src/components/ward-management/capacity/planned-admissions-reducer.ts",
"line": 299,
"sha256": "fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3"
"line": 334,
"sha256": "d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df"
},
{
"path": "src/components/ward-management/capacity/planned-admissions-reducer.ts",
"line": 263,
"sha256": "fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3"
"line": 298,
"sha256": "d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df"
},
{
"path": "src/components/ward-management/ward-flow-events.ts",
Expand Down Expand Up @@ -2361,13 +2361,13 @@
"sources": [
{
"path": "src/components/ward-management/capacity/planned-admissions-reducer.ts",
"line": 383,
"sha256": "fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3"
"line": 418,
"sha256": "d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df"
},
{
"path": "src/components/ward-management/capacity/planned-admissions-reducer.ts",
"line": 263,
"sha256": "fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3"
"line": 298,
"sha256": "d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df"
},
{
"path": "src/components/ward-management/ward-flow-events.ts",
Expand Down Expand Up @@ -2408,8 +2408,8 @@
"sources": [
{
"path": "src/components/ward-management/capacity/planned-admissions-reducer.ts",
"line": 459,
"sha256": "fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3"
"line": 494,
"sha256": "d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df"
},
{
"path": "src/components/ward-management/ward-flow-events.ts",
Expand Down Expand Up @@ -2450,8 +2450,8 @@
"sources": [
{
"path": "src/components/ward-management/capacity/planned-admissions-reducer.ts",
"line": 483,
"sha256": "fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3"
"line": 518,
"sha256": "d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df"
},
{
"path": "src/components/ward-management/ward-flow-events.ts",
Expand Down
4 changes: 2 additions & 2 deletions docs/ward-flow/journey/machines.json
Original file line number Diff line number Diff line change
Expand Up @@ -1738,8 +1738,8 @@
"sources": [
{
"path": "src/components/ward-management/capacity/planned-admissions-reducer.ts",
"line": 584,
"sha256": "fcd8e3b7667a295dc58225efa80ca7dc36863ad203ca4b6873c535c7ae5a0eb3"
"line": 619,
"sha256": "d91a5be952e2110e25d304edcac7c17bede49897d9115701033cc8be1e0c61df"
},
{
"path": "src/components/ward-management/legal-forms/legal-forms-screen.tsx",
Expand Down
2 changes: 1 addition & 1 deletion docs/ward-flow/journey/ward-journey-explorer.html

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions script-budget.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"$comment": "Phone first-load script budgets in KB gzipped, checked by scripts/check-script-budget.mjs after the production build. Set on 16 October 2026 at the measured size plus about 10 KB, and lowered the same day after the rail and search left a phone's first load. Lower a budget when a route shrinks. Raise one only with a reason in the PR. Raise and lowering history: docs/script-budget-history.md up to 4 November 2026, then one file per PR in docs/script-budget-history/ (see its README), not here.",
"budgets": {
"/ward-flow": 575.4,
"/ward-flow/referrals": 583.6,
"/ward-flow/ed": 535.4,
"/ward-flow/ward/rph-adult-secure": 613.8,
"/ward-flow": 575.7,
"/ward-flow/referrals": 583.9,
"/ward-flow/ed": 535.7,
"/ward-flow/ward/rph-adult-secure": 614.1,
"/ward-flow/movements/WF-001": 609.8,
"/ward-flow/capacity": 654.3,
"/ward-flow/capacity": 654.7,
"/ward-flow/ed/arm-ed": 667.4,
"/ward-flow/bed-meeting": 592.2,
"/ward-flow/statistics": 632.6
Expand Down
5 changes: 4 additions & 1 deletion src/components/ward-management/capacity/beds-forecast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { dayOf, MINUTES_PER_DAY, type Instant } from "@/components/ward-manageme
import { isOpen } from "@/components/ward-management/ward-derivations";
import type { BedRelease, Movement, MovementStage, Unit } from "@/components/ward-management/ward-model";
import { bedsForecastSources, type BedsForecastSources } from "./beds-forecast-sources";
import { plannedAdmissionsStillNeedingABed } from "./planned-admissions-reducer";
import { patientsHoldingANamedBed, plannedAdmissionsStillNeedingABed } from "./planned-admissions-reducer";
import { forecastReleaseAt } from "../forecast/forecast-release-date";

/**
Expand Down Expand Up @@ -113,6 +113,8 @@ export function bedsForecast(
const reservations = [...movements, ...referrals];
const readyNow = openUnits.reduce((sum, unit) => sum + bedStates(unit, admissions, [], [], reservations).ready, 0);
const openUnitIds = new Set(openUnits.map((unit) => unit.id));
// A booked person who already holds a named bed is out of Ready once; never count the booking too.
const namedBedHolders = patientsHoldingANamedBed(openUnits, admissions, reservations);
const unitIds = new Set(units.map((unit) => unit.id));
const admissionsById = new Map(admissions.map((admission) => [admission.id, admission]));
const expectedAt = (release: BedRelease) => forecastReleaseAt(release, admissionsById.get(release.admissionId));
Expand Down Expand Up @@ -146,6 +148,7 @@ export function bedsForecast(
until,
waitingMovements,
admissions,
namedBedHolders,
).length;
const bedsNeeded = waitingForBed + planned;
const low = readyNow + confirmed - bedsNeeded;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ export function expectedOccupancy(
.map((unit): OccupancyRow => {
const states = bedStates(unit, admissions, [...releases], [], reservations);
const takenNow = states.occupied + states.pulled;
// The forecast for this ward alone, with nobody waiting: waiting people have no ward yet.
const forecast = bedsForecast([unit], [...releases], [...admissions], [], now, planned);
// The forecast for this ward alone, with nobody waiting: waiting people have no ward yet. The
// named holds go in too, so a booking for someone already holding a bed here (in `takenNow`)
// is not added a second time.
const forecast = bedsForecast([unit], [...releases], [...admissions], [], now, planned, reservations);
return {
unitId: unit.id,
name: unit.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import { closedToAdmissionsRefusal } from "./ward-closure";
import { bedsPendingPreparation } from "../ward-bed-availability";
import { lockedBedsFree, openBedsFree } from "../ward-bed-designation";
import { pendingNumberedReservations } from "../ward-movement-bed-hold";
import {
numberedReservationBeds,
pendingNumberedReservations,
type ReservationReferral,
} from "../ward-movement-bed-hold";
import { pulledBedAvailability } from "../ward-ready-beds";
import {
isPlannedAdmissionCancelReason,
Expand Down Expand Up @@ -173,9 +177,37 @@ function onAnOpenJourney(state: WardFlowState, patientId: string): boolean {
);
}

/**
* The people who already hold a numbered bed by name on one of `units`, by the same rule as
* `numberedReservationBeds`. Their bed is already out of Ready (and in a ward's Pulled count), so a
* booking for the same person must not be counted as fresh demand again (Josh, 11 Nov 2026).
*/
export function patientsHoldingANamedBed(
units: readonly Unit[],
admissions: readonly Admission[],
reservations: readonly (Movement | ReservationReferral)[],
): Set<string> {
const holders = new Set<string>();
for (const row of reservations) {
if (!row.patientId || holders.has(row.patientId)) continue;
// Only a row carrying a bed number can hold one; the full rule then runs for that row alone.
const numbered =
"stage" in row
? row.heldBedNumber !== undefined
: ("destinations" in row ? row.destinations : [row]).some((arm) => arm.heldBedNumber !== undefined);
if (!numbered) continue;
const asMovement = "stage" in row ? [row] : [];
const asReferral = "stage" in row ? [] : [row];
if (units.some((unit) => numberedReservationBeds(unit, admissions, asMovement, asReferral).size > 0))
holders.add(row.patientId);
}
return holders;
}

/**
* The bookings still to count as beds needed: booked, on a known ward, due by `until`, and not a
* linked patient already counted through a waiting movement or already holding a pulled/occupied bed.
* linked patient already counted through a waiting movement, already holding a pulled/occupied bed,
* or already holding a numbered bed by name.
* Initials-only bookings are always counted: nothing links them to a movement.
*/
export function plannedAdmissionsStillNeedingABed(
Expand All @@ -184,10 +216,13 @@ export function plannedAdmissionsStillNeedingABed(
until: Instant,
waitingMovements: readonly Movement[],
admissions: readonly Admission[] = [],
/** People already holding a numbered bed by name (`patientsHoldingANamedBed`): their bed is out of Ready. */
namedBedHolders: Iterable<string> = [],
): PlannedAdmission[] {
const accountedPatientIds = new Set(
waitingMovements.map((movement) => movement.patientId).filter((id): id is NonNullable<typeof id> => Boolean(id)),
);
const accountedPatientIds = new Set<string>([
...waitingMovements.map((movement) => movement.patientId).filter((id): id is NonNullable<typeof id> => Boolean(id)),
...namedBedHolders,
]);
// The booking remains on the calendar after an ordinary movement is pulled or arrives.
// Its bed has already left Ready; do not count that same person as fresh demand again.
for (const admission of admissions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { edById } from "@/components/ward-management/ward-sites";
import {
ED_REFERRED_NOT_SEEN_MINUTES,
ED_SEVERE_PRESSURE_WAIT_MINUTES,
isLongWait,
LATE_ARRIVAL_GRACE_MINUTES,
LONG_WAIT_MINUTES,
LONG_WAIT_TEXT,
Expand Down Expand Up @@ -474,7 +475,7 @@ export function deriveEdScreen({
onAction: () => setArrivalPlanOpenFor(m.id),
});
}
if (m.originEdId === thisEdId && isWaitingInEd(m) && now - m.openedAt > LONG_WAIT_MINUTES) {
if (m.originEdId === thisEdId && isWaitingInEd(m) && isLongWait(now - m.openedAt)) {
priorityFlags.push({
tone: "quiet",
kind: `Waiting ${LONG_WAIT_TEXT}`,
Expand Down
Loading
Loading