From 99cf0084066dd2ea25c39d64773e66d806dd9b01 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:38:16 +0800 Subject: [PATCH 001/106] =?UTF-8?q?feat(ward-flow):=20Ward=20Flow=20protot?= =?UTF-8?q?ype=20baseline=20=E2=80=94=20routes,=20model,=20spec=20and=20ph?= =?UTF-8?q?ase=201=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Baseline for the Phase 1 model rework. Adds the ward-management surface (command, constellation, network, queue, capacity, movements, exceptions, transport, governance, patient workspace), its synthetic fixtures, the domain glossary and decision records, the metro patient-flow design spec, and the Phase 1 implementation plan. Co-Authored-By: Claude Opus 5 --- design-qa.md | 44 + docs/codebase-index.md | 64 +- docs/design-system/ADOPTION.md | 9 +- docs/design-system/COMPONENTS.md | 2 +- docs/design-system/adoption-contract.json | 60 + docs/design-system/adoption-manifest.json | 250 +- docs/site-map.md | 10 + .../2026-08-14-ward-management-mockups.md | 202 ++ .../2026-08-18-ward-flow-model-and-modes.md | 972 ++++++++ .../2026-08-18-ward-flow-phase-1-model.md | 1124 +++++++++ .../2026-08-14-ward-management-design.md | 294 +++ ...-18-ward-flow-metro-patient-flow-design.md | 383 ++++ docs/ward-management-context.md | 159 ++ docs/ward-management-decisions.md | 148 ++ docs/ward-management-mode-map.md | 119 + playwright.config.ts | 4 +- src/app/ward-management/capacity/page.tsx | 12 + .../ward-management/constellation/page.tsx | 12 + src/app/ward-management/exceptions/page.tsx | 12 + src/app/ward-management/governance/page.tsx | 12 + src/app/ward-management/movements/page.tsx | 12 + src/app/ward-management/network/page.tsx | 13 + src/app/ward-management/page.tsx | 12 + .../patients/[patientId]/page.tsx | 13 + src/app/ward-management/queue/page.tsx | 12 + src/app/ward-management/transport/page.tsx | 12 + src/components/applications-launcher-page.tsx | 2 + .../tools-page-mockups/tool-fixtures.ts | 10 + .../tools/tools-search-results-page.tsx | 2 + .../ward-management/synthetic-fixtures.ts | 736 ++++++ .../ward-management-console.tsx | 917 ++++++++ .../ward-management-modes.module.css | 1384 +++++++++++ .../ward-management/ward-management-modes.tsx | 888 +++++++ .../ward-management-navigation.tsx | 196 ++ .../ward-management-network.module.css | 753 ++++++ .../ward-management-network.tsx | 503 ++++ .../ward-management.module.css | 2037 +++++++++++++++++ src/lib/tools-catalog.ts | 40 + tests/tools-catalog.test.ts | 7 + tests/ui-ward-management.spec.ts | 164 ++ tests/ward-management.test.ts | 104 + 41 files changed, 11668 insertions(+), 41 deletions(-) create mode 100644 design-qa.md create mode 100644 docs/superpowers/plans/2026-08-14-ward-management-mockups.md create mode 100644 docs/superpowers/plans/2026-08-18-ward-flow-model-and-modes.md create mode 100644 docs/superpowers/plans/2026-08-18-ward-flow-phase-1-model.md create mode 100644 docs/superpowers/specs/2026-08-14-ward-management-design.md create mode 100644 docs/superpowers/specs/2026-08-18-ward-flow-metro-patient-flow-design.md create mode 100644 docs/ward-management-context.md create mode 100644 docs/ward-management-decisions.md create mode 100644 docs/ward-management-mode-map.md create mode 100644 src/app/ward-management/capacity/page.tsx create mode 100644 src/app/ward-management/constellation/page.tsx create mode 100644 src/app/ward-management/exceptions/page.tsx create mode 100644 src/app/ward-management/governance/page.tsx create mode 100644 src/app/ward-management/movements/page.tsx create mode 100644 src/app/ward-management/network/page.tsx create mode 100644 src/app/ward-management/page.tsx create mode 100644 src/app/ward-management/patients/[patientId]/page.tsx create mode 100644 src/app/ward-management/queue/page.tsx create mode 100644 src/app/ward-management/transport/page.tsx create mode 100644 src/components/ward-management/synthetic-fixtures.ts create mode 100644 src/components/ward-management/ward-management-console.tsx create mode 100644 src/components/ward-management/ward-management-modes.module.css create mode 100644 src/components/ward-management/ward-management-modes.tsx create mode 100644 src/components/ward-management/ward-management-navigation.tsx create mode 100644 src/components/ward-management/ward-management-network.module.css create mode 100644 src/components/ward-management/ward-management-network.tsx create mode 100644 src/components/ward-management/ward-management.module.css create mode 100644 tests/ui-ward-management.spec.ts create mode 100644 tests/ward-management.test.ts diff --git a/design-qa.md b/design-qa.md new file mode 100644 index 000000000..3d9f36c37 --- /dev/null +++ b/design-qa.md @@ -0,0 +1,44 @@ +# Ward Flow constellation design QA + +## Comparison setup + +- Visual target: `C:\Users\joshs\.codex\generated_images\01a00060-be1a-7252-922f-b9dfc7a496b3\exec-c93f388a-175c-463d-89bd-db70dadc014f.png` +- Implementation: `artifacts/ward-management/ward-constellation-final-1440x1024.png` +- Combined source-and-implementation view: `artifacts/ward-management/design-qa-constellation-comparison.png` +- Source dimensions: 1487 x 1058 pixels, normalized to 1440 x 1024 for comparison. +- Implementation viewport: 1440 x 1024 CSS pixels at density 1. +- Compared state: Flow coordinator, WF-204 selected, Fiona Stanley proposed, placement awaiting authorised confirmation. + +## Fidelity review + +| Surface | Result | Evidence | +| ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Typography | Passed | The graphite hierarchy, tabular operational figures, compact labels, and restrained microcopy preserve the reference's clinical command-centre character with stronger scanning contrast. | +| Spacing and layout | Passed | The slim independent priority queue, central statewide network, and right-hand decision rail keep the reference's three-zone structure. The added local mode strip and role-focus band make the broader system legible without crowding the decision surface. | +| Colour and state | Passed | Clinical blue remains the only structural accent. Green, amber, and red are reserved for capacity and exception meaning and are always reinforced by text, rank, or iconography. | +| Assets and icons | Passed | Existing Clinical KB branding and product iconography are used consistently. The constellation remains a schematic operational view rather than implying geographic precision. | +| Copy and data | Passed | Synthetic identifiers, WA service names, catchment, legal status, setting, wait, transport readiness, and capacity evidence are clear. AI is labelled as a best-fit proposal and requires an authorised human confirmation. | + +## Intentional improvements from the source + +1. Replaced literal route-line density with four stable service clusters around a statewide coordination hub. +2. Added eight persistent Ward Flow modes so every major operational task is reachable from the same local navigation. +3. Made the priority queue independently scrollable and kept role-based ordering visible rather than hiding it behind controls. +4. Turned the shortlist into an explainable, ranked proposal with catchment, setting, availability, transport, and wait-time evidence. +5. Added a plain-language no-automatic-allocation boundary directly beside the confirmation action. +6. Kept the full constellation as a dedicated wide command view while preserving the denser command home for routine work. + +## Functional and responsive evidence + +- All eight Ward Flow routes were opened and checked at 1440 x 1024 with no page-level horizontal overflow. +- Flow coordinator, ED clinician, and ward coordinator role changes alter the operational focus and queue order. +- Queue selection, ranked fit review, and authorised confirmation were exercised in the browser. +- The 390 x 844 phone fallback was checked across every mode with no page-level horizontal overflow. +- Fresh browser console on the final constellation route contained no warnings or errors. +- Focused Chromium coverage includes role switching, queue collapse, all mode links, confirmation, phone layout, dark mode, forced colours, and print. + +## Final review + +The implementation retains the reference's visual intent and decision hierarchy while improving system navigation, privacy signalling, role clarity, and AI governance. No P0, P1, or actionable P2 fidelity issue remains. + +final result: passed diff --git a/docs/codebase-index.md b/docs/codebase-index.md index 3f3bc2b29..dbe6f091c 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -66,30 +66,31 @@ Smaller top-level directories that are easy to miss: ### Product pages (`src/app/`) -| Route | File | -| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| `/` | `src/app/(search-app)/page.tsx` | -| Shared mode-home route group (`/(search-app)`) | `src/app/(search-app)/` | -| Mode homes (`/services`, `/dsm`, `/documents/…`, …) | `src/app/(search-app)/` shared shell group | -| `/applications` | `src/app/applications/route.ts` | -| `/differentials`, `/diagnoses`, `/presentations`, `/compare` | `src/app/(search-app)/differentials/` | -| `/dsm`, `/dsm/search`, `/dsm/compare`, `/dsm/diagnoses/[slug]` | `src/app/(search-app)/dsm/` | -| `/documents/search`, `/source`, `/evidence`, `/[id]` | `src/app/(search-app)/documents/` | -| `/factsheets`, `/factsheets/search`, `/factsheets/[slug]` | `src/app/(search-app)/factsheets/` | -| `/favourites` | `src/app/(search-app)/favourites/page.tsx` | -| `/forms`, `/forms/[slug]` | `src/app/(search-app)/forms/` | -| `/medications`, `/medications/[slug]` | `src/app/(search-app)/medications/` | -| `/privacy` | `src/app/privacy/page.tsx` → `privacy-quiet-signal-page.tsx` + `privacy-page-content.tsx` | -| `/reference/colour-coding` | `src/app/reference/` | -| `/safety-plan` | `src/app/safety-plan/page.tsx` | -| `/calculators` | `src/app/(search-app)/calculators/page.tsx` | -| `/services`, `/services/[slug]` | `src/app/(search-app)/services/` | -| `/therapy-compass` | `src/app/(search-app)/therapy-compass/` | -| `/tools` | `src/app/(search-app)/tools/` | -| `/specifiers`, `/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, `/specifiers/map` | `src/app/(search-app)/specifiers/` | -| `/formulation`, `/formulation/[slug]`, `/formulation/builder`, `/formulation/compare`, `/formulation/map` | `src/app/(search-app)/formulation/` | -| `/mockups/*` | `src/app/mockups/` (404 in production) | -| `/auth/callback` | `src/app/auth/callback/route.ts` | +| Route | File | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `/` | `src/app/(search-app)/page.tsx` | +| Shared mode-home route group (`/(search-app)`) | `src/app/(search-app)/` | +| Mode homes (`/services`, `/dsm`, `/documents/…`, …) | `src/app/(search-app)/` shared shell group | +| `/applications` | `src/app/applications/route.ts` | +| `/differentials`, `/diagnoses`, `/presentations`, `/compare` | `src/app/(search-app)/differentials/` | +| `/dsm`, `/dsm/search`, `/dsm/compare`, `/dsm/diagnoses/[slug]` | `src/app/(search-app)/dsm/` | +| `/documents/search`, `/source`, `/evidence`, `/[id]` | `src/app/(search-app)/documents/` | +| `/factsheets`, `/factsheets/search`, `/factsheets/[slug]` | `src/app/(search-app)/factsheets/` | +| `/favourites` | `src/app/(search-app)/favourites/page.tsx` | +| `/forms`, `/forms/[slug]` | `src/app/(search-app)/forms/` | +| `/medications`, `/medications/[slug]` | `src/app/(search-app)/medications/` | +| `/privacy` | `src/app/privacy/page.tsx` → `privacy-quiet-signal-page.tsx` + `privacy-page-content.tsx` | +| `/reference/colour-coding` | `src/app/reference/` | +| `/safety-plan` | `src/app/safety-plan/page.tsx` | +| `/calculators` | `src/app/(search-app)/calculators/page.tsx` | +| `/services`, `/services/[slug]` | `src/app/(search-app)/services/` | +| `/therapy-compass` | `src/app/(search-app)/therapy-compass/` | +| `/tools` | `src/app/(search-app)/tools/` | +| `/specifiers`, `/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, `/specifiers/map` | `src/app/(search-app)/specifiers/` | +| `/formulation`, `/formulation/[slug]`, `/formulation/builder`, `/formulation/compare`, `/formulation/map` | `src/app/(search-app)/formulation/` | +| `/ward-management`, `/constellation`, `/network`, `/queue`, `/capacity`, `/movements`, `/exceptions`, `/transport`, `/governance`, `/patients/[patientId]` | `src/app/ward-management/` — Ward Flow synthetic patient-flow prototype | +| `/mockups/*` | `src/app/mockups/` (404 in production) | +| `/auth/callback` | `src/app/auth/callback/route.ts` | ### API routes (`src/app/api/`) @@ -320,6 +321,21 @@ sequenceDiagram - Registry modes: services, forms, medications, differentials; Formulation is a local mechanism and structured-draft workspace - Demo mode: synthetic data when Supabase unavailable (`demo-data.ts`, `isDemoMode()` in `env.ts`) +### Ward Flow (`src/app/ward-management/`, `src/components/ward-management/`) + +Synthetic prototype for WA metro psychiatry patient flow: getting a patient from an emergency +department to an inpatient psychiatric bed. Offline and fixture-backed — no provider calls, no +persistence, no patient-identifiable data. Advisory only: the system proposes destinations with +visible reasons and a human confirms or overrides. + +- **Design spec:** `docs/superpowers/specs/2026-08-18-ward-flow-metro-patient-flow-design.md` +- **Glossary:** `docs/ward-management-context.md` · **Decisions:** `docs/ward-management-decisions.md` +- **Route/role map:** `docs/ward-management-mode-map.md` +- **Fixtures:** `src/components/ward-management/synthetic-fixtures.ts` — sites, units, movements +- **Surfaces:** `ward-management-console.tsx` (command), `ward-management-modes.tsx` (mode + workspaces), `ward-management-network.tsx` (network diagram), `ward-management-navigation.tsx` +- **Tests:** `tests/ward-management.test.ts`, `tests/ui-ward-management.spec.ts` + ### Global search composer placement rules One shared composer (`master-search-header.tsx`) serves every mode. Placement: diff --git a/docs/design-system/ADOPTION.md b/docs/design-system/ADOPTION.md index 8219dd6f8..8f1c000a6 100644 --- a/docs/design-system/ADOPTION.md +++ b/docs/design-system/ADOPTION.md @@ -359,12 +359,12 @@ product exclusions; the only route-only disposition is the documented legacy doc redirect. Shared shell/component roots carry their own explicit `shared-shell` disposition. Registered public components: 54 -Declared product roots: 59 +Declared product roots: 73 Roots with a literal `.ckb-v2` opt-in: 1 -Roots inheriting `.ckb-v2` from the global ``: 58 -Production surfaces observed under v2: 14/14 +Roots inheriting `.ckb-v2` from the global ``: 72 +Production surfaces observed under v2: 15/15 Dynamic `ckb-v2` constructions: 0 -Declared production page routes: 51/51 +Declared production page routes: 61/61 Source observation and contract declaration are independent. A literal `ckb-v2` on the global `` makes every production surface inherit v2, but it does not approve that adoption. The Proof column summarizes each surface's dark, forced-colours, 320px, print and browser declarations; exact statuses and evidence paths live in the manifest. @@ -382,6 +382,7 @@ Observed v2 under a compatibility declaration fails closed. A declared v2 shell | `documents-source-legacy-redirect` | legacy-redirect | 1 | 0 | v2 | v2 (inherited-global-root) | not-applicable | not-applicable | | `favourites` | owned | 1 | 1 | v2 | v2 (inherited-global-root) | passed | not-committed | | `tools-and-calculators` | owned | 2 | 2 | v2 | v2 (inherited-global-root) | passed | not-committed | +| `ward-management` | owned | 10 | 14 | v2 | v2 (inherited-global-root) | passed | not-committed | | `privacy-safety-and-reference` | owned | 3 | 3 | v2 | v2 (inherited-global-root) | passed | not-committed | | `search-results-shared` | shared-shell | 0 | 1 | v2 | v2 (inherited-global-root) | passed | not-committed | | `answers-shared` | shared-shell | 0 | 2 | v2 | v2 (inherited-global-root) | passed | not-committed | diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index e2eea7685..4b380eac4 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -1001,7 +1001,7 @@ This generated snapshot is a local source-derived inventory. It does not assert | `SearchField` | controls | yes | yes | no | yes | no | 0 | | `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 10 | | `Select` | controls | yes | yes | inherited-global-root | yes | no | 2 | -| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 26 | +| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 27 | | `Skeleton` | feedback | yes | yes | inherited-global-root | yes | no | 6 | | `SourceDesignationBadge` | source | yes | yes | inherited-global-root | yes | no | 1 | | `SourceProvenance` | source | yes | yes | inherited-global-root | yes | no | 1 | diff --git a/docs/design-system/adoption-contract.json b/docs/design-system/adoption-contract.json index 169ba2527..eab6f203b 100644 --- a/docs/design-system/adoption-contract.json +++ b/docs/design-system/adoption-contract.json @@ -486,6 +486,66 @@ }, "sanctionedSpecialPatterns": ["ApplicationsLauncherPage", "CalculatorsSearchPage"] }, + { + "id": "ward-management", + "disposition": "owned", + "routes": [ + "src/app/ward-management/page.tsx", + "src/app/ward-management/capacity/page.tsx", + "src/app/ward-management/constellation/page.tsx", + "src/app/ward-management/exceptions/page.tsx", + "src/app/ward-management/governance/page.tsx", + "src/app/ward-management/movements/page.tsx", + "src/app/ward-management/network/page.tsx", + "src/app/ward-management/patients/[patientId]/page.tsx", + "src/app/ward-management/queue/page.tsx", + "src/app/ward-management/transport/page.tsx" + ], + "routeRoots": true, + "roots": [ + "src/components/ward-management/ward-management-console.tsx", + "src/components/ward-management/ward-management-modes.tsx", + "src/components/ward-management/ward-management-navigation.tsx", + "src/components/ward-management/ward-management-network.tsx" + ], + "permittedComponentFamilies": ["controls", "feedback", "layout"], + "expectedShellState": "v2", + "proof": { + "dark": { + "status": "passed", + "evidence": ["tests/ui-style-contract.spec.ts"] + }, + "forcedColours": { + "status": "passed", + "evidence": ["tests/ckb-v2-token-contract.test.ts"] + }, + "compact320": { + "status": "passed", + "evidence": ["tests/ui-smoke.spec.ts"] + }, + "print": { + "status": "passed", + "evidence": ["tests/global-v2-activation.test.ts"] + }, + "browser": { + "status": "passed", + "evidence": ["tests/ui-visual-baseline.spec.ts"] + } + }, + "baseline": { + "status": "not-committed", + "files": [] + }, + "sanctionedSpecialPatterns": [ + "WardManagementConsole", + "PatientMovementWorkspace", + "WardModeWorkspace", + "WardModeNavigation", + "ClinicalRail", + "WardNetworkWorkspace" + ], + "documentedDisposition": "Ward Flow is a synthetic operational coordination surface with a command home, full-screen constellation, role-aware queue-first phone fallback, linked operational workspaces, and an explainable human-confirmed destination workflow." + }, { "id": "privacy-safety-and-reference", "disposition": "owned", diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 0f706d3d3..3cc657a2c 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -1517,7 +1517,8 @@ "src/components/mode-nav/mode-nav.tsx", "src/components/services/service-group-nav.tsx", "src/components/tools/tools-search-results-page.tsx", - "src/components/ui/confirm-dialog.tsx" + "src/components/ui/confirm-dialog.tsx", + "src/components/ward-management/ward-management-console.tsx" ], "productImportFiles": [ "src/components/AccessibleTable.tsx", @@ -1545,7 +1546,8 @@ "src/components/in-page-nav/in-page-nav-header.tsx", "src/components/mode-nav/mode-nav.tsx", "src/components/services/service-group-nav.tsx", - "src/components/tools/tools-search-results-page.tsx" + "src/components/tools/tools-search-results-page.tsx", + "src/components/ward-management/ward-management-console.tsx" ], "designSync": { "listedInSourceMap": true, @@ -3024,6 +3026,214 @@ } ] }, + { + "id": "ward-management", + "disposition": "owned", + "documentedDisposition": "Ward Flow is a synthetic operational coordination surface with a command home, full-screen constellation, role-aware queue-first phone fallback, linked operational workspaces, and an explainable human-confirmed destination workflow.", + "routes": [ + "src/app/ward-management/capacity/page.tsx", + "src/app/ward-management/constellation/page.tsx", + "src/app/ward-management/exceptions/page.tsx", + "src/app/ward-management/governance/page.tsx", + "src/app/ward-management/movements/page.tsx", + "src/app/ward-management/network/page.tsx", + "src/app/ward-management/page.tsx", + "src/app/ward-management/patients/[patientId]/page.tsx", + "src/app/ward-management/queue/page.tsx", + "src/app/ward-management/transport/page.tsx" + ], + "routeRoots": true, + "proofApplicability": "required", + "nonVisualRoute": null, + "declaredShellState": "v2", + "observedShellState": "v2", + "v2ShellMounted": true, + "v2MountMode": "inherited-global-root", + "inheritedFrom": "src/app/layout.tsx", + "directV2MountFiles": [], + "proof": { + "browser": { + "status": "passed", + "evidence": ["tests/ui-visual-baseline.spec.ts"] + }, + "compact320": { + "status": "passed", + "evidence": ["tests/ui-smoke.spec.ts"] + }, + "dark": { + "status": "passed", + "evidence": ["tests/ui-style-contract.spec.ts"] + }, + "forcedColours": { + "status": "passed", + "evidence": ["tests/ckb-v2-token-contract.test.ts"] + }, + "print": { + "status": "passed", + "evidence": ["tests/global-v2-activation.test.ts"] + } + }, + "baseline": { + "status": "not-committed", + "files": [] + }, + "permittedComponentFamilies": ["controls", "feedback", "layout"], + "sanctionedSpecialPatterns": [ + "ClinicalRail", + "PatientMovementWorkspace", + "WardManagementConsole", + "WardModeNavigation", + "WardModeWorkspace", + "WardNetworkWorkspace" + ], + "roots": [ + { + "file": "src/app/ward-management/capacity/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardModeWorkspace"] + }, + { + "file": "src/app/ward-management/constellation/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardModeWorkspace"] + }, + { + "file": "src/app/ward-management/exceptions/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardModeWorkspace"] + }, + { + "file": "src/app/ward-management/governance/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardModeWorkspace"] + }, + { + "file": "src/app/ward-management/movements/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardModeWorkspace"] + }, + { + "file": "src/app/ward-management/network/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardModeWorkspace"] + }, + { + "file": "src/app/ward-management/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardManagementConsole"] + }, + { + "file": "src/app/ward-management/patients/[patientId]/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": [] + }, + { + "file": "src/app/ward-management/queue/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardModeWorkspace"] + }, + { + "file": "src/app/ward-management/transport/page.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardModeWorkspace"] + }, + { + "file": "src/components/ward-management/ward-management-console.tsx", + "exists": true, + "imports": ["Sheet"], + "importedFamilies": ["layout"], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardManagementConsole", "WardModeNavigation", "ClinicalRail"] + }, + { + "file": "src/components/ward-management/ward-management-modes.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": [ + "WardModeWorkspace", + "WardModeNavigation", + "ClinicalRail", + "WardNetworkWorkspace" + ] + }, + { + "file": "src/components/ward-management/ward-management-navigation.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardModeNavigation", "ClinicalRail"] + }, + { + "file": "src/components/ward-management/ward-management-network.tsx", + "exists": true, + "imports": [], + "importedFamilies": [], + "literalCkbV2": false, + "dynamicCkbV2": false, + "v2MountMode": "inherited-global-root", + "sanctionedPatternsPresent": ["WardNetworkWorkspace"] + } + ] + }, { "id": "privacy-safety-and-reference", "disposition": "owned", @@ -3331,7 +3541,17 @@ "src/app/(search-app)/tools/page.tsx", "src/app/privacy/page.tsx", "src/app/reference/colour-coding/page.tsx", - "src/app/safety-plan/page.tsx" + "src/app/safety-plan/page.tsx", + "src/app/ward-management/capacity/page.tsx", + "src/app/ward-management/constellation/page.tsx", + "src/app/ward-management/exceptions/page.tsx", + "src/app/ward-management/governance/page.tsx", + "src/app/ward-management/movements/page.tsx", + "src/app/ward-management/network/page.tsx", + "src/app/ward-management/page.tsx", + "src/app/ward-management/patients/[patientId]/page.tsx", + "src/app/ward-management/queue/page.tsx", + "src/app/ward-management/transport/page.tsx" ], "declared": [ "src/app/(search-app)/calculators/page.tsx", @@ -3384,7 +3604,17 @@ "src/app/(search-app)/tools/page.tsx", "src/app/privacy/page.tsx", "src/app/reference/colour-coding/page.tsx", - "src/app/safety-plan/page.tsx" + "src/app/safety-plan/page.tsx", + "src/app/ward-management/capacity/page.tsx", + "src/app/ward-management/constellation/page.tsx", + "src/app/ward-management/exceptions/page.tsx", + "src/app/ward-management/governance/page.tsx", + "src/app/ward-management/movements/page.tsx", + "src/app/ward-management/network/page.tsx", + "src/app/ward-management/page.tsx", + "src/app/ward-management/patients/[patientId]/page.tsx", + "src/app/ward-management/queue/page.tsx", + "src/app/ward-management/transport/page.tsx" ], "undeclared": [], "missing": [], @@ -3392,17 +3622,17 @@ }, "adoption": { "literalCkbV2RootCount": 1, - "inheritedCkbV2RootCount": 58, + "inheritedCkbV2RootCount": 72, "dynamicCkbV2RootCount": 0, - "v2MountedSurfaceCount": 14, - "declaredV2SurfaceCount": 14 + "v2MountedSurfaceCount": 15, + "declaredV2SurfaceCount": 15 }, "summary": { "registeredComponentCount": 54, "previewCount": 54, "productImportedComponentCount": 32, - "rootCount": 59, - "productionRouteCount": 51, - "nextUiEntryCount": 103 + "rootCount": 73, + "productionRouteCount": 61, + "nextUiEntryCount": 113 } } diff --git a/docs/site-map.md b/docs/site-map.md index 55ea7e389..9c9c59d56 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -45,6 +45,16 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir - `/therapy-compass/review` - Therapy records awaiting qualified-clinician source review. Source: `src/app/(search-app)/therapy-compass/review/page.tsx`. - `/therapy-compass/search` - Therapy library search surface. Source: `src/app/(search-app)/therapy-compass/search/page.tsx`. - `/tools` - Route discovered from app directory Source: `src/app/(search-app)/tools/page.tsx`. +- `/ward-management` - Route discovered from app directory Source: `src/app/ward-management/page.tsx`. +- `/ward-management/capacity` - Route discovered from app directory Source: `src/app/ward-management/capacity/page.tsx`. +- `/ward-management/constellation` - Route discovered from app directory Source: `src/app/ward-management/constellation/page.tsx`. +- `/ward-management/exceptions` - Route discovered from app directory Source: `src/app/ward-management/exceptions/page.tsx`. +- `/ward-management/governance` - Route discovered from app directory Source: `src/app/ward-management/governance/page.tsx`. +- `/ward-management/movements` - Route discovered from app directory Source: `src/app/ward-management/movements/page.tsx`. +- `/ward-management/network` - Route discovered from app directory Source: `src/app/ward-management/network/page.tsx`. +- `/ward-management/patients/[patientId]` - Route discovered from app directory Source: `src/app/ward-management/patients/[patientId]/page.tsx`. +- `/ward-management/queue` - Route discovered from app directory Source: `src/app/ward-management/queue/page.tsx`. +- `/ward-management/transport` - Route discovered from app directory Source: `src/app/ward-management/transport/page.tsx`. ## Mode/query routes diff --git a/docs/superpowers/plans/2026-08-14-ward-management-mockups.md b/docs/superpowers/plans/2026-08-14-ward-management-mockups.md new file mode 100644 index 000000000..675c18923 --- /dev/null +++ b/docs/superpowers/plans/2026-08-14-ward-management-mockups.md @@ -0,0 +1,202 @@ +# Ward Management Mockup Generation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: `superpowers:executing-plans` (inline execution). Use Product Design `ideate` for image directions. Do not use subagents for this plan. + +**Goal:** Produce and validate three independent desktop visual directions for the approved synthetic WA mental-health ward-management command map, then create responsive and role-specific companions only after the user selects a direction. + +**Architecture:** Visual discovery is separate from product implementation. The approved specification fixes the clinical workflow, information architecture, responsive behaviour, state semantics, and safety boundaries. Image generation may vary visual hierarchy, density, and interaction treatment, but it may not vary those approved product contracts. No production source code is changed until a visual target is selected and a separate production implementation plan is approved. + +**Tech stack:** Clinical KB design tokens and shared shell as visual references; Product Design `ideate`; built-in Image Generation; local Clinical KB preview for source capture; synthetic fixtures only. + +## Global constraints + +- Approved source of truth: `docs/superpowers/specs/2026-08-14-ward-management-design.md`. +- Planned production route: `/ward-management`; do not create it during image ideation. +- Desktop foundation: schematic hospital network grouped by North, East, South, and Country coordinating services. +- Phone foundation: role-specific operational queue; `Hospitals` and `Map` are secondary views. +- Roles: flow coordinator, ED team, ward manager. +- Cohorts: general adult and older adult. +- Data: visibly synthetic; no real names, MRNs, dates of birth, addresses, contacts, narrative histories, or real-time bed counts. +- Bed states: occupied, allocatable now, held, potential after action, blocked/unavailable. +- Movement stages: placement requested, destination review, accepted/bed held, handover and transport ready, moving, arrived/closed. +- Matching: catchment first, explainable statewide escalation, human confirmation required. +- Priority: human urgency tier first; algorithmic score only orders within a tier and exposes its factors. +- Legal state: plain language first with form and timing detail available. +- Visual system: Clinical White / Sky Graphite; graphite commands, clinical-blue focus/movement, green ready/complete only, amber held/potential/time-sensitive, red blocked/overdue only. +- Do not use colour as the sole state signal. +- Current-date anchor for every generated concept: 14 August 2026, Australia/Perth. +- Do not deploy, publish, call live clinical providers, or use patient-identifiable information. + +## Artifact map + +### Existing sources to inspect + +- `docs/superpowers/specs/2026-08-14-ward-management-design.md` — approved product and safety contract. +- `docs/design-system.md` — current reusable design-system rules. +- `docs/redesign/02-design-direction.md` — Clinical White / Sky Graphite rationale. +- `docs/search-chrome-behaviour.md` — shared responsive shell and phone-chrome ownership. +- `src/app/globals.css` — implemented role tokens and typography. +- `src/components/clinical-dashboard/global-search-shell.tsx` — production shell owner. +- `src/app/mockups/layout.tsx` and `src/app/mockups/mockups-layout-client.tsx` — design-scratch route ownership for any later coded prototype. + +### Source captures outside Git + +- `C:/Users/joshs/.codex/visualizations/2026/08/14/01a00060-be1a-7252-922f-b9dfc7a496b3/ward-management/sources/clinical-kb-shell-desktop.png` +- `C:/Users/joshs/.codex/visualizations/2026/08/14/01a00060-be1a-7252-922f-b9dfc7a496b3/ward-management/sources/clinical-kb-shell-phone.png` + +### Generated visual set + +- Three independent `1440 × 1024` desktop coordinator concepts, shown in the conversation as separate generated images. +- After selection: one refined desktop coordinator target, one `390 × 844` phone coordinator target, and representative ED and ward-manager companion screens. +- Generated images stay outside the Git worktree unless the user later approves a coded mockup or asset package. + +### Possible later coded mockup files — not part of this plan's execution + +- `src/app/mockups/ward-management/page.tsx` +- `src/components/ward-management-mockups/ward-management-mockup-page.tsx` +- `src/components/ward-management-mockups/synthetic-fixtures.ts` +- `tests/ward-management-mockup.contract.test.ts` + +These paths are reserved only to make the next production or coded-prototype plan concrete. Do not create them during image ideation. + +## Task 1: Capture the current Clinical KB visual source + +**Files** + +- Read: the existing sources listed in the artifact map. +- Create outside Git: the two source captures listed above. + +**Produces** + +- A desktop and phone reference showing the real Clinical KB shell, typography, surfaces, spacing, header behaviour, and role-token use. +- A brief visual-source checklist recorded in the execution notes: shell, typography, colour roles, radius, border, density, and phone behaviour. + +**Steps** + +- [ ] Confirm the isolated worktree remains on `codex/ward-management-design` and contains no unrelated changes. +- [ ] Run `node scripts/setup-codex-worktree.mjs` so the isolated worktree has a lockfile-identical dependency runtime. +- [ ] Run `npm run workflow:design-sweep -- --write-evidence` and retain its evidence path. +- [ ] Run `npm run ensure`; use only the URL it prints. +- [ ] Verify `/api/local-project-id` identifies this Clinical KB checkout before capture. +- [ ] Capture the production shell at `1440 × 1024` and `390 × 844` using synthetic/demo content. +- [ ] Inspect both captures directly; do not infer design details from filenames. +- [ ] Confirm neither capture contains patient-identifiable or secret-bearing data. + +**Gate** + +- Both source images exist, open successfully, match the specified dimensions, and visibly represent the current Clinical KB design system. + +## Task 2: Generate three independent desktop coordinator directions + +**Consumes** + +- Approved specification. +- Both inspected source captures. +- Global constraints in this plan. + +**Produces** + +- Three separate Image Generation results at `1440 × 1024`. +- Each result is a focused primary coordinator screen, not a feature inventory. + +**Direction briefs** + +### Signal Spine + +- Schematic HSP columns form a quiet central network spine. +- Priority movement and action inbox occupy one restrained side rail. +- Highest glanceability and greatest whitespace. + +### Network Ledger + +- Hospitals read as a structured operational ledger connected by selective movement lines. +- Strongest comparison of demand, bed state, ownership, and last-confirmed time. +- Highest information density without turning every row into a card. + +### Operational Constellation + +- Coordinating-service groups form a more spatial network with selected movement paths. +- A contextual patient-to-bed panel appears only for the selected synthetic patient. +- Strongest visual sense of statewide coordination while remaining schematic rather than geographic. + +**Steps** + +- [ ] Read Product Design `ideate` and Image Generation instructions before the first generation. +- [ ] Attach both actual source captures to every generation; do not claim they were attached unless the tool receives them. +- [ ] Generate `Signal Spine` as one independent image call. +- [ ] Generate `Network Ledger` as one independent image call. +- [ ] Generate `Operational Constellation` as one independent image call. +- [ ] Do not batch the three calls or place several concepts in one image. +- [ ] Use the exact current-date anchor, approved workflow states, and clearly synthetic data in every prompt. +- [ ] Ensure every concept shows the flow-coordinator role, four coordinating-service groups, five bed states, a priority movement, one visible action exception, catchment-first/statewide escalation, and last-confirmed time. +- [ ] Ensure no concept shows automatic allocation, a clinical severity score, real patient data, literal WA travel distance as the matching rule, or a phone device bezel. + +**Gate** + +- Exactly three generated images are visible once each in the main conversation. +- No screen is clipped, illegible, crowded, or inconsistent with Clinical KB's source captures. +- Every state remains understandable without colour alone. + +## Task 3: Stop for visual selection + +**Consumes** + +- The three visible generated-image results in their displayed order. + +**Produces** + +- One user-selected direction or a precise combination/revision brief. + +**Steps** + +- [ ] Number options only by their displayed order after all three image results are visible. +- [ ] Ask only: `Which option should I build: 1, 2, or 3? Or tell me what you'd like to refine or personalize first.` +- [ ] Do not describe or rename the options in the selection message. +- [ ] Stop. Do not generate companion screens or write code until the user selects or requests a revision. + +**Gate** + +- The selected visual target is unambiguous. If the user combines directions, generate and obtain approval for the combined target before continuing. + +## Task 4: Generate the selected responsive and role companions + +**Runs only after Task 3 approval.** + +**Produces** + +- One refined desktop coordinator target at `1440 × 1024`. +- One phone coordinator target at `390 × 844` using the approved role-first queue. +- One representative ED-team screen. +- One representative ward-manager screen. +- One patient-movement detail treatment shared across roles. + +**Steps** + +- [ ] Resolve the selected target against the displayed result, never the original request order. +- [ ] Carry forward the selected typography, spacing, network treatment, state language, and interaction hierarchy. +- [ ] Keep the phone default queue-first; Map remains secondary. +- [ ] Preserve role ownership: ED readiness/referral/legal/handover, ward capacity/release/acceptance/hold, coordinator match/escalation/movement. +- [ ] Check all outputs against the specification's acceptance criteria. +- [ ] Stop for user approval of the complete visual set. + +## Task 5: Hand off to production planning + +**Runs only after the complete visual set is approved.** + +**Produces** + +- A separate, repository-grounded production implementation plan with exact source files, interfaces, tests, accessibility evidence, privacy controls, and rollout boundaries. + +**Steps** + +- [ ] Do not treat image approval as production-readiness evidence. +- [ ] Reinspect current `origin/main`, applicable Next 16 local documentation, shared shell ownership, app-mode registration, route reachability, and design-system contracts. +- [ ] Plan a synthetic fixture-backed slice before any real integration or patient-data work. +- [ ] Keep real clinical integrations, RBAC, algorithm validation, privacy assessment, and production governance as separately authorised work. + +## Plan verification + +- [ ] Run the standard specification placeholder scan and resolve every hit. +- [ ] Run `git diff --check`. +- [ ] Run Prettier against this plan with a byte-identical lockfile installation. +- [ ] Confirm this plan changes documentation only and leaves tests, builds, providers, commits, and pushes unrun. diff --git a/docs/superpowers/plans/2026-08-18-ward-flow-model-and-modes.md b/docs/superpowers/plans/2026-08-18-ward-flow-model-and-modes.md new file mode 100644 index 000000000..4be01810e --- /dev/null +++ b/docs/superpowers/plans/2026-08-18-ward-flow-model-and-modes.md @@ -0,0 +1,972 @@ +# Ward Flow model correction and missing modes — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Close the four model defects that let Ward Flow propose an unlawful or impossible placement, then add the four modes the WA pathway needs but the prototype cannot currently express. + +**Architecture:** Ward Flow is a synthetic, offline product wireframe: one fixture module (`synthetic-fixtures.ts`) feeds every route, each route is a thin server component that renders a client workspace, and every workspace is proved by one Vitest contract test plus one Chromium journey. This plan keeps that shape. Model corrections land first as fixture + type changes with contract tests, because every new mode reads the corrected model. New modes follow, one route per task. No mode performs an allocation; all remain advisory with human confirmation, per ADR 2. + +**Tech Stack:** Next.js 16 App Router (server component route → `"use client"` workspace), React 19, TypeScript 6 strict, CSS Modules with local `--net-*`/`--ward-*` token scales, Vitest for contracts, Playwright Chromium for journeys. + +**Spec:** [`docs/ward-management-mode-map.md`](../../ward-management-mode-map.md) (route/role model), [`docs/ward-management-context.md`](../../ward-management-context.md) (glossary), [`docs/ward-management-decisions.md`](../../ward-management-decisions.md) (ADRs 1–3) + +## Global Constraints + +- **Synthetic only.** No record may carry name, date of birth, MRN, address, diagnosis, narrative history or treatment. `tests/ward-management.test.ts` asserts absence of those keys; extend that assertion, never relax it. +- **Advisory only.** No surface may allocate, auto-accept, or default after a timeout (ADR 2). Every placement control is a confirm/override pair. +- **No provider calls.** Everything runs offline against fixtures. Never add a fetch, Supabase call, or OpenAI call to any Ward Flow file. +- **Design tokens only.** No raw hex, and no raw padding/gap/z-index/line-height literals in CSS Modules — declare a local token in the module's root block first. `npm run check:design-system-contract` ratchets these counts and fails on any increase. +- **Tap targets are `3rem` (48px) minimum** on every interactive element. Never reduce to `2.75rem` to satisfy generic WCAG advice. +- **Button wiring.** Every ` + ); +} + +function CapacityLine({ + state, + count, + label, +}: { + state: "available" | "held" | "potential" | "blocked" | "occupied"; + count: number; + label: string; +}) { + return ( + + + ); +} + +function HospitalNode({ + hospital, + selected, + onSelect, +}: { + hospital: (typeof wardHospitals)[number]; + selected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +function QueuePanel({ + role, + patients, + selectedPatient, + collapsed, + onCollapse, + onSelectPatient, +}: { + role: WardRole; + patients: WardPatient[]; + selectedPatient: WardPatient; + collapsed: boolean; + onCollapse: () => void; + onSelectPatient: (patient: WardPatient) => void; +}) { + const [filtersOpen, setFiltersOpen] = useState(false); + if (collapsed) { + return ( + + ); + } + + return ( + + ); +} + +function ActionInbox({ onOpen }: { onOpen: (item: (typeof actionInbox)[number] | null) => void }) { + return ( +
+ {actionInbox.map((item) => { + const Icon = item.icon; + return ( + + ); + })} + +
+ ); +} + +function MovementPipeline({ + activeStage, + onStageChange, +}: { + activeStage: MovementStage; + onStageChange: (stage: MovementStage) => void; +}) { + return ( + + ); +} + +function WardNetwork({ + selectedHospitalCode, + onSelectHospital, +}: { + selectedHospitalCode: string; + onSelectHospital: (code: string) => void; +}) { + return ( +
+

+ WA psychiatry bed network +

+
+ {wardRegions.map((region) => { + const hospitals = wardHospitals.filter((hospital) => hospital.region === region); + const totalBeds = hospitals.reduce((sum, hospital) => sum + hospital.beds, 0); + const totalAvailable = hospitals.reduce((sum, hospital) => sum + hospital.available, 0); + return ( +
+
+ + + +

{region}

+

+ {totalBeds} beds · {totalAvailable} available +

+
+
+ {hospitals.map((hospital) => ( + onSelectHospital(hospital.code)} + /> + ))} +
+
+ ); + })} +
+
+ + + + + + + +
+
+ ); +} + +function DecisionDock({ + patient, + role, + selectedHospitalCode, + confirmed, + onConfirm, + onClose, + onSelectHospital, +}: { + patient: WardPatient; + role: WardRole; + selectedHospitalCode: string; + confirmed: boolean; + onConfirm: () => void; + onClose: () => void; + onSelectHospital: (code: string) => void; +}) { + const destination = wardHospitalByCode(selectedHospitalCode); + const recommendationSelected = + destination.code.startsWith(patient.destinationCode) || patient.destinationCode.startsWith(destination.code); + return ( +
+ +
+

+ {patient.id} + · + {stageLabel(patient.stage)} +

+ + +

+ {shortHospitalName(destination.name)}{" "} + + {patient.cohort} {patient.setting} + {" "} + · {recommendationSelected ? patient.score : Math.max(patient.score - 8, 0)}% +

+
+
+ + + + + + + + + +
+
+

+ Priority {operationalPriorityScore(patient)} + ·Tier {patient.urgency} +

+ Alternatives + {patient.alternatives.slice(0, 2).map((alternative) => ( + + ))} +
+
+ Readiness + + + + + + + + +
+
+ + + Open full patient workspace
+

+ AI proposes · coordinator confirms · operational score, not clinical severity +

+
+ ); +} + +export function WardManagementConsole() { + const [role, setRole] = useState("flow"); + const [selectedPatient, setSelectedPatient] = useState(wardPatients[0]); + const [selectedHospitalCode, setSelectedHospitalCode] = useState(wardPatients[0].destinationCode); + const [queueCollapsed, setQueueCollapsed] = useState(false); + const [activeStage, setActiveStage] = useState("destination_review"); + const [decisionOpen, setDecisionOpen] = useState(true); + const [confirmedPatientId, setConfirmedPatientId] = useState(null); + const [inboxOpen, setInboxOpen] = useState(false); + const [selectedInboxItem, setSelectedInboxItem] = useState<(typeof actionInbox)[number] | null>(actionInbox[0]); + + const rolePatients = useMemo(() => { + if (role === "flow") return wardPatients; + if (role === "ed") + return [...wardPatients].sort((a, b) => Number(b.owner.includes("ED")) - Number(a.owner.includes("ED"))); + return [...wardPatients].sort((a, b) => Number(b.owner.includes("Ward")) - Number(a.owner.includes("Ward"))); + }, [role]); + + function selectPatient(patient: WardPatient) { + const rolePatient = rolePatients.find((candidate) => candidate.id === patient.id) ?? patient; + setSelectedPatient(rolePatient); + setSelectedHospitalCode(rolePatient.destinationCode); + setActiveStage(rolePatient.stage); + setDecisionOpen(true); + setConfirmedPatientId(null); + } + + function openInbox(item: (typeof actionInbox)[number] | null) { + setSelectedInboxItem(item); + setInboxOpen(true); + } + + return ( +
+ +
+
+ + Clinical KB + Source-backed clinical search + +
+
+

Ward Flow

+ +
+
+ Synthetic prototype + + + + + + WA +
+
+ + setQueueCollapsed((collapsed) => !collapsed)} + onSelectPatient={selectPatient} + /> + +
+ + + + { + setSelectedHospitalCode(code); + setDecisionOpen(true); + setConfirmedPatientId(null); + }} + /> + {decisionOpen ? ( + setConfirmedPatientId(selectedPatient.id)} + onClose={() => setDecisionOpen(false)} + onSelectHospital={(code) => { + setSelectedHospitalCode(code); + setConfirmedPatientId(null); + }} + /> + ) : ( + + )} + + {confirmedPatientId ? `Destination confirmed for ${confirmedPatientId}` : ""} + +
+ + setInboxOpen(false)} + title={selectedInboxItem ? selectedInboxItem.title : "Action inbox"} + description={ + selectedInboxItem + ? `${selectedInboxItem.detail}. Owned by ${selectedInboxItem.owner}.` + : "Operational exceptions requiring an owned next action." + } + contentClassName={styles.inboxSheet} + > +
+ Synthetic prototype + {selectedInboxItem ? ( + <> +

Required action

+

+ Open the matching patient movement, check the current blocker and timing, then record the next owned + action. +

+ + + ) : ( + actionInbox.map((item) => ( + + )) + )} +
+
+
+ ); +} + +export function WardPatientWorkspace({ patientId }: { patientId: string }) { + const patient = wardPatientById(patientId); + const destination = wardHospitalByCode(patient.destinationCode); + const [confirmed, setConfirmed] = useState(false); + const [activeSection, setActiveSection] = useState<"overview" | "legal" | "transport" | "timeline">("overview"); + const [activeStage, setActiveStage] = useState(patient.stage); + + return ( +
+ +
+ +
+
+
+
+ + +

{destination.name}

+

+ {patient.catchment} catchment · {patient.cohort} {patient.setting} · {patient.score}% operational fit +

+
+
+ Operational priority + {operationalPriorityScore(patient)} + Tier {patient.urgency} leads +
+ +
+ + + + + +
+
+

Movement facts

+
+
+
Current stage
+
{stageLabel(patient.stage)}
+
+
+
Owner
+
{patient.owner}
+
+
+
Referral
+
{patient.referredTo}
+
+
+
Response
+
{patient.referralStatus}
+
+
+
Catchment
+
{patient.catchment}
+
+
+
Setting
+
+ {patient.cohort} · {patient.setting} +
+
+
+
+ +
+

Why this match

+
    + {patient.recommendationReasons.map((reason) => ( +
  • +
  • + ))} +
+

Alternatives

+ {patient.alternatives.map((alternative) => ( +
+ + {alternative.hospitalName} + {alternative.reason} + + {alternative.score}% +
+ ))} +
+ +
+

Readiness

+
    +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
+
+
+ + {activeSection === "legal" ? ( +
+

Legal and forms

+

{patient.voluntaryStatus}

+

{patient.legalDetail}

+
+ ) : null} + {activeSection === "transport" ? ( +
+

Transport chain

+

{patient.transport}

+

+ Provider, ETA, risk documentation and legal-form readiness are visible here; dispatch and live vehicle + tracking are not part of this prototype. +

+
+ ) : null} + {activeSection === "timeline" ? ( +
+

Synthetic audit timeline

+
    +
  1. + + Ward capacity confirmed by ward manager +
  2. +
  3. + + AI shortlist recalculated after availability update +
  4. +
  5. + + Referral sent to {patient.destinationName} +
  6. +
  7. + + Movement record created by ED mental health team +
  8. +
+
+ ) : null} + +

+ Synthetic prototype only. AI proposes an explainable operational fit; an authorised human confirms every + destination. This score is not clinical severity. +

+ + {confirmed ? `Destination confirmed for ${patient.id}` : ""} + +
+
+ ); +} diff --git a/src/components/ward-management/ward-management-modes.module.css b/src/components/ward-management/ward-management-modes.module.css new file mode 100644 index 000000000..3470e6a23 --- /dev/null +++ b/src/components/ward-management/ward-management-modes.module.css @@ -0,0 +1,1384 @@ +.modeShell { + --ward-blue: var(--clinical-accent); + --ward-blue-soft: var(--clinical-accent-soft); + --ward-blue-border: var(--clinical-accent-border); + --ward-canvas: var(--surface); + --ward-chrome: var(--surface-chrome); + --ward-subtle: var(--surface-subtle); + --ward-border: var(--border); + --ward-border-strong: var(--border-strong); + --ward-text: var(--text); + --ward-heading: var(--text-heading); + --ward-muted: var(--text-muted); + --ward-success: var(--success-text); + --ward-success-soft: var(--success-bg); + --ward-warning: var(--warning-text); + --ward-warning-soft: var(--warning-bg); + --ward-danger: var(--danger-text); + --ward-danger-soft: var(--danger-bg); + --ward-space-2: 0.125rem; + --ward-space-3: 0.1875rem; + --ward-space-4: 0.25rem; + --ward-space-6: 0.375rem; + --ward-space-7: 0.4375rem; + --ward-space-8: 0.5rem; + --ward-space-10: 0.625rem; + --ward-space-12: 0.75rem; + --ward-space-14: 0.875rem; + --ward-space-16: 1rem; + --ward-space-20: 1.25rem; + --ward-leading-compact: 1; + --ward-leading-tight: 1.2; + --ward-leading-body: 1.4; + --ward-leading-relaxed: 1.45; + --ward-leading-prose: 1.5; + --ward-radius-round: 50%; + --ward-z-base: 1; + --ward-z-phone: 20; + display: grid; + height: 100dvh; + min-height: 44rem; + grid-template-columns: 4.5rem minmax(0, 1fr); + grid-template-rows: 4rem auto minmax(0, 1fr); + overflow: hidden; + background: var(--ward-canvas); + color: var(--ward-text); + font-variant-numeric: tabular-nums; +} + +.modeHeader { + grid-column: 2; + grid-row: 1; + display: grid; + min-width: 0; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: var(--ward-space-16); + border-bottom: 0.0625rem solid var(--ward-border); + padding: 0 var(--ward-space-20); +} + +.modeIdentity { + display: flex; + min-width: 0; + align-items: center; + gap: var(--ward-space-16); +} + +.modeIdentity span:first-child { + min-width: 8rem; + padding-right: var(--ward-space-16); + border-right: 0.0625rem solid var(--ward-border); +} + +.modeIdentity strong, +.modeIdentity small { + display: block; +} + +.modeIdentity strong { + color: var(--ward-heading); + font-size: var(--text-lg); + line-height: var(--ward-leading-tight); +} + +.modeIdentity small { + overflow: hidden; + color: var(--ward-muted); + font-size: var(--text-xs); + text-overflow: ellipsis; + white-space: nowrap; +} + +.modeTitle h1 { + color: var(--ward-heading); + font-size: var(--text-lg); + font-weight: 680; + letter-spacing: -0.02em; + line-height: var(--ward-leading-tight); +} + +.modeTitle p { + margin-top: 0.125rem; + color: var(--ward-muted); + font-size: var(--text-2xs); +} + +.roleSelect { + display: flex; + height: 2.5rem; + align-items: center; + gap: var(--ward-space-8); + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-md); + padding: 0 var(--ward-space-12); + background: var(--ward-canvas); + color: var(--ward-heading); +} + +.roleSelect svg { + width: 1rem; + height: 1rem; + color: var(--ward-blue); +} + +.roleSelect select { + min-width: 9rem; + appearance: none; + border: 0; + background: transparent; + color: inherit; + font-size: var(--text-xs); + font-weight: 650; + outline: 0; +} + +.headerMeta { + display: flex; + align-items: center; + gap: var(--ward-space-12); + color: var(--ward-muted); + font-size: var(--text-2xs); + white-space: nowrap; +} + +.prototypeBadge, +.aiBadge, +.statusGood, +.statusWarning, +.statusDanger, +.statusNeutral { + display: inline-flex; + min-height: 1.5rem; + align-items: center; + gap: var(--ward-space-6); + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-pill, 9999px); + padding: var(--ward-space-3) var(--ward-space-8); + font-size: var(--text-3xs); + font-weight: 680; + line-height: var(--ward-leading-compact); +} + +.prototypeBadge, +.aiBadge { + border-color: var(--ward-blue-border); + background: var(--ward-blue-soft); + color: var(--ward-blue); +} + +.aiBadge svg { + width: 0.875rem; + height: 0.875rem; +} + +.statusGood { + border-color: color-mix(in srgb, var(--ward-success) 34%, var(--ward-border)); + background: var(--ward-success-soft); + color: var(--ward-success); +} + +.statusWarning { + border-color: color-mix(in srgb, var(--ward-warning) 38%, var(--ward-border)); + background: var(--ward-warning-soft); + color: var(--ward-warning); +} + +.statusDanger { + border-color: color-mix(in srgb, var(--ward-danger) 34%, var(--ward-border)); + background: var(--ward-danger-soft); + color: var(--ward-danger); +} + +.statusNeutral { + color: var(--ward-muted); +} + +.modeNavigation { + display: flex; + min-width: 0; + min-height: 2.75rem; + align-items: stretch; + gap: var(--ward-space-2); + overflow-x: auto; + border-bottom: 0.0625rem solid var(--ward-border); + background: var(--ward-chrome); + padding: var(--ward-space-4) var(--ward-space-16); + scrollbar-width: thin; +} + +.modeShell > .modeNavigation { + grid-column: 2; + grid-row: 2; +} + +.modeLink, +.modeLinkActive { + display: inline-flex; + min-width: max-content; + align-items: center; + gap: var(--ward-space-7); + border: 0.0625rem solid transparent; + border-radius: var(--radius-md); + padding: 0 var(--ward-space-12); + color: var(--ward-muted); + font-size: var(--text-2xs); + font-weight: 650; +} + +.modeLink:hover { + border-color: var(--ward-border); + background: var(--ward-canvas); + color: var(--ward-heading); +} + +.modeLinkActive { + border-color: var(--ward-blue-border); + background: var(--ward-blue-soft); + color: var(--ward-blue); + box-shadow: var(--shadow-inset); +} + +.modeLink svg, +.modeLinkActive svg { + width: 1rem; + height: 1rem; +} + +.modeShortLabel { + display: none; +} + +.modeContent { + grid-column: 2; + grid-row: 3; + display: grid; + min-width: 0; + min-height: 0; + grid-template-rows: auto minmax(0, 1fr); + gap: var(--ward-space-8); + overflow: auto; + padding: var(--ward-space-12); + background: var(--ward-subtle); +} + +.roleFocus { + display: flex; + min-height: 2.5rem; + align-items: center; + gap: var(--ward-space-10); + border: 0.0625rem solid var(--ward-blue-border); + border-radius: var(--radius-md); + background: var(--ward-blue-soft); + padding: var(--ward-space-6) var(--ward-space-12); + color: var(--ward-blue); + font-size: var(--text-2xs); +} + +.roleFocus strong { + color: var(--ward-heading); +} + +.roleFocus span { + color: var(--ward-muted); +} + +.pageGrid, +.constellationGrid { + display: grid; + min-height: 100%; + gap: var(--ward-space-10); +} + +.pageGrid { + grid-template-columns: minmax(34rem, 1.55fr) minmax(19rem, 0.75fr); +} + +.constellationGrid { + grid-template-columns: 14rem minmax(38rem, 1fr) 22rem; +} + +.panel, +.panelInset, +.regionCluster, +.summaryCard, +.stageColumn, +.exceptionRow, +.transportRow, +.governanceCard { + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-lg); + background: var(--ward-canvas); + box-shadow: var(--e0); +} + +.panel { + min-width: 0; + padding: var(--ward-space-14); +} + +.panelHeader, +.sectionHeader, +.rowTop, +.decisionHeader { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: var(--ward-space-12); +} + +.panelHeader { + margin-bottom: 0.75rem; +} + +.panelHeader h2, +.sectionHeader h2 { + color: var(--ward-heading); + font-size: var(--text-sm); + font-weight: 700; +} + +.panelHeader p, +.sectionHeader p { + margin-top: 0.125rem; + color: var(--ward-muted); + font-size: var(--text-2xs); +} + +.compactQueue { + display: flex; + min-height: 0; + flex-direction: column; + overflow: hidden; +} + +.queueList, +.dataList, +.exceptionList, +.transportList { + display: grid; + min-height: 0; + gap: var(--ward-space-6); + overflow: auto; +} + +.queueRow, +.queueRowSelected { + display: grid; + min-width: 0; + gap: var(--ward-space-6); + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-md); + padding: var(--ward-space-10); + background: var(--ward-canvas); + color: var(--ward-text); + text-align: left; +} + +.queueRow:hover { + background: var(--ward-subtle); +} + +.queueRowSelected { + border-color: var(--ward-blue-border); + background: var(--ward-blue-soft); + box-shadow: var(--shadow-inset); +} + +.rowTop strong { + color: var(--ward-heading); + font-size: var(--text-xs); +} + +.rowMeta, +.microCopy { + color: var(--ward-muted); + font-size: var(--text-3xs); + line-height: var(--ward-leading-body); +} + +.score { + color: var(--ward-success); + font-size: var(--text-xs); + font-weight: 750; +} + +.networkCanvas { + display: grid; + min-width: 0; + min-height: 38rem; + grid-template-columns: repeat(2, minmax(15rem, 1fr)); + grid-template-rows: minmax(0, 1fr) auto minmax(0, 1fr); + gap: var(--ward-space-10); +} + +.regionCluster { + min-width: 0; + padding: var(--ward-space-12); +} + +.regionCluster:nth-of-type(3) { + grid-column: 1; + grid-row: 3; +} + +.regionCluster:nth-of-type(4) { + grid-column: 2; + grid-row: 3; +} + +.regionCluster header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--ward-space-8); + margin-bottom: 0.5rem; +} + +.regionTitle { + display: flex; + align-items: center; + gap: var(--ward-space-8); + color: var(--ward-heading); +} + +.regionTitle svg { + width: 1rem; + height: 1rem; + color: var(--ward-blue); +} + +.regionCluster header small { + color: var(--ward-muted); + font-size: var(--text-3xs); +} + +.hospitalChipGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--ward-space-6); +} + +.hospitalChip, +.hospitalChipSelected { + display: grid; + min-width: 0; + gap: var(--ward-space-4); + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-md); + padding: var(--ward-space-8); + background: var(--ward-canvas); + color: var(--ward-text); + text-align: left; +} + +.hospitalChipSelected { + border-color: var(--ward-blue); + background: var(--ward-blue-soft); + box-shadow: var(--shadow-inset); +} + +.hospitalChip strong, +.hospitalChipSelected strong { + overflow: hidden; + color: var(--ward-heading); + font-size: var(--text-2xs); + text-overflow: ellipsis; + white-space: nowrap; +} + +.hospitalState { + display: flex; + align-items: center; + gap: var(--ward-space-4); + color: var(--ward-muted); + font-size: var(--text-3xs); +} + +.hospitalState b { + color: var(--ward-success); +} + +.flowHub { + grid-column: 1 / -1; + grid-row: 2; + display: grid; + min-height: 7.25rem; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: var(--ward-space-12); + border: 0.0625rem solid var(--ward-blue-border); + border-radius: var(--radius-lg); + background: var(--ward-blue-soft); + padding: var(--ward-space-12) var(--ward-space-16); + color: var(--ward-blue); +} + +.flowHub > div:nth-child(2) { + min-width: 11rem; + text-align: center; +} + +.flowHub strong, +.flowHub span { + display: block; +} + +.flowHub strong { + color: var(--ward-heading); + font-size: var(--text-sm); +} + +.flowHub span { + margin-top: 0.25rem; + font-size: var(--text-3xs); +} + +.routeSignal { + display: flex; + align-items: center; + gap: var(--ward-space-6); + color: var(--ward-blue); + font-size: var(--text-3xs); + font-weight: 700; +} + +.routeSignal:last-child { + justify-content: flex-end; +} + +.routeSignal svg { + width: 1rem; + height: 1rem; + flex: 0 0 auto; +} + +.decisionPanel { + display: flex; + min-height: 0; + flex-direction: column; + gap: var(--ward-space-12); +} + +.decisionHeader h2 { + color: var(--ward-heading); + font-size: var(--text-sm); +} + +.patientFacts, +.reasonList, +.auditList, +.sourceList { + display: grid; + gap: var(--ward-space-8); +} + +.patientFacts { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.patientFacts div, +.factorRow { + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-md); + padding: var(--ward-space-8); +} + +.patientFacts dt, +.factorRow span, +.candidateRow small { + color: var(--ward-muted); + font-size: var(--text-3xs); +} + +.patientFacts dd { + margin-top: 0.125rem; + color: var(--ward-heading); + font-size: var(--text-2xs); + font-weight: 650; +} + +.candidateTable { + display: grid; + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-md); + overflow: hidden; +} + +.candidateRow, +.candidateRowSelected { + display: grid; + min-width: 0; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: var(--ward-space-8); + border: 0; + border-bottom: 0.0625rem solid var(--ward-border); + background: var(--ward-canvas); + padding: var(--ward-space-10); + color: var(--ward-text); + text-align: left; +} + +.candidateRow:last-child, +.candidateRowSelected:last-child { + border-bottom: 0; +} + +.candidateRowSelected { + background: var(--ward-blue-soft); +} + +.candidateRank { + display: grid; + width: 1.5rem; + height: 1.5rem; + place-items: center; + border-radius: var(--radius-pill, 9999px); + background: var(--ward-subtle); + color: var(--ward-muted); + font-size: var(--text-3xs); + font-weight: 750; +} + +.candidateRowSelected .candidateRank { + background: var(--ward-blue); + color: var(--ward-canvas); +} + +.candidateRow strong, +.candidateRowSelected strong { + display: block; + color: var(--ward-heading); + font-size: var(--text-2xs); +} + +.primaryButton, +.secondaryButton { + display: inline-flex; + min-height: 2.75rem; + align-items: center; + justify-content: center; + gap: var(--ward-space-8); + border: 0.0625rem solid var(--ward-blue); + border-radius: var(--radius-md); + padding: 0 var(--ward-space-14); + font-size: var(--text-xs); + font-weight: 700; +} + +.primaryButton { + background: var(--ward-blue); + color: var(--ward-canvas); +} + +.secondaryButton { + border-color: var(--ward-border); + background: var(--ward-canvas); + color: var(--ward-heading); +} + +.buttonRow { + display: grid; + grid-template-columns: 1fr auto; + gap: var(--ward-space-8); + margin-top: auto; +} + +.dataTable { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: var(--text-2xs); +} + +.dataTable th, +.dataTable td { + border-bottom: 0.0625rem solid var(--ward-border); + padding: var(--ward-space-10) var(--ward-space-8); + text-align: left; + vertical-align: middle; +} + +.dataTable th { + position: sticky; + top: 0; + z-index: var(--ward-z-base); + background: var(--ward-chrome); + color: var(--ward-muted); + font-size: var(--text-3xs); + font-weight: 700; + text-transform: uppercase; +} + +.dataTable tbody tr { + cursor: pointer; +} + +.dataTable tbody tr:hover, +.dataTable tbody tr[data-selected="true"] { + background: var(--ward-blue-soft); +} + +.capacitySummary { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: var(--ward-space-8); + margin-bottom: 0.75rem; +} + +.summaryCard { + padding: var(--ward-space-12); +} + +.summaryCard span, +.summaryCard small { + display: block; + color: var(--ward-muted); + font-size: var(--text-3xs); +} + +.summaryCard strong { + display: block; + margin: 0.25rem 0 0.125rem; + color: var(--ward-heading); + font-size: var(--text-xl); +} + +.bedStates { + display: grid; + grid-template-columns: repeat(5, minmax(3rem, 1fr)); + gap: var(--ward-space-4); +} + +.bedStates span { + display: grid; + min-height: 2.75rem; + place-items: center; + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-sm); + color: var(--ward-muted); + font-size: var(--text-3xs); +} + +.bedStates strong { + display: block; + color: var(--ward-heading); + font-size: var(--text-xs); +} + +.stageBoard { + display: grid; + min-width: 68rem; + grid-template-columns: repeat(6, minmax(10rem, 1fr)); + gap: var(--ward-space-8); +} + +.stageColumn { + display: flex; + min-height: 32rem; + flex-direction: column; + gap: var(--ward-space-8); + padding: var(--ward-space-10); +} + +.stageColumn header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--ward-space-6); + border-bottom: 0.0625rem solid var(--ward-border); + padding-bottom: var(--ward-space-8); +} + +.stageColumn h2 { + color: var(--ward-heading); + font-size: var(--text-2xs); +} + +.stageColumn header strong { + color: var(--ward-blue); + font-size: var(--text-sm); +} + +.movementCard { + display: grid; + gap: var(--ward-space-6); + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-md); + padding: var(--ward-space-10); + background: var(--ward-canvas); +} + +.movementCard strong { + color: var(--ward-heading); + font-size: var(--text-2xs); +} + +.exceptionRow, +.transportRow { + display: grid; + min-width: 0; + grid-template-columns: auto minmax(0, 1.4fr) minmax(9rem, 0.7fr) auto; + align-items: center; + gap: var(--ward-space-12); + padding: var(--ward-space-12); +} + +.exceptionRow > svg, +.transportRow > svg { + width: 1.25rem; + height: 1.25rem; + color: var(--ward-warning); +} + +.exceptionRow strong, +.transportRow strong { + display: block; + color: var(--ward-heading); + font-size: var(--text-xs); +} + +.exceptionRow small, +.transportRow small { + display: block; + margin-top: 0.125rem; + color: var(--ward-muted); + font-size: var(--text-3xs); +} + +.assuranceGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--ward-space-10); +} + +.governanceLowerGrid { + margin-top: 0.625rem; +} + +.governanceCard { + padding: var(--ward-space-14); +} + +.governanceCard svg { + width: 1.25rem; + height: 1.25rem; + color: var(--ward-blue); +} + +.governanceCard h2 { + margin-top: 0.75rem; + color: var(--ward-heading); + font-size: var(--text-sm); +} + +.governanceCard p { + margin-top: 0.375rem; + color: var(--ward-muted); + font-size: var(--text-2xs); + line-height: var(--ward-leading-prose); +} + +.auditList li, +.sourceList li, +.reasonList li { + display: flex; + align-items: flex-start; + gap: var(--ward-space-8); + border-bottom: 0.0625rem solid var(--ward-border); + padding: var(--ward-space-8) 0; + color: var(--ward-text); + font-size: var(--text-2xs); + line-height: var(--ward-leading-relaxed); +} + +.reasonList svg, +.auditList svg, +.sourceList svg { + width: 1rem; + height: 1rem; + flex: 0 0 auto; + margin-top: 0.125rem; + color: var(--ward-success); +} + +.sourceList a { + color: var(--ward-blue); + text-decoration: underline; + text-underline-offset: 0.18em; +} + +.notice { + margin-top: 0.75rem; + border-left: 0.1875rem solid var(--ward-blue); + background: var(--ward-blue-soft); + padding: var(--ward-space-10) var(--ward-space-12); + color: var(--ward-text); + font-size: var(--text-2xs); + line-height: var(--ward-leading-prose); +} + +@media (max-width: 78rem) { + .headerMeta > span:not(.prototypeBadge) { + display: none; + } + + .constellationGrid { + grid-template-columns: 12rem minmax(32rem, 1fr) 19rem; + } + + .modeLink, + .modeLinkActive { + padding-inline: var(--ward-space-10); + } +} + +@media (max-width: 62rem) { + .modeShell { + grid-template-columns: 4rem minmax(0, 1fr); + } + + .modeHeader, + .modeShell > .modeNavigation, + .modeContent { + display: block; + grid-column: 2; + } + + .roleFocus { + display: grid; + gap: var(--ward-space-2); + margin-bottom: 0.5rem; + } + + .modeHeader { + grid-template-columns: minmax(0, 1fr) auto; + } + + .headerMeta { + display: none; + } + + .modeIdentity span:first-child { + display: none; + } + + .constellationGrid, + .pageGrid { + grid-template-columns: 1fr; + } + + .constellationGrid { + min-width: 48rem; + } + + .compactQueue, + .decisionPanel { + min-height: 24rem; + } + + .assuranceGrid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 40rem) { + .modeShell { + display: block; + min-height: 100dvh; + height: auto; + overflow: visible; + } + + .modeShell > aside { + display: none; + } + + .modeHeader { + position: sticky; + top: 0; + z-index: var(--ward-z-phone); + display: grid; + min-height: 4.25rem; + grid-template-columns: minmax(0, 1fr) auto; + padding: var(--ward-space-8) var(--ward-space-12); + background: var(--ward-canvas); + } + + .modeTitle p { + display: none; + } + + .roleSelect { + width: 2.75rem; + padding: 0; + justify-content: center; + } + + .roleSelect select { + position: absolute; + width: 2.75rem; + min-width: 0; + height: 2.75rem; + opacity: 0; + } + + .modeShell > .modeNavigation { + position: sticky; + top: 4.25rem; + z-index: calc(var(--ward-z-phone) - 1); + min-height: 3.25rem; + padding-inline: var(--ward-space-8); + } + + .modeLink, + .modeLinkActive { + min-width: 4.25rem; + min-height: 2.75rem; + justify-content: center; + padding: 0 var(--ward-space-8); + } + + .modeLabel { + display: none; + } + + .modeShortLabel { + display: inline; + } + + .modeContent { + min-height: calc(100dvh - 7.5rem); + overflow: visible; + padding: var(--ward-space-8); + } + + .pageGrid, + .constellationGrid { + display: block; + min-width: 0; + } + + .pageGrid > *, + .constellationGrid > * { + margin-bottom: 0.5rem; + } + + .networkCanvas { + min-width: 42rem; + min-height: 36rem; + } + + .panel:has(.networkCanvas) { + overflow-x: auto; + } + + .capacitySummary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .dataTable { + min-width: 48rem; + } + + .panel:has(.dataTable), + .panel:has(.stageBoard) { + overflow-x: auto; + } + + .exceptionRow, + .transportRow { + grid-template-columns: auto minmax(0, 1fr); + } + + .exceptionRow > :nth-child(n + 3), + .transportRow > :nth-child(n + 3) { + grid-column: 2; + } + + .assuranceGrid { + grid-template-columns: 1fr; + } + + .patientFacts { + grid-template-columns: 1fr; + } +} + +@media (prefers-reduced-motion: reduce) { + .modeShell * { + scroll-behavior: auto !important; + transition: none !important; + } +} + +@media (forced-colors: active) { + .modeLinkActive, + .queueRowSelected, + .hospitalChipSelected, + .candidateRowSelected, + .primaryButton { + border: 0.125rem solid Highlight; + } +} + +.networkLayout { + display: grid; + min-height: 100%; + gap: var(--ward-space-10); + grid-template-columns: minmax(34rem, 1.6fr) minmax(20rem, 0.7fr); +} + +.networkDiagram { + position: relative; + min-height: 30rem; + height: 100%; + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-lg); + background: var(--ward-subtle); +} + +.networkEdges { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.networkEdge, +.networkEdgeEscalated { + fill: none; + vector-effect: non-scaling-stroke; +} + +.networkEdge { + stroke: var(--ward-border-strong); + stroke-width: 0.0625rem; +} + +.networkEdgeEscalated { + stroke: var(--ward-warning); + stroke-width: 0.125rem; + stroke-dasharray: 0.25rem 0.1875rem; +} + +.networkEdge[data-dimmed="true"], +.networkEdgeEscalated[data-dimmed="true"] { + opacity: 0.18; +} + +.networkHub { + position: absolute; + left: 50%; + top: 50%; + display: grid; + width: 9.5rem; + justify-items: center; + gap: var(--ward-space-2); + transform: translate(-50%, -50%); + border: 0.0625rem solid var(--ward-blue-border); + border-radius: var(--radius-md); + background: var(--ward-canvas); + padding: var(--ward-space-8); + color: var(--ward-blue); + text-align: center; +} + +.networkHub svg { + width: 1rem; + height: 1rem; +} + +.networkHub strong { + color: var(--ward-heading); + font-size: var(--text-2xs); + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.networkHub span { + color: var(--ward-muted); + font-size: var(--text-3xs); + line-height: var(--ward-leading-tight); +} + +.networkAnchor { + position: absolute; + transform: translate(-50%, -50%); + border-radius: var(--radius-sm); + background: var(--ward-canvas); + padding: var(--ward-space-2) var(--ward-space-6); + color: var(--ward-muted); + font-size: var(--text-3xs); + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.networkNodes { + position: absolute; + inset: 0; + pointer-events: none; +} + +.networkNode { + position: absolute; + pointer-events: auto; + display: grid; + width: 3.25rem; + min-height: 3.25rem; + place-content: center; + justify-items: center; + gap: var(--ward-space-2); + transform: translate(-50%, -50%); + border: 0.125rem solid var(--ward-border-strong); + border-radius: var(--ward-radius-round); + background: var(--ward-canvas); + color: var(--ward-heading); +} + +.networkNode strong { + font-size: var(--text-sm); + line-height: var(--ward-leading-compact); +} + +.networkNode span { + font-size: var(--text-3xs); + font-weight: 700; + letter-spacing: 0.04em; +} + +.networkNode[data-band="critical"] { + border-color: var(--ward-danger); + background: var(--ward-danger-soft); + color: var(--ward-danger); +} + +.networkNode[data-band="tight"] { + border-color: var(--ward-warning); + background: var(--ward-warning-soft); + color: var(--ward-warning); +} + +.networkNode[data-band="moderate"] { + border-color: var(--ward-blue-border); + background: var(--ward-blue-soft); + color: var(--ward-blue); +} + +.networkNode[data-band="open"] { + border-color: var(--ward-success); + background: var(--ward-success-soft); + color: var(--ward-success); +} + +.networkNode[aria-pressed="true"] { + box-shadow: var(--e2); + outline: 0.1875rem solid var(--ward-blue); + outline-offset: 0.125rem; +} + +.networkLegend { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: var(--ward-space-12); + padding-top: var(--ward-space-10); +} + +.networkLegendGroup { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--ward-space-12); +} + +.networkLegendItem { + display: inline-flex; + align-items: center; + gap: var(--ward-space-6); + color: var(--ward-muted); + font-size: var(--text-3xs); + font-weight: 600; +} + +.networkSwatch { + width: 0.75rem; + height: 0.75rem; + border: 0.125rem solid var(--ward-border-strong); + border-radius: var(--ward-radius-round); +} + +.networkSwatch[data-band="critical"] { + border-color: var(--ward-danger); + background: var(--ward-danger-soft); +} + +.networkSwatch[data-band="tight"] { + border-color: var(--ward-warning); + background: var(--ward-warning-soft); +} + +.networkSwatch[data-band="moderate"] { + border-color: var(--ward-blue-border); + background: var(--ward-blue-soft); +} + +.networkSwatch[data-band="open"] { + border-color: var(--ward-success); + background: var(--ward-success-soft); +} + +.networkSwatchLine, +.networkSwatchLineEscalated { + width: 1.25rem; + height: 0; +} + +.networkSwatchLine { + border-top: 0.0625rem solid var(--ward-border-strong); +} + +.networkSwatchLineEscalated { + border-top: 0.125rem dashed var(--ward-warning); +} + +.networkDetail { + display: flex; + min-width: 0; + flex-direction: column; + gap: var(--ward-space-10); + overflow-y: auto; +} + +.networkDetailHeading { + color: var(--ward-heading); + font-size: var(--text-2xs); + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.networkRouteList { + display: grid; + gap: var(--ward-space-8); + list-style: none; +} + +.networkRouteLink { + display: grid; + gap: var(--ward-space-4); + border: 0.0625rem solid var(--ward-border); + border-radius: var(--radius-md); + padding: var(--ward-space-10); +} + +@media (max-width: 68rem) { + .networkLayout { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/src/components/ward-management/ward-management-modes.tsx b/src/components/ward-management/ward-management-modes.tsx new file mode 100644 index 000000000..804267411 --- /dev/null +++ b/src/components/ward-management/ward-management-modes.tsx @@ -0,0 +1,888 @@ +"use client"; + +import Link from "next/link"; +import { + ArrowRight, + Building2, + CalendarDays, + Check, + CheckCircle2, + ChevronDown, + CircleAlert, + Clock3, + FileCheck2, + FileClock, + Fingerprint, + Info, + LockKeyhole, + MapPin, + Route, + Scale, + ShieldCheck, + Sparkles, + Truck, + UserRound, +} from "lucide-react"; +import { useMemo, useState } from "react"; + +import { + movementStages, + operationalPriorityScore, + wardHospitalByCode, + wardHospitals, + wardPatients, + wardRegions, + type WardPatient, + type WardRole, +} from "@/components/ward-management/synthetic-fixtures"; +import { WardNetworkWorkspace } from "@/components/ward-management/ward-management-network"; +import { + ClinicalRail, + WardModeNavigation, + type WardMode, +} from "@/components/ward-management/ward-management-navigation"; + +import styles from "./ward-management-modes.module.css"; + +const roleLabels: Record = { + flow: "Flow coordinator", + ed: "ED mental health", + ward: "Ward manager", +}; + +const roleFocusCopy: Record = { + flow: { + title: "Statewide coordination focus", + detail: "Review matches, cross-catchment escalation, holds and owned exceptions.", + }, + ed: { + title: "ED readiness focus", + detail: "Prioritise referral, legal/form timing, handover and transport request readiness.", + }, + ward: { + title: "Ward capacity focus", + detail: "Prioritise capacity freshness, suitability response, acceptance and time-limited holds.", + }, +}; + +const modeCopy: Record = { + command: { title: "Ward Flow", description: "Statewide command view" }, + constellation: { + title: "Operational constellation", + description: "Statewide demand, capacity and selected movement routes", + }, + network: { + title: "Network diagram", + description: "Services as nodes · movements as routes · fill shows bed pressure", + }, + queue: { title: "Priority queue", description: "Human urgency first · operational score within tier" }, + capacity: { title: "Capacity", description: "Ward-confirmed state, capability and freshness" }, + movements: { title: "Movements", description: "Six-stage patient movement board" }, + exceptions: { title: "Action inbox", description: "Owned exceptions, deadlines and stale state" }, + transport: { title: "Transport", description: "Legal, document, booking and handover readiness" }, + governance: { title: "Governance", description: "AI assurance, audit and synthetic data boundary" }, +}; + +const exceptions = [ + { + patientId: "WF-198", + title: "Destination review overdue", + timing: "1h 12m overdue", + owner: "Flow coordinator", + action: "Escalate destination response", + tone: "danger" as const, + }, + { + patientId: "WF-204", + title: "Bed hold expires", + timing: "18 min remaining", + owner: "Ward manager", + action: "Confirm handover or release hold", + tone: "warning" as const, + }, + { + patientId: "WF-201", + title: "Transport delayed", + timing: "ETA +90 min", + owner: "ED mental health", + action: "Review provider and escalation", + tone: "warning" as const, + }, + { + patientId: "WF-209", + title: "Country transfer escalation", + timing: "Provider response pending", + owner: "WACHS MHPF", + action: "Confirm metro destination pathway", + tone: "neutral" as const, + }, +]; + +function toneClass(tone: "good" | "warning" | "danger" | "neutral") { + if (tone === "good") return styles.statusGood; + if (tone === "warning") return styles.statusWarning; + if (tone === "danger") return styles.statusDanger; + return styles.statusNeutral; +} + +function ModeHeader({ + mode, + role, + onRoleChange, +}: { + mode: WardMode; + role: WardRole; + onRoleChange: (role: WardRole) => void; +}) { + const copy = modeCopy[mode]; + return ( +
+
+ + Clinical KB + Source-backed clinical search + +
+

{copy.title}

+

{copy.description}

+
+
+ +
+ Synthetic prototype + Updated 10:42 + 15 Aug 2026 · WA +
+
+ ); +} + +function CompactQueue({ + patients, + selected, + onSelect, +}: { + patients: WardPatient[]; + selected: WardPatient; + onSelect: (patient: WardPatient) => void; +}) { + return ( +
+
+
+

Priority queue

+

Tier first · AI orders within tier

+
+ {patients.length} +
+
+ {patients.slice(0, 9).map((patient, index) => ( + + ))} +
+
+ ); +} + +function HospitalChip({ + hospital, + selected, + onSelect, +}: { + hospital: (typeof wardHospitals)[number]; + selected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +function NetworkCanvas({ selectedCode, onSelect }: { selectedCode: string; onSelect: (code: string) => void }) { + return ( +
+
+
+

Statewide mental health flow

+

Schematic service network · selective movement routes only

+
+ Not geographic +
+
+ {wardRegions.map((region) => { + const hospitals = wardHospitals.filter((hospital) => hospital.region === region); + const available = hospitals.reduce((total, hospital) => total + hospital.available, 0); + return ( +
+
+ + + {available} available now +
+
+ {hospitals.map((hospital) => ( + onSelect(hospital.code)} + /> + ))} +
+
+ ); + })} +
+
+ North / Country demand
+
+
+
+
+
+
+
+ ); +} + +function DecisionPanel({ + patient, + role, + selectedCode, + onSelectCode, +}: { + patient: WardPatient; + role: WardRole; + selectedCode: string; + onSelectCode: (code: string) => void; +}) { + const [confirmed, setConfirmed] = useState(false); + const candidates = useMemo( + () => [ + { + hospital: wardHospitalByCode(patient.destinationCode), + score: patient.score, + reason: "Best eligible operational fit", + }, + ...patient.alternatives.map((alternative) => ({ + hospital: wardHospitalByCode(alternative.hospitalCode), + score: alternative.score, + reason: alternative.reason, + })), + ], + [patient], + ); + const selected = candidates.find((candidate) => candidate.hospital.code === selectedCode) ?? candidates[0]; + + return ( + + ); +} + +function ConstellationView({ role }: { role: WardRole }) { + const [selectedPatient, setSelectedPatient] = useState(wardPatients[0]); + const [selectedCode, setSelectedCode] = useState(selectedPatient.destinationCode); + const rolePatients = useMemo(() => { + if (role === "flow") return wardPatients; + if (role === "ed") + return [...wardPatients].sort((a, b) => Number(b.owner.includes("ED")) - Number(a.owner.includes("ED"))); + return [...wardPatients].sort((a, b) => Number(b.owner.includes("Ward")) - Number(a.owner.includes("Ward"))); + }, [role]); + + function selectPatient(patient: WardPatient) { + setSelectedPatient(patient); + setSelectedCode(patient.destinationCode); + } + + return ( +
+ + + +
+ ); +} + +function QueueView({ role }: { role: WardRole }) { + const [selected, setSelected] = useState(wardPatients[0]); + const rolePatients = useMemo(() => { + if (role === "flow") return wardPatients; + if (role === "ed") + return [...wardPatients].sort((a, b) => Number(b.owner.includes("ED")) - Number(a.owner.includes("ED"))); + return [...wardPatients].sort((a, b) => Number(b.owner.includes("Ward")) - Number(a.owner.includes("Ward"))); + }, [role]); + return ( +
+
+
+
+

Placement-ready movements

+

Human tier remains primary. Operational score explains ordering within tier.

+
+ {wardPatients.length} synthetic records +
+ + + + + + + + + + + + + + {rolePatients.map((patient) => ( + + + + + + + + + + ))} + +
PatientPriorityWaitNeedCatchmentReferral / blockerAI fit
+ + P{patient.urgency}{patient.elapsed} + {patient.cohort} · {patient.setting} + {patient.catchment}{patient.blocker}{patient.score}%
+
+ undefined} + /> +
+ ); +} + +function CapacityView() { + const totals = { + available: wardHospitals.reduce((sum, hospital) => sum + hospital.available, 0), + held: wardHospitals.reduce((sum, hospital) => sum + hospital.held, 0), + potential: wardHospitals.reduce((sum, hospital) => sum + hospital.potential, 0), + blocked: wardHospitals.reduce((sum, hospital) => sum + hospital.blocked, 0), + occupied: wardHospitals.reduce((sum, hospital) => sum + hospital.occupied, 0), + }; + return ( +
+
+
+

Ward-confirmed capacity

+

Availability is not suitability. Every count includes a freshness signal.

+
+ Synthetic counts +
+
+ {Object.entries(totals).map(([label, value]) => ( +
+ {label.replace(/^./, (character) => character.toUpperCase())} + {value} + Across 16 synthetic services +
+ ))} +
+ + + + + + + + + + + + {wardHospitals.map((hospital) => ( + + + + + + + + ))} + +
ServiceRegionCapability cueFive bed statesFreshness
+ {hospital.name} +
{hospital.beds} total beds
+
{hospital.region}Adult / older adult · open / secure review +
+ + {hospital.available}Now + + + {hospital.held}Held + + + {hospital.potential}Potential + + + {hospital.blocked}Blocked + + + {hospital.occupied}Occupied + +
+
+ + {hospital.lastConfirmed < "10:28" ? "Review soon" : "Current"} · {hospital.lastConfirmed} + +
+

+ Potential capacity requires a named action and estimate. No departing-patient details are shown in this + prototype. +

+
+ ); +} + +function MovementsView() { + return ( +
+
+
+

Patient movement board

+

Cross-catchment escalation remains a flag, not an extra stage.

+
+ Shared record · role-owned actions +
+
+ {movementStages.map((stage) => ( +
+
+

{stage.label}

+ {stage.count} +
+ {wardPatients + .filter((patient) => patient.stage === stage.id) + .slice(0, 4) + .map((patient) => ( + + + {patient.id} + P{patient.urgency} + + + {patient.elapsed} · {patient.catchment} · {patient.setting} + + {patient.owner} + + ))} +
+ ))} +
+
+ ); +} + +function ExceptionsView() { + return ( +
+
+
+
+

Action exceptions

+

Only items with an owner and required next action appear here.

+
+ 1 overdue +
+
+ {exceptions.map((item) => ( +
+
+ ))} +
+
+ +
+ ); +} + +function TransportView() { + const transportPatients = wardPatients.filter((patient) => patient.stage !== "arrived").slice(0, 8); + return ( +
+
+
+
+

Transport readiness

+

Metro and country pathways retain distinct documentation and escalation cues.

+
+ No dispatch integration +
+
+ {transportPatients.map((patient) => { + const delayed = + patient.transport.toLowerCase().includes("delay") || patient.transport.toLowerCase().includes("pending"); + return ( +
+
+ ); + })} +
+
+ +
+ ); +} + +function GovernanceView() { + const sources = [ + ["WA Health System Flow Centre", "https://www.health.wa.gov.au/Improving-WA-Health/System-Flow-Centre"], + [ + "Mental Health Act 2014 forms", + "https://www.chiefpsychiatrist.wa.gov.au/laws-and-rights/legislation/mental-health-act-2014-forms/", + ], + ["WA mental health patient transport", "https://www.health.wa.gov.au/Articles/J_M/Mental-health-patient-transport"], + [ + "WA Health Artificial Intelligence Policy", + "https://www.health.wa.gov.au/about-us/policy-frameworks/digital-health/mandatory-requirements/artificial-intelligence-policy", + ], + ]; + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Synthetic decision audit

+

Representative review trail for WF-204

+
+
+
    +
  1. +
  2. +
  3. +
  4. +
  5. +
  6. +
  7. +
  8. +
+
+ +
+
+ ); +} + +function RoleFocus({ role }: { role: WardRole }) { + const focus = roleFocusCopy[role]; + return ( +
+ {focus.title} + {focus.detail} +
+ ); +} + +function ModeBody({ mode, role }: { mode: Exclude; role: WardRole }) { + if (mode === "constellation") return ; + if (mode === "network") return ; + if (mode === "queue") return ; + if (mode === "capacity") return ; + if (mode === "movements") return ; + if (mode === "exceptions") return ; + if (mode === "transport") return ; + return ; +} + +export function WardModeWorkspace({ mode }: { mode: Exclude }) { + const [role, setRole] = useState("flow"); + return ( +
+ + + +
+ + +
+
+ ); +} diff --git a/src/components/ward-management/ward-management-navigation.tsx b/src/components/ward-management/ward-management-navigation.tsx new file mode 100644 index 000000000..9c4805b1a --- /dev/null +++ b/src/components/ward-management/ward-management-navigation.tsx @@ -0,0 +1,196 @@ +"use client"; + +import Link from "next/link"; +import { + Activity, + BedSingle, + CircleAlert, + FileCheck2, + HeartPulse, + LayoutDashboard, + LayoutGrid, + ListFilter, + MapPinned, + MessageSquarePlus, + Pill, + Route, + Settings, + ShieldCheck, + SlidersHorizontal, + Truck, + Waypoints, + Wrench, +} from "lucide-react"; + +import { BrandMark } from "@/components/clinical-dashboard/brand"; + +import shellStyles from "./ward-management.module.css"; +import modeStyles from "./ward-management-modes.module.css"; + +export type WardMode = + | "command" + | "constellation" + | "network" + | "queue" + | "capacity" + | "movements" + | "exceptions" + | "transport" + | "governance"; + +function RailLink({ + href, + label, + active, + children, +}: { + href: string; + label: string; + active?: boolean; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} + +export function ClinicalRail() { + return ( + + ); +} + +export function WardModeNavigation({ active }: { active: WardMode }) { + return ( + + ); +} diff --git a/src/components/ward-management/ward-management-network.module.css b/src/components/ward-management/ward-management-network.module.css new file mode 100644 index 000000000..7fc95818e --- /dev/null +++ b/src/components/ward-management/ward-management-network.module.css @@ -0,0 +1,753 @@ +.networkPage { + --net-canvas: var(--surface); + --net-subtle: var(--surface-subtle); + --net-chrome: var(--surface-chrome); + --net-border: var(--border); + --net-border-strong: var(--border-strong); + --net-text: var(--text); + --net-heading: var(--text-heading); + --net-muted: var(--text-muted); + --net-blue: var(--clinical-accent); + --net-blue-soft: var(--clinical-accent-soft); + --net-blue-border: var(--clinical-accent-border); + --net-good: var(--success-text); + --net-good-soft: var(--success-bg); + --net-warn: var(--warning-text); + --net-warn-soft: var(--warning-bg); + --net-danger: var(--danger-text); + --net-danger-soft: var(--danger-bg); + --net-space-2: 0.125rem; + --net-space-4: 0.25rem; + --net-space-6: 0.375rem; + --net-space-8: 0.5rem; + --net-space-10: 0.625rem; + --net-space-12: 0.75rem; + --net-space-16: 1rem; + --net-space-20: 1.25rem; + --net-leading-compact: 1; + --net-leading-tight: 1.25; + --net-leading-body: 1.45; + --net-z-canvas: 1; + --net-z-node: 2; + --net-round: 50%; + display: grid; + min-height: 100%; + gap: var(--net-space-10); + grid-template-rows: auto minmax(0, 1fr); + color: var(--net-text); + font-variant-numeric: tabular-nums; +} + +.pipeline { + display: grid; + align-items: center; + gap: var(--net-space-8); + grid-auto-flow: column; + grid-auto-columns: minmax(0, 1fr); + border: 0.0625rem solid var(--net-border); + border-radius: var(--radius-lg); + background: var(--net-canvas); + padding: var(--net-space-12) var(--net-space-16); + box-shadow: var(--e0); +} + +.pipelineStage { + display: grid; + min-width: 0; + gap: var(--net-space-2); + border-left: 0.0625rem solid var(--net-border); + padding-left: var(--net-space-12); +} + +.pipelineStage:first-child { + border-left: 0; + padding-left: 0; +} + +.pipelineLabel { + color: var(--net-muted); + font-size: var(--text-3xs); + line-height: var(--net-leading-tight); +} + +.pipelineStage strong { + color: var(--net-heading); + font-size: var(--text-lg); + line-height: var(--net-leading-compact); +} + +.networkGrid { + display: grid; + min-height: 0; + gap: var(--net-space-10); + grid-template-columns: 13.5rem minmax(38rem, 1fr) 23rem; +} + +/* Focus mode hands the shortlist's width to the diagram without unmounting it. */ +.networkPage[data-shortlist="collapsed"] .networkGrid { + grid-template-columns: 13.5rem minmax(38rem, 1fr); +} + +.networkPage[data-shortlist="collapsed"] .shortlistPanel { + display: none; +} + +.headerActions { + display: inline-flex; + align-items: center; + gap: var(--net-space-8); +} + +.focusToggle { + display: inline-flex; + min-height: 3rem; + align-items: center; + gap: var(--net-space-4); + border: 0.0625rem solid var(--net-border); + border-radius: var(--radius-sm); + background: var(--net-canvas); + padding: 0 var(--net-space-10); + color: var(--net-heading); + font-size: var(--text-3xs); + font-weight: 700; +} + +.focusToggle svg { + width: 0.875rem; + height: 0.875rem; +} + +.queuePanel, +.canvasPanel, +.shortlistPanel { + display: flex; + min-width: 0; + flex-direction: column; + gap: var(--net-space-10); + border: 0.0625rem solid var(--net-border); + border-radius: var(--radius-lg); + background: var(--net-canvas); + padding: var(--net-space-12); + box-shadow: var(--e0); +} + +.queuePanel, +.shortlistPanel { + overflow-y: auto; +} + +.panelHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--net-space-8); +} + +.panelHeader h2 { + display: inline-flex; + align-items: center; + gap: var(--net-space-6); + color: var(--net-heading); + font-size: var(--text-sm); + font-weight: 700; + line-height: var(--net-leading-tight); +} + +.panelHeader h2 svg { + width: 1rem; + height: 1rem; + color: var(--net-blue); +} + +.count { + border-radius: var(--radius-sm); + background: var(--net-subtle); + padding: var(--net-space-2) var(--net-space-8); + color: var(--net-muted); + font-size: var(--text-3xs); + font-weight: 700; +} + +.schematicBadge { + display: inline-flex; + align-items: center; + gap: var(--net-space-4); + border: 0.0625rem solid var(--net-blue-border); + border-radius: var(--radius-sm); + background: var(--net-blue-soft); + padding: var(--net-space-2) var(--net-space-8); + color: var(--net-blue); + font-size: var(--text-3xs); + font-weight: 700; +} + +.schematicBadge svg { + width: 0.75rem; + height: 0.75rem; +} + +/* --- queue ------------------------------------------------------------- */ + +.queueList { + display: grid; + gap: var(--net-space-6); +} + +.queueRow { + display: grid; + min-height: 3rem; + gap: var(--net-space-2); + border: 0.0625rem solid var(--net-border); + border-radius: var(--radius-md); + background: var(--net-canvas); + padding: var(--net-space-8); + text-align: left; +} + +.queueRow[aria-pressed="true"] { + border-color: var(--net-blue); + background: var(--net-blue-soft); +} + +.queueTop { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--net-space-6); +} + +.queueTop strong { + color: var(--net-heading); + font-size: var(--text-xs); +} + +.elapsed { + color: var(--net-warn); + font-size: var(--text-3xs); + font-weight: 700; +} + +.queueMeta { + display: flex; + align-items: center; + gap: var(--net-space-4); + color: var(--net-muted); + font-size: var(--text-3xs); + line-height: var(--net-leading-tight); +} + +.tier { + display: inline-grid; + width: 1.125rem; + height: 1.125rem; + place-items: center; + border-radius: var(--radius-xs); + font-size: var(--text-3xs); + font-weight: 700; +} + +.tier[data-tier="1"] { + background: var(--net-danger-soft); + color: var(--net-danger); +} + +.tier[data-tier="2"] { + background: var(--net-warn-soft); + color: var(--net-warn); +} + +.tier[data-tier="3"] { + background: var(--net-subtle); + color: var(--net-muted); +} + +/* --- canvas ------------------------------------------------------------ */ + +.canvas { + position: relative; + display: grid; + min-height: 34rem; + flex: 1; + align-items: center; + gap: var(--net-space-16); + grid-template-columns: minmax(0, 1fr) 12.5rem minmax(0, 1fr); + border: 0.0625rem solid var(--net-border); + border-radius: var(--radius-lg); + background: var(--net-subtle); + padding: var(--net-space-16); +} + +.connectorLayer { + position: absolute; + z-index: var(--net-z-canvas); + inset: 0; + width: 100%; + height: 100%; + overflow: visible; + pointer-events: none; +} + +.connector, +.connectorRoute { + fill: none; + vector-effect: non-scaling-stroke; +} + +.connector { + stroke: var(--net-border-strong); + stroke-width: 0.0625rem; +} + +.connectorRoute { + stroke: var(--net-blue); + stroke-width: 0.125rem; +} + +.arrowHead { + fill: var(--net-border-strong); +} + +.arrowHeadRoute { + fill: var(--net-blue); +} + +.column { + z-index: var(--net-z-node); + display: grid; + align-content: center; + gap: var(--net-space-16); +} + +.column[data-side="left"] { + grid-column: 1; +} + +.column[data-side="right"] { + grid-column: 3; +} + +.cluster { + display: grid; + gap: var(--net-space-6); +} + +.clusterHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--net-space-8); + border: 0.0625rem solid var(--net-border); + border-radius: var(--radius-md); + background: var(--net-canvas); + padding: var(--net-space-6) var(--net-space-10); +} + +.clusterHeader strong { + color: var(--net-heading); + font-size: var(--text-3xs); + font-weight: 700; + letter-spacing: 0.08em; +} + +.clusterHeader span { + color: var(--net-muted); + font-size: var(--text-3xs); +} + +.clusterCards { + display: grid; + gap: var(--net-space-6); +} + +.serviceCard { + display: grid; + min-height: 3rem; + gap: var(--net-space-2); + border: 0.0625rem solid var(--net-border); + border-radius: var(--radius-md); + background: var(--net-canvas); + padding: var(--net-space-8) var(--net-space-10); + text-align: left; + box-shadow: var(--e0); +} + +.serviceCard[data-routed="true"] { + border-color: var(--net-blue); + box-shadow: var(--e1); +} + +.serviceCard[aria-pressed="true"] { + outline: 0.125rem solid var(--net-blue); + outline-offset: 0.0625rem; +} + +.serviceName { + color: var(--net-heading); + font-size: var(--text-xs); + font-weight: 700; + line-height: var(--net-leading-tight); +} + +.serviceCapability { + color: var(--net-muted); + font-size: var(--text-3xs); + line-height: var(--net-leading-tight); +} + +.bedChips { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: var(--net-space-4); +} + +.bedChip { + display: inline-grid; + min-width: 1.125rem; + height: 1.125rem; + place-items: center; + border: 0.0625rem solid var(--net-border-strong); + border-radius: var(--radius-xs); + padding: 0 var(--net-space-2); + font-size: var(--text-3xs); + font-weight: 700; +} + +.bedChip[data-state="available"] { + border-color: var(--net-good); + background: var(--net-good-soft); + color: var(--net-good); +} + +.bedChip[data-state="held"] { + border-color: var(--net-warn); + background: var(--net-warn-soft); + color: var(--net-warn); +} + +.bedChip[data-state="potential"] { + border-style: dashed; + border-color: var(--net-blue); + color: var(--net-blue); +} + +.bedChip[data-state="blocked"] { + border-color: var(--net-danger); + background: var(--net-danger-soft); + color: var(--net-danger); +} + +.bedTime { + color: var(--net-muted); + font-size: var(--text-3xs); +} + +.hub { + z-index: var(--net-z-node); + grid-column: 2; + grid-row: 1; + display: grid; + justify-items: center; + gap: var(--net-space-2); + border: 0.0625rem solid var(--net-blue-border); + border-radius: var(--radius-md); + background: var(--net-canvas); + padding: var(--net-space-12) var(--net-space-10); + text-align: center; + box-shadow: var(--e1); +} + +.hub svg { + width: 1.25rem; + height: 1.25rem; + color: var(--net-blue); +} + +.hub strong { + color: var(--net-blue); + font-size: var(--text-3xs); + font-weight: 700; + letter-spacing: 0.06em; +} + +.hub span { + color: var(--net-muted); + font-size: var(--text-3xs); + line-height: var(--net-leading-tight); +} + +.hubMeta { + color: var(--net-heading); + font-weight: 700; +} + +/* --- legend ------------------------------------------------------------ */ + +.legend { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--net-space-12); +} + +.legendTitle { + color: var(--net-heading); + font-size: var(--text-3xs); + font-weight: 700; +} + +.legendItem { + display: inline-flex; + align-items: center; + gap: var(--net-space-4); + color: var(--net-muted); + font-size: var(--text-3xs); +} + +.legendItem b { + color: var(--net-heading); +} + +.legendSwatch { + width: 0.75rem; + height: 0.75rem; + border: 0.0625rem solid var(--net-border-strong); + border-radius: var(--radius-xs); +} + +.legendSwatch[data-state="available"] { + border-color: var(--net-good); + background: var(--net-good-soft); +} + +.legendSwatch[data-state="held"] { + border-color: var(--net-warn); + background: var(--net-warn-soft); +} + +.legendSwatch[data-state="potential"] { + border-style: dashed; + border-color: var(--net-blue); +} + +.legendSwatch[data-state="blocked"] { + border-color: var(--net-danger); + background: var(--net-danger-soft); +} + +.legendRoute, +.legendDemand { + width: 1.25rem; + height: 0; +} + +.legendRoute { + border-top: 0.125rem solid var(--net-blue); +} + +.legendDemand { + border-top: 0.0625rem solid var(--net-border-strong); +} + +/* --- shortlist --------------------------------------------------------- */ + +.patientLine { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--net-space-4); + color: var(--net-heading); + font-size: var(--text-3xs); + font-weight: 700; +} + +.patientSubLine { + color: var(--net-muted); + font-size: var(--text-3xs); + line-height: var(--net-leading-body); +} + +.tableScroll { + overflow-x: auto; +} + +.compareTable { + width: 100%; + border-collapse: collapse; + font-size: var(--text-3xs); +} + +.compareTable th, +.compareTable td { + border: 0.0625rem solid var(--net-border); + padding: var(--net-space-6); + text-align: left; + vertical-align: top; +} + +.compareTable thead th { + background: var(--net-subtle); + color: var(--net-blue); + font-weight: 700; +} + +.compareTable tbody th { + color: var(--net-muted); + font-weight: 600; +} + +.compareTable td strong { + color: var(--net-heading); + font-size: var(--text-xs); +} + +.compareTable td[data-tone="good"] { + color: var(--net-good); + font-weight: 700; +} + +.compareTable td[data-tone="warning"] { + color: var(--net-warn); + font-weight: 700; +} + +.compareTable td[data-tone="danger"] { + color: var(--net-danger); + font-weight: 700; +} + +.tierNote { + display: flex; + align-items: flex-start; + gap: var(--net-space-4); + border-radius: var(--radius-md); + background: var(--net-subtle); + padding: var(--net-space-8); + color: var(--net-muted); + font-size: var(--text-3xs); + line-height: var(--net-leading-body); +} + +.tierNote b { + color: var(--net-heading); +} + +.factorsToggle { + display: flex; + min-height: 3rem; + align-items: center; + justify-content: space-between; + gap: var(--net-space-8); + border: 0.0625rem solid var(--net-border); + border-radius: var(--radius-md); + background: var(--net-canvas); + padding: 0 var(--net-space-10); + color: var(--net-heading); + font-size: var(--text-3xs); + font-weight: 700; +} + +.factorsToggle svg { + width: 0.875rem; + height: 0.875rem; +} + +.factorsToggle svg[data-open="true"] { + transform: rotate(180deg); +} + +.factorList { + display: grid; + gap: var(--net-space-4); + padding-left: var(--net-space-16); + color: var(--net-muted); + font-size: var(--text-3xs); + line-height: var(--net-leading-body); +} + +.ownerBlock { + display: grid; + gap: var(--net-space-2); + border: 0.0625rem solid var(--net-border); + border-radius: var(--radius-md); + padding: var(--net-space-10); + font-size: var(--text-3xs); +} + +.ownerLabel { + color: var(--net-muted); +} + +.ownerBlock strong { + color: var(--net-heading); + font-size: var(--text-xs); +} + +.ownerBlock span { + color: var(--net-muted); + line-height: var(--net-leading-body); +} + +.primaryLink { + display: inline-flex; + min-height: 3rem; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + background: var(--net-blue); + padding: 0 var(--net-space-16); + color: var(--net-canvas); + font-size: var(--text-xs); + font-weight: 700; +} + +.assurance { + color: var(--net-muted); + font-size: var(--text-3xs); + text-align: center; +} + +.detailBlock { + display: grid; + gap: var(--net-space-6); + border: 0.0625rem solid var(--net-blue-border); + border-radius: var(--radius-md); + background: var(--net-blue-soft); + padding: var(--net-space-10); +} + +.detailBlock h3 { + color: var(--net-heading); + font-size: var(--text-xs); + font-weight: 700; +} + +.detailBlock p { + color: var(--net-muted); + font-size: var(--text-3xs); + line-height: var(--net-leading-body); +} + +.detailMeta { + color: var(--net-muted); +} + +@media (max-width: 80rem) { + .networkGrid { + grid-template-columns: minmax(0, 1fr); + } + + .canvas { + grid-template-columns: minmax(0, 1fr); + } + + .connectorLayer { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .networkPage * { + transition: none !important; + } +} diff --git a/src/components/ward-management/ward-management-network.tsx b/src/components/ward-management/ward-management-network.tsx new file mode 100644 index 000000000..d23befc73 --- /dev/null +++ b/src/components/ward-management/ward-management-network.tsx @@ -0,0 +1,503 @@ +"use client"; + +import Link from "next/link"; +import { ChevronDown, ChevronLeft, ChevronRight, Info, Network, Sparkles } from "lucide-react"; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; + +import { + movementStages, + wardHospitalByCode, + wardHospitals, + wardPatients, + wardRegions, + type WardHospital, + type WardPatient, +} from "@/components/ward-management/synthetic-fixtures"; + +import styles from "./ward-management-network.module.css"; + +type BedStateKey = "available" | "held" | "potential" | "blocked"; + +const bedStateCopy: Record = { + available: { label: "Ready", detail: "Available now" }, + held: { label: "Held", detail: "Bed held" }, + potential: { label: "Potential", detail: "May become available" }, + blocked: { label: "Blocked", detail: "Not available" }, +}; + +/** Left column carries North then Country; right column carries East then South. */ +const columnRegions = { + left: ["North", "Country"] as const, + right: ["East", "South"] as const, +}; + +type Connector = { id: string; path: string; kind: "demand" | "route" }; + +function capabilityLabel(hospital: WardHospital) { + const cohorts = hospital.cohorts.map((cohort) => (cohort === "Older adult" ? "Older" : cohort)).join("/"); + return `${hospital.settings.join(" / ")} · ${cohorts}`; +} + +function candidatesFor(patient: WardPatient) { + return [ + { + hospital: wardHospitalByCode(patient.destinationCode), + score: patient.score, + etaLabel: patient.transport, + rank: 1, + }, + ...patient.alternatives.map((alternative, index) => ({ + hospital: wardHospitalByCode(alternative.hospitalCode), + score: alternative.score, + etaLabel: alternative.etaLabel ?? "Re-book required", + rank: index + 2, + })), + ]; +} + +function catchmentFit(patient: WardPatient, hospital: WardHospital) { + if (hospital.region === patient.catchment) return { label: "Best", tone: "good" as const }; + if (hospital.service === "WACHS" && patient.catchment === "Country") return { label: "Good", tone: "good" as const }; + return { label: "Escalation", tone: "warning" as const }; +} + +function settingFit(patient: WardPatient, hospital: WardHospital) { + const setting = hospital.settings.includes(patient.setting); + const cohort = hospital.cohorts.includes(patient.cohort); + if (setting && cohort) return { label: "Exact match", tone: "good" as const }; + if (setting || cohort) return { label: "Partial match", tone: "warning" as const }; + return { label: "Not eligible", tone: "danger" as const }; +} + +function BedStateChips({ hospital, showTime }: { hospital: WardHospital; showTime?: boolean }) { + return ( + + {(Object.keys(bedStateCopy) as BedStateKey[]).map((key) => ( + + {hospital[key]} + + ))} + {showTime ? {hospital.lastConfirmed} : null} + + ); +} + +function ServiceCard({ + hospital, + routed, + selected, + onSelect, + registerRef, +}: { + hospital: WardHospital; + routed: boolean; + selected: boolean; + onSelect: () => void; + registerRef: (code: string, node: HTMLButtonElement | null) => void; +}) { + return ( + + ); +} + +export function WardNetworkWorkspace() { + const [selectedPatientId, setSelectedPatientId] = useState(wardPatients[0].id); + const [selectedCode, setSelectedCode] = useState(null); + const [factorsOpen, setFactorsOpen] = useState(false); + const [shortlistOpen, setShortlistOpen] = useState(true); + + const patient = useMemo( + () => wardPatients.find((candidate) => candidate.id === selectedPatientId) ?? wardPatients[0], + [selectedPatientId], + ); + const candidates = useMemo(() => candidatesFor(patient), [patient]); + const routedCodes = useMemo(() => new Set(candidates.map((candidate) => candidate.hospital.code)), [candidates]); + + const canvasRef = useRef(null); + const hubRef = useRef(null); + const clusterRefs = useRef(new Map()); + const cardRefs = useRef(new Map()); + const [connectors, setConnectors] = useState([]); + + const registerCard = useCallback((code: string, node: HTMLButtonElement | null) => { + cardRefs.current.set(code, node); + }, []); + const registerCluster = useCallback((region: string, node: HTMLElement | null) => { + clusterRefs.current.set(region, node); + }, []); + + const measure = useCallback(() => { + const canvas = canvasRef.current; + const hub = hubRef.current; + if (!canvas || !hub) return; + const base = canvas.getBoundingClientRect(); + const hubBox = hub.getBoundingClientRect(); + const hubLeft = { x: hubBox.left - base.left, y: hubBox.top - base.top + hubBox.height / 2 }; + const hubRight = { x: hubBox.right - base.left, y: hubLeft.y }; + const next: Connector[] = []; + + /** Elbow: leave the source edge, run along a mid trunk, then enter the target edge. */ + const elbow = (from: { x: number; y: number }, to: { x: number; y: number }) => { + const trunk = from.x + (to.x - from.x) / 2; + return `M ${from.x} ${from.y} H ${trunk} V ${to.y} H ${to.x}`; + }; + + for (const region of wardRegions) { + const node = clusterRefs.current.get(region); + if (!node) continue; + const box = node.getBoundingClientRect(); + const onLeft = (columnRegions.left as readonly string[]).includes(region); + const from = { x: (onLeft ? box.right : box.left) - base.left, y: box.top - base.top + box.height / 2 }; + next.push({ id: `demand-${region}`, path: elbow(from, onLeft ? hubLeft : hubRight), kind: "demand" }); + } + + for (const candidate of candidates) { + const node = cardRefs.current.get(candidate.hospital.code); + if (!node) continue; + const box = node.getBoundingClientRect(); + const onLeft = (columnRegions.left as readonly string[]).includes(candidate.hospital.region); + const to = { x: (onLeft ? box.right : box.left) - base.left, y: box.top - base.top + box.height / 2 }; + next.push({ + id: `route-${candidate.hospital.code}`, + path: elbow(onLeft ? hubLeft : hubRight, to), + kind: "route", + }); + } + + setConnectors(next); + }, [candidates]); + + useLayoutEffect(() => { + measure(); + }, [measure]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas || typeof ResizeObserver === "undefined") return; + const observer = new ResizeObserver(() => measure()); + observer.observe(canvas); + window.addEventListener("resize", measure); + return () => { + observer.disconnect(); + window.removeEventListener("resize", measure); + }; + }, [measure]); + + const detail = selectedCode ? wardHospitalByCode(selectedCode) : null; + const openMovements = movementStages.reduce((sum, stage) => sum + stage.count, 0); + + return ( +
+
+ {movementStages.map((stage, index) => ( + + + {index + 1} {stage.label} + + {stage.count} + + ))} +
+ +
+
+
+

Priority queue

+ {wardPatients.length} +
+
+ {wardPatients.map((candidate) => ( + + ))} +
+
+ +
+
+

+

+ + + + + +
+ +
+ + + {(["left", "right"] as const).map((side) => ( +
+ {columnRegions[side].map((region) => ( +
registerCluster(region, node)} + aria-labelledby={`ward-network-${region}`} + > +
+ {region.toUpperCase()} + + {wardHospitals + .filter((hospital) => hospital.region === region) + .reduce((sum, hospital) => sum + hospital.available, 0)}{" "} + ready + +
+
+ {wardHospitals + .filter((hospital) => hospital.region === region) + .map((hospital) => ( + setSelectedCode(detail?.code === hospital.code ? null : hospital.code)} + registerRef={registerCard} + /> + ))} +
+
+ ))} +
+ ))} + +
+
+
+ +
+ Legend + {(Object.keys(bedStateCopy) as BedStateKey[]).map((key) => ( + +