<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hospital Desk — Halden General</title>
<!-- Standalone build of HospitalDesk.dc.html: boot.js fetches the product's
     files at runtime, which a bundler cannot follow, so every file is inlined
     below in the order boot.js loads them. Image URLs that live only as strings
     in the product's code are lifted into meta tags; at runtime
     window.__resources holds the inlined blob URLs. -->
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-3.jpg" data-resource-id="r001">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-2.jpg" data-resource-id="r002">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-5.jpg" data-resource-id="r003">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-4.jpg" data-resource-id="r004">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-7.jpg" data-resource-id="r005">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-6.jpg" data-resource-id="r006">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-9.jpg" data-resource-id="r007">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-9.jpg" data-resource-id="r008">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-11.jpg" data-resource-id="r009">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-10.jpg" data-resource-id="r010">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-13.jpg" data-resource-id="r011">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-12.jpg" data-resource-id="r012">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-15.jpg" data-resource-id="r013">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-14.jpg" data-resource-id="r014">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-2.png" data-resource-id="r015">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-3.jpg" data-resource-id="r016">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-4.jpg" data-resource-id="r017">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-5.jpg" data-resource-id="r018">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-6.jpg" data-resource-id="r019">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-7.jpg" data-resource-id="r020">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-8.jpg" data-resource-id="r021">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-13.jpg" data-resource-id="r022">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-10.jpg" data-resource-id="r023">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-15.jpg" data-resource-id="r024">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-12.jpg" data-resource-id="r025">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-m-16.jpg" data-resource-id="r026">
<meta name="ext-resource-dependency" content="../../assets/templates/logos/24-logo-demo.svg" data-resource-id="r027">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-f-17.jpg" data-resource-id="r028">
<script>window.__resources = Object.assign({"r001":"../../assets/img/avatar-f-3.jpg","r002":"../../assets/img/avatar-m-2.jpg","r003":"../../assets/img/avatar-f-5.jpg","r004":"../../assets/img/avatar-m-4.jpg","r005":"../../assets/img/avatar-f-7.jpg","r006":"../../assets/img/avatar-m-6.jpg","r007":"../../assets/img/avatar-f-9.jpg","r008":"../../assets/img/avatar-m-9.jpg","r009":"../../assets/img/avatar-f-11.jpg","r010":"../../assets/img/avatar-m-10.jpg","r011":"../../assets/img/avatar-f-13.jpg","r012":"../../assets/img/avatar-m-12.jpg","r013":"../../assets/img/avatar-f-15.jpg","r014":"../../assets/img/avatar-m-14.jpg","r015":"../../assets/img/avatar-f-2.png","r016":"../../assets/img/avatar-m-3.jpg","r017":"../../assets/img/avatar-f-4.jpg","r018":"../../assets/img/avatar-m-5.jpg","r019":"../../assets/img/avatar-f-6.jpg","r020":"../../assets/img/avatar-m-7.jpg","r021":"../../assets/img/avatar-f-8.jpg","r022":"../../assets/img/avatar-m-13.jpg","r023":"../../assets/img/avatar-f-10.jpg","r024":"../../assets/img/avatar-m-15.jpg","r025":"../../assets/img/avatar-f-12.jpg","r026":"../../assets/img/avatar-m-16.jpg","r027":"../../assets/templates/logos/24-logo-demo.svg","r028":"../../assets/img/avatar-f-17.jpg"}, window.__resources || {});</script>
<link rel="stylesheet" href="../../styles.css">
<link rel="stylesheet" href="./kit.css">
<style>html,body{margin:0;padding:0}body{background:var(--sidebar)}</style>
<template id="__bundler_thumbnail" data-bg-color="#f7f7f6">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg"><rect width="1200" height="800" fill="#f7f7f6"/><rect x="110" y="110" width="150" height="580" rx="16" fill="#e6e6e4"/><rect x="290" y="110" width="230" height="580" rx="16" fill="#ededea"/><g fill="#26262b"><rect x="470" y="330" width="260" height="60" rx="12"/><rect x="570" y="230" width="60" height="260" rx="12"/></g><g fill="#26262b" opacity="0.85"><rect x="800" y="200" width="290" height="26" rx="6"/><rect x="800" y="260" width="290" height="26" rx="6"/><rect x="800" y="320" width="200" height="26" rx="6"/></g></svg>
</template>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/lucide@0.544.0/dist/umd/lucide.min.js" crossorigin="anonymous"></script>
<script src="../../_ds_bundle.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-presets="react" data-file="lib/contracts.js">
(function(){
/* Contract only — permanent machinery, copied into every product folder so that
   deleting the product deletes it too. No domain value may appear in this file:
   if an unrelated product could not use a value with the same meaning, it
   belongs in enums.js or nav.js beside it. These copies start byte-identical
   between products and stay that way — a fix here is a fix everywhere, never a
   local edit.

   Shapes
     Tone          "neutral" | "info" | "success" | "warning" | "danger"
     StatusValue   { id, label, tone }
     StatusField   { entity, field, means, values: StatusValue[] }
     LabelledValue { value, label }
     Route         { path, label, icon?, group?, parent?, badge?,
                     offSidebar?, menuOnly?, roles? }
*/

const TONES = ["neutral", "info", "success", "warning", "danger"];

/* Classification values render through this — never through a Badge. */
function labelOf(list, value) {
  const hit = (list || []).find((i) => i.value === value);
  return hit ? hit.label : value;
}

/* The one place a status id becomes a label + tone. */
function statusValue(values, id) {
  return (values || []).find((v) => v.id === id) || null;
}

function indexRoutes(nav) {
  const index = {};
  (nav || []).forEach((r) => { index[r.path] = r; });
  return index;
}

function matchRoute(path, index) {
  if (index[path]) return index[path];
  return Object.values(index).find((r) =>
    r.path.indexOf(":") > -1 &&
    new RegExp("^" + r.path.replace(/:[^/]+/g, "[^/]+") + "$").test(path)
  ) || null;
}

/* Breadcrumbs read the index, never the nav tree, and never invent a segment:
   an unresolved path returns [] so the screen — not the route table — is fixed. */
function breadcrumb(path, index) {
  const route = matchRoute(path, index);
  if (!route) return [];
  const trail = [route];
  let parent = route.parent;
  while (parent && index[parent]) { trail.unshift(index[parent]); parent = index[parent].parent; }
  return trail.map((s) => ({ label: s.label, path: s.path, isNamespace: !!s.menuOnly }));
}

Object.assign(window, { TONES, labelOf, statusValue, indexRoutes, matchRoute, breadcrumb });

})();
</script>
<script type="text/babel" data-presets="react" data-file="enums.js">
(function(){
/* Ward Desk — this hospital's status vocabulary.
   A value that is not listed here does not exist. No screen may invent a status,
   pass a string literal into a Badge, or assign a tone: StatusBadge reads these
   lists and nothing else does.

   Everything computed from other fields is a DERIVED FLAG, not a status: a bed
   that has been waited on too long, a consent that has not been recorded, a dose
   that is due. Those live in the *_FLAGS blocks below with the clock injected,
   and a record carries one status and any number of flags.

   Tone grouping, one question, product-wide: WHO IS THIS WAITING ON.
     warning  — waiting on this hospital, and somebody here has to act
     info     — running normally, nobody is owed anything
     success  — closed well
     danger   — a failure event, not a deliberate state
     neutral  — closed, or parked on purpose
*/

/* Where a patient is being seen. Stored on the patient, moved by admission and
   discharge; "deceased" is recorded here because every downstream list must stop
   offering clinical actions on the record. */
const PATIENT_STATUS = [
  { id: "outpatient", label: "Outpatient", tone: "neutral" },
  { id: "inpatient", label: "Inpatient", tone: "info" },
  { id: "day_case", label: "Day case", tone: "info" },
  { id: "discharged", label: "Discharged", tone: "success" },
  { id: "deceased", label: "Deceased", tone: "neutral" },
];

/* One episode of care on a ward, in the order it moves.
   awaiting_bed is warning because the bed is this hospital's to find;
   fit_for_discharge is warning for the same reason — the ward is holding a bed
   for somebody who no longer needs it. */
const ADMISSION_STATUS = [
  { id: "awaiting_bed", label: "Awaiting bed", tone: "warning" },
  { id: "on_ward", label: "On ward", tone: "info" },
  { id: "fit_for_discharge", label: "Fit for discharge", tone: "warning" },
  { id: "discharged", label: "Discharged", tone: "success" },
  { id: "transferred", label: "Transferred out", tone: "neutral" },
];

/* A procedure. requested is warning: it is sitting on a scheduler's desk.
   cancelled is neutral, not danger — a cancelled list is a decision, not a
   failure — while abandoned is danger, because the procedure started and could
   not be finished. */
const INTERVENTION_STATUS = [
  { id: "requested", label: "Requested", tone: "warning" },
  { id: "scheduled", label: "Scheduled", tone: "info" },
  { id: "in_theatre", label: "In theatre", tone: "info" },
  { id: "recovery", label: "In recovery", tone: "info" },
  { id: "completed", label: "Completed", tone: "success" },
  { id: "abandoned", label: "Abandoned", tone: "danger" },
  { id: "cancelled", label: "Cancelled", tone: "neutral" },
];

/* A prescription on a chart. awaiting_verification and on_hold are both warning:
   one is owed a pharmacist, the other is owed a prescriber's decision. */
const MEDICATION_STATUS = [
  { id: "awaiting_verification", label: "Awaiting verification", tone: "warning" },
  { id: "active", label: "Active", tone: "info" },
  { id: "on_hold", label: "On hold", tone: "warning" },
  { id: "completed", label: "Course finished", tone: "success" },
  { id: "stopped", label: "Stopped", tone: "neutral" },
];

/* Stock of a formulary line. Deliberately absent: "expiring" — that is derived
   from the earliest batch date and is a flag, not a stored state. */
const STOCK_STATUS = [
  { id: "in_stock", label: "In stock", tone: "success" },
  { id: "low", label: "Low", tone: "warning" },
  { id: "out", label: "Out of stock", tone: "danger" },
  { id: "discontinued", label: "Discontinued", tone: "neutral" },
];

/* Employment state of anybody with a badge — doctors and every other employee
   read one field, because the directory screens are two views of one list. */
const STAFF_STATUS = [
  { id: "onboarding", label: "Onboarding", tone: "info" },
  { id: "active", label: "Active", tone: "success" },
  { id: "on_leave", label: "On leave", tone: "neutral" },
  { id: "suspended", label: "Suspended", tone: "warning" },
  { id: "left", label: "Left", tone: "neutral" },
];

/* An invoice for one episode of care, in the order it moves. awaiting_coding and
   disputed are both warning: one is owed a coder here, the other is owed an
   answer to the payer's query. Deliberately absent because it is DERIVED:
   "overdue" — a due date in the past with a balance outstanding. */
const INVOICE_STATUS = [
  { id: "draft", label: "Draft", tone: "neutral" },
  { id: "awaiting_coding", label: "Awaiting coding", tone: "warning" },
  { id: "issued", label: "Issued", tone: "info" },
  { id: "part_paid", label: "Part paid", tone: "info" },
  { id: "paid", label: "Paid", tone: "success" },
  { id: "disputed", label: "Disputed", tone: "warning" },
  { id: "written_off", label: "Written off", tone: "neutral" },
];

/* One movement of money against an invoice. failed is danger — the money did not
   arrive; refunded is neutral, because sending money back is a decision. */
const PAYMENT_STATUS = [
  { id: "pending", label: "Pending", tone: "info" },
  { id: "cleared", label: "Cleared", tone: "success" },
  { id: "failed", label: "Failed", tone: "danger" },
  { id: "refunded", label: "Refunded", tone: "neutral" },
];

/* An import or an export job. needs_review is warning — rows were parsed but
   somebody has to accept or fix them; failed is danger. */
const TRANSFER_STATUS = [
  { id: "queued", label: "Queued", tone: "neutral" },
  { id: "running", label: "Running", tone: "info" },
  { id: "needs_review", label: "Needs review", tone: "warning" },
  { id: "completed", label: "Completed", tone: "success" },
  { id: "failed", label: "Failed", tone: "danger" },
];

/* A contract with a payer. Deliberately absent because it is DERIVED:
   "expiring" — an end date inside the next 30 days. */
const CONTRACT_STATUS = [
  { id: "live", label: "Live", tone: "success" },
  { id: "expired", label: "Expired", tone: "neutral" },
];

/* ── Classification lists — read through labelOf, never through a Badge ───── */
const WARDS = [
  { value: "beaumont", label: "Beaumont — general surgery" },
  { value: "linden", label: "Linden — acute medicine" },
  { value: "corvid", label: "Corvid — cardiology" },
  { value: "aster", label: "Aster — day surgery" },
  { value: "hollis", label: "Hollis — older people" },
];
const SPECIALTIES = [
  { value: "general_surgery", label: "General surgery" },
  { value: "cardiology", label: "Cardiology" },
  { value: "acute_medicine", label: "Acute medicine" },
  { value: "anaesthetics", label: "Anaesthetics" },
  { value: "geriatrics", label: "Geriatric medicine" },
  { value: "radiology", label: "Radiology" },
];
const JOB_FAMILIES = [
  { value: "medical", label: "Medical" },
  { value: "nursing", label: "Nursing" },
  { value: "pharmacy", label: "Pharmacy" },
  { value: "imaging", label: "Imaging" },
  { value: "admin", label: "Administration" },
  { value: "facilities", label: "Facilities" },
];
const DRUG_ROUTES = [
  { value: "oral", label: "Oral" },
  { value: "iv", label: "Intravenous" },
  { value: "sc", label: "Subcutaneous" },
  { value: "topical", label: "Topical" },
  { value: "inhaled", label: "Inhaled" },
];
const THEATRES = [
  { value: "t1", label: "Theatre 1" },
  { value: "t2", label: "Theatre 2" },
  { value: "t3", label: "Theatre 3" },
  { value: "day", label: "Day-case suite" },
];
const PRIORITIES = [
  { value: "routine", label: "Routine" },
  { value: "urgent", label: "Urgent" },
  { value: "emergency", label: "Emergency" },
];
const ADMISSION_SOURCES = [
  { value: "ed", label: "Emergency department" },
  { value: "elective", label: "Elective list" },
  { value: "gp", label: "GP referral" },
  { value: "transfer", label: "Transfer from another trust" },
];
const TRANSFER_KINDS = [
  { value: "import", label: "Import" },
  { value: "export", label: "Export" },
];
const IMPORT_SOURCES = [
  { value: "pas_csv", label: "Patient administration system — CSV" },
  { value: "lab_hl7", label: "Pathology — HL7 feed" },
  { value: "rota_xlsx", label: "Rota system — XLSX" },
  { value: "formulary_csv", label: "Formulary supplier — CSV" },
];
const SHIFT_TYPES = [
  { value: "day", label: "Day, 07:00–15:00" },
  { value: "late", label: "Late, 14:00–22:00" },
  { value: "night", label: "Night, 21:00–07:30" },
];

const PAYERS = [
  { value: "nhs_commissioner", label: "NHS commissioner" },
  { value: "private_insurer", label: "Private insurer" },
  { value: "self_pay", label: "Self-pay" },
  { value: "overseas", label: "Overseas visitor" },
];
const BILL_LINE_KINDS = [
  { value: "procedure", label: "Procedure" },
  { value: "bed", label: "Bed day" },
  { value: "medication", label: "Medication" },
  { value: "other", label: "Other" },
];
/* The chase ladder, in the order it escalates. One step per contact; the next
   step is derived from the last one, never stored. */
const CHASE_STEPS = [
  { value: "reminder", label: "Reminder emailed" },
  { value: "statement", label: "Statement sent" },
  { value: "call", label: "Called the payer" },
  { value: "final_notice", label: "Final notice" },
  { value: "referred", label: "Referred to the debt agency" },
];
const CHASE_OUTCOMES = [
  { value: "no_reply", label: "No reply" },
  { value: "awaiting", label: "Awaiting reply" },
  { value: "promised", label: "Payment promised" },
  { value: "queried", label: "Queried the bill" },
];

const PAYMENT_METHODS = [
  { value: "remittance", label: "Commissioner remittance" },
  { value: "bank_transfer", label: "Bank transfer" },
  { value: "card", label: "Card" },
];

const STAFF_ROLES = [
  { value: "clinician", label: "Clinician" },
  { value: "ward_manager", label: "Ward manager" },
  { value: "pharmacist", label: "Pharmacist" },
  { value: "coordinator", label: "Coordinator" },
  { value: "read_only", label: "Read only" },
];

/* What a role may do. The screens read these maps, never a name or a job title. */
const ROLE_CAN_VERIFY_MEDICATION = { clinician: false, ward_manager: false, pharmacist: true, coordinator: false, read_only: false };
const ROLE_CAN_MOVE_ADMISSION = { clinician: true, ward_manager: true, pharmacist: false, coordinator: true, read_only: false };
const ROLE_CAN_RECORD_CONSENT = { clinician: true, ward_manager: false, pharmacist: false, coordinator: false, read_only: false };
const ROLE_CAN_BILL = { clinician: false, ward_manager: true, pharmacist: false, coordinator: true, read_only: false };
const ROLE_CAN_IMPORT = { clinician: false, ward_manager: true, pharmacist: true, coordinator: true, read_only: false };

/* ── Derived flags — computed at render, never stored, clock injected ─────── */
const DAY_MS = 864e5;
const HOUR_MS = 36e5;

const ADMISSION_FLAGS = {
  /* A bed that has been waited on for more than four hours is the hospital's
     own breach clock, not a status. */
  bedWaitBreached: (a, now) => a.status === "awaiting_bed" && now - a.requestedAt > 4 * HOUR_MS,
  /* Fit for discharge yesterday or earlier: the bed is being held. */
  dischargeHeld: (a, now) => a.status === "fit_for_discharge" && now - a.fitAt > DAY_MS,
  lengthOfStayDays: (a, now) => Math.max(0, Math.round(((a.dischargedAt || now) - a.admittedAt) / DAY_MS)),
  overExpectedStay: (a, now) => !a.dischargedAt && ADMISSION_FLAGS.lengthOfStayDays(a, now) > a.expectedStayDays,
};

const INTERVENTION_FLAGS = {
  /* Consent is a record, not a status: a scheduled procedure without it cannot
     start, and that is what the theatre list is asking. */
  consentMissing: (i) => (i.status === "scheduled" || i.status === "requested") && !i.consentAt,
  isToday: (i, now) => !!i.scheduledAt && new Date(i.scheduledAt).toDateString() === new Date(now).toDateString(),
  unscheduled: (i) => i.status === "requested" && !i.scheduledAt,
};

const MEDICATION_FLAGS = {
  doseDue: (o, now) => o.status === "active" && !!o.nextDoseAt && o.nextDoseAt - now <= HOUR_MS,
  doseMissed: (o, now) => o.status === "active" && !!o.nextDoseAt && now - o.nextDoseAt > HOUR_MS,
  awaitingPharmacy: (o) => o.status === "awaiting_verification",
};

const PATIENT_FLAGS = {
  hasAllergyAlert: (p) => (p.allergies || []).length > 0,
  reviewOverdue: (p, now) => p.status === "inpatient" && !!p.lastReviewAt && now - p.lastReviewAt > DAY_MS,
};

/* A shift is understaffed when fewer people are rostered onto it than the ward
   establishment asks for. The establishment is data (fixtures), not an enum, and
   the shortfall is derived at render — never stored. */
const SHIFT_FLAGS = {
  understaffed: (rostered, establishment) => rostered.length < establishment,
  shortfall: (rostered, establishment) => Math.max(0, establishment - rostered.length),
};

/* Overdue is derived from the due date and the balance, never stored: an invoice
   carries a status and any number of flags. */
const INVOICE_FLAGS = {
  overdue: (invoice, balance, now) => balance > 0 && !!invoice.dueAt && invoice.dueAt < now && ["issued", "part_paid", "disputed"].indexOf(invoice.status) > -1,
  ageDays: (invoice, now) => (invoice.issuedAt ? Math.max(0, Math.round((now - invoice.issuedAt) / DAY_MS)) : 0),
  billable: (invoice) => ["issued", "part_paid", "paid", "disputed"].indexOf(invoice.status) > -1,
};

/* Contracts and the prices under them. Both flags are computed at render. */
const CONTRACT_FLAGS = {
  expiringSoon: (contract, now) => contract.status === "live" && !!contract.expiresAt && contract.expiresAt - now <= 30 * DAY_MS,
  daysLeft: (contract, now) => (contract.expiresAt ? Math.round((contract.expiresAt - now) / DAY_MS) : null),
};

/* What the chase ladder says to do next. A disputed invoice is never chased —
   the query is answered first — so it is not in the worklist at all. */
function nextChaseStep(lastStep) {
  if (!lastStep) return CHASE_STEPS[0];
  const index = CHASE_STEPS.findIndex((s) => s.value === lastStep);
  return index > -1 && index < CHASE_STEPS.length - 1 ? CHASE_STEPS[index + 1] : null;
}

function chaseTransitions(row, viewer) {
  const may = ROLE_CAN_BILL[viewer.staffRole];
  const next = nextChaseStep(row.lastStep);
  if (!next) return [];
  const gate = may ? {} : { disabled: true, disabledReason: "Chasing a payer is a ward manager or coordinator job. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." };
  return [Object.assign({ id: "chase", label: next.label, primary: true, step: next.value }, gate)];
}

const FORMULARY_FLAGS = {
  belowReorder: (d) => d.onHand <= d.reorderAt && d.stock !== "discontinued",
};

/* ── Transitions — one function per entity, called by every surface ────────
   The set derives from the current status. A transition that does not apply is
   omitted; one that exists but the signed-in person may not take is disabled
   with the reason stated (AGENTS §1.2). One transition is primary: the record's
   normal path. */
function admissionTransitions(admission, viewer) {
  const may = ROLE_CAN_MOVE_ADMISSION[viewer.staffRole];
  const gate = (t) => Object.assign({}, t, may ? {} : { disabled: true, disabledReason: "Only a ward manager, a coordinator or a clinician can move an admission. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." });
  if (admission.status === "awaiting_bed") return [gate({ id: "assign_bed", label: "Assign bed", primary: true }), gate({ id: "transfer_out", label: "Transfer out" })];
  if (admission.status === "on_ward") return [gate({ id: "mark_fit", label: "Mark fit for discharge", primary: true }), gate({ id: "transfer_out", label: "Transfer out" })];
  if (admission.status === "fit_for_discharge") return [gate({ id: "discharge", label: "Discharge", primary: true })];
  return [];
}

function interventionTransitions(intervention, viewer) {
  const consent = ROLE_CAN_RECORD_CONSENT[viewer.staffRole]
    ? {}
    : { disabled: true, disabledReason: "Consent is recorded by the clinician taking it. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." };
  const out = [];
  if (intervention.status === "requested") out.push({ id: "schedule", label: "Schedule", primary: true });
  if (intervention.status === "scheduled") {
    if (!intervention.consentAt) out.push(Object.assign({ id: "record_consent", label: "Record consent", primary: true }, consent));
    else out.push({ id: "start", label: "Start in theatre", primary: true });
  }
  if (intervention.status === "in_theatre") out.push({ id: "to_recovery", label: "Move to recovery", primary: true });
  if (intervention.status === "recovery") out.push({ id: "complete", label: "Complete", primary: true });
  if (["requested", "scheduled"].indexOf(intervention.status) > -1) out.push({ id: "cancel", label: "Cancel procedure" });
  if (intervention.status === "in_theatre") out.push({ id: "abandon", label: "Record as abandoned", destructive: true });
  return out;
}

function medicationTransitions(order, viewer) {
  const verifyGate = ROLE_CAN_VERIFY_MEDICATION[viewer.staffRole]
    ? {}
    : { disabled: true, disabledReason: "Only a pharmacist can verify an order. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." };
  if (order.status === "awaiting_verification") return [Object.assign({ id: "verify", label: "Verify order", primary: true }, verifyGate)];
  if (order.status === "active") return [{ id: "record_dose", label: "Record dose", primary: true }, { id: "hold", label: "Put on hold" }, { id: "stop", label: "Stop course", destructive: true }];
  if (order.status === "on_hold") return [{ id: "resume", label: "Resume", primary: true }, { id: "stop", label: "Stop course", destructive: true }];
  return [];
}

/* Billing transitions. Writing an invoice off is irreversible and opens a
   confirm dialog; everything else applies with an undo toast. Only a ward manager
   or a coordinator may bill (ROLE_CAN_BILL). */
function invoiceTransitions(invoice, viewer, balance) {
  const may = ROLE_CAN_BILL[viewer.staffRole];
  const gate = (t) => Object.assign({}, t, may ? {} : { disabled: true, disabledReason: "Billing is a ward manager or coordinator job. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." });
  if (invoice.status === "draft") return [gate({ id: "send_for_coding", label: "Send for coding", primary: true })];
  if (invoice.status === "awaiting_coding") return [gate({ id: "issue", label: "Issue invoice", primary: true })];
  if (invoice.status === "issued" || invoice.status === "part_paid") {
    return [gate({ id: "record_payment", label: "Record payment", primary: true }), gate({ id: "dispute", label: "Mark disputed" }), gate({ id: "write_off", label: "Write off " + (balance != null ? "the balance" : "invoice"), destructive: true })];
  }
  if (invoice.status === "disputed") return [gate({ id: "resolve", label: "Resolve dispute", primary: true }), gate({ id: "write_off", label: "Write off the balance", destructive: true })];
  return [];
}

function transferTransitions(job) {
  if (job.status === "needs_review") return [{ id: "review_rows", label: "Review rows", primary: true }];
  if (job.status === "failed") return [{ id: "retry_job", label: "Run again", primary: true }];
  if (job.status === "completed" && job.kind === "export") return [{ id: "download_file", label: "Download", primary: true }];
  return [];
}

/* Access to the badge system. Suspending is consequential and reversible, so it
   raises an undo toast rather than a confirm dialog; nothing here deletes a
   person, because an employee who has left keeps their record. */
function staffTransitions(person, viewer) {
  const may = ROLE_CAN_MOVE_ADMISSION[viewer.staffRole] && viewer.staffRole !== "clinician";
  const gate = (t) => Object.assign({}, t, may ? {} : { disabled: true, disabledReason: "Changing somebody's access is a ward manager or coordinator job. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." });
  if (person.id === viewer.id) return [];
  if (person.status === "onboarding") return [gate({ id: "activate", label: "Mark active", primary: true })];
  if (person.status === "active") return [gate({ id: "suspend", label: "Suspend access" })];
  if (person.status === "suspended") return [gate({ id: "restore", label: "Restore access", primary: true })];
  if (person.status === "on_leave") return [gate({ id: "returned", label: "Mark returned", primary: true })];
  return [];
}

/* Registry — what a status field means, for the reader of this file. */
const STATUS_FIELDS = [
  { entity: "patient", field: "status", means: "where this person is being seen", values: PATIENT_STATUS },
  { entity: "admission", field: "status", means: "where one episode of ward care has got to", values: ADMISSION_STATUS },
  { entity: "intervention", field: "status", means: "where one procedure has got to", values: INTERVENTION_STATUS },
  { entity: "medicationOrder", field: "status", means: "whether a prescription may be given", values: MEDICATION_STATUS },
  { entity: "formularyLine", field: "stock", means: "whether the pharmacy can dispense it", values: STOCK_STATUS },
  { entity: "staff", field: "status", means: "whether this employee is working", values: STAFF_STATUS },
  { entity: "transfer", field: "status", means: "where an import or export job has got to", values: TRANSFER_STATUS },
  { entity: "invoice", field: "status", means: "where the bill for one episode of care has got to", values: INVOICE_STATUS },
  { entity: "payment", field: "status", means: "whether the money has arrived", values: PAYMENT_STATUS },
  { entity: "contract", field: "status", means: "whether a payer's price list still applies", values: CONTRACT_STATUS },
];

/* Coverage — values with no fixture yet: patient.deceased, intervention.abandoned,
   staff.left, transfer.queued, invoice.written_off, payment.refunded, contract.expired. They are reachable in the running product (an
   abandoned procedure and a queued job are both one commit away) but no seeded
   record carries them, so no screen claims a count for them. */

Object.assign(window, {
  PATIENT_STATUS, ADMISSION_STATUS, INTERVENTION_STATUS, MEDICATION_STATUS, STOCK_STATUS, STAFF_STATUS, TRANSFER_STATUS, INVOICE_STATUS, PAYMENT_STATUS, CONTRACT_STATUS,
  WARDS, SPECIALTIES, JOB_FAMILIES, DRUG_ROUTES, THEATRES, PRIORITIES, ADMISSION_SOURCES, TRANSFER_KINDS, IMPORT_SOURCES, STAFF_ROLES, SHIFT_TYPES, PAYERS, BILL_LINE_KINDS, PAYMENT_METHODS, CHASE_STEPS, CHASE_OUTCOMES,
  ROLE_CAN_VERIFY_MEDICATION, ROLE_CAN_MOVE_ADMISSION, ROLE_CAN_RECORD_CONSENT, ROLE_CAN_IMPORT, ROLE_CAN_BILL,
  ADMISSION_FLAGS, INTERVENTION_FLAGS, MEDICATION_FLAGS, PATIENT_FLAGS, FORMULARY_FLAGS, SHIFT_FLAGS, INVOICE_FLAGS, CONTRACT_FLAGS,
  admissionTransitions, interventionTransitions, medicationTransitions, transferTransitions, staffTransitions, invoiceTransitions, chaseTransitions, nextChaseStep,
  STATUS_FIELDS, DAY_MS, HOUR_MS,
});

})();
</script>
<script type="text/babel" data-presets="react" data-file="nav.js">
(function(){
/* Ward Desk — the route table.
   Deliberately NOT routes: the ⌘K palette, the notifications panel, the rail's
   category flyout, every row menu, every filter bar, the import wizard dialog,
   the bed-assignment dialog and every confirm dialog. They are states of a
   screen. A breadcrumb that wants one of them is a bug in the screen, not a
   missing route.

   Twenty top-level destinations over six categories, dense record work and ten
   per-destination counts, so this product runs the SIDEBAR shell in its
   RAIL + PANEL mode: the rail owns the category, the panel owns the destinations
   inside it. The category is not a route — it is the sectioning device, so a
   category holds 2–4 destinations rather than §6.3's 5–9 for a labelled group,
   and the panel header names it instead of an uppercase group label.

   Parameterised routes exist where the thing has its own controls and its own
   numbers: a patient (their admission, chart, procedures and the notes written
   on them), a procedure (its theatre slot, consent and team), a member of staff
   (what they are responsible for and what they have written). Registering a new
   patient is a route rather than a dialog because it is a real form.

   Settings is two screens off the sidebar, reached from the account menu. The
   auth namespace carries sign-in and the whole password reset flow; signup, MFA
   and lockout are not built, so nothing links to them. */

const HD_CATEGORIES = [
  { id: "care", label: "Care", note: "Wards, patients and theatre", icon: "stethoscope" },
  { id: "pharmacy", label: "Pharmacy", note: "Prescribing and stock", icon: "pill" },
  { id: "people", label: "People", note: "Who is on the badge system", icon: "users-round" },
  { id: "finance", label: "Finance", note: "What care cost and who owes it", icon: "landmark" },
  { id: "business", label: "Business", note: "Contracts, debt and what care costs", icon: "briefcase" },
  { id: "workspace", label: "Workspace", note: "The hospital's own record", icon: "message-circle" },
];

const HD_NAV = [
  /* Care */
  { path: "/", label: "Today", icon: "layout-dashboard", category: "care" },
  { path: "/patients", label: "Patients", icon: "contact-round", category: "care" },
  { path: "/admissions", label: "Admissions", icon: "bed", badge: "bedsWaiting", category: "care" },
  { path: "/interventions", label: "Interventions", icon: "scissors", badge: "consentMissing", category: "care" },
  { path: "/wards", label: "Wards and beds", icon: "layout-grid", category: "care" },

  /* Pharmacy */
  { path: "/medication", label: "Medication orders", icon: "pill", badge: "ordersAwaitingPharmacy", category: "pharmacy" },
  { path: "/rounds", label: "Drug round", icon: "clipboard-check", badge: "dosesDueOrMissed", category: "pharmacy" },
  { path: "/formulary", label: "Formulary and stock", icon: "package", category: "pharmacy" },

  /* Finance */
  { path: "/invoices", label: "Invoices", icon: "file-text", badge: "invoicesToCode", category: "finance" },
  { path: "/payments", label: "Payments", icon: "banknote", category: "finance" },
  { path: "/revenue", label: "Revenue and debt", icon: "chart-line", category: "finance" },

  /* Business */
  { path: "/contracts", label: "Contracts and tariffs", icon: "file-signature", badge: "linesOffContract", category: "business" },
  { path: "/credit-control", label: "Credit control", icon: "phone-call", badge: "invoicesOverdue", category: "business" },
  { path: "/margin", label: "Cost and margin", icon: "scale", badge: "thinMargins", category: "business" },

  /* People */
  { path: "/doctors", label: "Doctors", icon: "stethoscope", category: "people" },
  { path: "/employees", label: "Employees", icon: "id-card", category: "people" },
  { path: "/rota", label: "Rota", icon: "calendar-days", badge: "shiftsUnderstaffed", category: "people" },

  /* Workspace */
  { path: "/feed", label: "Feed", icon: "message-circle", badge: "mentionsOwed", category: "workspace" },
  { path: "/reports", label: "Reports", icon: "chart-line", category: "workspace" },
  { path: "/data", label: "Import and export", icon: "arrow-down-up", badge: "transfersNeedingReview", category: "workspace" },

  /* Registered, not in the nav. */
  { path: "/patients/new", label: "New patient", parent: "/patients", offSidebar: true },
  { path: "/patients/:id", label: "Patient", parent: "/patients", offSidebar: true },
  { path: "/interventions/:id", label: "Intervention", parent: "/interventions", offSidebar: true },
  { path: "/invoices/:id", label: "Invoice", parent: "/invoices", offSidebar: true },
  { path: "/doctors/:id", label: "Doctor", parent: "/doctors", offSidebar: true },
  { path: "/employees/:id", label: "Employee", parent: "/employees", offSidebar: true },
  { path: "/me/profile", label: "Your profile", offSidebar: true },

  /* Settings: one namespace, seven registered screens. They are off the sidebar
     and reached from the account menu; the within-destination level is a tab
     strip on the page, derived from this table (§5, §6.3) — never a third
     navigation column. */
  { path: "/settings", label: "Hospital", offSidebar: true, group: "Settings" },
  { path: "/settings/wards", label: "Wards and beds", parent: "/settings", offSidebar: true, group: "Settings" },
  { path: "/settings/roles", label: "Roles and permissions", parent: "/settings", offSidebar: true, group: "Settings" },
  { path: "/settings/billing", label: "Billing", parent: "/settings", offSidebar: true, group: "Settings" },
  { path: "/settings/integrations", label: "Integrations and feeds", parent: "/settings", offSidebar: true, group: "Settings" },
  { path: "/settings/notifications", label: "Notifications", parent: "/settings", offSidebar: true, group: "Settings" },
  { path: "/settings/security", label: "Security", parent: "/settings", offSidebar: true, group: "Settings" },

  /* Auth namespace — excluded from the shell nav; no product chrome. The reset
     flow is all four screens AGENTS §7.5 requires, plus the expired-token state:
     request, the sent confirmation, set-new behind the token, the outcome and the
     dead link. Signup, MFA and lockout are not built, so nothing links to them. */
  { path: "/signin", label: "Sign in", offSidebar: true },
  { path: "/forgot-password", label: "Forgot password", offSidebar: true },
  { path: "/forgot-password/sent", label: "Reset link sent", parent: "/forgot-password", offSidebar: true },
  { path: "/reset-password", label: "Set a new password", offSidebar: true },
  { path: "/reset-password/expired", label: "Reset link expired", parent: "/reset-password", offSidebar: true },
  { path: "/reset-password/done", label: "Password changed", parent: "/reset-password", offSidebar: true },
];

const HD_ROUTES = indexRoutes(HD_NAV);

/* Projections the shell renders — derived by filtering the tree, never retyped. */
const HD_RAIL = HD_CATEGORIES.map((category) => Object.assign({}, category, {
  items: HD_NAV.filter((r) => !r.offSidebar && r.category === category.id),
}));

/* Which category a path belongs to — the rail's active state is derived from the
   route, exactly like the panel's. Settings, your profile and auth belong to no
   category, so this returns null and the shell keeps whichever category the
   reader was already in rather than snapping them back to the first one. */
function categoryOf(path) {
  const route = matchRoute(path, HD_ROUTES);
  if (!route) return null;
  if (route.category) return route.category;
  let node = route;
  while (node && node.parent && HD_ROUTES[node.parent]) {
    node = HD_ROUTES[node.parent];
    if (node.category) return node.category;
  }
  return null;
}

/* The settings tab strip — the route table filtered, never retyped. */
const HD_SETTINGS_NAV = HD_NAV.filter((r) => r.group === "Settings");

/* The account menu carries registered routes plus sign out (§6.2). Settings is
   one entry: its own tab strip carries the seven screens inside it. */
const HD_ACCOUNT_MENU = [HD_ROUTES["/me/profile"], HD_ROUTES["/settings"]];

/* Paths that render without product chrome (AGENTS §7.1). */
const HD_AUTH_PATHS = ["/signin", "/forgot-password", "/forgot-password/sent", "/reset-password", "/reset-password/expired", "/reset-password/done"];

/* /patients/:id → /patients/pat_04. Breadcrumbs and every nested row use it, so
   no screen builds a path by concatenation. */
const resolvePath = (pattern, params) => pattern.replace(/:([^/]+)/g, (_, key) => (params && params[key] != null ? params[key] : ":" + key));

/* The preview menu's page list — this table read whole and grouped, with the
   parameterised routes resolved against the fixture so each link lands on a real
   record rather than on ":id". */
const HD_PAGES = (data) => {
  const patient = data.patients[0];
  const intervention = data.interventions[0];
  const invoice = data.invoices[0];
  const doctor = data.staff.find((s) => s.family === "medical");
  const employee = data.staff.find((s) => s.family !== "medical");
  return templatePages(HD_NAV, HD_AUTH_PATHS, {
    "/patients/:id": patient ? resolvePath("/patients/:id", { id: patient.id }) : null,
    "/interventions/:id": intervention ? resolvePath("/interventions/:id", { id: intervention.id }) : null,
    "/invoices/:id": invoice ? resolvePath("/invoices/:id", { id: invoice.id }) : null,
    "/doctors/:id": doctor ? resolvePath("/doctors/:id", { id: doctor.id }) : null,
    "/employees/:id": employee ? resolvePath("/employees/:id", { id: employee.id }) : null,
  });
};

Object.assign(window, { HD_CATEGORIES, HD_NAV, HD_ROUTES, HD_RAIL, HD_SETTINGS_NAV, HD_ACCOUNT_MENU, HD_AUTH_PATHS, HD_PAGES, categoryOf, resolvePath });

})();
</script>
<script type="text/babel" data-presets="react" data-file="fixtures.js">
(function(){
/* Ward Desk — one fixture for the whole product.
   Every screen reads these arrays; no screen writes a second literal to match.
   The clock is frozen so the derived flags are reproducible: Tuesday 17 March
   2026, 09:20 on the Beaumont ward.

   The notes, the feed and the notifications are authored AGAINST these records:
   a note that names a bed wait, a consent or a stock line agrees with that
   record's status and timeline at the moment it was written. */

const HD_NOW = Date.parse("2026-03-17T09:20:00Z");
const hoursAgo = (h) => HD_NOW - h * HOUR_MS;
const hoursOn = (h) => HD_NOW + h * HOUR_MS;
const daysAgo = (d) => HD_NOW - d * DAY_MS;
const dayAt = (offset, hour, minute) => {
  const d = new Date(HD_NOW + offset * DAY_MS);
  d.setHours(hour, minute || 0, 0, 0);
  return d.getTime();
};

const HD_HOSPITAL = {
  name: "Halden General Hospital",
  short: "Halden General",
  trust: "Halden and Westmere NHS Trust",
  timezone: "Europe/London",
  recordsEmail: "records@haldengeneral.nhs.uk",
  retentionYears: 8,
};

const HD_SIGNED_IN = "stf_03";

/* ── Staff: doctors and every other employee in one list ─────────────────── */
const STAFF = [
  { id: "stf_01", name: "Adaeze Okonkwo", email: "a.okonkwo@haldengeneral.nhs.uk", family: "medical", jobTitle: "Consultant general surgeon", specialty: "general_surgery", staffRole: "clinician", status: "active", ward: "beaumont", avatar: (window.__resources["r001"]), startedAt: daysAgo(2190), phone: "ext 4402", gmc: "GMC 7104882", timezone: "Europe/London" },
  { id: "stf_02", name: "Rasmus Feld", email: "r.feld@haldengeneral.nhs.uk", family: "medical", jobTitle: "Consultant cardiologist", specialty: "cardiology", staffRole: "clinician", status: "active", ward: "corvid", avatar: (window.__resources["r002"]), startedAt: daysAgo(1560), phone: "ext 4419", gmc: "GMC 6620145", timezone: "Europe/London" },
  { id: "stf_03", name: "Imogen Vance", email: "i.vance@haldengeneral.nhs.uk", family: "nursing", jobTitle: "Ward manager", specialty: null, staffRole: "ward_manager", status: "active", ward: "beaumont", avatar: (window.__resources["r003"]), startedAt: daysAgo(1204), phone: "ext 4380", gmc: null, timezone: "Europe/London" },
  { id: "stf_04", name: "Tobias Lenz", email: "t.lenz@haldengeneral.nhs.uk", family: "medical", jobTitle: "Registrar, acute medicine", specialty: "acute_medicine", staffRole: "clinician", status: "active", ward: "linden", avatar: (window.__resources["r004"]), startedAt: daysAgo(640), phone: "ext 4455", gmc: "GMC 7391066", timezone: "Europe/London" },
  { id: "stf_05", name: "Priya Raghunathan", email: "p.raghunathan@haldengeneral.nhs.uk", family: "pharmacy", jobTitle: "Lead pharmacist", specialty: null, staffRole: "pharmacist", status: "active", ward: null, avatar: (window.__resources["r005"]), startedAt: daysAgo(1830), phone: "ext 4210", gmc: null, timezone: "Europe/London" },
  { id: "stf_06", name: "Marek Solis", email: "m.solis@haldengeneral.nhs.uk", family: "nursing", jobTitle: "Staff nurse", specialty: null, staffRole: "clinician", status: "active", ward: "linden", avatar: (window.__resources["r006"]), startedAt: daysAgo(410), phone: "ext 4391", gmc: null, timezone: "Europe/London" },
  { id: "stf_07", name: "Hanne Dahl", email: "h.dahl@haldengeneral.nhs.uk", family: "admin", jobTitle: "Theatre coordinator", specialty: null, staffRole: "coordinator", status: "active", ward: null, avatar: (window.__resources["r007"]), startedAt: daysAgo(980), phone: "ext 4501", gmc: null, timezone: "Europe/London" },
  { id: "stf_08", name: "Yusuf Baraka", email: "y.baraka@haldengeneral.nhs.uk", family: "imaging", jobTitle: "Radiographer", specialty: "radiology", staffRole: "coordinator", status: "active", ward: null, avatar: (window.__resources["r008"]), startedAt: daysAgo(720), phone: "ext 4622", gmc: null, timezone: "Europe/London" },
  { id: "stf_09", name: "Cordelia Mbeki", email: "c.mbeki@haldengeneral.nhs.uk", family: "medical", jobTitle: "Consultant anaesthetist", specialty: "anaesthetics", staffRole: "clinician", status: "on_leave", ward: null, avatar: (window.__resources["r009"]), startedAt: daysAgo(2010), phone: "ext 4470", gmc: "GMC 6902377", timezone: "Europe/London", leaveUntil: daysAgo(-11) },
  { id: "stf_10", name: "Ivo Petrescu", email: "i.petrescu@haldengeneral.nhs.uk", family: "facilities", jobTitle: "Portering lead", specialty: null, staffRole: "read_only", status: "active", ward: null, avatar: (window.__resources["r010"]), startedAt: daysAgo(2560), phone: "ext 4118", gmc: null, timezone: "Europe/London" },
  { id: "stf_11", name: "Saoirse Whelan", email: "s.whelan@haldengeneral.nhs.uk", family: "nursing", jobTitle: "Staff nurse", specialty: null, staffRole: "clinician", status: "onboarding", ward: "beaumont", avatar: (window.__resources["r011"]), startedAt: daysAgo(-4), phone: "ext 4398", gmc: null, timezone: "Europe/London" },
  { id: "stf_12", name: "Emeka Nwosu", email: "e.nwosu@haldengeneral.nhs.uk", family: "pharmacy", jobTitle: "Pharmacy technician", specialty: null, staffRole: "read_only", status: "active", ward: null, avatar: (window.__resources["r012"]), startedAt: daysAgo(300), phone: "ext 4216", gmc: null, timezone: "Europe/London" },
  { id: "stf_13", name: "Beatrix Halloran", email: "b.halloran@haldengeneral.nhs.uk", family: "medical", jobTitle: "Consultant geriatrician", specialty: "geriatrics", staffRole: "clinician", status: "active", ward: "hollis", avatar: (window.__resources["r013"]), startedAt: daysAgo(3120), phone: "ext 4433", gmc: "GMC 5540912", timezone: "Europe/London" },
  { id: "stf_14", name: "Dmitri Volkov", email: "d.volkov@haldengeneral.nhs.uk", family: "admin", jobTitle: "Records officer", specialty: null, staffRole: "coordinator", status: "suspended", ward: null, avatar: (window.__resources["r014"]), startedAt: daysAgo(1440), phone: "ext 4507", gmc: null, timezone: "Europe/London", suspendedNote: "Access suspended pending an information-governance review opened 4 March." },
];

/* ── Patients ────────────────────────────────────────────────────────────── */
const PATIENTS = [
  { id: "pat_01", name: "Ruth Adeyemi", number: "H-40-2291", born: "14 Feb 1958", sex: "Female", status: "inpatient", avatar: (window.__resources["r015"]), allergies: ["Penicillin"], lastReviewAt: hoursAgo(30), gp: "Fern Lane Surgery", phone: "07700 900 118", registeredAt: daysAgo(1820), nextOfKin: "Deborah Adeyemi (daughter)" },
  { id: "pat_02", name: "Callum Frayne", number: "H-40-4013", born: "3 Sep 1991", sex: "Male", status: "inpatient", avatar: (window.__resources["r016"]), allergies: [], lastReviewAt: hoursAgo(2), gp: "Halden Road Practice", phone: "07700 900 204", registeredAt: daysAgo(430), nextOfKin: "Erin Frayne (wife)" },
  { id: "pat_03", name: "Margit Sølve", number: "H-40-1188", born: "22 Jun 1947", sex: "Female", status: "inpatient", avatar: (window.__resources["r017"]), allergies: ["Latex", "Codeine"], lastReviewAt: hoursAgo(14), gp: "Westmere Health Centre", phone: "07700 900 331", registeredAt: daysAgo(2600), nextOfKin: "Anton Sølve (son)" },
  { id: "pat_04", name: "Desmond Ayo", number: "H-40-3376", born: "9 Nov 1962", sex: "Male", status: "inpatient", avatar: (window.__resources["r018"]), allergies: [], lastReviewAt: hoursAgo(40), gp: "Fern Lane Surgery", phone: "07700 900 447", registeredAt: daysAgo(900), nextOfKin: "Ada Ayo (wife)" },
  { id: "pat_05", name: "Ivy Trethewey", number: "H-40-2740", born: "30 Jan 1939", sex: "Female", status: "inpatient", avatar: (window.__resources["r019"]), allergies: ["Aspirin"], lastReviewAt: hoursAgo(6), gp: "Halden Road Practice", phone: "07700 900 512", registeredAt: daysAgo(3400), nextOfKin: "Peter Trethewey (son)" },
  { id: "pat_06", name: "Hakim Barzani", number: "H-40-3902", born: "17 Mar 1955", sex: "Male", status: "inpatient", avatar: (window.__resources["r020"]), allergies: [], lastReviewAt: hoursAgo(4), gp: "Westmere Health Centre", phone: "07700 900 660", registeredAt: daysAgo(1120), nextOfKin: "Layla Barzani (daughter)" },
  { id: "pat_07", name: "Nour Haddad", number: "H-40-4402", born: "5 Jul 1984", sex: "Female", status: "inpatient", avatar: (window.__resources["r021"]), allergies: ["Iodine contrast"], lastReviewAt: hoursAgo(3), gp: "Fern Lane Surgery", phone: "07700 900 771", registeredAt: daysAgo(260), nextOfKin: "Samir Haddad (husband)" },
  { id: "pat_08", name: "Gordon Mealy", number: "H-40-2015", born: "28 Apr 1970", sex: "Male", status: "discharged", avatar: (window.__resources["r022"]), allergies: [], lastReviewAt: daysAgo(6), gp: "Halden Road Practice", phone: "07700 900 802", registeredAt: daysAgo(2200), nextOfKin: "Rita Mealy (sister)" },
  { id: "pat_09", name: "Eluned Pryce", number: "H-40-1663", born: "12 Dec 1936", sex: "Female", status: "inpatient", avatar: (window.__resources["r023"]), allergies: ["Penicillin", "Sulfonamides"], lastReviewAt: hoursAgo(20), gp: "Westmere Health Centre", phone: "07700 900 915", registeredAt: daysAgo(4100), nextOfKin: "Gareth Pryce (son)" },
  { id: "pat_10", name: "Anselm Dubois", number: "H-40-3120", born: "1 Aug 1978", sex: "Male", status: "discharged", avatar: (window.__resources["r024"]), allergies: [], lastReviewAt: daysAgo(9), gp: "Out of area", phone: "07700 900 118", registeredAt: daysAgo(680), nextOfKin: "Colette Dubois (wife)" },
  { id: "pat_11", name: "Farida Kettle", number: "H-40-4521", born: "19 May 1996", sex: "Female", status: "day_case", avatar: (window.__resources["r025"]), allergies: [], lastReviewAt: hoursAgo(22), gp: "Halden Road Practice", phone: "07700 900 227", registeredAt: daysAgo(90), nextOfKin: "Jonah Kettle (brother)" },
  { id: "pat_12", name: "Stefan Kovač", number: "H-40-4560", born: "2 Oct 1988", sex: "Male", status: "outpatient", avatar: (window.__resources["r026"]), allergies: [], lastReviewAt: daysAgo(12), gp: "Fern Lane Surgery", phone: "07700 900 340", registeredAt: daysAgo(45), nextOfKin: "Not recorded" },
];

/* ── Admissions: one episode of ward care each ───────────────────────────── */
const ADMISSIONS = [
  { id: "adm_01", patientId: "pat_01", ward: "linden", bed: null, status: "awaiting_bed", source: "ed", reason: "Chest pain, troponin rise — needs a monitored bed", requestedAt: hoursAgo(5), admittedAt: null, fitAt: null, dischargedAt: null, expectedStayDays: 3, consultantId: "stf_04" },
  { id: "adm_02", patientId: "pat_02", ward: "beaumont", bed: null, status: "awaiting_bed", source: "ed", reason: "Right iliac fossa pain, surgical review", requestedAt: hoursAgo(2), admittedAt: null, fitAt: null, dischargedAt: null, expectedStayDays: 2, consultantId: "stf_01" },
  { id: "adm_03", patientId: "pat_03", ward: "beaumont", bed: "B-04", status: "on_ward", source: "elective", reason: "Gallstone disease, for cholecystectomy", requestedAt: daysAgo(4), admittedAt: daysAgo(3), fitAt: null, dischargedAt: null, expectedStayDays: 4, consultantId: "stf_01" },
  { id: "adm_04", patientId: "pat_04", ward: "corvid", bed: "C-02", status: "on_ward", source: "gp", reason: "Unstable angina, awaiting angiography", requestedAt: daysAgo(10), admittedAt: daysAgo(9), fitAt: null, dischargedAt: null, expectedStayDays: 5, consultantId: "stf_02" },
  { id: "adm_05", patientId: "pat_05", ward: "beaumont", bed: "B-07", status: "fit_for_discharge", source: "ed", reason: "Infected surgical wound, debrided 15 March", requestedAt: daysAgo(7), admittedAt: daysAgo(6), fitAt: hoursAgo(30), dischargedAt: null, expectedStayDays: 5, consultantId: "stf_01" },
  { id: "adm_06", patientId: "pat_06", ward: "hollis", bed: "H-01", status: "fit_for_discharge", source: "gp", reason: "Iron-deficiency anaemia, scoped this morning", requestedAt: daysAgo(12), admittedAt: daysAgo(11), fitAt: hoursAgo(5), dischargedAt: null, expectedStayDays: 9, consultantId: "stf_13" },
  { id: "adm_07", patientId: "pat_07", ward: "linden", bed: "L-05", status: "on_ward", source: "ed", reason: "Sepsis of unknown source, on IV antibiotics", requestedAt: daysAgo(1), admittedAt: daysAgo(1), fitAt: null, dischargedAt: null, expectedStayDays: 3, consultantId: "stf_04" },
  { id: "adm_08", patientId: "pat_08", ward: "aster", bed: "D-02", status: "discharged", source: "elective", reason: "Appendicectomy, uncomplicated", requestedAt: daysAgo(9), admittedAt: daysAgo(8), fitAt: daysAgo(7), dischargedAt: daysAgo(6), expectedStayDays: 2, consultantId: "stf_01" },
  { id: "adm_09", patientId: "pat_09", ward: "hollis", bed: "H-06", status: "on_ward", source: "ed", reason: "Fractured neck of femur, for hemiarthroplasty", requestedAt: daysAgo(4), admittedAt: daysAgo(4), fitAt: null, dischargedAt: null, expectedStayDays: 7, consultantId: "stf_13" },
  { id: "adm_10", patientId: "pat_10", ward: "corvid", bed: "C-05", status: "transferred", source: "transfer", reason: "Transferred to Westmere for electrophysiology", requestedAt: daysAgo(15), admittedAt: daysAgo(14), fitAt: null, dischargedAt: daysAgo(9), expectedStayDays: 4, consultantId: "stf_02" },
];

/* ── Interventions ───────────────────────────────────────────────────────── */
const INTERVENTIONS = [
  { id: "int_01", patientId: "pat_03", name: "Laparoscopic cholecystectomy", specialty: "general_surgery", priority: "routine", status: "scheduled", theatre: "t1", requestedAt: daysAgo(3), scheduledAt: dayAt(0, 11, 0), consentAt: hoursAgo(19), surgeonId: "stf_01", anaesthetistId: "stf_09", plannedMin: 90, completedAt: null, notes: "Day-three inpatient, fasted from midnight." },
  { id: "int_02", patientId: "pat_11", name: "Inguinal hernia repair", specialty: "general_surgery", priority: "routine", status: "scheduled", theatre: "day", requestedAt: daysAgo(18), scheduledAt: dayAt(0, 14, 0), consentAt: null, surgeonId: "stf_01", anaesthetistId: null, plannedMin: 60, completedAt: null, notes: "Day case. Consent not on the record — she arrived at 08:40." },
  { id: "int_03", patientId: "pat_04", name: "Coronary angiography", specialty: "cardiology", priority: "urgent", status: "scheduled", theatre: "t2", requestedAt: daysAgo(8), scheduledAt: dayAt(1, 9, 0), consentAt: null, surgeonId: "stf_02", anaesthetistId: null, plannedMin: 45, completedAt: null, notes: "Ninth day on Corvid; the list has slipped twice." },
  { id: "int_04", patientId: "pat_07", name: "Central line insertion", specialty: "acute_medicine", priority: "urgent", status: "in_theatre", theatre: "t3", requestedAt: hoursAgo(20), scheduledAt: hoursAgo(1), consentAt: hoursAgo(3), surgeonId: "stf_04", anaesthetistId: null, plannedMin: 40, completedAt: null, notes: "Started 08:20 for antibiotic access." },
  { id: "int_05", patientId: "pat_05", name: "Wound debridement", specialty: "general_surgery", priority: "urgent", status: "completed", theatre: "t1", requestedAt: daysAgo(3), scheduledAt: daysAgo(2), consentAt: daysAgo(2), surgeonId: "stf_01", anaesthetistId: "stf_09", plannedMin: 45, completedAt: daysAgo(2), notes: "Healthy margins; dressing changed daily." },
  { id: "int_06", patientId: "pat_09", name: "Hip hemiarthroplasty", specialty: "general_surgery", priority: "urgent", status: "requested", theatre: null, requestedAt: hoursAgo(26), scheduledAt: null, consentAt: null, surgeonId: "stf_13", anaesthetistId: null, plannedMin: 120, completedAt: null, notes: "Fourth day since the fall — she needs a slot." },
  { id: "int_07", patientId: "pat_06", name: "Upper GI endoscopy", specialty: "geriatrics", priority: "routine", status: "recovery", theatre: "t2", requestedAt: daysAgo(6), scheduledAt: hoursAgo(3), consentAt: daysAgo(1), surgeonId: "stf_13", anaesthetistId: null, plannedMin: 30, completedAt: null, notes: "In recovery since 08:40; awaiting sedation score." },
  { id: "int_08", patientId: "pat_01", name: "CT-guided drainage", specialty: "radiology", priority: "emergency", status: "requested", theatre: null, requestedAt: hoursAgo(4), scheduledAt: null, consentAt: null, surgeonId: "stf_04", anaesthetistId: null, plannedMin: 60, completedAt: null, notes: "Requested from ED at the same time as the bed." },
  { id: "int_09", patientId: "pat_08", name: "Appendicectomy", specialty: "general_surgery", priority: "emergency", status: "completed", theatre: "t1", requestedAt: daysAgo(8), scheduledAt: daysAgo(8), consentAt: daysAgo(8), surgeonId: "stf_01", anaesthetistId: "stf_09", plannedMin: 75, completedAt: daysAgo(8), notes: "Uncomplicated; discharged two days later." },
  { id: "int_10", patientId: "pat_12", name: "Skin lesion excision", specialty: "general_surgery", priority: "routine", status: "scheduled", theatre: "day", requestedAt: daysAgo(20), scheduledAt: dayAt(6, 10, 30), consentAt: null, surgeonId: "stf_01", anaesthetistId: null, plannedMin: 30, completedAt: null, notes: "Outpatient; consent at pre-assessment on the day." },
];

/* ── Medication orders ───────────────────────────────────────────────────── */
const MEDICATION_ORDERS = [
  { id: "med_01", patientId: "pat_07", drug: "Piperacillin-tazobactam", dose: "4.5 g", route: "iv", frequency: "Three times daily", status: "active", indication: "Sepsis of unknown source", prescriberId: "stf_04", verifiedById: "stf_05", startedAt: daysAgo(1), nextDoseAt: hoursOn(0.5), lastGivenAt: hoursAgo(7.5), endsAt: daysAgo(-4), drugId: "drg_02" },
  { id: "med_02", patientId: "pat_01", drug: "Enoxaparin", dose: "40 mg", route: "sc", frequency: "Once daily", status: "awaiting_verification", indication: "Thromboprophylaxis", prescriberId: "stf_04", verifiedById: null, startedAt: hoursAgo(4), nextDoseAt: hoursOn(9), lastGivenAt: null, endsAt: null, drugId: "drg_05" },
  { id: "med_03", patientId: "pat_02", drug: "Morphine sulfate", dose: "5 mg", route: "iv", frequency: "As required, up to four hourly", status: "awaiting_verification", indication: "Abdominal pain", prescriberId: "stf_01", verifiedById: null, startedAt: hoursAgo(1.5), nextDoseAt: null, lastGivenAt: null, endsAt: null, drugId: "drg_07" },
  { id: "med_04", patientId: "pat_09", drug: "Paracetamol", dose: "1 g", route: "oral", frequency: "Four times daily", status: "active", indication: "Hip pain", prescriberId: "stf_13", verifiedById: "stf_05", startedAt: daysAgo(4), nextDoseAt: hoursAgo(2), lastGivenAt: hoursAgo(8), endsAt: null, drugId: "drg_01" },
  { id: "med_05", patientId: "pat_03", drug: "Ondansetron", dose: "4 mg", route: "iv", frequency: "As required, eight hourly", status: "active", indication: "Post-operative nausea cover", prescriberId: "stf_01", verifiedById: "stf_05", startedAt: hoursAgo(19), nextDoseAt: hoursOn(6), lastGivenAt: hoursAgo(2), endsAt: null, drugId: "drg_04" },
  { id: "med_06", patientId: "pat_04", drug: "Bisoprolol", dose: "2.5 mg", route: "oral", frequency: "Once daily", status: "on_hold", indication: "Rate control — held before angiography", prescriberId: "stf_02", verifiedById: "stf_05", startedAt: daysAgo(9), nextDoseAt: null, lastGivenAt: daysAgo(1), endsAt: null, drugId: "drg_06" },
  { id: "med_07", patientId: "pat_05", drug: "Flucloxacillin", dose: "1 g", route: "iv", frequency: "Four times daily", status: "active", indication: "Wound infection", prescriberId: "stf_01", verifiedById: "stf_05", startedAt: daysAgo(5), nextDoseAt: hoursOn(2.5), lastGivenAt: hoursAgo(3.5), endsAt: daysAgo(-2), drugId: "drg_03" },
  { id: "med_08", patientId: "pat_06", drug: "Ferrous fumarate", dose: "210 mg", route: "oral", frequency: "Twice daily", status: "active", indication: "Iron-deficiency anaemia", prescriberId: "stf_13", verifiedById: "stf_05", startedAt: daysAgo(10), nextDoseAt: hoursOn(3), lastGivenAt: hoursAgo(9), endsAt: null, drugId: "drg_08" },
  { id: "med_09", patientId: "pat_03", drug: "Co-amoxiclav", dose: "1.2 g", route: "iv", frequency: "Three times daily", status: "awaiting_verification", indication: "Surgical prophylaxis for today's list", prescriberId: "stf_01", verifiedById: null, startedAt: hoursAgo(0.75), nextDoseAt: null, lastGivenAt: null, endsAt: null, drugId: "drg_09" },
  { id: "med_10", patientId: "pat_08", drug: "Paracetamol", dose: "1 g", route: "oral", frequency: "Four times daily", status: "completed", indication: "Post-operative pain", prescriberId: "stf_01", verifiedById: "stf_05", startedAt: daysAgo(8), nextDoseAt: null, lastGivenAt: daysAgo(6), endsAt: daysAgo(6), drugId: "drg_01" },
  { id: "med_11", patientId: "pat_10", drug: "Amiodarone", dose: "200 mg", route: "oral", frequency: "Once daily", status: "stopped", indication: "Stopped on transfer to Westmere", prescriberId: "stf_02", verifiedById: "stf_05", startedAt: daysAgo(13), nextDoseAt: null, lastGivenAt: daysAgo(9), endsAt: daysAgo(9), drugId: "drg_10" },
  { id: "med_12", patientId: "pat_09", drug: "Enoxaparin", dose: "40 mg", route: "sc", frequency: "Once daily", status: "active", indication: "Thromboprophylaxis", prescriberId: "stf_13", verifiedById: "stf_05", startedAt: daysAgo(4), nextDoseAt: hoursOn(11), lastGivenAt: hoursAgo(13), endsAt: null, drugId: "drg_05" },
];

/* ── Formulary and stock ─────────────────────────────────────────────────── */
const FORMULARY = [
  { id: "drg_01", name: "Paracetamol", form: "500 mg tablet", route: "oral", stock: "in_stock", onHand: 2400, reorderAt: 600, unit: "tablets", supplier: "Trust wholesaler", earliestExpiry: daysAgo(-420), unitCostMinor: 4, controlled: false },
  { id: "drg_02", name: "Piperacillin-tazobactam", form: "4.5 g vial", route: "iv", stock: "low", onHand: 34, reorderAt: 40, unit: "vials", supplier: "Halden pharmacy production", earliestExpiry: daysAgo(-95), unitCostMinor: 2100, controlled: false },
  { id: "drg_03", name: "Flucloxacillin", form: "1 g vial", route: "iv", stock: "in_stock", onHand: 180, reorderAt: 60, unit: "vials", supplier: "Trust wholesaler", earliestExpiry: daysAgo(-260), unitCostMinor: 320, controlled: false },
  { id: "drg_04", name: "Ondansetron", form: "4 mg/2 ml ampoule", route: "iv", stock: "in_stock", onHand: 210, reorderAt: 80, unit: "ampoules", supplier: "Trust wholesaler", earliestExpiry: daysAgo(-310), unitCostMinor: 180, controlled: false },
  { id: "drg_05", name: "Enoxaparin", form: "40 mg syringe", route: "sc", stock: "low", onHand: 52, reorderAt: 75, unit: "syringes", supplier: "Trust wholesaler", earliestExpiry: daysAgo(-140), unitCostMinor: 490, controlled: false },
  { id: "drg_06", name: "Bisoprolol", form: "2.5 mg tablet", route: "oral", stock: "in_stock", onHand: 900, reorderAt: 300, unit: "tablets", supplier: "Trust wholesaler", earliestExpiry: daysAgo(-500), unitCostMinor: 6, controlled: false },
  { id: "drg_07", name: "Morphine sulfate", form: "10 mg/1 ml ampoule", route: "iv", stock: "out", onHand: 0, reorderAt: 30, unit: "ampoules", supplier: "Controlled drugs cupboard", earliestExpiry: null, unitCostMinor: 220, controlled: true },
  { id: "drg_08", name: "Ferrous fumarate", form: "210 mg tablet", route: "oral", stock: "in_stock", onHand: 640, reorderAt: 200, unit: "tablets", supplier: "Trust wholesaler", earliestExpiry: daysAgo(-380), unitCostMinor: 5, controlled: false },
  { id: "drg_09", name: "Co-amoxiclav", form: "1.2 g vial", route: "iv", stock: "in_stock", onHand: 145, reorderAt: 60, unit: "vials", supplier: "Trust wholesaler", earliestExpiry: daysAgo(-210), unitCostMinor: 280, controlled: false },
  { id: "drg_10", name: "Amiodarone", form: "200 mg tablet", route: "oral", stock: "discontinued", onHand: 0, reorderAt: 0, unit: "tablets", supplier: "Withdrawn — cardiology switched to sotalol", earliestExpiry: null, unitCostMinor: 30, controlled: false },
];

/* ── The social layer: notes written on records, and the feed that reads them ── */
const COMMENTS = [
  { id: "cmt_01", record: { type: "admission", id: "adm_01" }, authorId: "stf_04", at: hoursAgo(4.5), body: "ED has held her five hours now. @stf_03 is there anything on Linden after the 11:00 discharge round?", mentions: ["stf_03"], reactions: {} },
  { id: "cmt_02", record: { type: "admission", id: "adm_01" }, authorId: "stf_07", at: hoursAgo(3), body: "Portering can move her the moment a bed is named — Ivo has a team free until noon.", mentions: [], reactions: { ack: ["stf_04"] } },
  { id: "cmt_03", record: { type: "intervention", id: "int_02" }, authorId: "stf_07", at: hoursAgo(0.6), body: "Consent still not on Farida's record and she is second on the day-case list. @stf_01 can you see her before 14:00?", mentions: ["stf_01"], reactions: {} },
  { id: "cmt_04", record: { type: "intervention", id: "int_06" }, authorId: "stf_13", at: hoursAgo(22), body: "Fourth day since the fall. She needs a slot tomorrow at the latest — every day here costs her mobility.", mentions: [], reactions: { agree: ["stf_01", "stf_03"] } },
  { id: "cmt_05", record: { type: "intervention", id: "int_06" }, authorId: "stf_07", at: hoursAgo(19), body: "Theatre 3 has a 13:30 gap tomorrow if anaesthetics can cover it. @stf_09 is on leave until the 28th.", mentions: ["stf_09"], reactions: {} },
  { id: "cmt_06", record: { type: "medication", id: "med_03" }, authorId: "stf_05", at: hoursAgo(1), body: "Morphine is out of stock in the cupboard — I will not verify this until we have ampoules back. @stf_01 oral oxycodone in the meantime?", mentions: ["stf_01"], reactions: {} },
  { id: "cmt_07", record: { type: "medication", id: "med_04" }, authorId: "stf_06", at: hoursAgo(1.8), body: "Dose was due 07:20 and has not gone in. She was off the ward for imaging — flagged to the nurse in charge.", mentions: [], reactions: { seen: ["stf_03"] } },
  { id: "cmt_08", record: { type: "patient", id: "pat_05" }, authorId: "stf_03", at: hoursAgo(28), body: "Fit for discharge since yesterday morning; we are waiting on a district-nurse dressing visit. Family can collect any time after 10:00.", mentions: [], reactions: { agree: ["stf_01"] } },
  { id: "cmt_09", record: { type: "patient", id: "pat_04" }, authorId: "stf_02", at: hoursAgo(26), body: "Ninth day, angiography now booked for 09:00 tomorrow. Bisoprolol held from this morning. @stf_03 please keep him nil by mouth from midnight.", mentions: ["stf_03"], reactions: {} },
  { id: "cmt_10", record: { type: "patient", id: "pat_09" }, authorId: "stf_06", at: hoursAgo(9), body: "Slept better; pain score 3 with regular paracetamol. Daughter asked about the operating list.", mentions: [], reactions: { seen: ["stf_13"] } },
  { id: "cmt_11", record: { type: "patient", id: "pat_01" }, authorId: "stf_03", at: hoursAgo(2.2), body: "Penicillin allergy is on the front of her chart. Drainage request went in at 05:20 alongside the bed request.", mentions: [], reactions: {} },
  { id: "cmt_12", record: { type: "transfer", id: "trf_02" }, authorId: "stf_14", at: hoursAgo(21), body: "Twelve rows from the PAS export have no hospital number. They need a human before I let them near the patient index.", mentions: ["stf_03"], reactions: {} },
  { id: "cmt_13", record: { type: "formulary", id: "drg_07" }, authorId: "stf_12", at: hoursAgo(7), body: "Controlled-drugs order placed with the wholesaler this morning; delivery expected Thursday.", mentions: [], reactions: { ack: ["stf_05"] } },
  { id: "cmt_15", record: { type: "invoice", id: "inv_10" }, authorId: "stf_07", at: hoursAgo(30), body: "The commissioner is right that the stay ran to eleven days — two of those were waiting on a district-nurse visit, not on care. @stf_03 can you confirm the dates so I can answer them?", mentions: ["stf_03"], reactions: {} },
  { id: "cmt_16", record: { type: "invoice", id: "inv_08" }, authorId: "stf_14", at: hoursAgo(18), body: "Bank transfer came back this morning. Letter sent to the address on the record; nothing else to do until she replies.", mentions: [], reactions: { seen: ["stf_07"] } },
  { id: "cmt_14", record: { type: "intervention", id: "int_07" }, authorId: "stf_06", at: hoursAgo(0.4), body: "Sedation score 1, observations stable. Ready to go back to Hollis as soon as a porter is free.", mentions: [], reactions: {} },
];

const ACTIVITY = [
  { id: "act_01", actorId: "stf_04", at: hoursAgo(0.3), verb: "requested a bed", record: { type: "admission", id: "adm_02" }, note: "Right iliac fossa pain from ED" },
  { id: "act_02", actorId: "stf_13", at: hoursAgo(3), verb: "moved to recovery", record: { type: "intervention", id: "int_07" }, note: null },
  { id: "act_03", actorId: "stf_04", at: hoursAgo(1), verb: "started in theatre", record: { type: "intervention", id: "int_04" }, note: "Theatre 3" },
  { id: "act_04", actorId: "stf_05", at: hoursAgo(1.1), verb: "commented", record: { type: "medication", id: "med_03" }, note: null },
  { id: "act_05", actorId: "stf_01", at: hoursAgo(19), verb: "recorded consent", record: { type: "intervention", id: "int_01" }, note: null },
  { id: "act_06", actorId: "stf_03", at: hoursAgo(28), verb: "commented", record: { type: "patient", id: "pat_05" }, note: null },
  { id: "act_07", actorId: "stf_13", at: hoursAgo(5), verb: "marked fit for discharge", record: { type: "admission", id: "adm_06" }, note: null },
  { id: "act_08", actorId: "stf_05", at: hoursAgo(6), verb: "verified an order", record: { type: "medication", id: "med_07" }, note: null },
  { id: "act_09", actorId: "stf_14", at: hoursAgo(21), verb: "imported records", record: { type: "transfer", id: "trf_02" }, note: "318 rows from the patient administration system" },
  { id: "act_10", actorId: "stf_03", at: hoursAgo(23), verb: "exported a report", record: { type: "transfer", id: "trf_01" }, note: "Beaumont ward board, CSV" },
  { id: "act_11", actorId: "stf_07", at: hoursAgo(19), verb: "commented", record: { type: "intervention", id: "int_06" }, note: null },
  { id: "act_12", actorId: "stf_02", at: hoursAgo(26), verb: "commented", record: { type: "patient", id: "pat_04" }, note: null },
  { id: "act_13", actorId: "stf_01", at: daysAgo(2), verb: "completed a procedure", record: { type: "intervention", id: "int_05" }, note: "Wound debridement, 45 minutes" },
  { id: "act_14", actorId: "stf_12", at: hoursAgo(7), verb: "commented", record: { type: "formulary", id: "drg_07" }, note: null },
  { id: "act_15", actorId: "stf_07", at: daysAgo(3), verb: "recorded a payment", record: { type: "invoice", id: "inv_01" }, note: "Commissioner remittance REM-88214" },
  { id: "act_16", actorId: "stf_14", at: hoursAgo(18), verb: "commented", record: { type: "invoice", id: "inv_08" }, note: null },
  { id: "act_17", actorId: "stf_07", at: daysAgo(1), verb: "issued an invoice", record: { type: "invoice", id: "inv_03" }, note: null },
];

/* ── Import and export jobs ─────────────────────────────────────────────── */
const TRANSFERS = [
  { id: "trf_01", kind: "export", label: "Beaumont ward board", scope: "Admissions on Beaumont, 17 March", format: "CSV", source: null, status: "completed", rows: 4, accepted: 4, rejected: 0, pending: 0, startedAt: hoursAgo(23), finishedAt: hoursAgo(23), byId: "stf_03", fileName: "beaumont-board-17-03-2026.csv", sizeKb: 6 },
  { id: "trf_02", kind: "import", label: "Patient administration system", scope: "Nightly patient index", format: "CSV", source: "pas_csv", status: "needs_review", rows: 318, accepted: 306, rejected: 0, pending: 12, startedAt: hoursAgo(21.5), finishedAt: hoursAgo(21), byId: "stf_14", fileName: "pas-index-16-03-2026.csv", sizeKb: 412 },
  { id: "trf_03", kind: "import", label: "Pathology feed", scope: "Results for Linden and Corvid", format: "HL7", source: "lab_hl7", status: "running", rows: 96, accepted: 71, rejected: 0, pending: 0, startedAt: hoursAgo(0.4), finishedAt: null, byId: "stf_08", fileName: "path-feed-live", sizeKb: 88 },
  { id: "trf_04", kind: "import", label: "Rota system", scope: "March nursing rota", format: "XLSX", source: "rota_xlsx", status: "failed", rows: 0, accepted: 0, rejected: 0, pending: 0, startedAt: daysAgo(1), finishedAt: daysAgo(1), byId: "stf_07", fileName: "nursing-rota-march.xlsx", sizeKb: 240, failure: "The sheet named “Week 12” is missing the ward column, so no row could be matched to a ward." },
  { id: "trf_05", kind: "export", label: "Theatre utilisation", scope: "All interventions, 1–16 March", format: "XLSX", source: null, status: "completed", rows: 61, accepted: 61, rejected: 0, pending: 0, startedAt: daysAgo(1.2), finishedAt: daysAgo(1.2), byId: "stf_07", fileName: "theatre-utilisation-01-16-03.xlsx", sizeKb: 74 },
  { id: "trf_06", kind: "import", label: "Formulary supplier catalogue", scope: "Price and pack-size update", format: "CSV", source: "formulary_csv", status: "completed", rows: 1204, accepted: 1204, rejected: 0, pending: 0, startedAt: daysAgo(3), finishedAt: daysAgo(3), byId: "stf_05", fileName: "supplier-catalogue-14-03.csv", sizeKb: 980 },
  { id: "trf_07", kind: "export", label: "Medication administration record", scope: "Active orders, all wards", format: "CSV", source: null, status: "completed", rows: 7, accepted: 7, rejected: 0, pending: 0, startedAt: daysAgo(2), finishedAt: daysAgo(2), byId: "stf_05", fileName: "mar-active-15-03-2026.csv", sizeKb: 11 },
];

/* The twelve rows the PAS import could not place. The review screen executes on
   these, so the accept and reject counts on trf_02 move with what is decided. */
const TRANSFER_ROWS = {
  trf_02: [
    { line: 14, name: "Kettle, Farida", problem: "No hospital number", detail: "Matches H-40-4521 on name and date of birth" },
    { line: 27, name: "Ayo, Desmond", problem: "No hospital number", detail: "Matches H-40-3376 on name and date of birth" },
    { line: 38, name: "Unknown", problem: "Date of birth unreadable", detail: "Field reads “00/00/0000”" },
    { line: 51, name: "Pryce, Eluned", problem: "Duplicate of line 50", detail: "Both lines carry H-40-1663" },
    { line: 66, name: "Sølve, Margit", problem: "No hospital number", detail: "Matches H-40-1188 on name and date of birth" },
    { line: 90, name: "Frayne, Callum", problem: "Ward not recognised", detail: "Ward code “BEA2” is not a Halden ward" },
    { line: 104, name: "Unknown", problem: "Name field empty", detail: "Only a GP code was supplied" },
    { line: 133, name: "Barzani, Hakim", problem: "No hospital number", detail: "Matches H-40-3902 on name and date of birth" },
    { line: 178, name: "Dubois, Anselm", problem: "Ward not recognised", detail: "Ward code “WST” belongs to Westmere" },
    { line: 205, name: "Unknown", problem: "Date of birth unreadable", detail: "Field reads “31/02/1954”" },
    { line: 261, name: "Trethewey, Ivy", problem: "Duplicate of line 259", detail: "Both lines carry H-40-2740" },
    { line: 300, name: "Adeyemi, Ruth", problem: "No hospital number", detail: "Matches H-40-2291 on name and date of birth" },
  ],
};

/* Invoices. Every line names the record it bills — an intervention, an
   admission's bed days, a medication course — so a line is checkable against the
   care record rather than being a plausible amount. Totals are NOT stored: they
   are the sum of the lines, and what has been paid is the sum of the cleared
   payments below. Amounts are pence. */
const INVOICES = [
  { id: "inv_01", patientId: "pat_08", payer: "nhs_commissioner", status: "paid", reference: "HG-2026-0331", issuedAt: daysAgo(5), dueAt: daysAgo(-25), codedById: "stf_07", note: null,
    lines: [
      { label: "Emergency appendicectomy", kind: "procedure", ref: { type: "intervention", id: "int_09" }, qty: 1, unitMinor: 241000 },
      { label: "Day-surgery bed day, Aster", kind: "bed", ref: { type: "admission", id: "adm_08" }, qty: 2, unitMinor: 48000 },
    ] },
  { id: "inv_02", patientId: "pat_10", payer: "private_insurer", status: "part_paid", reference: "HG-2026-0318", issuedAt: daysAgo(8), dueAt: daysAgo(-22), codedById: "stf_07", note: "Insurer settles in two instalments by agreement.",
    lines: [
      { label: "Cardiology inpatient care", kind: "bed", ref: { type: "admission", id: "adm_10" }, qty: 5, unitMinor: 52000 },
      { label: "Telemetry monitoring", kind: "other", ref: null, qty: 5, unitMinor: 9000 },
    ] },
  { id: "inv_03", patientId: "pat_05", payer: "nhs_commissioner", status: "issued", reference: "HG-2026-0342", issuedAt: daysAgo(1), dueAt: daysAgo(-29), codedById: "stf_07", note: null,
    lines: [
      { label: "Wound debridement", kind: "procedure", ref: { type: "intervention", id: "int_05" }, qty: 1, unitMinor: 118000 },
      { label: "Surgical bed day, Beaumont", kind: "bed", ref: { type: "admission", id: "adm_05" }, qty: 6, unitMinor: 48000 },
    ] },
  { id: "inv_04", patientId: "pat_03", payer: "nhs_commissioner", status: "draft", reference: "HG-2026-0349", issuedAt: null, dueAt: null, codedById: null, note: "Opens for coding when today's cholecystectomy is recorded as completed.",
    lines: [
      { label: "Laparoscopic cholecystectomy", kind: "procedure", ref: { type: "intervention", id: "int_01" }, qty: 1, unitMinor: 294000 },
      { label: "Surgical bed day, Beaumont", kind: "bed", ref: { type: "admission", id: "adm_03" }, qty: 3, unitMinor: 48000 },
    ] },
  { id: "inv_05", patientId: "pat_11", payer: "self_pay", status: "draft", reference: "HG-2026-0350", issuedAt: null, dueAt: null, codedById: null, note: "Quoted at pre-assessment; payable before the day-case list.",
    lines: [{ label: "Inguinal hernia repair, day case", kind: "procedure", ref: { type: "intervention", id: "int_02" }, qty: 1, unitMinor: 219000 }] },
  { id: "inv_06", patientId: "pat_04", payer: "nhs_commissioner", status: "awaiting_coding", reference: "HG-2026-0344", issuedAt: null, dueAt: null, codedById: null, note: "The angiography is not billed until it happens; bed days are coded to date.",
    lines: [
      { label: "Cardiology inpatient care", kind: "bed", ref: { type: "admission", id: "adm_04" }, qty: 9, unitMinor: 52000 },
      { label: "Telemetry monitoring", kind: "other", ref: null, qty: 9, unitMinor: 9000 },
    ] },
  { id: "inv_07", patientId: "pat_09", payer: "nhs_commissioner", status: "issued", reference: "HG-2026-0296", issuedAt: daysAgo(33), dueAt: daysAgo(3), codedById: "stf_14", note: null,
    lines: [
      { label: "Older people's inpatient care", kind: "bed", ref: { type: "admission", id: "adm_09" }, qty: 4, unitMinor: 44000 },
      { label: "Fractured neck of femur pathway", kind: "other", ref: null, qty: 1, unitMinor: 68000 },
    ] },
  { id: "inv_08", patientId: "pat_07", payer: "overseas", status: "issued", reference: "HG-2026-0347", issuedAt: daysAgo(2), dueAt: daysAgo(-12), codedById: "stf_07", note: "Overseas visitor; the trust invoices the patient directly.",
    lines: [
      { label: "Acute medicine inpatient care", kind: "bed", ref: { type: "admission", id: "adm_07" }, qty: 1, unitMinor: 51000 },
      { label: "Central line insertion", kind: "procedure", ref: { type: "intervention", id: "int_04" }, qty: 1, unitMinor: 62000 },
      { label: "Intravenous antibiotic course", kind: "medication", ref: { type: "medication", id: "med_01" }, qty: 1, unitMinor: 31000 },
    ] },
  { id: "inv_09", patientId: "pat_12", payer: "self_pay", status: "draft", reference: "HG-2026-0351", issuedAt: null, dueAt: null, codedById: null, note: "Quoted for the excision booked in six days.",
    lines: [{ label: "Skin lesion excision, day case", kind: "procedure", ref: { type: "intervention", id: "int_10" }, qty: 1, unitMinor: 68000 }] },
  { id: "inv_10", patientId: "pat_06", payer: "nhs_commissioner", status: "disputed", reference: "HG-2026-0309", issuedAt: daysAgo(9), dueAt: daysAgo(-21), codedById: "stf_14", note: "The commissioner queries 11 bed days against a 9-day expected stay.",
    lines: [
      { label: "Upper GI endoscopy", kind: "procedure", ref: { type: "intervention", id: "int_07" }, qty: 1, unitMinor: 96000 },
      { label: "Older people's inpatient care", kind: "bed", ref: { type: "admission", id: "adm_06" }, qty: 11, unitMinor: 44000 },
    ] },
];

/* ── Contracts, tariffs and what care costs ───────────────────────────────
   A tariff item is a thing that can be billed; a contract price is what one
   payer pays for it. An invoice line's unit price is checked against the
   contract price at render, so a line billed off contract is a finding rather
   than a silent difference. Item labels are the labels the invoice lines use —
   that is the join. */
const TARIFF_ITEMS = [
  { id: "tar_01", code: "HRG-FZ21A", label: "Emergency appendicectomy", kind: "procedure" },
  { id: "tar_02", code: "HRG-GA10C", label: "Laparoscopic cholecystectomy", kind: "procedure" },
  { id: "tar_03", code: "HRG-JC42Z", label: "Wound debridement", kind: "procedure" },
  { id: "tar_04", code: "HRG-FZ18B", label: "Inguinal hernia repair, day case", kind: "procedure" },
  { id: "tar_05", code: "HRG-EY43Z", label: "Coronary angiography", kind: "procedure" },
  { id: "tar_06", code: "HRG-FZ31Z", label: "Upper GI endoscopy", kind: "procedure" },
  { id: "tar_07", code: "HRG-WA22Z", label: "Central line insertion", kind: "procedure" },
  { id: "tar_08", code: "HRG-JC07Z", label: "Skin lesion excision, day case", kind: "procedure" },
  { id: "tar_09", code: "BED-SURG", label: "Surgical bed day, Beaumont", kind: "bed" },
  { id: "tar_10", code: "BED-DAYC", label: "Day-surgery bed day, Aster", kind: "bed" },
  { id: "tar_11", code: "BED-CARD", label: "Cardiology inpatient care", kind: "bed" },
  { id: "tar_12", code: "BED-OLDR", label: "Older people's inpatient care", kind: "bed" },
  { id: "tar_13", code: "BED-ACUT", label: "Acute medicine inpatient care", kind: "bed" },
  { id: "tar_14", code: "MON-TELE", label: "Telemetry monitoring", kind: "other" },
  { id: "tar_15", code: "DRUG-IVAB", label: "Intravenous antibiotic course", kind: "medication" },
];

const CONTRACTS = [
  { id: "con_01", payer: "nhs_commissioner", name: "Halden and Westmere block contract, 2026/27", status: "live", signedAt: daysAgo(200), expiresAt: daysAgo(-380), dueDays: 30, note: "National tariff, no local uplift. Activity returned monthly." },
  { id: "con_02", payer: "private_insurer", name: "Mercia Health schedule B", status: "live", signedAt: daysAgo(420), expiresAt: daysAgo(-26), dueDays: 30, note: "Renewal negotiation opens six weeks before the end date." },
  { id: "con_03", payer: "self_pay", name: "Self-pay price list, 2026", status: "live", signedAt: daysAgo(90), expiresAt: null, dueDays: 0, note: "Payable in full before the procedure; no credit is given." },
  { id: "con_04", payer: "overseas", name: "Overseas visitor charging schedule", status: "live", signedAt: daysAgo(150), expiresAt: daysAgo(-200), dueDays: 14, note: "Charged under the overseas visitor regulations." },
];

/* One row per price a contract actually carries. A combination that is missing
   is missing on purpose: the invoice line is then off contract and says so. */
const CONTRACT_PRICES = [
  { payer: "nhs_commissioner", itemId: "tar_01", minor: 241000 },
  { payer: "nhs_commissioner", itemId: "tar_02", minor: 294000 },
  { payer: "nhs_commissioner", itemId: "tar_03", minor: 118000 },
  { payer: "nhs_commissioner", itemId: "tar_05", minor: 176000 },
  { payer: "nhs_commissioner", itemId: "tar_06", minor: 96000 },
  { payer: "nhs_commissioner", itemId: "tar_09", minor: 48000 },
  { payer: "nhs_commissioner", itemId: "tar_10", minor: 48000 },
  { payer: "nhs_commissioner", itemId: "tar_11", minor: 52000 },
  { payer: "nhs_commissioner", itemId: "tar_12", minor: 44000 },
  { payer: "nhs_commissioner", itemId: "tar_14", minor: 9000 },
  { payer: "private_insurer", itemId: "tar_01", minor: 262000 },
  { payer: "private_insurer", itemId: "tar_02", minor: 318000 },
  { payer: "private_insurer", itemId: "tar_11", minor: 52000 },
  { payer: "private_insurer", itemId: "tar_14", minor: 9000 },
  { payer: "self_pay", itemId: "tar_02", minor: 348000 },
  { payer: "self_pay", itemId: "tar_04", minor: 219000 },
  { payer: "self_pay", itemId: "tar_08", minor: 68000 },
  { payer: "overseas", itemId: "tar_07", minor: 62000 },
  { payer: "overseas", itemId: "tar_12", minor: 49000 },
  { payer: "overseas", itemId: "tar_13", minor: 51000 },
  { payer: "overseas", itemId: "tar_15", minor: 28000 },
];

/* What the hospital's own accounts say care costs. Bed days are per ward per
   day; theatre is per minute; a drug course is costed from the order's own
   frequency against the formulary's unit cost. Pence throughout. */
const COST_RATES = {
  bedDayByWard: { beaumont: 39000, linden: 41000, corvid: 43000, aster: 31000, hollis: 36000 },
  theatreMinute: 1400,
  source: "Halden finance, 2025/26 reference costs",
};

/* Chases against an invoice: one row per contact, escalating. */
const CHASES = [
  { id: "chs_01", invoiceId: "inv_07", step: "reminder", at: daysAgo(10), byId: "stf_14", outcome: "no_reply", note: "Automatic reminder emailed to the commissioner's finance inbox." },
  { id: "chs_02", invoiceId: "inv_07", step: "statement", at: daysAgo(4), byId: "stf_07", outcome: "no_reply", note: "Statement sent with the March activity return." },
  { id: "chs_03", invoiceId: "inv_08", step: "reminder", at: hoursAgo(16), byId: "stf_14", outcome: "awaiting", note: "Letter to the address on the record after the bank transfer was returned." },
  { id: "chs_04", invoiceId: "inv_02", step: "call", at: daysAgo(2), byId: "stf_07", outcome: "promised", note: "Spoke to Mercia Health; the second instalment is advised for this week." },
];

/* Money against an invoice. What an invoice has been paid is the sum of the
   CLEARED rows here — pending and failed money is not paid money. */
const PAYMENTS = [
  { id: "pay_01", invoiceId: "inv_01", payer: "nhs_commissioner", method: "remittance", status: "cleared", amountMinor: 337000, receivedAt: daysAgo(3), reference: "REM-88214", byId: "stf_07", note: null },
  { id: "pay_02", invoiceId: "inv_02", payer: "private_insurer", method: "bank_transfer", status: "cleared", amountMinor: 120000, receivedAt: daysAgo(6), reference: "BT-40921", byId: "stf_07", note: "First of two instalments." },
  { id: "pay_03", invoiceId: "inv_02", payer: "private_insurer", method: "bank_transfer", status: "pending", amountMinor: 60000, receivedAt: hoursAgo(20), reference: "BT-41077", byId: "stf_07", note: "Second instalment, advised but not cleared." },
  { id: "pay_04", invoiceId: "inv_08", payer: "overseas", method: "bank_transfer", status: "failed", amountMinor: 144000, receivedAt: daysAgo(1), reference: "BT-41102", byId: "stf_14", note: "Returned by the sending bank; the patient has been written to." },
];

/* What each ward physically holds, and how many nurses its establishment asks
   for on a shift. Occupancy percentages and the rota's shortfall both read this;
   neither invents a denominator. */
const WARD_BEDS = [
  { ward: "beaumont", beds: 6, establishment: { day: 3, late: 3, night: 2 } },
  { ward: "linden", beds: 4, establishment: { day: 4, late: 3, night: 2 } },
  { ward: "corvid", beds: 4, establishment: { day: 2, late: 2, night: 1 } },
  { ward: "aster", beds: 3, establishment: { day: 2, late: 2, night: 1 } },
  { ward: "hollis", beds: 4, establishment: { day: 2, late: 2, night: 1 } },
];

/* The rota as authored on the ward board: three days out, three shifts a day,
   two wards. Tuples read [staff, day offset, shift] so the coverage is countable
   at a glance and every name is a real staff record. */
const ROTA = [
  ["stf_03", 0, "day", "beaumont"], ["stf_11", 0, "day", "beaumont"], ["stf_01", 0, "day", "beaumont"],
  ["stf_06", 0, "late", "beaumont"], ["stf_11", 0, "late", "beaumont"], ["stf_10", 0, "late", "beaumont"],
  ["stf_06", 0, "night", "beaumont"],
  ["stf_04", 0, "day", "linden"], ["stf_06", 0, "day", "linden"], ["stf_08", 0, "day", "linden"], ["stf_12", 0, "day", "linden"],
  ["stf_04", 0, "late", "linden"], ["stf_11", 0, "late", "linden"],
  ["stf_10", 0, "night", "linden"], ["stf_06", 0, "night", "linden"], ["stf_04", 0, "night", "linden"],
  ["stf_03", 1, "day", "beaumont"], ["stf_01", 1, "day", "beaumont"], ["stf_11", 1, "day", "beaumont"], ["stf_05", 1, "day", "beaumont"],
  ["stf_06", 1, "late", "beaumont"], ["stf_10", 1, "late", "beaumont"], ["stf_11", 1, "late", "beaumont"],
  ["stf_06", 1, "night", "beaumont"], ["stf_10", 1, "night", "beaumont"], ["stf_11", 1, "night", "beaumont"],
  ["stf_04", 1, "day", "linden"], ["stf_13", 1, "day", "linden"], ["stf_08", 1, "day", "linden"],
  ["stf_04", 1, "late", "linden"], ["stf_06", 1, "late", "linden"], ["stf_12", 1, "late", "linden"], ["stf_10", 1, "late", "linden"],
  ["stf_06", 1, "night", "linden"], ["stf_11", 1, "night", "linden"], ["stf_10", 1, "night", "linden"],
  ["stf_03", 2, "day", "beaumont"], ["stf_11", 2, "day", "beaumont"], ["stf_01", 2, "day", "beaumont"], ["stf_07", 2, "day", "beaumont"],
  ["stf_06", 2, "late", "beaumont"], ["stf_10", 2, "late", "beaumont"],
  ["stf_06", 2, "night", "beaumont"], ["stf_11", 2, "night", "beaumont"], ["stf_10", 2, "night", "beaumont"],
  ["stf_04", 2, "day", "linden"], ["stf_08", 2, "day", "linden"], ["stf_12", 2, "day", "linden"], ["stf_13", 2, "day", "linden"], ["stf_06", 2, "day", "linden"],
  ["stf_04", 2, "late", "linden"], ["stf_11", 2, "late", "linden"], ["stf_10", 2, "late", "linden"], ["stf_12", 2, "late", "linden"],
  ["stf_06", 2, "night", "linden"], ["stf_10", 2, "night", "linden"], ["stf_11", 2, "night", "linden"],
].map((row, index) => ({ id: "rot_" + (index + 1), staffId: row[0], day: row[1], shift: row[2], ward: row[3] }));

/* A theatre day is every room in THEATRES for eight hours: the denominator
   theatre utilisation is measured against. Derived, so adding a theatre to the
   enum moves the figure rather than leaving it stale. */
const THEATRE_MINUTES_PER_DAY = THEATRES.length * 8 * 60;

const NOTIFICATIONS = [
  { id: "ntf_01", title: "Bed wait past four hours", body: "Ruth Adeyemi has waited 5 hours for a monitored bed on Linden.", at: hoursAgo(1), route: "/admissions" },
  { id: "ntf_02", title: "Consent not recorded", body: "Farida Kettle is second on the day-case list at 14:00.", at: hoursAgo(0.6), route: "/interventions" },
  { id: "ntf_03", title: "Priya Raghunathan mentioned you", body: "On the morphine order for Callum Frayne.", at: hoursAgo(1), route: "/feed" },
  { id: "ntf_04", title: "Import needs review", body: "12 of 318 rows from the patient administration system could not be placed.", at: hoursAgo(21), route: "/data" },
];

const NOTIFICATION_PREFS = {
  quietHours: { from: "22:00", to: "07:00" },
  rows: [
    { id: "bed_wait", label: "A bed wait passes four hours", email: true, pager: true, inApp: true },
    { id: "consent", label: "A scheduled procedure has no consent recorded", email: false, pager: true, inApp: true },
    { id: "mention", label: "Somebody mentions you on a record", email: true, pager: false, inApp: true },
    { id: "import", label: "An import finishes with rows to review", email: true, pager: false, inApp: true },
    { id: "stock", label: "A formulary line drops below its reorder level", email: false, pager: false, inApp: true },
  ],
};

/* The reset link the /reset-password screen is standing behind. */
/* Settings the hospital has chosen. Each one has a real effect somewhere:
   the reference prefix names every invoice raised, the write-off threshold
   decides whether a balance can be written off here or has to be approved, an
   integration that is off is not offered in the import dialog, and the session
   timeout and two-factor rule are stated on the security screen. */
const BILLING_SETTINGS = {
  referencePrefix: "HG-2026",
  defaultPayer: "nhs_commissioner",
  writeOffThresholdMinor: 250000,
  approver: "Finance director",
};

const INTEGRATIONS = IMPORT_SOURCES.map((source, index) => ({
  source: source.value,
  enabled: index !== 2,
  autoAccept: index === 1,
}));

const SECURITY_SETTINGS = {
  sessionMinutes: 30,
  requireTwoFactor: true,
  passwordAgeDays: 180,
};

/* Signed-in sessions for the account the security screen belongs to. */
const SESSIONS = [
  { id: "ses_01", device: "Ward station, Beaumont nurses' base", browser: "Chrome on Windows", ip: "10.14.2.61", lastSeenAt: hoursAgo(0.1), current: true },
  { id: "ses_02", device: "Trust laptop", browser: "Edge on Windows", ip: "10.14.9.4", lastSeenAt: hoursAgo(26), current: false },
  { id: "ses_03", device: "Personal phone", browser: "Safari on iOS", ip: "82.14.66.201", lastSeenAt: daysAgo(4), current: false },
];

const HD_RESET_LINK = { email: "i.vance@haldengeneral.nhs.uk", issuedAt: hoursAgo(0.5), expiresInMinutes: 60 };

const PASSWORD_RULES = [
  { id: "length", label: "At least 12 characters", test: (v) => (v || "").length >= 12 },
  { id: "case", label: "An upper and a lower case letter", test: (v) => /[a-z]/.test(v || "") && /[A-Z]/.test(v || "") },
  { id: "number", label: "A number", test: (v) => /\d/.test(v || "") },
  { id: "name", label: "Not your name or your email address", test: (v) => !!v && !/vance|imogen/i.test(v) },
];

Object.assign(window, {
  HD_NOW, HD_HOSPITAL, HD_SIGNED_IN, STAFF, PATIENTS, ADMISSIONS, INTERVENTIONS, MEDICATION_ORDERS, FORMULARY,
  COMMENTS, ACTIVITY, TRANSFERS, TRANSFER_ROWS, INVOICES, PAYMENTS, TARIFF_ITEMS, CONTRACTS, CONTRACT_PRICES, COST_RATES, CHASES, WARD_BEDS, ROTA, THEATRE_MINUTES_PER_DAY, SESSIONS, BILLING_SETTINGS, INTEGRATIONS, SECURITY_SETTINGS, NOTIFICATIONS, NOTIFICATION_PREFS, HD_RESET_LINK, PASSWORD_RULES,
  hoursAgo, hoursOn, daysAgo, dayAt,
});

})();
</script>
<script type="text/babel" data-presets="react" data-file="lib/ui-runtime.js">
(function(){
/* Shared render runtime — copied per product, domain-free. Status tone, money,
   time and the floating layer live here so no screen assigns a tone or spells a
   duration its own way.

   Local addition, reported in README: Floating / FloatingTip. These portal to the
   document in viewport coordinates, reposition on scroll and resize, flip when
   there is no room, and clamp to the viewport. The design system's own Menu,
   Popover and Tooltip now do all of that too — these remain because they carry a
   different API the products lean on: one element as the trigger, a render
   callback that receives `close`, and no compound Trigger/Popup pair. Reach for
   the design system's overlays in new work. */

const { Badge } = window.OriginDesignSystem_7c998b;

const TONE_VARIANT = { neutral: "secondary", info: "info", success: "success", warning: "warning", danger: "destructive" };

/* The only place a status becomes a label and a colour. */
function StatusBadge({ values, value, size = "sm" }) {
  const found = statusValue(values, value);
  if (!found) { console.warn("[ui-runtime] value not in enum:", value); return null; }
  return <Badge size={size} variant={TONE_VARIANT[found.tone]}>{found.label}</Badge>;
}

const moneyFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
const formatMoney = (cents) => moneyFormatter.format(cents / 100);
const formatPercent = (ratio) => (ratio === null ? "—" : (ratio * 100).toFixed(1) + "%");
const formatCount = (n) => new Intl.NumberFormat("en-US").format(n);

/* One format rule per column. Distance is absolute: three days ago and three
   days ahead format alike. Direction is never carried by the format. */
function formatWhen(ts, now) {
  if (!ts) return "Never";
  const d = Math.abs(ts - now);
  const date = new Date(ts);
  if (d < 24 * 3600e3) return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
  if (d < 7 * 24 * 3600e3) return date.toLocaleDateString("en-GB", { weekday: "short" }) + " " + date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
  return date.toLocaleDateString("en-GB", { day: "numeric", month: "short" });
}
const formatAbsolute = (ts) => (ts ? new Date(ts).toLocaleString("en-GB", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) : "");

/* One spelling for every countdown in the product. Direction is in the words. */
function formatCountdown(ts, now) {
  if (!ts) return "No deadline";
  const diff = ts - now;
  const abs = Math.abs(diff);
  const unit = abs >= 36 * 3600e3 ? Math.round(abs / 864e5) : Math.round(abs / 3600e3);
  const noun = abs >= 36 * 3600e3 ? (unit === 1 ? "day" : "days") : (unit === 1 ? "hour" : "hours");
  return unit + " " + noun + (diff >= 0 ? " left" : " over");
}

function Icon({ name, size = 16 }) {
  const host = React.useRef(null);
  React.useEffect(() => {
    if (!host.current || !window.lucide) return;
    host.current.innerHTML = "";
    const el = document.createElement("i");
    el.setAttribute("data-lucide", name);
    host.current.appendChild(el);
    window.lucide.createIcons({ attrs: { width: size, height: size } });
  }, [name, size]);
  return <span className="fd-icon" ref={host} aria-hidden="true" style={{ width: size, height: size }} />;
}

function useFloating(open, anchorRef, panelRef, align, side) {
  const [pos, setPos] = React.useState({ top: -9999, left: -9999 });
  const place = React.useCallback(() => {
    const host = anchorRef.current, panel = panelRef.current;
    if (!host || !panel) return;
    /* Measure the TRIGGER, not the wrapper. The wrapper is inline-flex and static,
       so an absolutely-positioned trigger inside it — a chart bar, a pinned badge —
       does not sit where the wrapper does, and measuring the wrapper puts every
       overlay in the same wrong place. */
    const anchor = host.firstElementChild || host;
    const a = anchor.getBoundingClientRect(), p = panel.getBoundingClientRect();
    const m = 8;
    let top, left;
    if (side === "right") {
      left = a.right + 6;
      top = a.top + (a.height - p.height) / 2;
    } else {
      top = a.bottom + 4;
      if (top + p.height > window.innerHeight - m) {
        const above = a.top - 4 - p.height;
        top = above > m ? above : Math.max(m, window.innerHeight - m - p.height);
      }
      left = align === "end" ? a.right - p.width : align === "center" ? a.left + (a.width - p.width) / 2 : a.left;
    }
    left = Math.min(Math.max(m, left), Math.max(m, window.innerWidth - m - p.width));
    top = Math.min(Math.max(m, top), Math.max(m, window.innerHeight - m - p.height));
    setPos({ top: top, left: left });
  }, [align, side, anchorRef, panelRef]);
  React.useLayoutEffect(() => { if (open) place(); }, [open, place]);
  React.useEffect(() => {
    if (!open) return undefined;
    const onMove = () => place();
    window.addEventListener("scroll", onMove, true);
    window.addEventListener("resize", onMove);
    return () => { window.removeEventListener("scroll", onMove, true); window.removeEventListener("resize", onMove); };
  }, [open, place]);
  return pos;
}

function Floating({ trigger, children, align = "end", width, role = "menu", panelClass = "o-menu", label }) {
  const [open, setOpen] = React.useState(false);
  const anchorRef = React.useRef(null), panelRef = React.useRef(null);
  const pos = useFloating(open, anchorRef, panelRef, align, "bottom");
  React.useEffect(() => {
    if (!open) return undefined;
    const away = (e) => {
      if (anchorRef.current && anchorRef.current.contains(e.target)) return;
      if (panelRef.current && panelRef.current.contains(e.target)) return;
      setOpen(false);
    };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("mousedown", away);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("mousedown", away); document.removeEventListener("keydown", onKey); };
  }, [open]);
  const close = () => setOpen(false);
  return (
    <span ref={anchorRef} className="fd-floating-anchor">
      {React.cloneElement(trigger, { onClick: () => setOpen(!open), "aria-expanded": open, "aria-haspopup": role === "menu" ? "menu" : "dialog" })}
      {open ? ReactDOM.createPortal(
        <div ref={panelRef} role={role} aria-label={label} className={"o-popup " + panelClass} style={{ position: "fixed", top: pos.top, left: pos.left, zIndex: 60, minWidth: width || "12rem" }}>
          {typeof children === "function" ? children(close) : children}
        </div>, document.body) : null}
    </span>
  );
}

function FloatingTip({ label, side = "right", children }) {
  const [open, setOpen] = React.useState(false);
  const anchorRef = React.useRef(null), panelRef = React.useRef(null);
  const pos = useFloating(open, anchorRef, panelRef, "center", side);
  return (
    <span ref={anchorRef} className="fd-floating-anchor" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)} onFocus={() => setOpen(true)} onBlur={() => setOpen(false)}>
      {children}
      {open ? ReactDOM.createPortal(
        <span ref={panelRef} role="tooltip" className="o-tooltip" style={{ position: "fixed", top: pos.top, left: pos.left, zIndex: 70, whiteSpace: "nowrap" }}>{label}</span>,
        document.body) : null}
    </span>
  );
}

Object.assign(window, { StatusBadge, TONE_VARIANT, formatMoney, formatPercent, formatCount, formatWhen, formatAbsolute, formatCountdown, Icon, Floating, FloatingTip });

})();
</script>
<script type="text/babel" data-presets="react" data-file="metrics.js">
(function(){
/* Ward Desk — every number on every screen comes from here.
   The rule this file exists to keep: a figure, a section count and a nav badge
   are one function called three times. If a screen cannot name the array a
   number came from, the number is wrong.

   Nothing here formats and nothing here assigns a tone — ui-runtime.js does the
   first, enums.js the second. */

const staffById = (staff, id) => staff.find((s) => s.id === id) || null;
const staffName = (staff, id) => { const s = staffById(staff, id); return s ? s.name : "Not recorded"; };
const patientById = (patients, id) => patients.find((p) => p.id === id) || null;
const patientName = (patients, id) => { const p = patientById(patients, id); return p ? p.name : "Unknown patient"; };
const wardName = (value) => labelOf(WARDS, value).split(" — ")[0];
const initialsOf = (name) => (name || "").split(/\s+/).slice(0, 2).map((w) => w[0]).join("").toUpperCase();
const drugById = (formulary, id) => formulary.find((d) => d.id === id) || null;

/* Doctors are the medical family; employees are everybody with a badge. Two
   views of one array, never two arrays. */
const doctorsOf = (staff) => staff.filter((s) => s.family === "medical");
const employeesOf = (staff) => staff.slice();

/* ── Admissions ──────────────────────────────────────────────────────────── */
const openAdmissions = (admissions) => admissions.filter((a) => a.status === "awaiting_bed" || a.status === "on_ward" || a.status === "fit_for_discharge");
const bedsWaiting = (admissions) => admissions.filter((a) => a.status === "awaiting_bed");
const bedWaitsBreached = (admissions, now) => bedsWaiting(admissions).filter((a) => ADMISSION_FLAGS.bedWaitBreached(a, now));
const fitForDischarge = (admissions) => admissions.filter((a) => a.status === "fit_for_discharge");
const onWard = (admissions) => admissions.filter((a) => a.status === "on_ward");
const admissionOfPatient = (admissions, patientId) => admissions
  .filter((a) => a.patientId === patientId)
  .sort((a, b) => (b.admittedAt || b.requestedAt) - (a.admittedAt || a.requestedAt))[0] || null;

/* ── Interventions ───────────────────────────────────────────────────────── */
const interventionsToday = (interventions, now) => interventions
  .filter((i) => INTERVENTION_FLAGS.isToday(i, now) && i.status !== "cancelled")
  .sort((a, b) => a.scheduledAt - b.scheduledAt);
const consentMissing = (interventions) => interventions.filter((i) => INTERVENTION_FLAGS.consentMissing(i));
const unscheduledInterventions = (interventions) => interventions.filter((i) => INTERVENTION_FLAGS.unscheduled(i));
const interventionsForPatient = (interventions, patientId) => interventions
  .filter((i) => i.patientId === patientId)
  .sort((a, b) => (b.scheduledAt || b.requestedAt) - (a.scheduledAt || a.requestedAt));
const interventionsForSurgeon = (interventions, staffId) => interventions.filter((i) => i.surgeonId === staffId || i.anaesthetistId === staffId);

/* ── Medication ──────────────────────────────────────────────────────────── */
const ordersAwaitingPharmacy = (orders) => orders.filter((o) => MEDICATION_FLAGS.awaitingPharmacy(o));
const activeOrders = (orders) => orders.filter((o) => o.status === "active");
const dosesDue = (orders, now) => orders.filter((o) => MEDICATION_FLAGS.doseDue(o, now));
const dosesMissed = (orders, now) => orders.filter((o) => MEDICATION_FLAGS.doseMissed(o, now));
const ordersForPatient = (orders, patientId) => orders.filter((o) => o.patientId === patientId);
const ordersOnHold = (orders) => orders.filter((o) => o.status === "on_hold");

/* ── Formulary ───────────────────────────────────────────────────────────── */
const belowReorder = (formulary) => formulary.filter((d) => FORMULARY_FLAGS.belowReorder(d));
const outOfStock = (formulary) => formulary.filter((d) => d.stock === "out");
const ordersForDrug = (orders, drugId) => orders.filter((o) => o.drugId === drugId && (o.status === "active" || o.status === "awaiting_verification"));

/* ── The social layer ────────────────────────────────────────────────────── */
const commentsFor = (comments, type, id) => comments
  .filter((c) => c.record.type === type && c.record.id === id)
  .sort((a, b) => a.at - b.at);

/* A mention is owed until the person mentioned has written on that record since.
   The nav badge, the feed section and the "waiting on" line all read this one
   predicate. */
function mentionsOwed(comments, viewerId) {
  return comments.filter((c) => {
    if (c.mentions.indexOf(viewerId) < 0) return false;
    return !comments.some((later) => later.record.type === c.record.type && later.record.id === c.record.id && later.authorId === viewerId && later.at > c.at);
  });
}

const commentsByAuthor = (comments, authorId) => comments.filter((c) => c.authorId === authorId).sort((a, b) => b.at - a.at);
const feedItems = (activity) => activity.slice().sort((a, b) => b.at - a.at);

/* ── Import and export ───────────────────────────────────────────────────── */
const transfersNeedingReview = (transfers) => transfers.filter((t) => t.status === "needs_review");
const failedTransfers = (transfers) => transfers.filter((t) => t.status === "failed");
const imports = (transfers) => transfers.filter((t) => t.kind === "import");
const exports_ = (transfers) => transfers.filter((t) => t.kind === "export");
const rowsAccepted = (transfers) => imports(transfers).reduce((n, t) => n + t.accepted, 0);

/* ── Money ────────────────────────────────────────────────────────────────
   The product bills in sterling, so it spells money itself rather than reaching
   for the shared USD formatter in lib/ui-runtime.js. One helper, product-wide;
   amounts are pence everywhere. */
const poundsFormatter = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP", maximumFractionDigits: 0 });
const poundsExact = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP", minimumFractionDigits: 2 });
const formatPounds = (minor) => poundsFormatter.format((minor || 0) / 100);
const formatPoundsExact = (minor) => poundsExact.format((minor || 0) / 100);

/* ── Invoices: nothing is stored that can be summed ──────────────────────── */
const lineTotal = (line) => line.qty * line.unitMinor;
const invoiceTotal = (invoice) => invoice.lines.reduce((n, line) => n + lineTotal(line), 0);
const paymentsFor = (payments, invoiceId) => payments.filter((p) => p.invoiceId === invoiceId).sort((a, b) => b.receivedAt - a.receivedAt);
const invoicePaid = (payments, invoiceId) => paymentsFor(payments, invoiceId).filter((p) => p.status === "cleared").reduce((n, p) => n + p.amountMinor, 0);
const invoiceBalance = (invoice, payments) => Math.max(0, invoiceTotal(invoice) - invoicePaid(payments, invoice.id));
const invoicesOverdue = (invoices, payments, now) => invoices.filter((i) => INVOICE_FLAGS.overdue(i, invoiceBalance(i, payments), now));
const invoicesBillable = (invoices) => invoices.filter((i) => INVOICE_FLAGS.billable(i));
const invoicesUnbilled = (invoices) => invoices.filter((i) => i.status === "draft" || i.status === "awaiting_coding");
const invoicesForPatient = (invoices, patientId) => invoices.filter((i) => i.patientId === patientId);
const invoicesDisputed = (invoices) => invoices.filter((i) => i.status === "disputed");
const billedTotal = (invoices) => invoicesBillable(invoices).reduce((n, i) => n + invoiceTotal(i), 0);
const collectedTotal = (invoices, payments) => invoicesBillable(invoices).reduce((n, i) => n + invoicePaid(payments, i.id), 0);
const outstandingTotal = (invoices, payments) => invoicesBillable(invoices).reduce((n, i) => n + invoiceBalance(i, payments), 0);

/* Aged debt: every issued invoice with a balance, in the bucket its due date
   puts it in. The buckets partition one set — an invoice appears once. */
function agedDebt(invoices, payments, now) {
  const buckets = [
    { id: "current", label: "Not yet due", rows: [] },
    { id: "d30", label: "1–30 days over", rows: [] },
    { id: "d60", label: "31–60 days over", rows: [] },
    { id: "d60plus", label: "Over 60 days", rows: [] },
  ];
  invoicesBillable(invoices).forEach((invoice) => {
    const balance = invoiceBalance(invoice, payments);
    if (balance <= 0) return;
    const over = invoice.dueAt ? Math.round((now - invoice.dueAt) / DAY_MS) : 0;
    const bucket = over <= 0 ? buckets[0] : over <= 30 ? buckets[1] : over <= 60 ? buckets[2] : buckets[3];
    bucket.rows.push({ invoice: invoice, balance: balance, over: over });
  });
  return buckets.map((b) => Object.assign({}, b, { total: b.rows.reduce((n, r) => n + r.balance, 0) }));
}

/* Revenue by payer: billed, collected and outstanding over the same set. */
function revenueByPayer(invoices, payments) {
  return PAYERS.map((payer) => {
    const rows = invoicesBillable(invoices).filter((i) => i.payer === payer.value);
    const billed = rows.reduce((n, i) => n + invoiceTotal(i), 0);
    const collected = rows.reduce((n, i) => n + invoicePaid(payments, i.id), 0);
    return { payer: payer.value, label: payer.label, invoices: rows.length, billed: billed, collected: collected, outstanding: billed - collected, share: 0, rows: rows };
  }).map((row, _, all) => {
    const total = all.reduce((n, r) => n + r.billed, 0);
    return Object.assign({}, row, { share: total ? row.billed / total : 0 });
  });
}

/* What the hospital billed for, by the kind of line it billed. */
function revenueByKind(invoices) {
  return BILL_LINE_KINDS.map((kind) => {
    const lines = invoicesBillable(invoices).reduce((list, i) => list.concat(i.lines.filter((l) => l.kind === kind.value)), []);
    return { kind: kind.value, label: kind.label, lines: lines.length, total: lines.reduce((n, l) => n + lineTotal(l), 0) };
  }).filter((row) => row.lines > 0);
}

/* ── Contracts and tariffs ────────────────────────────────────────────────
   A line's contracted price is looked up by the payer and the line's own label;
   a line with no contract row is off contract, and so is one whose unit price
   differs from the row. Both are findings, not silent differences. */
const contractOf = (contracts, payer) => contracts.find((c) => c.payer === payer) || null;
const contractDueDays = (contracts, payer) => { const c = contractOf(contracts, payer); return c ? c.dueDays : 30; };
const tariffItemFor = (items, label) => items.find((i) => i.label === label) || null;
function contractedPrice(data, payer, label) {
  const item = tariffItemFor(data.tariffItems, label);
  if (!item) return null;
  const row = data.contractPrices.find((p) => p.payer === payer && p.itemId === item.id);
  return row ? row.minor : null;
}
/* One predicate, read by the finding list, the invoice line chip and the badge. */
function lineOffContract(data, invoice, line) {
  const price = contractedPrice(data, invoice.payer, line.label);
  if (price === null) return { kind: "no_price", price: null, difference: 0 };
  if (price !== line.unitMinor) return { kind: "different", price: price, difference: line.unitMinor - price };
  return null;
}
function offContractLines(data) {
  const out = [];
  data.invoices.forEach((invoice) => invoice.lines.forEach((line) => {
    const finding = lineOffContract(data, invoice, line);
    if (finding) out.push(Object.assign({ invoice: invoice, line: line }, finding));
  }));
  return out;
}
const contractPricesFor = (data, payer) => data.contractPrices.filter((p) => p.payer === payer);
const contractsExpiring = (contracts, now) => contracts.filter((c) => CONTRACT_FLAGS.expiringSoon(c, now));

/* ── Credit control ───────────────────────────────────────────────────────
   The worklist is every issued invoice with a balance that is NOT disputed — a
   query is answered before a payer is chased. Each row carries its last contact
   and the step the ladder says comes next. */
const chasesFor = (chases, invoiceId) => chases.filter((c) => c.invoiceId === invoiceId).sort((a, b) => b.at - a.at);
function creditWorklist(data, now) {
  return data.invoices
    .filter((i) => (i.status === "issued" || i.status === "part_paid") && invoiceBalance(i, data.payments) > 0)
    .map((invoice) => {
      const rows = chasesFor(data.chases, invoice.id);
      const last = rows[0] || null;
      const balance = invoiceBalance(invoice, data.payments);
      const failed = data.payments.filter((p) => p.invoiceId === invoice.id && p.status === "failed");
      const promisedRows = data.payments.filter((p) => p.invoiceId === invoice.id && p.status === "pending");
      return {
        invoice: invoice, balance: balance, chases: rows, last: last, lastStep: last ? last.step : null,
        next: nextChaseStep(last ? last.step : null),
        overdue: INVOICE_FLAGS.overdue(invoice, balance, now),
        failed: failed, promised: promisedRows.reduce((n, p) => n + p.amountMinor, 0),
      };
    })
    .sort((a, b) => (a.invoice.dueAt || 0) - (b.invoice.dueAt || 0));
}

/* ── Cost and margin per episode ──────────────────────────────────────────
   A line is costed from the record it names: bed days at the ward's day cost, a
   procedure at its planned theatre minutes, a drug course from the order's own
   frequency against the formulary unit cost. A line naming no record carries no
   cost, and the screen says how much billing that is. */
const DOSES_PER_DAY = { "Once daily": 1, "Twice daily": 2, "Three times daily": 3, "Four times daily": 4 };
function doseCount(order, now) {
  const perDay = DOSES_PER_DAY[order.frequency] || 1;
  const until = order.endsAt || now;
  const days = Math.max(1, Math.round((until - order.startedAt) / DAY_MS));
  return days * perDay;
}
function lineCost(data, line, now) {
  if (!line.ref) return null;
  if (line.kind === "bed" && line.ref.type === "admission") {
    const admission = data.admissions.find((a) => a.id === line.ref.id);
    return admission ? line.qty * (COST_RATES.bedDayByWard[admission.ward] || 0) : null;
  }
  if (line.kind === "procedure" && line.ref.type === "intervention") {
    const intervention = data.interventions.find((i) => i.id === line.ref.id);
    return intervention ? intervention.plannedMin * COST_RATES.theatreMinute : null;
  }
  if (line.kind === "medication" && line.ref.type === "medication") {
    const order = data.orders.find((o) => o.id === line.ref.id);
    const drug = order ? drugById(data.formulary, order.drugId) : null;
    return order && drug ? doseCount(order, now) * (drug.unitCostMinor || 0) : null;
  }
  return null;
}
function episodeCost(data, invoice, now) {
  let cost = 0, uncosted = 0;
  invoice.lines.forEach((line) => {
    const c = lineCost(data, line, now);
    if (c === null) uncosted += lineTotal(line); else cost += c;
  });
  return { cost: cost, uncosted: uncosted };
}
function marginRows(data, now) {
  return invoicesBillable(data.invoices).map((invoice) => {
    const billed = invoiceTotal(invoice);
    const costed = episodeCost(data, invoice, now);
    const margin = billed - costed.cost;
    return { invoice: invoice, billed: billed, cost: costed.cost, uncosted: costed.uncosted, margin: margin, rate: billed ? margin / billed : 0 };
  }).sort((a, b) => a.rate - b.rate);
}
function costByKind(data, now) {
  return BILL_LINE_KINDS.map((kind) => {
    let cost = 0, billed = 0, lines = 0;
    invoicesBillable(data.invoices).forEach((invoice) => invoice.lines.filter((l) => l.kind === kind.value).forEach((line) => {
      const c = lineCost(data, line, now);
      lines += 1; billed += lineTotal(line); if (c !== null) cost += c;
    }));
    return { kind: kind.value, label: kind.label, lines: lines, billed: billed, cost: cost, margin: billed - cost };
  }).filter((row) => row.lines > 0);
}

/* ── Wards: capacity, occupancy and who is on ─────────────────────────────── */
const wardBedsOf = (beds, ward) => beds.find((b) => b.ward === ward) || { ward: ward, beds: 0, establishment: { day: 0, late: 0, night: 0 } };

function wardRows(admissions, beds) {
  return WARDS.map((ward) => {
    const capacity = wardBedsOf(beds, ward.value);
    const rows = admissions.filter((a) => a.ward === ward.value);
    const occupied = rows.filter((a) => a.status === "on_ward" || a.status === "fit_for_discharge");
    const held = rows.filter((a) => a.status === "fit_for_discharge");
    const waiting = rows.filter((a) => a.status === "awaiting_bed");
    return {
      ward: ward.value, label: wardName(ward.value), specialty: labelOf(WARDS, ward.value).split(" — ")[1],
      beds: capacity.beds, establishment: capacity.establishment,
      occupied: occupied.length, held: held.length, waiting: waiting.length,
      free: Math.max(0, capacity.beds - occupied.length),
      occupancy: capacity.beds ? occupied.length / capacity.beds : 0,
      rows: occupied.concat(waiting),
    };
  });
}

/* ── Rota ─────────────────────────────────────────────────────────────────── */
const rotaFor = (rota, day, ward, shift) => rota.filter((r) => r.day === day && r.ward === ward && r.shift === shift);
function understaffedShifts(rota, beds, days) {
  const out = [];
  (days || [0, 1, 2]).forEach((day) => {
    beds.forEach((capacity) => {
      SHIFT_TYPES.forEach((shift) => {
        const rostered = rotaFor(rota, day, capacity.ward, shift.value);
        if (!rostered.length && !rota.some((r) => r.ward === capacity.ward)) return;
        const establishment = capacity.establishment[shift.value];
        if (SHIFT_FLAGS.understaffed(rostered, establishment)) out.push({ day: day, ward: capacity.ward, shift: shift.value, rostered: rostered, establishment: establishment, shortfall: SHIFT_FLAGS.shortfall(rostered, establishment) });
      });
    });
  });
  return out;
}
const rotaWards = (rota) => WARDS.filter((w) => rota.some((r) => r.ward === w.value));

/* ── The drug round: active orders grouped by where their clock has got to ── */
function roundGroups(orders, now) {
  const active = activeOrders(orders).filter((o) => o.nextDoseAt);
  return [
    { id: "overdue", label: "Overdue", note: "Due more than an hour ago and not recorded as given.", rows: active.filter((o) => MEDICATION_FLAGS.doseMissed(o, now)) },
    { id: "now", label: "Due within the hour", note: "The round you are on.", rows: active.filter((o) => MEDICATION_FLAGS.doseDue(o, now) && !MEDICATION_FLAGS.doseMissed(o, now)) },
    { id: "later", label: "Later today", note: "Due after this hour and before midnight.", rows: active.filter((o) => o.nextDoseAt > now + HOUR_MS && new Date(o.nextDoseAt).toDateString() === new Date(now).toDateString()) },
  ].map((group) => Object.assign({}, group, { rows: group.rows.slice().sort((a, b) => a.nextDoseAt - b.nextDoseAt) }));
}
const dosesGivenSince = (orders, now, hours) => orders.filter((o) => o.lastGivenAt && now - o.lastGivenAt <= hours * HOUR_MS);

/* ── Report figures: each one names the array it divides ──────────────────── */
function reportFigures(data, now) {
  const open = openAdmissions(data.admissions);
  const beds = data.wardBeds.reduce((n, b) => n + b.beds, 0);
  const occupied = onWard(data.admissions).concat(fitForDischarge(data.admissions));
  const todaysTheatre = interventionsToday(data.interventions, now);
  const bookedMinutes = todaysTheatre.reduce((n, i) => n + i.plannedMin, 0);
  const scheduled = data.interventions.filter((i) => i.status === "scheduled");
  const withConsent = scheduled.filter((i) => !!i.consentAt);
  const active = activeOrders(data.orders);
  const missed = dosesMissed(data.orders, now);
  const stays = open.filter((a) => a.admittedAt).map((a) => ADMISSION_FLAGS.lengthOfStayDays(a, now));
  return {
    occupancy: { value: beds ? occupied.length / beds : 0, of: beds, count: occupied.length },
    theatre: { value: bookedMinutes / THEATRE_MINUTES_PER_DAY, minutes: bookedMinutes, cases: todaysTheatre.length },
    consent: { value: scheduled.length ? withConsent.length / scheduled.length : 1, of: scheduled.length, count: withConsent.length },
    doses: { value: active.length ? (active.length - missed.length) / active.length : 1, of: active.length, count: active.length - missed.length },
    stay: { mean: stays.length ? stays.reduce((n, d) => n + d, 0) / stays.length : 0, longest: stays.length ? Math.max.apply(null, stays) : 0, of: stays.length },
    breaches: { count: bedWaitsBreached(data.admissions, now).length, of: bedsWaiting(data.admissions).length },
  };
}

/* ── What a note is attached to. One place, so a title never drifts. ─────── */
function recordTitle(data, type, id) {
  if (type === "patient") return patientName(data.patients, id);
  if (type === "admission") {
    const admission = data.admissions.find((a) => a.id === id);
    return admission ? patientName(data.patients, admission.patientId) + " · " + wardName(admission.ward) : "Admission";
  }
  if (type === "intervention") {
    const intervention = data.interventions.find((i) => i.id === id);
    return intervention ? intervention.name + " · " + patientName(data.patients, intervention.patientId) : "Intervention";
  }
  if (type === "medication") {
    const order = data.orders.find((o) => o.id === id);
    return order ? order.drug + " " + order.dose + " · " + patientName(data.patients, order.patientId) : "Medication order";
  }
  if (type === "formulary") {
    const drug = data.formulary.find((d) => d.id === id);
    return drug ? drug.name + " " + drug.form : "Formulary line";
  }
  if (type === "invoice") {
    const invoice = data.invoices.find((i) => i.id === id);
    return invoice ? invoice.reference + " · " + patientName(data.patients, invoice.patientId) : "Invoice";
  }
  if (type === "transfer") {
    const job = data.transfers.find((t) => t.id === id);
    return job ? job.label + " · " + labelOf(TRANSFER_KINDS, job.kind) : "Transfer";
  }
  return id;
}

/* Where a note lives, so the feed and the inbox can open it. */
function recordRoute(data, record) {
  if (record.type === "patient") return "/patients/" + record.id;
  if (record.type === "admission") {
    const admission = data.admissions.find((a) => a.id === record.id);
    return admission ? "/patients/" + admission.patientId : "/admissions";
  }
  if (record.type === "intervention") return "/interventions/" + record.id;
  if (record.type === "medication") return "/medication";
  if (record.type === "formulary") return "/formulary";
  if (record.type === "invoice") return "/invoices/" + record.id;
  if (record.type === "transfer") return "/data";
  return "/";
}

/* ── Nav badges: one predicate over one array, recoverable by the reader ─── */
const HD_NAV_BADGES = {
  bedsWaiting: { label: "waiting for a bed", count: (data) => bedsWaiting(data.admissions).length },
  dosesDueOrMissed: { label: "doses due or overdue", count: (data) => dosesDue(data.orders, HD_NOW).length + dosesMissed(data.orders, HD_NOW).length },
  shiftsUnderstaffed: { label: "shifts under establishment", count: (data) => understaffedShifts(data.rota, data.wardBeds).length },
  consentMissing: { label: "without consent recorded", count: (data) => consentMissing(data.interventions).length },
  ordersAwaitingPharmacy: { label: "awaiting a pharmacist", count: (data) => ordersAwaitingPharmacy(data.orders).length },
  mentionsOwed: { label: "mentions you have not answered", count: (data, viewerId) => mentionsOwed(data.comments, viewerId).length },
  transfersNeedingReview: { label: "imports with rows to review", count: (data) => transfersNeedingReview(data.transfers).length },
  invoicesOverdue: { label: "invoices past their due date", count: (data) => invoicesOverdue(data.invoices, data.payments, HD_NOW).length },
  linesOffContract: { label: "invoice lines billed off contract", count: (data) => offContractLines(data).length },
  thinMargins: { label: "episodes under a 15% margin", count: (data) => marginRows(data, HD_NOW).filter((r) => r.rate < 0.15).length },
  invoicesToCode: { label: "invoices waiting on a coder", count: (data) => data.invoices.filter((i) => i.status === "awaiting_coding").length },
};

/* Whether a category's rail icon shows a dot: a badge that would sum different
   kinds of record degrades to a dot rather than counting them together (§6.3). */
function categoryHasSignal(category, data, viewerId) {
  return category.items.some((route) => {
    const spec = route.badge ? HD_NAV_BADGES[route.badge] : null;
    return spec ? spec.count(data, viewerId) > 0 : false;
  });
}

Object.assign(window, {
  staffById, staffName, patientById, patientName, wardName, initialsOf, drugById, doctorsOf, employeesOf,
  openAdmissions, bedsWaiting, bedWaitsBreached, fitForDischarge, onWard, admissionOfPatient,
  interventionsToday, consentMissing, unscheduledInterventions, interventionsForPatient, interventionsForSurgeon,
  ordersAwaitingPharmacy, activeOrders, dosesDue, dosesMissed, ordersForPatient, ordersOnHold,
  belowReorder, outOfStock, ordersForDrug,
  commentsFor, mentionsOwed, commentsByAuthor, feedItems,
  transfersNeedingReview, failedTransfers, imports, exports_, rowsAccepted,
  recordTitle, recordRoute, HD_NAV_BADGES, categoryHasSignal,
  wardBedsOf, wardRows, rotaFor, understaffedShifts, rotaWards, roundGroups, dosesGivenSince, reportFigures,
  formatPounds, formatPoundsExact, lineTotal, invoiceTotal, paymentsFor, invoicePaid, invoiceBalance, invoicesOverdue,
  invoicesBillable, invoicesUnbilled, invoicesForPatient, invoicesDisputed, billedTotal, collectedTotal, outstandingTotal,
  agedDebt, revenueByPayer, revenueByKind,
  contractOf, contractDueDays, tariffItemFor, contractedPrice, lineOffContract, offContractLines, contractPricesFor, contractsExpiring,
  chasesFor, creditWorklist, doseCount, lineCost, episodeCost, marginRows, costByKind,
});

})();
</script>
<script type="text/babel" data-presets="react" data-file="lib/command-palette.js">
(function(){
/* App-scope search — copied per product, domain-free. This file owns the
   shortcut, the query, the empty state and the route check; the shell supplies
   the record groups. The field in the toolbar is read-only and opens this same
   overlay, so search is a palette, not a filter.

   The shortcut is suppressed while a modal is open (`blocked`): a palette over a
   staged dialog is two dialogs deep, and navigating from it would leave the
   dialog floating over a screen it does not belong to. The host passes whether
   something modal is up; Escape still closes the palette itself.

   Optional `footer`: one persistent row under the list, given the palette's own
   close function. It is not filtered by the query, so it is for a destination
   rather than a record — "see every result", "open the full list". Products
   without one pass nothing and get no row. */

const { Command } = window.OriginDesignSystem_7c998b;

function useCommandPalette(blocked) {
  const [open, setOpen] = React.useState(false);
  React.useEffect(() => { if (blocked) setOpen(false); }, [blocked]);
  React.useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
        e.preventDefault();
        if (!blocked) setOpen((v) => !v);
      }
      if (e.key === "Escape") setOpen(false);
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [blocked]);
  return { open: open && !blocked, setOpen: (v) => setOpen(!!v && !blocked), bound: true };
}

function CommandPalette({ open, onClose, routes, recordGroups = [], onNavigate, placeholder, label, emptyMessage, footer }) {
  /* Route check: an item that leads nowhere is a bug in the caller, not a
     silent dead end. */
  React.useEffect(() => {
    if (!open) return;
    recordGroups.forEach((g) => g.items.forEach((i) => {
      if (i.route && !matchRoute(i.route, routes)) console.warn("[command-palette] item points at an unregistered route:", i.route);
    }));
  }, [open, recordGroups, routes]);

  if (!open) return null;

  const destinations = Object.values(routes)
    .filter((r) => !r.offSidebar)
    .map((r) => ({ value: r.path, label: r.label, route: r.path, shortcut: r.path === "/" ? null : null }));
  const groups = [{ label: "Go to", items: destinations }].concat(recordGroups);

  return (
    <>
      <div className="o-backdrop" onClick={onClose} />
      <div className="o-viewport">
        <div className="o-dialog fd-command" role="dialog" aria-modal="true" aria-label={label || "Search"}>
          <Command
            groups={groups}
            placeholder={placeholder || "Search…"}
            emptyMessage={emptyMessage || "Nothing matches that. Try a record or a destination."}
            onSelect={(item) => { if (item.route) onNavigate(item.route); onClose(); }}
          />
          {footer ? <div className="fd-command__foot">{typeof footer === "function" ? footer(onClose) : footer}</div> : null}
        </div>
      </div>
    </>
  );
}

Object.assign(window, { useCommandPalette, CommandPalette });

})();
</script>
<script type="text/babel" data-presets="react" data-file="lib/template-menu.js">
(function(){
/* Preview page menu — copied per product, domain-free, byte-identical across
   products. This is the template's own harness chrome, not part of any product
   shell: it exists so a reader who opens a template can reach every screen the
   route table registers, including the ones a running product would only reach
   from a real session — a record, an account screen, an expired reset link.

   Two things it deliberately owns, and why:
   · The page list is a projection of the product's route table (nav.js passes
     it through templatePages), so a route added anywhere shows up here without
     being retyped, and a record route with no fixture example is not offered.
   · The theme control. Auth screens render no shell (AGENTS §7.1), so on those
     screens the product has no theme control at all — and §7.3 wants dark mode
     verified on them too. Outside auth this sits beside the shell's own theme
     button on purpose: one belongs to the product, one to the harness, and the
     harness panel says "Template" at the top so the two do not read as a
     duplicated duty.

   It injects its own stylesheet once rather than adding rules to each product's
   kit.css, which is what keeps the eight copies identical. The one rule it
   reaches outside itself lifts the toast region so toasts stack above the
   button instead of burying it. */

const { Input } = window.OriginDesignSystem_7c998b;

const TEMPLATE_MENU_CSS = `
.tm-root{position:fixed;inset-block-end:var(--space-5);inset-inline-end:var(--space-5);z-index:80;display:flex;flex-direction:column;align-items:flex-end;gap:var(--space-2);pointer-events:none}
.tm-root>*{pointer-events:auto}
.tm-fab{display:inline-flex;width:44px;height:44px;flex:none;align-items:center;justify-content:center;border:0;border-radius:var(--radius-full);background:var(--foreground);color:var(--background);box-shadow:var(--shadow-lg);cursor:pointer;opacity:0;transition:transform var(--duration-fast) ease,opacity var(--duration-normal) ease}
/* The fab is preview chrome, not product UI: it stays out of the way until the
   pointer is in the app, and is always present for the keyboard and while open. */
html:hover .tm-fab,.tm-root:focus-within .tm-fab,.tm-fab[aria-expanded="true"]{opacity:1}
.tm-fab:hover{transform:translateY(-1px)}
.tm-fab:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
.tm-panel{display:flex;width:21rem;max-width:calc(100vw - var(--space-6));max-height:min(70vh,32rem);flex-direction:column;overflow:hidden;border:1px solid var(--border);border-radius:var(--radius-xl);background:var(--popover);color:var(--popover-foreground);box-shadow:var(--shadow-lg)}
.tm-head{display:flex;align-items:center;gap:var(--space-2);border-bottom:1px solid var(--border);padding:var(--space-3)}
.tm-chip{border:1px solid var(--border);border-radius:var(--radius-full);padding:1px var(--space-1-5);font-family:var(--font-mono);font-size:10px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted-foreground)}
.tm-head__title{font-family:var(--font-heading);font-size:var(--text-sm);font-weight:var(--font-weight-semibold)}
.tm-head__count{margin-inline-start:auto;font-size:var(--text-xs);color:var(--muted-foreground)}
.tm-filter{padding:var(--space-2) var(--space-3)}
.tm-body{flex:1;min-height:0;overflow-y:auto;padding:0 var(--space-2) var(--space-2)}
.tm-group+.tm-group{margin-block-start:var(--space-2)}
.tm-group__label{padding:var(--space-1-5) var(--space-2);font-size:var(--text-xs);font-weight:var(--font-weight-medium);color:var(--muted-foreground)}
.tm-item{display:flex;width:100%;align-items:baseline;gap:var(--space-2);border:0;border-radius:var(--radius-lg);background:transparent;padding:var(--space-1-5) var(--space-2);text-align:left;font:inherit;color:inherit;cursor:pointer}
.tm-item:hover{background:var(--accent)}
.tm-item:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}
.tm-item.is-active{background:var(--accent);color:var(--accent-foreground);font-weight:var(--font-weight-medium)}
.tm-item__label{flex:1;min-width:0;font-size:var(--text-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tm-item__path{flex:none;max-width:9rem;font-family:var(--font-mono);font-size:10px;color:var(--muted-foreground);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tm-empty{padding:var(--space-4) var(--space-2);font-size:var(--text-sm);color:var(--muted-foreground)}
.tm-foot{display:flex;align-items:center;gap:var(--space-2);border-top:1px solid var(--border);padding:var(--space-2-5) var(--space-3)}
.tm-foot__label{font-size:var(--text-xs);color:var(--muted-foreground)}
.tm-seg{display:flex;margin-inline-start:auto;gap:2px;border:1px solid var(--border);border-radius:var(--radius-md);padding:2px}
.tm-seg__btn{border:0;border-radius:calc(var(--radius-md) - 3px);background:transparent;padding:2px var(--space-2);font:inherit;font-size:var(--text-xs);color:var(--muted-foreground);cursor:pointer}
.tm-seg__btn.is-on{background:var(--accent);color:var(--accent-foreground)}
.o-toast-region{inset-block-end:calc(var(--space-5) + 44px + var(--space-2))}
@media (max-width:600px){.tm-root{inset-inline:var(--space-3);inset-block-end:var(--space-3)}.tm-panel{width:100%;max-width:none;max-height:64vh}.o-toast-region{inset-block-end:calc(var(--space-3) + 44px + var(--space-2))}}
`;

/* The route table read whole and grouped. `examples` maps a parameterised
   pattern to one concrete path from the fixture; a pattern with no example is
   left out rather than offered as a link that resolves nowhere. */
function templatePages(nav, authPaths, examples) {
  const destinations = [], records = [], settings = [], auth = [];
  /* An off-nav route may name its own group ("Create", "Reports"); a route in
     the nav may not, because there `group` is the sidebar's own sectioning. */
  const named = {}, order = [];
  const put = (label, item) => { if (!named[label]) { named[label] = []; order.push(label); } named[label].push(item); };
  nav.forEach((route) => {
    const parameterised = route.path.indexOf(":") > -1;
    const to = parameterised ? (examples || {})[route.path] : route.path;
    if (!to) return;
    const item = { to: to, label: route.label };
    if (authPaths.indexOf(route.path) > -1) auth.push(item);
    else if (!route.offSidebar) destinations.push(item);
    else if (route.group) put(route.group, item);
    else if (parameterised) records.push(item);
    else settings.push(item);
  });
  return [{ label: "Destinations", items: destinations }]
    .concat(order.map((label) => ({ label: label, items: named[label] })))
    .concat([
      { label: "Records", items: records },
      { label: "Account and settings", items: settings },
      { label: "Auth", items: auth },
    ])
    .filter((group) => group.items.length > 0);
}

function TemplateMenu({ product, path, onNavigate, groups, dark, onToggleTheme }) {
  const [open, setOpen] = React.useState(false);
  const [query, setQuery] = React.useState("");
  const rootRef = React.useRef(null);

  React.useEffect(() => {
    if (document.getElementById("tm-styles")) return;
    const style = document.createElement("style");
    style.id = "tm-styles";
    style.textContent = TEMPLATE_MENU_CSS;
    document.head.appendChild(style);
  }, []);

  React.useEffect(() => {
    if (!open) return undefined;
    const away = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("mousedown", away);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("mousedown", away); document.removeEventListener("keydown", onKey); };
  }, [open]);

  const total = groups.reduce((n, group) => n + group.items.length, 0);
  const q = query.trim().toLowerCase();
  const shown = groups
    .map((group) => ({ label: group.label, items: group.items.filter((item) => !q || (item.label + " " + item.to).toLowerCase().indexOf(q) > -1) }))
    .filter((group) => group.items.length > 0);
  const go = (to) => { onNavigate(to); setOpen(false); setQuery(""); };

  return (
    <div className="tm-root" ref={rootRef}>
      {open ? (
        <div className="tm-panel" role="dialog" aria-label={product + " — template pages"}>
          <div className="tm-head">
            <span className="tm-chip">Template</span>
            <span className="tm-head__title">{product}</span>
            <span className="tm-head__count">{total} pages</span>
          </div>
          <div className="tm-filter">
            <Input size="sm" value={query} onChange={(e) => setQuery(e.target.value)}
              aria-label="Filter pages" placeholder="Filter pages…" startAddon={<Icon name="search" size={14} />} />
          </div>
          <div className="tm-body">
            {shown.length ? shown.map((group) => (
              <div className="tm-group" key={group.label}>
                <div className="tm-group__label">{group.label}</div>
                {group.items.map((item) => {
                  const active = item.to === path;
                  return (
                    <button type="button" key={item.to} className={"tm-item" + (active ? " is-active" : "")}
                      aria-current={active ? "page" : undefined} onClick={() => go(item.to)}>
                      <span className="tm-item__label">{item.label}</span>
                      <span className="tm-item__path">{item.to}</span>
                    </button>
                  );
                })}
              </div>
            )) : <p className="tm-empty">No page matches “{query.trim()}”.</p>}
          </div>
          {onToggleTheme ? (
            <div className="tm-foot">
              <span className="tm-foot__label">Theme</span>
              <div className="tm-seg" role="group" aria-label="Theme">
                <button type="button" className={"tm-seg__btn" + (dark ? "" : " is-on")} aria-pressed={!dark} onClick={() => { if (dark) onToggleTheme(); }}>Light</button>
                <button type="button" className={"tm-seg__btn" + (dark ? " is-on" : "")} aria-pressed={!!dark} onClick={() => { if (!dark) onToggleTheme(); }}>Dark</button>
              </div>
            </div>
          ) : null}
        </div>
      ) : null}
      <button type="button" className="tm-fab" aria-expanded={open}
        aria-label={open ? "Close the template page menu" : "Template pages — " + total + " screens in " + product}
        onClick={() => setOpen(!open)}>
        <Icon name={open ? "x" : "layout-grid"} size={18} />
      </button>
    </div>
  );
}

Object.assign(window, { TemplateMenu, templatePages });

})();
</script>
<script type="text/babel" data-presets="react" data-file="HdShell.jsx">
(function(){
/* Ward Desk shell — SIDEBAR shell in RAIL + PANEL mode, one navigation owner.
   The rail owns the category, the panel owns the destinations inside it, and the
   top bar is utility only: it holds no destination and repeats nothing.
   Frame: two navigation columns on one tinted plane, each divided by a single
   hairline; the content column sits flush on the page colour with no inset pane
   and is the only scroll owner.
   Density: tight — 28px nav rows, 20px page gutters, 40px table rows.
   Separation: full-bleed hairline rules and the gutter. No cards, no elevation
   inside the content column; rounding belongs to controls and to overlays.
   Active state: --sidebar-accent wash + foreground token + medium weight; the
   rail's active category takes the page colour and a hairline instead, so it
   reads as the selected tab of the panel beside it.
   App-scope duties: search → top bar, a read-only field that opens the palette ·
   session and account → the rail footer, at every width · utility →
   top bar (notifications, theme) · page context → the page header band.
   Primary content per destination:
     Today          — figure row, then the ward board and the four lists of work
                      that is actually waiting on somebody
     Patients       — directory table with a photo per row; /patients/:id is one
                      person's admission, chart, procedures and notes
     Admissions     — the ward board as a table, bed waits first
     Interventions  — today's theatre list as a timed ladder over the full table;
                      /interventions/:id is one procedure, its consent and team
     Medication     — orders table scoped by ward, with the dose clock per row
     Formulary      — stock table with a reorder bar per line
     Doctors        — directory of the medical family, by specialty
     Employees      — the whole badge list, by job family
     Feed           — one chronological feed of what the hospital did, and the
                      notes owed a reply
     Import/export  — job table, and the row-review screen an import needs
   Interventions is the destination that would most plausibly have been a plain
   table and is not: the question a theatre list answers is "what happens next,
   and can it start", which a timed ladder answers in one glance.
   Sign-in and the reset flow render with no shell at all (AGENTS §7.1). */

const {
  Button, Input, Kbd, Avatar, MenuItem, MenuGroupLabel,
  Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator,
  Toast, ToastRegion,
} = window.OriginDesignSystem_7c998b;

const HD_BRAND_MARK = (window.__resources["r027"]);

/* One declaration for the whole shell: 1280 rail + panel · 1024 rail only, the
   panel's destinations become a flyout · below that one off-canvas drawer
   carrying both columns. */
function useShellMode() {
  const read = () => (window.innerWidth >= 1280 ? "expanded" : window.innerWidth >= 1024 ? "rail" : "drawer");
  const [mode, setMode] = React.useState(read);
  React.useEffect(() => {
    const onResize = () => setMode(read());
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  return mode;
}

function NavLink({ route, active, badge, onNavigate }) {
  return (
    <button type="button" className="hd-link" aria-current={active ? "page" : undefined} onClick={() => onNavigate(route.path)}>
      <Icon name={route.icon} size={15} />
      <span className="hd-link__label">{route.label}</span>
      {badge && badge.count > 0 ? <span className="hd-link__count" aria-label={badge.count + " " + badge.label}>{badge.count}</span> : null}
    </button>
  );
}

/* The rail. A category is not a route (nav.js says so), so these are tabs over
   the panel rather than links: they carry aria-selected, and the label survives
   as both a tooltip and an aria-label at every width. */
function CategoryRail({ categories, selected, path, data, viewer, onSelect, mode, onNavigate }) {
  return (
    <nav className="hd-rail" aria-label={HD_HOSPITAL.short + " — categories"}>
      <span className="hd-rail__brand" aria-hidden="true"><img src={HD_BRAND_MARK} alt="" width="20" height="20" /></span>
      <div className="hd-rail__list" role="tablist" aria-orientation="vertical" aria-label="Categories">
        {categories.map((category) => {
          const on = category.id === selected;
          const signal = categoryHasSignal(category, data, viewer.id);
          const button = (
            <button type="button" role="tab" className={"hd-cat" + (on ? " is-on" : "")} aria-selected={on} aria-label={category.label + " — " + category.note}
              aria-controls={mode === "expanded" ? "hd-panel" : undefined} onClick={() => onSelect(category.id)}>
              <Icon name={category.icon} size={18} />
              {signal ? <span className="hd-cat__dot" aria-hidden="true" /> : null}
            </button>
          );
          if (mode === "expanded") return <FloatingTip key={category.id} label={category.label}>{button}</FloatingTip>;
          /* Rail mode: the panel is gone, so the category opens its own list. */
          return (
            <Floating key={category.id} align="start" width="15rem" label={category.label} panelClass="o-menu hd-flyout"
              trigger={button}>
              {(close) => (
                <>
                  <span className="hd-flyout__title">{category.label} — {category.note}</span>
                  {category.items.map((route) => {
                    const spec = route.badge ? HD_NAV_BADGES[route.badge] : null;
                    const count = spec ? spec.count(data, viewer.id) : 0;
                    return (
                      <MenuItem key={route.path} onClick={() => { onSelect(category.id); onNavigate(route.path); close(); }}>
                        {route.label}{count > 0 ? " · " + count + " " + spec.label : ""}
                      </MenuItem>
                    );
                  })}
                </>
              )}
            </Floating>
          );
        })}
      </div>
      <div className="hd-rail__foot">
        <AccountMenu rail viewer={viewer} onNavigate={onNavigate} />
      </div>
    </nav>
  );
}

function CategoryPanel({ category, path, data, viewer, onNavigate, onClose }) {
  return (
    <div className="hd-panel" id="hd-panel" role="tabpanel" aria-label={category.label}>
      <div className="hd-panel__head">
        <span className="hd-panel__hospital">{HD_HOSPITAL.short}</span>
        <span className="hd-panel__title">{category.label}</span>
        <span className="hd-panel__note">{category.note}</span>
      </div>
      <div className="hd-panel__nav">
        {category.items.map((route) => {
          const spec = route.badge ? HD_NAV_BADGES[route.badge] : null;
          return (
            <NavLink
              key={route.path}
              route={route}
              active={route.path === path || (route.path !== "/" && path.indexOf(route.path + "/") === 0)}
              badge={spec ? { count: spec.count(data, viewer.id), label: spec.label } : null}
              onNavigate={(p) => { onNavigate(p); if (onClose) onClose(); }}
            />
          );
        })}
      </div>
    </div>
  );
}

function AccountMenu({ rail, viewer, onNavigate }) {
  const trigger = (
    <button type="button" className={"hd-account" + (rail ? " hd-account--rail" : "")} aria-label={"Account — " + viewer.name + ", " + viewer.jobTitle}>
      <Avatar size={rail ? 28 : 32} src={viewer.avatar} alt="" fallback={initialsOf(viewer.name)} />
      {rail ? null : (
        <span className="hd-account__text">
          <span className="hd-account__name">{viewer.name}</span>
          <span className="hd-account__role">{viewer.jobTitle}</span>
        </span>
      )}
    </button>
  );
  return (
    <Floating trigger={trigger} align="start" width="16rem" label="Account">
      {(close) => (
        <>
          <MenuGroupLabel>{viewer.name} · {viewer.jobTitle}</MenuGroupLabel>
          {HD_ACCOUNT_MENU.map((route) => (
            <MenuItem key={route.path} onClick={() => { onNavigate(route.path); close(); }}>{route.label}</MenuItem>
          ))}
          <MenuItem onClick={() => { onNavigate("/signin"); close(); }}>Sign out</MenuItem>
        </>
      )}
    </Floating>
  );
}

function NotificationsButton({ items, unread, onRead, onNavigate }) {
  const trigger = (
    <span className="hd-utility">
      <Button variant="ghost" size="icon-sm" aria-label={unread ? unread + " unread notifications" : "Notifications"}>
        <Icon name="bell" />
      </Button>
      {unread ? <span className="hd-badge-dot" aria-hidden="true">{unread}</span> : null}
    </span>
  );
  return (
    <FloatingTip label="Notifications" side="bottom">
      <Floating trigger={trigger} align="end" width="24rem" role="dialog" panelClass="o-popover hd-notify" label="Notifications">
        {(close) => (
          <>
            <div className="hd-notify__head">
              <span className="o-popover__title">Notifications</span>
              <Button variant="ghost" size="xs" onClick={onRead}>Mark all read</Button>
            </div>
            <div className="hd-notify__list">
              {items.map((n) => (
                <button type="button" key={n.id} className="hd-notify__item" onClick={() => { onNavigate(n.route); close(); }}>
                  <span className="hd-notify__title">{n.title}</span>
                  <span className="hd-notify__body">{n.body}</span>
                  <span className="hd-notify__time" title={formatAbsolute(n.at)}>{formatWhen(n.at, HD_NOW)}</span>
                </button>
              ))}
            </div>
          </>
        )}
      </Floating>
    </FloatingTip>
  );
}

function TopBar({ mode, dark, onToggleTheme, onOpenPalette, onOpenDrawer, notifications, unread, onRead, onNavigate }) {
  return (
    <div className="hd-topbar">
      {mode === "drawer" ? (
        <Button variant="ghost" size="icon-sm" aria-label="Open navigation" onClick={onOpenDrawer}><Icon name="menu" /></Button>
      ) : null}
      <div className="hd-search" onClick={onOpenPalette}>
        <Input
          readOnly
          size="sm"
          aria-label={"Search " + HD_HOSPITAL.short}
          placeholder="Search patients, procedures, staff…"
          startAddon={<Icon name="search" />}
          endAddon={<Kbd>⌘K</Kbd>}
          onFocus={onOpenPalette}
        />
      </div>
      <span className="hd-search-btn">
        <FloatingTip label="Search — ⌘K" side="bottom">
          <Button variant="ghost" size="icon-sm" aria-label={"Search " + HD_HOSPITAL.short} onClick={onOpenPalette}><Icon name="search" /></Button>
        </FloatingTip>
      </span>
      <div className="hd-topbar__utility">
        <NotificationsButton items={notifications} unread={unread} onRead={onRead} onNavigate={onNavigate} />
        <FloatingTip label={dark ? "Switch to light" : "Switch to dark"} side="bottom">
          <Button variant="ghost" size="icon-sm" aria-label={dark ? "Switch to light theme" : "Switch to dark theme"} onClick={onToggleTheme}>
            <Icon name={dark ? "sun" : "moon"} />
          </Button>
        </FloatingTip>
      </div>
    </div>
  );
}

/* Every screen renders PageHeader and none of them pass the router, so the shell
   registers it once per render and the crumbs read it from here. */
let breadcrumbNavigate = null;

/* The last crumb NAMES the record: the trail still comes from the route table,
   but a parameterised route's own segment reads the record's name rather than
   the generic label, because "Patients / Patient" tells the reader nothing. */
function PageHeader({ path, title, sub, actions, params, crumb }) {
  const trail = breadcrumb(path, HD_ROUTES).map((seg, i, all) => (
    crumb && i === all.length - 1 ? Object.assign({}, seg, { label: crumb }) : seg
  ));
  return (
    <header className="hd-pagehead">
      <div>
        <Breadcrumb>
          <BreadcrumbList>
            {trail.map((seg, i) => (
              <React.Fragment key={seg.path}>
                {i > 0 ? <BreadcrumbSeparator /> : null}
                <BreadcrumbItem>
                  {i === trail.length - 1
                    ? <BreadcrumbPage>{seg.label}</BreadcrumbPage>
                    : seg.isNamespace ? <span>{seg.label}</span>
                      : <BreadcrumbLink href={"#" + resolvePath(seg.path, params || {})} onSelect={() => breadcrumbNavigate && breadcrumbNavigate(resolvePath(seg.path, params || {}))}>{seg.label}</BreadcrumbLink>}
                </BreadcrumbItem>
              </React.Fragment>
            ))}
          </BreadcrumbList>
        </Breadcrumb>
        <h1 className="hd-pagetitle">{title}</h1>
        {sub ? <p className="hd-pagesub">{sub}</p> : null}
      </div>
      {actions ? <div className="hd-actions">{actions}</div> : null}
    </header>
  );
}

function HdShell({ path, onNavigate, dark, onToggleTheme, data, viewer, notifications, unread, onRead, palette, toasts, onDismissToast, children }) {
  breadcrumbNavigate = onNavigate;
  const mode = useShellMode();
  const [drawer, setDrawer] = React.useState(false);
  /* The panel follows the route, and a reader may also browse a category without
     navigating — the rail is a tablist over the panel, not a set of links. */
  const [category, setCategory] = React.useState(() => categoryOf(path) || HD_RAIL[0].id);
  React.useEffect(() => { const next = categoryOf(path); if (next) setCategory(next); }, [path]);
  React.useEffect(() => { if (mode !== "drawer") setDrawer(false); }, [mode]);
  const selected = HD_RAIL.find((c) => c.id === category) || HD_RAIL[0];

  const rail = (
    <CategoryRail categories={HD_RAIL} selected={selected.id} path={path} data={data} viewer={viewer}
      onSelect={setCategory} mode={mode === "rail" ? "rail" : "expanded"} onNavigate={(p) => { onNavigate(p); setDrawer(false); }} />
  );
  const panel = (
    <CategoryPanel category={selected} path={path} data={data} viewer={viewer}
      onNavigate={onNavigate} onClose={() => setDrawer(false)} />
  );

  return (
    <div className="hd-shell" data-mode={mode}>
      {mode === "drawer" ? (
        drawer ? (
          <>
            <div className="hd-drawer-scrim" onClick={() => setDrawer(false)} />
            <div className="hd-drawer">{rail}{panel}</div>
          </>
        ) : null
      ) : (
        <>
          {rail}
          {mode === "expanded" ? panel : null}
        </>
      )}

      <main className="hd-main">
        <TopBar
          mode={mode}
          dark={dark}
          onToggleTheme={onToggleTheme}
          onOpenPalette={() => palette.setOpen(true)}
          onOpenDrawer={() => setDrawer(true)}
          notifications={notifications}
          unread={unread}
          onRead={onRead}
          onNavigate={onNavigate}
        />
        {children}
      </main>

      <ToastRegion>
        {toasts.map((t) => (
          <Toast key={t.id} variant={t.variant} title={t.title} description={t.description} onClose={() => onDismissToast(t.id)}
            action={t.onUndo ? <Button variant="outline" size="xs" onClick={() => { t.onUndo(); onDismissToast(t.id); }}>Undo</Button> : null} />
        ))}
      </ToastRegion>
    </div>
  );
}

Object.assign(window, { HdShell, PageHeader, useShellMode, AccountMenu, HD_BRAND_MARK });

})();
</script>
<script type="text/babel" data-presets="react" data-file="HdParts.jsx">
(function(){
/* Ward Desk — the parts every screen shares.
   A Band is a full-bleed region divided by a hairline rule; there are no cards
   in this product, so nothing here draws a box. Nothing here holds data or
   assigns a status tone: StatusBadge does the tone, metrics.js does the
   arithmetic, enums.js decides which transitions exist. Reported in the README
   as local additions to the design system's inventory. */
(() => {
  const {
    Button, Input, Select, Checkbox, Textarea, Field, FieldLabel, FieldDescription, FieldError,
    Dialog, DialogPopup, DialogHeader, DialogTitle, DialogDescription, DialogPanel, DialogFooter,
    AlertDialog, AlertDialogPopup, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter,
    RadioGroup, MenuItem, MenuGroupLabel, Badge, Avatar, Separator,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  /* ── Structure ─────────────────────────────────────────────────────────── */
  /* Four variants, and nothing else: default, flush (a child bleeds to the
     card edge), tint (recessed — work nobody can act on yet), fill (stretches to
     its row so cards side by side align their footers). */
  function Band({ title, note, actions, flush, tint, fill, children }) {
    return (
      <section className={"hd-band" + (flush ? " hd-band--flush" : "") + (tint ? " hd-band--tint" : "") + (fill ? " hd-band--fill" : "")}>
        {title || actions ? (
          <div className="hd-band__head">
            <div>
              <h2 className="hd-band__title">{title}</h2>
              {note ? <p className="hd-band__note">{note}</p> : null}
            </div>
            {actions ? <div className="hd-actions">{actions}</div> : null}
          </div>
        ) : null}
        {children}
      </section>
    );
  }

  /* Four figures to a row, or three where the screen has three. Every one
     carries a qualifier that decomposes its own figure's set. */
  function Figures({ items, three }) {
    return (
      <div className={"hd-figs" + (three ? " hd-figs--3" : "")}>
        {items.map((item) => (
          <div key={item.label}>
            <span className="hd-fig__label">{item.label}</span>
            <span className="hd-fig">{item.value}</span>
            <span className="hd-fig__qualifier">{item.qualifier}</span>
          </div>
        ))}
      </div>
    );
  }

  function Pairs({ items }) {
    return (
      <div className="hd-pairs">
        {items.map((item) => (
          <div className="hd-pair" key={item.label}>
            <span className="hd-pair__label">{item.label}</span>
            <span className="hd-pair__value">{item.value}</span>
          </div>
        ))}
      </div>
    );
  }

  function StockBar({ percent, tone }) {
    return (
      <span className="hd-bar" aria-hidden="true">
        <span className={"hd-bar__fill" + (tone ? " hd-bar__fill--" + tone : "")} style={{ width: Math.max(2, Math.min(100, percent)) + "%" }} />
      </span>
    );
  }

  /* ── People cells: the photo is part of identifying a person ────────────── */
  function PersonCell({ name, meta, avatar, size = 28, onOpen, badge }) {
    const label = (
      <span className="hd-person__text">
        <span className="hd-person__name">{onOpen ? <button type="button" className="hd-linkbtn" onClick={onOpen}>{name}</button> : name}</span>
        {meta ? <span className="hd-person__meta">{meta}</span> : null}
      </span>
    );
    return (
      <span className="hd-person">
        <Avatar size={size} src={avatar} alt={"Photograph of " + name} fallback={initialsOf(name)} />
        {label}
        {badge || null}
      </span>
    );
  }

  /* ── Time in a cell ────────────────────────────────────────────────────
     One format rule per column (§8.3). `problem` is a property of the column,
     declared by the caller once: whether a time in the past is a problem. */
  function Clock({ at, now, problem, dangerous }) {
    if (!at) return <span className="hd-muted">Never</span>;
    const past = at < now;
    const cls = past && problem ? (dangerous ? "hd-danger" : "hd-warn") : "";
    return <span className={cls} title={formatAbsolute(at)}>{formatWhen(at, now)}</span>;
  }

  /* ── Actions ───────────────────────────────────────────────────────────── */
  function Transitions({ transitions, onRun, size = "sm" }) {
    if (!transitions.length) return null;
    return (
      <span className="hd-actions__row">
        {transitions.map((t) => {
          const button = (
            <Button key={t.id} size={size} disabled={!!t.disabled}
              variant={t.destructive ? "destructive-outline" : t.primary ? "default" : "outline"}
              onClick={() => onRun(t)}>{t.label}</Button>
          );
          return t.disabled && t.disabledReason
            ? <FloatingTip key={t.id} label={t.disabledReason} side="bottom">{button}</FloatingTip>
            : button;
        })}
      </span>
    );
  }

  /* A row carries the transition its status asks for — the primary one — as a
     button. Everything else goes in the row's menu: a list is not where you put
     an action that cannot be undone. */
  function RowActions({ transitions, onRun, label, extra }) {
    const primary = transitions.filter((t) => t.primary);
    const rest = transitions.filter((t) => !t.primary).concat(extra || []);
    if (!primary.length && !rest.length) return <span className="hd-muted">—</span>;
    return (
      <span className="hd-actions__row">
        {primary.length ? <Transitions transitions={primary} onRun={onRun} /> : null}
        {rest.length ? (
          <Floating align="end" width="14rem" label={label}
            trigger={<Button variant="ghost" size="icon-sm" aria-label={label}><Icon name="ellipsis" /></Button>}>
            {(close) => rest.map((t) => (
              <MenuItem key={t.id} disabled={!!t.disabled} variant={t.destructive ? "destructive" : "default"} onClick={() => { close(); onRun(t); }}>{t.label}</MenuItem>
            ))}
          </Floating>
        ) : null}
      </span>
    );
  }

  /* ── List furniture: same order on every list screen (§5) ───────────────── */
  function FilterBar({ query, onQuery, placeholder, selects, onClear, dirty }) {
    return (
      <div className="hd-filters">
        <div className="hd-filters__text">
          <Input size="sm" value={query} placeholder={placeholder} aria-label={placeholder} startAddon={<Icon name="search" />} onChange={(e) => onQuery(e.target.value)} />
        </div>
        {(selects || []).map((s) => (
          <span className="hd-filters__select" key={s.id}>
            <Select size="sm" items={s.items} value={s.value} aria-label={s.label} onValueChange={s.onChange} />
          </span>
        ))}
        {dirty ? <Button variant="ghost" size="sm" onClick={onClear}>Clear filters</Button> : null}
      </div>
    );
  }

  function BulkBar({ count, note, children, onClear }) {
    if (!count) return null;
    return (
      <div className="hd-bulk">
        <span className="hd-bulk__count">{count} selected</span>
        {note ? <span className="hd-footnote">{note}</span> : null}
        <span className="hd-bulk__actions">
          {children}
          <Button variant="ghost" size="sm" onClick={onClear}>Clear selection</Button>
        </span>
      </div>
    );
  }

  /* Export sends data out; Import brings it in (§8.2). Both are offered on every
     list, and both say what scope they are about to act on. */
  function ExportMenu({ scope, onExport, size = "sm" }) {
    return (
      <Floating align="end" width="13rem" label="Export"
        trigger={<Button variant="outline" size={size}><Icon name="download" />Export<Icon name="chevron-down" /></Button>}>
        {(close) => (
          <>
            <MenuGroupLabel>{scope}</MenuGroupLabel>
            {["CSV", "XLSX"].map((format) => (
              <MenuItem key={format} onClick={() => { close(); onExport(format); }}>{format}</MenuItem>
            ))}
          </>
        )}
      </Floating>
    );
  }

  function ImportButton({ onImport, viewer, size = "sm" }) {
    const may = ROLE_CAN_IMPORT[viewer.staffRole];
    const button = <Button variant="outline" size={size} disabled={!may} onClick={onImport}><Icon name="upload" />Import</Button>;
    return may ? button : <FloatingTip label={"Importing writes into the patient index. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "."} side="bottom">{button}</FloatingTip>;
  }

  function TableWrap({ children }) {
    return <div className="hd-tablewrap">{children}</div>;
  }

  function TableFoot({ shown, total, noun, children }) {
    return (
      <div className="hd-tablefoot">
        <span className="hd-footnote">Showing {shown} of {total} {noun}</span>
        {children ? <span className="hd-actions__row">{children}</span> : null}
      </div>
    );
  }

  function NoResults({ noun, onClear }) {
    return (
      <div style={{ padding: "var(--space-6) var(--gutter-page)" }}>
        <Empty>
          <EmptyHeader>
            <EmptyMedia variant="icon"><Icon name="search-x" size={18} /></EmptyMedia>
            <EmptyTitle>No {noun} match those filters</EmptyTitle>
            <EmptyDescription>Nothing in this list matches every filter at once. Clearing them brings the whole list back.</EmptyDescription>
          </EmptyHeader>
          <EmptyContent><Button onClick={onClear}>Clear filters</Button></EmptyContent>
        </Empty>
      </div>
    );
  }

  /* ── Notes: the social layer, instant, no Save anywhere near it ─────────── */
  function noteBody(body, staff) {
    const parts = String(body).split(/(@stf_\d+)/g);
    return parts.map((part, i) => {
      if (!/^@stf_\d+$/.test(part)) return part;
      const person = staffById(staff, part.slice(1));
      return <mark key={i}>@{person ? person.name : part.slice(1)}</mark>;
    });
  }

  const REACTIONS = [
    { key: "ack", label: "Acknowledged" },
    { key: "agree", label: "Agree" },
    { key: "seen", label: "Seen" },
  ];

  function Note({ comment, staff, viewer, now, onReact, context, plain }) {
    const author = staffById(staff, comment.authorId);
    return (
      <article className={"hd-note" + (plain ? " hd-note--plain" : "")}>
        <span className="hd-note__avatar">
          <Avatar size={32} src={author ? author.avatar : undefined} alt={author ? "Photograph of " + author.name : ""} fallback={initialsOf(author ? author.name : "?")} />
        </span>
        <div className="hd-note__head">
          <span className="hd-note__author">{author ? author.name : "Former colleague"}</span>
          <span className="hd-note__role">{author ? author.jobTitle : ""}</span>
          <span className="hd-note__time" title={formatAbsolute(comment.at)}>{formatWhen(comment.at, now)}</span>
          {context ? <span className="hd-note__role">on {context}</span> : null}
        </div>
        <p className="hd-note__body">{noteBody(comment.body, staff)}</p>
        <div className="hd-note__foot">
          {REACTIONS.map((r) => {
            const ids = comment.reactions[r.key] || [];
            if (!ids.length && r.key !== "ack") return null;
            const mine = ids.indexOf(viewer.id) > -1;
            return (
              <button type="button" key={r.key} className={"hd-react" + (mine ? " is-on" : "")}
                aria-pressed={mine} aria-label={r.label + (ids.length ? " — " + ids.map((id) => staffName(staff, id)).join(", ") : "")}
                onClick={() => onReact(comment, r.key)}>
                {r.label}{ids.length ? " " + ids.length : ""}
              </button>
            );
          })}
        </div>
      </article>
    );
  }

  /* Posting is instant: the note is written into the fixture and every count
     that reads it moves. Mentions are picked from the staff list rather than
     typed, so a mention always resolves to somebody real. */
  function NoteComposer({ staff, viewer, onPost, placeholder }) {
    const [body, setBody] = React.useState("");
    const [mention, setMention] = React.useState("");
    const dirty = body.trim().length > 0;
    const post = () => {
      const text = mention ? body.trim() + " @" + mention : body.trim();
      onPost(text);
      setBody("");
      setMention("");
    };
    return (
      <div className="hd-composer">
        <Textarea rows={2} value={body} placeholder={placeholder} aria-label="Write a note" onChange={(e) => setBody(e.target.value)} />
        <div className="hd-composer__foot">
          <span className="hd-filters__select">
            <Select size="sm" aria-label="Mention somebody" value={mention} onValueChange={setMention}
              items={[{ value: "", label: "Mention nobody" }].concat(staff.filter((s) => s.id !== viewer.id && s.status !== "left").map((s) => ({ value: s.id, label: s.name + " — " + s.jobTitle })))} />
          </span>
          <Button size="sm" disabled={!dirty} onClick={post}>Post note</Button>
        </div>
      </div>
    );
  }

  function NoteThread({ comments, staff, viewer, now, onReact, onPost, placeholder }) {
    return (
      <>
        <div className="hd-notes">
          {comments.length
            ? comments.map((c) => <Note key={c.id} comment={c} staff={staff} viewer={viewer} now={now} onReact={onReact} />)
            : <p className="hd-footnote">Nothing has been written on this record yet. A note here is visible to everybody with access to it.</p>}
        </div>
        <NoteComposer staff={staff} viewer={viewer} onPost={onPost} placeholder={placeholder} />
      </>
    );
  }

  /* ── Dialogs ───────────────────────────────────────────────────────────── */
  function ConfirmDialog({ request, onCancel, onConfirm }) {
    return (
      <AlertDialog open={!!request} onOpenChange={(open) => { if (!open) onCancel(); }}>
        <AlertDialogPopup>
          <AlertDialogHeader>
            <AlertDialogTitle>{request ? request.title : ""}</AlertDialogTitle>
            <AlertDialogDescription>{request ? request.description : ""}</AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <Button variant="outline" onClick={onCancel}>Cancel</Button>
            <Button variant="destructive" onClick={onConfirm}>{request ? request.confirmLabel : ""}</Button>
          </AlertDialogFooter>
        </AlertDialogPopup>
      </AlertDialog>
    );
  }

  /* A staged dialog — its own commit model, so an instant screen may open one. */
  function FormDialog({ request, onCancel, onSubmit }) {
    const [values, setValues] = React.useState({});
    const [touched, setTouched] = React.useState(false);
    const [submitted, setSubmitted] = React.useState(false);
    React.useEffect(() => { setValues(request ? request.initial || {} : {}); setTouched(false); setSubmitted(false); }, [request]);
    if (!request) return null;
    const errors = request.validate ? request.validate(values) : {};
    const valid = Object.keys(errors).length === 0;
    const set = (id, value) => { setTouched(true); setValues((v) => Object.assign({}, v, { [id]: value })); };
    return (
      <Dialog open onOpenChange={(open) => { if (!open) onCancel(); }}>
        <DialogPopup>
          <DialogHeader>
            <DialogTitle>{request.title}</DialogTitle>
            <DialogDescription>{request.description}</DialogDescription>
          </DialogHeader>
          <DialogPanel>
            <div className="hd-fields hd-fields--one">
              {request.fields.filter((f) => !f.when || f.when(values)).map((field) => (
                <Field key={field.id}>
                  <FieldLabel htmlFor={"hd-f-" + field.id}>{field.label}</FieldLabel>
                  {field.kind === "radio" ? (
                    <RadioGroup name={"hd-f-" + field.id} items={field.options} value={values[field.id] || ""} onValueChange={(v) => set(field.id, v)} />
                  ) : field.kind === "select" ? (
                    <Select items={field.options} value={values[field.id] || ""} aria-label={field.label} onValueChange={(v) => set(field.id, v)} />
                  ) : field.kind === "text" ? (
                    <Textarea id={"hd-f-" + field.id} rows={3} placeholder={field.placeholder} value={values[field.id] || ""} invalid={submitted && !!errors[field.id]} onChange={(e) => set(field.id, e.target.value)} />
                  ) : field.kind === "checkbox" ? (
                    <span className="hd-person">
                      <Checkbox id={"hd-f-" + field.id} checked={!!values[field.id]} onCheckedChange={(v) => set(field.id, v)} />
                      <span className="hd-pair__value">{field.checkboxLabel}</span>
                    </span>
                  ) : (
                    <Input id={"hd-f-" + field.id} placeholder={field.placeholder} value={values[field.id] || ""} invalid={submitted && !!errors[field.id]} onChange={(e) => set(field.id, e.target.value)} />
                  )}
                  {submitted && errors[field.id] ? <FieldError>{errors[field.id]}</FieldError> : field.hint ? <FieldDescription>{field.hint}</FieldDescription> : null}
                </Field>
              ))}
            </div>
          </DialogPanel>
          <DialogFooter>
            {touched ? <Button variant="outline" onClick={onCancel}>Cancel</Button> : null}
            <Button disabled={!touched} onClick={() => { setSubmitted(true); if (valid) onSubmit(values); }}>{request.submitLabel}</Button>
          </DialogFooter>
        </DialogPopup>
      </Dialog>
    );
  }

  /* The staged form bar a staged screen ends with. Sticky to the bottom of the
     content column, which is the scroll owner. */
  function FormBar({ dirty, note, onCancel, onSave, label }) {
    return (
      <div className="hd-formbar">
        <span className="hd-formbar__note">{note}</span>
        {dirty ? <Button variant="outline" onClick={onCancel}>Cancel</Button> : null}
        <Button disabled={!dirty} onClick={onSave}>{label}</Button>
      </div>
    );
  }

  /* Not a route and not an empty state: the fallback for a hand-typed or stale
     path, which has no breadcrumb because breadcrumbs are made of the route
     table. */
  function RouteNotBuilt({ path, onNavigate }) {
    return (
      <div style={{ padding: "var(--space-8) var(--gutter-page)" }}>
        <Empty>
          <EmptyHeader>
            <EmptyMedia variant="icon"><Icon name="signpost" size={18} /></EmptyMedia>
            <EmptyTitle>Nothing is registered at {path}</EmptyTitle>
            <EmptyDescription>Ward Desk registers twenty destinations and the screens behind them. The route table decides what exists, so this address has no screen behind it.</EmptyDescription>
          </EmptyHeader>
          <EmptyContent><Button onClick={() => onNavigate("/")}>Go to Today</Button></EmptyContent>
        </Empty>
      </div>
    );
  }

  /* Sorting: one hook, one header control, one comparator, so every sortable
     table in the product behaves identically. */
  function useSort(key, dir) {
    const [sort, setSort] = React.useState({ key: key, dir: dir || "desc" });
    const onSort = (next) => setSort((s) => (s.key === next ? { key: next, dir: s.dir === "asc" ? "desc" : "asc" } : { key: next, dir: "desc" }));
    return { sort: sort, onSort: onSort };
  }

  function SortHead({ label, sortKey, sort, onSort }) {
    const on = sort.key === sortKey;
    return (
      <button type="button" className="hd-sort" data-on={on ? "true" : "false"}
        aria-label={"Sort by " + label + (on && sort.dir === "asc" ? ", highest first" : ", lowest first")}
        onClick={() => onSort(sortKey)}>
        {label}
        <Icon name={on ? (sort.dir === "asc" ? "arrow-up" : "arrow-down") : "chevrons-up-down"} size={12} />
      </button>
    );
  }

  function sortRows(rows, sort, accessors) {
    const get = accessors[sort.key];
    if (!get) return rows;
    return rows.slice().sort((a, b) => {
      const av = get(a), bv = get(b);
      if (typeof av === "string" || typeof bv === "string") {
        const cmp = String(av == null ? "" : av).localeCompare(String(bv == null ? "" : bv));
        return sort.dir === "asc" ? cmp : -cmp;
      }
      const an = av == null ? -Infinity : av, bn = bv == null ? -Infinity : bv;
      return sort.dir === "asc" ? an - bn : bn - an;
    });
  }

  function Chip({ children, variant = "secondary" }) {
    return <Badge size="sm" variant={variant}>{children}</Badge>;
  }

  /* An allergy is not a status — it is a fact about the person that every
     clinical screen has to carry. */
  function AllergyChips({ patient }) {
    if (!PATIENT_FLAGS.hasAllergyAlert(patient)) return null;
    return (
      <span className="hd-chips">
        {patient.allergies.map((a) => <Badge key={a} size="sm" variant="destructive">{a}</Badge>)}
      </span>
    );
  }

  Object.assign(window, {
    Band, Figures, Pairs, StockBar, PersonCell, Clock, Transitions, RowActions, FilterBar, BulkBar,
    ExportMenu, ImportButton, TableWrap, TableFoot, NoResults, Note, NoteThread, NoteComposer, noteBody, REACTIONS,
    ConfirmDialog, FormDialog, FormBar, RouteNotBuilt, useSort, SortHead, sortRows, Chip, AllergyChips,
  });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="TodayScreen.jsx">
(function(){
/* Today — the shift-start screen. Commit model: INSTANT. Every control here
   applies on its own and raises an undo toast where the change is consequential;
   there is no Save on this screen.

   Four figures, then the ward board, then the work that is actually waiting on
   somebody. Every list runs its own predicate from metrics.js — none of them is
   a slice — and every figure names the array it counts. */
(() => {
  const { Button, Badge } = window.OriginDesignSystem_7c998b;

  function WardBoard({ data, onNavigate }) {
    const wards = wardRows(data.admissions, data.wardBeds);
    return (
      <div className="hd-ward">
        {wards.map((ward) => (
          <div className="hd-ward__cell" key={ward.ward}>
            <span className="hd-ward__top">
              <span className="hd-ward__name">{ward.label}</span>
              <span className="hd-ward__count">{ward.occupied}<span className="hd-footnote"> / {ward.beds}</span></span>
            </span>
            <BedGrid ward={ward} />
            <span className="hd-footnote">
              {ward.specialty} · {ward.free} free
              {ward.held ? " · " + ward.held + (ward.held === 1 ? " bed held" : " beds held") : ""}
              {ward.waiting ? " · " + ward.waiting + " waiting" : ""}
            </span>
          </div>
        ))}
      </div>
    );
  }

  function WaitingList({ rows, data, now, viewer, onNavigate, onAdmissionTransition }) {
    if (!rows.length) return <p className="hd-footnote">Nobody is waiting for a bed. Every request from the emergency department has a bed named against it.</p>;
    return (
      <div className="hd-rows">
        {rows.map((admission) => {
          const patient = patientById(data.patients, admission.patientId);
          const breached = ADMISSION_FLAGS.bedWaitBreached(admission, now);
          return (
            <div className="hd-row" key={admission.id}>
              <div className="hd-row__text">
                <span className="hd-row__title">
                  <PersonCell name={patient.name} avatar={patient.avatar} onOpen={() => onNavigate("/patients/" + patient.id)}
                    meta={wardName(admission.ward) + " · " + labelOf(ADMISSION_SOURCES, admission.source)} />
                </span>
                <span className="hd-row__note">{admission.reason} · requested {formatWhen(admission.requestedAt, now)}, <span className={breached ? "hd-warn" : ""}>{formatCountdown(admission.requestedAt, now)}</span></span>
              </div>
              <AllergyChips patient={patient} />
              <Transitions transitions={admissionTransitions(admission, viewer).filter((t) => t.primary)} onRun={(t) => onAdmissionTransition(admission, t)} />
            </div>
          );
        })}
      </div>
    );
  }

  function HeldList({ rows, data, now, viewer, onNavigate, onAdmissionTransition }) {
    if (!rows.length) return <p className="hd-footnote">No bed is being held. Everybody marked fit for discharge has gone home the same day.</p>;
    return (
      <div className="hd-rows">
        {rows.map((admission) => {
          const patient = patientById(data.patients, admission.patientId);
          const held = ADMISSION_FLAGS.dischargeHeld(admission, now);
          return (
            <div className="hd-row" key={admission.id}>
              <div className="hd-row__text">
                <span className="hd-row__title">
                  <PersonCell name={patient.name} avatar={patient.avatar} onOpen={() => onNavigate("/patients/" + patient.id)}
                    meta={wardName(admission.ward) + " " + admission.bed} />
                </span>
                <span className="hd-row__note">
                  Fit since {formatWhen(admission.fitAt, now)} · <span className={held ? "hd-warn" : ""}>{formatCountdown(admission.fitAt, now)}</span> · day {ADMISSION_FLAGS.lengthOfStayDays(admission, now)} of an expected {admission.expectedStayDays}
                </span>
              </div>
              <Transitions transitions={admissionTransitions(admission, viewer).filter((t) => t.primary)} onRun={(t) => onAdmissionTransition(admission, t)} />
            </div>
          );
        })}
      </div>
    );
  }

  /* The theatre list is a timed ladder rather than a table: the question it
     answers is "what happens next, and can it start". */
  function TheatreLadder({ rows, data, now, viewer, onNavigate, onInterventionTransition }) {
    if (!rows.length) return <p className="hd-footnote">Nothing is booked into a theatre today.</p>;
    return (
      <div className="hd-rows">
        {rows.map((intervention) => {
          const patient = patientById(data.patients, intervention.patientId);
          const noConsent = INTERVENTION_FLAGS.consentMissing(intervention);
          return (
            <div className="hd-row" key={intervention.id}>
              <span className="hd-row__time">{new Date(intervention.scheduledAt).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })}</span>
              <div className="hd-row__text">
                <span className="hd-row__title">
                  <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/interventions/" + intervention.id)}>{intervention.name}</button>
                </span>
                <span className="hd-row__note">
                  {patient.name} · {intervention.theatre ? labelOf(THEATRES, intervention.theatre) : "No theatre yet"} · {staffName(data.staff, intervention.surgeonId)} · {intervention.plannedMin} min planned
                </span>
              </div>
              <StatusBadge values={INTERVENTION_STATUS} value={intervention.status} />
              {noConsent ? <Badge size="sm" variant="warning">Consent not recorded</Badge> : null}
              <Transitions transitions={interventionTransitions(intervention, viewer).filter((t) => t.primary)} onRun={(t) => onInterventionTransition(intervention, t)} />
            </div>
          );
        })}
      </div>
    );
  }

  function TodayScreen({ data, now, viewer, onNavigate, onAdmissionTransition, onInterventionTransition, onReactToNote, onExport }) {
    const waiting = bedsWaiting(data.admissions);
    const breached = bedWaitsBreached(data.admissions, now);
    const occupied = onWard(data.admissions).concat(fitForDischarge(data.admissions));
    const held = fitForDischarge(data.admissions);
    const theatre = interventionsToday(data.interventions, now);
    const theatreWithoutConsent = theatre.filter((i) => INTERVENTION_FLAGS.consentMissing(i));
    const awaitingPharmacy = ordersAwaitingPharmacy(data.orders);
    const onCharts = awaitingPharmacy.concat(activeOrders(data.orders));
    const owed = mentionsOwed(data.comments, viewer.id);

    return (
      <>
        <PageHeader
          path="/"
          title="Today"
          sub={new Date(now).toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long", year: "numeric" }) + " · " + HD_HOSPITAL.short + " · you are covering " + wardName(viewer.ward) + " ward"}
          actions={<ExportMenu scope={"Open admissions across all " + WARDS.length + " wards"} onExport={(format) => onExport("Ward board", "Open admissions across all " + WARDS.length + " wards", openAdmissions(data.admissions).length, format)} />}
        />
        <Figures items={[
          { label: "Waiting for a bed", value: waiting.length, qualifier: breached.length + " of them past four hours" },
          { label: "Beds occupied", value: occupied.length, qualifier: held.length + (held.length === 1 ? " held for somebody fit to go home" : " held for people fit to go home") },
          { label: "In theatre today", value: theatre.length, qualifier: theatreWithoutConsent.length + " without consent recorded" },
          { label: "Orders awaiting a pharmacist", value: awaitingPharmacy.length, qualifier: "of " + onCharts.length + " orders live on charts now" },
        ]} />

        <Band title="Ward board" note="Occupied beds per ward against the establishment in each ward's record. One pip per bed; amber where a bed is held for somebody fit to go home."
          actions={<Button variant="outline" size="sm" onClick={() => onNavigate("/wards")}>Wards and beds</Button>}>
          <WardBoard data={data} onNavigate={onNavigate} />
        </Band>

        <div className="hd-cols">
          <Band fill title={"Waiting for a bed (" + waiting.length + ")"} note="Every admission whose status is awaiting a bed, oldest request first.">
            <WaitingList rows={waiting.slice().sort((a, b) => a.requestedAt - b.requestedAt)} data={data} now={now} viewer={viewer} onNavigate={onNavigate} onAdmissionTransition={onAdmissionTransition} />
          </Band>
          <Band fill title={"Fit for discharge, still here (" + held.length + ")"} note="Marked fit by a consultant and not yet gone home.">
            <HeldList rows={held.slice().sort((a, b) => a.fitAt - b.fitAt)} data={data} now={now} viewer={viewer} onNavigate={onNavigate} onAdmissionTransition={onAdmissionTransition} />
          </Band>
        </div>

        <Band title={"Theatre list, today (" + theatre.length + ")"} note="Scheduled for today and not cancelled, in time order."
          actions={<Button variant="outline" size="sm" onClick={() => onNavigate("/interventions")}>All interventions</Button>}>
          <TheatreLadder rows={theatre} data={data} now={now} viewer={viewer} onNavigate={onNavigate} onInterventionTransition={onInterventionTransition} />
        </Band>

        <Band title={"Notes waiting on you (" + owed.length + ")"} note="Notes that mention you and that you have not written back on."
          actions={<Button variant="outline" size="sm" onClick={() => onNavigate("/feed")}>Open the feed</Button>}>
          {owed.length ? (
            <div className="hd-notes">
              {owed.map((comment) => (
                <div className="hd-noteitem" key={comment.id}>
                  <Note comment={comment} staff={data.staff} viewer={viewer} now={now} onReact={onReactToNote}
                    context={recordTitle(data, comment.record.type, comment.record.id)} />
                  <Button variant="outline" size="xs" onClick={() => onNavigate(recordRoute(data, comment.record))}>Open the record</Button>
                </div>
              ))}
            </div>
          ) : <p className="hd-footnote">Nothing mentions you without an answer. Every note that named you has had a reply from you since.</p>}
        </Band>
      </>
    );
  }

  Object.assign(window, { TodayScreen, TheatreLadder, WardBoard });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="PatientsScreens.jsx">
(function(){
/* Patients — the directory, one person's record, and registering a new one.
   Commit model: the list and the record are INSTANT (every control applies on
   its own, consequential ones raise an undo toast). /patients/new is its own
   registered route and is STAGED: nothing is written until Save. */
(() => {
  const {
    Button, Badge, Avatar, Checkbox, Select, Input, Textarea, Field, FieldLabel, FieldDescription, FieldError,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  } = window.OriginDesignSystem_7c998b;

  const STATUS_FILTER = [{ value: "all", label: "All care states" }].concat(PATIENT_STATUS.map((s) => ({ value: s.id, label: s.label })));
  const WARD_FILTER = [{ value: "all", label: "All wards" }].concat(WARDS.map((w) => ({ value: w.value, label: wardName(w.value) })));

  function PatientsScreen({ data, now, viewer, onNavigate, onNewPatient, onImport, onExport }) {
    const [query, setQuery] = React.useState("");
    const [status, setStatus] = React.useState("all");
    const [ward, setWard] = React.useState("all");
    const [selected, setSelected] = React.useState([]);
    const { sort, onSort } = useSort("name", "asc");

    const admissionOf = (patient) => admissionOfPatient(data.admissions, patient.id);
    const dirty = !!query || status !== "all" || ward !== "all";
    const clear = () => { setQuery(""); setStatus("all"); setWard("all"); };

    const filtered = data.patients.filter((patient) => {
      const admission = admissionOf(patient);
      if (status !== "all" && patient.status !== status) return false;
      if (ward !== "all" && (!admission || admission.ward !== ward)) return false;
      if (query) {
        const q = query.toLowerCase();
        if ((patient.name + " " + patient.number + " " + patient.gp).toLowerCase().indexOf(q) < 0) return false;
      }
      return true;
    });
    const rows = sortRows(filtered, sort, {
      name: (p) => p.name,
      born: (p) => Date.parse(p.born),
      review: (p) => p.lastReviewAt,
      orders: (p) => activeOrders(ordersForPatient(data.orders, p.id)).length,
    });
    const chosen = data.patients.filter((p) => selected.indexOf(p.id) > -1);

    return (
      <>
        <PageHeader
          path="/patients"
          title="Patients"
          sub={"Everybody registered at " + HD_HOSPITAL.short + ", whether they are on a ward today or not."}
          actions={
            <>
              <ImportButton viewer={viewer} onImport={onImport} />
              <ExportMenu scope={dirty ? filtered.length + " patients matching the filters" : "All " + data.patients.length + " patients"}
                onExport={(format) => onExport("Patient directory", dirty ? "Filtered patient list" : "All patients", filtered.length, format)} />
              <Button size="sm" onClick={onNewPatient}><Icon name="plus" />New patient</Button>
            </>
          }
        />
        <Band flush>
          <FilterBar query={query} onQuery={setQuery} placeholder="Search name, hospital number or GP…" dirty={dirty} onClear={clear}
            selects={[
              { id: "status", label: "Care state", items: STATUS_FILTER, value: status, onChange: setStatus },
              { id: "ward", label: "Ward", items: WARD_FILTER, value: ward, onChange: setWard },
            ]} />
          <BulkBar count={chosen.length} note="Export writes one row per patient with their current ward and care state." onClear={() => setSelected([])}>
            <ExportMenu scope={chosen.length + " selected patients"} onExport={(format) => { onExport("Patient selection", chosen.map((p) => p.name).join(", "), chosen.length, format); setSelected([]); }} />
          </BulkBar>
          {rows.length === 0 ? <NoResults noun="patients" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead style={{ width: 36 }}>
                        <Checkbox aria-label="Select every patient in this list"
                          checked={rows.length > 0 && rows.every((p) => selected.indexOf(p.id) > -1)}
                          indeterminate={selected.length > 0 && !rows.every((p) => selected.indexOf(p.id) > -1)}
                          onCheckedChange={(on) => setSelected(on ? rows.map((p) => p.id) : [])} />
                      </TableHead>
                      <TableHead><SortHead label="Patient" sortKey="name" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead><SortHead label="Born" sortKey="born" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Care state</TableHead>
                      <TableHead>Where</TableHead>
                      <TableHead>Alerts</TableHead>
                      <TableHead className="hd-num"><SortHead label="Active orders" sortKey="orders" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead><SortHead label="Last reviewed" sortKey="review" sort={sort} onSort={onSort} /></TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((patient) => {
                      const admission = admissionOf(patient);
                      const orders = activeOrders(ordersForPatient(data.orders, patient.id));
                      return (
                        <TableRow key={patient.id}>
                          <TableCell>
                            <Checkbox aria-label={"Select " + patient.name} checked={selected.indexOf(patient.id) > -1}
                              onCheckedChange={(on) => setSelected((list) => (on ? list.concat([patient.id]) : list.filter((id) => id !== patient.id)))} />
                          </TableCell>
                          <TableCell>
                            <PersonCell name={patient.name} avatar={patient.avatar} meta={patient.number + " · " + patient.sex}
                              onOpen={() => onNavigate("/patients/" + patient.id)} />
                          </TableCell>
                          <TableCell>{patient.born}</TableCell>
                          <TableCell><StatusBadge values={PATIENT_STATUS} value={patient.status} /></TableCell>
                          <TableCell>
                            {admission && (admission.status === "on_ward" || admission.status === "fit_for_discharge")
                              ? wardName(admission.ward) + " " + admission.bed
                              : admission && admission.status === "awaiting_bed"
                                ? <span className="hd-warn">Waiting for a bed on {wardName(admission.ward)}</span>
                                : <span className="hd-muted">Not on a ward</span>}
                          </TableCell>
                          <TableCell>{PATIENT_FLAGS.hasAllergyAlert(patient) ? <AllergyChips patient={patient} /> : <span className="hd-muted">None recorded</span>}</TableCell>
                          <TableCell className="hd-num">{orders.length}</TableCell>
                          <TableCell><Clock at={patient.lastReviewAt} now={now} problem={PATIENT_FLAGS.reviewOverdue(patient, now)} /></TableCell>
                        </TableRow>
                      );
                    })}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={data.patients.length} noun="patients" />
            </>
          )}
        </Band>
      </>
    );
  }

  function PatientScreen({ data, now, viewer, patientId, onNavigate, onAdmissionTransition, onMedicationTransition, onPostNote, onReactToNote, onExport }) {
    const patient = patientById(data.patients, patientId);
    const admission = admissionOfPatient(data.admissions, patient.id);
    const orders = ordersForPatient(data.orders, patient.id);
    const live = activeOrders(orders).concat(ordersAwaitingPharmacy(orders)).concat(ordersOnHold(orders));
    const procedures = interventionsForPatient(data.interventions, patient.id);
    const booked = procedures.filter((i) => i.status === "requested" || i.status === "scheduled");
    const notes = commentsFor(data.comments, "patient", patient.id);
    const patientInvoices = invoicesForPatient(data.invoices, patient.id);
    const onWardNow = admission && (admission.status === "on_ward" || admission.status === "fit_for_discharge");

    return (
      <>
        <PageHeader path="/patients/:id" params={{ id: patient.id }} crumb={patient.name} title={patient.name}
          actions={<ExportMenu scope={"This record: admission, chart and " + procedures.length + " procedures"} onExport={(format) => onExport("Patient record", patient.name + " · " + patient.number, 1, format)} />} />

        <div className="hd-record">
          <div className="hd-record__id">
            <Avatar size={56} src={patient.avatar} alt={"Photograph of " + patient.name} fallback={initialsOf(patient.name)} />
            <div className="hd-record__text">
              <span className="hd-record__name">{patient.name}</span>
              <span className="hd-record__meta">
                <span>{patient.number}</span>
                <span>Born {patient.born}</span>
                <span>{patient.sex}</span>
                <StatusBadge values={PATIENT_STATUS} value={patient.status} />
                <AllergyChips patient={patient} />
              </span>
            </div>
          </div>
          {admission ? (
            <div className="hd-record__actions">
              <Transitions transitions={admissionTransitions(admission, viewer)} onRun={(t) => onAdmissionTransition(admission, t)} />
            </div>
          ) : null}
        </div>

        <Figures three items={[
          { label: onWardNow ? "Day of stay" : "Days since last review", value: onWardNow ? ADMISSION_FLAGS.lengthOfStayDays(admission, now) : Math.round((now - patient.lastReviewAt) / DAY_MS), qualifier: onWardNow ? "of an expected " + admission.expectedStayDays + " on " + wardName(admission.ward) : "reviewed " + formatWhen(patient.lastReviewAt, now) },
          { label: "Orders on the chart", value: live.length, qualifier: ordersAwaitingPharmacy(orders).length + " awaiting a pharmacist, " + ordersOnHold(orders).length + " on hold" },
          { label: "Procedures booked", value: booked.length, qualifier: booked.filter((i) => INTERVENTION_FLAGS.consentMissing(i)).length + " without consent recorded" },
        ]} />

        <Band title="Admission" note={admission ? labelOf(ADMISSION_SOURCES, admission.source) + " · " + admission.reason : null}>
          {admission ? (
            <Pairs items={[
              { label: "Status", value: <StatusBadge values={ADMISSION_STATUS} value={admission.status} /> },
              { label: "Ward and bed", value: admission.bed ? wardName(admission.ward) + " " + admission.bed : wardName(admission.ward) + " — no bed yet" },
              { label: "Consultant", value: staffName(data.staff, admission.consultantId) },
              { label: "Bed requested", value: <Clock at={admission.requestedAt} now={now} problem={ADMISSION_FLAGS.bedWaitBreached(admission, now)} /> },
              { label: "Admitted", value: admission.admittedAt ? formatWhen(admission.admittedAt, now) : "Not yet on a ward" },
              { label: "Expected stay", value: admission.expectedStayDays + " days" + (ADMISSION_FLAGS.overExpectedStay(admission, now) ? " — over by " + (ADMISSION_FLAGS.lengthOfStayDays(admission, now) - admission.expectedStayDays) : "") },
              { label: "Fit for discharge", value: admission.fitAt ? formatWhen(admission.fitAt, now) : "Not yet" },
              { label: "Left the ward", value: admission.dischargedAt ? formatWhen(admission.dischargedAt, now) : "Still here" },
            ]} />
          ) : <p className="hd-footnote">No ward episode on this record — {patient.name} is seen as {statusValue(PATIENT_STATUS, patient.status).label.toLowerCase()}.</p>}
        </Band>

        <Band flush title={"Medication chart (" + live.length + ")"} note="Orders that can still be given: active, awaiting verification or on hold.">
          {live.length ? (
            <TableWrap>
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead>Drug</TableHead>
                    <TableHead>Dose and route</TableHead>
                    <TableHead>Status</TableHead>
                    <TableHead>Next dose</TableHead>
                    <TableHead>Prescriber</TableHead>
                    <TableHead>Action</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {live.map((order) => (
                    <TableRow key={order.id}>
                      <TableCell>
                        <span className="hd-person__text">
                          <span className="hd-person__name">{order.drug}</span>
                          <span className="hd-person__meta">{order.indication}</span>
                        </span>
                      </TableCell>
                      <TableCell>{order.dose} · {labelOf(DRUG_ROUTES, order.route)} · {order.frequency.toLowerCase()}</TableCell>
                      <TableCell><StatusBadge values={MEDICATION_STATUS} value={order.status} /></TableCell>
                      <TableCell><Clock at={order.nextDoseAt} now={now} problem dangerous={MEDICATION_FLAGS.doseMissed(order, now)} /></TableCell>
                      <TableCell>{staffName(data.staff, order.prescriberId)}</TableCell>
                      <TableCell><RowActions transitions={medicationTransitions(order, viewer)} onRun={(t) => onMedicationTransition(order, t)} label={"Actions for " + order.drug} /></TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </TableWrap>
          ) : <p className="hd-footnote" style={{ paddingInline: "var(--gutter-page)" }}>Nothing on the chart. Every course this patient has been given has finished or been stopped.</p>}
        </Band>

        <Band title={"Procedures (" + procedures.length + ")"} note="Everything requested for this patient, most recent first.">
          {procedures.length ? (
            <div className="hd-rows">
              {procedures.map((intervention) => (
                <div className="hd-row" key={intervention.id}>
                  <div className="hd-row__text">
                    <span className="hd-row__title">
                      <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/interventions/" + intervention.id)}>{intervention.name}</button>
                    </span>
                    <span className="hd-row__note">
                      {intervention.scheduledAt ? formatWhen(intervention.scheduledAt, now) + " · " + labelOf(THEATRES, intervention.theatre) : "Not scheduled — requested " + formatWhen(intervention.requestedAt, now)} · {staffName(data.staff, intervention.surgeonId)}
                    </span>
                  </div>
                  <StatusBadge values={INTERVENTION_STATUS} value={intervention.status} />
                  {INTERVENTION_FLAGS.consentMissing(intervention) ? <Badge size="sm" variant="warning">Consent not recorded</Badge> : null}
                </div>
              ))}
            </div>
          ) : <p className="hd-footnote">Nothing has been requested for this patient.</p>}
        </Band>

        <Band title={"Billing (" + patientInvoices.length + ")"} note="Invoices raised for this patient's care. A total is the sum of the invoice's lines; a balance is what its cleared payments have not covered.">
          {patientInvoices.length ? (
            <div className="hd-rows">
              {patientInvoices.map((invoice) => {
                const balance = invoiceBalance(invoice, data.payments);
                return (
                  <div className="hd-row" key={invoice.id}>
                    <div className="hd-row__text">
                      <span className="hd-row__title">
                        <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/invoices/" + invoice.id)}>{invoice.reference}</button>
                      </span>
                      <span className="hd-row__note">
                        {labelOf(PAYERS, invoice.payer)} · {formatPounds(invoiceTotal(invoice))} over {invoice.lines.length} {invoice.lines.length === 1 ? "line" : "lines"}
                        {balance ? " · " + formatPounds(balance) + " outstanding" : " · settled"}
                        {invoice.dueAt ? " · due " + formatWhen(invoice.dueAt, now) : " · not issued"}
                      </span>
                    </div>
                    <StatusBadge values={INVOICE_STATUS} value={invoice.status} />
                    {INVOICE_FLAGS.overdue(invoice, balance, now) ? <Badge size="sm" variant="warning">{formatCountdown(invoice.dueAt, now)}</Badge> : null}
                  </div>
                );
              })}
            </div>
          ) : <p className="hd-footnote">Nothing has been billed for this patient.</p>}
        </Band>

        <Band title={"Notes (" + notes.length + ")"} note="Written on this record by anybody with access to it. Posting is immediate.">
          <NoteThread comments={notes} staff={data.staff} viewer={viewer} now={now}
            onReact={onReactToNote} onPost={(body) => onPostNote({ type: "patient", id: patient.id }, body)}
            placeholder={"Something the next shift needs to know about " + patient.name.split(" ")[0] + "…"} />
        </Band>
      </>
    );
  }

  /* Registering somebody is a real form, so it is a route and it is staged
     (§5). Nothing is written into the patient index until Save. */
  function NewPatientScreen({ data, viewer, onCreate, onNavigate }) {
    const empty = { name: "", born: "", sex: "Female", status: "outpatient", gp: "", phone: "", nextOfKin: "", allergies: "" };
    const [values, setValues] = React.useState(empty);
    const [submitted, setSubmitted] = React.useState(false);
    const set = (id, value) => setValues((v) => Object.assign({}, v, { [id]: value }));
    const dirty = Object.keys(empty).some((key) => values[key] !== empty[key]);

    const errors = {};
    if (!values.name.trim() || values.name.trim().split(/\s+/).length < 2) errors.name = "Enter a full name — family name and given name.";
    else if (data.patients.some((p) => p.name.toLowerCase() === values.name.trim().toLowerCase())) errors.name = "Somebody with that name is already registered. Check the directory before you add a second record.";
    if (!/^\d{1,2} [A-Za-z]{3} \d{4}$/.test(values.born.trim())) errors.born = "Use the form 14 Feb 1958.";
    if (!values.gp.trim()) errors.gp = "Name the practice, or write “Out of area”.";
    if (!/^[\d ]{9,}$/.test(values.phone.trim())) errors.phone = "Enter a contact number.";
    const valid = Object.keys(errors).length === 0;

    return (
      <>
        <PageHeader path="/patients/new" title="New patient" sub="Registering somebody adds them to the patient index and gives them a hospital number. It does not admit them to a ward." />
        <Band>
          <div className="hd-fields">
            <Field>
              <FieldLabel htmlFor="hd-p-name">Full name</FieldLabel>
              <Input id="hd-p-name" value={values.name} placeholder="Ruth Adeyemi" invalid={submitted && !!errors.name} onChange={(e) => set("name", e.target.value)} />
              {submitted && errors.name ? <FieldError>{errors.name}</FieldError> : <FieldDescription>As it appears on their identity document.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-p-born">Date of birth</FieldLabel>
              <Input id="hd-p-born" value={values.born} placeholder="14 Feb 1958" invalid={submitted && !!errors.born} onChange={(e) => set("born", e.target.value)} />
              {submitted && errors.born ? <FieldError>{errors.born}</FieldError> : <FieldDescription>Day, three-letter month, year.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel>Sex recorded at registration</FieldLabel>
              <Select aria-label="Sex recorded at registration" value={values.sex} onValueChange={(v) => set("sex", v)}
                items={[{ value: "Female", label: "Female" }, { value: "Male", label: "Male" }, { value: "Not stated", label: "Not stated" }]} />
            </Field>
            <Field>
              <FieldLabel>Care state</FieldLabel>
              <Select aria-label="Care state" value={values.status} onValueChange={(v) => set("status", v)}
                items={PATIENT_STATUS.filter((s) => s.id === "outpatient" || s.id === "day_case").map((s) => ({ value: s.id, label: s.label }))} />
              <FieldDescription>A new record starts outside the wards; admitting them is a separate step on Admissions.</FieldDescription>
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-p-gp">GP practice</FieldLabel>
              <Input id="hd-p-gp" value={values.gp} placeholder="Fern Lane Surgery" invalid={submitted && !!errors.gp} onChange={(e) => set("gp", e.target.value)} />
              {submitted && errors.gp ? <FieldError>{errors.gp}</FieldError> : null}
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-p-phone">Contact number</FieldLabel>
              <Input id="hd-p-phone" value={values.phone} placeholder="07700 900 118" invalid={submitted && !!errors.phone} onChange={(e) => set("phone", e.target.value)} />
              {submitted && errors.phone ? <FieldError>{errors.phone}</FieldError> : null}
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-p-kin">Next of kin</FieldLabel>
              <Input id="hd-p-kin" value={values.nextOfKin} placeholder="Deborah Adeyemi (daughter)" onChange={(e) => set("nextOfKin", e.target.value)} />
              <FieldDescription>Leave it blank and the record reads “Not recorded”.</FieldDescription>
            </Field>
            <Field className="hd-fields__wide">
              <FieldLabel htmlFor="hd-p-allergies">Known allergies</FieldLabel>
              <Textarea id="hd-p-allergies" rows={2} value={values.allergies} placeholder="Penicillin, latex" onChange={(e) => set("allergies", e.target.value)} />
              <FieldDescription>Separate them with commas. Each one becomes an alert on every clinical screen that shows this patient.</FieldDescription>
            </Field>
          </div>
        </Band>
        <FormBar dirty={dirty} label="Save patient"
          note={"A hospital number is issued on save, in the " + HD_HOSPITAL.short + " series."}
          onCancel={() => { setValues(empty); setSubmitted(false); onNavigate("/patients"); }}
          onSave={() => { setSubmitted(true); if (valid) onCreate(values); }} />
      </>
    );
  }

  Object.assign(window, { PatientsScreen, PatientScreen, NewPatientScreen });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="CareScreens.jsx">
(function(){
/* Admissions and interventions — the ward board and the theatre list.
   Commit model: INSTANT on both screens. Consequential moves apply immediately
   and raise an undo toast; the two irreversible ones (discharging somebody,
   recording a procedure as abandoned) open a confirm dialog from App.jsx. The
   two creation actions are staged dialogs opened from these instant screens. */
(() => {
  const {
    Button, Badge, Checkbox, Alert, AlertTitle, AlertDescription, Avatar,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  } = window.OriginDesignSystem_7c998b;

  const ADM_STATUS_FILTER = [{ value: "all", label: "All statuses" }].concat(ADMISSION_STATUS.map((s) => ({ value: s.id, label: s.label })));
  const ADM_WARD_FILTER = [{ value: "all", label: "All wards" }].concat(WARDS.map((w) => ({ value: w.value, label: wardName(w.value) })));
  const canMarkFit = (admission, viewer) => admissionTransitions(admission, viewer).some((t) => t.id === "mark_fit" && !t.disabled);

  function AdmissionsScreen({ data, now, viewer, onNavigate, onAdmissionTransition, onBulkMarkFit, onNewAdmission, onImport, onExport }) {
    const [query, setQuery] = React.useState("");
    const [status, setStatus] = React.useState("all");
    const [ward, setWard] = React.useState("all");
    const [selected, setSelected] = React.useState([]);
    const { sort, onSort } = useSort("clock", "asc");

    const dirty = !!query || status !== "all" || ward !== "all";
    const clear = () => { setQuery(""); setStatus("all"); setWard("all"); };
    const patientOf = (admission) => patientById(data.patients, admission.patientId);

    const filtered = data.admissions.filter((admission) => {
      if (status !== "all" && admission.status !== status) return false;
      if (ward !== "all" && admission.ward !== ward) return false;
      if (query) {
        const q = query.toLowerCase();
        const patient = patientOf(admission);
        if ((patient.name + " " + patient.number + " " + admission.reason + " " + (admission.bed || "")).toLowerCase().indexOf(q) < 0) return false;
      }
      return true;
    });
    const rows = sortRows(filtered, sort, {
      clock: (a) => a.admittedAt || a.requestedAt,
      patient: (a) => patientOf(a).name,
      stay: (a) => ADMISSION_FLAGS.lengthOfStayDays(a, now),
    });
    const chosen = data.admissions.filter((a) => selected.indexOf(a.id) > -1);
    const fitable = chosen.filter((a) => canMarkFit(a, viewer));

    return (
      <>
        <PageHeader
          path="/admissions"
          title="Admissions"
          sub={bedsWaiting(data.admissions).length + " waiting for a bed · " + onWard(data.admissions).length + " on the wards · " + fitForDischarge(data.admissions).length + " fit for discharge and still here"}
          actions={
            <>
              <ImportButton viewer={viewer} onImport={onImport} />
              <ExportMenu scope={dirty ? filtered.length + " admissions matching the filters" : "All " + data.admissions.length + " admissions"}
                onExport={(format) => onExport("Admissions", dirty ? "Filtered admissions" : "All admissions", filtered.length, format)} />
              <Button size="sm" onClick={onNewAdmission}><Icon name="plus" />New admission</Button>
            </>
          }
        />
        <Band flush>
          <FilterBar query={query} onQuery={setQuery} placeholder="Search patient, bed or reason…" dirty={dirty} onClear={clear}
            selects={[
              { id: "status", label: "Status", items: ADM_STATUS_FILTER, value: status, onChange: setStatus },
              { id: "ward", label: "Ward", items: ADM_WARD_FILTER, value: ward, onChange: setWard },
            ]} />
          <BulkBar count={chosen.length}
            note={fitable.length ? fitable.length + " of them can be marked fit for discharge" : "None of the selected admissions can be marked fit — that only applies to somebody already on a ward"}
            onClear={() => setSelected([])}>
            {fitable.length ? <Button size="sm" onClick={() => { onBulkMarkFit(fitable); setSelected([]); }}>Mark {fitable.length} fit for discharge</Button> : null}
            <ExportMenu scope={chosen.length + " selected admissions"} onExport={(format) => { onExport("Admission selection", chosen.map((a) => patientOf(a).name).join(", "), chosen.length, format); setSelected([]); }} />
          </BulkBar>
          {rows.length === 0 ? <NoResults noun="admissions" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead style={{ width: 36 }}>
                        <Checkbox aria-label="Select every admission in this list"
                          checked={rows.length > 0 && rows.every((a) => selected.indexOf(a.id) > -1)}
                          indeterminate={selected.length > 0 && !rows.every((a) => selected.indexOf(a.id) > -1)}
                          onCheckedChange={(on) => setSelected(on ? rows.map((a) => a.id) : [])} />
                      </TableHead>
                      <TableHead><SortHead label="Patient" sortKey="patient" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Status</TableHead>
                      <TableHead>Ward and bed</TableHead>
                      <TableHead>From</TableHead>
                      <TableHead><SortHead label="On the clock since" sortKey="clock" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead className="hd-num"><SortHead label="Days" sortKey="stay" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Consultant</TableHead>
                      <TableHead>Action</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((admission) => {
                      const patient = patientOf(admission);
                      const waiting = admission.status === "awaiting_bed";
                      const over = ADMISSION_FLAGS.overExpectedStay(admission, now);
                      return (
                        <TableRow key={admission.id}>
                          <TableCell>
                            <Checkbox aria-label={"Select the admission for " + patient.name} checked={selected.indexOf(admission.id) > -1}
                              onCheckedChange={(on) => setSelected((list) => (on ? list.concat([admission.id]) : list.filter((id) => id !== admission.id)))} />
                          </TableCell>
                          <TableCell>
                            <PersonCell name={patient.name} avatar={patient.avatar} meta={patient.number + " · " + admission.reason}
                              onOpen={() => onNavigate("/patients/" + patient.id)} />
                          </TableCell>
                          <TableCell><StatusBadge values={ADMISSION_STATUS} value={admission.status} /></TableCell>
                          <TableCell>{admission.bed ? wardName(admission.ward) + " " + admission.bed : <span className="hd-muted">{wardName(admission.ward)} — no bed</span>}</TableCell>
                          <TableCell>{labelOf(ADMISSION_SOURCES, admission.source)}</TableCell>
                          <TableCell>
                            <Clock at={admission.admittedAt || admission.requestedAt} now={now} problem={waiting && ADMISSION_FLAGS.bedWaitBreached(admission, now)} />
                            {waiting ? <span className={ADMISSION_FLAGS.bedWaitBreached(admission, now) ? "hd-warn" : "hd-muted"}> · {formatCountdown(admission.requestedAt, now)}</span> : null}
                          </TableCell>
                          <TableCell className="hd-num">
                            <span className={over ? "hd-warn" : ""}>{admission.admittedAt ? ADMISSION_FLAGS.lengthOfStayDays(admission, now) : "—"}</span>
                            {admission.admittedAt ? <span className="hd-muted"> / {admission.expectedStayDays}</span> : null}
                          </TableCell>
                          <TableCell>{staffName(data.staff, admission.consultantId)}</TableCell>
                          <TableCell><RowActions transitions={admissionTransitions(admission, viewer)} onRun={(t) => onAdmissionTransition(admission, t)} label={"Actions for " + patient.name + "'s admission"} /></TableCell>
                        </TableRow>
                      );
                    })}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={data.admissions.length} noun="admissions" />
            </>
          )}
        </Band>
      </>
    );
  }

  const INT_STATUS_FILTER = [{ value: "all", label: "All statuses" }].concat(INTERVENTION_STATUS.map((s) => ({ value: s.id, label: s.label })));
  const INT_SPECIALTY_FILTER = [{ value: "all", label: "All specialties" }].concat(SPECIALTIES);

  function InterventionsScreen({ data, now, viewer, onNavigate, onInterventionTransition, onNewIntervention, onExport }) {
    const [query, setQuery] = React.useState("");
    const [status, setStatus] = React.useState("all");
    const [specialty, setSpecialty] = React.useState("all");
    const { sort, onSort } = useSort("scheduled", "asc");

    const dirty = !!query || status !== "all" || specialty !== "all";
    const clear = () => { setQuery(""); setStatus("all"); setSpecialty("all"); };
    const today = interventionsToday(data.interventions, now);
    const unscheduled = unscheduledInterventions(data.interventions);

    const filtered = data.interventions.filter((intervention) => {
      if (status !== "all" && intervention.status !== status) return false;
      if (specialty !== "all" && intervention.specialty !== specialty) return false;
      if (query) {
        const q = query.toLowerCase();
        const patient = patientById(data.patients, intervention.patientId);
        if ((intervention.name + " " + patient.name + " " + patient.number).toLowerCase().indexOf(q) < 0) return false;
      }
      return true;
    });
    const rows = sortRows(filtered, sort, {
      scheduled: (i) => i.scheduledAt || i.requestedAt,
      patient: (i) => patientById(data.patients, i.patientId).name,
      length: (i) => i.plannedMin,
    });

    return (
      <>
        <PageHeader
          path="/interventions"
          title="Interventions"
          sub={today.length + " in a theatre today · " + consentMissing(data.interventions).length + " without consent recorded · " + unscheduled.length + " requested and not yet scheduled"}
          actions={
            <>
              <ExportMenu scope={dirty ? filtered.length + " interventions matching the filters" : "All " + data.interventions.length + " interventions"}
                onExport={(format) => onExport("Interventions", dirty ? "Filtered interventions" : "All interventions", filtered.length, format)} />
              <Button size="sm" onClick={onNewIntervention}><Icon name="plus" />New intervention</Button>
            </>
          }
        />

        <Band title={"Theatre list, today (" + today.length + ")"} note="Scheduled for today and not cancelled, in time order. A procedure cannot start until consent is on the record.">
          <TheatreLadder rows={today} data={data} now={now} viewer={viewer} onNavigate={onNavigate} onInterventionTransition={onInterventionTransition} />
        </Band>

        <Band flush title={"Everything requested (" + data.interventions.length + ")"}>
          <FilterBar query={query} onQuery={setQuery} placeholder="Search procedure or patient…" dirty={dirty} onClear={clear}
            selects={[
              { id: "status", label: "Status", items: INT_STATUS_FILTER, value: status, onChange: setStatus },
              { id: "specialty", label: "Specialty", items: INT_SPECIALTY_FILTER, value: specialty, onChange: setSpecialty },
            ]} />
          {rows.length === 0 ? <NoResults noun="interventions" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead>Procedure</TableHead>
                      <TableHead><SortHead label="Patient" sortKey="patient" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Status</TableHead>
                      <TableHead>Priority</TableHead>
                      <TableHead>Theatre</TableHead>
                      <TableHead><SortHead label="Scheduled" sortKey="scheduled" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Consent</TableHead>
                      <TableHead>Surgeon</TableHead>
                      <TableHead>Action</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((intervention) => {
                      const patient = patientById(data.patients, intervention.patientId);
                      return (
                        <TableRow key={intervention.id}>
                          <TableCell>
                            <span className="hd-person__text">
                              <span className="hd-person__name"><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/interventions/" + intervention.id)}>{intervention.name}</button></span>
                              <span className="hd-person__meta">{labelOf(SPECIALTIES, intervention.specialty)} · {intervention.plannedMin} min</span>
                            </span>
                          </TableCell>
                          <TableCell><PersonCell name={patient.name} avatar={patient.avatar} size={24} onOpen={() => onNavigate("/patients/" + patient.id)} /></TableCell>
                          <TableCell><StatusBadge values={INTERVENTION_STATUS} value={intervention.status} /></TableCell>
                          <TableCell>{labelOf(PRIORITIES, intervention.priority)}</TableCell>
                          <TableCell>{intervention.theatre ? labelOf(THEATRES, intervention.theatre) : <span className="hd-muted">Not allocated</span>}</TableCell>
                          <TableCell>{intervention.scheduledAt ? <Clock at={intervention.scheduledAt} now={now} /> : <span className="hd-warn">Not scheduled</span>}</TableCell>
                          <TableCell>{intervention.consentAt ? formatWhen(intervention.consentAt, now) : INTERVENTION_FLAGS.consentMissing(intervention) ? <Badge size="sm" variant="warning">Not recorded</Badge> : <span className="hd-muted">—</span>}</TableCell>
                          <TableCell>{staffName(data.staff, intervention.surgeonId)}</TableCell>
                          <TableCell><RowActions transitions={interventionTransitions(intervention, viewer)} onRun={(t) => onInterventionTransition(intervention, t)} label={"Actions for " + intervention.name} /></TableCell>
                        </TableRow>
                      );
                    })}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={data.interventions.length} noun="interventions" />
            </>
          )}
        </Band>
      </>
    );
  }

  function InterventionScreen({ data, now, viewer, interventionId, onNavigate, onInterventionTransition, onPostNote, onReactToNote, onExport }) {
    const intervention = data.interventions.find((i) => i.id === interventionId);
    const patient = patientById(data.patients, intervention.patientId);
    const notes = commentsFor(data.comments, "intervention", intervention.id);
    const noConsent = INTERVENTION_FLAGS.consentMissing(intervention);

    return (
      <>
        <PageHeader path="/interventions/:id" params={{ id: intervention.id }} crumb={intervention.name} title={intervention.name}
          actions={<ExportMenu scope={"This procedure and its " + notes.length + " notes"} onExport={(format) => onExport("Intervention record", intervention.name + " · " + patient.name, 1, format)} />} />

        <div className="hd-record">
          <div className="hd-record__id">
            <Avatar size={56} src={patient.avatar} alt={"Photograph of " + patient.name} fallback={initialsOf(patient.name)} />
            <div className="hd-record__text">
              <span className="hd-record__name">{patient.name}</span>
              <span className="hd-record__meta">
                <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/patients/" + patient.id)}>{patient.number}</button>
                <span>{labelOf(PRIORITIES, intervention.priority)}</span>
                <StatusBadge values={INTERVENTION_STATUS} value={intervention.status} />
                <AllergyChips patient={patient} />
              </span>
            </div>
          </div>
          <div className="hd-record__actions">
            <Transitions transitions={interventionTransitions(intervention, viewer)} onRun={(t) => onInterventionTransition(intervention, t)} />
          </div>
        </div>

        {noConsent ? (
          <div className="hd-alertwrap">
            <Alert variant="warning">
              <AlertTitle>Consent is not on the record</AlertTitle>
              <AlertDescription>
                {intervention.scheduledAt
                  ? "This is booked for " + formatWhen(intervention.scheduledAt, now) + " and cannot start until the clinician taking consent records it here."
                  : "Nothing can be booked until the clinician taking consent records it here."}
              </AlertDescription>
            </Alert>
          </div>
        ) : null}

        <Band title="The procedure" note={intervention.notes}>
          <Pairs items={[
            { label: "Specialty", value: labelOf(SPECIALTIES, intervention.specialty) },
            { label: "Theatre", value: intervention.theatre ? labelOf(THEATRES, intervention.theatre) : "Not allocated" },
            { label: "Scheduled", value: intervention.scheduledAt ? formatWhen(intervention.scheduledAt, now) + " · " + formatCountdown(intervention.scheduledAt, now) : "Not scheduled" },
            { label: "Planned length", value: intervention.plannedMin + " minutes" },
            { label: "Requested", value: <Clock at={intervention.requestedAt} now={now} /> },
            { label: "Consent recorded", value: intervention.consentAt ? formatWhen(intervention.consentAt, now) : "Not recorded" },
            { label: "Surgeon", value: staffName(data.staff, intervention.surgeonId) },
            { label: "Anaesthetist", value: intervention.anaesthetistId ? staffName(data.staff, intervention.anaesthetistId) : "Not allocated" },
            { label: "Completed", value: intervention.completedAt ? formatWhen(intervention.completedAt, now) : "Not yet" },
          ]} />
        </Band>

        <Band title={"Notes (" + notes.length + ")"} note="Written on this procedure. Posting is immediate.">
          <NoteThread comments={notes} staff={data.staff} viewer={viewer} now={now}
            onReact={onReactToNote} onPost={(body) => onPostNote({ type: "intervention", id: intervention.id }, body)}
            placeholder="Anything theatre or the ward needs to know…" />
        </Band>
      </>
    );
  }

  Object.assign(window, { AdmissionsScreen, InterventionsScreen, InterventionScreen, canMarkFit });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="PharmacyScreens.jsx">
(function(){
/* Pharmacy — the medication orders on charts, and the stock behind them.
   Commit model: INSTANT on both screens. Verifying, holding, resuming and
   recording a dose apply immediately with an undo toast; stopping a course is
   irreversible and opens a confirm dialog from App.jsx. Creating an order or a
   formulary line is a staged dialog.

   The ward control is a SCOPE filter (§5.3): it is above the figures and moves
   them. The text and status filters are inside the list container and narrow the
   list only — the figures do not move with them. */
(() => {
  const {
    Button, Badge, Select, Alert, AlertTitle, AlertDescription,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  } = window.OriginDesignSystem_7c998b;

  const MED_STATUS_FILTER = [{ value: "all", label: "All statuses" }].concat(MEDICATION_STATUS.map((s) => ({ value: s.id, label: s.label })));
  const SCOPE_WARDS = [{ value: "all", label: "Every ward" }].concat(WARDS.map((w) => ({ value: w.value, label: wardName(w.value) })));

  function MedicationScreen({ data, now, viewer, ward, onWardChange, onNavigate, onMedicationTransition, onNewOrder, onExport }) {
    const [query, setQuery] = React.useState("");
    const [status, setStatus] = React.useState("all");
    const { sort, onSort } = useSort("next", "asc");

    const wardOf = (order) => {
      const admission = admissionOfPatient(data.admissions, order.patientId);
      return admission ? admission.ward : null;
    };
    /* Scoped first: every figure below is computed from this array. */
    const scoped = ward === "all" ? data.orders : data.orders.filter((o) => wardOf(o) === ward);
    const awaiting = ordersAwaitingPharmacy(scoped);
    const due = dosesDue(scoped, now);
    const missed = dosesMissed(scoped, now);
    const active = activeOrders(scoped);

    const dirty = !!query || status !== "all";
    const clear = () => { setQuery(""); setStatus("all"); };
    const filtered = scoped.filter((order) => {
      if (status !== "all" && order.status !== status) return false;
      if (query) {
        const q = query.toLowerCase();
        const patient = patientById(data.patients, order.patientId);
        if ((order.drug + " " + order.indication + " " + patient.name + " " + patient.number).toLowerCase().indexOf(q) < 0) return false;
      }
      return true;
    });
    const rows = sortRows(filtered, sort, {
      next: (o) => o.nextDoseAt || Infinity,
      patient: (o) => patientById(data.patients, o.patientId).name,
      drug: (o) => o.drug,
    });

    return (
      <>
        <PageHeader
          path="/medication"
          title="Medication orders"
          sub={"Everything prescribed at " + HD_HOSPITAL.short + ". A pharmacist has to verify an order before the first dose can be given."}
          actions={
            <>
              <ExportMenu scope={ward === "all" ? "All " + data.orders.length + " orders" : scoped.length + " orders on " + wardName(ward)}
                onExport={(format) => onExport("Medication administration record", ward === "all" ? "All wards" : wardName(ward) + " ward", scoped.length, format)} />
              <Button size="sm" onClick={onNewOrder}><Icon name="plus" />New order</Button>
            </>
          }
        />
        <div className="hd-band" style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--space-2) var(--space-4)" }}>
          <span className="hd-fig__label">Ward</span>
          <span className="hd-filters__select"><Select size="sm" aria-label="Scope the figures and the list to one ward" items={SCOPE_WARDS} value={ward} onValueChange={onWardChange} /></span>
          <p className="hd-band__note" style={{ flex: 1, minWidth: "16rem" }}>
            {ward === "all"
              ? "Counting every order in the hospital, on any ward and none."
              : "Counting the " + scoped.length + " orders whose patient is on " + wardName(ward) + " ward."}
          </p>
          {ward !== "all" ? <Button variant="ghost" size="sm" onClick={() => onWardChange("all")}>Reset scope</Button> : null}
        </div>

        <Figures items={[
          { label: "Awaiting a pharmacist", value: awaiting.length, qualifier: awaiting.filter((o) => (drugById(data.formulary, o.drugId) || {}).stock === "out").length + " of them for a drug that is out of stock" },
          { label: "Doses due within the hour", value: due.length, qualifier: due.length ? "next at " + formatWhen(due.slice().sort((a, b) => a.nextDoseAt - b.nextDoseAt)[0].nextDoseAt, now) : "nothing on the clock" },
          { label: "Doses missed", value: missed.length, qualifier: missed.length ? "overdue by more than an hour" : "every due dose has gone in" },
          { label: "Active courses", value: active.length, qualifier: ordersOnHold(scoped).length + " further courses are on hold" },
        ]} />

        {missed.length ? (
          <div className="hd-alertwrap">
            <Alert variant="warning">
              <AlertTitle>{missed.length === 1 ? "One dose is overdue" : missed.length + " doses are overdue"}</AlertTitle>
              <AlertDescription>
                {missed.map((o) => patientName(data.patients, o.patientId) + " — " + o.drug + " " + o.dose + ", due " + formatWhen(o.nextDoseAt, now)).join(" · ")}. Recording the dose clears it.
              </AlertDescription>
            </Alert>
          </div>
        ) : null}

        <Band flush title={"Orders (" + scoped.length + ")"} note={ward === "all" ? "Every order in the hospital." : "Orders for patients on " + wardName(ward) + " ward."}>
          <FilterBar query={query} onQuery={setQuery} placeholder="Search drug, indication or patient…" dirty={dirty} onClear={clear}
            selects={[{ id: "status", label: "Status", items: MED_STATUS_FILTER, value: status, onChange: setStatus }]} />
          {rows.length === 0 ? <NoResults noun="orders" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead><SortHead label="Patient" sortKey="patient" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead><SortHead label="Drug" sortKey="drug" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Dose</TableHead>
                      <TableHead>Status</TableHead>
                      <TableHead><SortHead label="Next dose" sortKey="next" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Verified by</TableHead>
                      <TableHead>Action</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((order) => {
                      const patient = patientById(data.patients, order.patientId);
                      const drug = drugById(data.formulary, order.drugId);
                      return (
                        <TableRow key={order.id}>
                          <TableCell>
                            <PersonCell name={patient.name} avatar={patient.avatar} size={24}
                              meta={(wardOf(order) ? wardName(wardOf(order)) : "Not on a ward") + " · " + patient.number}
                              onOpen={() => onNavigate("/patients/" + patient.id)} />
                          </TableCell>
                          <TableCell>
                            <span className="hd-person__text">
                              <span className="hd-person__name">{order.drug}</span>
                              <span className="hd-person__meta">{order.indication}</span>
                            </span>
                          </TableCell>
                          <TableCell>{order.dose} · {labelOf(DRUG_ROUTES, order.route)} · {order.frequency.toLowerCase()}</TableCell>
                          <TableCell>
                            <span className="hd-actions__row">
                              <StatusBadge values={MEDICATION_STATUS} value={order.status} />
                              {drug && drug.stock === "out" ? <Badge size="sm" variant="destructive">No stock</Badge> : null}
                            </span>
                          </TableCell>
                          <TableCell><Clock at={order.nextDoseAt} now={now} problem dangerous={MEDICATION_FLAGS.doseMissed(order, now)} /></TableCell>
                          <TableCell>{order.verifiedById ? staffName(data.staff, order.verifiedById) : <span className="hd-warn">Nobody yet</span>}</TableCell>
                          <TableCell><RowActions transitions={medicationTransitions(order, viewer)} onRun={(t) => onMedicationTransition(order, t)} label={"Actions for " + order.drug} /></TableCell>
                        </TableRow>
                      );
                    })}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={scoped.length} noun={ward === "all" ? "orders" : "orders on " + wardName(ward)} />
            </>
          )}
        </Band>
      </>
    );
  }

  const STOCK_FILTER = [{ value: "all", label: "All stock states" }].concat(STOCK_STATUS.map((s) => ({ value: s.id, label: s.label })));

  function FormularyScreen({ data, now, viewer, onNavigate, onReorder, onNewLine, onImport, onExport }) {
    const [query, setQuery] = React.useState("");
    const [stock, setStock] = React.useState("all");
    const { sort, onSort } = useSort("name", "asc");

    const low = belowReorder(data.formulary);
    const out = outOfStock(data.formulary);
    const affected = data.orders.filter((o) => {
      const drug = drugById(data.formulary, o.drugId);
      return drug && (drug.stock === "out" || drug.stock === "low") && (o.status === "active" || o.status === "awaiting_verification");
    });

    const dirty = !!query || stock !== "all";
    const clear = () => { setQuery(""); setStock("all"); };
    const filtered = data.formulary.filter((drug) => {
      if (stock !== "all" && drug.stock !== stock) return false;
      if (query && (drug.name + " " + drug.form + " " + drug.supplier).toLowerCase().indexOf(query.toLowerCase()) < 0) return false;
      return true;
    });
    const rows = sortRows(filtered, sort, {
      name: (d) => d.name,
      onHand: (d) => d.onHand,
      expiry: (d) => d.earliestExpiry || Infinity,
    });

    return (
      <>
        <PageHeader
          path="/formulary"
          title="Formulary and stock"
          sub={"What " + HD_HOSPITAL.short + " may prescribe, and how much of it is in the cupboard."}
          actions={
            <>
              <ImportButton viewer={viewer} onImport={onImport} />
              <ExportMenu scope={dirty ? filtered.length + " lines matching the filters" : "All " + data.formulary.length + " formulary lines"}
                onExport={(format) => onExport("Formulary", dirty ? "Filtered formulary" : "Whole formulary", filtered.length, format)} />
              <Button size="sm" onClick={onNewLine}><Icon name="plus" />New formulary line</Button>
            </>
          }
        />
        <Figures three items={[
          { label: "Lines below reorder", value: low.length, qualifier: out.length + " of them at zero" },
          { label: "Courses relying on them", value: affected.length, qualifier: "active or unverified orders for a low or empty line" },
          { label: "Lines in the formulary", value: data.formulary.length, qualifier: data.formulary.filter((d) => d.stock === "discontinued").length + " discontinued and unprescribable" },
        ]} />

        <Band flush>
          <FilterBar query={query} onQuery={setQuery} placeholder="Search drug, form or supplier…" dirty={dirty} onClear={clear}
            selects={[{ id: "stock", label: "Stock", items: STOCK_FILTER, value: stock, onChange: setStock }]} />
          {rows.length === 0 ? <NoResults noun="formulary lines" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead><SortHead label="Drug" sortKey="name" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Route</TableHead>
                      <TableHead>Stock</TableHead>
                      <TableHead style={{ minWidth: "11rem" }}><SortHead label="On hand" sortKey="onHand" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead className="hd-num">Courses</TableHead>
                      <TableHead><SortHead label="Earliest expiry" sortKey="expiry" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Supplier</TableHead>
                      <TableHead>Action</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((drug) => {
                      const courses = ordersForDrug(data.orders, drug.id);
                      const percent = drug.reorderAt ? (drug.onHand / (drug.reorderAt * 3)) * 100 : 0;
                      return (
                        <TableRow key={drug.id}>
                          <TableCell>
                            <span className="hd-person__text">
                              <span className="hd-person__name">{drug.name}{drug.controlled ? " " : ""}{drug.controlled ? <Badge size="sm" variant="outline">Controlled</Badge> : null}</span>
                              <span className="hd-person__meta">{drug.form}</span>
                            </span>
                          </TableCell>
                          <TableCell>{labelOf(DRUG_ROUTES, drug.route)}</TableCell>
                          <TableCell><StatusBadge values={STOCK_STATUS} value={drug.stock} /></TableCell>
                          <TableCell>
                            <span style={{ display: "flex", flexDirection: "column", gap: 4, minWidth: "9rem" }}>
                              <span className="hd-footnote">{formatCount(drug.onHand)} {drug.unit}{drug.reorderAt ? " · reorder at " + formatCount(drug.reorderAt) : ""}</span>
                              <StockBar percent={percent} tone={FORMULARY_FLAGS.belowReorder(drug) ? "warn" : null} />
                            </span>
                          </TableCell>
                          <TableCell className="hd-num">{courses.length}</TableCell>
                          <TableCell>{drug.earliestExpiry ? formatWhen(drug.earliestExpiry, now) : <span className="hd-muted">No batch held</span>}</TableCell>
                          <TableCell>{drug.supplier}</TableCell>
                          <TableCell>
                            {drug.reorderedAt
                              ? <span className="hd-footnote">Ordered {formatWhen(drug.reorderedAt, now)}</span>
                              : FORMULARY_FLAGS.belowReorder(drug)
                                ? <Button size="sm" variant="outline" onClick={() => onReorder(drug)}>Order stock</Button>
                                : <span className="hd-muted">—</span>}
                          </TableCell>
                        </TableRow>
                      );
                    })}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={data.formulary.length} noun="formulary lines" />
            </>
          )}
        </Band>
      </>
    );
  }

  Object.assign(window, { MedicationScreen, FormularyScreen });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="WardScreens.jsx">
(function(){
/* Wards and beds · the rota · the drug round.
   Commit model: INSTANT on all three. Assigning a bed and recording a dose apply
   immediately with an undo toast; nothing here is staged.

   Card variants used, and why: the default card for a titled region, --tint for
   a shift nobody has filled yet and for supporting detail, --fill where cards sit
   side by side and their footers should line up, --flush where a table bleeds to
   the card edge. */
(() => {
  const {
    Button, Badge, Select, Avatar, Alert, AlertTitle, AlertDescription,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  } = window.OriginDesignSystem_7c998b;

  /* One pip per bed: occupancy as a shape, not only a number. Every pip is a bed
     the ward establishment says exists. */
  function BedGrid({ ward }) {
    const pips = [];
    for (let i = 0; i < ward.beds; i++) {
      const state = i < ward.occupied - ward.held ? "occupied" : i < ward.occupied ? "held" : "free";
      pips.push(<span className="hd-bed" key={i} data-state={state} />);
    }
    const label = ward.occupied + " of " + ward.beds + " beds occupied on " + ward.label + ", " + ward.held + " held for somebody fit to go home";
    return (
      <span className="hd-beds" role="img" aria-label={label}>{pips}</span>
    );
  }

  function WardsScreen({ data, now, viewer, onNavigate, onAdmissionTransition, onExport }) {
    const wards = wardRows(data.admissions, data.wardBeds);
    const beds = data.wardBeds.reduce((n, b) => n + b.beds, 0);
    const occupied = wards.reduce((n, w) => n + w.occupied, 0);
    const free = wards.reduce((n, w) => n + w.free, 0);
    const held = wards.reduce((n, w) => n + w.held, 0);
    const waiting = bedsWaiting(data.admissions);
    const [ward, setWard] = React.useState(viewer.ward || WARDS[0].value);
    const chosen = wards.find((w) => w.ward === ward);
    const onShift = rotaFor(data.rota, 0, ward, "day");

    return (
      <>
        <PageHeader
          path="/wards"
          title="Wards and beds"
          sub={"Five wards and the " + beds + " beds Ward Desk covers. Occupancy is counted from the admissions each ward renders, against the bed count in the ward record."}
          actions={<ExportMenu scope={"Bed state across all " + WARDS.length + " wards"} onExport={(format) => onExport("Ward and bed state", "Every ward", wards.length, format)} />}
        />
        <Figures items={[
          { label: "Beds occupied", value: occupied, qualifier: "of " + beds + " · " + Math.round((occupied / beds) * 100) + "% of the hospital" },
          { label: "Beds free", value: free, qualifier: waiting.length + (waiting.length === 1 ? " person is waiting for one" : " people are waiting for one") },
          { label: "Held for a discharge", value: held, qualifier: held ? "occupied by somebody already fit to go home" : "nobody fit to go home is still in a bed" },
          /* The qualifier decomposes this figure's own set and nothing else: each
             request, the ward it was made onto, and how long it has waited. Why a
             wait persists beside a free bed is not in the fixture, so this does not
             claim it. */
          { label: "Waiting for a bed", value: waiting.length, qualifier: waiting.length
            ? waiting.slice().sort((a, b) => a.requestedAt - b.requestedAt)
                .map((a) => wardName(a.ward) + ", " + formatCountdown(a.requestedAt, now).replace(" over", "").replace(" left", ""))
                .join(" · ")
            : "every request has a bed against it" },
        ]} />

        <Band title="The board" note="One card per ward: a pip per bed, dark for occupied, amber where the bed is held for somebody fit to go home.">
          <div className="hd-ward">
            {wards.map((w) => (
              <div className="hd-ward__cell" key={w.ward}>
                <span className="hd-ward__top">
                  <span className="hd-ward__name">{w.label}</span>
                  <span className="hd-ward__count">{w.occupied}<span className="hd-footnote"> / {w.beds}</span></span>
                </span>
                <BedGrid ward={w} />
                <span className="hd-footnote">
                  {w.specialty} · {w.free} free
                  {w.waiting ? " · " + w.waiting + " waiting" : ""}
                  {w.held ? " · " + w.held + " held" : ""}
                </span>
                <Button variant="outline" size="xs" onClick={() => setWard(w.ward)}>Open {w.label}</Button>
              </div>
            ))}
          </div>
        </Band>

        <div className="hd-cols hd-cols--wide">
          <div>
            <Band flush fill title={chosen.label + " — who is in it (" + chosen.rows.length + ")"}
              note={"Occupied beds and outstanding requests on " + chosen.label + ", " + chosen.specialty + "."}
              actions={
                <span className="hd-filters__select">
                  <Select size="sm" aria-label="Ward" items={WARDS.map((w) => ({ value: w.value, label: wardName(w.value) }))} value={ward} onValueChange={setWard} />
                </span>
              }>
              {chosen.rows.length ? (
                <TableWrap>
                  <Table>
                    <TableHeader>
                      <TableRow>
                        <TableHead>Bed</TableHead>
                        <TableHead>Patient</TableHead>
                        <TableHead>Status</TableHead>
                        <TableHead>Day</TableHead>
                        <TableHead>Consultant</TableHead>
                        <TableHead>Action</TableHead>
                      </TableRow>
                    </TableHeader>
                    <TableBody>
                      {chosen.rows.map((admission) => {
                        const patient = patientById(data.patients, admission.patientId);
                        return (
                          <TableRow key={admission.id}>
                            <TableCell>{admission.bed || <span className="hd-warn">No bed</span>}</TableCell>
                            <TableCell>
                              <PersonCell name={patient.name} avatar={patient.avatar} size={24} meta={patient.number}
                                onOpen={() => onNavigate("/patients/" + patient.id)} />
                            </TableCell>
                            <TableCell><StatusBadge values={ADMISSION_STATUS} value={admission.status} /></TableCell>
                            <TableCell className="hd-num">{admission.admittedAt ? ADMISSION_FLAGS.lengthOfStayDays(admission, now) : "—"}</TableCell>
                            <TableCell>{staffName(data.staff, admission.consultantId)}</TableCell>
                            <TableCell><RowActions transitions={admissionTransitions(admission, viewer)} onRun={(t) => onAdmissionTransition(admission, t)} label={"Actions for " + patient.name} /></TableCell>
                          </TableRow>
                        );
                      })}
                    </TableBody>
                  </Table>
                </TableWrap>
              ) : <p className="hd-footnote" style={{ paddingInline: "var(--card-pad)" }}>{chosen.label} is empty — {chosen.beds} beds and nobody in them.</p>}
            </Band>
          </div>
          <div>
            <Band tint fill title={"On " + chosen.label + " today"} note={"The day shift, 07:00–15:00. The establishment asks for " + chosen.establishment.day + "."}>
              <div className="hd-rows">
                {onShift.length ? onShift.map((entry) => {
                  const person = staffById(data.staff, entry.staffId);
                  return (
                    <div className="hd-row" key={entry.id}>
                      <PersonCell name={person.name} avatar={person.avatar} size={28} meta={person.jobTitle}
                        onOpen={() => onNavigate((person.family === "medical" ? "/doctors/" : "/employees/") + person.id)} />
                    </div>
                  );
                }) : <p className="hd-footnote">Nobody is rostered onto {chosen.label} today.</p>}
              </div>
              <p className="hd-footnote" style={{ marginTop: "var(--space-3)" }}>
                {onShift.length >= chosen.establishment.day
                  ? "Fully staffed for the day shift."
                  : (chosen.establishment.day - onShift.length) + " short of the establishment for the day shift."}
                {" "}<button type="button" className="hd-linkbtn" onClick={() => onNavigate("/rota")}>Open the rota</button>
              </p>
            </Band>
          </div>
        </div>
      </>
    );
  }

  /* The rota is a grid rather than a table: the question it answers is "is this
     shift covered", which a row of names cannot show. */
  function RotaScreen({ data, now, viewer, onNavigate, onExport }) {
    const [ward, setWard] = React.useState(viewer.ward || rotaWards(data.rota)[0].value);
    const days = [0, 1, 2];
    const capacity = wardBedsOf(data.wardBeds, ward);
    const thin = understaffedShifts(data.rota, data.wardBeds, days);
    const thinHere = thin.filter((s) => s.ward === ward);
    const rosteredHere = data.rota.filter((r) => r.ward === ward && days.indexOf(r.day) > -1);
    const people = rosteredHere.reduce((list, r) => (list.indexOf(r.staffId) > -1 ? list : list.concat([r.staffId])), []);

    return (
      <>
        <PageHeader
          path="/rota"
          title="Rota"
          sub={"Three days out, three shifts a day. A shift is short when fewer people are rostered onto it than the ward establishment asks for."}
          actions={<ExportMenu scope={rosteredHere.length + " shifts on " + wardName(ward) + " over three days"} onExport={(format) => onExport("Rota", wardName(ward) + " ward, three days", rosteredHere.length, format)} />}
        />
        <Figures three items={[
          { label: "Shifts short of establishment", value: thin.length, qualifier: thinHere.length + " of them on " + wardName(ward) },
          { label: "People rostered here", value: people.length, qualifier: "across " + rosteredHere.length + " shifts on " + wardName(ward) },
          { label: "People short in total", value: thin.reduce((n, s) => n + s.shortfall, 0), qualifier: thin.length ? "summed over every short shift on both wards" : "every shift meets its establishment" },
        ]} />

        {thinHere.length ? (
          <div className="hd-alertwrap">
            <Alert variant="warning">
              <AlertTitle>{thinHere.length === 1 ? "One shift on " + wardName(ward) + " is short" : thinHere.length + " shifts on " + wardName(ward) + " are short"}</AlertTitle>
              <AlertDescription>
                {thinHere.map((s) => (s.day === 0 ? "Today" : new Date(now + s.day * DAY_MS).toLocaleDateString("en-GB", { weekday: "long" })) + " " + labelOf(SHIFT_TYPES, s.shift).split(",")[0].toLowerCase() + ", " + s.shortfall + " short").join(" · ")}.
              </AlertDescription>
            </Alert>
          </div>
        ) : null}

        <Band title={wardName(ward) + " — three days"} note={"The establishment is " + SHIFT_TYPES.map((s) => s.label.split(",")[0].toLowerCase() + " " + capacity.establishment[s.value]).join(", ") + "."}
          actions={
            <span className="hd-filters__select">
              <Select size="sm" aria-label="Ward" items={rotaWards(data.rota).map((w) => ({ value: w.value, label: wardName(w.value) }))} value={ward} onValueChange={setWard} />
            </span>
          }>
          <div className="hd-rota">
            <div className="hd-rota__head">
              <span>Day</span>
              {SHIFT_TYPES.map((shift) => <span key={shift.value}>{shift.label}</span>)}
            </div>
            {days.map((day) => (
              <div className="hd-rota__day" key={day}>
                <span className="hd-rota__label">
                  {day === 0 ? "Today" : new Date(now + day * DAY_MS).toLocaleDateString("en-GB", { weekday: "long" })}
                  <span className="hd-footnote">{new Date(now + day * DAY_MS).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}</span>
                </span>
                {SHIFT_TYPES.map((shift) => {
                  const rostered = rotaFor(data.rota, day, ward, shift.value);
                  const establishment = capacity.establishment[shift.value];
                  const short = SHIFT_FLAGS.understaffed(rostered, establishment);
                  return (
                    <div className="hd-rota__cell" key={shift.value} data-thin={short ? "true" : "false"}>
                      <span className="hd-rota__shift">
                        {rostered.length} of {establishment}
                        {short ? " · " + SHIFT_FLAGS.shortfall(rostered, establishment) + " short" : " · covered"}
                      </span>
                      {rostered.map((entry) => {
                        const person = staffById(data.staff, entry.staffId);
                        return (
                          <PersonCell key={entry.id} size={22} name={person.name} avatar={person.avatar} meta={person.jobTitle}
                            onOpen={() => onNavigate((person.family === "medical" ? "/doctors/" : "/employees/") + person.id)} />
                        );
                      })}
                      {rostered.length ? null : <span className="hd-footnote">Nobody rostered.</span>}
                    </div>
                  );
                })}
              </div>
            ))}
          </div>
        </Band>
      </>
    );
  }

  /* The drug round: active orders grouped by where their clock has got to, so a
     nurse works down one card at a time. */
  function RoundsScreen({ data, now, viewer, onNavigate, onMedicationTransition, onExport }) {
    const groups = roundGroups(data.orders, now);
    const due = dosesDue(data.orders, now);
    const missed = dosesMissed(data.orders, now);
    const given = dosesGivenSince(data.orders, now, 8);
    const active = activeOrders(data.orders);

    return (
      <>
        <PageHeader
          path="/rounds"
          title="Drug round"
          sub={"Every verified course with a dose on the clock. Recording a dose moves its next dose by the interval its frequency states."}
          actions={<ExportMenu scope={active.length + " active courses with a dose clock"} onExport={(format) => onExport("Drug round", "Active courses, all wards", active.length, format)} />}
        />
        <Figures items={[
          { label: "Overdue", value: missed.length, qualifier: missed.length ? "due more than an hour ago" : "nothing has been missed" },
          { label: "Due within the hour", value: due.filter((o) => !MEDICATION_FLAGS.doseMissed(o, now)).length, qualifier: "the round you are on" },
          { label: "Given in the last 8 hours", value: given.length, qualifier: "of " + active.length + " active courses" },
          { label: "Awaiting a pharmacist", value: ordersAwaitingPharmacy(data.orders).length, qualifier: "not on the round until they are verified" },
        ]} />

        {groups.map((group) => (
          <Band key={group.id} tint={group.id === "later"} title={group.label + " (" + group.rows.length + ")"} note={group.note}>
            {group.rows.length ? (
              <div className="hd-rows">
                {group.rows.map((order) => {
                  const patient = patientById(data.patients, order.patientId);
                  const admission = admissionOfPatient(data.admissions, order.patientId);
                  return (
                    <div className={"hd-row" + (group.id === "later" ? " hd-row--quiet" : "")} key={order.id}>
                      <span className="hd-row__time">{new Date(order.nextDoseAt).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })}</span>
                      <div className="hd-row__text">
                        <span className="hd-row__title">
                          <PersonCell name={patient.name} avatar={patient.avatar} size={26}
                            meta={(admission && admission.bed ? wardName(admission.ward) + " " + admission.bed : "Not on a ward") + " · " + patient.number}
                            onOpen={() => onNavigate("/patients/" + patient.id)} />
                        </span>
                        <span className="hd-row__note">
                          {order.drug} {order.dose} · {labelOf(DRUG_ROUTES, order.route)} · {order.frequency.toLowerCase()} · {order.indication}
                          {order.lastGivenAt ? " · last given " + formatWhen(order.lastGivenAt, now) : " · never given"}
                        </span>
                      </div>
                      <AllergyChips patient={patient} />
                      {MEDICATION_FLAGS.doseMissed(order, now) ? <Badge size="sm" variant="warning">{formatCountdown(order.nextDoseAt, now)}</Badge> : null}
                      <Transitions transitions={medicationTransitions(order, viewer).filter((t) => t.id === "record_dose")} onRun={(t) => onMedicationTransition(order, t)} />
                    </div>
                  );
                })}
              </div>
            ) : (
              <p className="hd-footnote">
                {group.id === "overdue" ? "Nothing is overdue. Every dose due before this hour has been recorded as given." : group.id === "now" ? "Nothing is due in this hour." : "Nothing further is due before midnight."}
              </p>
            )}
          </Band>
        ))}
      </>
    );
  }

  Object.assign(window, { WardsScreen, RotaScreen, RoundsScreen, BedGrid });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="ReportsScreen.jsx">
(function(){
/* Reports — the six figures the hospital is actually judged on, each one a rate
   with its own denominator named, and each one drilling through to the rows it
   was computed from. Commit model: INSTANT (there is nothing to commit; the only
   controls are the export and the links).

   Every figure here is `reportFigures(data, now)` in metrics.js — the same
   arrays the destination screens render, so no number on this screen exists
   only here. */
(() => {
  const {
    Button, Badge, Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  } = window.OriginDesignSystem_7c998b;

  function Rate({ label, value, detail, route, onNavigate, tone }) {
    return (
      <div className="hd-tile">
        <span className="hd-tile__label">{label}</span>
        <span className="hd-tile__value">{Math.round(value * 100)}%</span>
        <StockBar percent={value * 100} tone={tone} />
        <span className="hd-footnote">{detail}</span>
        {route ? <Button variant="link" size="xs" onClick={() => onNavigate(route)}>See the rows</Button> : null}
      </div>
    );
  }

  function ReportsScreen({ data, now, viewer, onNavigate, onExport }) {
    const f = reportFigures(data, now);
    const wards = wardRows(data.admissions, data.wardBeds);
    const theatre = interventionsToday(data.interventions, now);
    const open = openAdmissions(data.admissions);
    const longStays = open.filter((a) => ADMISSION_FLAGS.overExpectedStay(a, now));

    return (
      <>
        <PageHeader
          path="/reports"
          title="Reports"
          sub={"Halden General at " + new Date(now).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }) + " on " + new Date(now).toLocaleDateString("en-GB", { day: "numeric", month: "long" }) + ". Every rate below states the set it divides, and drills through to it."}
          actions={<ExportMenu scope="These six figures and the rows behind them" onExport={(format) => onExport("Hospital report", "Six figures, " + new Date(now).toLocaleDateString("en-GB", { day: "numeric", month: "short" }), 6, format)} />}
        />

        <Figures three items={[
          { label: "Beds occupied", value: f.occupancy.count + " / " + f.occupancy.of, qualifier: Math.round(f.occupancy.value * 100) + "% of the beds on the " + wards.length + " wards Ward Desk covers" },
          { label: "Mean length of stay", value: f.stay.mean.toFixed(1), qualifier: "days, over the " + f.stay.of + " admissions currently on a ward · longest " + f.stay.longest },
          { label: "Bed waits past four hours", value: f.breaches.count, qualifier: "of " + f.breaches.of + " people waiting for a bed" },
        ]} />

        <Band title="Rates" note="Each rate is a count over a named set, computed at render from the arrays the destination screens use.">
          <div className="hd-split">
            <Rate label="Theatre time booked today" value={Math.min(1, f.theatre.value)} onNavigate={onNavigate} route="/interventions"
              detail={f.theatre.minutes + " minutes across " + f.theatre.cases + " cases, of " + THEATRE_MINUTES_PER_DAY + " minutes across " + THEATRES.length + " rooms"} />
            <Rate label="Consent recorded" value={f.consent.value} onNavigate={onNavigate} route="/interventions"
              tone={f.consent.value < 1 ? "warn" : null}
              detail={f.consent.count + " of " + f.consent.of + " scheduled procedures have consent on the record"} />
            <Rate label="Doses on time" value={f.doses.value} onNavigate={onNavigate} route="/rounds"
              tone={f.doses.value < 1 ? "warn" : null}
              detail={f.doses.count + " of " + f.doses.of + " active courses have no dose more than an hour overdue"} />
            <Rate label="Hospital occupancy" value={f.occupancy.value} onNavigate={onNavigate} route="/wards"
              detail={f.occupancy.count + " of " + f.occupancy.of + " beds, counted from the admissions each ward renders"} />
          </div>
        </Band>

        <div className="hd-cols">
          <div>
            <Band flush fill title={"Occupancy by ward (" + wards.length + ")"} note="Occupied beds against the establishment in each ward's record.">
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead>Ward</TableHead>
                      <TableHead className="hd-num">Occupied</TableHead>
                      <TableHead className="hd-num">Beds</TableHead>
                      <TableHead style={{ minWidth: "8rem" }}>Occupancy</TableHead>
                      <TableHead className="hd-num">Held</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {wards.map((w) => (
                      <TableRow key={w.ward}>
                        <TableCell>
                          <span className="hd-person__text">
                            <span className="hd-person__name"><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/wards")}>{w.label}</button></span>
                            <span className="hd-person__meta">{w.specialty}</span>
                          </span>
                        </TableCell>
                        <TableCell className="hd-num">{w.occupied}</TableCell>
                        <TableCell className="hd-num">{w.beds}</TableCell>
                        <TableCell>
                          <span style={{ display: "flex", flexDirection: "column", gap: 4, minWidth: "7rem" }}>
                            <span className="hd-footnote">{Math.round(w.occupancy * 100)}%</span>
                            <StockBar percent={w.occupancy * 100} tone={w.occupancy > 0.9 ? "warn" : null} />
                          </span>
                        </TableCell>
                        <TableCell className="hd-num">{w.held || <span className="hd-muted">—</span>}</TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </TableWrap>
            </Band>
          </div>
          <div className="hd-stack">
            <Band title={"Stays running long (" + longStays.length + ")"} note="On a ward past the stay their admission expected.">
              {longStays.length ? (
                <div className="hd-rows">
                  {longStays.map((admission) => {
                    const patient = patientById(data.patients, admission.patientId);
                    const days = ADMISSION_FLAGS.lengthOfStayDays(admission, now);
                    return (
                      <div className="hd-row" key={admission.id}>
                        <div className="hd-row__text">
                          <span className="hd-row__title">
                            <PersonCell name={patient.name} avatar={patient.avatar} size={26}
                              meta={wardName(admission.ward) + " " + admission.bed + " · " + staffName(data.staff, admission.consultantId)}
                              onOpen={() => onNavigate("/patients/" + patient.id)} />
                          </span>
                          <span className="hd-row__note">{admission.reason}</span>
                        </div>
                        <Badge size="sm" variant="warning">Day {days} of {admission.expectedStayDays}</Badge>
                      </div>
                    );
                  })}
                </div>
              ) : <p className="hd-footnote">Every open admission is inside the stay it expected.</p>}
            </Band>
            <Band tint title={"Theatre list today (" + theatre.length + ")"} note="What the utilisation figure above is made of.">
              <div className="hd-rows">
                {theatre.map((intervention) => (
                  <div className="hd-row hd-row--quiet" key={intervention.id}>
                    <span className="hd-row__time">{new Date(intervention.scheduledAt).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })}</span>
                    <div className="hd-row__text">
                      <span className="hd-row__title"><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/interventions/" + intervention.id)}>{intervention.name}</button></span>
                      <span className="hd-row__note">{intervention.plannedMin} min · {labelOf(THEATRES, intervention.theatre)} · {patientName(data.patients, intervention.patientId)}</span>
                    </div>
                    <StatusBadge values={INTERVENTION_STATUS} value={intervention.status} />
                  </div>
                ))}
              </div>
            </Band>
          </div>
        </div>
      </>
    );
  }

  Object.assign(window, { ReportsScreen });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="FinanceScreens.jsx">
(function(){
/* Finance — invoices, the money against them, and what is owed.
   Commit model: INSTANT on all three screens. Coding, issuing, disputing and
   resolving apply immediately with an undo toast; recording a payment and
   marking a dispute need fields, so they open staged dialogs from App.jsx;
   writing a balance off is irreversible and opens a confirm dialog.

   Nothing here stores a total. An invoice's total is the sum of its lines, what
   it has been paid is the sum of its CLEARED payments, and its balance is the
   difference — metrics.js owns all three, so the list, the record, the report and
   the nav badge cannot disagree. */
(() => {
  const {
    Button, Badge, Avatar, Checkbox, Select, Alert, AlertTitle, AlertDescription,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableFooter,
  } = window.OriginDesignSystem_7c998b;

  const INV_STATUS_FILTER = [{ value: "all", label: "All statuses" }].concat(INVOICE_STATUS.map((s) => ({ value: s.id, label: s.label })));
  const PAYER_FILTER = [{ value: "all", label: "All payers" }].concat(PAYERS);
  const canIssue = (invoice, viewer) => invoiceTransitions(invoice, viewer).some((t) => t.id === "issue" && !t.disabled);

  function InvoicesScreen({ data, now, viewer, onNavigate, onInvoiceTransition, onBulkIssue, onNewInvoice, onExport }) {
    const [query, setQuery] = React.useState("");
    const [status, setStatus] = React.useState("all");
    const [payer, setPayer] = React.useState("all");
    const [selected, setSelected] = React.useState([]);
    const { sort, onSort } = useSort("issued", "desc");

    const overdue = invoicesOverdue(data.invoices, data.payments, now);
    const unbilled = invoicesUnbilled(data.invoices);
    const outstanding = outstandingTotal(data.invoices, data.payments);
    const disputed = invoicesDisputed(data.invoices);

    const dirty = !!query || status !== "all" || payer !== "all";
    const clear = () => { setQuery(""); setStatus("all"); setPayer("all"); };
    const filtered = data.invoices.filter((invoice) => {
      if (status !== "all" && invoice.status !== status) return false;
      if (payer !== "all" && invoice.payer !== payer) return false;
      if (query) {
        const q = query.toLowerCase();
        const patient = patientById(data.patients, invoice.patientId);
        if ((invoice.reference + " " + patient.name + " " + patient.number).toLowerCase().indexOf(q) < 0) return false;
      }
      return true;
    });
    const rows = sortRows(filtered, sort, {
      issued: (i) => i.issuedAt || 0,
      patient: (i) => patientById(data.patients, i.patientId).name,
      total: (i) => invoiceTotal(i),
      balance: (i) => invoiceBalance(i, data.payments),
      due: (i) => i.dueAt || Infinity,
    });
    const chosen = data.invoices.filter((i) => selected.indexOf(i.id) > -1);
    const issuable = chosen.filter((i) => canIssue(i, viewer));

    return (
      <>
        <PageHeader
          path="/invoices"
          title="Invoices"
          sub={"One invoice per episode of care. Every line names the procedure, the bed days or the medication course it bills, so a payer's query can be answered from the care record."}
          actions={
            <>
              <ExportMenu scope={dirty ? filtered.length + " invoices matching the filters" : "All " + data.invoices.length + " invoices"}
                onExport={(format) => onExport("Invoices", dirty ? "Filtered invoices" : "All invoices", filtered.length, format)} />
              <Button size="sm" onClick={onNewInvoice}><Icon name="plus" />New invoice</Button>
            </>
          }
        />
        <Figures items={[
          { label: "Outstanding", value: formatPounds(outstanding), qualifier: formatPounds(overdue.reduce((n, i) => n + invoiceBalance(i, data.payments), 0)) + " of it past the due date" },
          { label: "Past due", value: overdue.length, qualifier: overdue.length ? "oldest " + formatCountdown(overdue.slice().sort((a, b) => a.dueAt - b.dueAt)[0].dueAt, now) : "nothing is past its due date" },
          { label: "Not yet billed", value: unbilled.length, qualifier: unbilled.filter((i) => i.status === "awaiting_coding").length + " waiting on a coder, worth " + formatPounds(unbilled.reduce((n, i) => n + invoiceTotal(i), 0)) },
          { label: "Disputed", value: disputed.length, qualifier: disputed.length ? formatPounds(disputed.reduce((n, i) => n + invoiceBalance(i, data.payments), 0)) + " held behind a payer's query" : "no payer has queried a bill" },
        ]} />

        {disputed.length ? (
          <div className="hd-alertwrap">
            <Alert variant="warning">
              <AlertTitle>{disputed.length === 1 ? "One invoice is disputed" : disputed.length + " invoices are disputed"}</AlertTitle>
              <AlertDescription>{disputed.map((i) => i.reference + " — " + (i.note || "no query recorded")).join(" · ")}</AlertDescription>
            </Alert>
          </div>
        ) : null}

        <Band flush title={"Invoices (" + data.invoices.length + ")"} note="Totals are the sum of each invoice's lines; the balance is what its cleared payments have not covered.">
          <FilterBar query={query} onQuery={setQuery} placeholder="Search reference, patient or hospital number…" dirty={dirty} onClear={clear}
            selects={[
              { id: "status", label: "Status", items: INV_STATUS_FILTER, value: status, onChange: setStatus },
              { id: "payer", label: "Payer", items: PAYER_FILTER, value: payer, onChange: setPayer },
            ]} />
          <BulkBar count={chosen.length}
            note={issuable.length ? issuable.length + " of them are coded and can be issued" : "none of the selected invoices are coded and ready to issue"}
            onClear={() => setSelected([])}>
            {issuable.length ? <Button size="sm" onClick={() => { onBulkIssue(issuable); setSelected([]); }}>Issue {issuable.length}</Button> : null}
            <ExportMenu scope={chosen.length + " selected invoices"} onExport={(format) => { onExport("Invoice selection", chosen.map((i) => i.reference).join(", "), chosen.length, format); setSelected([]); }} />
          </BulkBar>
          {rows.length === 0 ? <NoResults noun="invoices" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead style={{ width: 36 }}>
                        <Checkbox aria-label="Select every invoice in this list"
                          checked={rows.length > 0 && rows.every((i) => selected.indexOf(i.id) > -1)}
                          indeterminate={selected.length > 0 && !rows.every((i) => selected.indexOf(i.id) > -1)}
                          onCheckedChange={(on) => setSelected(on ? rows.map((i) => i.id) : [])} />
                      </TableHead>
                      <TableHead>Reference</TableHead>
                      <TableHead><SortHead label="Patient" sortKey="patient" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Payer</TableHead>
                      <TableHead>Status</TableHead>
                      <TableHead className="hd-num"><SortHead label="Total" sortKey="total" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead className="hd-num"><SortHead label="Balance" sortKey="balance" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead><SortHead label="Due" sortKey="due" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Action</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((invoice) => {
                      const patient = patientById(data.patients, invoice.patientId);
                      const balance = invoiceBalance(invoice, data.payments);
                      const late = INVOICE_FLAGS.overdue(invoice, balance, now);
                      return (
                        <TableRow key={invoice.id}>
                          <TableCell>
                            <Checkbox aria-label={"Select " + invoice.reference} checked={selected.indexOf(invoice.id) > -1}
                              onCheckedChange={(on) => setSelected((list) => (on ? list.concat([invoice.id]) : list.filter((id) => id !== invoice.id)))} />
                          </TableCell>
                          <TableCell>
                            <span className="hd-person__text">
                              <span className="hd-person__name"><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/invoices/" + invoice.id)}>{invoice.reference}</button></span>
                              <span className="hd-person__meta">{invoice.lines.length} {invoice.lines.length === 1 ? "line" : "lines"}</span>
                            </span>
                          </TableCell>
                          <TableCell>
                            <PersonCell name={patient.name} avatar={patient.avatar} size={24} meta={patient.number}
                              onOpen={() => onNavigate("/patients/" + patient.id)} />
                          </TableCell>
                          <TableCell>{labelOf(PAYERS, invoice.payer)}</TableCell>
                          <TableCell>
                            <span className="hd-actions__row">
                              <StatusBadge values={INVOICE_STATUS} value={invoice.status} />
                              {late ? <Badge size="sm" variant="warning">{formatCountdown(invoice.dueAt, now)}</Badge> : null}
                            </span>
                          </TableCell>
                          <TableCell className="hd-num">{formatPounds(invoiceTotal(invoice))}</TableCell>
                          <TableCell className="hd-num">{balance ? formatPounds(balance) : <span className="hd-muted">Settled</span>}</TableCell>
                          <TableCell>{invoice.dueAt ? <Clock at={invoice.dueAt} now={now} problem={late} /> : <span className="hd-muted">Not issued</span>}</TableCell>
                          <TableCell><RowActions transitions={invoiceTransitions(invoice, viewer, balance)} onRun={(t) => onInvoiceTransition(invoice, t)} label={"Actions for " + invoice.reference} /></TableCell>
                        </TableRow>
                      );
                    })}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={data.invoices.length} noun="invoices" />
            </>
          )}
        </Band>
      </>
    );
  }

  function InvoiceScreen({ data, now, viewer, invoiceId, onNavigate, onInvoiceTransition, onPostNote, onReactToNote, onExport }) {
    const invoice = data.invoices.find((i) => i.id === invoiceId);
    const patient = patientById(data.patients, invoice.patientId);
    const total = invoiceTotal(invoice);
    const paid = invoicePaid(data.payments, invoice.id);
    const balance = invoiceBalance(invoice, data.payments);
    const rows = paymentsFor(data.payments, invoice.id);
    const pending = rows.filter((p) => p.status === "pending");
    const late = INVOICE_FLAGS.overdue(invoice, balance, now);
    const notes = commentsFor(data.comments, "invoice", invoice.id);
    const admission = admissionOfPatient(data.admissions, patient.id);

    /* A line's reference is a real record, so the row opens it. */
    const lineRoute = (ref) => (ref ? recordRoute(data, ref) : null);

    return (
      <>
        <PageHeader path="/invoices/:id" params={{ id: invoice.id }} crumb={invoice.reference} title={invoice.reference}
          actions={<ExportMenu scope={"This invoice, its " + invoice.lines.length + " lines and " + rows.length + " payments"} onExport={(format) => onExport("Invoice", invoice.reference + " · " + patient.name, 1, format)} />} />

        <div className="hd-record">
          <div className="hd-record__id">
            <Avatar size={56} src={patient.avatar} alt={"Photograph of " + patient.name} fallback={initialsOf(patient.name)} />
            <div className="hd-record__text">
              <span className="hd-record__name">{formatPoundsExact(total)}</span>
              <span className="hd-record__meta">
                <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/patients/" + patient.id)}>{patient.name}</button>
                <span>{labelOf(PAYERS, invoice.payer)}</span>
                <StatusBadge values={INVOICE_STATUS} value={invoice.status} />
                {late ? <Badge size="sm" variant="warning">{formatCountdown(invoice.dueAt, now)}</Badge> : null}
              </span>
            </div>
          </div>
          <div className="hd-record__actions">
            <Transitions transitions={invoiceTransitions(invoice, viewer, balance)} onRun={(t) => onInvoiceTransition(invoice, t)} />
          </div>
        </div>

        <Figures three items={[
          { label: "Invoiced", value: formatPounds(total), qualifier: invoice.lines.length + (invoice.lines.length === 1 ? " line" : " lines") + " · " + revenueLineNote(invoice) },
          { label: "Cleared", value: formatPounds(paid), qualifier: rows.filter((p) => p.status === "cleared").length + " cleared payments" + (pending.length ? " · " + formatPounds(pending.reduce((n, p) => n + p.amountMinor, 0)) + " advised but not cleared" : "") },
          { label: "Balance", value: formatPounds(balance), qualifier: balance ? (invoice.dueAt ? "due " + formatWhen(invoice.dueAt, now) + " · " + formatCountdown(invoice.dueAt, now) : "not issued, so nothing is due yet") : "nothing outstanding" },
        ]} />

        {invoice.note ? (
          <div className="hd-alertwrap">
            <Alert variant={invoice.status === "disputed" ? "warning" : "info"}>
              <AlertTitle>{invoice.status === "disputed" ? "The payer has queried this invoice" : invoice.status === "draft" ? "Not issued yet" : "Note on this invoice"}</AlertTitle>
              <AlertDescription>{invoice.note}</AlertDescription>
            </Alert>
          </div>
        ) : null}

        <Band flush title={"Lines (" + invoice.lines.length + ")"} note="Each line bills a record in this patient's care, priced against the payer's contract. Open the record to see what it was.">
          <TableWrap>
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>What is billed</TableHead>
                  <TableHead>Kind</TableHead>
                  <TableHead className="hd-num">Quantity</TableHead>
                  <TableHead className="hd-num">Unit</TableHead>
                  <TableHead>Against contract</TableHead>
                  <TableHead className="hd-num">Line total</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {invoice.lines.map((line, index) => {
                  const route = lineRoute(line.ref);
                  return (
                    <TableRow key={line.label + index}>
                      <TableCell>
                        <span className="hd-person__text">
                          <span className="hd-person__name">
                            {route ? <button type="button" className="hd-linkbtn" onClick={() => onNavigate(route)}>{line.label}</button> : line.label}
                          </span>
                          <span className="hd-person__meta">{line.ref ? recordTitle(data, line.ref.type, line.ref.id) : "Not tied to one record"}</span>
                        </span>
                      </TableCell>
                      <TableCell>{labelOf(BILL_LINE_KINDS, line.kind)}</TableCell>
                      <TableCell className="hd-num">{line.qty}</TableCell>
                      <TableCell className="hd-num">{formatPoundsExact(line.unitMinor)}</TableCell>
                      <TableCell>
                        {(function () {
                          const finding = lineOffContract(data, invoice, line);
                          if (!finding) return <span className="hd-footnote">At contract</span>;
                          return finding.kind === "no_price"
                            ? <Badge size="sm" variant="warning">Not in the contract</Badge>
                            : <Badge size="sm" variant="warning">{finding.difference > 0 ? "Over by " : "Under by "}{formatPoundsExact(Math.abs(finding.difference))}</Badge>;
                        })()}
                      </TableCell>
                      <TableCell className="hd-num">{formatPoundsExact(lineTotal(line))}</TableCell>
                    </TableRow>
                  );
                })}
              </TableBody>
              <TableFooter>
                <TableRow>
                  <TableCell colSpan={5}>Total invoiced</TableCell>
                  <TableCell className="hd-num">{formatPoundsExact(total)}</TableCell>
                </TableRow>
              </TableFooter>
            </Table>
          </TableWrap>
        </Band>

        <div className="hd-cols">
          <div>
            <Band flush fill title={"Payments (" + rows.length + ")"} note="Only cleared money counts against the balance.">
              {rows.length ? (
                <TableWrap>
                  <Table>
                    <TableHeader>
                      <TableRow>
                        <TableHead>Received</TableHead>
                        <TableHead>Method</TableHead>
                        <TableHead>Status</TableHead>
                        <TableHead className="hd-num">Amount</TableHead>
                        <TableHead>Reference</TableHead>
                      </TableRow>
                    </TableHeader>
                    <TableBody>
                      {rows.map((payment) => (
                        <TableRow key={payment.id}>
                          <TableCell><Clock at={payment.receivedAt} now={now} /></TableCell>
                          <TableCell>{labelOf(PAYMENT_METHODS, payment.method)}</TableCell>
                          <TableCell><StatusBadge values={PAYMENT_STATUS} value={payment.status} /></TableCell>
                          <TableCell className="hd-num">{formatPoundsExact(payment.amountMinor)}</TableCell>
                          <TableCell>{payment.reference}</TableCell>
                        </TableRow>
                      ))}
                    </TableBody>
                  </Table>
                </TableWrap>
              ) : <p className="hd-footnote" style={{ paddingInline: "var(--card-pad)" }}>No money has been received against {invoice.reference} yet.</p>}
            </Band>
          </div>
          <div>
            <Band tint fill title="The care this bills">
              <Pairs items={[
                { label: "Patient", value: <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/patients/" + patient.id)}>{patient.name}</button> },
                { label: "Hospital number", value: patient.number },
                { label: "Episode", value: admission ? wardName(admission.ward) + " · " + labelOf(ADMISSION_SOURCES, admission.source) : "No ward episode" },
                { label: "Payer", value: labelOf(PAYERS, invoice.payer) },
                { label: "Issued", value: invoice.issuedAt ? formatWhen(invoice.issuedAt, now) : "Not issued" },
                { label: "Due", value: invoice.dueAt ? formatWhen(invoice.dueAt, now) : "Not issued" },
                { label: "Coded by", value: invoice.codedById ? staffName(data.staff, invoice.codedById) : "Not coded yet" },
                { label: "Age", value: invoice.issuedAt ? INVOICE_FLAGS.ageDays(invoice, now) + " days since issue" : "—" },
              ]} />
            </Band>
          </div>
        </div>

        <Band title={"Notes (" + notes.length + ")"} note="Written on this invoice — what a payer asked, and what was answered. Posting is immediate.">
          <NoteThread comments={notes} staff={data.staff} viewer={viewer} now={now}
            onReact={onReactToNote} onPost={(body) => onPostNote({ type: "invoice", id: invoice.id }, body)}
            placeholder={"What the payer asked about " + invoice.reference + ", and what you answered…"} />
        </Band>
      </>
    );
  }

  /* The one sentence an invoice's figure row needs about its own lines. */
  function revenueLineNote(invoice) {
    const kinds = invoice.lines.reduce((list, line) => (list.indexOf(line.kind) > -1 ? list : list.concat([line.kind])), []);
    return kinds.map((kind) => labelOf(BILL_LINE_KINDS, kind).toLowerCase()).join(", ");
  }

  function PaymentsScreen({ data, now, viewer, onNavigate, onNewPayment, onExport }) {
    const [query, setQuery] = React.useState("");
    const [status, setStatus] = React.useState("all");
    const { sort, onSort } = useSort("received", "desc");

    const cleared = data.payments.filter((p) => p.status === "cleared");
    const pending = data.payments.filter((p) => p.status === "pending");
    const failed = data.payments.filter((p) => p.status === "failed");

    const dirty = !!query || status !== "all";
    const clear = () => { setQuery(""); setStatus("all"); };
    const filtered = data.payments.filter((payment) => {
      if (status !== "all" && payment.status !== status) return false;
      if (query) {
        const invoice = data.invoices.find((i) => i.id === payment.invoiceId);
        const patient = invoice ? patientById(data.patients, invoice.patientId) : null;
        if ((payment.reference + " " + (invoice ? invoice.reference : "") + " " + (patient ? patient.name : "")).toLowerCase().indexOf(query.toLowerCase()) < 0) return false;
      }
      return true;
    });
    const rows = sortRows(filtered, sort, {
      received: (p) => p.receivedAt,
      amount: (p) => p.amountMinor,
    });

    return (
      <>
        <PageHeader
          path="/payments"
          title="Payments"
          sub={"Every movement of money against an invoice. Pending money is advised, not received — it counts against nothing until it clears."}
          actions={
            <>
              <ExportMenu scope={dirty ? filtered.length + " payments matching the filters" : "All " + data.payments.length + " payments"}
                onExport={(format) => onExport("Payments", dirty ? "Filtered payments" : "All payments", filtered.length, format)} />
              <Button size="sm" onClick={onNewPayment}><Icon name="plus" />Record payment</Button>
            </>
          }
        />
        <Figures three items={[
          { label: "Cleared", value: formatPounds(cleared.reduce((n, p) => n + p.amountMinor, 0)), qualifier: cleared.length + " payments across " + cleared.reduce((list, p) => (list.indexOf(p.invoiceId) > -1 ? list : list.concat([p.invoiceId])), []).length + " invoices" },
          { label: "Advised, not cleared", value: formatPounds(pending.reduce((n, p) => n + p.amountMinor, 0)), qualifier: pending.length + (pending.length === 1 ? " pending payment" : " pending payments") },
          { label: "Failed", value: formatPounds(failed.reduce((n, p) => n + p.amountMinor, 0)), qualifier: failed.length ? failed.map((p) => p.note).join(" · ") : "no payment has been returned" },
        ]} />

        <Band flush title={"Payments (" + data.payments.length + ")"}>
          <FilterBar query={query} onQuery={setQuery} placeholder="Search payment or invoice reference…" dirty={dirty} onClear={clear}
            selects={[{ id: "status", label: "Status", items: [{ value: "all", label: "All statuses" }].concat(PAYMENT_STATUS.map((s) => ({ value: s.id, label: s.label }))), value: status, onChange: setStatus }]} />
          {rows.length === 0 ? <NoResults noun="payments" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead><SortHead label="Received" sortKey="received" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Invoice</TableHead>
                      <TableHead>Patient</TableHead>
                      <TableHead>Payer and method</TableHead>
                      <TableHead>Status</TableHead>
                      <TableHead className="hd-num"><SortHead label="Amount" sortKey="amount" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Reference</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((payment) => {
                      const invoice = data.invoices.find((i) => i.id === payment.invoiceId);
                      const patient = invoice ? patientById(data.patients, invoice.patientId) : null;
                      return (
                        <TableRow key={payment.id}>
                          <TableCell><Clock at={payment.receivedAt} now={now} /></TableCell>
                          <TableCell><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/invoices/" + payment.invoiceId)}>{invoice ? invoice.reference : payment.invoiceId}</button></TableCell>
                          <TableCell>{patient ? <PersonCell name={patient.name} avatar={patient.avatar} size={24} onOpen={() => onNavigate("/patients/" + patient.id)} /> : <span className="hd-muted">—</span>}</TableCell>
                          <TableCell>{labelOf(PAYERS, payment.payer)} · {labelOf(PAYMENT_METHODS, payment.method)}</TableCell>
                          <TableCell><StatusBadge values={PAYMENT_STATUS} value={payment.status} /></TableCell>
                          <TableCell className="hd-num">{formatPoundsExact(payment.amountMinor)}</TableCell>
                          <TableCell>{payment.reference}</TableCell>
                        </TableRow>
                      );
                    })}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={data.payments.length} noun="payments" />
            </>
          )}
        </Band>
      </>
    );
  }

  function RevenueScreen({ data, now, viewer, onNavigate, onExport }) {
    const payers = revenueByPayer(data.invoices, data.payments);
    const kinds = revenueByKind(data.invoices);
    const aged = agedDebt(data.invoices, data.payments, now);
    const billed = billedTotal(data.invoices);
    const collected = collectedTotal(data.invoices, data.payments);
    const outstanding = outstandingTotal(data.invoices, data.payments);
    const unbilled = invoicesUnbilled(data.invoices);
    const rate = billed ? collected / billed : 0;

    return (
      <>
        <PageHeader
          path="/revenue"
          title="Revenue and debt"
          sub={"Every figure divides the " + invoicesBillable(data.invoices).length + " issued invoices — drafts and invoices awaiting coding are counted separately, because nothing has been asked for yet."}
          actions={<ExportMenu scope={"Revenue by payer, by kind, and the aged debt"} onExport={(format) => onExport("Revenue report", "Issued invoices as at " + new Date(now).toLocaleDateString("en-GB", { day: "numeric", month: "short" }), invoicesBillable(data.invoices).length, format)} />}
        />
        <Figures items={[
          { label: "Billed", value: formatPounds(billed), qualifier: "across " + invoicesBillable(data.invoices).length + " issued invoices" },
          { label: "Collected", value: formatPounds(collected), qualifier: Math.round(rate * 100) + "% of what was billed" },
          { label: "Outstanding", value: formatPounds(outstanding), qualifier: formatPounds(aged.slice(1).reduce((n, b) => n + b.total, 0)) + " of it past due" },
          { label: "Not yet billed", value: formatPounds(unbilled.reduce((n, i) => n + invoiceTotal(i), 0)), qualifier: unbilled.length + " drafts and invoices awaiting coding" },
        ]} />

        <div className="hd-cols hd-cols--wide">
          <div className="hd-stack">
            <Band flush title={"By payer (" + payers.filter((p) => p.invoices).length + ")"} note="Billed, collected and outstanding over the same set of issued invoices.">
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead>Payer</TableHead>
                      <TableHead className="hd-num">Invoices</TableHead>
                      <TableHead className="hd-num">Billed</TableHead>
                      <TableHead style={{ minWidth: "8rem" }}>Share of billing</TableHead>
                      <TableHead className="hd-num">Collected</TableHead>
                      <TableHead className="hd-num">Outstanding</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {payers.filter((p) => p.invoices).map((row) => (
                      <TableRow key={row.payer}>
                        <TableCell>{row.label}</TableCell>
                        <TableCell className="hd-num">{row.invoices}</TableCell>
                        <TableCell className="hd-num">{formatPounds(row.billed)}</TableCell>
                        <TableCell>
                          <span style={{ display: "flex", flexDirection: "column", gap: 4, minWidth: "7rem" }}>
                            <span className="hd-footnote">{Math.round(row.share * 100)}%</span>
                            <StockBar percent={row.share * 100} />
                          </span>
                        </TableCell>
                        <TableCell className="hd-num">{formatPounds(row.collected)}</TableCell>
                        <TableCell className="hd-num">{row.outstanding ? formatPounds(row.outstanding) : <span className="hd-muted">Settled</span>}</TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                  <TableFooter>
                    <TableRow>
                      <TableCell>All payers</TableCell>
                      <TableCell className="hd-num">{invoicesBillable(data.invoices).length}</TableCell>
                      <TableCell className="hd-num">{formatPounds(billed)}</TableCell>
                      <TableCell />
                      <TableCell className="hd-num">{formatPounds(collected)}</TableCell>
                      <TableCell className="hd-num">{formatPounds(outstanding)}</TableCell>
                    </TableRow>
                  </TableFooter>
                </Table>
              </TableWrap>
            </Band>
            <Band flush title={"Past due (" + invoicesOverdue(data.invoices, data.payments, now).length + ")"} note="Issued, with a balance, and past the date the payer was given.">
              {invoicesOverdue(data.invoices, data.payments, now).length ? (
                <TableWrap>
                  <Table>
                    <TableHeader>
                      <TableRow>
                        <TableHead>Reference</TableHead>
                        <TableHead>Patient</TableHead>
                        <TableHead>Payer</TableHead>
                        <TableHead className="hd-num">Balance</TableHead>
                        <TableHead>Overdue by</TableHead>
                        <TableHead>Action</TableHead>
                      </TableRow>
                    </TableHeader>
                    <TableBody>
                      {invoicesOverdue(data.invoices, data.payments, now).slice().sort((a, b) => a.dueAt - b.dueAt).map((invoice) => {
                        const patient = patientById(data.patients, invoice.patientId);
                        return (
                          <TableRow key={invoice.id}>
                            <TableCell><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/invoices/" + invoice.id)}>{invoice.reference}</button></TableCell>
                            <TableCell><PersonCell name={patient.name} avatar={patient.avatar} size={24} onOpen={() => onNavigate("/patients/" + patient.id)} /></TableCell>
                            <TableCell>{labelOf(PAYERS, invoice.payer)}</TableCell>
                            <TableCell className="hd-num">{formatPounds(invoiceBalance(invoice, data.payments))}</TableCell>
                            <TableCell><span className="hd-warn">{formatCountdown(invoice.dueAt, now)}</span></TableCell>
                            <TableCell><Button variant="outline" size="sm" onClick={() => onNavigate("/invoices/" + invoice.id)}>Open</Button></TableCell>
                          </TableRow>
                        );
                      })}
                    </TableBody>
                  </Table>
                </TableWrap>
              ) : <p className="hd-footnote" style={{ paddingInline: "var(--card-pad)" }}>Nothing is past due. Every issued invoice with a balance is still inside the time its payer was given.</p>}
            </Band>
          </div>
          <div className="hd-stack">
            <Band title="Aged debt" note="Every issued invoice with a balance, in the bucket its due date puts it in. An invoice appears once.">
              <div className="hd-stack">
                {aged.map((bucket) => (
                  <div className="hd-tile" key={bucket.id}>
                    <span className="hd-tile__label">{bucket.label}</span>
                    <span className="hd-tile__value">{formatPounds(bucket.total)}</span>
                    <StockBar percent={outstanding ? (bucket.total / outstanding) * 100 : 0} tone={bucket.id === "current" ? null : "warn"} />
                    <span className="hd-footnote">
                      {bucket.rows.length
                        ? bucket.rows.map((row) => row.invoice.reference).join(", ")
                        : "nothing in this bucket"}
                    </span>
                  </div>
                ))}
              </div>
            </Band>
            <Band tint title="What was billed for" note="Every line on an issued invoice, grouped by kind.">
              <div className="hd-stack">
                {kinds.map((kind) => (
                  <div className="hd-row hd-row--quiet" key={kind.kind}>
                    <div className="hd-row__text">
                      <span className="hd-row__title">{kind.label}</span>
                      <span className="hd-row__note">{kind.lines} {kind.lines === 1 ? "line" : "lines"} · {Math.round((kind.total / billed) * 100)}% of billing</span>
                    </div>
                    <span className="hd-tile__value">{formatPounds(kind.total)}</span>
                  </div>
                ))}
              </div>
            </Band>
          </div>
        </div>

      </>
    );
  }

  Object.assign(window, { InvoicesScreen, InvoiceScreen, PaymentsScreen, RevenueScreen });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="BusinessScreens.jsx">
(function(){
/* Business — the contracts behind the prices, the chasing of what is owed, and
   whether an episode paid for itself.
   Commit model: INSTANT on all three. Logging a chase needs a note and an
   outcome, so it opens a staged dialog from App.jsx; nothing else here commits.

   These three screens exist to close the loop Finance opens: Contracts says what
   a payer agreed to pay, Credit control says who has not paid it, and Cost and
   margin says whether the price covered the care. Every figure divides an array
   metrics.js owns, so none of them can disagree with the invoice it came from. */
(() => {
  const {
    Button, Badge, Avatar, Select, Alert, AlertTitle, AlertDescription,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableFooter,
  } = window.OriginDesignSystem_7c998b;

  /* ── Contracts and tariffs ───────────────────────────────────────────── */
  function ContractsScreen({ data, now, viewer, onNavigate, onExport }) {
    const [payer, setPayer] = React.useState("all");
    const findings = offContractLines(data);
    const expiring = contractsExpiring(data.contracts, now);
    const priced = data.contractPrices.length;
    const cover = data.tariffItems.length * data.contracts.length;
    const items = payer === "all" ? data.tariffItems : data.tariffItems.filter((item) => data.contractPrices.some((p) => p.payer === payer && p.itemId === item.id));

    return (
      <>
        <PageHeader
          path="/contracts"
          title="Contracts and tariffs"
          sub={"What each payer has agreed to pay for each thing the hospital bills. An invoice line is checked against this at render, so a price billed off contract is a finding rather than a difference nobody sees."}
          actions={<ExportMenu scope={priced + " contracted prices across " + data.contracts.length + " contracts"} onExport={(format) => onExport("Contracts and tariffs", "Every contracted price", priced, format)} />}
        />
        <Figures items={[
          { label: "Live contracts", value: data.contracts.filter((c) => c.status === "live").length, qualifier: expiring.length ? expiring.length + " expiring within 30 days" : "none expiring within 30 days" },
          { label: "Contracted prices", value: priced, qualifier: "of " + cover + " possible payer and item pairs — the rest are not covered" },
          { label: "Lines off contract", value: findings.length, qualifier: findings.filter((f) => f.kind === "no_price").length + " with no contracted price, " + findings.filter((f) => f.kind === "different").length + " priced differently" },
          { label: "Billed off contract", value: formatPounds(findings.reduce((n, f) => n + (f.kind === "no_price" ? lineTotal(f.line) : Math.abs(f.difference) * f.line.qty), 0)), qualifier: "the value at risk if a payer queries these lines" },
        ]} />

        {expiring.length ? (
          <div className="hd-alertwrap">
            <Alert variant="warning">
              <AlertTitle>{expiring.length === 1 ? "One contract expires within 30 days" : expiring.length + " contracts expire within 30 days"}</AlertTitle>
              <AlertDescription>
                {expiring.map((c) => c.name + " — " + CONTRACT_FLAGS.daysLeft(c, now) + " days left, " + contractPricesFor(data, c.payer).length + " prices under it").join(" · ")}. Renewal is negotiated before the end date; prices stop applying the day after.
              </AlertDescription>
            </Alert>
          </div>
        ) : null}

        <Band title={"Contracts (" + data.contracts.length + ")"} note="One per payer. The terms here decide the due date every invoice to that payer is given.">
          <div className="hd-split">
            {data.contracts.map((contract) => {
              const prices = contractPricesFor(data, contract.payer);
              const invoices = data.invoices.filter((i) => i.payer === contract.payer && INVOICE_FLAGS.billable(i));
              const soon = CONTRACT_FLAGS.expiringSoon(contract, now);
              return (
                <div className={"hd-tile" + (soon ? "" : " hd-tile--tint")} key={contract.id}>
                  <span className="hd-tile__label">{labelOf(PAYERS, contract.payer)}</span>
                  <span className="hd-person__name">{contract.name}</span>
                  <span className="hd-actions__row" style={{ marginBlock: "var(--space-1)" }}>
                    <StatusBadge values={CONTRACT_STATUS} value={contract.status} />
                    {soon ? <Badge size="sm" variant="warning">{CONTRACT_FLAGS.daysLeft(contract, now)} days left</Badge> : null}
                  </span>
                  <span className="hd-footnote">
                    {contract.dueDays ? "Payment terms " + contract.dueDays + " days" : "Payable before care"} · {prices.length} contracted {prices.length === 1 ? "price" : "prices"} · {invoices.length} issued {invoices.length === 1 ? "invoice" : "invoices"} worth {formatPounds(invoices.reduce((n, i) => n + invoiceTotal(i), 0))}
                  </span>
                  <span className="hd-footnote">
                    {contract.expiresAt ? "Runs to " + formatWhen(contract.expiresAt, now) : "No end date"} · signed {formatWhen(contract.signedAt, now)}
                  </span>
                  <span className="hd-footnote">{contract.note}</span>
                  <Button variant="link" size="xs" onClick={() => setPayer(contract.payer)}>Show its prices</Button>
                </div>
              );
            })}
          </div>
        </Band>

        <Band title="The price list" note="A blank cell is a price the contract does not carry — billing that item to that payer is off contract."
          actions={
            <span className="hd-filters__select">
              <Select size="sm" aria-label="Payer" items={[{ value: "all", label: "Every payer" }].concat(PAYERS)} value={payer} onValueChange={setPayer} />
            </span>
          }>
          <div className="hd-tablewrap">
            <table className="hd-matrix">
              <thead>
                <tr>
                  <th scope="col">Item</th>
                  <th scope="col">Code</th>
                  <th scope="col">Kind</th>
                  {(payer === "all" ? PAYERS : PAYERS.filter((p) => p.value === payer)).map((p) => <th scope="col" key={p.value} style={{ textAlign: "right" }}>{p.label}</th>)}
                </tr>
              </thead>
              <tbody>
                {items.map((item) => (
                  <tr key={item.id}>
                    <th scope="row" style={{ fontWeight: "var(--font-weight-medium)", color: "var(--foreground)" }}>{item.label}</th>
                    <td className="hd-footnote" style={{ fontFamily: "var(--font-mono)" }}>{item.code}</td>
                    <td>{labelOf(BILL_LINE_KINDS, item.kind)}</td>
                    {(payer === "all" ? PAYERS : PAYERS.filter((p) => p.value === payer)).map((p) => {
                      const price = contractedPrice(data, p.value, item.label);
                      return <td key={p.value} className="hd-num">{price === null ? <span className="hd-muted">—</span> : formatPounds(price)}</td>;
                    })}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          {payer !== "all" ? <p className="hd-footnote" style={{ marginTop: "var(--space-3)" }}>Showing the {items.length} items {labelOf(PAYERS, payer)} carries a price for, of {data.tariffItems.length} in the tariff. <button type="button" className="hd-linkbtn" onClick={() => setPayer("all")}>Show every payer</button></p> : null}
        </Band>

        <Band flush title={"Lines billed off contract (" + findings.length + ")"} note="Every invoice line whose unit price is not the contracted one, or whose item the contract does not price at all.">
          {findings.length ? (
            <TableWrap>
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead>Invoice</TableHead>
                    <TableHead>Line</TableHead>
                    <TableHead>Payer</TableHead>
                    <TableHead className="hd-num">Billed</TableHead>
                    <TableHead className="hd-num">Contract</TableHead>
                    <TableHead>Finding</TableHead>
                    <TableHead>Action</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {findings.map((finding, index) => (
                    <TableRow key={finding.invoice.id + index}>
                      <TableCell><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/invoices/" + finding.invoice.id)}>{finding.invoice.reference}</button></TableCell>
                      <TableCell>
                        <span className="hd-person__text">
                          <span className="hd-person__name">{finding.line.label}</span>
                          <span className="hd-person__meta">{patientName(data.patients, finding.invoice.patientId)} · {finding.line.qty} × {formatPoundsExact(finding.line.unitMinor)}</span>
                        </span>
                      </TableCell>
                      <TableCell>{labelOf(PAYERS, finding.invoice.payer)}</TableCell>
                      <TableCell className="hd-num">{formatPoundsExact(finding.line.unitMinor)}</TableCell>
                      <TableCell className="hd-num">{finding.price === null ? <span className="hd-muted">No price</span> : formatPoundsExact(finding.price)}</TableCell>
                      <TableCell>
                        {finding.kind === "no_price"
                          ? <Badge size="sm" variant="warning">Not in the contract</Badge>
                          : <Badge size="sm" variant="warning">{finding.difference > 0 ? "Over by " : "Under by "}{formatPoundsExact(Math.abs(finding.difference))}</Badge>}
                      </TableCell>
                      <TableCell><Button variant="outline" size="sm" onClick={() => onNavigate("/invoices/" + finding.invoice.id)}>Open invoice</Button></TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </TableWrap>
          ) : <p className="hd-footnote" style={{ paddingInline: "var(--card-pad)" }}>Every invoice line is priced exactly as its payer's contract says.</p>}
        </Band>
      </>
    );
  }

  /* ── Credit control ──────────────────────────────────────────────────── */
  function CreditControlScreen({ data, now, viewer, onNavigate, onChase, onExport }) {
    const rows = creditWorklist(data, now);
    const overdue = rows.filter((r) => r.overdue);
    const promised = rows.filter((r) => r.promised > 0);
    const failed = rows.filter((r) => r.failed.length);
    const disputed = invoicesDisputed(data.invoices);
    const outstanding = rows.reduce((n, r) => n + r.balance, 0);

    return (
      <>
        <PageHeader
          path="/credit-control"
          title="Credit control"
          sub={"Every issued invoice with a balance, in due-date order. A disputed invoice is not chased — the query is answered first — so the " + disputed.length + " disputed " + (disputed.length === 1 ? "invoice is" : "invoices are") + " not in this list."}
          actions={<ExportMenu scope={rows.length + " invoices with a balance"} onExport={(format) => onExport("Credit control worklist", "Issued invoices with a balance", rows.length, format)} />}
        />
        <Figures items={[
          { label: "Chaseable balance", value: formatPounds(outstanding), qualifier: "across " + rows.length + (rows.length === 1 ? " invoice" : " invoices") + " with money still to come" },
          { label: "Past due", value: formatPounds(overdue.reduce((n, r) => n + r.balance, 0)), qualifier: overdue.length ? overdue.length + (overdue.length === 1 ? " invoice, oldest " : " invoices, oldest ") + formatCountdown(overdue[0].invoice.dueAt, now) : "nothing in this list is past its due date" },
          { label: "Payment promised", value: formatPounds(promised.reduce((n, r) => n + r.promised, 0)), qualifier: promised.length ? "advised by " + promised.map((r) => labelOf(PAYERS, r.invoice.payer)).join(", ") + " and not yet cleared" : "nobody has promised a payment" },
          { label: "Returned payments", value: failed.length, qualifier: failed.length ? failed.map((r) => r.invoice.reference).join(", ") + " — the money came back" : "no payment has been returned" },
        ]} />

        {failed.length ? (
          <div className="hd-alertwrap">
            <Alert variant="error">
              <AlertTitle>{failed.length === 1 ? "One payment was returned" : failed.length + " payments were returned"}</AlertTitle>
              <AlertDescription>{failed.map((r) => r.invoice.reference + " — " + r.failed.map((p) => formatPoundsExact(p.amountMinor) + ", " + (p.note || "no reason recorded")).join("; ")).join(" · ")}</AlertDescription>
            </Alert>
          </div>
        ) : null}

        <Band title={"Worklist (" + rows.length + ")"} note="The next step comes from the chase ladder: reminder, statement, call, final notice, then the debt agency.">
          {rows.length ? (
            <div className="hd-rows">
              {rows.map((row) => {
                const patient = patientById(data.patients, row.invoice.patientId);
                return (
                  <div className="hd-row" key={row.invoice.id}>
                    <div className="hd-row__text">
                      <span className="hd-row__title">
                        <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/invoices/" + row.invoice.id)}>{row.invoice.reference}</button>
                        {" · "}{formatPoundsExact(row.balance)} outstanding
                      </span>
                      <span className="hd-row__note">
                        <PersonCell name={patient.name} avatar={patient.avatar} size={22} onOpen={() => onNavigate("/patients/" + patient.id)} />
                      </span>
                      <span className="hd-row__note">
                        {labelOf(PAYERS, row.invoice.payer)} · {contractDueDays(data.contracts, row.invoice.payer)}-day terms · due {formatWhen(row.invoice.dueAt, now)}
                        {" · "}<span className={row.overdue ? "hd-warn" : ""}>{formatCountdown(row.invoice.dueAt, now)}</span>
                        {row.last ? " · last contact " + labelOf(CHASE_STEPS, row.last.step).toLowerCase() + " " + formatWhen(row.last.at, now) + ", " + labelOf(CHASE_OUTCOMES, row.last.outcome).toLowerCase() : " · never chased"}
                      </span>
                      {row.promised ? <span className="hd-row__note">{formatPoundsExact(row.promised)} advised and not cleared.</span> : null}
                    </div>
                    <span className="hd-actions__row">
                      <StatusBadge values={INVOICE_STATUS} value={row.invoice.status} />
                      {row.overdue ? <Badge size="sm" variant="warning">Past due</Badge> : null}
                      <Badge size="sm" variant="secondary">{row.chases.length} {row.chases.length === 1 ? "contact" : "contacts"}</Badge>
                    </span>
                    <Transitions transitions={chaseTransitions(row, viewer)} onRun={(t) => onChase(row, t)} />
                  </div>
                );
              })}
            </div>
          ) : <p className="hd-footnote">Nothing to chase. Every issued invoice is either settled or disputed.</p>}
        </Band>

        <Band tint flush title={"Contact history (" + data.chases.length + ")"} note="Every chase against every invoice, newest first.">
          {data.chases.length ? (
            <TableWrap>
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead>When</TableHead>
                    <TableHead>Invoice</TableHead>
                    <TableHead>Step</TableHead>
                    <TableHead>Outcome</TableHead>
                    <TableHead>What was said</TableHead>
                    <TableHead>By</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {data.chases.slice().sort((a, b) => b.at - a.at).map((chase) => {
                    const invoice = data.invoices.find((i) => i.id === chase.invoiceId);
                    return (
                      <TableRow key={chase.id}>
                        <TableCell><Clock at={chase.at} now={now} /></TableCell>
                        <TableCell><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/invoices/" + chase.invoiceId)}>{invoice ? invoice.reference : chase.invoiceId}</button></TableCell>
                        <TableCell>{labelOf(CHASE_STEPS, chase.step)}</TableCell>
                        <TableCell>{labelOf(CHASE_OUTCOMES, chase.outcome)}</TableCell>
                        <TableCell style={{ whiteSpace: "normal", maxWidth: "26rem" }}>{chase.note}</TableCell>
                        <TableCell>{staffName(data.staff, chase.byId)}</TableCell>
                      </TableRow>
                    );
                  })}
                </TableBody>
              </Table>
            </TableWrap>
          ) : <p className="hd-footnote" style={{ paddingInline: "var(--card-pad)" }}>No payer has been contacted yet.</p>}
        </Band>
      </>
    );
  }

  /* ── Cost and margin per episode ─────────────────────────────────────── */
  function MarginScreen({ data, now, viewer, onNavigate, onExport }) {
    const rows = marginRows(data, now);
    const kinds = costByKind(data, now);
    const billed = rows.reduce((n, r) => n + r.billed, 0);
    const cost = rows.reduce((n, r) => n + r.cost, 0);
    const uncosted = rows.reduce((n, r) => n + r.uncosted, 0);
    const thin = rows.filter((r) => r.rate < 0.15);
    const losing = rows.filter((r) => r.margin < 0);

    return (
      <>
        <PageHeader
          path="/margin"
          title="Cost and margin"
          sub={"What each issued episode was billed against what it cost to deliver. Bed days at the ward's own day cost, procedures at their planned theatre minutes, drug courses from the order's frequency — " + COST_RATES.source + "."}
          actions={<ExportMenu scope={rows.length + " issued episodes with a cost"} onExport={(format) => onExport("Cost and margin", "Issued episodes as at " + new Date(now).toLocaleDateString("en-GB", { day: "numeric", month: "short" }), rows.length, format)} />}
        />
        <Figures items={[
          { label: "Billed", value: formatPounds(billed), qualifier: "across " + rows.length + " issued " + (rows.length === 1 ? "episode" : "episodes") },
          { label: "Cost to deliver", value: formatPounds(cost), qualifier: Math.round((cost / billed) * 100) + "% of what was billed" },
          { label: "Margin", value: formatPounds(billed - cost), qualifier: Math.round(((billed - cost) / billed) * 100) + "% over the same episodes" },
          { label: "Billing with no cost attached", value: formatPounds(uncosted), qualifier: "lines that name no record, so nothing can be costed against them" },
        ]} />

        {thin.length ? (
          <div className="hd-alertwrap">
            <Alert variant="warning">
              <AlertTitle>{thin.length === 1 ? "One episode is under a 15% margin" : thin.length + " episodes are under a 15% margin"}</AlertTitle>
              <AlertDescription>
                {thin.map((r) => r.invoice.reference + " — " + Math.round(r.rate * 100) + "% on " + formatPounds(r.billed) + " billed").join(" · ")}.
                {losing.length ? " " + losing.length + " of them cost more than they were billed." : " None of them cost more than they were billed."}
              </AlertDescription>
            </Alert>
          </div>
        ) : null}

        <div className="hd-cols hd-cols--wide">
          <div className="hd-stack">
            <Band flush title={"By episode (" + rows.length + ")"} note="Thinnest margin first. An episode is one issued invoice and the care its lines name.">
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead>Invoice</TableHead>
                      <TableHead>Patient</TableHead>
                      <TableHead className="hd-num">Billed</TableHead>
                      <TableHead className="hd-num">Cost</TableHead>
                      <TableHead className="hd-num">Margin</TableHead>
                      <TableHead style={{ minWidth: "8rem" }}>Margin rate</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((row) => {
                      const patient = patientById(data.patients, row.invoice.patientId);
                      return (
                        <TableRow key={row.invoice.id}>
                          <TableCell>
                            <span className="hd-person__text">
                              <span className="hd-person__name"><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/invoices/" + row.invoice.id)}>{row.invoice.reference}</button></span>
                              <span className="hd-person__meta">{labelOf(PAYERS, row.invoice.payer)}{row.uncosted ? " · " + formatPounds(row.uncosted) + " uncosted" : ""}</span>
                            </span>
                          </TableCell>
                          <TableCell><PersonCell name={patient.name} avatar={patient.avatar} size={24} onOpen={() => onNavigate("/patients/" + patient.id)} /></TableCell>
                          <TableCell className="hd-num">{formatPounds(row.billed)}</TableCell>
                          <TableCell className="hd-num">{formatPounds(row.cost)}</TableCell>
                          <TableCell className="hd-num">{formatPounds(row.margin)}</TableCell>
                          <TableCell>
                            <span style={{ display: "flex", flexDirection: "column", gap: 4, minWidth: "7rem" }}>
                              <span className="hd-footnote">{Math.round(row.rate * 100)}%</span>
                              <StockBar percent={Math.max(0, row.rate * 100)} tone={row.rate < 0.15 ? "warn" : null} />
                            </span>
                          </TableCell>
                        </TableRow>
                      );
                    })}
                  </TableBody>
                  <TableFooter>
                    <TableRow>
                      <TableCell colSpan={2}>All issued episodes</TableCell>
                      <TableCell className="hd-num">{formatPounds(billed)}</TableCell>
                      <TableCell className="hd-num">{formatPounds(cost)}</TableCell>
                      <TableCell className="hd-num">{formatPounds(billed - cost)}</TableCell>
                      <TableCell className="hd-num">{Math.round(((billed - cost) / billed) * 100)}%</TableCell>
                    </TableRow>
                  </TableFooter>
                </Table>
              </TableWrap>
            </Band>
          </div>
          <div className="hd-stack">
            <Band title="Where the cost sits" note="Every line on an issued invoice, grouped by kind, billed against costed.">
              <div className="hd-stack">
                {kinds.map((kind) => (
                  <div className="hd-tile" key={kind.kind}>
                    <span className="hd-tile__label">{kind.label}</span>
                    <span className="hd-tile__value">{formatPounds(kind.margin)}</span>
                    <StockBar percent={kind.billed ? (kind.cost / kind.billed) * 100 : 0} tone={kind.billed && kind.cost / kind.billed > 0.85 ? "warn" : null} />
                    <span className="hd-footnote">
                      {formatPounds(kind.billed)} billed · {formatPounds(kind.cost)} cost · {kind.lines} {kind.lines === 1 ? "line" : "lines"}
                      {kind.cost === 0 ? " · nothing costed against it" : ""}
                    </span>
                  </div>
                ))}
              </div>
            </Band>
            <Band tint title="The cost model" note="Stated here so a figure above can be argued with.">
              <div className="hd-stack">
                {WARDS.map((ward) => (
                  <div className="hd-row hd-row--quiet" key={ward.value}>
                    <div className="hd-row__text">
                      <span className="hd-row__title">{wardName(ward.value)}</span>
                      <span className="hd-row__note">Cost of one bed day on this ward</span>
                    </div>
                    <span className="hd-tile__value">{formatPounds(COST_RATES.bedDayByWard[ward.value])}</span>
                  </div>
                ))}
                <div className="hd-row hd-row--quiet">
                  <div className="hd-row__text">
                    <span className="hd-row__title">Theatre</span>
                    <span className="hd-row__note">Cost of one theatre minute, staffed</span>
                  </div>
                  <span className="hd-tile__value">{formatPoundsExact(COST_RATES.theatreMinute)}</span>
                </div>
                <div className="hd-row hd-row--quiet">
                  <div className="hd-row__text">
                    <span className="hd-row__title">Drugs</span>
                    <span className="hd-row__note">Doses in the course × the formulary unit cost</span>
                  </div>
                  <span className="hd-tile__value"><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/formulary")}>Formulary</button></span>
                </div>
              </div>
            </Band>
          </div>
        </div>
      </>
    );
  }

  Object.assign(window, { ContractsScreen, CreditControlScreen, MarginScreen });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="FinanceCommits.jsx">
(function(){
/* Finance and credit control — every commit money makes.
   Kept out of App.jsx so the router stays readable: this file holds the invoice
   ladder (code, issue, pay, dispute, resolve, write off), the chase ladder, and
   the two staged dialogs an instant screen opens.

   The factory is given the commit machinery it needs and nothing else. Two rules
   it exists to keep:
     · No total is ever written. A commit writes a status, a date or a payment
       row; metrics.js sums the rest, so no figure can drift from its invoice.
     · A due date comes from the payer's contracted terms, never a constant.

   Reversibility (AGENTS §3): everything here applies immediately with an undo
   toast, except writing a balance off — irreversible, so it confirms — and,
   above the hospital's write-off threshold, it is not offered at all: it becomes
   a request for approval instead. */
function makeFinanceCommits(ctx) {
  const data = ctx.data, setData = ctx.setData, patch = ctx.patch, restore = ctx.restore;
  const logActivity = ctx.logActivity, unlogActivity = ctx.unlogActivity, pushToast = ctx.pushToast;
  const setForm = ctx.setForm, setConfirm = ctx.setConfirm, viewer = ctx.viewer, now = ctx.now;
  const setPath = ctx.setPath, billing = ctx.billing;

  /* ── Finance ───────────────────────────────────────────────────────────
     An invoice's total and balance are never stored, so a commit here only ever
     writes a status, a date or a payment row — and every figure that divides
     them moves on the next render. */
  /* The due date is the payer's contracted terms, never a constant here. */
  const dueDateFor = (invoice) => now + contractDueDays(data.contracts, invoice.payer) * DAY_MS;

  const issueInvoice = (invoice) => {
    const total = invoiceTotal(invoice);
    const due = dueDateFor(invoice);
    patch("invoices", invoice.id, { status: "issued", issuedAt: now, dueAt: due, codedById: viewer.id });
    const act = logActivity("issued an invoice", { type: "invoice", id: invoice.id }, formatPounds(total) + " to " + labelOf(PAYERS, invoice.payer));
    pushToast({
      variant: "success", title: "Invoice issued",
      description: invoice.reference + " · " + formatPounds(total) + " to " + labelOf(PAYERS, invoice.payer) + " · " + contractDueDays(data.contracts, invoice.payer) + "-day terms, due " + formatWhen(due, now),
      onUndo: () => { restore("invoices", invoice); unlogActivity(act); },
    });
  };

  const bulkIssue = (invoices) => {
    const snapshot = data.invoices;
    const ids = invoices.map((i) => i.id);
    setData((d) => Object.assign({}, d, {
      invoices: d.invoices.map((i) => (ids.indexOf(i.id) > -1 ? Object.assign({}, i, { status: "issued", issuedAt: now, dueAt: now + contractDueDays(d.contracts, i.payer) * DAY_MS, codedById: viewer.id }) : i)),
    }));
    pushToast({
      variant: "success",
      title: invoices.length + (invoices.length === 1 ? " invoice issued" : " invoices issued"),
      description: formatPounds(invoices.reduce((n, i) => n + invoiceTotal(i), 0)) + " asked for across " + invoices.map((i) => labelOf(PAYERS, i.payer)).reduce((list, p) => (list.indexOf(p) > -1 ? list : list.concat([p])), []).length + " payers · each due on its own contracted terms",
      onUndo: () => setData((d) => Object.assign({}, d, { invoices: snapshot })),
    });
  };

  /* Recording money needs three decisions, so this instant screen opens a staged
     dialog. The payment is written as CLEARED, which is what moves the balance —
     and the invoice's status follows the balance rather than being typed. */
  const recordPayment = (invoice) => {
    const openInvoices = data.invoices.filter((i) => INVOICE_FLAGS.billable(i) && invoiceBalance(i, data.payments) > 0);
    const target = invoice || openInvoices[0];
    if (!target) {
      return pushToast({ title: "Nothing to pay", description: "Every issued invoice is settled; there is no balance to record money against." });
    }
    setForm({
      title: invoice ? "Record a payment against " + invoice.reference : "Record a payment",
      description: "Money recorded here is cleared money: it moves the invoice's balance immediately. Advise a payment that has not cleared by leaving it out until it does.",
      submitLabel: "Record payment",
      initial: { invoice: target.id, amount: (invoiceBalance(target, data.payments) / 100).toFixed(2), method: target.payer === "nhs_commissioner" ? "remittance" : "bank_transfer", reference: "" },
      fields: [
        invoice ? null : { id: "invoice", kind: "select", label: "Invoice", options: openInvoices.map((i) => ({ value: i.id, label: i.reference + " — " + patientName(data.patients, i.patientId) + " · " + formatPounds(invoiceBalance(i, data.payments)) + " outstanding" })) },
        { id: "amount", kind: "input", label: "Amount in pounds", hint: "The balance on " + target.reference + " is " + formatPoundsExact(invoiceBalance(target, data.payments)) + "." },
        { id: "method", kind: "select", label: "How it arrived", options: PAYMENT_METHODS },
        { id: "reference", kind: "input", label: "Remittance or transfer reference", placeholder: "REM-88231" },
      ].filter(Boolean),
      validate: (v) => {
        const errors = {};
        const chosen = data.invoices.find((i) => i.id === (invoice ? invoice.id : v.invoice));
        const balance = chosen ? invoiceBalance(chosen, data.payments) : 0;
        const minor = Math.round(Number(v.amount) * 100);
        if (!(minor > 0)) errors.amount = "Enter an amount in pounds, like 1250.00.";
        else if (minor > balance) errors.amount = "That is more than the " + formatPoundsExact(balance) + " outstanding on " + (chosen ? chosen.reference : "the invoice") + ".";
        if (!v.reference || v.reference.trim().length < 4) errors.reference = "Give the reference the payer used.";
        return errors;
      },
      run: (v) => {
        const chosen = data.invoices.find((i) => i.id === (invoice ? invoice.id : v.invoice));
        const minor = Math.round(Number(v.amount) * 100);
        const id = "pay_" + Math.random().toString(36).slice(2, 6);
        const payment = {
          id: id, invoiceId: chosen.id, payer: chosen.payer, method: v.method, status: "cleared",
          amountMinor: minor, receivedAt: now, reference: v.reference.trim(), byId: viewer.id, note: null,
        };
        const settled = invoiceBalance(chosen, data.payments) - minor <= 0;
        setData((d) => Object.assign({}, d, {
          payments: [payment].concat(d.payments),
          invoices: d.invoices.map((i) => (i.id === chosen.id ? Object.assign({}, i, { status: settled ? "paid" : "part_paid" }) : i)),
          activity: [{ id: "act_" + id, actorId: viewer.id, at: now, verb: "recorded a payment", record: { type: "invoice", id: chosen.id }, note: labelOf(PAYMENT_METHODS, v.method) + " " + v.reference.trim() }].concat(d.activity),
        }));
        pushToast({
          variant: "success", title: settled ? "Invoice settled" : "Payment recorded",
          description: formatPoundsExact(minor) + " against " + chosen.reference + " · " + (settled ? "nothing outstanding" : formatPoundsExact(invoiceBalance(chosen, data.payments) - minor) + " still outstanding"),
          onUndo: () => setData((d) => Object.assign({}, d, {
            payments: d.payments.filter((p) => p.id !== id),
            invoices: d.invoices.map((i) => (i.id === chosen.id ? chosen : i)),
            activity: d.activity.filter((a) => a.id !== "act_" + id),
          })),
        });
      },
    });
  };

  const disputeInvoice = (invoice) => setForm({
    title: "Mark " + invoice.reference + " disputed",
    description: "This records that the payer has queried the bill. It stops the invoice being chased and shows the query beside it until somebody answers.",
    submitLabel: "Mark disputed",
    initial: { note: "" },
    fields: [{ id: "note", kind: "text", label: "What the payer is querying", placeholder: "The commissioner queries 11 bed days against a 9-day expected stay.", hint: "Written onto the invoice and shown to anybody who opens it." }],
    validate: (v) => (!v.note || v.note.trim().length < 12 ? { note: "Record the query — a sentence is enough." } : {}),
    run: (v) => {
      patch("invoices", invoice.id, { status: "disputed", note: v.note.trim() });
      const act = logActivity("marked an invoice disputed", { type: "invoice", id: invoice.id }, null);
      pushToast({
        title: "Marked disputed",
        description: invoice.reference + " · " + formatPounds(invoiceBalance(invoice, data.payments)) + " held behind the query",
        onUndo: () => { restore("invoices", invoice); unlogActivity(act); },
      });
    },
  });

  const onInvoiceTransition = (invoice, transition) => {
    const balance = invoiceBalance(invoice, data.payments);
    if (transition.id === "send_for_coding") {
      patch("invoices", invoice.id, { status: "awaiting_coding" });
      return pushToast({
        title: "Sent for coding",
        description: invoice.reference + " · " + formatPounds(invoiceTotal(invoice)) + " over " + invoice.lines.length + (invoice.lines.length === 1 ? " line" : " lines") + " · a coder checks it before it is issued",
        onUndo: () => restore("invoices", invoice),
      });
    }
    if (transition.id === "issue") return issueInvoice(invoice);
    if (transition.id === "record_payment") return recordPayment(invoice);
    if (transition.id === "dispute") return disputeInvoice(invoice);
    if (transition.id === "resolve") {
      const paid = invoicePaid(data.payments, invoice.id);
      patch("invoices", invoice.id, { status: paid > 0 ? "part_paid" : "issued", note: null });
      const act = logActivity("resolved a dispute", { type: "invoice", id: invoice.id }, null);
      return pushToast({
        variant: "success", title: "Dispute resolved",
        description: invoice.reference + " is being chased again · " + formatPounds(balance) + " outstanding",
        onUndo: () => { restore("invoices", invoice); unlogActivity(act); },
      });
    }
    if (transition.id === "write_off") {
      /* Above the hospital's threshold this is not a local decision (Settings →
         Billing), so the action becomes a request for approval instead. */
      if (billing && balance > billing.writeOffThresholdMinor) {
        return setForm({
          title: "Request approval to write off " + formatPoundsExact(balance),
          description: "The threshold for writing a balance off here is " + formatPounds(billing.writeOffThresholdMinor) + ". Above it, " + billing.approver.toLowerCase() + " approval is needed, so this records the request rather than writing the balance off.",
          submitLabel: "Request approval",
          initial: { note: "" },
          fields: [{ id: "note", kind: "text", label: "Why it should be written off", placeholder: "Chased four times over eleven weeks; the payer has been dissolved.", hint: "Sent to " + billing.approver.toLowerCase() + " with the invoice and its contact history." }],
          validate: (v) => (!v.note || v.note.trim().length < 12 ? { note: "Say why — a sentence is enough." } : {}),
          run: (v) => {
            const id = "cmt_" + Math.random().toString(36).slice(2, 7);
            setData((d) => Object.assign({}, d, {
              comments: d.comments.concat([{ id: id, record: { type: "invoice", id: invoice.id }, authorId: viewer.id, at: now, body: "Write-off requested for " + formatPoundsExact(balance) + ": " + v.note.trim(), mentions: [], reactions: {} }]),
              activity: [{ id: "act_" + id, actorId: viewer.id, at: now, verb: "requested a write-off", record: { type: "invoice", id: invoice.id }, note: formatPounds(balance) }].concat(d.activity),
            }));
            pushToast({
              title: "Approval requested",
              description: invoice.reference + " · " + formatPoundsExact(balance) + " · with " + billing.approver.toLowerCase() + "; the balance is still owed until they answer",
              onUndo: () => setData((d) => Object.assign({}, d, {
                comments: d.comments.filter((c) => c.id !== id),
                activity: d.activity.filter((x) => x.id !== "act_" + id),
              })),
            });
          },
        });
      }
      return setConfirm({
        title: "Write off " + formatPoundsExact(balance) + " on " + invoice.reference + "?",
        description: "The balance stops being owed and the invoice is closed against " + labelOf(PAYERS, invoice.payer) + ". It cannot be undone, and the amount leaves every revenue figure that counts money still expected.",
        confirmLabel: "Write off " + formatPounds(balance),
        run: () => {
          patch("invoices", invoice.id, { status: "written_off", note: "Balance of " + formatPoundsExact(balance) + " written off by " + viewer.name + "." });
          logActivity("wrote off a balance", { type: "invoice", id: invoice.id }, formatPounds(balance));
          pushToast({ title: "Written off", description: invoice.reference + " · " + formatPoundsExact(balance) + " is no longer expected" });
        },
      });
    }
  };

  /* A new invoice starts as a draft with the one line the biller can state. More
     lines are coded onto it before it is issued — which is why the coding step
     exists and why nothing is invented here. */
  const newInvoice = () => setForm({
    title: "New invoice",
    description: "It starts as a draft with one line. Coding adds the rest from the care record, and only an issued invoice asks anybody for money.",
    submitLabel: "Create draft",
    initial: { patient: data.patients[0].id, payer: billing.defaultPayer, label: "", kind: "procedure", qty: "1", unit: "" },
    fields: [
      { id: "patient", kind: "select", label: "Patient", options: data.patients.map((p) => ({ value: p.id, label: p.name + " — " + p.number })) },
      { id: "payer", kind: "select", label: "Who is billed", options: PAYERS },
      { id: "label", kind: "input", label: "What is billed", placeholder: "Laparoscopic cholecystectomy" },
      { id: "kind", kind: "select", label: "Kind of line", options: BILL_LINE_KINDS },
      { id: "qty", kind: "input", label: "Quantity", hint: "Bed days, doses or one for a procedure." },
      { id: "unit", kind: "input", label: "Unit price in pounds", placeholder: "2940.00", hint: "If the payer's contract prices this item, use that price — Contracts and tariffs flags anything billed off contract." },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.label || v.label.trim().length < 4) errors.label = "Say what is being billed.";
      if (!/^\d+$/.test(v.qty || "") || Number(v.qty) < 1) errors.qty = "A whole number, one or more.";
      if (!(Math.round(Number(v.unit) * 100) > 0)) errors.unit = "Enter a unit price in pounds, like 2940.00.";
      return errors;
    },
    run: (v) => {
      const id = "inv_" + Math.random().toString(36).slice(2, 6);
      const invoice = {
        id: id, patientId: v.patient, payer: v.payer, status: "draft",
        reference: billing.referencePrefix + "-0" + Math.floor(352 + Math.random() * 40), issuedAt: null, dueAt: null, codedById: null,
        note: "Raised by " + viewer.name + "; one line so far.",
        lines: [{ label: v.label.trim(), kind: v.kind, ref: null, qty: Number(v.qty), unitMinor: Math.round(Number(v.unit) * 100) }],
      };
      setData((d) => Object.assign({}, d, { invoices: [invoice].concat(d.invoices) }));
      setPath("/invoices/" + id);
      pushToast({
        variant: "success", title: "Draft invoice created",
        description: invoice.reference + " · " + formatPounds(invoiceTotal(invoice)) + " for " + patientName(data.patients, v.patient) + " · draft, so nobody has been asked for anything",
        onUndo: () => { setData((d) => Object.assign({}, d, { invoices: d.invoices.filter((i) => i.id !== id) })); setPath("/invoices"); },
      });
    },
  });

  /* ── Credit control ────────────────────────────────────────────────────
     A chase is a contact, so it needs what was said and what came of it: a
     staged dialog from an instant screen. Logging one moves the ladder, because
     the next step is derived from the last. */
  const onChase = (row, transition) => setForm({
    title: labelOf(CHASE_STEPS, transition.step) + " — " + row.invoice.reference,
    description: formatPoundsExact(row.balance) + " outstanding with " + labelOf(PAYERS, row.invoice.payer) + " on " + contractDueDays(data.contracts, row.invoice.payer) + "-day terms · " +
      (row.overdue ? formatCountdown(row.invoice.dueAt, now) : "due " + formatWhen(row.invoice.dueAt, now)) +
      (row.last ? " · last contact " + labelOf(CHASE_STEPS, row.last.step).toLowerCase() + " " + formatWhen(row.last.at, now) : " · never chased before"),
    submitLabel: "Log " + labelOf(CHASE_STEPS, transition.step).toLowerCase(),
    initial: { outcome: "awaiting", note: "" },
    fields: [
      { id: "outcome", kind: "radio", label: "What came of it", options: CHASE_OUTCOMES.map((o) => ({ value: o.value, label: o.label })) },
      { id: "note", kind: "text", label: "What was said", placeholder: "Spoke to the finance inbox; they will pay with the April remittance.", hint: "Kept on the contact history for anybody who picks this up next." },
    ],
    validate: (v) => (!v.note || v.note.trim().length < 12 ? { note: "Record what was said — a sentence is enough." } : {}),
    run: (v) => {
      const id = "chs_" + Math.random().toString(36).slice(2, 6);
      const chase = { id: id, invoiceId: row.invoice.id, step: transition.step, at: now, byId: viewer.id, outcome: v.outcome, note: v.note.trim() };
      setData((d) => Object.assign({}, d, {
        chases: d.chases.concat([chase]),
        activity: [{ id: "act_" + id, actorId: viewer.id, at: now, verb: "chased a payer", record: { type: "invoice", id: row.invoice.id }, note: labelOf(CHASE_STEPS, transition.step) }].concat(d.activity),
      }));
      const next = nextChaseStep(transition.step);
      pushToast({
        variant: v.outcome === "promised" ? "success" : undefined,
        title: labelOf(CHASE_STEPS, transition.step) + " logged",
        description: row.invoice.reference + " · " + labelOf(CHASE_OUTCOMES, v.outcome).toLowerCase() + " · " + (next ? "next step is " + labelOf(CHASE_STEPS, next.value).toLowerCase() : "the ladder ends here; the debt agency has it"),
        onUndo: () => setData((d) => Object.assign({}, d, {
          chases: d.chases.filter((c) => c.id !== id),
          activity: d.activity.filter((a) => a.id !== "act_" + id),
        })),
      });
    },
  });

  return {
    onInvoiceTransition: onInvoiceTransition,
    bulkIssue: bulkIssue,
    recordPayment: recordPayment,
    newInvoice: newInvoice,
    onChase: onChase,
    issueInvoice: issueInvoice,
  };
}
window.makeFinanceCommits = makeFinanceCommits;

})();
</script>
<script type="text/babel" data-presets="react" data-file="PeopleScreens.jsx">
(function(){
/* Doctors, employees, and one person's record.
   Two views of one staff array (metrics.js: doctorsOf, employeesOf) — never two
   lists. Doctors is the medical family; Employees is everybody with a badge, so
   somebody new is created once, on Employees, and appears in both.
   Commit model: INSTANT. Access changes apply immediately with an undo toast. */
(() => {
  const {
    Button, Badge, Avatar, Alert, AlertTitle, AlertDescription,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  } = window.OriginDesignSystem_7c998b;

  const STAFF_STATUS_FILTER = [{ value: "all", label: "All employment states" }].concat(STAFF_STATUS.map((s) => ({ value: s.id, label: s.label })));
  const SPECIALTY_FILTER = [{ value: "all", label: "All specialties" }].concat(SPECIALTIES);
  const FAMILY_FILTER = [{ value: "all", label: "All job families" }].concat(JOB_FAMILIES);

  function DoctorsScreen({ data, now, viewer, onNavigate, onStaffTransition, onExport }) {
    const [query, setQuery] = React.useState("");
    const [specialty, setSpecialty] = React.useState("all");
    const [status, setStatus] = React.useState("all");
    const { sort, onSort } = useSort("name", "asc");

    const doctors = doctorsOf(data.staff);
    const dirty = !!query || specialty !== "all" || status !== "all";
    const clear = () => { setQuery(""); setSpecialty("all"); setStatus("all"); };
    const filtered = doctors.filter((person) => {
      if (specialty !== "all" && person.specialty !== specialty) return false;
      if (status !== "all" && person.status !== status) return false;
      if (query && (person.name + " " + person.jobTitle + " " + (person.gmc || "")).toLowerCase().indexOf(query.toLowerCase()) < 0) return false;
      return true;
    });
    const rows = sortRows(filtered, sort, {
      name: (p) => p.name,
      booked: (p) => interventionsForSurgeon(data.interventions, p.id).length,
      started: (p) => p.startedAt,
    });

    return (
      <>
        <PageHeader
          path="/doctors"
          title="Doctors"
          sub={doctors.length + " of the " + data.staff.length + " people on the badge system are in the medical family. Somebody new is added on Employees."}
          actions={<ExportMenu scope={dirty ? filtered.length + " doctors matching the filters" : "All " + doctors.length + " doctors"}
            onExport={(format) => onExport("Doctors", dirty ? "Filtered doctors" : "All doctors", filtered.length, format)} />}
        />
        <Band flush>
          <FilterBar query={query} onQuery={setQuery} placeholder="Search name, grade or GMC number…" dirty={dirty} onClear={clear}
            selects={[
              { id: "specialty", label: "Specialty", items: SPECIALTY_FILTER, value: specialty, onChange: setSpecialty },
              { id: "status", label: "Employment", items: STAFF_STATUS_FILTER, value: status, onChange: setStatus },
            ]} />
          {rows.length === 0 ? <NoResults noun="doctors" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead><SortHead label="Doctor" sortKey="name" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Specialty</TableHead>
                      <TableHead>Employment</TableHead>
                      <TableHead>Base ward</TableHead>
                      <TableHead className="hd-num"><SortHead label="Procedures" sortKey="booked" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead className="hd-num">Patients as consultant</TableHead>
                      <TableHead>Extension</TableHead>
                      <TableHead>Action</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((person) => (
                      <TableRow key={person.id}>
                        <TableCell>
                          <PersonCell name={person.name} avatar={person.avatar} meta={person.jobTitle + (person.gmc ? " · " + person.gmc : "")}
                            onOpen={() => onNavigate("/doctors/" + person.id)} />
                        </TableCell>
                        <TableCell>{person.specialty ? labelOf(SPECIALTIES, person.specialty) : <span className="hd-muted">Not recorded</span>}</TableCell>
                        <TableCell>
                          <span className="hd-actions__row">
                            <StatusBadge values={STAFF_STATUS} value={person.status} />
                            {person.status === "on_leave" && person.leaveUntil ? <span className="hd-footnote">until {formatWhen(person.leaveUntil, now)}</span> : null}
                          </span>
                        </TableCell>
                        <TableCell>{person.ward ? wardName(person.ward) : <span className="hd-muted">No base ward</span>}</TableCell>
                        <TableCell className="hd-num">{interventionsForSurgeon(data.interventions, person.id).length}</TableCell>
                        <TableCell className="hd-num">{data.admissions.filter((a) => a.consultantId === person.id).length}</TableCell>
                        <TableCell>{person.phone}</TableCell>
                        <TableCell><RowActions transitions={staffTransitions(person, viewer)} onRun={(t) => onStaffTransition(person, t)} label={"Actions for " + person.name} /></TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={doctors.length} noun="doctors" />
            </>
          )}
        </Band>
      </>
    );
  }

  function EmployeesScreen({ data, now, viewer, onNavigate, onStaffTransition, onNewEmployee, onImport, onExport }) {
    const [query, setQuery] = React.useState("");
    const [family, setFamily] = React.useState("all");
    const [status, setStatus] = React.useState("all");
    const { sort, onSort } = useSort("name", "asc");

    const everybody = employeesOf(data.staff);
    const dirty = !!query || family !== "all" || status !== "all";
    const clear = () => { setQuery(""); setFamily("all"); setStatus("all"); };
    const filtered = everybody.filter((person) => {
      if (family !== "all" && person.family !== family) return false;
      if (status !== "all" && person.status !== status) return false;
      if (query && (person.name + " " + person.jobTitle + " " + person.email).toLowerCase().indexOf(query.toLowerCase()) < 0) return false;
      return true;
    });
    const rows = sortRows(filtered, sort, {
      name: (p) => p.name,
      started: (p) => p.startedAt,
      family: (p) => labelOf(JOB_FAMILIES, p.family),
    });
    const suspended = everybody.filter((p) => p.status === "suspended");

    return (
      <>
        <PageHeader
          path="/employees"
          title="Employees"
          sub={everybody.length + " people on the badge system across " + JOB_FAMILIES.length + " job families, including the " + doctorsOf(data.staff).length + " in the medical family."}
          actions={
            <>
              <ImportButton viewer={viewer} onImport={onImport} />
              <ExportMenu scope={dirty ? filtered.length + " employees matching the filters" : "All " + everybody.length + " employees"}
                onExport={(format) => onExport("Employees", dirty ? "Filtered employees" : "All employees", filtered.length, format)} />
              <Button size="sm" onClick={onNewEmployee}><Icon name="plus" />New employee</Button>
            </>
          }
        />
        {suspended.length ? (
          <div className="hd-alertwrap">
            <Alert variant="warning">
              <AlertTitle>{suspended.length === 1 ? "One account is suspended" : suspended.length + " accounts are suspended"}</AlertTitle>
              <AlertDescription>{suspended.map((p) => p.name + " — " + (p.suspendedNote || "no reason recorded")).join(" · ")}</AlertDescription>
            </Alert>
          </div>
        ) : null}
        <Band flush>
          <FilterBar query={query} onQuery={setQuery} placeholder="Search name, job title or email…" dirty={dirty} onClear={clear}
            selects={[
              { id: "family", label: "Job family", items: FAMILY_FILTER, value: family, onChange: setFamily },
              { id: "status", label: "Employment", items: STAFF_STATUS_FILTER, value: status, onChange: setStatus },
            ]} />
          {rows.length === 0 ? <NoResults noun="employees" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead><SortHead label="Employee" sortKey="name" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead><SortHead label="Job family" sortKey="family" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>What they may do</TableHead>
                      <TableHead>Employment</TableHead>
                      <TableHead>Base ward</TableHead>
                      <TableHead><SortHead label="Started" sortKey="started" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Action</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((person) => (
                      <TableRow key={person.id}>
                        <TableCell>
                          <PersonCell name={person.name} avatar={person.avatar} meta={person.jobTitle + " · " + person.email}
                            onOpen={() => onNavigate((person.family === "medical" ? "/doctors/" : "/employees/") + person.id)} />
                        </TableCell>
                        <TableCell>{labelOf(JOB_FAMILIES, person.family)}</TableCell>
                        <TableCell>{labelOf(STAFF_ROLES, person.staffRole)}</TableCell>
                        <TableCell><StatusBadge values={STAFF_STATUS} value={person.status} /></TableCell>
                        <TableCell>{person.ward ? wardName(person.ward) : <span className="hd-muted">No base ward</span>}</TableCell>
                        <TableCell>{person.startedAt > now ? "Starts " + formatWhen(person.startedAt, now) : formatWhen(person.startedAt, now)}</TableCell>
                        <TableCell><RowActions transitions={staffTransitions(person, viewer)} onRun={(t) => onStaffTransition(person, t)} label={"Actions for " + person.name} /></TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={everybody.length} noun="employees" />
            </>
          )}
        </Band>
      </>
    );
  }

  /* One record for anybody with a badge, reached from either directory. The
     route it renders under decides the breadcrumb, so a doctor opened from
     Doctors reads Doctors / their name. */
  function StaffScreen({ data, now, viewer, staffId, routePath, onNavigate, onStaffTransition, onReactToNote, onExport }) {
    const person = staffById(data.staff, staffId);
    const procedures = interventionsForSurgeon(data.interventions, person.id);
    const upcoming = procedures.filter((i) => i.status === "requested" || i.status === "scheduled" || i.status === "in_theatre");
    const asConsultant = data.admissions.filter((a) => a.consultantId === person.id);
    const openAsConsultant = openAdmissions(asConsultant);
    const written = commentsByAuthor(data.comments, person.id);
    const prescribed = data.orders.filter((o) => o.prescriberId === person.id);
    const verified = data.orders.filter((o) => o.verifiedById === person.id);

    return (
      <>
        <PageHeader path={routePath} params={{ id: person.id }} crumb={person.name} title={person.name}
          actions={<ExportMenu scope={"This record and the " + procedures.length + " procedures against it"} onExport={(format) => onExport("Staff record", person.name + " · " + person.jobTitle, 1, format)} />} />

        <div className="hd-record">
          <div className="hd-record__id">
            <Avatar size={72} src={person.avatar} alt={"Photograph of " + person.name} fallback={initialsOf(person.name)} />
            <div className="hd-record__text">
              <span className="hd-record__name">{person.name}</span>
              <span className="hd-record__meta">
                <span>{person.jobTitle}</span>
                <span>{labelOf(JOB_FAMILIES, person.family)}</span>
                <StatusBadge values={STAFF_STATUS} value={person.status} />
                {person.id === viewer.id ? <Badge size="sm" variant="secondary">This is you</Badge> : null}
              </span>
            </div>
          </div>
          <div className="hd-record__actions">
            <Transitions transitions={staffTransitions(person, viewer)} onRun={(t) => onStaffTransition(person, t)} />
            {person.id === viewer.id ? <Button variant="outline" size="sm" onClick={() => onNavigate("/me/profile")}>Edit your profile</Button> : null}
          </div>
        </div>

        {person.status === "suspended" ? (
          <div className="hd-alertwrap">
            <Alert variant="warning">
              <AlertTitle>Access is suspended</AlertTitle>
              <AlertDescription>{person.suspendedNote || "No reason has been recorded against this suspension."} Nothing in Ward Desk can be signed by this account until access is restored.</AlertDescription>
            </Alert>
          </div>
        ) : null}

        <Figures three items={[
          { label: "Procedures against their name", value: procedures.length, qualifier: upcoming.length + " still to happen" },
          { label: "Patients as consultant", value: asConsultant.length, qualifier: openAsConsultant.length + " of them on a ward now" },
          { label: "Notes written", value: written.length, qualifier: written.length ? "most recently " + formatWhen(written[0].at, now) : "nothing written yet" },
        ]} />

        <Band title="Record">
          <Pairs items={[
            { label: "Email", value: person.email },
            { label: "Extension", value: person.phone },
            { label: "What they may do", value: labelOf(STAFF_ROLES, person.staffRole) },
            { label: "Specialty", value: person.specialty ? labelOf(SPECIALTIES, person.specialty) : "Not applicable" },
            { label: "Base ward", value: person.ward ? wardName(person.ward) : "No base ward" },
            { label: "Started", value: person.startedAt > now ? "Starts " + formatWhen(person.startedAt, now) : formatWhen(person.startedAt, now) },
            { label: "Registration", value: person.gmc || "Not a registered clinician" },
            { label: "Prescribing", value: prescribed.length ? prescribed.length + " orders prescribed" : verified.length ? verified.length + " orders verified" : "No orders against their name" },
          ]} />
        </Band>

        <Band title={"What they are holding (" + (upcoming.length + openAsConsultant.length) + ")"} note="Procedures still to happen and admissions still open — the two things that would need handing over.">
          {upcoming.length + openAsConsultant.length ? (
            <div className="hd-rows">
              {upcoming.map((intervention) => (
                <div className="hd-row" key={intervention.id}>
                  <div className="hd-row__text">
                    <span className="hd-row__title"><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/interventions/" + intervention.id)}>{intervention.name}</button></span>
                    <span className="hd-row__note">{patientName(data.patients, intervention.patientId)} · {intervention.scheduledAt ? formatWhen(intervention.scheduledAt, now) : "not scheduled"}</span>
                  </div>
                  <StatusBadge values={INTERVENTION_STATUS} value={intervention.status} />
                </div>
              ))}
              {openAsConsultant.map((admission) => (
                <div className="hd-row" key={admission.id}>
                  <div className="hd-row__text">
                    <span className="hd-row__title"><button type="button" className="hd-linkbtn" onClick={() => onNavigate("/patients/" + admission.patientId)}>{patientName(data.patients, admission.patientId)}</button></span>
                    <span className="hd-row__note">{admission.bed ? wardName(admission.ward) + " " + admission.bed : wardName(admission.ward) + " — no bed yet"} · {admission.reason}</span>
                  </div>
                  <StatusBadge values={ADMISSION_STATUS} value={admission.status} />
                </div>
              ))}
            </div>
          ) : <p className="hd-footnote">Nothing open against their name — no procedure to do and no admission still on a ward.</p>}
        </Band>

        <Band title={"Notes written (" + written.length + ")"} note="Everything this person has written on a record, most recent first.">
          {written.length ? (
            <div className="hd-notes">
              {written.map((comment) => (
                <Note key={comment.id} comment={comment} staff={data.staff} viewer={viewer} now={now} onReact={onReactToNote}
                  context={recordTitle(data, comment.record.type, comment.record.id)} />
              ))}
            </div>
          ) : <p className="hd-footnote">Nothing written yet.</p>}
        </Band>
      </>
    );
  }

  Object.assign(window, { DoctorsScreen, EmployeesScreen, StaffScreen });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="SocialScreens.jsx">
(function(){
/* Feed — the hospital's own record of itself: notes written on records, and what
   people did. Commit model: INSTANT. A note posts on its own and a reaction is a
   bare control; there is no Save on this screen.

   Notes are written ON a record, never here, so this screen has no composer:
   every item opens the record it belongs to, where the composer lives. */
(() => {
  const { Button, Avatar } = window.OriginDesignSystem_7c998b;

  function FeedScreen({ data, now, viewer, onNavigate, onReactToNote, onExport }) {
    const owed = mentionsOwed(data.comments, viewer.id);
    const written = data.comments.slice().sort((a, b) => b.at - a.at);
    const today = written.filter((c) => new Date(c.at).toDateString() === new Date(now).toDateString());
    const authorsToday = today.reduce((list, c) => (list.indexOf(c.authorId) > -1 ? list : list.concat([c.authorId])), []);
    const items = feedItems(data.activity);

    return (
      <>
        <PageHeader
          path="/feed"
          title="Feed"
          sub={"Everything written and everything done at " + HD_HOSPITAL.short + ", newest first."}
          actions={<ExportMenu scope={"All " + items.length + " activity entries"} onExport={(format) => onExport("Activity", "Every activity entry", items.length, format)} />}
        />
        <Figures three items={[
          { label: "Notes written today", value: today.length, qualifier: authorsToday.length + (authorsToday.length === 1 ? " person has written something" : " people have written something") },
          { label: "Waiting on you", value: owed.length, qualifier: owed.length ? "mentions you have not written back on" : "every mention of you has had a reply" },
          { label: "Notes on the record", value: written.length, qualifier: "across " + written.reduce((list, c) => (list.indexOf(c.record.type + c.record.id) > -1 ? list : list.concat([c.record.type + c.record.id])), []).length + " records" },
        ]} />

        <Band title={"Waiting on you (" + owed.length + ")"} note="Notes that mention you and that you have not written back on.">
          {owed.length ? (
            <div className="hd-notes">
              {owed.map((comment) => (
                <div className="hd-noteitem" key={comment.id}>
                  <Note comment={comment} staff={data.staff} viewer={viewer} now={now} onReact={onReactToNote}
                    context={recordTitle(data, comment.record.type, comment.record.id)} />
                  <Button variant="outline" size="xs" onClick={() => onNavigate(recordRoute(data, comment.record))}>Open the record to reply</Button>
                </div>
              ))}
            </div>
          ) : <p className="hd-footnote">Nothing is waiting on you. Every note that named you has had a reply from you since.</p>}
        </Band>

        <Band title={"Notes on records (" + written.length + ")"} note="Written on a patient, an admission, a procedure, an order, a stock line or an import.">
          <div className="hd-notes">
            {written.map((comment) => (
              <div className="hd-noteitem" key={comment.id}>
                <Note comment={comment} staff={data.staff} viewer={viewer} now={now} onReact={onReactToNote}
                  context={recordTitle(data, comment.record.type, comment.record.id)} />
                <Button variant="ghost" size="xs" onClick={() => onNavigate(recordRoute(data, comment.record))}>Open the record</Button>
              </div>
            ))}
          </div>
        </Band>

        <Band title={"What the hospital did (" + items.length + ")"} note="One entry per commit: a bed requested, a consent recorded, a course verified, a file imported.">
          <div className="hd-feed">
            {items.map((item) => {
              const actor = staffById(data.staff, item.actorId);
              return (
                <article className="hd-feed__item" key={item.id}>
                  <span className="hd-feed__avatar">
                    <Avatar size={32} src={actor ? actor.avatar : undefined} alt={actor ? "Photograph of " + actor.name : ""} fallback={initialsOf(actor ? actor.name : "?")} />
                  </span>
                  <span className="hd-feed__line">
                    <strong style={{ fontWeight: "var(--font-weight-medium)" }}>{actor ? actor.name : "Somebody"}</strong> {item.verb} ·{" "}
                    <button type="button" className="hd-linkbtn" onClick={() => onNavigate(recordRoute(data, item.record))}>{recordTitle(data, item.record.type, item.record.id)}</button>
                  </span>
                  <span className="hd-feed__time" title={formatAbsolute(item.at)}>{formatWhen(item.at, now)}</span>
                  {item.note ? <span className="hd-feed__note">{item.note}</span> : null}
                </article>
              );
            })}
          </div>
        </Band>
      </>
    );
  }

  Object.assign(window, { FeedScreen });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="DataScreens.jsx">
(function(){
/* Import and export — every job that has brought data in or sent it out, and the
   review an import needs when rows cannot be placed.
   Commit model: INSTANT. Accepting or rejecting a row writes into the fixture
   immediately, so the job's own accepted and rejected counts move with the
   decision; starting an import is a staged dialog.

   One data state per screen (§1.1): this screen is EITHER the job list OR the
   review of one job — never both at once. */
(() => {
  const {
    Button, Badge, Tabs, TabsList, TabsTab, Alert, AlertTitle, AlertDescription,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  } = window.OriginDesignSystem_7c998b;

  function ReviewState({ data, now, viewer, job, onCloseReview, onRowDecision, onAcceptMatched, onPostNote, onReactToNote }) {
    const rows = data.transferRows[job.id] || [];
    const matched = rows.filter((r) => r.detail.indexOf("Matches") === 0);
    const notes = commentsFor(data.comments, "transfer", job.id);
    return (
      <>
        <PageHeader path="/data" title={job.label}
          sub={"Reviewing the rows this import could not place · " + labelOf(IMPORT_SOURCES, job.source) + " · " + job.fileName}
          actions={<Button variant="outline" size="sm" onClick={onCloseReview}><Icon name="arrow-left" />Back to all jobs</Button>} />

        <Figures three items={[
          { label: "Rows in the file", value: formatCount(job.rows), qualifier: formatCount(job.accepted) + " already written into the patient index" },
          { label: "Rows still to decide", value: rows.length, qualifier: matched.length + " of them match an existing patient on name and date of birth" },
          { label: "Rows rejected", value: job.rejected, qualifier: "rejected rows are kept in the file and never written into the index" },
        ]} />

        <Band flush title={"Rows to decide (" + rows.length + ")"}
          note="Accepting writes the row into the patient index against the record it matches. Rejecting leaves it in the file."
          actions={matched.length ? <Button size="sm" onClick={() => onAcceptMatched(job, matched)}>Accept {matched.length} matched rows</Button> : null}>
          {rows.length ? (
            <TableWrap>
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead className="hd-num">Line</TableHead>
                    <TableHead>Name in the file</TableHead>
                    <TableHead>Why it stopped</TableHead>
                    <TableHead>What we know</TableHead>
                    <TableHead>Decision</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {rows.map((row) => (
                    <TableRow key={row.line}>
                      <TableCell className="hd-num">{row.line}</TableCell>
                      <TableCell>{row.name === "Unknown" ? <span className="hd-muted">Unknown</span> : row.name}</TableCell>
                      <TableCell><Badge size="sm" variant="warning">{row.problem}</Badge></TableCell>
                      <TableCell>{row.detail}</TableCell>
                      <TableCell>
                        <span className="hd-actions__row">
                          <Button size="sm" disabled={row.detail.indexOf("Matches") !== 0}
                            onClick={() => onRowDecision(job, row, "accept")}>Accept</Button>
                          <Button size="sm" variant="outline" onClick={() => onRowDecision(job, row, "reject")}>Reject</Button>
                        </span>
                      </TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </TableWrap>
          ) : (
            <p className="hd-footnote" style={{ paddingInline: "var(--gutter-page)" }}>
              Every row has been decided. {formatCount(job.accepted)} of {formatCount(job.rows)} rows are in the patient index and {job.rejected} were rejected.
            </p>
          )}
        </Band>

        <Band title={"Notes (" + notes.length + ")"} note="Written on this job. Posting is immediate.">
          <NoteThread comments={notes} staff={data.staff} viewer={viewer} now={now}
            onReact={onReactToNote} onPost={(body) => onPostNote({ type: "transfer", id: job.id }, body)}
            placeholder="What you decided about these rows, and why…" />
        </Band>
      </>
    );
  }

  function DataScreen({ data, now, viewer, reviewingId, onReview, onCloseReview, onTransferTransition, onImport, onExport, onRowDecision, onAcceptMatched, onPostNote, onReactToNote }) {
    const [kind, setKind] = React.useState("all");
    const [query, setQuery] = React.useState("");
    const { sort, onSort } = useSort("started", "desc");

    const reviewing = reviewingId ? data.transfers.find((t) => t.id === reviewingId) : null;
    if (reviewing) {
      return <ReviewState data={data} now={now} viewer={viewer} job={reviewing} onCloseReview={onCloseReview}
        onRowDecision={onRowDecision} onAcceptMatched={onAcceptMatched} onPostNote={onPostNote} onReactToNote={onReactToNote} />;
    }

    const needsReview = transfersNeedingReview(data.transfers);
    const toDecide = needsReview.reduce((n, t) => n + (t.pending || 0), 0);
    const failed = failedTransfers(data.transfers);
    const dirty = !!query || kind !== "all";
    const clear = () => { setQuery(""); setKind("all"); };
    const filtered = data.transfers.filter((job) => {
      if (kind !== "all" && job.kind !== kind) return false;
      if (query && (job.label + " " + job.scope + " " + job.fileName).toLowerCase().indexOf(query.toLowerCase()) < 0) return false;
      return true;
    });
    const rows = sortRows(filtered, sort, {
      started: (t) => t.startedAt,
      label: (t) => t.label,
      rows: (t) => t.rows,
    });

    return (
      <>
        <PageHeader
          path="/data"
          title="Import and export"
          sub={"Every job that has brought data into " + HD_HOSPITAL.short + " or sent it out. Exports are kept for " + HD_HOSPITAL.retentionYears + " years."}
          actions={
            <>
              <ImportButton viewer={viewer} onImport={onImport} />
              <ExportMenu scope="This job history" onExport={(format) => onExport("Transfer history", "Every import and export job", data.transfers.length, format)} />
            </>
          }
        />
        <Figures items={[
          { label: "Rows brought in", value: formatCount(rowsAccepted(data.transfers)), qualifier: "written into the index across " + imports(data.transfers).length + " imports" },
          { label: "Rows to review", value: toDecide, qualifier: needsReview.length + (needsReview.length === 1 ? " import is waiting on a decision" : " imports are waiting on a decision") },
          { label: "Exports run", value: exports_(data.transfers).length, qualifier: formatCount(exports_(data.transfers).reduce((n, t) => n + t.rows, 0)) + " rows sent out in total" },
          { label: "Jobs that failed", value: failed.length, qualifier: failed.length ? "nothing was written by them" : "every job finished" },
        ]} />

        {failed.length ? (
          <div className="hd-alertwrap">
            <Alert variant="error">
              <AlertTitle>{failed.length === 1 ? failed[0].label + " failed" : failed.length + " jobs failed"}</AlertTitle>
              <AlertDescription>{failed.map((t) => t.failure).join(" · ")}</AlertDescription>
            </Alert>
          </div>
        ) : null}

        <Band flush title={"Jobs (" + data.transfers.length + ")"} note={kind === "all" ? "Imports and exports together, newest first." : kind === "import" ? "Imports only." : "Exports only."}>
          <div style={{ padding: "0 var(--gutter-page) var(--space-3)" }}>
            <Tabs value={kind} onValueChange={setKind}>
              <TabsList>
                <TabsTab value="all" size="sm">All ({data.transfers.length})</TabsTab>
                <TabsTab value="import" size="sm">Imports ({imports(data.transfers).length})</TabsTab>
                <TabsTab value="export" size="sm">Exports ({exports_(data.transfers).length})</TabsTab>
              </TabsList>
            </Tabs>
          </div>
          <FilterBar query={query} onQuery={setQuery} placeholder="Search job, scope or file name…" dirty={dirty} onClear={clear} selects={[]} />
          {rows.length === 0 ? <NoResults noun="jobs" onClear={clear} /> : (
            <>
              <TableWrap>
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead><SortHead label="Job" sortKey="label" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Direction</TableHead>
                      <TableHead>Status</TableHead>
                      <TableHead className="hd-num"><SortHead label="Rows" sortKey="rows" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Accepted / rejected</TableHead>
                      <TableHead><SortHead label="Started" sortKey="started" sort={sort} onSort={onSort} /></TableHead>
                      <TableHead>Run by</TableHead>
                      <TableHead>Action</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {rows.map((job) => (
                      <TableRow key={job.id}>
                        <TableCell>
                          <span className="hd-person__text">
                            <span className="hd-person__name">{job.label}</span>
                            <span className="hd-person__meta">{job.scope} · {job.fileName}</span>
                          </span>
                        </TableCell>
                        <TableCell>{labelOf(TRANSFER_KINDS, job.kind)} · {job.format}</TableCell>
                        <TableCell><StatusBadge values={TRANSFER_STATUS} value={job.status} /></TableCell>
                        <TableCell className="hd-num">{formatCount(job.rows)}</TableCell>
                        <TableCell>
                          {formatCount(job.accepted)} accepted
                          {job.pending ? <span className="hd-warn"> · {job.pending} to decide</span> : null}
                          {job.rejected ? <span className="hd-muted"> · {job.rejected} rejected</span> : null}
                        </TableCell>
                        <TableCell><Clock at={job.startedAt} now={now} /></TableCell>
                        <TableCell>{staffName(data.staff, job.byId)}</TableCell>
                        <TableCell>
                          <RowActions transitions={transferTransitions(job)} label={"Actions for " + job.label}
                            onRun={(t) => (t.id === "review_rows" ? onReview(job.id) : onTransferTransition(job, t))} />
                        </TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </TableWrap>
              <TableFoot shown={rows.length} total={data.transfers.length} noun="jobs" />
            </>
          )}
        </Band>
      </>
    );
  }

  Object.assign(window, { DataScreen });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="SettingsScreens.jsx">
(function(){
/* Settings — seven registered screens in one namespace, reached from the account
   menu, with the within-destination level as a tab strip derived from the route
   table (§5, §6.3). Never a third navigation column.

   Commit model: every screen here is STAGED. Nothing applies until Save, Save
   and Cancel are disabled until the form is dirty, checkboxes are submitted with
   the form rather than applying themselves, and errors render on validation
   rather than on first paint (§3).

   Each screen has a real effect, stated on the screen: bed counts are the
   denominator of every occupancy figure, the role matrix decides which actions
   appear across the product, the write-off threshold decides whether a balance
   can be written off here or has to go for approval, and an integration that is
   off is not offered in the import dialog. */
(() => {
  const {
    Button, Badge, Input, Select, Checkbox, Field, FieldLabel, FieldDescription, FieldError, Alert, AlertTitle, AlertDescription,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  } = window.OriginDesignSystem_7c998b;

  function SettingsTabs({ path, onNavigate }) {
    return (
      <div className="hd-tabs" role="tablist" aria-label="Settings">
        {HD_SETTINGS_NAV.map((route) => (
          <button type="button" key={route.path} className="hd-tab" role="tab"
            aria-current={route.path === path ? "page" : undefined} aria-selected={route.path === path}
            onClick={() => onNavigate(route.path)}>{route.label}</button>
        ))}
      </div>
    );
  }

  /* Every settings screen is the same shape: header, the tab strip, the form in
     cards, then one sticky staged bar. */
  function SettingsPage({ path, title, sub, onNavigate, dirty, note, label, onCancel, onSave, children }) {
    return (
      <>
        <PageHeader path={path} title={title} sub={sub} />
        <SettingsTabs path={path} onNavigate={onNavigate} />
        {children}
        <FormBar dirty={dirty} note={note} label={label} onCancel={onCancel} onSave={onSave} />
      </>
    );
  }

  function useStaged(initial) {
    const [values, setValues] = React.useState(initial);
    const [submitted, setSubmitted] = React.useState(false);
    const set = (id, value) => setValues((v) => Object.assign({}, v, { [id]: value }));
    const reset = () => { setValues(initial); setSubmitted(false); };
    const dirty = JSON.stringify(values) !== JSON.stringify(initial);
    return { values: values, set: set, setValues: setValues, submitted: submitted, setSubmitted: setSubmitted, dirty: dirty, reset: reset };
  }

  /* ── Hospital ────────────────────────────────────────────────────────── */
  function HospitalSettingsScreen({ data, hospital, viewer, onNavigate, onSave }) {
    const form = useStaged({ name: hospital.name, trust: hospital.trust, recordsEmail: hospital.recordsEmail, retentionYears: String(hospital.retentionYears), timezone: hospital.timezone });
    const { values, set, submitted } = form;
    const errors = {};
    if (!values.name.trim()) errors.name = "The hospital's name appears on every export.";
    if (!values.trust.trim()) errors.trust = "Name the trust this hospital belongs to.";
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(values.recordsEmail.trim())) errors.recordsEmail = "Enter a valid email address.";
    const years = Number(values.retentionYears);
    if (!(years >= 1 && years <= 30)) errors.retentionYears = "Between 1 and 30 years.";
    const valid = Object.keys(errors).length === 0;

    return (
      <SettingsPage path="/settings" title="Hospital" onNavigate={onNavigate}
        sub={"Applies to everybody at " + hospital.short + " — " + data.staff.length + " people across " + WARDS.length + " wards."}
        dirty={form.dirty} label="Save hospital"
        note={"Saving rewrites the header of every export and the address a rejected import row is sent to."}
        onCancel={form.reset} onSave={() => { form.setSubmitted(true); if (valid) onSave(values); }}>
        <Band title="Identity" note="What the hospital is called on anything that leaves the building.">
          <div className="hd-fields">
            <Field>
              <FieldLabel htmlFor="hd-hs-name">Hospital name</FieldLabel>
              <Input id="hd-hs-name" value={values.name} invalid={submitted && !!errors.name} onChange={(e) => set("name", e.target.value)} />
              {submitted && errors.name ? <FieldError>{errors.name}</FieldError> : <FieldDescription>Written into the header row of every export file.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-hs-trust">Trust</FieldLabel>
              <Input id="hd-hs-trust" value={values.trust} invalid={submitted && !!errors.trust} onChange={(e) => set("trust", e.target.value)} />
              {submitted && errors.trust ? <FieldError>{errors.trust}</FieldError> : <FieldDescription>Shown on the sign-in screen and on invoices.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel>Timezone</FieldLabel>
              <Select aria-label="Timezone" value={values.timezone} onValueChange={(v) => set("timezone", v)}
                items={[{ value: "Europe/London", label: "Europe/London" }, { value: "Europe/Dublin", label: "Europe/Dublin" }, { value: "UTC", label: "UTC" }]} />
              <FieldDescription>Every clock in Ward Desk, including quiet hours and the drug round.</FieldDescription>
            </Field>
          </div>
        </Band>
        <Band title="Records and retention" note="Where records work goes, and how long files are kept.">
          <div className="hd-fields">
            <Field>
              <FieldLabel htmlFor="hd-hs-email">Records office</FieldLabel>
              <Input id="hd-hs-email" value={values.recordsEmail} invalid={submitted && !!errors.recordsEmail} onChange={(e) => set("recordsEmail", e.target.value)} />
              {submitted && errors.recordsEmail ? <FieldError>{errors.recordsEmail}</FieldError> : <FieldDescription>Where a rejected import row is sent for a human to place.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-hs-years">Keep exports for</FieldLabel>
              <Input id="hd-hs-years" value={values.retentionYears} invalid={submitted && !!errors.retentionYears} onChange={(e) => set("retentionYears", e.target.value)} />
              {submitted && errors.retentionYears ? <FieldError>{errors.retentionYears}</FieldError> : <FieldDescription>Years. Files are deleted the day after they age out; {exports_(data.transfers).length} exports are held now.</FieldDescription>}
            </Field>
          </div>
        </Band>
      </SettingsPage>
    );
  }

  /* ── Wards and beds ──────────────────────────────────────────────────── */
  function WardSettingsScreen({ data, now, viewer, onNavigate, onSave }) {
    const form = useStaged({ rows: data.wardBeds.map((w) => ({ ward: w.ward, beds: String(w.beds), day: String(w.establishment.day), late: String(w.establishment.late), night: String(w.establishment.night) })) });
    const { values, submitted } = form;
    const setRow = (ward, key, value) => form.setValues((v) => ({ rows: v.rows.map((r) => (r.ward === ward ? Object.assign({}, r, { [key]: value }) : r)) }));

    const errors = {};
    values.rows.forEach((row) => {
      if (!/^\d+$/.test(row.beds) || Number(row.beds) < 1 || Number(row.beds) > 60) errors[row.ward + "beds"] = true;
      ["day", "late", "night"].forEach((shift) => {
        if (!/^\d+$/.test(row[shift]) || Number(row[shift]) > 12) errors[row.ward + shift] = true;
      });
    });
    const valid = Object.keys(errors).length === 0;
    const totalBeds = values.rows.reduce((n, r) => n + (Number(r.beds) || 0), 0);
    const occupied = onWard(data.admissions).concat(fitForDischarge(data.admissions)).length;
    const shortNow = understaffedShifts(data.rota, data.wardBeds).length;

    return (
      <SettingsPage path="/settings/wards" title="Wards and beds" onNavigate={onNavigate}
        sub="The bed count is the denominator of every occupancy figure in the product; the establishment is what the rota is measured against."
        dirty={form.dirty} label="Save wards"
        note={"Saving moves occupancy on Today, Wards and Reports at once — " + occupied + " occupied beds against " + totalBeds + " as entered."}
        onCancel={form.reset} onSave={() => { form.setSubmitted(true); if (valid) onSave(values.rows); }}>
        {submitted && !valid ? (
          <div className="hd-alertwrap">
            <Alert variant="error">
              <AlertTitle>Some numbers cannot be saved</AlertTitle>
              <AlertDescription>A ward holds between 1 and 60 beds, and a shift's establishment is between 0 and 12 people.</AlertDescription>
            </Alert>
          </div>
        ) : null}
        <Band flush title="Beds and nursing establishment" note="One row per ward. A shift with fewer people rostered than its establishment shows as short on the rota.">
          <TableWrap>
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>Ward</TableHead>
                  <TableHead>Beds</TableHead>
                  <TableHead>Day shift</TableHead>
                  <TableHead>Late shift</TableHead>
                  <TableHead>Night shift</TableHead>
                  <TableHead>Occupied now</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {values.rows.map((row) => {
                  const wardOccupied = data.admissions.filter((a) => a.ward === row.ward && (a.status === "on_ward" || a.status === "fit_for_discharge")).length;
                  const over = wardOccupied > (Number(row.beds) || 0);
                  return (
                    <TableRow key={row.ward}>
                      <TableCell>
                        <span className="hd-person__text">
                          <span className="hd-person__name">{wardName(row.ward)}</span>
                          <span className="hd-person__meta">{labelOf(WARDS, row.ward).split(" — ")[1]}</span>
                        </span>
                      </TableCell>
                      {["beds", "day", "late", "night"].map((key) => (
                        <TableCell key={key} style={{ width: "7rem" }}>
                          <Input size="sm" value={row[key]} aria-label={wardName(row.ward) + " " + (key === "beds" ? "beds" : key + " shift establishment")}
                            invalid={submitted && !!errors[row.ward + key]} onChange={(e) => setRow(row.ward, key, e.target.value)} />
                        </TableCell>
                      ))}
                      <TableCell>
                        <span className={over ? "hd-warn" : ""}>{wardOccupied}</span>
                        {over ? <span className="hd-footnote"> — more people than beds</span> : null}
                      </TableCell>
                    </TableRow>
                  );
                })}
              </TableBody>
            </Table>
          </TableWrap>
          <p className="hd-footnote" style={{ paddingInline: "var(--card-pad)", paddingTop: "var(--space-3)" }}>
            {totalBeds} beds entered · {shortNow} {shortNow === 1 ? "shift is" : "shifts are"} short against the establishment saved now ·{" "}
            <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/rota")}>open the rota</button>
          </p>
        </Band>
      </SettingsPage>
    );
  }

  /* ── Roles and permissions ───────────────────────────────────────────── */
  const CAPABILITIES = [
    { id: "verify", label: "Verify medication orders", note: "The first dose cannot be given until somebody with this verifies the order." },
    { id: "move", label: "Move admissions and beds", note: "Assign a bed, mark fit for discharge, discharge, transfer out." },
    { id: "consent", label: "Record consent", note: "A scheduled procedure cannot start without it." },
    { id: "import", label: "Import data", note: "Writes into the patient index." },
    { id: "bill", label: "Bill and chase", note: "Issue an invoice, record a payment, chase a payer." },
  ];

  function RoleSettingsScreen({ data, viewer, onNavigate, onSave }) {
    const form = useStaged({
      rows: STAFF_ROLES.map((role) => ({
        role: role.value,
        verify: !!ROLE_CAN_VERIFY_MEDICATION[role.value],
        move: !!ROLE_CAN_MOVE_ADMISSION[role.value],
        consent: !!ROLE_CAN_RECORD_CONSENT[role.value],
        import: !!ROLE_CAN_IMPORT[role.value],
        bill: !!ROLE_CAN_BILL[role.value],
      })),
    });
    const { values } = form;
    const setCell = (role, cap, on) => form.setValues((v) => ({ rows: v.rows.map((r) => (r.role === role ? Object.assign({}, r, { [cap]: on }) : r)) }));
    const countFor = (cap) => data.staff.filter((s) => {
      const row = values.rows.find((r) => r.role === s.staffRole);
      return row && row[cap] && s.status !== "left" && s.status !== "suspended";
    }).length;
    const orphaned = CAPABILITIES.filter((cap) => countFor(cap.id) === 0);
    const valid = orphaned.length === 0;

    return (
      <SettingsPage path="/settings/roles" title="Roles and permissions" onNavigate={onNavigate}
        sub="These decide which actions appear across the product. An action somebody may not take is shown disabled with the reason, never hidden."
        dirty={form.dirty} label="Save permissions"
        note={"Your own role is " + labelOf(STAFF_ROLES, viewer.staffRole) + ", so saving this can change what you can do."}
        onCancel={form.reset} onSave={() => { form.setSubmitted(true); if (valid) onSave(values.rows); }}>
        {form.submitted && !valid ? (
          <div className="hd-alertwrap">
            <Alert variant="error">
              <AlertTitle>Nobody would be able to do some of this work</AlertTitle>
              <AlertDescription>{orphaned.map((c) => c.label.toLowerCase()).join(", ")} — at least one role that somebody actually holds has to keep each of these.</AlertDescription>
            </Alert>
          </div>
        ) : null}
        <Band flush title="What each role may do" note="Checkboxes here are submitted with the form; nothing changes until you save.">
          <div className="hd-tablewrap">
            <table className="hd-matrix">
              <thead>
                <tr>
                  <th scope="col">Role</th>
                  <th scope="col">People</th>
                  {CAPABILITIES.map((cap) => <th scope="col" key={cap.id} style={{ minWidth: "9rem" }}>{cap.label}</th>)}
                </tr>
              </thead>
              <tbody>
                {values.rows.map((row) => {
                  const people = data.staff.filter((s) => s.staffRole === row.role);
                  return (
                    <tr key={row.role}>
                      <th scope="row" style={{ fontWeight: "var(--font-weight-medium)", color: "var(--foreground)" }}>
                        {labelOf(STAFF_ROLES, row.role)}
                        {row.role === viewer.staffRole ? <Badge size="sm" variant="secondary" style={{ marginInlineStart: "var(--space-2)" }}>Yours</Badge> : null}
                      </th>
                      <td>
                        {people.length
                          ? <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/employees")}>{people.length}</button>
                          : <span className="hd-muted">nobody</span>}
                      </td>
                      {CAPABILITIES.map((cap) => (
                        <td key={cap.id}>
                          <Checkbox checked={!!row[cap.id]} aria-label={labelOf(STAFF_ROLES, row.role) + " — " + cap.label}
                            onCheckedChange={(on) => setCell(row.role, cap.id, on)} />
                        </td>
                      ))}
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </Band>
        <Band tint title="What each permission gates" note="Counted from the roles as entered above, over the people who currently hold them.">
          <div className="hd-split">
            {CAPABILITIES.map((cap) => (
              <div className={"hd-tile" + (countFor(cap.id) ? "" : " hd-tile--tint")} key={cap.id}>
                <span className="hd-tile__label">{cap.label}</span>
                <span className="hd-tile__value">{countFor(cap.id)}</span>
                <span className="hd-footnote">{countFor(cap.id) === 1 ? "person could do this" : "people could do this"} · {cap.note}</span>
              </div>
            ))}
          </div>
        </Band>
      </SettingsPage>
    );
  }

  /* ── Billing ─────────────────────────────────────────────────────────── */
  function BillingSettingsScreen({ data, now, billing, viewer, onNavigate, onSave }) {
    const form = useStaged({
      referencePrefix: billing.referencePrefix,
      defaultPayer: billing.defaultPayer,
      threshold: (billing.writeOffThresholdMinor / 100).toFixed(0),
      approver: billing.approver,
    });
    const { values, set, submitted } = form;
    const errors = {};
    if (!/^[A-Z]{2}-\d{4}$/.test(values.referencePrefix.trim())) errors.referencePrefix = "Two letters, a dash and a year — HG-2026.";
    if (!/^\d+$/.test(values.threshold) || Number(values.threshold) < 100) errors.threshold = "A whole number of pounds, 100 or more.";
    if (!values.approver.trim()) errors.approver = "Name the role that approves a write-off above the threshold.";
    const valid = Object.keys(errors).length === 0;
    const thresholdMinor = Number(values.threshold) * 100;
    const above = data.invoices.filter((i) => INVOICE_FLAGS.billable(i) && invoiceBalance(i, data.payments) > thresholdMinor);

    return (
      <SettingsPage path="/settings/billing" title="Billing" onNavigate={onNavigate}
        sub="How invoices are referenced, who is billed by default, and the point at which writing off a balance stops being a local decision."
        dirty={form.dirty} label="Save billing"
        note={"Payment terms are not set here — they come from each payer's contract."}
        onCancel={form.reset} onSave={() => { form.setSubmitted(true); if (valid) onSave({ referencePrefix: values.referencePrefix.trim(), defaultPayer: values.defaultPayer, writeOffThresholdMinor: thresholdMinor, approver: values.approver.trim() }); }}>
        <Band title="Invoicing">
          <div className="hd-fields">
            <Field>
              <FieldLabel htmlFor="hd-bs-prefix">Reference prefix</FieldLabel>
              <Input id="hd-bs-prefix" value={values.referencePrefix} invalid={submitted && !!errors.referencePrefix} onChange={(e) => set("referencePrefix", e.target.value)} />
              {submitted && errors.referencePrefix ? <FieldError>{errors.referencePrefix}</FieldError> : <FieldDescription>Every invoice raised from now takes this prefix; the {data.invoices.length} that exist keep theirs.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel>Default payer</FieldLabel>
              <Select aria-label="Default payer" value={values.defaultPayer} onValueChange={(v) => set("defaultPayer", v)} items={PAYERS} />
              <FieldDescription>Pre-selected when somebody raises a new invoice.</FieldDescription>
            </Field>
          </div>
        </Band>
        <Band title="Writing off a balance" note="Above the threshold, writing off stops being a local decision and goes for approval instead.">
          <div className="hd-fields">
            <Field>
              <FieldLabel htmlFor="hd-bs-threshold">Threshold in pounds</FieldLabel>
              <Input id="hd-bs-threshold" value={values.threshold} invalid={submitted && !!errors.threshold} onChange={(e) => set("threshold", e.target.value)} />
              {submitted && errors.threshold ? <FieldError>{errors.threshold}</FieldError> : <FieldDescription>{above.length} of the {invoicesBillable(data.invoices).length} issued invoices carry a balance above this.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-bs-approver">Approved by</FieldLabel>
              <Input id="hd-bs-approver" value={values.approver} invalid={submitted && !!errors.approver} onChange={(e) => set("approver", e.target.value)} />
              {submitted && errors.approver ? <FieldError>{errors.approver}</FieldError> : <FieldDescription>Named in the request Ward Desk raises instead of writing the balance off.</FieldDescription>}
            </Field>
          </div>
        </Band>
        <Band tint flush title={"Payment terms (" + data.contracts.length + ")"} note="Read-only here: terms belong to the payer's contract, so they are changed on Contracts and tariffs.">
          <TableWrap>
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>Payer</TableHead>
                  <TableHead>Contract</TableHead>
                  <TableHead>Terms</TableHead>
                  <TableHead>Runs to</TableHead>
                  <TableHead>Action</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {data.contracts.map((contract) => (
                  <TableRow key={contract.id}>
                    <TableCell>{labelOf(PAYERS, contract.payer)}</TableCell>
                    <TableCell>{contract.name}</TableCell>
                    <TableCell>{contract.dueDays ? contract.dueDays + " days" : "Before care"}</TableCell>
                    <TableCell>
                      {contract.expiresAt ? <Clock at={contract.expiresAt} now={now} problem={CONTRACT_FLAGS.expiringSoon(contract, now)} /> : <span className="hd-muted">No end date</span>}
                    </TableCell>
                    <TableCell><Button variant="outline" size="sm" onClick={() => onNavigate("/contracts")}>Open contract</Button></TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </TableWrap>
        </Band>
      </SettingsPage>
    );
  }

  /* ── Integrations and feeds ──────────────────────────────────────────── */
  function IntegrationSettingsScreen({ data, now, integrations, viewer, onNavigate, onSave }) {
    const form = useStaged({ rows: integrations.map((row) => Object.assign({}, row)) });
    const { values } = form;
    const setRow = (source, key, on) => form.setValues((v) => ({ rows: v.rows.map((r) => (r.source === source ? Object.assign({}, r, { [key]: on }) : r)) }));
    const enabled = values.rows.filter((r) => r.enabled);

    return (
      <SettingsPage path="/settings/integrations" title="Integrations and feeds" onNavigate={onNavigate}
        sub="Where data comes in from. A feed that is off is not offered when somebody starts an import, and nothing arrives from it."
        dirty={form.dirty} label="Save integrations"
        note={enabled.length + " of " + values.rows.length + " feeds would be available in the import dialog."}
        onCancel={form.reset} onSave={() => onSave(values.rows)}>
        <Band flush title="Feeds" note="Auto-accepting matched rows skips the review step for rows that match a patient on name and date of birth.">
          <TableWrap>
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>Feed</TableHead>
                  <TableHead>Format</TableHead>
                  <TableHead>Last run</TableHead>
                  <TableHead>Rows brought in</TableHead>
                  <TableHead>Available</TableHead>
                  <TableHead>Auto-accept matched rows</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {values.rows.map((row) => {
                  const jobs = data.transfers.filter((t) => t.source === row.source);
                  const last = jobs.slice().sort((a, b) => b.startedAt - a.startedAt)[0];
                  const label = labelOf(IMPORT_SOURCES, row.source);
                  return (
                    <TableRow key={row.source}>
                      <TableCell>
                        <span className="hd-person__text">
                          <span className="hd-person__name">{label.split(" — ")[0]}</span>
                          <span className="hd-person__meta">{jobs.length} {jobs.length === 1 ? "job" : "jobs"} run</span>
                        </span>
                      </TableCell>
                      <TableCell>{label.split(" — ")[1] || "CSV"}</TableCell>
                      <TableCell>{last ? <Clock at={last.startedAt} now={now} /> : <span className="hd-muted">Never</span>}</TableCell>
                      <TableCell>
                        {last
                          ? <span className="hd-actions__row"><StatusBadge values={TRANSFER_STATUS} value={last.status} /><span className="hd-footnote">{formatCount(last.accepted)} accepted</span></span>
                          : <span className="hd-muted">—</span>}
                      </TableCell>
                      <TableCell>
                        <Checkbox checked={!!row.enabled} aria-label={label + " available for import"} onCheckedChange={(on) => setRow(row.source, "enabled", on)} />
                      </TableCell>
                      <TableCell>
                        <Checkbox checked={!!row.autoAccept} disabled={!row.enabled} aria-label={label + " auto-accepts matched rows"} onCheckedChange={(on) => setRow(row.source, "autoAccept", on)} />
                      </TableCell>
                    </TableRow>
                  );
                })}
              </TableBody>
            </Table>
          </TableWrap>
        </Band>
        <Band tint title="What is waiting" note="Read from the jobs themselves, not from this form.">
          <div className="hd-split">
            <div className="hd-tile">
              <span className="hd-tile__label">Imports needing review</span>
              <span className="hd-tile__value">{transfersNeedingReview(data.transfers).length}</span>
              <span className="hd-footnote">{transfersNeedingReview(data.transfers).reduce((n, t) => n + (t.pending || 0), 0)} rows to decide · <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/data")}>open them</button></span>
            </div>
            <div className="hd-tile">
              <span className="hd-tile__label">Jobs that failed</span>
              <span className="hd-tile__value">{failedTransfers(data.transfers).length}</span>
              <span className="hd-footnote">{failedTransfers(data.transfers).length ? "nothing was written by them" : "every job finished"}</span>
            </div>
            <div className="hd-tile hd-tile--tint">
              <span className="hd-tile__label">Rows brought in</span>
              <span className="hd-tile__value">{formatCount(rowsAccepted(data.transfers))}</span>
              <span className="hd-footnote">across {imports(data.transfers).length} imports</span>
            </div>
          </div>
        </Band>
      </SettingsPage>
    );
  }

  /* ── Notifications ───────────────────────────────────────────────────── */
  function NotificationSettingsScreen({ data, prefs, hospital, viewer, onNavigate, onSave }) {
    const form = useStaged({ rows: prefs.rows.map((r) => Object.assign({}, r)), from: prefs.quietHours.from, to: prefs.quietHours.to });
    const { values, set, submitted } = form;
    const setRow = (id, channel, on) => form.setValues((v) => Object.assign({}, v, { rows: v.rows.map((r) => (r.id === id ? Object.assign({}, r, { [channel]: on }) : r)) }));
    const errors = {};
    if (!/^\d\d:\d\d$/.test(values.from)) errors.from = "Use HH:MM.";
    if (!/^\d\d:\d\d$/.test(values.to)) errors.to = "Use HH:MM.";
    const valid = Object.keys(errors).length === 0;
    const paged = values.rows.filter((r) => r.pager).length;

    return (
      <SettingsPage path="/settings/notifications" title="Notifications" onNavigate={onNavigate}
        sub="Who is told what, and when they are left alone. In-app notifications always arrive; email and the pager respect quiet hours."
        dirty={form.dirty} label="Save notifications"
        note={paged + " of " + values.rows.length + " events would page somebody."}
        onCancel={form.reset} onSave={() => { form.setSubmitted(true); if (valid) onSave({ rows: values.rows, quietHours: { from: values.from, to: values.to } }); }}>
        <Band title="Events" note="Checkboxes here are submitted with the form; nothing changes until you save.">
          <div className="hd-tablewrap">
            <table className="hd-matrix">
              <thead>
                <tr>
                  <th scope="col">Event</th>
                  <th scope="col">Email</th>
                  <th scope="col">Pager</th>
                  <th scope="col">In Ward Desk</th>
                </tr>
              </thead>
              <tbody>
                {values.rows.map((row) => (
                  <tr key={row.id}>
                    <th scope="row" style={{ fontWeight: "var(--font-weight-normal)", color: "var(--foreground)" }}>{row.label}</th>
                    {["email", "pager", "inApp"].map((channel) => (
                      <td key={channel}>
                        <Checkbox checked={!!row[channel]} aria-label={row.label + " — " + (channel === "inApp" ? "in Ward Desk" : channel)}
                          onCheckedChange={(on) => setRow(row.id, channel, on)} />
                      </td>
                    ))}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </Band>
        <Band title="Quiet hours" note={"Nothing is emailed or paged between these times, " + hospital.timezone + "."}>
          <div className="hd-fields">
            <Field>
              <FieldLabel htmlFor="hd-ns-from">From</FieldLabel>
              <Input id="hd-ns-from" value={values.from} invalid={submitted && !!errors.from} onChange={(e) => set("from", e.target.value)} />
              {submitted && errors.from ? <FieldError>{errors.from}</FieldError> : <FieldDescription>24-hour clock.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-ns-to">Until</FieldLabel>
              <Input id="hd-ns-to" value={values.to} invalid={submitted && !!errors.to} onChange={(e) => set("to", e.target.value)} />
              {submitted && errors.to ? <FieldError>{errors.to}</FieldError> : null}
            </Field>
          </div>
        </Band>
      </SettingsPage>
    );
  }

  /* ── Security ────────────────────────────────────────────────────────── */
  function SecuritySettingsScreen({ data, now, security, viewer, onNavigate, onSave }) {
    const form = useStaged({ sessionMinutes: String(security.sessionMinutes), requireTwoFactor: !!security.requireTwoFactor, passwordAgeDays: String(security.passwordAgeDays) });
    const { values, set, submitted } = form;
    const errors = {};
    if (!/^\d+$/.test(values.passwordAgeDays) || Number(values.passwordAgeDays) < 30 || Number(values.passwordAgeDays) > 730) errors.passwordAgeDays = "Between 30 and 730 days.";
    const valid = Object.keys(errors).length === 0;

    return (
      <SettingsPage path="/settings/security" title="Security" onNavigate={onNavigate}
        sub="Ward Desk holds patient data, and a ward station is a shared screen. These settings apply to everybody who signs in."
        dirty={form.dirty} label="Save security"
        note={"A shorter session locks a ward station sooner; " + data.staff.filter((s) => s.status === "active").length + " active accounts are affected."}
        onCancel={form.reset} onSave={() => { form.setSubmitted(true); if (valid) onSave({ sessionMinutes: Number(values.sessionMinutes), requireTwoFactor: values.requireTwoFactor, passwordAgeDays: Number(values.passwordAgeDays) }); }}>
        <Band title="Signing in">
          <div className="hd-fields">
            <Field>
              <FieldLabel>Lock an idle session after</FieldLabel>
              <Select aria-label="Session timeout" value={values.sessionMinutes} onValueChange={(v) => set("sessionMinutes", v)}
                items={[{ value: "15", label: "15 minutes" }, { value: "30", label: "30 minutes" }, { value: "60", label: "60 minutes" }]} />
              <FieldDescription>A ward station left open is the commonest way a record is seen by the wrong person.</FieldDescription>
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-ss-age">Passwords expire after</FieldLabel>
              <Input id="hd-ss-age" value={values.passwordAgeDays} invalid={submitted && !!errors.passwordAgeDays} onChange={(e) => set("passwordAgeDays", e.target.value)} />
              {submitted && errors.passwordAgeDays ? <FieldError>{errors.passwordAgeDays}</FieldError> : <FieldDescription>Days. The reset flow is the same one on the sign-in screen.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel>Second factor</FieldLabel>
              <span className="hd-person">
                <Checkbox checked={values.requireTwoFactor} aria-label="Require a second factor for everybody" onCheckedChange={(on) => set("requireTwoFactor", on)} />
                <span className="hd-pair__value">Require a second factor for everybody</span>
              </span>
              <FieldDescription>Submitted with this form, like every control on this screen.</FieldDescription>
            </Field>
          </div>
        </Band>
        <Band tint title="What a password must be" note="Read-only: the rules the reset screen validates against, so the two cannot disagree.">
          <div className="hd-checklist">
            {PASSWORD_RULES.map((rule) => (
              <span className="hd-checklist__item" key={rule.id}><Icon name="check" size={14} />{rule.label}</span>
            ))}
          </div>
        </Band>
        <Band flush title={"Your signed-in sessions (" + data.sessions.length + ")"} note="Read from the session store; ending one is done by the trust's identity service, not here.">
          <TableWrap>
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>Where</TableHead>
                  <TableHead>Browser</TableHead>
                  <TableHead>Address</TableHead>
                  <TableHead>Last seen</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {data.sessions.map((session) => (
                  <TableRow key={session.id}>
                    <TableCell>
                      <span className="hd-actions__row">
                        {session.device}
                        {session.current ? <Badge size="sm" variant="success">This one</Badge> : null}
                      </span>
                    </TableCell>
                    <TableCell>{session.browser}</TableCell>
                    <TableCell style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)" }}>{session.ip}</TableCell>
                    <TableCell><Clock at={session.lastSeenAt} now={now} /></TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </TableWrap>
        </Band>
      </SettingsPage>
    );
  }

  Object.assign(window, {
    SettingsTabs, SettingsPage, useStaged, HospitalSettingsScreen, WardSettingsScreen, RoleSettingsScreen,
    BillingSettingsScreen, IntegrationSettingsScreen, NotificationSettingsScreen, SecuritySettingsScreen, CAPABILITIES,
  });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="AccountScreens.jsx">
(function(){
/* Your profile — the one screen that is about you rather than the hospital.
   Commit model: STAGED. The photograph, the name and the extension are all form
   fields; nothing is written until Save, so a picture dropped here is a preview
   until then. A staged screen may not hold an instant control, which is why
   "remove photograph" clears the field rather than saving anything. */
(() => {
  const {
    Button, Badge, Avatar, Input, Field, FieldLabel, FieldDescription, FieldError,
  } = window.OriginDesignSystem_7c998b;

  /* Four photographs already in the badge system, offered as the alternative to
     uploading one. */
  const PHOTO_CHOICES = [
    (window.__resources["r003"]),
    (window.__resources["r007"]),
    (window.__resources["r011"]),
    (window.__resources["r028"]),
  ];

  const MAX_PHOTO_KB = 2048;

  /* The upload control: one real file input, a drop zone that is also its label
     so the keyboard reaches it, and a preview. It validates type and size and
     says what it found; it never writes anything itself. */
  function PhotoUpload({ value, name, fileName, onPick, onClear, error, onError }) {
    const [over, setOver] = React.useState(false);
    const inputRef = React.useRef(null);

    const take = (file) => {
      if (!file) return;
      if (file.type.indexOf("image/") !== 0) {
        onError("That is a " + (file.type || "file with no type") + ". Use a JPEG or a PNG.");
        return;
      }
      if (file.size / 1024 > MAX_PHOTO_KB) {
        onError("That photograph is " + Math.round(file.size / 1024 / 1024 * 10) / 10 + " MB. The limit is 2 MB — the badge system rescales anything larger.");
        return;
      }
      const reader = new FileReader();
      reader.onload = () => { onError(null); onPick(String(reader.result), file.name); };
      reader.onerror = () => onError("That file could not be read. Try saving it again and re-uploading.");
      reader.readAsDataURL(file);
    };

    return (
      <div className="hd-upload">
        <span className="hd-preview">
          <Avatar size={72} src={value || undefined} alt={value ? "Your photograph" : ""} fallback={initialsOf(name)} />
          <span className="hd-footnote">{value ? (fileName || "On the badge system") : "No photograph"}</span>
        </span>
        <label className="hd-drop" data-over={over ? "true" : "false"} data-invalid={error ? "true" : "false"}
          onDragOver={(e) => { e.preventDefault(); setOver(true); }}
          onDragLeave={() => setOver(false)}
          onDrop={(e) => { e.preventDefault(); setOver(false); take(e.dataTransfer.files && e.dataTransfer.files[0]); }}>
          <Icon name="image-up" size={20} />
          <span className="hd-drop__title">Drop a photograph here, or choose a file</span>
          <span className="hd-footnote">JPEG or PNG, up to 2 MB. It is stored on the badge system and shown beside everything you write.</span>
          <input ref={inputRef} type="file" accept="image/jpeg,image/png" aria-label="Upload a photograph"
            onChange={(e) => { take(e.target.files && e.target.files[0]); e.target.value = ""; }} />
          <span className="hd-actions__row" style={{ marginTop: "var(--space-1)" }}>
            <Button size="sm" variant="outline" onClick={(e) => { e.preventDefault(); inputRef.current && inputRef.current.click(); }}>Choose a file</Button>
            {value ? <Button size="sm" variant="ghost" onClick={(e) => { e.preventDefault(); onError(null); onClear(); }}>Remove photograph</Button> : null}
          </span>
        </label>
      </div>
    );
  }

  function MyProfileScreen({ data, now, viewer, onSave, onNavigate }) {
    const initial = { name: viewer.name, jobTitle: viewer.jobTitle, phone: viewer.phone, avatar: viewer.avatar || null };
    const [values, setValues] = React.useState(initial);
    const [fileName, setFileName] = React.useState(null);
    const [photoError, setPhotoError] = React.useState(null);
    const [submitted, setSubmitted] = React.useState(false);
    React.useEffect(() => {
      setValues({ name: viewer.name, jobTitle: viewer.jobTitle, phone: viewer.phone, avatar: viewer.avatar || null });
      setFileName(null); setPhotoError(null); setSubmitted(false);
    }, [viewer]);
    const set = (id, value) => setValues((v) => Object.assign({}, v, { [id]: value }));
    const dirty = Object.keys(initial).some((key) => values[key] !== initial[key]);

    const errors = {};
    if (!values.name.trim() || values.name.trim().split(/\s+/).length < 2) errors.name = "Enter the name colleagues will see beside your notes.";
    if (!values.jobTitle.trim()) errors.jobTitle = "Your job title appears beside every note you write.";
    if (!/^ext \d{3,5}$/.test(values.phone.trim())) errors.phone = "Use the form ext 4380.";
    const valid = Object.keys(errors).length === 0;
    const written = commentsByAuthor(data.comments, viewer.id);

    return (
      <>
        <PageHeader path="/me/profile" title="Your profile"
          sub={"What colleagues see beside the " + written.length + (written.length === 1 ? " note you have written" : " notes you have written") + ". Your role, your ward and your access are set by a coordinator on Employees."}
          actions={<Button variant="outline" size="sm" onClick={() => onNavigate("/settings")}>Hospital settings</Button>} />

        <Band title="Photograph" note="Shown on every note, every row and every record you touch — and on your badge.">
          <PhotoUpload
            value={values.avatar}
            name={values.name}
            fileName={fileName}
            error={photoError}
            onError={setPhotoError}
            onPick={(dataUrl, name) => { set("avatar", dataUrl); setFileName(name); }}
            onClear={() => { set("avatar", null); setFileName(null); }}
          />
          {photoError ? <p className="hd-footnote hd-danger" style={{ marginTop: "var(--space-3)" }}>{photoError}</p> : null}
          <div className="hd-photos" style={{ marginTop: "var(--space-4)" }}>
            <span className="hd-footnote" style={{ width: "100%" }}>Or use one already on the badge system:</span>
            {PHOTO_CHOICES.map((src) => {
              const on = values.avatar === src;
              return (
                <button type="button" key={src} className="hd-photo" aria-pressed={on}
                  aria-label={on ? "Badge photograph, selected" : "Use this badge photograph"}
                  onClick={() => { setPhotoError(null); set("avatar", src); setFileName(null); }}>
                  <Avatar size={46} src={src} alt="" fallback={initialsOf(values.name)} />
                </button>
              );
            })}
          </div>
        </Band>

        <Band title="What colleagues see">
          <div className="hd-fields">
            <Field>
              <FieldLabel htmlFor="hd-me-name">Name</FieldLabel>
              <Input id="hd-me-name" value={values.name} invalid={submitted && !!errors.name} onChange={(e) => set("name", e.target.value)} />
              {submitted && errors.name ? <FieldError>{errors.name}</FieldError> : <FieldDescription>Saving rewrites your name beside every note you have already written.</FieldDescription>}
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-me-title">Job title</FieldLabel>
              <Input id="hd-me-title" value={values.jobTitle} invalid={submitted && !!errors.jobTitle} onChange={(e) => set("jobTitle", e.target.value)} />
              {submitted && errors.jobTitle ? <FieldError>{errors.jobTitle}</FieldError> : null}
            </Field>
            <Field>
              <FieldLabel htmlFor="hd-me-phone">Extension</FieldLabel>
              <Input id="hd-me-phone" value={values.phone} placeholder="ext 4380" invalid={submitted && !!errors.phone} onChange={(e) => set("phone", e.target.value)} />
              {submitted && errors.phone ? <FieldError>{errors.phone}</FieldError> : <FieldDescription>Reachable from any ward phone.</FieldDescription>}
            </Field>
          </div>
        </Band>

        <Band tint title="Set by somebody else" note="These decide what you can do in Ward Desk; a coordinator changes them on Employees, and the rules themselves live in Settings.">
          <Pairs items={[
            { label: "Email", value: viewer.email },
            { label: "What you may do", value: labelOf(STAFF_ROLES, viewer.staffRole) },
            { label: "Job family", value: labelOf(JOB_FAMILIES, viewer.family) },
            { label: "Base ward", value: viewer.ward ? wardName(viewer.ward) : "No base ward" },
            { label: "Employment", value: <StatusBadge values={STAFF_STATUS} value={viewer.status} /> },
            { label: "Your record", value: <button type="button" className="hd-linkbtn" onClick={() => onNavigate("/employees/" + viewer.id)}>Open it on Employees</button> },
          ]} />
        </Band>

        <FormBar dirty={dirty} label="Save profile"
          note={values.avatar && values.avatar.indexOf("data:") === 0 ? "The photograph you dropped is a preview until you save." : "Saving rewrites your name and picture beside every note you have written."}
          onCancel={() => { setValues(initial); setFileName(null); setPhotoError(null); setSubmitted(false); }}
          onSave={() => { setSubmitted(true); if (valid) onSave(values); }} />
      </>
    );
  }

  Object.assign(window, { MyProfileScreen, PhotoUpload, PHOTO_CHOICES, MAX_PHOTO_KB });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="AuthScreens.jsx">
(function(){
/* Auth — its own shell, and the only shell in the product with NO navigation
   owner (AGENTS §7.1): no rail, no panel, no account menu. Split layout, chosen
   once for the product; the form side is the wider of the two.
   Every screen here is STAGED. One state per screen, and a server error and a
   field error never render for the same submission.
   Strategy: password, one credential. No providers are offered because no
   official provider mark is held in this product's assets (§7.4) — an entry
   point without a screen behind it is worse than none. */
(() => {
  const {
    Button, Input, Alert, AlertTitle, AlertDescription, Field, FieldLabel, FieldDescription, FieldError, Separator,
  } = window.OriginDesignSystem_7c998b;

  function AuthShell({ title, lede, claim, fine, children, foot }) {
    return (
      <div className="hd-auth">
        <aside className="hd-auth__brandside">
          <span className="hd-auth__art" aria-hidden="true" />
          <span className="hd-auth__mark">
            <img src={HD_BRAND_MARK} alt="" width="20" height="20" />
            Ward Desk
          </span>
          <p className="hd-auth__claim">{claim}</p>
          <p className="hd-auth__fine">{fine}</p>
        </aside>
        <main className="hd-auth__formside">
          <div className="hd-auth__form">
            <div>
              <h1 className="hd-auth__title">{title}</h1>
              <p className="hd-auth__lede">{lede}</p>
            </div>
            {children}
            {foot ? <p className="hd-auth__foot">{foot}</p> : null}
          </div>
        </main>
      </div>
    );
  }

  const CLAIM = "One board for the wards, the theatres and the pharmacy.";
  const FINE = HD_HOSPITAL.trust + " · Ward Desk holds patient data. Everything you open is logged against your name.";

  function SignInScreen({ onSignIn, onNavigate }) {
    const [email, setEmail] = React.useState("");
    const [password, setPassword] = React.useState("");
    const [reveal, setReveal] = React.useState(false);
    const [submitted, setSubmitted] = React.useState(false);
    const [rejected, setRejected] = React.useState(false);
    const dirty = !!email || !!password;

    const errors = {};
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim())) errors.email = "Enter a valid email address.";
    if (password.length < 8) errors.password = "Your password is at least 8 characters.";
    const valid = Object.keys(errors).length === 0;

    const submit = () => {
      setSubmitted(true);
      if (!valid) { setRejected(false); return; }
      /* One state per submission: a server error only where there is no field
         error, and the password field is the only thing cleared. */
      if (email.trim().toLowerCase().indexOf("@haldengeneral.nhs.uk") < 0) { setRejected(true); setPassword(""); return; }
      setRejected(false);
      onSignIn();
    };

    return (
      <AuthShell title="Sign in" claim={CLAIM} fine={FINE}
        lede={"Use your " + HD_HOSPITAL.short + " account. Ward Desk does not hold a separate password."}
        foot={<>Cannot get in? The service desk is on ext 4000, or write to {HD_HOSPITAL.recordsEmail}.</>}>
        {rejected ? (
          <Alert variant="error">
            <AlertTitle>Email or password is incorrect</AlertTitle>
            <AlertDescription>Check the address and try again. After five attempts the account locks for fifteen minutes.</AlertDescription>
          </Alert>
        ) : null}
        <div className="hd-auth__fields">
          <Field>
            <FieldLabel htmlFor="hd-signin-email">Work email</FieldLabel>
            <Input id="hd-signin-email" type="email" autoComplete="email" value={email} placeholder="i.vance@haldengeneral.nhs.uk"
              invalid={submitted && !!errors.email} onChange={(e) => setEmail(e.target.value)} />
            {submitted && errors.email ? <FieldError>{errors.email}</FieldError> : null}
          </Field>
          <Field>
            <span className="hd-auth__row">
              <FieldLabel htmlFor="hd-signin-password">Password</FieldLabel>
              <Button variant="link" size="xs" onClick={() => onNavigate("/forgot-password")}>Forgot password?</Button>
            </span>
            <Input id="hd-signin-password" type={reveal ? "text" : "password"} autoComplete="current-password" value={password}
              invalid={submitted && !!errors.password} onChange={(e) => setPassword(e.target.value)}
              endAddon={
                <Button variant="ghost" size="icon-xs" aria-label={reveal ? "Hide password" : "Show password"} onClick={() => setReveal(!reveal)}>
                  <Icon name={reveal ? "eye-off" : "eye"} size={14} />
                </Button>
              } />
            {submitted && errors.password ? <FieldError>{errors.password}</FieldError> : null}
          </Field>
          <Button disabled={!dirty} onClick={submit}>Sign in</Button>
        </div>
      </AuthShell>
    );
  }

  function ForgotPasswordScreen({ email, onSubmit, onNavigate }) {
    const [value, setValue] = React.useState(email || "");
    const [submitted, setSubmitted] = React.useState(false);
    const error = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value.trim()) ? null : "Enter a valid email address.";
    return (
      <AuthShell title="Forgot password" claim={CLAIM} fine={FINE}
        lede="We will send a link that sets a new password. The link works once and expires after an hour."
        foot={<>Remembered it? <Button variant="link" size="xs" onClick={() => onNavigate("/signin")}>Back to sign in</Button></>}>
        <div className="hd-auth__fields">
          <Field>
            <FieldLabel htmlFor="hd-forgot-email">Work email</FieldLabel>
            <Input id="hd-forgot-email" type="email" autoComplete="username" value={value} placeholder="i.vance@haldengeneral.nhs.uk"
              invalid={submitted && !!error} onChange={(e) => setValue(e.target.value)} />
            {submitted && error ? <FieldError>{error}</FieldError> : <FieldDescription>The address your rota and payslips go to.</FieldDescription>}
          </Field>
          <Button disabled={!value} onClick={() => { setSubmitted(true); if (!error) onSubmit(value.trim()); }}>Send reset link</Button>
        </div>
      </AuthShell>
    );
  }

  function ResetLinkSentScreen({ email, onNavigate }) {
    return (
      <AuthShell title="Check your email" claim={CLAIM} fine={FINE}
        lede={"If an account exists for " + email + ", we have sent a link that sets a new password. It expires in " + HD_RESET_LINK.expiresInMinutes + " minutes."}
        foot={<>Nothing arrived within ten minutes? Check the junk folder, then call the service desk on ext 4000.</>}>
        <div className="hd-auth__fields">
          <Button variant="outline" onClick={() => onNavigate("/forgot-password")}>Send it again</Button>
          <Separator />
          <Button variant="link" onClick={() => onNavigate("/signin")}>Back to sign in</Button>
        </div>
      </AuthShell>
    );
  }

  function SetNewPasswordScreen({ now, onSubmit, onNavigate }) {
    const [value, setValue] = React.useState("");
    const [reveal, setReveal] = React.useState(false);
    const [submitted, setSubmitted] = React.useState(false);
    const met = PASSWORD_RULES.map((rule) => ({ id: rule.id, label: rule.label, ok: rule.test(value) }));
    const valid = met.every((r) => r.ok);
    const minutesLeft = Math.max(0, HD_RESET_LINK.expiresInMinutes - Math.round((now - HD_RESET_LINK.issuedAt) / 6e4));
    return (
      <AuthShell title="Set a new password" claim={CLAIM} fine={FINE}
        lede={"For " + HD_RESET_LINK.email + ". This link expires in " + minutesLeft + " minutes and works once."}
        foot={<>Link expired while you were reading? <Button variant="link" size="xs" onClick={() => onNavigate("/reset-password/expired")}>See what to do</Button></>}>
        <div className="hd-auth__fields">
          <Field>
            <FieldLabel htmlFor="hd-reset-password">New password</FieldLabel>
            <Input id="hd-reset-password" type={reveal ? "text" : "password"} autoComplete="new-password" value={value}
              invalid={submitted && !valid} onChange={(e) => setValue(e.target.value)}
              endAddon={
                <Button variant="ghost" size="icon-xs" aria-label={reveal ? "Hide password" : "Show password"} onClick={() => setReveal(!reveal)}>
                  <Icon name={reveal ? "eye-off" : "eye"} size={14} />
                </Button>
              } />
            {submitted && !valid ? <FieldError>Your password does not meet every requirement yet.</FieldError> : null}
          </Field>
          <div className="hd-checklist">
            {met.map((rule) => (
              <span className="hd-checklist__item" key={rule.id} data-met={rule.ok ? "true" : "false"}>
                <Icon name={rule.ok ? "check" : "dot"} size={14} />{rule.label}
              </span>
            ))}
          </div>
          <Button disabled={!value} onClick={() => { setSubmitted(true); if (valid) onSubmit(); }}>Set new password</Button>
        </div>
      </AuthShell>
    );
  }

  function ResetLinkExpiredScreen({ onNavigate }) {
    return (
      <AuthShell title="That link has expired" claim={CLAIM} fine={FINE}
        lede={"A reset link works once and lasts " + HD_RESET_LINK.expiresInMinutes + " minutes. Ask for a new one and it will arrive within a minute."}
        foot={<>Somebody else may have used it. If you did not ask for a reset, call the service desk on ext 4000.</>}>
        <div className="hd-auth__fields">
          <Button onClick={() => onNavigate("/forgot-password")}>Send a new link</Button>
          <Button variant="link" onClick={() => onNavigate("/signin")}>Back to sign in</Button>
        </div>
      </AuthShell>
    );
  }

  function PasswordResetDoneScreen({ email, onNavigate }) {
    return (
      <AuthShell title="Password changed" claim={CLAIM} fine={FINE}
        lede={"The password for " + email + " has been changed and every other session has been signed out — including the ward station you may have left open."}
        foot={<>Did not change it yourself? Call the service desk on ext 4000 immediately.</>}>
        <div className="hd-auth__fields">
          <Button onClick={() => onNavigate("/signin")}>Sign in</Button>
        </div>
      </AuthShell>
    );
  }

  Object.assign(window, { SignInScreen, ForgotPasswordScreen, ResetLinkSentScreen, SetNewPasswordScreen, ResetLinkExpiredScreen, PasswordResetDoneScreen });
})();

})();
</script>
<script type="text/babel" data-presets="react" data-file="App.jsx">
(function(){
/* Ward Desk — routing and every commit in the product.
   The reversibility tiers are decided here, once (§3):
     · instant and harmless   → a bare control (a reaction, a filter)
     · instant, consequential → applies immediately plus an undo toast
     · irreversible           → a confirm dialog: discharging somebody, recording
                                a procedure as abandoned, stopping a course
     · needs three decisions  → a staged dialog opened from an instant screen
   Auth renders outside the shell (§7.1): it has no navigation owner, so it is
   returned before HdShell is reached rather than nested inside it. */
function App(props) {
  const options = props || {};
  const [path, setPath] = React.useState(options.startPath || "/");
  const [dark, setDark] = React.useState(options.theme === "dark");
  const [read, setRead] = React.useState(false);
  const [toasts, setToasts] = React.useState([]);
  const [confirm, setConfirm] = React.useState(null);
  const [form, setForm] = React.useState(null);
  const [medWard, setMedWard] = React.useState("all");
  const [reviewingId, setReviewingId] = React.useState(null);
  /* The address the reset flow is working on: the request screen sets it and the
     confirmation states it, so the confirmation never claims one nobody typed. */
  const [authEmail, setAuthEmail] = React.useState(HD_RESET_LINK.email);
  const [data, setData] = React.useState({
    patients: PATIENTS, admissions: ADMISSIONS, interventions: INTERVENTIONS, orders: MEDICATION_ORDERS,
    formulary: FORMULARY, staff: STAFF, comments: COMMENTS, activity: ACTIVITY,
    transfers: TRANSFERS, transferRows: TRANSFER_ROWS, wardBeds: WARD_BEDS, rota: ROTA,
    invoices: INVOICES, payments: PAYMENTS, tariffItems: TARIFF_ITEMS, contracts: CONTRACTS,
    contractPrices: CONTRACT_PRICES, chases: CHASES, sessions: SESSIONS,
  });
  const [hospital, setHospital] = React.useState(HD_HOSPITAL);
  const [prefs, setPrefs] = React.useState(NOTIFICATION_PREFS);
  const [billing, setBilling] = React.useState(BILLING_SETTINGS);
  const [integrations, setIntegrations] = React.useState(INTEGRATIONS);
  const [security, setSecurity] = React.useState(SECURITY_SETTINGS);
  /* Bumped when the role matrix is saved: the capability maps in enums.js are
     what every transition function reads, so writing into them is what makes
     Settings real — and this makes the render that follows it honest. */
  const [rolesVersion, setRolesVersion] = React.useState(0);
  const palette = useCommandPalette(!!confirm || !!form);
  const now = HD_NOW;
  /* The signed-in person is a staff record, not a second literal: the profile
     screen edits the same entry every permission predicate reads. */
  const viewer = data.staff.find((s) => s.id === HD_SIGNED_IN);

  React.useEffect(() => { if (options.startPath) setPath(options.startPath); }, [options.startPath]);
  React.useEffect(() => { setDark(options.theme === "dark"); }, [options.theme]);
  React.useEffect(() => {
    document.documentElement.classList.toggle("dark", dark);
    document.body.classList.toggle("dark", dark);
  }, [dark]);
  /* Anything scoped to one record closes when the route changes: the row-review
     state, the confirm dialog and the staged dialog all belong to the screen they
     were opened from, and a live commit closure must not outlive it. */
  React.useEffect(() => {
    if (path !== "/data") setReviewingId(null);
    setConfirm(null);
    setForm(null);
  }, [path]);

  const dismiss = (id) => setToasts((list) => list.filter((t) => t.id !== id));
  const pushToast = (toast) => {
    const id = "t" + Date.now() + Math.random().toString(36).slice(2, 6);
    setToasts((list) => list.concat([Object.assign({ id: id }, toast)]));
    setTimeout(() => dismiss(id), 7000);
  };

  const patch = (key, id, fields) => setData((d) => Object.assign({}, d, { [key]: d[key].map((r) => (r.id === id ? Object.assign({}, r, fields) : r)) }));
  const restore = (key, snapshot) => setData((d) => Object.assign({}, d, { [key]: d[key].map((r) => (r.id === snapshot.id ? snapshot : r)) }));
  const logActivity = (verb, record, note) => {
    const id = "act_" + Math.random().toString(36).slice(2, 7);
    setData((d) => Object.assign({}, d, { activity: [{ id: id, actorId: viewer.id, at: now, verb: verb, record: record, note: note || null }].concat(d.activity) }));
    return id;
  };
  const unlogActivity = (id) => setData((d) => Object.assign({}, d, { activity: d.activity.filter((a) => a.id !== id) }));

  /* ── Admissions ────────────────────────────────────────────────────────── */
  const assignBed = (admission) => {
    const patient = patientById(data.patients, admission.patientId);
    const taken = data.admissions.filter((a) => a.ward === admission.ward && a.bed).map((a) => a.bed);
    const prefix = wardName(admission.ward)[0].toUpperCase();
    const free = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((n) => prefix + "-" + String(n).padStart(2, "0")).filter((bed) => taken.indexOf(bed) < 0);
    setForm({
      title: "Assign a bed to " + patient.name,
      description: "They have waited " + formatCountdown(admission.requestedAt, now).replace(" over", "").replace(" left", "") + " for a bed on " + wardName(admission.ward) + ". Assigning one puts them on the ward and starts their length of stay.",
      submitLabel: "Assign bed",
      initial: { bed: free[0], ward: admission.ward },
      fields: [
        { id: "ward", kind: "select", label: "Ward", options: WARDS.map((w) => ({ value: w.value, label: labelOf(WARDS, w.value) })), hint: "Changing the ward moves the request as well as the bed." },
        { id: "bed", kind: "select", label: "Bed", options: free.map((bed) => ({ value: bed, label: bed })), hint: free.length + " beds on " + wardName(admission.ward) + " have nobody against them." },
      ],
      validate: (v) => (!v.bed ? { bed: "Pick a bed." } : {}),
      run: (v) => {
        const snapshot = admission;
        patch("admissions", admission.id, { status: "on_ward", ward: v.ward, bed: v.bed, admittedAt: now });
        patch("patients", patient.id, { status: "inpatient" });
        const act = logActivity("assigned a bed", { type: "admission", id: admission.id }, wardName(v.ward) + " " + v.bed);
        pushToast({
          variant: "success",
          title: "Bed assigned",
          description: patient.name + " is on " + wardName(v.ward) + " " + v.bed + " · " + (bedsWaiting(data.admissions).length - 1) + " people still waiting for a bed",
          onUndo: () => { restore("admissions", snapshot); restore("patients", patient); unlogActivity(act); },
        });
      },
    });
  };

  const markFit = (admission) => {
    const patient = patientById(data.patients, admission.patientId);
    patch("admissions", admission.id, { status: "fit_for_discharge", fitAt: now });
    const act = logActivity("marked fit for discharge", { type: "admission", id: admission.id }, null);
    pushToast({
      title: "Marked fit for discharge",
      description: patient.name + " · " + wardName(admission.ward) + " " + admission.bed + " is now being held until they go home",
      onUndo: () => { restore("admissions", admission); unlogActivity(act); },
    });
  };

  const bulkMarkFit = (admissions) => {
    const snapshot = data.admissions;
    const ids = admissions.map((a) => a.id);
    setData((d) => Object.assign({}, d, {
      admissions: d.admissions.map((a) => (ids.indexOf(a.id) > -1 ? Object.assign({}, a, { status: "fit_for_discharge", fitAt: now }) : a)),
    }));
    pushToast({
      title: admissions.length + (admissions.length === 1 ? " admission marked fit" : " admissions marked fit"),
      description: admissions.map((a) => patientName(data.patients, a.patientId)).join(", ") + " — their beds are held until they go home",
      onUndo: () => setData((d) => Object.assign({}, d, { admissions: snapshot })),
    });
  };

  const discharge = (admission) => {
    const patient = patientById(data.patients, admission.patientId);
    const live = activeOrders(ordersForPatient(data.orders, patient.id));
    setConfirm({
      title: "Discharge " + patient.name + "?",
      description: "They leave " + wardName(admission.ward) + " " + admission.bed + " after " + ADMISSION_FLAGS.lengthOfStayDays(admission, now) + " days. " +
        (live.length ? live.length + (live.length === 1 ? " active order" : " active orders") + " on their chart will be closed with the episode and cannot be reopened." : "Nothing is active on their chart.") +
        " The bed is released immediately.",
      confirmLabel: "Discharge " + patient.name.split(" ")[0],
      run: () => {
        patch("admissions", admission.id, { status: "discharged", dischargedAt: now });
        patch("patients", patient.id, { status: "discharged" });
        setData((d) => Object.assign({}, d, {
          orders: d.orders.map((o) => (o.patientId === patient.id && o.status === "active" ? Object.assign({}, o, { status: "completed", endsAt: now, nextDoseAt: null }) : o)),
        }));
        logActivity("discharged", { type: "admission", id: admission.id }, wardName(admission.ward) + " " + admission.bed + " released");
        pushToast({ title: "Discharged", description: patient.name + " has left " + wardName(admission.ward) + " " + admission.bed + " · " + live.length + (live.length === 1 ? " order closed" : " orders closed") });
      },
    });
  };

  const transferOut = (admission) => {
    const patient = patientById(data.patients, admission.patientId);
    setForm({
      title: "Transfer " + patient.name + " out",
      description: "They leave " + HD_HOSPITAL.short + " for another hospital. The episode closes here and the receiving trust takes over the record.",
      submitLabel: "Transfer out",
      initial: { destination: "", reason: "" },
      fields: [
        { id: "destination", kind: "input", label: "Receiving hospital", placeholder: "Westmere General" },
        { id: "reason", kind: "text", label: "Why", placeholder: "Needs electrophysiology, not available here.", hint: "Written onto the record as a note." },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.destination || v.destination.trim().length < 3) errors.destination = "Name the hospital taking them.";
        if (!v.reason || v.reason.trim().length < 10) errors.reason = "Say why — a sentence is enough.";
        return errors;
      },
      run: (v) => {
        const snapshot = admission;
        patch("admissions", admission.id, { status: "transferred", dischargedAt: now, reason: "Transferred to " + v.destination.trim() + " — " + v.reason.trim() });
        const act = logActivity("transferred out", { type: "admission", id: admission.id }, "To " + v.destination.trim());
        pushToast({
          title: "Transferred out",
          description: patient.name + " → " + v.destination.trim() + " · the bed on " + wardName(admission.ward) + " is released",
          onUndo: () => { restore("admissions", snapshot); unlogActivity(act); },
        });
      },
    });
  };

  const onAdmissionTransition = (admission, transition) => {
    if (transition.id === "assign_bed") assignBed(admission);
    if (transition.id === "mark_fit") markFit(admission);
    if (transition.id === "discharge") discharge(admission);
    if (transition.id === "transfer_out") transferOut(admission);
  };

  /* ── Interventions ─────────────────────────────────────────────────────── */
  const scheduleIntervention = (intervention) => {
    const patient = patientById(data.patients, intervention.patientId);
    setForm({
      title: "Schedule " + intervention.name,
      description: "For " + patient.name + ", requested " + formatWhen(intervention.requestedAt, now) + ". Scheduling does not record consent — that is a separate step, and the procedure cannot start without it.",
      submitLabel: "Schedule",
      initial: { theatre: "t1", day: "1", time: "09:00" },
      fields: [
        { id: "theatre", kind: "select", label: "Theatre", options: THEATRES.map((t) => ({ value: t.value, label: t.label })) },
        { id: "day", kind: "select", label: "Day", options: [0, 1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: n === 0 ? "Today" : new Date(now + n * DAY_MS).toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "short" }) })) },
        { id: "time", kind: "input", label: "Start time", placeholder: "09:00", hint: "24-hour clock. " + intervention.plannedMin + " minutes are planned for it." },
      ],
      validate: (v) => (!/^\d\d:\d\d$/.test(v.time || "") ? { time: "Use HH:MM." } : {}),
      run: (v) => {
        const snapshot = intervention;
        const [h, m] = v.time.split(":").map(Number);
        const at = dayAt(Number(v.day), h, m);
        patch("interventions", intervention.id, { status: "scheduled", theatre: v.theatre, scheduledAt: at });
        const act = logActivity("scheduled a procedure", { type: "intervention", id: intervention.id }, labelOf(THEATRES, v.theatre) + " · " + formatWhen(at, now));
        pushToast({
          variant: "success",
          title: "Scheduled",
          description: intervention.name + " · " + labelOf(THEATRES, v.theatre) + " · " + formatWhen(at, now) + " · consent is still not recorded, so it cannot start",
          onUndo: () => { restore("interventions", snapshot); unlogActivity(act); },
        });
      },
    });
  };

  const onInterventionTransition = (intervention, transition) => {
    const patient = patientById(data.patients, intervention.patientId);
    if (transition.id === "schedule") return scheduleIntervention(intervention);
    if (transition.id === "record_consent") {
      patch("interventions", intervention.id, { consentAt: now });
      const act = logActivity("recorded consent", { type: "intervention", id: intervention.id }, null);
      return pushToast({
        variant: "success", title: "Consent recorded",
        description: patient.name + " · " + intervention.name + " can now start · " + (consentMissing(data.interventions).length - 1) + " procedures still without consent",
        onUndo: () => { restore("interventions", intervention); unlogActivity(act); },
      });
    }
    if (transition.id === "start") {
      patch("interventions", intervention.id, { status: "in_theatre" });
      const act = logActivity("started in theatre", { type: "intervention", id: intervention.id }, labelOf(THEATRES, intervention.theatre));
      return pushToast({ title: "In theatre", description: intervention.name + " · " + patient.name + " · " + labelOf(THEATRES, intervention.theatre), onUndo: () => { restore("interventions", intervention); unlogActivity(act); } });
    }
    if (transition.id === "to_recovery") {
      patch("interventions", intervention.id, { status: "recovery" });
      const act = logActivity("moved to recovery", { type: "intervention", id: intervention.id }, null);
      return pushToast({ title: "In recovery", description: patient.name + " left " + labelOf(THEATRES, intervention.theatre) + " at " + formatWhen(now, now), onUndo: () => { restore("interventions", intervention); unlogActivity(act); } });
    }
    if (transition.id === "complete") {
      patch("interventions", intervention.id, { status: "completed", completedAt: now });
      const act = logActivity("completed a procedure", { type: "intervention", id: intervention.id }, intervention.plannedMin + " minutes planned");
      return pushToast({ variant: "success", title: "Completed", description: intervention.name + " · " + patient.name, onUndo: () => { restore("interventions", intervention); unlogActivity(act); } });
    }
    if (transition.id === "cancel") {
      patch("interventions", intervention.id, { status: "cancelled" });
      const act = logActivity("cancelled a procedure", { type: "intervention", id: intervention.id }, null);
      return pushToast({
        title: "Cancelled",
        description: intervention.name + " · " + patient.name + (intervention.scheduledAt ? " · the " + labelOf(THEATRES, intervention.theatre) + " slot at " + formatWhen(intervention.scheduledAt, now) + " is free" : ""),
        onUndo: () => { restore("interventions", intervention); unlogActivity(act); },
      });
    }
    if (transition.id === "abandon") {
      return setConfirm({
        title: "Record " + intervention.name + " as abandoned?",
        description: "This says the procedure started and could not be finished. It goes on " + patient.name + "'s record permanently and cannot be undone — a new request would be needed to try again.",
        confirmLabel: "Record as abandoned",
        run: () => {
          patch("interventions", intervention.id, { status: "abandoned", completedAt: now });
          logActivity("recorded a procedure as abandoned", { type: "intervention", id: intervention.id }, null);
          pushToast({ variant: "error", title: "Recorded as abandoned", description: intervention.name + " · " + patient.name });
        },
      });
    }
  };

  /* ── Medication ────────────────────────────────────────────────────────── */
  const DOSE_HOURS = { "Once daily": 24, "Twice daily": 12, "Three times daily": 8, "Four times daily": 6 };

  const onMedicationTransition = (order, transition) => {
    const patient = patientById(data.patients, order.patientId);
    const drug = drugById(data.formulary, order.drugId);
    if (transition.id === "verify") {
      const hours = DOSE_HOURS[order.frequency] || 8;
      patch("orders", order.id, { status: "active", verifiedById: viewer.id, nextDoseAt: order.nextDoseAt || now + hours * HOUR_MS });
      const act = logActivity("verified an order", { type: "medication", id: order.id }, null);
      return pushToast({
        variant: "success", title: "Order verified",
        description: order.drug + " " + order.dose + " for " + patient.name + " · " + (ordersAwaitingPharmacy(data.orders).length - 1) + " orders still awaiting a pharmacist",
        onUndo: () => { restore("orders", order); unlogActivity(act); },
      });
    }
    if (transition.id === "record_dose") {
      const hours = DOSE_HOURS[order.frequency] || 8;
      patch("orders", order.id, { lastGivenAt: now, nextDoseAt: now + hours * HOUR_MS });
      const act = logActivity("recorded a dose", { type: "medication", id: order.id }, order.dose + " " + labelOf(DRUG_ROUTES, order.route).toLowerCase());
      return pushToast({
        title: "Dose recorded",
        description: order.drug + " " + order.dose + " given to " + patient.name + " · next dose " + formatWhen(now + hours * HOUR_MS, now),
        onUndo: () => { restore("orders", order); unlogActivity(act); },
      });
    }
    if (transition.id === "hold") {
      patch("orders", order.id, { status: "on_hold", nextDoseAt: null });
      const act = logActivity("put an order on hold", { type: "medication", id: order.id }, null);
      return pushToast({ title: "On hold", description: order.drug + " for " + patient.name + " — no further doses until somebody resumes it", onUndo: () => { restore("orders", order); unlogActivity(act); } });
    }
    if (transition.id === "resume") {
      const hours = DOSE_HOURS[order.frequency] || 8;
      patch("orders", order.id, { status: "active", nextDoseAt: now + hours * HOUR_MS });
      const act = logActivity("resumed an order", { type: "medication", id: order.id }, null);
      return pushToast({ variant: "success", title: "Resumed", description: order.drug + " for " + patient.name + " · next dose " + formatWhen(now + hours * HOUR_MS, now), onUndo: () => { restore("orders", order); unlogActivity(act); } });
    }
    if (transition.id === "stop") {
      return setConfirm({
        title: "Stop " + order.drug + " for " + patient.name + "?",
        description: "The course closes now and cannot be restarted — a new order would have to be written and verified again by a pharmacist." + (drug ? " " + drug.name + " has " + formatCount(drug.onHand) + " " + drug.unit + " in stock." : ""),
        confirmLabel: "Stop course",
        run: () => {
          patch("orders", order.id, { status: "stopped", endsAt: now, nextDoseAt: null });
          logActivity("stopped a course", { type: "medication", id: order.id }, null);
          pushToast({ title: "Course stopped", description: order.drug + " " + order.dose + " · " + patient.name });
        },
      });
    }
  };

  /* ── Formulary ─────────────────────────────────────────────────────────── */
  const onReorder = (drug) => {
    patch("formulary", drug.id, { reorderedAt: now });
    pushToast({
      title: "Stock ordered",
      description: drug.name + " " + drug.form + " · " + drug.supplier + " · " + ordersForDrug(data.orders, drug.id).length + " live courses depend on it",
      onUndo: () => restore("formulary", drug),
    });
  };

  /* ── Creating records (§5: everything the product lists can be made) ───── */
  const createPatient = (values) => {
    const id = "pat_" + Math.random().toString(36).slice(2, 6);
    const number = "H-40-" + Math.floor(4600 + Math.random() * 300);
    const patient = {
      id: id, name: values.name.trim(), number: number, born: values.born.trim(), sex: values.sex,
      status: values.status, avatar: null, allergies: values.allergies.split(",").map((a) => a.trim()).filter(Boolean),
      lastReviewAt: now, gp: values.gp.trim(), phone: values.phone.trim(), registeredAt: now,
      nextOfKin: values.nextOfKin.trim() || "Not recorded",
    };
    setData((d) => Object.assign({}, d, { patients: d.patients.concat([patient]) }));
    setPath("/patients/" + id);
    pushToast({
      variant: "success", title: "Patient registered",
      description: patient.name + " · " + number + " · " + (patient.allergies.length ? patient.allergies.length + " allergy alert" + (patient.allergies.length === 1 ? "" : "s") + " recorded" : "no allergies recorded") + " · no photograph yet",
      onUndo: () => { setData((d) => Object.assign({}, d, { patients: d.patients.filter((p) => p.id !== id) })); setPath("/patients"); },
    });
  };

  const newAdmission = () => {
    const admittable = data.patients.filter((p) => {
      const admission = admissionOfPatient(data.admissions, p.id);
      return !admission || admission.status === "discharged" || admission.status === "transferred";
    });
    setForm({
      title: "New admission",
      description: "This requests a bed. Nobody is on a ward until a bed is assigned, and the wait is on the clock from the moment you save.",
      submitLabel: "Request bed",
      initial: { patient: admittable.length ? admittable[0].id : "", ward: "linden", source: "ed", reason: "", stay: "3" },
      fields: [
        { id: "patient", kind: "select", label: "Patient", options: admittable.map((p) => ({ value: p.id, label: p.name + " — " + p.number })), hint: admittable.length + " of " + data.patients.length + " registered patients are not on a ward." },
        { id: "ward", kind: "select", label: "Ward", options: WARDS.map((w) => ({ value: w.value, label: labelOf(WARDS, w.value) })) },
        { id: "source", kind: "select", label: "Coming from", options: ADMISSION_SOURCES },
        { id: "reason", kind: "input", label: "Why they need a bed", placeholder: "Chest pain, troponin rise" },
        { id: "stay", kind: "input", label: "Expected stay in days", hint: "Used to flag a stay that runs long." },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.patient) errors.patient = "Pick a patient, or register them first.";
        if (!v.reason || v.reason.trim().length < 5) errors.reason = "Say why in a few words.";
        const days = Number(v.stay);
        if (!(days >= 1 && days <= 60)) errors.stay = "Between 1 and 60 days.";
        return errors;
      },
      run: (v) => {
        const id = "adm_" + Math.random().toString(36).slice(2, 6);
        const admission = {
          id: id, patientId: v.patient, ward: v.ward, bed: null, status: "awaiting_bed", source: v.source,
          reason: v.reason.trim(), requestedAt: now, admittedAt: null, fitAt: null, dischargedAt: null,
          expectedStayDays: Number(v.stay), consultantId: viewer.family === "medical" ? viewer.id : "stf_04",
        };
        setData((d) => Object.assign({}, d, { admissions: d.admissions.concat([admission]) }));
        logActivity("requested a bed", { type: "admission", id: id }, v.reason.trim());
        pushToast({
          variant: "success", title: "Bed requested",
          description: patientName(data.patients, v.patient) + " · " + wardName(v.ward) + " · " + (bedsWaiting(data.admissions).length + 1) + " people are now waiting for a bed",
          onUndo: () => setData((d) => Object.assign({}, d, { admissions: d.admissions.filter((a) => a.id !== id) })),
        });
      },
    });
  };

  const newIntervention = () => setForm({
    title: "New intervention",
    description: "It starts as a request with no theatre and no consent. Scheduling it and recording consent are the next two steps, in that order.",
    submitLabel: "Request procedure",
    initial: { patient: data.patients[0].id, name: "", specialty: "general_surgery", priority: "routine", surgeon: doctorsOf(data.staff)[0].id, minutes: "60" },
    fields: [
      { id: "patient", kind: "select", label: "Patient", options: data.patients.map((p) => ({ value: p.id, label: p.name + " — " + p.number })) },
      { id: "name", kind: "input", label: "Procedure", placeholder: "Laparoscopic cholecystectomy" },
      { id: "specialty", kind: "select", label: "Specialty", options: SPECIALTIES },
      { id: "priority", kind: "select", label: "Priority", options: PRIORITIES },
      { id: "surgeon", kind: "select", label: "Under", options: doctorsOf(data.staff).filter((s) => s.status === "active").map((s) => ({ value: s.id, label: s.name + " — " + s.jobTitle })) },
      { id: "minutes", kind: "input", label: "Planned minutes", hint: "What theatre will hold for it." },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.name || v.name.trim().length < 4) errors.name = "Name the procedure.";
      const minutes = Number(v.minutes);
      if (!(minutes >= 10 && minutes <= 480)) errors.minutes = "Between 10 and 480 minutes.";
      return errors;
    },
    run: (v) => {
      const id = "int_" + Math.random().toString(36).slice(2, 6);
      const intervention = {
        id: id, patientId: v.patient, name: v.name.trim(), specialty: v.specialty, priority: v.priority,
        status: "requested", theatre: null, requestedAt: now, scheduledAt: null, consentAt: null,
        surgeonId: v.surgeon, anaesthetistId: null, plannedMin: Number(v.minutes), completedAt: null,
        notes: "Requested by " + viewer.name + " on " + formatAbsolute(now).split(",")[0] + ".",
      };
      setData((d) => Object.assign({}, d, { interventions: d.interventions.concat([intervention]) }));
      logActivity("requested a procedure", { type: "intervention", id: id }, labelOf(PRIORITIES, v.priority));
      setPath("/interventions/" + id);
      pushToast({
        variant: "success", title: "Procedure requested",
        description: intervention.name + " · " + patientName(data.patients, v.patient) + " · not scheduled and no consent, so it counts against both badges",
        onUndo: () => { setData((d) => Object.assign({}, d, { interventions: d.interventions.filter((i) => i.id !== id) })); setPath("/interventions"); },
      });
    },
  });

  const newOrder = () => setForm({
    title: "New medication order",
    description: "It arrives awaiting verification. A pharmacist has to verify it before the first dose can be given.",
    submitLabel: "Write order",
    initial: { patient: data.patients[0].id, drug: data.formulary[0].id, dose: "", frequency: "Three times daily", indication: "" },
    fields: [
      { id: "patient", kind: "select", label: "Patient", options: data.patients.map((p) => ({ value: p.id, label: p.name + " — " + p.number + (PATIENT_FLAGS.hasAllergyAlert(p) ? " · allergic to " + p.allergies.join(", ") : "") })) },
      { id: "drug", kind: "select", label: "Drug", options: data.formulary.filter((d) => d.stock !== "discontinued").map((d) => ({ value: d.id, label: d.name + " " + d.form + (d.stock === "out" ? " — out of stock" : "") })) },
      { id: "dose", kind: "input", label: "Dose", placeholder: "1 g" },
      { id: "frequency", kind: "select", label: "Frequency", options: ["Once daily", "Twice daily", "Three times daily", "Four times daily", "As required, eight hourly"].map((f) => ({ value: f, label: f })) },
      { id: "indication", kind: "input", label: "Indication", placeholder: "Wound infection" },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.dose || !/\d/.test(v.dose)) errors.dose = "Give a dose with a number in it.";
      if (!v.indication || v.indication.trim().length < 4) errors.indication = "Say what it is for.";
      const drug = drugById(data.formulary, v.drug);
      const patient = patientById(data.patients, v.patient);
      if (drug && patient && patient.allergies.some((a) => drug.name.toLowerCase().indexOf(a.toLowerCase().slice(0, 6)) > -1)) errors.drug = patient.name + " is recorded as allergic to " + patient.allergies.join(", ") + ".";
      return errors;
    },
    run: (v) => {
      const id = "med_" + Math.random().toString(36).slice(2, 6);
      const drug = drugById(data.formulary, v.drug);
      const order = {
        id: id, patientId: v.patient, drug: drug.name, dose: v.dose.trim(), route: drug.route, frequency: v.frequency,
        status: "awaiting_verification", indication: v.indication.trim(), prescriberId: viewer.id, verifiedById: null,
        startedAt: now, nextDoseAt: null, lastGivenAt: null, endsAt: null, drugId: drug.id,
      };
      setData((d) => Object.assign({}, d, { orders: d.orders.concat([order]) }));
      logActivity("wrote an order", { type: "medication", id: id }, drug.name + " " + v.dose.trim());
      pushToast({
        variant: "success", title: "Order written",
        description: drug.name + " " + order.dose + " for " + patientName(data.patients, v.patient) + " · " + (ordersAwaitingPharmacy(data.orders).length + 1) + " orders now awaiting a pharmacist" + (drug.stock === "out" ? " · nothing in stock to dispense" : ""),
        onUndo: () => setData((d) => Object.assign({}, d, { orders: d.orders.filter((o) => o.id !== id) })),
      });
    },
  });

  const newFormularyLine = () => setForm({
    title: "New formulary line",
    description: "A line can be prescribed the moment it exists. Its stock state is worked out from what you hold against the reorder level.",
    submitLabel: "Add line",
    initial: { name: "", form: "", route: "oral", onHand: "0", reorderAt: "50", unit: "tablets", supplier: "Trust wholesaler" },
    fields: [
      { id: "name", kind: "input", label: "Drug", placeholder: "Oxycodone" },
      { id: "form", kind: "input", label: "Form", placeholder: "5 mg capsule" },
      { id: "route", kind: "select", label: "Route", options: DRUG_ROUTES },
      { id: "onHand", kind: "input", label: "Held now" },
      { id: "reorderAt", kind: "input", label: "Reorder at" },
      { id: "unit", kind: "input", label: "Counted in", placeholder: "capsules" },
      { id: "supplier", kind: "input", label: "Supplier" },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.name || v.name.trim().length < 3) errors.name = "Name the drug.";
      else if (data.formulary.some((d) => d.name.toLowerCase() === v.name.trim().toLowerCase())) errors.name = "That drug is already in the formulary.";
      if (!v.form || v.form.trim().length < 3) errors.form = "Give the form and strength.";
      if (!/^\d+$/.test(v.onHand || "")) errors.onHand = "A whole number.";
      if (!/^\d+$/.test(v.reorderAt || "")) errors.reorderAt = "A whole number.";
      return errors;
    },
    run: (v) => {
      const id = "drg_" + Math.random().toString(36).slice(2, 6);
      const onHand = Number(v.onHand), reorderAt = Number(v.reorderAt);
      const line = {
        id: id, name: v.name.trim(), form: v.form.trim(), route: v.route,
        stock: onHand === 0 ? "out" : onHand <= reorderAt ? "low" : "in_stock",
        onHand: onHand, reorderAt: reorderAt, unit: v.unit.trim() || "units", supplier: v.supplier.trim(),
        earliestExpiry: null, controlled: false,
      };
      setData((d) => Object.assign({}, d, { formulary: d.formulary.concat([line]) }));
      pushToast({
        variant: "success", title: "Formulary line added",
        description: line.name + " " + line.form + " · " + statusValue(STOCK_STATUS, line.stock).label.toLowerCase() + " at " + formatCount(onHand) + " " + line.unit,
        onUndo: () => setData((d) => Object.assign({}, d, { formulary: d.formulary.filter((x) => x.id !== id) })),
      });
    },
  });

  const newEmployee = () => setForm({
    title: "New employee",
    description: "They arrive onboarding: the badge exists and Ward Desk works, but nothing is signed in their name until somebody marks them active.",
    submitLabel: "Add employee",
    initial: { name: "", email: "", family: "nursing", jobTitle: "", staffRole: "clinician", ward: "beaumont" },
    fields: [
      { id: "name", kind: "input", label: "Name", placeholder: "Saoirse Whelan" },
      { id: "email", kind: "input", label: "Work email", placeholder: "s.whelan@haldengeneral.nhs.uk" },
      { id: "family", kind: "select", label: "Job family", options: JOB_FAMILIES },
      { id: "jobTitle", kind: "input", label: "Job title", placeholder: "Staff nurse" },
      { id: "staffRole", kind: "radio", label: "What they may do", options: STAFF_ROLES.map((role) => ({ value: role.value, label: role.label + " — " + (ROLE_CAN_VERIFY_MEDICATION[role.value] ? "verifies medication orders" : ROLE_CAN_MOVE_ADMISSION[role.value] ? "moves admissions and beds" : "reads only") })) },
      { id: "ward", kind: "select", label: "Base ward", options: [{ value: "", label: "No base ward" }].concat(WARDS.map((w) => ({ value: w.value, label: wardName(w.value) }))) },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.name || v.name.trim().split(/\s+/).length < 2) errors.name = "Give a full name.";
      if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test((v.email || "").trim())) errors.email = "Enter a valid email address.";
      else if (data.staff.some((s) => s.email.toLowerCase() === v.email.trim().toLowerCase())) errors.email = "Somebody already has that address.";
      if (!v.jobTitle || v.jobTitle.trim().length < 3) errors.jobTitle = "Give their job title.";
      return errors;
    },
    run: (v) => {
      const id = "stf_" + Math.random().toString(36).slice(2, 6);
      const person = {
        id: id, name: v.name.trim(), email: v.email.trim().toLowerCase(), family: v.family,
        jobTitle: v.jobTitle.trim(), specialty: null, staffRole: v.staffRole, status: "onboarding",
        ward: v.ward || null, avatar: null, startedAt: now, phone: "ext 4000", gmc: null, timezone: hospital.timezone,
      };
      setData((d) => Object.assign({}, d, { staff: d.staff.concat([person]) }));
      pushToast({
        variant: "success", title: "Employee added",
        description: person.name + " · " + labelOf(JOB_FAMILIES, v.family) + " · " + labelOf(STAFF_ROLES, v.staffRole) + " · onboarding, so nothing can be signed in their name yet",
        onUndo: () => setData((d) => Object.assign({}, d, { staff: d.staff.filter((s) => s.id !== id) })),
      });
    },
  });

  /* ── Access ────────────────────────────────────────────────────────────── */
  const onStaffTransition = (person, transition) => {
    if (transition.id === "suspend") {
      return setForm({
        title: "Suspend " + person.name + "'s access",
        description: "They keep their record and their history. They cannot open Ward Desk, and nothing can be signed in their name, until access is restored.",
        submitLabel: "Suspend access",
        initial: { note: "" },
        fields: [{ id: "note", kind: "text", label: "Reason", placeholder: "Access suspended pending an information-governance review opened 4 March.", hint: "Shown on their record and on the Employees screen." }],
        validate: (v) => (!v.note || v.note.trim().length < 12 ? { note: "Record the reason — a sentence is enough." } : {}),
        run: (v) => {
          patch("staff", person.id, { status: "suspended", suspendedNote: v.note.trim() });
          pushToast({ title: "Access suspended", description: person.name + " · " + person.jobTitle, onUndo: () => restore("staff", person) });
        },
      });
    }
    const next = { activate: "active", restore: "active", returned: "active" }[transition.id];
    patch("staff", person.id, { status: next, suspendedNote: undefined });
    pushToast({
      variant: "success",
      title: transition.id === "activate" ? "Marked active" : transition.id === "restore" ? "Access restored" : "Marked returned",
      description: person.name + " · " + labelOf(STAFF_ROLES, person.staffRole) + " · they can sign work in their own name again",
      onUndo: () => restore("staff", person),
    });
  };

  /* Finance and credit control commit in their own file — App.jsx is the router
     and the commit index, not the place every dialog is spelled out. The factory
     is given exactly what a commit needs, so nothing there reaches for state it
     does not own. */
  const money = makeFinanceCommits({
    data: data, setData: setData, patch: patch, restore: restore, logActivity: logActivity,
    unlogActivity: unlogActivity, pushToast: pushToast, setForm: setForm, setConfirm: setConfirm,
    viewer: viewer, now: now, setPath: setPath, billing: billing,
  });
  const { onInvoiceTransition, bulkIssue, recordPayment, newInvoice, onChase } = money;

  /* ── Import and export ─────────────────────────────────────────────────── */
  const onExport = (label, scope, rows, format) => {
    const id = "trf_" + Math.random().toString(36).slice(2, 6);
    const job = {
      id: id, kind: "export", label: label, scope: scope, format: format, source: null,
      status: "completed", rows: rows, accepted: rows, rejected: 0, pending: 0,
      startedAt: now, finishedAt: now, byId: viewer.id,
      fileName: label.toLowerCase().replace(/[^a-z0-9]+/g, "-") + "-17-03-2026." + format.toLowerCase(), sizeKb: Math.max(1, Math.round(rows * 1.4)),
    };
    setData((d) => Object.assign({}, d, { transfers: [job].concat(d.transfers) }));
    logActivity("exported a report", { type: "transfer", id: id }, label + ", " + format);
    pushToast({
      variant: "success", title: "Export ready",
      description: formatCount(rows) + " rows · " + scope + " · " + job.fileName + " · kept for " + hospital.retentionYears + " years",
    });
  };

  const onImport = () => setForm({
    title: "Import data",
    description: "The file is parsed and every row matched against the patient index. Nothing is written until you have decided what to do with the rows that cannot be placed.",
    submitLabel: "Start import",
    initial: { source: "pas_csv", fileName: "", rows: "" },
    fields: [
      { id: "source", kind: "select", label: "Where it is from", options: IMPORT_SOURCES.filter((s) => integrations.some((i) => i.source === s.value && i.enabled)), hint: integrations.filter((i) => !i.enabled).length ? integrations.filter((i) => !i.enabled).length + " further feeds are switched off in Settings and cannot be imported from." : null },
      { id: "fileName", kind: "input", label: "File", placeholder: "pas-index-17-03-2026.csv", hint: "The file name is kept with the job so the run can be traced." },
      { id: "rows", kind: "input", label: "Rows in the file", hint: "The sending system states this in its header." },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.fileName || v.fileName.trim().length < 5) errors.fileName = "Name the file, with its extension.";
      if (!/^\d+$/.test(v.rows || "") || Number(v.rows) < 1) errors.rows = "A whole number of rows.";
      return errors;
    },
    run: (v) => {
      const id = "trf_" + Math.random().toString(36).slice(2, 6);
      const source = IMPORT_SOURCES.find((s) => s.value === v.source);
      const job = {
        id: id, kind: "import", label: source.label.split(" — ")[0], scope: "Uploaded by " + viewer.name, format: source.label.split(" — ")[1] || "CSV",
        source: v.source, status: "queued", rows: Number(v.rows), accepted: 0, rejected: 0, pending: 0,
        startedAt: now, finishedAt: null, byId: viewer.id, fileName: v.fileName.trim(), sizeKb: Math.round(Number(v.rows) * 1.3),
      };
      setData((d) => Object.assign({}, d, { transfers: [job].concat(d.transfers) }));
      logActivity("queued an import", { type: "transfer", id: id }, formatCount(Number(v.rows)) + " rows from " + job.label);
      pushToast({
        title: "Import queued",
        description: formatCount(Number(v.rows)) + " rows from " + job.label + " · nothing is written into the patient index until it runs",
        onUndo: () => setData((d) => Object.assign({}, d, { transfers: d.transfers.filter((t) => t.id !== id) })),
      });
    },
  });

  const onTransferTransition = (job, transition) => {
    if (transition.id === "retry_job") {
      patch("transfers", job.id, { status: "queued", startedAt: now, finishedAt: null, failure: undefined });
      pushToast({ title: "Queued again", description: job.label + " · " + job.fileName + " · it will run against the same file", onUndo: () => restore("transfers", job) });
    }
    if (transition.id === "download_file") {
      pushToast({ title: "File sent to your browser", description: job.fileName + " · " + formatCount(job.rows) + " rows · " + job.sizeKb + " KB" });
    }
  };

  /* Deciding a row writes into the fixture, so the job's own accepted, rejected
     and pending counts — and the nav badge above them — move with the decision. */
  const onRowDecision = (job, row, decision) => {
    const snapshotRows = data.transferRows[job.id];
    const snapshotJob = job;
    const remaining = snapshotRows.filter((r) => r.line !== row.line);
    setData((d) => Object.assign({}, d, {
      transferRows: Object.assign({}, d.transferRows, { [job.id]: remaining }),
      transfers: d.transfers.map((t) => (t.id === job.id ? Object.assign({}, t, {
        accepted: t.accepted + (decision === "accept" ? 1 : 0),
        rejected: t.rejected + (decision === "reject" ? 1 : 0),
        pending: remaining.length,
        status: remaining.length ? "needs_review" : "completed",
        finishedAt: remaining.length ? t.finishedAt : now,
      }) : t)),
    }));
    pushToast({
      title: decision === "accept" ? "Row accepted" : "Row rejected",
      description: "Line " + row.line + " · " + (decision === "accept" ? row.detail.replace("Matches", "written against") : "left in the file and sent to " + hospital.recordsEmail) + " · " + remaining.length + " rows still to decide",
      onUndo: () => setData((d) => Object.assign({}, d, {
        transferRows: Object.assign({}, d.transferRows, { [job.id]: snapshotRows }),
        transfers: d.transfers.map((t) => (t.id === job.id ? snapshotJob : t)),
      })),
    });
  };

  const onAcceptMatched = (job, matched) => {
    const snapshotRows = data.transferRows[job.id];
    const snapshotJob = job;
    const lines = matched.map((r) => r.line);
    const remaining = snapshotRows.filter((r) => lines.indexOf(r.line) < 0);
    setData((d) => Object.assign({}, d, {
      transferRows: Object.assign({}, d.transferRows, { [job.id]: remaining }),
      transfers: d.transfers.map((t) => (t.id === job.id ? Object.assign({}, t, {
        accepted: t.accepted + matched.length, pending: remaining.length,
        status: remaining.length ? "needs_review" : "completed",
        finishedAt: remaining.length ? t.finishedAt : now,
      }) : t)),
    }));
    pushToast({
      variant: "success",
      title: matched.length + " rows accepted",
      description: "Each matched an existing patient on name and date of birth · " + remaining.length + " rows still to decide",
      onUndo: () => setData((d) => Object.assign({}, d, {
        transferRows: Object.assign({}, d.transferRows, { [job.id]: snapshotRows }),
        transfers: d.transfers.map((t) => (t.id === job.id ? snapshotJob : t)),
      })),
    });
  };

  /* ── The social layer, instant ─────────────────────────────────────────── */
  const onPostNote = (record, body) => {
    const id = "cmt_" + Math.random().toString(36).slice(2, 7);
    const comment = {
      id: id, record: record, authorId: viewer.id, at: now, body: body,
      mentions: (body.match(/@stf_\w+/g) || []).map((m) => m.slice(1)),
      reactions: {},
    };
    setData((d) => Object.assign({}, d, {
      comments: d.comments.concat([comment]),
      activity: [{ id: "act_" + id, actorId: viewer.id, at: now, verb: "commented", record: record, note: null }].concat(d.activity),
    }));
    const named = comment.mentions.map((m) => staffName(data.staff, m));
    pushToast({
      variant: "success", title: "Note posted",
      description: named.length
        ? "On " + recordTitle(data, record.type, record.id) + " · waiting on " + named.join(" and ") + " until they write on it"
        : "On " + recordTitle(data, record.type, record.id) + " · nobody was mentioned, so nothing is waiting on anyone",
      onUndo: () => setData((d) => Object.assign({}, d, {
        comments: d.comments.filter((c) => c.id !== id),
        activity: d.activity.filter((a) => a.id !== "act_" + id),
      })),
    });
  };

  /* Instant and harmless, so it is a bare control: no toast, no confirm.
     Clicking again takes yours off. */
  const onReactToNote = (comment, key) => setData((d) => Object.assign({}, d, {
    comments: d.comments.map((c) => {
      if (c.id !== comment.id) return c;
      const ids = c.reactions[key] || [];
      const next = ids.indexOf(viewer.id) > -1 ? ids.filter((id) => id !== viewer.id) : ids.concat([viewer.id]);
      const reactions = Object.assign({}, c.reactions);
      if (next.length) reactions[key] = next; else delete reactions[key];
      return Object.assign({}, c, { reactions: reactions });
    }),
  }));

  /* ── Staged screens ───────────────────────────────────────────────────── */
  const saveWards = (rows) => {
    const before = data.wardBeds;
    setData((d) => Object.assign({}, d, {
      wardBeds: d.wardBeds.map((w) => {
        const row = rows.find((r) => r.ward === w.ward);
        return row ? { ward: w.ward, beds: Number(row.beds), establishment: { day: Number(row.day), late: Number(row.late), night: Number(row.night) } } : w;
      }),
    }));
    const beds = rows.reduce((n, r) => n + Number(r.beds), 0);
    const occupied = onWard(data.admissions).concat(fitForDischarge(data.admissions)).length;
    pushToast({
      variant: "success", title: "Wards saved",
      description: beds + " beds across " + rows.length + " wards · occupancy now reads " + Math.round((occupied / beds) * 100) + "% on Today, Wards and Reports",
      onUndo: () => setData((d) => Object.assign({}, d, { wardBeds: before })),
    });
  };

  /* The capability maps in enums.js are the live source every transition reads,
     so saving writes into them rather than into a copy nothing consults. */
  const saveRoles = (rows) => {
    const before = rows.map((row) => ({
      role: row.role,
      verify: !!ROLE_CAN_VERIFY_MEDICATION[row.role], move: !!ROLE_CAN_MOVE_ADMISSION[row.role],
      consent: !!ROLE_CAN_RECORD_CONSENT[row.role], import: !!ROLE_CAN_IMPORT[row.role], bill: !!ROLE_CAN_BILL[row.role],
    }));
    const apply = (list) => {
      list.forEach((row) => {
        ROLE_CAN_VERIFY_MEDICATION[row.role] = row.verify;
        ROLE_CAN_MOVE_ADMISSION[row.role] = row.move;
        ROLE_CAN_RECORD_CONSENT[row.role] = row.consent;
        ROLE_CAN_IMPORT[row.role] = row.import;
        ROLE_CAN_BILL[row.role] = row.bill;
      });
      setRolesVersion((n) => n + 1);
    };
    apply(rows);
    const mine = rows.find((r) => r.role === viewer.staffRole);
    const gained = mine ? ["verify", "move", "consent", "import", "bill"].filter((k) => mine[k]).length : 0;
    pushToast({
      variant: "success", title: "Permissions saved",
      description: "Your role can now do " + gained + " of the 5 things · every row action and every disabled reason in the product reads this",
      onUndo: () => apply(before),
    });
  };

  const saveBilling = (values) => {
    const before = billing;
    setBilling(values);
    pushToast({
      variant: "success", title: "Billing saved",
      description: values.referencePrefix + " on new invoices · balances over " + formatPounds(values.writeOffThresholdMinor) + " need " + values.approver.toLowerCase() + " approval to write off",
      onUndo: () => setBilling(before),
    });
  };

  const saveIntegrations = (rows) => {
    const before = integrations;
    setIntegrations(rows);
    const on = rows.filter((r) => r.enabled);
    pushToast({
      variant: "success", title: "Integrations saved",
      description: on.length + " of " + rows.length + " feeds are available to import · " + rows.filter((r) => r.enabled && r.autoAccept).length + " auto-accept matched rows",
      onUndo: () => setIntegrations(before),
    });
  };

  const saveNotifications = (values) => {
    const before = prefs;
    setPrefs(Object.assign({}, prefs, { rows: values.rows, quietHours: values.quietHours }));
    const paged = values.rows.filter((r) => r.pager).length;
    pushToast({
      variant: "success", title: "Notifications saved",
      description: paged + " of " + values.rows.length + " events page somebody · quiet from " + values.quietHours.from + " to " + values.quietHours.to,
      onUndo: () => setPrefs(before),
    });
  };

  const saveSecurity = (values) => {
    const before = security;
    setSecurity(values);
    pushToast({
      variant: "success", title: "Security saved",
      description: "Idle sessions lock after " + values.sessionMinutes + " minutes · " + (values.requireTwoFactor ? "a second factor is required" : "no second factor is required") + " · passwords expire after " + values.passwordAgeDays + " days",
      onUndo: () => setSecurity(before),
    });
  };

  const onSaveProfile = (values) => {
    const before = viewer;
    patch("staff", viewer.id, values);
    const uploaded = values.avatar && values.avatar.indexOf("data:") === 0;
    pushToast({
      variant: "success", title: "Profile saved",
      description: values.name + " · " + values.jobTitle + " · " + (uploaded ? "your uploaded photograph is" : values.avatar ? "your badge photograph is" : "no photograph is") + " now beside every note you have written",
      onUndo: () => restore("staff", before),
    });
  };

  const saveHospital = (values) => {
    const before = hospital;
    setHospital(Object.assign({}, hospital, {
      name: values.name.trim(), trust: values.trust.trim(), recordsEmail: values.recordsEmail.trim(),
      retentionYears: Number(values.retentionYears), timezone: values.timezone,
    }));
    pushToast({
      variant: "success", title: "Hospital saved",
      description: values.name.trim() + " · exports kept " + values.retentionYears + " years · rejected rows go to " + values.recordsEmail.trim(),
      onUndo: () => setHospital(before),
    });
  };

  /* Harness chrome, not product chrome: the floating page menu is how a reader
     reaches every registered screen, including the auth ones the shell cannot
     render. It sits outside the shell so it is on those screens too. */
  const preview = (
    <TemplateMenu product="Ward Desk" path={path} onNavigate={setPath} groups={HD_PAGES(data)}
      dark={dark} onToggleTheme={() => setDark(!dark)} />
  );

  if (HD_AUTH_PATHS.indexOf(path) > -1) {
    const authScreen = () => {
      if (path === "/forgot-password") return <ForgotPasswordScreen email={authEmail} onSubmit={(value) => { setAuthEmail(value); setPath("/forgot-password/sent"); }} onNavigate={setPath} />;
      if (path === "/forgot-password/sent") return <ResetLinkSentScreen email={authEmail} onNavigate={setPath} />;
      if (path === "/reset-password") return <SetNewPasswordScreen now={now} onSubmit={() => setPath("/reset-password/done")} onNavigate={setPath} />;
      if (path === "/reset-password/expired") return <ResetLinkExpiredScreen onNavigate={setPath} />;
      if (path === "/reset-password/done") return <PasswordResetDoneScreen email={HD_RESET_LINK.email} onNavigate={setPath} />;
      return <SignInScreen onSignIn={() => setPath("/")} onNavigate={setPath} />;
    };
    return (<>{authScreen()}{preview}</>);
  }

  const patientMatch = path.match(/^\/patients\/([^/]+)$/);
  const selectedPatient = patientMatch && data.patients.some((p) => p.id === patientMatch[1]) ? patientMatch[1] : null;
  const interventionMatch = path.match(/^\/interventions\/([^/]+)$/);
  const selectedIntervention = interventionMatch && data.interventions.some((i) => i.id === interventionMatch[1]) ? interventionMatch[1] : null;
  const invoiceMatch = path.match(/^\/invoices\/([^/]+)$/);
  const selectedInvoice = invoiceMatch && data.invoices.some((i) => i.id === invoiceMatch[1]) ? invoiceMatch[1] : null;
  const doctorMatch = path.match(/^\/doctors\/([^/]+)$/);
  const employeeMatch = path.match(/^\/employees\/([^/]+)$/);
  const staffMatch = (doctorMatch && data.staff.some((s) => s.id === doctorMatch[1]) ? doctorMatch[1] : null)
    || (employeeMatch && data.staff.some((s) => s.id === employeeMatch[1]) ? employeeMatch[1] : null);

  const screen = () => {
    if (path === "/") {
      return <TodayScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onAdmissionTransition={onAdmissionTransition} onInterventionTransition={onInterventionTransition}
        onReactToNote={onReactToNote} onExport={onExport} />;
    }
    if (path === "/patients/new") return <NewPatientScreen data={data} viewer={viewer} onCreate={createPatient} onNavigate={setPath} />;
    if (selectedPatient) {
      return <PatientScreen data={data} now={now} viewer={viewer} patientId={selectedPatient} onNavigate={setPath}
        onAdmissionTransition={onAdmissionTransition} onMedicationTransition={onMedicationTransition}
        onPostNote={onPostNote} onReactToNote={onReactToNote} onExport={onExport} />;
    }
    if (path === "/patients") {
      return <PatientsScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onNewPatient={() => setPath("/patients/new")} onImport={onImport} onExport={onExport} />;
    }
    if (path === "/admissions") {
      return <AdmissionsScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onAdmissionTransition={onAdmissionTransition} onBulkMarkFit={bulkMarkFit} onNewAdmission={newAdmission}
        onImport={onImport} onExport={onExport} />;
    }
    if (selectedIntervention) {
      return <InterventionScreen data={data} now={now} viewer={viewer} interventionId={selectedIntervention} onNavigate={setPath}
        onInterventionTransition={onInterventionTransition} onPostNote={onPostNote} onReactToNote={onReactToNote} onExport={onExport} />;
    }
    if (path === "/interventions") {
      return <InterventionsScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onInterventionTransition={onInterventionTransition} onNewIntervention={newIntervention} onExport={onExport} />;
    }
    if (path === "/contracts") return <ContractsScreen data={data} now={now} viewer={viewer} onNavigate={setPath} onExport={onExport} />;
    if (path === "/credit-control") return <CreditControlScreen data={data} now={now} viewer={viewer} onNavigate={setPath} onChase={onChase} onExport={onExport} />;
    if (path === "/margin") return <MarginScreen data={data} now={now} viewer={viewer} onNavigate={setPath} onExport={onExport} />;
    if (selectedInvoice) {
      return <InvoiceScreen data={data} now={now} viewer={viewer} invoiceId={selectedInvoice} onNavigate={setPath}
        onInvoiceTransition={onInvoiceTransition} onPostNote={onPostNote} onReactToNote={onReactToNote} onExport={onExport} />;
    }
    if (path === "/invoices") {
      return <InvoicesScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onInvoiceTransition={onInvoiceTransition} onBulkIssue={bulkIssue} onNewInvoice={newInvoice} onExport={onExport} />;
    }
    if (path === "/payments") {
      return <PaymentsScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onNewPayment={() => recordPayment(null)} onExport={onExport} />;
    }
    if (path === "/revenue") return <RevenueScreen data={data} now={now} viewer={viewer} onNavigate={setPath} onExport={onExport} />;
    if (path === "/wards") {
      return <WardsScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onAdmissionTransition={onAdmissionTransition} onExport={onExport} />;
    }
    if (path === "/rota") return <RotaScreen data={data} now={now} viewer={viewer} onNavigate={setPath} onExport={onExport} />;
    if (path === "/rounds") {
      return <RoundsScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onMedicationTransition={onMedicationTransition} onExport={onExport} />;
    }
    if (path === "/reports") return <ReportsScreen data={data} now={now} viewer={viewer} onNavigate={setPath} onExport={onExport} />;
    if (path === "/medication") {
      return <MedicationScreen data={data} now={now} viewer={viewer} ward={medWard} onWardChange={setMedWard} onNavigate={setPath}
        onMedicationTransition={onMedicationTransition} onNewOrder={newOrder} onExport={onExport} />;
    }
    if (path === "/formulary") {
      return <FormularyScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onReorder={onReorder} onNewLine={newFormularyLine} onImport={onImport} onExport={onExport} />;
    }
    if (staffMatch) {
      return <StaffScreen data={data} now={now} viewer={viewer} staffId={staffMatch}
        routePath={doctorMatch ? "/doctors/:id" : "/employees/:id"} onNavigate={setPath}
        onStaffTransition={onStaffTransition} onReactToNote={onReactToNote} onExport={onExport} />;
    }
    if (path === "/doctors") return <DoctorsScreen data={data} now={now} viewer={viewer} onNavigate={setPath} onStaffTransition={onStaffTransition} onExport={onExport} />;
    if (path === "/employees") {
      return <EmployeesScreen data={data} now={now} viewer={viewer} onNavigate={setPath}
        onStaffTransition={onStaffTransition} onNewEmployee={newEmployee} onImport={onImport} onExport={onExport} />;
    }
    if (path === "/feed") return <FeedScreen data={data} now={now} viewer={viewer} onNavigate={setPath} onReactToNote={onReactToNote} onExport={onExport} />;
    if (path === "/data") {
      return <DataScreen data={data} now={now} viewer={viewer} reviewingId={reviewingId}
        onReview={setReviewingId} onCloseReview={() => setReviewingId(null)}
        onTransferTransition={onTransferTransition} onImport={onImport} onExport={onExport}
        onRowDecision={onRowDecision} onAcceptMatched={onAcceptMatched} onPostNote={onPostNote} onReactToNote={onReactToNote} />;
    }
    if (path === "/me/profile") return <MyProfileScreen data={data} now={now} viewer={viewer} onSave={onSaveProfile} onNavigate={setPath} />;
    if (path === "/settings") return <HospitalSettingsScreen data={data} hospital={hospital} viewer={viewer} onNavigate={setPath} onSave={saveHospital} />;
    if (path === "/settings/wards") return <WardSettingsScreen data={data} now={now} viewer={viewer} onNavigate={setPath} onSave={saveWards} />;
    if (path === "/settings/roles") return <RoleSettingsScreen key={rolesVersion} data={data} viewer={viewer} onNavigate={setPath} onSave={saveRoles} />;
    if (path === "/settings/billing") return <BillingSettingsScreen data={data} now={now} billing={billing} viewer={viewer} onNavigate={setPath} onSave={saveBilling} />;
    if (path === "/settings/integrations") return <IntegrationSettingsScreen data={data} now={now} integrations={integrations} viewer={viewer} onNavigate={setPath} onSave={saveIntegrations} />;
    if (path === "/settings/notifications") return <NotificationSettingsScreen data={data} prefs={prefs} hospital={hospital} viewer={viewer} onNavigate={setPath} onSave={saveNotifications} />;
    if (path === "/settings/security") return <SecuritySettingsScreen data={data} now={now} security={security} viewer={viewer} onNavigate={setPath} onSave={saveSecurity} />;
    return <RouteNotBuilt path={path} onNavigate={setPath} />;
  };

  const recordGroups = [
    { label: "Waiting for a bed", items: bedsWaiting(data.admissions).map((a) => ({ value: a.id, label: patientName(data.patients, a.patientId) + " · " + wardName(a.ward) + " · " + formatCountdown(a.requestedAt, now), route: "/patients/" + a.patientId })) },
    { label: "In a theatre today", items: interventionsToday(data.interventions, now).map((i) => ({ value: i.id, label: i.name + " · " + patientName(data.patients, i.patientId), route: "/interventions/" + i.id })) },
    { label: "Lines billed off contract", items: offContractLines(data).map((f, index) => ({ value: f.invoice.id + index, label: f.invoice.reference + " · " + f.line.label + " · " + (f.kind === "no_price" ? "not in the contract" : "off contract by " + formatPounds(Math.abs(f.difference))), route: "/invoices/" + f.invoice.id })) },
    { label: "Invoices past due", items: invoicesOverdue(data.invoices, data.payments, now).map((i) => ({ value: i.id, label: i.reference + " · " + formatPounds(invoiceBalance(i, data.payments)) + " · " + formatCountdown(i.dueAt, now), route: "/invoices/" + i.id })) },
    { label: "Invoices", items: data.invoices.map((i) => ({ value: i.id, label: i.reference + " · " + patientName(data.patients, i.patientId) + " · " + statusValue(INVOICE_STATUS, i.status).label, route: "/invoices/" + i.id })) },
    { label: "Patients", items: data.patients.map((p) => ({ value: p.id, label: p.name + " · " + p.number, route: "/patients/" + p.id })) },
    { label: "Interventions", items: data.interventions.map((i) => ({ value: i.id, label: i.name + " · " + statusValue(INTERVENTION_STATUS, i.status).label, route: "/interventions/" + i.id })) },
    { label: "Shifts short of establishment", items: understaffedShifts(data.rota, data.wardBeds).map((s) => ({ value: s.ward + s.day + s.shift, label: wardName(s.ward) + " · " + (s.day === 0 ? "today" : "in " + s.day + " days") + " · " + labelOf(SHIFT_TYPES, s.shift).split(",")[0].toLowerCase() + " · " + s.shortfall + " short", route: "/rota" })) },
    { label: "Doctors", items: doctorsOf(data.staff).map((s) => ({ value: s.id, label: s.name + " · " + s.jobTitle, route: "/doctors/" + s.id })) },
    { label: "Employees", items: data.staff.filter((s) => s.family !== "medical").map((s) => ({ value: s.id, label: s.name + " · " + s.jobTitle, route: "/employees/" + s.id })) },
  ];

  return (
    <HdShell
      path={path}
      onNavigate={setPath}
      dark={dark}
      onToggleTheme={() => setDark(!dark)}
      data={data}
      viewer={viewer}
      notifications={NOTIFICATIONS}
      unread={read ? 0 : NOTIFICATIONS.length}
      onRead={() => setRead(true)}
      palette={palette}
      toasts={toasts}
      onDismissToast={dismiss}
    >
      {screen()}
      <CommandPalette
        open={palette.open}
        onClose={() => palette.setOpen(false)}
        routes={HD_ROUTES}
        recordGroups={recordGroups}
        onNavigate={setPath}
        placeholder="Search patients, procedures, staff, invoices…"
      />
      <ConfirmDialog request={confirm} onCancel={() => setConfirm(null)} onConfirm={() => { confirm.run(); setConfirm(null); }} />
      <FormDialog request={form} onCancel={() => setForm(null)} onSubmit={(values) => { form.run(values); setForm(null); }} />
      {preview}
    </HdShell>
  );
}
window.__hdReal = App;
if (!window.HdApp) window.HdApp = App;

})();
</script>
<script type="text/babel" data-presets="react" data-file="mount.jsx">
(function(){
ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(window.HdApp, {}));
})();
</script>
</body>
</html>
