diff --git a/docs/caring-contacts/accessibility-acceptance.md b/docs/caring-contacts/accessibility-acceptance.md new file mode 100644 index 0000000000..a79e8ea21e --- /dev/null +++ b/docs/caring-contacts/accessibility-acceptance.md @@ -0,0 +1,99 @@ +# Caring contacts — accessibility and responsive acceptance + +**Status:** local synthetic-prototype evidence, 15 August 2026 +**Boundary:** Chromium evidence and source review; not physical-device or production acceptance + +## 1. Evidence classification + +| Area | Status | Evidence and limit | +| --------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 320px / 400% reflow proxy | Actual local Chromium evidence | 320px viewport is the repository proxy for 400% zoom on 1280px. Core, boundary and delivery journeys have no horizontal page overflow or obscured action. This is a proxy, not a browser-zoom measurement. | +| 390px | Actual local Chromium evidence | Compact shell, four-item dock, activation sheets, all overlays, offline/protected actions and final screenshots. | +| 430px | Actual local Chromium evidence | Compact continuity retained at the upper compact boundary; no horizontal overflow. | +| 768px | Actual local Chromium evidence | Collapsed desktop navigation/rail state, not enlarged phone; contextual delivery rail remains in bounds. | +| 1024px | Actual local Chromium evidence | Split-capable continuity; no clipped panes or horizontal page overflow. | +| 1440px | Actual local Chromium evidence | Wide navigation, workflow preview, dialogs/drawers and final screenshots. | +| Keyboard and focus | Actual DOM and Chromium evidence | Keyboard activation, workflow-heading focus, Escape dismissal, trigger focus restoration, session-gate containment, offline focusability and protected-decision denial. | +| Names, roles and states | Actual DOM and Chromium evidence plus source review | One page `h1`; named navigation, lists, dialogs, status banner, schedule and controls; expanded/current/unavailable states exposed. | +| Word/non-colour status | Actual DOM/source evidence | Every operational status has visible text and a dot/icon/structure. Tests retain wording in forced colours. | +| Text sizing and long values | Source plus responsive rendered evidence | Semantic type tokens; full values wrap; no truncated page/dialog title; 320px overflow proof. Dedicated browser text-resize beyond the 320px proxy is unrun. | +| Dock and safe area | Actual local Chromium evidence | Compact actions are scrolled clear of the fixed dock in routine journeys. During evidence capture only, the fixed phone dock is hidden and each required region is asserted not to intersect a painted/interactive fixed overlay before pixels are written. | +| Dark mode | Rendered specimen plus source review | Dark specimen uses repository theme roles. A whole-suite dark-theme screenshot journey is not part of this focused proof. | +| Forced colours | Actual local Chromium media evidence | Forced-colour media activates; status words remain; system-colour continuity and specimens are present. | +| Reduced motion | Actual local Chromium media evidence | Reduced-motion media activates; continuity and status meaning remain; motion classes stop/avoid animation. | +| Physical iPhone Safari | **Unrun — required later** | Chromium responsive emulation cannot close Safari viewport, safe-area, keyboard or focus behaviour. | +| Installed PWA | **Unrun — required later** | Requires an authorised production-like build on a managed physical device, including install/launch and offline boundary checks. | + +## 2. Final rendered-evidence manifest + +The ignored local directory +`.local/caring-contact-design-evidence/2026-08-15` contains exactly the following 26 synthetic PNGs. +Routine browser runs do not write or remove evidence; only +`CARING_CONTACT_CAPTURE_EVIDENCE=1` enables the deterministic reset and capture. Delivery and +component/state dialogs use named internal-scroll slices so the real viewport, header/footer and +every required section are represented without expanding the production Sheet. + +| File | Dimensions | +| ------------------------------------------------------- | ---------- | +| `foundation-phone-390.png` | 390×1410 | +| `foundation-desktop-1440.png` | 1440×698 | +| `today-phone-390.png` | 390×1142 | +| `today-desktop-1440.png` | 1440×606 | +| `activation-review-phone-390.png` | 390×2777 | +| `activation-review-desktop-1440.png` | 1440×2401 | +| `patient-overview-phone-390.png` | 390×2626 | +| `patient-overview-desktop-1440.png` | 1440×2193 | +| `schedule-phone-390.png` | 390×2644 | +| `schedule-desktop-1440.png` | 1440×2202 | +| `delivery-exception-transport-phone-390.png` | 390×844 | +| `delivery-exception-transport-desktop-1440.png` | 512×1000 | +| `delivery-exception-operational-task-phone-390.png` | 390×844 | +| `delivery-exception-operational-task-desktop-1440.png` | 512×1000 | +| `delivery-exception-clinical-boundary-phone-390.png` | 390×844 | +| `delivery-exception-clinical-boundary-desktop-1440.png` | 512×1000 | +| `delivery-exception-audit-phone-390.png` | 390×844 | +| `delivery-exception-audit-desktop-1440.png` | 512×1000 | +| `component-state-interaction-phone-390.png` | 390×844 | +| `component-state-interaction-desktop-1440.png` | 1392×952 | +| `component-state-content-phone-390.png` | 390×844 | +| `component-state-content-desktop-1440.png` | 1392×952 | +| `component-state-modes-phone-390.png` | 390×844 | +| `component-state-modes-desktop-1440.png` | 1392×952 | +| `component-state-system-phone-390.png` | 390×844 | +| `component-state-system-desktop-1440.png` | 1392×952 | + +Every file was visually inspected after the final capture. The phone dock is absent from capture +pixels; required content remains visible across the named slices; the desktop Version conflict tile +is fully visible in `component-state-system-desktop-1440.png`. + +## 3. Keyboard and focus acceptance + +- Destination buttons, More sheet, workflow stages and every enabled decision are keyboard + reachable and have an object-specific accessible name. +- Moving between activation stages focuses the new stage heading and announces the stage. +- Escape closes dismissible sheets/dialogs and returns focus to the invoking control. +- The session-expiry gate uses the shared Sheet stack, keeps the background inert, locks body scroll, + traps focus and resists Escape/backdrop dismissal until `Sign in again`. +- Persistent offline status is non-modal; focus can leave it. Mutation controls remain focusable with + `aria-disabled`, a visible reason and no state change. Read-only inspection remains available. +- Phone full-screen protected stages and desktop dialogs retain a visible decision, close path where + permitted and unobscured validation/action region. + +## 4. Responsive continuity + +The frozen shell mapping is 320–430 compact, 768 rail, 1024 split-or-safe-fallback and 1440 wide. +The same information and action sequence is retained; content is not removed to make a width pass. +Today remains action-first, patient identity stays in the activation flow, Schedule retains its +seven-day strip and day detail, and the continuity ordered list remains the semantic source of truth. + +At compact widths the four-item dock owns the bottom edge. In-flow screens retain enough final +clearance; full-screen Sheets own their safe-area action region. At desktop widths the delivery +exception is a constrained right contextual rail and confirmation decisions remain bounded. + +## 5. Known acceptance gaps + +Before production, run real 400% browser zoom with text-only enlargement, Windows High Contrast with +representative assistive technology, VoiceOver on physical iPhone Safari, keyboard appearance and +rotation, and installed-PWA launch/offline/focus journeys. Those checks must use an approved +production implementation and synthetic or authorised test data. The current prototype evidence +does not establish WCAG conformance, clinical accessibility approval or device support. diff --git a/docs/caring-contacts/clinical-boundaries.md b/docs/caring-contacts/clinical-boundaries.md new file mode 100644 index 0000000000..9dec780212 --- /dev/null +++ b/docs/caring-contacts/clinical-boundaries.md @@ -0,0 +1,137 @@ +# Caring contacts — clinical and content boundaries + +**Status:** binding language contract for synthetic design, 15 August 2026 +**Limit:** records settled product language; it is not clinical approval. + +## 1. Core boundary + +A caring contact is a brief, governed, one-way message supplemental to usual care and active +person-to-person follow-up. It is not monitoring, crisis response, triage, a safety check, clinical +advice, treatment or evidence that clinical follow-up occurred. The workspace does not assess, +predict or represent suicide risk, urgency, wellbeing, engagement or response. + +## 2. Exact distinctions + +### Transport is not patient state + +`Scheduled`, `Processing`, `Sent`, `Delivered`, `Not delivered`, `Number invalid`, `Contact changed`, +`Status unavailable` and `Missed` describe systems and events only. Delivered does not prove that a +patient saw, read or understood a message and never implies safety, wellbeing, engagement or benefit. +A failure does not imply deterioration or increased risk. It creates named operational work only. + +Never relabel transport as `Reached`, `Engaged`, `Responded`, `Safe`, `Concern` or `Clinical +follow-up`. A permanent failure pauses future contacts and creates a same-day operational task; it +does not automatically trigger patient contact or clinical review. + +### Pending referral is not accepted ownership + +Until explicit acceptance, the referring team retains responsibility. `Received`, `Awaiting review` +and `Clarification requested` show `Awaiting handover` and the referring team, not an aftercare owner. +After acceptance, coordinator claim/assignment identifies coordination; it does not erase the +handover history or independently transfer duty of care. + +### Objective eligibility is not inferred risk + +Eligibility uses adult status, qualifying discharge/referral, pilot-service scope, +patient-controlled/suitable-for-SMS evidence and `Agreement confirmed: Yes`. Diagnosis, +presentation, risk assessments, notes, delivery or non-response never drive automated eligibility or +priority. Order referrals by discharge and first eligible window. Do not use `high risk`, `risk +score`, `priority patient`, `best match` or unqualified `needs attention`; name the exact condition, +remedy and owner. + +### Imported evidence is not overstated verification + +The label is `Agreement confirmed: Yes/No`, not legal or treatment consent. Callback imports the +current hospital-record mobile without a test SMS, read-back or separate attestation, so do not label +it as separately destination-verified. Activation separately requires the source flag that the destination is +patient-controlled and suitable for discreet SMS; family, carer and shared destinations are +ineligible. + +## 3. Sender, one-way and support rules + +- Keep contact roles distinct: the Rowan patient mobile, staffed programme line and crisis-support + contact are separate fictional numbers. A patient mobile is never displayed as the programme or + crisis contact. +- Use a non-receiving sender with a discreet, recognisable, neutral team identity; do not expose + suicide, crisis or mental-health treatment on a lock screen. +- Sign every message with the named coordinator under governed substitution rules. +- Do not receive, store, analyse or display replies. No inbox, conversation, urgency detection or + triage route exists. +- Enrolment and the first SMS state the no-reply boundary, programme phone and staffed hours, + emergency direction and one locally approved crisis-support contact. +- Later messages retain the short no-reply boundary and programme contact. +- Patients request timing changes, pause or withdrawal through the programme phone, staffed seven + days during every sending window; any authorised team member may act immediately. + +The design fixes these required meanings, not final sentences. Exact sender label, phone, hours, +emergency wording, crisis contact and message text require a versioned locally approved set with +clinical programme lead and lived-experience/content approval. Synthetic prototypes demonstrate +structure using clearly fictional details. Never invite a reply with wording such as `Reply if`, +`Text us`, `Tell us`, `Let us know` or `We monitor this number`. + +## 4. Governed message rules + +Every message is warm, brief, non-demanding and discreet. It may substitute preferred name, neutral +team identity, coordinator signature and an approved variant only. There is no unrestricted free +text, generative authoring or dynamic translation; translated pathways need professional translation +and cultural approval. + +Every message: + +- makes no claim about current patient state or clinical effect; +- requests no task, appointment response or disclosure by reply; +- includes its required one-way/support content; +- shows the exact fully substituted patient-visible text before activation; and +- is limited to two concatenated SMS segments including notices and signature, with encoding and + exact count shown; overflow blocks activation. + +The first message includes discreet team identity, bounded caring-contact purpose, coordinator +signature, no-reply statement, programme phone/hours, emergency direction and one approved crisis +support contact. Later messages retain the short no-reply statement and programme contact. Final +activation shows the exact first message, not a generic summary. + +## 5. Clinical/operational action terms + +- **Pause:** reversible; preserves the original calendar; contacts within the pause are skipped; + resumption begins with the next future contact. +- **Withdrawal:** patient preference; immediately cancels unsent contacts; no approval; reason + optional; terminal with immutable history. +- **Cancellation:** distinct authorised operational action with a reason. +- **Readmission:** source event that pauses future contacts; later discharge needs a new referral. +- **Recorded death:** source event that irreversibly cancels unsent contacts; a correction is an + incident and any future plan needs a new referral. +- **Mobile change:** source event that pauses future contacts for coordinator review; never silently + switch destination. + +None is inferred from transport, reply or engagement data. + +## 6. Claims and reporting + +The illustrative twelve-month cadence is locally governed, not a universal prescription. Until +approved, label it `Illustrative locally governed pathway`, never `Recommended`, `Best practice` or +`Proven`. The pilot tests operational safety, reliability, clinician usability and separately +consented patient acceptability; it is not a clinical-effectiveness study. + +Each plan has one selected sending preference. All 10 planned contacts derive the same service +window; only different patients may contribute to different window aggregates on Schedule. A +one-contact exception is explicit, reasoned and audited and does not silently replace the plan +preference. + +Reporting may cover operational counts, completeness, due/dispatched/delayed contacts, exact +transport exceptions, resolution time, pauses/withdrawals/cancellations, duplicates, schedule drift +and versions. Approved demographics require small-cell suppression. Never rank clinicians or infer +patient safety, suicide risk, wellbeing, engagement, therapeutic response or effectiveness. + +## 7. Review reject list + +Reject any copy or visual that implies delivered means safe/read/helped; failure means deterioration; +pending means accepted ownership; coordinator assignment transfers duty of care; imported mobile is +independently verified; agreement is legal/treatment consent; the cadence is universally effective; +or that replies are received or monitored. Reject real patient information or PHI in +URLs/toasts/logs/analytics/page titles/screenshots, free/generated message text, and any +reply/inbox/conversation surface. Prototype and test screenshots are still required to contain +clearly fictional synthetic identities and details so boundary states can be reviewed without using +real data. + +Final patient-facing content and the complete prototype require separate lived-experience and +clinical-language approvals; either may block progression. diff --git a/docs/caring-contacts/clinical-language-review.md b/docs/caring-contacts/clinical-language-review.md new file mode 100644 index 0000000000..5e39a4fadb --- /dev/null +++ b/docs/caring-contacts/clinical-language-review.md @@ -0,0 +1,238 @@ +# Caring contacts — clinical-language review + +**Review date:** 15 August 2026 +**Scope:** complete synthetic suite under `src/components/caring-contacts/mockups/**` +**Outcome:** design-language review complete; clinical and lived-experience approval remain required + +This review checks whether the prototype stays inside the boundaries in +[clinical-boundaries.md](clinical-boundaries.md). It is a source-and-rendered-prototype review, not +clinical approval, WA Health endorsement or evidence of effectiveness. + +## 1. Screen and state-family coverage + +| Screen or state family | Risky language reviewed | Exact outcome | +| --------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Shell and foundation board | monitoring, delivery certainty, ownership, agreement | Pass — the persistent marker says synthetic; transport and agreement definitions state their limits; no search, inbox or reply route | +| Today | risk inference, priority, ownership | Pass — ordered by discharge/first window; `Awaiting handover` keeps Fictional Ward A responsible; `Needs action` names the condition, remedy and owner | +| Patient and agreement | coercion, consent, mobile certainty, ownership | Pass — `Agreement confirmed: Yes` is explicitly not legal/treatment consent; imported mobile provenance and accepted team/coordinator are separate assurances | +| Pathway selection | effectiveness, ranking, reply implication | Pass — `Illustrative locally governed pathway`; no best/recommended claim; one-way boundary is explicit | +| Personalisation and exact-message preview | crisis-service implication, replies, unrestricted content | Pass — governed substitutions only; exact two-segment evidence; no free text; no-reply and support boundaries visible | +| Review and activation | certainty, coercion, fresh authentication | Pass for design — the action records synthetic review only; fresh authentication and atomic activation are explicitly future contracts; no production mutation | +| Patient overview and chronology | monitoring, engagement, delivery certainty | Pass — chronology is operational; `Delivered` remains transport receipt and never becomes read/helped/safe | +| Schedule and contact detail | safety inference, missed-send behaviour, timing | Pass — geometry is elapsed schedule only; AWST windows are exact; missed/paused contacts do not catch up; Processing cannot be changed | +| Patient search, duplicate and episode boundaries | diagnostic eligibility, ownership, death/readmission | Pass — search is pilot-team only; duplicate active plan is blocked/linked; readmission pauses; recorded death is irreversible; correction is an incident | +| Pause, withdrawal, cancellation and reassignment | coercion, duty of care, protected action | Pass — actions are distinct; withdrawal is patient-requested; reassignment changes coordination only; fresh authentication is named | +| Delivery exception | risk inference, delivery certainty, automatic follow-up | Pass — three bounded attempts, same-day operational task and plan pause; `No automatic clinical follow-up`; no risk or wellbeing inference | +| Templates and version states | clinical effectiveness, approval certainty | Pass — current, pending and retired versions retain lifecycle and two-person approval evidence; approval pending cannot masquerade as current | +| Team and escalation | transferred ownership, PHI in alerts | Pass — assignment is coordination, not duty-of-care transfer; 60-minute alert is `Accepted referral remains unclaimed. Open Callback.` | +| Guidance and help | monitoring, crisis response, reply | Pass — explicitly supplemental, one-way and non-monitoring; incidents pause the pilot; no inbox or response route | +| Reports and suppression | effectiveness, clinician ranking, inference | Pass — operational aggregates only; no effectiveness claim or ranking; small cells show `Suppressed`, never zero | +| All 24 overlays | reply, certainty, ownership, protected decisions | Pass — every overlay names availability and decision; identity/activation/withdrawal/conflict use protected compact stages; session/offline fail closed | +| Loading, empty, error, offline, authentication, permission and conflict specimens | stale certainty, implicit success, unsafe mutation | Pass — explicit state words and remedies; offline data is not cached; errors do not become empty results; mutations remain unavailable | + +## 2. Required phrase-category findings + +| Category | Reviewed source/rendered phrase | Disposition | +| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| Monitoring | `supplements usual care and person-to-person follow-up`; `Replies are not received, stored, analysed or monitored` | Accepted. No patient monitoring, surveillance or clinical-observation claim found. | +| Safety | `does not indicate patient safety, receipt, wellbeing or response` | Accepted. Safety appears only in explicit non-inference boundaries and operational-pilot language. | +| Risk | `never by inferred patient state`; `creates no risk score, clinical priority, wellbeing inference` | Accepted. No risk score, risk-ranked queue or automated escalation from transport found. | +| Reply/response | `Replies are not received, stored, analysed or monitored`; `No automatic clinical follow-up` | Accepted. No reply invitation, Inbox, Messages, Conversation or urgency detection found. | +| Delivery certainty | `Delivered means transport receipt only`; `does not show that the message was read or helped` | Accepted. `Sent`, `Delivered`, failure and uncertainty remain transport states. | +| Ownership | `Fictional Ward A remains responsible until explicit acceptance`; assignment `does not ... transfer duty of care` | Accepted. Pending and coordinator states do not overstate handover. | +| Crisis-service implication | emergency direction and one approved crisis contact are message-content requirements; caring contacts are not crisis response | Accepted with authority gate. Exact wording and service remain unapproved and fictional in the prototype. | +| Coercion/agreement | `Agreement confirmed: Yes`; `not legal or treatment consent`; withdrawal is patient-requested and immediate | Accepted. No mandatory benefit, compliance or penalty language found. | +| Diagnostic eligibility | objective source/referral/mobile/agreement checks; no diagnostic score or presentation-based priority | Accepted. The cohort context is not used as a UI ranking signal. | +| Transport versus clinical state | `This is a transport exception`; `No automatic clinical follow-up` | Accepted. Transport events never become clinical observations or patient-state labels. | +| Fresh authentication/offline | `Fresh authentication is required before mutation`; `Governed mutation is unavailable while offline` | Accepted as prototype representation. Live SSO/MFA, session freshness and connectivity enforcement are future contracts. | +| Deceased/correction | `Recorded death — irreversible cancellation`; `correction is an incident, not an undo or reinstatement` | Accepted. No reinstate action exists; any future plan needs a new referral and authorised path. | +| Reporting suppression | `Suppressed`, never zero; threshold not invented | Accepted. Production threshold and approved demographic set remain governance-owned. | + +## 3. Source safety scan + +The final source scan covers monitoring, safety, risk, replies, delivery, responsibility/ownership, +crisis, consent/agreement, diagnosis/eligibility, read/effectiveness, fresh authentication, offline, +death/correction and suppression across every mockup file. Findings were manually classified against +the tables above. Boundary words occur in negations, explanations and governed state labels; no +prohibited affordance or positive clinical inference was identified. + +## 4. Phrase-level traceability appendix + +The family findings above are summaries only. The following inventories are the phrase-level review +record. `Accepted for design` means the phrase accurately states the synthetic prototype boundary; +it is not clinical approval. + +### 4.1 Screens and boundary states + +| Screen/state | Source location | Exact risky phrase or accessible label | Category | Disposition and rationale | Remaining approval gate | +| ------------------------------ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| Shell marker | `fixtures.ts`; `caring-contact-shell-frame.tsx` | `Synthetic prototype — fictional data only` | privacy/certainty | Accepted for design — prevents the mockup being mistaken for a live record. | Product and privacy acceptance of production environment labelling. | +| Foundation board | `foundation-board.tsx` | `Transport receipt recorded. It does not show that the contact was read.` | delivery certainty | Accepted for design — restricts the event to transport evidence. | Provider-event and clinical-language approval. | +| Today — pending referral | `today-screen.tsx` | `Fictional Ward A remains responsible until explicit acceptance.` | ownership | Accepted for design — no premature handover or duty-of-care claim. | Service protocol and role-matrix approval. | +| Today — action queue | `today-screen.tsx` | `Needs action` | risk/priority | Accepted with qualification — only a section label; each row names its observable condition, remedy and owner. | Clinical safety and service-operations approval. | +| Patient and agreement | `patient-agreement-screen.tsx` | `Agreement confirmed: Yes · imported source record, not legal or treatment consent` | coercion/agreement | Accepted for design — does not overstate the recorded agreement classification. | Legal/privacy/records and programme approval. | +| Patient and agreement | `patient-agreement-screen.tsx` | `Imported mobile` | mobile certainty | Accepted for design — provenance is shown without calling the destination verified. | Source contract and identity/mobile assurance approval. | +| Pathway selection | `pathway-selection-screen.tsx` | `Illustrative locally governed pathway` | effectiveness/ranking | Accepted for design — no best, recommended or proven pathway claim. | Local clinical-programme and lived-experience approval. | +| Pathway selection | `pathway-selection-screen.tsx` | `Replies are not received, stored, analysed or monitored` | reply/monitoring | Accepted for design — exact closed four-verb notice; no reply affordance. | Exact patient-copy approval. | +| Personalisation | `personalisation-screen.tsx` | `Replies are not received, stored, analysed or monitored` | reply/monitoring | Accepted for design — canonical patient-visible notice in the exact substituted text. | Exact patient-copy and sender approval. | +| Personalisation | `personalisation-screen.tsx` | `In an emergency call 000.` | crisis implication | Accepted with gate — real emergency direction is intentional; Callback is not described as crisis response. | Clinical, lived-experience and emergency-copy approval. | +| Personalisation | `personalisation-screen.tsx` | `272 septets · 2 of 2 SMS segments` | delivery/content certainty | Accepted for design — derived from the final exact GSM-7 string; the limit remains two segments. | Provider encoding and exact-copy acceptance. | +| Personalisation | `personalisation-screen.tsx`; `types.ts` | `For timing changes call +61 491 570 157` / `Fictional Support Line: +61 491 570 158` | ownership/crisis/privacy | Accepted for synthetic design — staffed-programme and crisis-support roles are distinct and neither reuses a patient mobile. | Programme-line, crisis-support and exact-copy approval. | +| Review and activation | `review-activation-screen.tsx` | `Activate 10-contact plan` | activation certainty | Accepted for design — exact rendered action; the synthetic handler records review only and does not create a production plan or send. | Fresh-auth, atomic activation and audit acceptance. | +| Review and activation | `review-activation-screen.tsx` | `Caring contacts do not monitor safety, wellbeing or response and supplement usual care.` | monitoring/safety | Accepted for design — explicit non-monitoring and supplementary-care boundary. | Clinical and lived-experience approval. | +| Patient overview | `patient-overview-screen.tsx` | `it does not show that the message was read` | delivery certainty | Accepted for design — chronology remains an operational event history. | Provider-event and clinical-language approval. | +| Patient overview — continuity | `continuity-thread-specimen.tsx` | `Delivered means transport receipt only; it does not show that a message was read, helped or reflected the patient’s state. Every future contact remains Scheduled.` | transport versus clinical/monitoring | Accepted for design — each dated row has a word-based transport state and the only completed state is explicitly transport-only. | Provider-state, continuity and accessibility approval. | +| Schedule | `schedule-screen.tsx` | `No named exceptions for this day.` | empty state/certainty | Accepted for design — absence is explicit and is not rendered as zero or success. | Operations and accessibility acceptance. | +| Patient search | `patient-boundary-screens.tsx` | `Search is limited to the active pilot team’s referrals and Callback episodes.` | privacy/diagnostic eligibility | Accepted for design — denies global/diagnostic discovery. | Access-control, privacy and audit acceptance. | +| Duplicate referral | `patient-boundary-screens.tsx` | `Duplicate referral blocked` | ownership/eligibility | Accepted for design — blocks a second active plan and requires episode linkage. | Source identity and idempotency approval. | +| Readmission | `patient-boundary-screens.tsx` | `Readmission pause` | clinical boundary | Accepted for design — future contacts pause; a later discharge requires a new linked referral. | Source-event and service-protocol approval. | +| Recorded death | `patient-boundary-screens.tsx` | `Recorded death — irreversible cancellation` | deceased/correction | Accepted for design — unsent contacts cancel irreversibly. | High-assurance source-event, incident and audit approval. | +| Death correction | `patient-boundary-screens.tsx` | `A correction is an incident, not an undo or reinstatement.` | deceased/correction | Accepted for design — no casual reinstate path exists. | Incident governance and new-referral authority. | +| Withdrawal | `patient-boundary-screens.tsx` | `Fresh authentication is required before mutation.` | coercion/fresh authentication | Accepted for design — patient-requested terminal action is protected and has no undo. | SSO/MFA freshness and withdrawal-policy approval. | +| Reassignment | `patient-boundary-screens.tsx` | `Assignment identifies coordination; it does not independently transfer duty of care` | ownership | Accepted for design — coordinator assignment does not overstate clinical responsibility. | Role matrix, handover and service-protocol approval. | +| Contact detail — Delivered | `patient-boundary-screens.tsx` | `Delivered means transport receipt only.` | transport versus clinical | Accepted for design — not read, helped, safe or responded. | Provider-event contract approval. | +| Contact detail — Not delivered | `patient-boundary-screens.tsx` | `No automatic clinical follow-up or late resend.` | transport versus clinical | Accepted for design — transport exception creates an operational task only. | Retry, incident and source write-back approval. | +| Delivery exception | `delivery-exception-screens.tsx` | `This is a transport exception. It does not indicate patient safety, receipt, wellbeing or response.` | safety/delivery certainty | Accepted for design — prevents patient-state inference. | Clinical safety and provider-event approval. | +| Delivery exception | `delivery-exception-screens.tsx` | `No automatic clinical follow-up` | transport versus clinical | Accepted for design — no risk score, clinical priority or automated contact is created. | Clinical safety and incident-process approval. | +| Templates | `template-screens.tsx` | `Replies are not received, stored, analysed or monitored` | reply/monitoring | Accepted for design — exact canonical patient-visible boundary is frozen with the template. | Two-person exact-copy/version approval. | +| Team | `team-guidance-reporting-screens.tsx` | `Accepted referral remains unclaimed. Open Callback.` | ownership/transient privacy | Accepted for design — operational alert has no patient identity or clinical inference. | Service escalation and notification privacy approval. | +| Guidance | `team-guidance-reporting-screens.tsx` | `Caring contacts supplement usual care and person-to-person follow-up.` | monitoring/clinical responsibility | Accepted for design — does not replace follow-up or claim monitoring. | Clinical-programme and lived-experience approval. | +| Reports | `team-guidance-reporting-screens.tsx` | `Aggregates support safety, reliability and usability review. They do not rank clinicians or claim effectiveness.` | reporting/effectiveness | Accepted for design — operational aggregates only. | Data dictionary, analytics and governance approval. | +| Reports — small cell | `team-guidance-reporting-screens.tsx` | `Suppressed` | reporting suppression | Accepted for design — avoids inferable zero/small counts and invents no threshold. | Approved threshold and demographic-set decision. | + +#### 4.1.1 Patient-boundary, action and contact-state reconciliation + +This is the closed phrase-level reconciliation for the complete rendered inventory in +`patient-boundary-screens.tsx`, plus boundary states named only in the developer handoff. Rendered +coverage is **42/42**: search scope; search field; search result; search empty; duplicate referral; +readmission; recorded death; wrong recipient; contact changed; pause summary; withdrawal summary; +cancel summary; detail disclosure; active record; ownership; frozen versions; future schedule; +audit history; offline unavailable; offline transition; terminal record; terminal action; partial +source data; corrected-death governance; corrected-death unresolved; corrected-death recorded; +Scheduled; Processing; Sent; Delivered; Named exception; pause decision; pause result; withdrawal +decision; withdrawal result; cancellation decision; cancellation result; reassignment decision; +reassignment authenticated; reassignment result; correction decision; correction result. Handoff-only +coverage adds **5/5** separately reviewed entries: pending referral; later qualifying discharge; +duplicate send; unauthorised content; lost audit. + +| Screen/state | Exact source location | Exact risky phrase or accessible label | Category | Disposition and rationale | Remaining approval gate | +| -------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Search scope | `patient-boundary-screens.tsx` — boundary introduction | `Search is limited to the active pilot team’s referrals and Callback episodes. No shared Clinical KB search, recent-search or analytics path is used.` | privacy/diagnostic eligibility | Accepted for design — closes discovery to the active pilot team and explicitly denies shared search and analytics reuse. | Production access-control, tenant isolation, search logging and privacy approval. | +| Search field | `patient-boundary-screens.tsx` — search label | `Search name, fictional identifier or date of birth` | privacy/identity | Accepted for synthetic design — names identity fields and marks the identifier fictional in this mockup. | Production minimum-necessary search fields, matching and audit approval. | +| Search result | `patient-boundary-screens.tsx` — result status | `Identity assurance required` | identity/ownership | Accepted for design — a result is not treated as verified selection. | Identity-assurance workflow and access audit approval. | +| Search empty | `patient-boundary-screens.tsx` — empty result | `No pilot-team records found` / `Check the fictional details or return to the referring service; do not broaden into global search.` | privacy/empty state | Accepted for design — absence is bounded to team records and the remedy does not broaden disclosure. | Production query-scope, failure-state and privacy approval. | +| Duplicate referral | `patient-boundary-screens.tsx` — `boundaryRecords` | `Duplicate referral blocked` / `Link SYN-REFERRAL-003 to the active episode; a second active plan cannot be created.` / `Link referral to active episode` | ownership/diagnostic eligibility | Accepted for design — blocks duplicate active-plan creation and preserves episode linkage. | Source identity, idempotency and active-episode uniqueness approval. | +| Readmission | `patient-boundary-screens.tsx` — `boundaryRecords` | `Readmission pause` / `Future contacts pause from the source event. A later discharge needs a new linked referral.` / `Paused by source event` | safety/ownership | Accepted for design — pauses future contacts and does not silently resume from a later event. | Assessed source-event integration and service-protocol approval. | +| Recorded death | `patient-boundary-screens.tsx` — `boundaryRecords` | `Recorded death — irreversible cancellation` / `All unsent contacts are cancelled. Correction is an incident; any future plan needs a new referral.` / `Cancelled irreversibly` | deceased/correction | Accepted for design — cancellation is terminal and correction cannot restore the plan. | High-assurance source event, atomic cancellation, incident and audit approval. | +| Wrong-recipient incident | `patient-boundary-screens.tsx` — `boundaryRecords` | `Wrong-recipient incident` / `Pause the entire pilot, preserve evidence and follow the joint incident restart authority.` / `Pilot stop` | privacy/safety/ownership | Accepted for design — declares a programme-wide stop, evidence preservation and joint restart authority without clinical inference. | Privacy/security incident, clinical programme and executive restart approval. | +| Contact changed | `patient-boundary-screens.tsx` — `boundaryRecords` | `Contact changed` / `Pause future contacts for coordinator review; never silently switch the destination.` / `Review required` | privacy/mobile certainty | Accepted for design — fails closed and prohibits silent destination replacement. | Source correction, identity/mobile assurance and coordinator-authority approval. | +| Pause summary | `patient-boundary-screens.tsx` — boundary action summary | `Pause plan` / `Reversible. Preserve the calendar, permanently skip contacts inside the pause and resume at the next future contact.` | timing/ownership | Accepted for design — distinguishes reversible plan state from permanently skipped contacts and no catch-up. | Pause/resumption authority, write-back and audit approval. | +| Withdrawal summary | `patient-boundary-screens.tsx` — boundary action summary | `Record withdrawal` / `Patient preference. Immediately cancel unsent contacts; no approval is required and history is immutable.` | coercion/ownership | Accepted for design — preserves patient agency and terminal history without an approval barrier. | Patient-request evidence, records and fresh-auth policy approval. | +| Cancellation summary | `patient-boundary-screens.tsx` — boundary action summary | `Cancel plan` / `A distinct authorised operational action with a recorded reason and audit event.` | ownership/audit certainty | Accepted for design — does not conflate cancellation with patient withdrawal. | Role, reason taxonomy, fresh-auth and audit approval. | +| Detail disclosure | `patient-boundary-screens.tsx` — boundary action summary | `Plan and contact detail` / `Open the complete plan, terminal, partial-data, corrected-death, reassignment and contact-state compositions.` | privacy/state certainty | Accepted for synthetic design — explicitly names the state specimen coverage rather than implying a live chart. | Production route permission and minimum-necessary disclosure approval. | +| Active record | `patient-boundary-screens.tsx` — `PlanAndContactDetail` heading | `Active plan and contact record` / `Synthetic plan SYN-EPISODE-001 · complete identity, governance, schedule and audit context.` | privacy/status certainty | Accepted for synthetic design — the record and identifier are explicitly synthetic. | Production episode identity and status contract approval. | +| Ownership and accepted handover | `patient-boundary-screens.tsx` — `Plan identity and ownership` definitions | `Owning team` / `Coordinator` / `Accepted handover` / `15 August 2026 at 9:35 am AWST · referring-team history retained` | ownership | Accepted for design — coordination, team ownership and accepted handover are separately visible. | Role matrix, referral acceptance and duty-of-care protocol approval. | +| Frozen versions and sender | `patient-boundary-screens.tsx` — `Frozen governed versions` definitions | `Example first contact · SYN-copy-v1.0 · frozen snapshot` / `Example Aftercare Team · non-receiving sender` | reply/approval certainty | Accepted for design — freezes the copy version and describes the sender as non-receiving. | Sender registration, template versioning and two-person approval. | +| Future schedule | `patient-boundary-screens.tsx` — `Future schedule` | `Original discharge-anchored calendar; pauses skip without catch-up.` | timing/cadence certainty | Accepted for design — no rebase or catch-up promise. | Cadence engine, public-holiday and scheduling approval. | +| Audit history | `patient-boundary-screens.tsx` — `Audit history` | `9:35 am · Taylor Fiction accepted referral · 9:42 am · Alex Example claimed coordination · frozen versions recorded.` | ownership/audit certainty | Accepted for synthetic design — separates acceptance from coordination and identifies immutable version evidence. | Actor identity, event schema, retention and audit approval. | +| Offline unavailable | `patient-boundary-screens.tsx` — `planOfflineReason` | `Unavailable while offline — reconnect before changing this plan.` | offline/safety | Accepted for design — mutations fail closed with a named remedy. | Connectivity detection, cache/security and retry approval. | +| Offline transition | `patient-boundary-screens.tsx` — `planOfflineTransitionNotice` | `Connectivity was lost. The open plan decision closed without recording a change. Reconnect to continue.` | offline/audit certainty | Accepted for design — explicitly says no change was recorded and closes the decision surface. | Transaction-boundary, reconnect and audit approval. | +| Terminal record | `patient-boundary-screens.tsx` — terminal specimen | `Terminal plan — read only` / `Withdrawn 4 September 2026 at 11:12 am AWST · unsent contacts cancelled · immutable history retained.` | coercion/status certainty | Accepted for design — terminal state, cancellation and retained history are explicit. | Withdrawal event, immutable records and access approval. | +| Terminal action | `patient-boundary-screens.tsx` — terminal specimen action | `Edit terminal plan unavailable` / `Terminal plans are read only` | permission/status certainty | Accepted for design — no implied edit or reinstatement path. | Production permission and accessible-unavailable-state approval. | +| Partial source data | `patient-boundary-screens.tsx` — partial-data specimen | `Partial source data` / `Patient-controlled and suitable-for-discreet-SMS evidence: Not supplied. Activation remains unavailable.` / `Remedy: referring team supplies the source flag; Callback does not infer it.` | diagnostic eligibility/coercion | Accepted for design — missing eligibility evidence blocks activation and is never inferred. | Source field, referring-team correction and activation-gate approval. | +| Corrected-death governance | `patient-boundary-screens.tsx` — corrected-death specimen | `Recorded death irreversibly cancelled unsent contacts. A correction is an incident, not an undo or reinstatement.` | deceased/correction | Accepted for design — preserves the original terminal consequence. | Incident authority, source reconciliation and new-referral approval. | +| Corrected-death unresolved | `patient-boundary-screens.tsx` — corrected-death current state | `No correction incident recorded. Existing plan remains cancelled and read only.` | deceased/correction/status certainty | Accepted for design — absence of an incident record does not reopen the plan. | Incident-state and audit contract approval. | +| Corrected-death recorded | `patient-boundary-screens.tsx` — corrected-death result state | `Correction incident recorded in audit. Existing plan remains cancelled; any future plan requires a new referral and approved restart path.` | deceased/correction/ownership | Accepted for design — recording the correction still cannot reinstate the plan. | Joint incident closure and new-referral/restart authority. | +| Scheduled contact | `patient-boundary-screens.tsx` — `contactDetailStates.Scheduled` | `Saturday 22 August 2026 at 10:00 am AWST. A coordinator may move it only within this scheduled day.` / `No transport attempt exists. Date changes require a reason and team-lead approval.` | timing/delivery certainty/ownership | Accepted for design — no send is implied and movement stays inside the governed day. | Scheduling, role and reason/audit contract approval. | +| Processing/too late | `patient-boundary-screens.tsx` — `contactDetailStates.Processing` | `Provider-neutral processing began at 2:00:03 pm AWST. It is too late to change or cancel this contact.` / `Await a signed transport event. Do not infer receipt, safety, wellbeing or response.` | transport versus clinical/safety | Accepted for design — locks mutation after processing and prohibits patient-state inference. | Provider state machine, signed-event and timeout approval. | +| Sent | `patient-boundary-screens.tsx` — `contactDetailStates.Sent` | `Accepted for transport at 2:00:08 pm AWST. Sent is not a delivery receipt.` / `No resend occurs while status is uncertain. Retain event and webhook evidence.` | delivery certainty/safety | Accepted for design — transport acceptance is not receipt and uncertain resend fails closed. | Provider event, idempotency, webhook and retry approval. | +| Delivered/transport only | `patient-boundary-screens.tsx` — `contactDetailStates.Delivered` | `Transport receipt recorded at 2:00:14 pm AWST. Delivered means transport receipt only.` / `It does not show that the message was read, helped, or reflected the patient’s state.` | transport versus clinical/monitoring | Accepted for design — explicitly denies read, benefit and patient-state claims. | Provider receipt semantics and clinical-language approval. | +| Named exception | `patient-boundary-screens.tsx` — `contactDetailStates["Named exception"]` | `Not delivered after three attempts in the original window. Future contacts are paused.` / `Create a same-day operational task. No automatic clinical follow-up or late resend.` | delivery exception/safety/ownership | Accepted for design — creates operational work only, pauses the sequence and prohibits late resend or clinical automation. | Retry limit, task ownership, incident and write-back approval. | +| Pause decision | `patient-boundary-screens.tsx` — pause `ConfirmDialog` | `Pause caring-contact plan` / `Pause is reversible, but the original discharge-anchored calendar is never rebased. Contacts inside the pause are permanently skipped.` / `Pause future contacts` | timing/ownership | Accepted for design — exact consequence is visible before confirmation. | Pause authority, persistence and audit approval. | +| Pause result | `patient-boundary-screens.tsx` — pause action notice | `Pause decision recorded for this synthetic review; contacts inside the pause are skipped.` | status certainty | Accepted for synthetic design — confines success to the prototype review and repeats the skipped-contact effect. | Production transaction and outcome-notification approval. | +| Withdrawal decision | `patient-boundary-screens.tsx` — withdrawal `Sheet`/`ConfirmDialog` | `Record patient-requested withdrawal` / `Withdrawal immediately cancels every unsent contact and has no undo.` / `Fresh authentication is required before mutation.` | coercion/fresh authentication | Accepted for design — patient-request origin, terminal consequence and protection are explicit. | Patient-request evidence, SSO/MFA freshness and audit approval. | +| Withdrawal result | `patient-boundary-screens.tsx` — withdrawal action notice | `Fresh authentication requested; no withdrawal mutation was recorded by this prototype.` | fresh authentication/status certainty | Accepted for synthetic design — requesting authentication is not presented as completed withdrawal. | Production authentication return flow and atomic mutation approval. | +| Cancellation decision | `patient-boundary-screens.tsx` — cancel `ConfirmDialog` | `Cancel caring-contact plan` / `Cancel every future contact for the recorded authorised reason and preserve the complete immutable audit history.` / `Cancel future contacts` | ownership/audit certainty | Accepted for design — states scope, authority evidence and retained history before confirmation. | Cancellation roles, reason taxonomy, fresh auth and transaction approval. | +| Cancellation result | `patient-boundary-screens.tsx` — cancellation action notice | `Authorised cancellation decision recorded for this synthetic review.` | ownership/status certainty | Accepted for synthetic design — does not claim a production mutation. | Production authorisation, transaction and outcome-notification approval. | +| Reassignment decision | `patient-boundary-screens.tsx` — reassignment `Sheet` | `Reassign active plan` / `Change coordination ownership while retaining handover and audit history.` / `Assignment identifies coordination; it does not independently transfer duty of care` | ownership/clinical responsibility | Accepted for design — separates coordination assignment from duty of care. | Role matrix, handover, fresh-auth and service-protocol approval. | +| Reassignment authenticated | `patient-boundary-screens.tsx` — reassignment protected state | `Fresh authentication confirmed for this synthetic decision. Recheck the new coordinator before confirming.` | fresh authentication/identity | Accepted for synthetic design — authentication alone does not complete reassignment and prompts a final identity check. | SSO/MFA assurance, coordinator identity and confirmation approval. | +| Reassignment result | `patient-boundary-screens.tsx` — reassignment action notice | `Reassignment recorded after fresh authentication in this synthetic workflow.` | ownership/status certainty | Accepted for synthetic design — the outcome is expressly synthetic. | Production atomic mutation, handover/write-back and audit approval. | +| Correction decision | `patient-boundary-screens.tsx` — correction `Sheet` | `Recorded-death correction governance` / `A source correction is an incident and cannot reinstate the cancelled plan.` / `Record correction incident` | deceased/correction | Accepted for design — the action records an incident, not an undo. | Incident roles, source reconciliation and new-referral authority. | +| Correction result | `patient-boundary-screens.tsx` — correction guidance | `Any future caring-contact plan needs a new referral. There is no reinstate or undo control.` | deceased/correction/ownership | Accepted for design — keeps a new referral as the only future-plan path. | Joint incident closure and approved restart process. | +| Pending referral (handoff-only) | `design-handoff.md` §2 — `Pending referral` | `Accept, Return for clarification, Decline; referring-team responsibility visible` | ownership | Accepted for handoff — named outcomes preserve referring-team responsibility until acceptance. | Source/write-back contract and authorised-clinician approval. | +| Later qualifying discharge (handoff-only) | `design-handoff.md` §2 — `Later qualifying discharge` | `new linked episode only after earlier closure` | diagnostic eligibility/ownership | Accepted for handoff — a later discharge does not reopen or overwrite the earlier episode. | Source-event identity and linkage approval. | +| Duplicate send incident (handoff-only) | `design-handoff.md` §2 — combined incident row | `Wrong recipient/duplicate send/unauthorised content/lost audit` / `pause entire pilot` | delivery exception/safety | Accepted for handoff — duplicate send is an explicit pilot-stop incident, not an ordinary delivery state. | Joint incident, privacy/security and clinical restart authority. | +| Unauthorised content incident (handoff-only) | `design-handoff.md` §2 — combined incident row | `Wrong recipient/duplicate send/unauthorised content/lost audit` / `pause entire pilot` | privacy/safety/approval certainty | Accepted for handoff — unapproved content triggers programme-wide containment. | Joint incident, template governance, privacy/security and clinical restart authority. | +| Lost audit incident (handoff-only) | `design-handoff.md` §2 — combined incident row | `Wrong recipient/duplicate send/unauthorised content/lost audit` / `pause entire pilot` | audit certainty/safety | Accepted for handoff — missing audit evidence is not treated as a recoverable cosmetic error. | Joint incident, records/security and clinical restart authority. | + +The handoff boundary inventory is also reconciled **12/12**: Pending referral → handoff-only row; +Duplicate active referral → Duplicate referral; Later qualifying discharge → handoff-only row; +Readmission → Readmission; Recorded death → Recorded death; Death correction → Corrected-death +governance/decision/result; Mobile/contact changed → Contact changed; Wrong recipient/duplicate +send/unauthorised content/lost audit → Wrong-recipient plus the three handoff-only incident rows; +Pause → Pause summary/decision/result; Withdrawal → Withdrawal summary/decision/result; +Cancellation → Cancellation summary/decision/result; Reassignment → Reassignment +decision/authenticated/result. This mapping, the 42/42 rendered list and the 5/5 handoff-only list are +the completeness gate for future review: adding a source state or handoff state requires a named row +and an updated count. + +### 4.2 Component and system states + +| State | Source location | Exact phrase or accessible label | Category | Disposition and rationale | Remaining approval gate | +| ----------------------- | ------------------------------- | --------------------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------- | +| Default | `component-state-specimens.tsx` | `Review details` | accessible action | Accepted for design — verb and object are explicit. | Accessibility acceptance in production component. | +| Hover | `component-state-specimens.tsx` | `Hover specimen` | interaction state | Accepted for design — visible words remain independent of hover colour. | Pointer/contrast acceptance. | +| Active | `component-state-specimens.tsx` | `Active specimen` | interaction state | Accepted for design — label remains visible in the pressed visual treatment. | Contrast and interaction acceptance. | +| Focus visible | `component-state-specimens.tsx` | `Focus-visible specimen` | focus state | Accepted for design — named control retains a non-colour outline. | Keyboard and assistive-technology acceptance. | +| Unavailable | `component-state-specimens.tsx` | `Approval evidence missing` | permission/approval certainty | Accepted for design — unavailable reason is explicit and focusable. | Role/approval contract and accessibility acceptance. | +| Busy | `component-state-specimens.tsx` | `Recording review…` | transient certainty | Accepted for design — describes in-progress state, not completion. | Async outcome and live-region acceptance. | +| Invalid | `component-state-specimens.tsx` | `Enter a structured operational reason.` | validation/coercion | Accepted for design — requests operational evidence without clinical inference. | Form/content and accessibility acceptance. | +| Empty | `component-state-specimens.tsx` | `No named exceptions for this day.` | empty state | Accepted for design — absence is stated, not inferred from blank content. | Operations and accessibility acceptance. | +| Long content | `component-state-specimens.tsx` | `full value wraps and remains available without clipping.` | privacy/accessibility | Accepted for design — synthetic value demonstrates non-truncation. | Responsive and assistive-technology acceptance. | +| Compact | `component-state-specimens.tsx` | `10:00 am AWST` | time certainty | Accepted for design — explicit selected-plan service timezone. | Scheduling contract approval. | +| Dark | `component-state-specimens.tsx` | `Raised surfaces use the dark luminance ladder.` | non-colour status | Accepted for design — documents repository token use only. | Full dark-theme contrast acceptance. | +| Forced colour | `component-state-specimens.tsx` | `System colour plus visible state words.` | non-colour status | Accepted for design — wording survives colour override. | Windows High Contrast/AT acceptance. | +| Reduced motion | `component-state-specimens.tsx` | `Transitions stop; chronology remains unchanged.` | motion/certainty | Accepted for design — motion is decorative and state order is stable. | Reduced-motion browser/device acceptance. | +| Loading | `component-state-specimens.tsx` | `Loading operational records…` | stale certainty | Accepted for design — loading is not rendered as empty or complete. | Async/live-region acceptance. | +| Error | `component-state-specimens.tsx` | `Records could not be loaded. Retry or return to Today.` | stale certainty | Accepted for design — failure has a remedy and does not invent data. | Error/retry contract approval. | +| Offline | `component-state-specimens.tsx` | `Offline — patient data is not cached; activation and sending fail closed.` | offline/privacy | Accepted for design — no offline PHI or mutation promise. | Connectivity, cache and security acceptance. | +| Authentication required | `component-state-specimens.tsx` | `Session expired. Sign in again before continuing.` | fresh authentication | Accepted for design — protected work cannot continue on stale auth. | WA Health SSO/MFA acceptance. | +| Permission unavailable | `component-state-specimens.tsx` | `Your role cannot perform this action. Contact the team lead.` | permission/ownership | Accepted for design — denies by role and names the remedy. | Production RBAC and audit acceptance. | +| Version conflict | `component-state-specimens.tsx` | `A governed version changed. Review both versions before resolving.` | conflict/certainty | Accepted for design — no silent overwrite or implicit success. | Versioning/conflict-resolution acceptance. | + +### 4.3 Complete 24-overlay traceability + +| Overlay | Source location | Exact risky phrase or accessible label | Category | Disposition and rationale | Remaining approval gate | +| ---------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| 1. Verify identity | `overlay-specimens.tsx` | `Verify identity before changing patient` | identity/privacy | Accepted for design — selected synthetic identity remains visible until confirmation. | Identity matching, access and audit approval. | +| 2. Change patient | `overlay-specimens.tsx` | `No patient context carries into the replacement search.` | privacy | Accepted for design — requires context clearing. | Search isolation and privacy acceptance. | +| 3. Pathway preview | `overlay-specimens.tsx` | `without ranking it as best or recommended` | effectiveness/ranking | Accepted for design — choice is governed, not algorithmically preferred. | Clinical-programme pathway approval. | +| 4. Message preview | `overlay-specimens.tsx` | `GSM-7 · 272 septets · 2 of 2 SMS segments · Replies are not received, stored, analysed or monitored.` | reply/delivery content | Accepted for design — exact canonical notice and derived segment evidence. | Exact-copy/provider encoding approval. | +| 5. Communication preference | `overlay-specimens.tsx` | `Current plan preference: Morning 10:00 am AWST. Proposed change: Early evening 5:00 pm AWST.` | coercion/preference | Accepted for design — current and proposed values are distinct; the specimen does not silently mutate the plan. | Preference-recording, future-contact update and service-process approval. | +| 6. Adjust date/time | `overlay-specimens.tsx` | `Proposed one-contact exception: move contact 2 from 10:00 am to 2:00 pm AWST on Saturday 22 August 2026. The plan preference remains Morning` | timing/ownership | Accepted for design — exception is explicit and does not rotate or replace the plan preference. | Scheduling, exception and authorisation approval. | +| 7. Outside-window warning | `overlay-specimens.tsx` | `The requested time is outside 9:00 am–6:00 pm AWST and cannot be scheduled.` | timing/safety | Accepted for design — blocked state names the governed window. | Service-hours and public-holiday approval. | +| 8. Save draft | `overlay-specimens.tsx` | `without sending or activating anything` | activation certainty | Accepted for design — draft does not imply activation. | Draft persistence/privacy approval. | +| 9. Discard changes | `overlay-specimens.tsx` | `Existing plan and audit history are not changed.` | audit certainty | Accepted for design — discard is session-bounded. | Audit and versioning approval. | +| 10. Final activation | `overlay-specimens.tsx` | `Fresh authentication and atomic activation are future production contracts; this synthetic action records review only.` | fresh authentication/activation certainty | Accepted for design — no production mutation claim. | SSO/MFA, transaction and audit approval. | +| 11. Activation success | `overlay-specimens.tsx` | `No message has been sent by this prototype.` | delivery certainty/transient privacy | Accepted for design — outcome is operational and identity-free. | Production outcome and send-state approval. | +| 12. Pause | `overlay-specimens.tsx` | `Contacts inside the pause are permanently skipped.` | schedule certainty | Accepted for design — no late catch-up promise. | Pause/resumption policy approval. | +| 13. Withdrawal | `overlay-specimens.tsx` | `Fresh authentication is required before mutation; there is no undo action.` | coercion/fresh authentication | Accepted for design — terminal patient-requested state is protected. | Withdrawal and SSO/MFA policy approval. | +| 14. Reassignment | `overlay-specimens.tsx` | `duty-of-care claims are not inferred from assignment` | ownership | Accepted for design — coordination change is not clinical handover. | Role matrix and handover approval. | +| 15. Delivery detail | `overlay-specimens.tsx` | `Delivered at 10:00:14 am AWST means transport receipt only.` | transport versus clinical | Accepted for design — no read/helped inference. | Provider event-contract approval. | +| 16. Resolve failed delivery | `overlay-specimens.tsx` | `do not create automatic clinical follow-up` | risk/transport versus clinical | Accepted for design — only operational resolution is offered. | Clinical safety, retry and incident approval. | +| 17. Contact-changed block | `overlay-specimens.tsx` | `The destination must never switch silently.` | mobile certainty/privacy | Accepted for design — source review and pause are mandatory. | Source correction and identity assurance approval. | +| 18. Template changed/retired | `overlay-specimens.tsx` | `New activation requires a current, locally approved version.` | approval certainty | Accepted for design — retired history stays readable but unusable. | Version lifecycle and two-person approval. | +| 19. Session expiry | `overlay-specimens.tsx` | `No mutation was recorded. Sign in through WA Health SSO/MFA before continuing; local credentials are absent.` | fresh authentication | Accepted for design — fails closed without local credential handling. | Production WA Health SSO/MFA acceptance. | +| 20. Offline banner | `overlay-specimens.tsx` | `Activation, mutation and uncertain resend are unavailable.` | offline/delivery certainty | Accepted for design — no offline mutation or ambiguous resend. | Connectivity, cache and provider-outage approval. | +| 21. Recoverable error | `overlay-specimens.tsx` | `never guess or display stale state as current` | stale certainty | Accepted for design — retry does not transform error into data. | Read/retry and observability approval. | +| 22. Permission unavailable | `overlay-specimens.tsx` | `Access remains deny-by-default and the attempted access is audited.` | permission/privacy | Accepted for design — no permissive fallback. | RBAC and audit acceptance. | +| 23. Team switcher | `overlay-specimens.tsx` | `No patient context crosses teams.` | privacy/ownership | Accepted for design — selected patient and drafts clear before switching. | Tenant/team isolation approval. | +| 24. Draft/version conflict | `overlay-specimens.tsx` | `Do not silently overwrite either record.` | conflict/audit certainty | Accepted for design — both frozen and current versions remain reviewable. | Version-conflict and audit acceptance. | + +## 5. Approval status and residual risk + +The content is suitable for a synthetic design handoff only. Final sender identity, programme phone, +hours, emergency wording, crisis contact, message variants and translated content require versioned +local clinical-programme and lived-experience/content approval. Privacy/security, records, +integration and service governance may block production. This review does not authorise patient +data, SMS delivery, clinical use or a pilot. diff --git a/docs/caring-contacts/content-style-guide.md b/docs/caring-contacts/content-style-guide.md new file mode 100644 index 0000000000..c62e843648 --- /dev/null +++ b/docs/caring-contacts/content-style-guide.md @@ -0,0 +1,121 @@ +# Caring contacts — content style guide + +**Status:** binding synthetic-design language, 15 August 2026 +**Applies to:** every screen, overlay, fixture, test and future implementation described by the +[binding specification](../superpowers/specs/2026-08-15-caring-contact-coordination-design.md) + +## 1. Voice and structure + +Use Australian English, sentence case and plain operational language. Lead with the observable +condition, then the remedy and owner. Actions are verb-first and name their object: `Review +fictional referral`, `Pause future contacts`, `Record operational review`. Do not use urgency, +engagement or patient-state shorthand when the system only knows a workflow or transport event. + +One filled command leads a region. Headings and explanatory text come before controls. Patient +identity, current ownership, availability and the effect of an action remain visible before the +decision. Destructive or protected decisions state whether fresh authentication, a reason or an +approval is required. + +## 2. Closed terminology + +| Meaning | Required wording | Do not substitute | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | +| Referral not accepted | `Awaiting handover`; `[referring team] remains responsible until explicit acceptance` | owned, assigned, accepted by aftercare | +| Source agreement | `Agreement confirmed: Yes/No` plus source/referrer/time | consented, treatment consent, legal consent | +| Mobile evidence | `Imported mobile`; `patient-controlled and suitable for discreet SMS`; source named | destination-verification claims, confirmed destination | +| Pathway status | `Illustrative locally governed pathway` until approved | recommended, best practice, proven | +| One-way channel | `Replies are not received, stored, analysed or monitored` | reply, text us, tell us, inbox, conversation | +| Schedule | one selected plan preference: `Morning 10:00 am AWST`, `Afternoon 2:00 pm AWST` or `Early evening 5:00 pm AWST` | rotating windows within one plan, local time, later today, ASAP | +| Transport state | `Scheduled`, `Processing`, `Sent`, `Delivered`, `Not delivered`, `Number invalid`, `Contact changed`, `Status unavailable`, `Missed` | reached, engaged, responded, safe, helped | +| Delivery interpretation | `Delivered means transport receipt only. It does not show that the message was read.` | received by patient, read, contact successful | +| Operational exception | exact condition, remedy and owner | concern, high risk, clinical priority, needs attention without qualification | +| Pause | reversible; original calendar retained; contacts during pause skipped | defer and catch up, restart cadence | +| Withdrawal | patient preference; unsent contacts cancelled immediately; terminal history | opt out later, reversible withdrawal | +| Cancellation | separately authorised operational action with reason | withdrawal | +| Recorded death | `Recorded death — irreversible cancellation`; correction is an incident | undo, reinstate | +| Small cell | `Suppressed` | zero, fewer than a guessed threshold | + +`Needs action` is permitted only as a section heading whose rows name the observable condition, +remedy and owner. It is never a patient-risk category. + +The canonical patient-visible no-reply notice is exactly `Replies are not received, stored, +analysed or monitored`. Use those four verbs in that order, with this capitalisation. It is a +complete sentence in the exact message, template, preview and review surfaces; do not shorten or +paraphrase it. + +## 3. Patient-facing message rules + +Patient-visible copy is warm, brief, discreet and non-demanding. It may substitute only the approved +preferred name, neutral team identity, coordinator signature and governed variant. It must not ask +for a reply, disclosure, task completion or reassurance. The exact fully substituted text, encoding +and segment count appear before activation. More than two SMS segments blocks progression. + +The first contact includes the discreet team identity, bounded caring-contact purpose, named +coordinator, no-reply statement, fictional programme phone and staffed hours, emergency direction +and one approved crisis-support contact. Later contacts retain the short no-reply statement and +programme contact. Exact real wording remains subject to clinical programme and lived-experience +approval; the synthetic text is not approved patient content. + +## 4. Synthetic names, identifiers and numbers + +All design and test evidence must use visibly fictional details. Approved patterns are: + +- names containing `Example`, `Sample` or `Fiction`; +- identifiers prefixed `SYN-`; +- fictional organisations such as `Example Aftercare Team` and `Fictional Ward A`; +- `.invalid` email domains; and +- only the designated fiction contact numbers and their frozen roles: + - Mira patient mobile: `+61 491 570 006`; + - Rowan patient mobile: `+61 491 570 156`; + - staffed programme line: `+61 491 570 157`; + - crisis-support contact: `+61 491 570 158`. + +Patient, programme and crisis roles are pairwise distinct. Never reuse a patient mobile as a +programme or crisis-support contact, and never use the programme line as the crisis-support contact. + +Do not improvise plausible Australian patient, staff, service, provider or phone details. Real PHI +must never enter fixtures, screenshots, tests, URLs, page titles, logs or analytics. All person, +service and contact identifiers are synthetic. The emergency direction intentionally uses the real +Australian emergency number `000`; it is safety instruction, not a person, service-programme or +contact identifier. + +## 5. Dates, times and missing values + +Store and exchange ISO 8601 timestamps. Render with `en-AU` and `Australia/Perth`; show `AWST` on +every operational send time. Use full dates where a decision depends on the date, for example +`Saturday 22 August 2026`. Use `10:00 am`, `2:00 pm` and `5:00 pm`, retaining the minute and meridiem. +Never infer the user's device zone. + +Use an explicit phrase for absence: `No named exceptions for this day`, `Approval evidence missing` +or `Status unavailable`. Never use a bare dash. + +Store one selected sending preference on each plan and derive every planned contact window from it. +For the Rowan fixture, the current value is `Morning 10:00 am AWST` across all 10 contacts. The +Schedule dashboard may aggregate different patients across all three service windows; it does not +permit one episode to rotate windows. A preference overlay must label the current value separately +from any proposed change. + +## 6. Accessibility labels and announcements + +Accessible names match the visible action and object. Overlay titles name the decision, close +controls name the object, and unavailable controls reference the visible reason. Announcements are +short outcomes without patient identity: `Operational review recorded`, not a name, phone, message +or identifier. Status uses visible words plus an icon, mark or structure; colour never carries the +meaning alone. The continuity graphic is decorative support for the immediately following ordered +list named `Caring-contact schedule`. + +## 7. Privacy-safe transient language + +Toasts, live regions, push alerts and page titles contain no patient name, identifier, phone number, +message text or clinical detail. Use object-neutral outcomes such as `Review recorded`, `Plan paused` +or `Accepted referral remains unclaimed. Open Callback.` If an exact object must be identified, keep +it inside the authenticated page body and audit record, not the transient surface. + +## 8. Prohibited claims + +Do not say or imply that Callback monitors a patient, detects or predicts risk, provides crisis +response, proves safety or wellbeing, records replies, establishes engagement, provides treatment, +replaces follow-up, proves effectiveness, independently verifies an imported mobile, or transfers +duty of care merely through coordinator assignment. Do not use diagnostic labels to determine +eligibility or priority. These prohibitions apply to visible copy, accessible names, icons, colours, +charts, ordering, empty states and analytics labels. diff --git a/docs/caring-contacts/design-handoff.md b/docs/caring-contacts/design-handoff.md new file mode 100644 index 0000000000..05a0244601 --- /dev/null +++ b/docs/caring-contacts/design-handoff.md @@ -0,0 +1,144 @@ +# Caring contacts — developer handoff + +**Status:** frozen synthetic design handoff, 15 August 2026 +**Implementation authority:** not granted + +This handoff maps the complete approved prototype to a future production architecture. Route and +component names below are targets, not existing production code. Production work must remain +separate from Clinical KB search, RAG, OpenAI, favourites, recent-search, analytics and browser +persistence. + +## 1. Route and domain-component inventory + +| Screen/state | Future route | Domain components | Prototype fixture/source | Required production contract | Safety invariant | +| ----------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| Workspace shell / Today | `/caring-contacts` | `CaringContactShell`, `ActiveTeamContext`, `TodayWorkQueue`, `SendingWindowPanel` | referrals, contacts, audit events | team-scoped queue projection; queue age; explicit handover state | actions precede metrics; no risk ranking; no PHI in title/alerts | +| Patients search/empty/results | `/caring-contacts/patients` | `PatientSearch`, `IdentityResultRow`, `IdentityAssurance` | synthetic patients/referrals | active-team-only search; audited view; no global-search indexing | identity-forward result plus separate assurance; clear state on team switch | +| Patient and agreement | `/plans/new` — agreement stage | `IdentityHeader`, `SourceEvidence`, `CommunicationEligibility`, `ActivationStepper` | patient + referral | source provenance, accepted team, coordinator, exact agreement classification | imported-mobile/suitability provenance; no verification or consent overclaim | +| Pathway selection | `/plans/new` — pathway stage | `PathwaySelector`, `PathwayVersionCard` | pathway fixtures | current locally approved versions and immutable approval evidence | no best/recommended ranking; unapproved cadence stays illustrative | +| Personalisation | `/plans/new` — personalisation stage | `GovernedVariantSelector`, `SmsPreview`, `SegmentCounter` | template/pathway/patient fixtures | allowlisted substitutions; deterministic encoding/segment calculation | one selected sending preference; no free text; more than two segments blocks | +| Review and activation | `/plans/new` — review stage | `ActivationAssuranceReview`, `ExactSchedule`, `FreshAuthGate` | all activation fixtures | fresh auth; atomic idempotent activation; frozen versions/text/schedule/audit | exact identity, content, selected window, dates and states before final action | +| Patient overview | `/patients/[patientId]` | `PatientIdentityHeader`, `PlanSummary`, `ContinuityThread`, `EpisodeChronology` | episode/contact/audit fixtures | authorised patient/episode read model with immutable chronology | each dated row has transport state; Delivered is transport-only | +| Plan detail | `/plans/[planId]` | `PlanDetail`, `GovernedPlanActions`, `AuditHistory` | episode/pathway/template fixtures | version snapshots, selected preference, current owner and deterministic state | terminal states read only; every mutation rechecks authority/connectivity | +| Schedule | `/caring-contacts/schedule` | `SevenDayStrip`, `DaySchedule`, `SendingWindowList`, `ExceptionList` | planned contacts | Perth calendar/public-holiday service, day/window projection | missed contacts never late; exceptions stay separate from routine lists | +| Contact detail | `/contacts/[contactId]` | `ContactDetail`, `TransportHistory` | contact/delivery fixtures | provider-neutral signed event history; bounded retry state | Processing cannot change; Delivered means transport receipt only | +| Delivery exception | same contact route with drawer state | `DeliveryExceptionDrawer`, `OperationalTask`, `WriteBackSummary` | synthetic delivery event | three-attempt evidence, same-day task, atomic pause/write-back/audit | no clinical inference or automatic follow-up; uncertain send not resent | +| Templates list/detail | `/caring-contacts/templates`, `/templates/[pathwayId]` | `TemplateList`, `TemplateVersionDetail`, `ApprovalEvidence` | pathway/template fixtures | immutable versions, two-person approval, lifecycle | retired version stays readable; only current approved version selectable | +| Team | `/caring-contacts/team` | `TeamRoster`, `UnclaimedWorkEscalation`, `TeamSwitcher` | team fixtures | service-managed groups, coverage and 60-minute escalation | assignment is coordination; external alert has no PHI | +| Guidance | `/caring-contacts/guidance` | `ProgrammeBoundary`, `IncidentGuidance` | static governed content | versioned locally approved operational guidance | caring contacts are not monitoring, triage or crisis response | +| Reports | `/caring-contacts/reports` | `OperationalMetrics`, `SuppressedCell` | synthetic aggregates | approved data dictionary/demographics/threshold and audited access | operational outcomes only; no clinician ranking/effectiveness inference | + +The approved rollout routes above are canonical. The earlier nested episode/version proposals are +**superseded and not approved**: `/caring-contacts/patients/[episodeId]/activate/*`, +`/caring-contacts/patients/[episodeId]/plan`, +`/caring-contacts/patients/[episodeId]/contacts/[contactId]`, +`/caring-contacts/patients/[episodeId]` and `/caring-contacts/templates/[versionId]`. Do not +implement aliases or parallel route identities for them. + +## 2. Episode and boundary state inventory + +| State | Required UI/state handling | Data/authority gate | +| -------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------- | +| Pending referral | Accept, Return for clarification, Decline; referring-team responsibility visible | source/write-back contract; authorised clinician | +| Duplicate active referral | block creation; link to active episode | idempotency key and active-episode uniqueness | +| Later qualifying discharge | new linked episode only after earlier closure | source event identity and linkage rules | +| Readmission | pause future contacts; later discharge needs new referral | assessed source-event integration | +| Recorded death | irreversibly cancel unsent contacts | high-assurance source event, transaction and audit | +| Death correction | incident workflow; old plan remains cancelled; no undo | incident authority and new-referral rule | +| Mobile/contact changed | pause; review source evidence; never silently switch | source correction/version contract | +| Wrong recipient/duplicate send/unauthorised content/lost audit | pause entire pilot | joint incident, privacy/security and clinical authority | +| Pause | preserve calendar and skip contacts during pause | authorised member, reason/audit/write-back | +| Withdrawal | immediately cancel unsent; immutable terminal history | patient request evidence and fresh auth; no approval | +| Cancellation | distinct reasoned authorised action | role and fresh-auth policy | +| Reassignment | retain handover/audit; change coordinator only | fresh auth and service role; no duty-of-care inference | + +## 3. System-state contract + +Every route must define loading, empty, recoverable error, offline, expired authentication, +permission unavailable and version/conflict behaviour. Loading never becomes terminal. Failed reads +do not render as zero results. Offline mode stores no patient data and permits no activation, +mutation or uncertain resend; an already-open mutation rechecks connectivity at commit time. Session +expiry blocks the shared overlay stack until re-authentication. Permission and version conflicts +preserve the current record and name the authorised remedy. + +The complete overlay inventory is: verify identity; change patient; pathway preview; message preview; +communication preference; adjust date/time; outside-window warning; save draft; discard changes; +final activation; activation success; pause; withdrawal; reassignment; delivery detail; resolve +failed delivery; contact-changed block; template changed/retired; session expiry; offline banner; +recoverable error; permission unavailable; team switcher; draft/version conflict. Implement with the +repository `Sheet`, `ConfirmDialog` and `OverlayRoot` contracts, including initial/return focus, +scroll containment, safe-area actions and responsive modality. + +The modality matrix is frozen as follows; `action only` means Escape, backdrop and close controls +cannot dismiss the session gate, while the offline banner remains until its recovery action: + +| # | Decision | Phone modality | Desktop modality | Dismissal | +| --- | ------------------------ | ---------------------------- | ---------------------------- | ------------------------- | +| 1 | Verify identity | protected full-screen stage | dialog | Escape, backdrop or close | +| 2 | Change patient | protected full-screen stage | dialog | Escape, backdrop or close | +| 3 | Pathway preview | full-screen inspection | right inspection drawer | Escape, backdrop or close | +| 4 | Message preview | full-screen inspection | right inspection drawer | Escape, backdrop or close | +| 5 | Communication preference | compact bottom sheet | dialog | Escape, backdrop or close | +| 6 | Adjust date/time | compact bottom sheet | dialog | Escape, backdrop or close | +| 7 | Outside-window warning | compact bottom sheet | dialog | Escape, backdrop or close | +| 8 | Save draft | compact bottom sheet | dialog | Escape, backdrop or close | +| 9 | Discard changes | compact bottom sheet | dialog | Escape, backdrop or close | +| 10 | Final activation | protected full-screen stage | dialog | Escape, backdrop or close | +| 11 | Activation success | compact bottom sheet | dialog | Escape, backdrop or close | +| 12 | Pause | compact bottom sheet | dialog | Escape, backdrop or close | +| 13 | Withdrawal | protected full-screen stage | dialog | Escape, backdrop or close | +| 14 | Reassignment | compact bottom sheet | dialog | Escape, backdrop or close | +| 15 | Delivery detail | full-screen inspection | right inspection drawer | Escape, backdrop or close | +| 16 | Resolve failed delivery | compact bottom sheet | dialog | Escape, backdrop or close | +| 17 | Contact-changed block | compact bottom sheet | dialog | Escape, backdrop or close | +| 18 | Template changed/retired | protected full-screen stage | dialog | Escape, backdrop or close | +| 19 | Session expiry | non-dismissible session gate | non-dismissible session gate | action only | +| 20 | Offline banner | persistent status banner | persistent status banner | recovery only | +| 21 | Recoverable error | compact bottom sheet | dialog | Escape, backdrop or close | +| 22 | Permission unavailable | compact bottom sheet | dialog | Escape, backdrop or close | +| 23 | Team switcher | compact bottom sheet | dialog | Escape, backdrop or close | +| 24 | Draft/version conflict | protected full-screen stage | dialog | Escape, backdrop or close | + +## 4. Production data and integration boundaries + +Create dedicated tenant-scoped patient, referral, episode, plan, contact, template snapshot, +delivery event, operational task, write-back and append-only audit contracts in a separately +approved Australian PHI-capable environment. Use WA Health SSO/MFA, conditional access and managed +sessions; deny by default. Signed replay-safe webhooks are the routine provider status source. +Manual reconciliation is permitted only for outage, discrepancy or suspected incident. Detailed +transport/access evidence stays in Callback; only approved structured milestones write back. + +Each plan stores exactly one selected sending preference (`Morning`/10:00 am, +`Afternoon`/2:00 pm or `Early evening`/5:00 pm AWST). Every planned contact derives its window and +time from that plan value; the preference is never rotated contact-by-contact. A governed proposed +change is distinct from the current value and must atomically update future eligible contacts while +preserving the original cadence, already-final transport history and audit evidence. The Rowan +fixture freezes `Morning 10:00 am AWST` across all 10 contacts. + +No patient-level export, browser persistence, offline cache, global search/RAG/OpenAI path or PHI in +logs, analytics, URLs, page titles, notifications or screenshots is permitted. + +## 5. Authority gates before implementation or release + +1. Explicit authority to begin production architecture and implementation. +2. Named service protocol, eligibility, role matrix, capacity/stopping rules and seven-day coverage. +3. Clinical-programme and lived-experience approval of every exact message version and the complete + prototype. +4. Legal/privacy/records agreement classification, PIA, data-flow, retention, correction and legal- + hold decisions. +5. Australian hosting, tenant/key/access controls and provider procurement/security acceptance. +6. Source identity/referral/readmission/death/mobile/write-back contracts and assessed simulation. +7. SMS sender, retry, webhook, outage and reconciliation proof with no late/uncertain resend. +8. Accessibility approval including physical iPhone Safari and installed-PWA evidence. +9. Incident, audit-integrity and joint restart simulation. +10. Explicit pilot/go-live authorisation; production migration/deployment remains separately gated. + +## 6. Frozen non-goals + +No two-way messaging, inbox, reply storage/analysis, crisis triage, risk scoring, diagnostic +eligibility, clinical advice, treatment workflow, effectiveness claim, clinician ranking, global +patient search, unrestricted content authoring, generative text, dynamic translation, automatic +round robin, late catch-up sends, automatic clinical follow-up from delivery failure, daily full +provider reconciliation, patient export or offline patient cache. Do not promote mockup fixtures or +components into production by copying them wholesale; implement domain contracts behind repository +primitives and gates. diff --git a/docs/caring-contacts/governance-decisions.md b/docs/caring-contacts/governance-decisions.md new file mode 100644 index 0000000000..3588fc789b --- /dev/null +++ b/docs/caring-contacts/governance-decisions.md @@ -0,0 +1,85 @@ +# Caring contacts — governance decisions and residual risks + +**Status:** approved product-decision record for synthetic design, 15 August 2026 +**Authority:** [Approved decision lock](../superpowers/plans/2026-08-14-caring-contact-coordination-rollout.md#approved-decision-lock--15-august-2026) + +`Locked` means the prototype must represent the decision consistently; it does not mean production +approval. A formal owner may block production use, but the conflict and owner must be recorded rather +than silently changing the model. Nothing here authorises patient data, APIs, Supabase, OpenAI/RAG, +SMS, migration, deployment, procurement or a live canary. + +## 1. Service, handover and pilot + +| Locked decision | Production evidence/owner | Current limit | +| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------- | +| One dedicated aftercare/transition team at one hospital/service; clinician-enrolled adults discharged after a suicidal crisis | Named service, protocol and eligibility — service clinical governance | Organisation and real enrolment not approved here | +| Structured hospital referral; Accept, Return for clarification or Decline with structured write-back | Interface/correction/write-back contract — integration, records and service owners | Synthetic referral only | +| Referring team responsible until explicit acceptance; coordinator claimed/assigned after acceptance; no round robin | Handover procedure, role matrix and coverage — clinical governance, service and identity | Locked in design | +| Single-team pilot has no numeric cap; every eligible referral accepted while open | Workload exposure, stopping rules and 6–8-week review — governance board | Conscious residual exposure | +| Two weeks of seven-day hypercare with named clinical, service, technical, privacy and incident leads | Approved go-live plan — service governance | Not scheduled or authorised | + +No cap is not unlimited safe capacity. Design must expose queue age, unclaimed work and workload. + +## 2. Identity, agreement, schedule and content + +| Locked decision | Production evidence/owner | Current limit | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | +| Source-system identity/mobile imported without Callback test SMS/read-back; separate patient-controlled/suitable flag required | Source-field definitions, provenance and correction flow — health-information/integration owner | No independently verified-mobile claim | +| `Agreement confirmed: Yes/No`; source referral/referrer/time retained | Classification and record procedure — clinical governance, legal/privacy and records | Legal/consent classification open | +| Discharge anchor; Morning 10:00, Afternoon 14:00, Early evening 17:00 AWST; weekends/public holidays within 09:00–18:00 | Versioned schedule and seven-day staffing — service governance | Illustrative pathway only | +| One selected sending preference per plan; all contacts derive the same window; missed contacts never sent late; pause skips without rebasing; withdrawal immediately cancels unsent contacts | Deterministic schedule/preference/audit/write-back proof — service and records | Locked in design | +| Governed variants/substitutions only; two SMS segments; approved English only | Versioned library — clinical programme lead and lived-experience/content representative; privacy/legal if disclosure changes | Exact content not approved here | +| Non-receiving sender; first SMS has phone/hours, emergency direction and one crisis contact; later SMS keeps no-reply boundary/contact | Provider proof and exact approved wording — SMS owner, clinical programme and lived experience | No provider/sender selected | + +## 3. Episode, delivery and reconciliation + +| Locked decision | Production evidence/owner | Current limit | +| ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------- | +| Duplicate active referral blocked; later discharge creates a new linked episode | Domain/idempotency/source-event proof | No datastore | +| Readmission pauses; recorded death irreversibly cancels; correction is an incident | Integration contract and assessed simulation | Synthetic events only | +| Two bounded retries, three attempts total, original window only; permanent failure pauses future contacts | Provider-neutral status/retry tests and service procedure | No provider | +| Provider outage misses are never sent late; uncertain contacts never resent automatically | Outage/recovery simulation | Locked in design | +| Signed replay-safe webhooks are routine status source; manual provider reconciliation only for outage, discrepancy or suspected incident | Provider, service and security acceptance | Conscious residual risk: no daily full reconciliation | + +Webhook-primary operation can leave a missing/delayed event temporarily undetected. Controls are an +exact `Status unavailable` state, retained evidence, event-triggered reconciliation and no automatic +resend of uncertainty. Provider, service and security owners must accept the remaining risk. + +## 4. Identity, records, hosting and reporting + +| Locked decision | Production evidence/owner | Current limit | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------- | +| WA Health SSO/MFA, conditional access and managed sessions; no local credentials; fresh auth for activation, withdrawal, reassignment/export | Identity and supported-device proof — identity/security | No live identity integration | +| Service-managed team groups, deny by default; context switch clears patient state | Role matrix, joiner/mover/leaver and team-scope tests — service/identity | Synthetic roles only | +| Callback is not the clinical record; structured milestones write back while detailed transport/access evidence remains in Callback | Authority, event set, retention/correction/legal hold — records and service | Target systems unresolved | +| Every search/view/decision/mutation/write-back/admin access audited; clinicians see episode operations, privacy/security see complete access | Append-only, transactional and role evidence — privacy/security/records | Design contract only | +| Separately contracted PHI-capable Australian-region hosting; all identifiers/messages/data/backups/logs/provider processing stay in Australia; dedicated tenant-scoped Australian keys | PIA, data flow, contracts, key/access/rotation proof — privacy/security/legal/procurement | Current Clinical KB deployment prohibited for PHI | +| No patient-level export or browser persistence/offline cache; no real patient information or PHI in Clinical KB/RAG/OpenAI/logs/URLs/toasts/analytics/screenshots; prototype/test screenshots require clearly fictional synthetic identities/details | Static/runtime/device/fixture proof — privacy/security | Locked prohibition | +| Aggregates may use approved source demographics with configured small-cell threshold and `Suppressed`; never rank clinicians | Data dictionary, threshold and access rules — governance/analytics | Threshold not invented in design | + +## 5. Incident authority + +A confirmed wrong-recipient message, duplicate send, unauthorised content, material privacy/security +incident or loss of audit integrity immediately pauses the entire pilot. Restart requires joint +approval after reconciliation/remediation from the incident lead, privacy/security owner and clinical +programme lead. No one role or automation may restart it. Downtime fails closed: no offline patient +cache, new activation or uncertain send. + +## 6. Residual-risk register + +| Risk | Current control | Acceptance owner | +| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| Patient-controlled-mobile evidence wrong/stale | Source provenance, deliberate review, pause on source change, no overstated verification | Integration, clinical governance, privacy | +| Missing/delayed webhook without daily reconciliation | Unavailable state, event-triggered reconciliation, no uncertain resend | Provider, service, security | +| No-cap pilot exceeds capacity | Queue/workload visibility, 60-minute unclaimed escalation, stopping rules, early review | Governance board, service operations | +| One-way contact feels mechanical or implies unavailable support | First-message support content, staffed phone, two lived-experience gates, external acceptability evaluation | Clinical programme, lived experience | +| Agreement classification unresolved | Exact neutral label; no legal/treatment-consent claim; synthetic only | Clinical governance, legal/privacy, records | +| Aggregate small cells enable inference | Approved threshold and non-inferable `Suppressed`; synthetic aggregates until set | Governance, analytics | +| Design mistaken for readiness | Persistent synthetic/non-production label and explicit gates | Product owner and design reviewers | + +## 7. Non-production limit + +Progression requires clinical/lived-experience approval of message versions, approval of the complete +prototype, privacy/security/accessibility review, assessed simulation and later explicit provider and +pilot authorisation. This record claims no clinical approval, WA Health endorsement, provider +acceptance, deployment, migration, production readiness or clinical effectiveness. diff --git a/docs/caring-contacts/repository-design-audit.md b/docs/caring-contacts/repository-design-audit.md new file mode 100644 index 0000000000..087d36d1c5 --- /dev/null +++ b/docs/caring-contacts/repository-design-audit.md @@ -0,0 +1,90 @@ +# Caring contacts — repository design audit + +**Status:** synthetic design-phase audit, 15 August 2026 +**Decision source:** [approved rollout plan](../superpowers/plans/2026-08-14-caring-contact-coordination-rollout.md) + +## 1. Conclusion and source order + +The repository-native direction is a dedicated `/caring-contacts/**` operational workspace. It +inherits Clinical KB v2 tokens, primitives, themes and gates but remains separate from query-first +search, RAG and browser persistence. Repository maturity supports synthetic design; it does not prove +the current deployment, privacy posture or individual-owner storage is suitable for patient data. + +Use sources in this order: repository `AGENTS.md`; `src/app/ckb-v2-tokens.css`; committed tests; +`.design-sync/conventions.md`; then [SPEC](../design-system/SPEC.md), +[TOKENS](../design-system/TOKENS.md), [COMPONENTS](../design-system/COMPONENTS.md), +[GATES](../design-system/GATES.md), DECISIONS and ADOPTION. Older design/redesign documents are +historical where they conflict. Token values remain in owning CSS files, not prose. + +## 2. Visual and rendered sources + +Inherit the true-white/graphite clinical canvas, restrained command hierarchy, Clinical Sky identity, +semantic-only status colour, Geist type, semantic spacing/radius/elevation, one edge owner, sparse +surfaces and the five named responsive states. Dark, forced-colour, reduced-motion, 320px and 400% +zoom treatments are required states. Do not copy Psychbase visual, navigation or clinical-state +assumptions. + +| Rendered source | Repository evidence | Lesson, not template | +| ----------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------- | +| Dashboard desktop/phone | `tests/__screenshots__/linux/dashboard-shell.png`, `dashboard-shell-phone.png` | Calm hierarchy, one command, compact safe-area discipline | +| Results desktop/phone | `search-results-band.png`, `search-results-band-phone.png` | Dense metadata in rows; controls collapse explicitly | +| Document viewer | `document-viewer.png` | Split inspection, provenance and progressive disclosure | +| Therapy Compass home | `therapy-compass-home.png` | Native workflow entry with restrained choices | + +## 3. Reuse and ownership map + +| Need | Reuse / owner | Binding rule | +| ----------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------- | +| Root theme, auth context, announcements, overlay root | `src/app/layout.tsx` | No duplicate provider or overlay tree | +| Tokens | `ckb-v2-tokens.css`, `globals.css` | Semantic roles only; theme parity | +| Structure | `PageHeader`, `Breadcrumb`, `PanelHeading`, `Disclosure` | One wrapping `

` and explicit hierarchy | +| Actions | `Button`, `IconButton` | One filled command; verb-first; busy/disabled named | +| Fields | `FormField` family, `ErrorSummary` | Persistent labels; connected hint/error; reviewed autocomplete | +| Decisions | `Sheet`, `ConfirmDialog`, `OverlayRoot` | Mandatory names, focus containment/restoration, one stack | +| View choice | `Tabs`, `SegmentedControl` | Tabs change panels; segments change sort/view | +| Status | `Chip`, `InlineNotice`, `StatusMark` | Domain vocabularies; text primary, shape/colour secondary | +| Dates/missing values | `DateDisplay`, `MissingValue` | ISO in; Perth display; explicit absence | +| Feedback | announcer, Toast, Empty/Error/Loading/Skeleton | No patient data; spinner never terminal | +| Tables | `AccessibleTable` | Only for real relationships; caption and compact strategy | +| Future launch/reachability | `tools-catalog.ts`, wiring convention, route test | One coordination entry; no `AppModeId`; no orphan route | + +Later domain components use a new caring-contact namespace under the existing components tree: +shell/context switcher, identity header/assurance, communication eligibility, plan summary, +continuity thread, activation stepper, pathway/variant selectors, SMS preview/segment count, one-way +notice, assurance review, closed plan/contact state chips, action/schedule rows, delivery/audit +history, coordinator selector and quiet programme metric. Promote one only after a second genuine +domain use and the design-system authoring contract. + +## 4. Mockup and browser isolation + +- The synthetic suite belongs under noindexed `/mockups/caring-contacts`; mockup-only fixtures cannot + be imported by production. +- No live patient, provider, Supabase, OpenAI/RAG or other API call is permitted. +- Controls are wired or explicitly unavailable with a stated reason; mockup exemptions do not permit + inert enabled controls. +- No patient persistence, recent-search, analytics or offline cache is copied into the mockup. +- Browser proof uses repository wrappers, a focused caring-contact spec and the URL selected by + `npm run ensure`; `/api/local-project-id` must confirm `Clinical KB`. Never call Playwright + directly or assume a port. +- Later proof covers 320/390/430/768/1024/1440, keyboard/focus, text/zoom reflow, dark, forced + colours, reduced motion and overlays. Physical iPhone Safari/PWA remains separate evidence. + +## 5. Explicitly unsuitable directions + +| Do not extend or copy | Reason | +| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| `GlobalSearchShell`, `MasterSearchHeader`, shared composer | Patient-first workspace; patient details must not enter global search | +| `app-modes.ts`, RAG/OpenAI answer or document routes | Caring-contact coordination has no search contract or generated content path | +| `patient-profile-storage.ts` or browser-local patient state | Persistent/offline patient storage is prohibited | +| `FilterBar`, `DataTable`, `AsyncButton` | Retired/deprecated; use surface filters, `AccessibleTable`, current `Button` | +| Risk-ranked queues or `best match` | Objective timing and named operational conditions only | +| Decorative status colour, generic SaaS, glass-heavy or marketing gradients | Conflicts with clinical colour and sparse hierarchy contracts | +| Copied Psychbase styling | Not a repository source of truth; carries unrelated assumptions | +| Raw mockup design values or ad-hoc z/portal stacks | Production promotion requires tokens and the shared overlay contract | + +## 6. Audit limit + +Design-sync registration proves local source/export/prop/preview publication, not complete browser +acceptance or caring-contact suitability. Real-patient use additionally needs approved team tenancy, +Australian PHI-capable hosting, datastore, audit, records and provider boundaries. This document is +verified only by the Task 1 documentation checks; it does not claim that routes or components exist. diff --git a/docs/scripts-index.md b/docs/scripts-index.md index 390cb1bab3..ecf31b5408 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (238 files) and the `package.json` script surface (246 entries), +Curated map of `scripts/` (238 files) and the `package.json` script surface (247 entries), grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run ` referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above. diff --git a/docs/site-map.md b/docs/site-map.md index 0aaef06d04..4e6ad76559 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -993,6 +993,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir - `/mockups/calculators-popup-sheet` - Route discovered from app directory Source: `src/app/mockups/calculators-popup-sheet/page.tsx`. - `/mockups/calculators-search` - Route discovered from app directory Source: `src/app/mockups/calculators-search/page.tsx`. - `/mockups/calculators-search-page` - Route discovered from app directory Source: `src/app/mockups/calculators-search-page/page.tsx`. +- `/mockups/caring-contacts` - Route discovered from app directory Source: `src/app/mockups/caring-contacts/page.tsx`. - `/mockups/document-navigation-contract` - Route discovered from app directory Source: `src/app/mockups/document-navigation-contract/page.tsx`. - `/mockups/document-navigation-final` - Route discovered from app directory Source: `src/app/mockups/document-navigation-final/page.tsx`. - `/mockups/document-navigation-final-review` - Route discovered from app directory Source: `src/app/mockups/document-navigation-final-review/page.tsx`. diff --git a/docs/superpowers/plans/2026-08-14-caring-contact-coordination-rollout.md b/docs/superpowers/plans/2026-08-14-caring-contact-coordination-rollout.md new file mode 100644 index 0000000000..a56d2be487 --- /dev/null +++ b/docs/superpowers/plans/2026-08-14-caring-contact-coordination-rollout.md @@ -0,0 +1,1054 @@ +# Caring Contact Coordination Workspace — Repository-Native Rollout Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans`. Steps use `- [ ]` checkboxes. Treat each implementation tranche below as an independent reviewed plan; do not execute the whole programme as one branch. + +**Goal:** Deliver a premium, responsive, one-way caring-contact coordination workspace for WA hospital services, beginning with an approved visual and clinical-operating model and progressing through a controlled synthetic-data build, governed integration, limited pilot, and measured expansion. + +**Architecture:** Keep the feature in this repository so it inherits the current Clinical KB design system, accessibility primitives, testing discipline, and application infrastructure, but give it a dedicated route group and operational shell. Do not make it a searchable Clinical KB mode and do not send patient data through the existing RAG/OpenAI path. Before real-patient use, deploy the workspace against an explicitly approved PHI-capable identity, hosting, datastore, audit, records, and SMS boundary rather than assuming the current Clinical KB deployment is suitable. + +**Tech stack:** Next.js 16 App Router, React 19, TypeScript, the repository v2 design system, Tailwind CSS token utilities, Vitest/Testing Library, repository-wrapped Playwright, PostgreSQL/Supabase only if separately approved, and a provider-neutral SMS outbox/adapter. + +**Decision revision:** 15 August 2026 — incorporated the completed product, clinical-operating, privacy, rollout and visual-direction grilling session. + +## Global constraints + +- Initial cohort: clinician-enrolled adults leaving an emergency department or hospital after a suicidal crisis. +- A broader clinician-selected mental-health discharge cohort is a later, separately governed expansion. It is not part of the initial clinical scope or pilot acceptance criteria. +- Initial channel: one-way caring-contact SMS. No inbox, conversation thread, reply workflow, automated urgency detection, risk score, risk prediction, triage, or clinical advice. +- Caring contacts supplement usual care and person-to-person follow-up. Delivery is never evidence of safety, wellbeing, engagement, or treatment response. +- Every plan has one owning clinical team and one visible coordinating clinician. Safe reassignment must preserve audit history. +- Patient-visible messages come from the named clinical team and are signed by the coordinating clinician. The sender label, signature, escalation wording and substitution rules are governed content, not free text. +- Agreement remains the approved lightweight collaborative verbal-agreement model. The UI label is `Agreement confirmed`, not a claim of legal or treatment consent. Its exact local policy classification and clinical-record documentation are approval gates before pilot. +- Pathways and message variants are locally approved and versioned. Clinicians personalise only within deterministic, governed boundaries. There is no generative-AI message authoring. +- The twelve-month cadence is illustrative: day 1, week 1, months 1, 2, 3, 4, 6, 8, 10 and 12. It must be labelled `Service approved` or `Illustrative locally governed pathway`, never presented as a universal prescription. +- Use fictional patient data in plans, mockups, screenshots, fixtures, tests, demos and local verification until the real-patient pilot is explicitly authorised. +- Patient identifiers, phone numbers, message content and clinical context must never enter the existing Clinical KB search, answer, RAG, OpenAI, query-log, favourites, recent-search, browser-history or analytics paths. +- No production or patient-data API, Supabase, SMS-provider, migration, deployment or live canary work is authorised by this plan. Those actions require an explicit later request naming the target. +- Use `Australia/Perth` for operational scheduling and `en-AU` for display. Preserve machine ISO timestamps and the original timezone/offset in audit events. +- Meet WCAG 2.2 AA and the repository's stronger contracts: 48px targets except an existing documented exception, 320px and 400% reflow, dark mode, forced colours, reduced motion, keyboard use, screen-reader naming, safe areas and no patient data in transient notifications. +- Design and implementation must preserve exact operational language. Prohibited concepts include `high risk`, `safe`, `engagement score`, `needs attention` without a named reason, `campaign`, `lead`, `conversion`, `best match`, `inbox`, `messages` and `conversation`. +- The design approval gate comes before production route, database, API or delivery implementation. + +## Approved decision lock — 15 August 2026 + +These decisions refine the approved concept and must not be reopened during routine implementation. A formal clinical, privacy, security, records, procurement or service approval may block a decision from production use, but it must record the conflict and accountable owner rather than silently changing the product model. + +### Service, referral and ownership model + +- The first real-patient pilot serves one dedicated hospital aftercare/transition team at one hospital/service. It is not a statewide or multi-health-service tenancy pilot. +- The discharging clinician confirms the source-system identity, mobile information and verbal agreement. The existing hospital record/referral workflow sends a structured referral to Callback. +- The dedicated aftercare team reviews, personalises, activates and owns the caring-contact plan for its full duration. +- New referrals appear first on Today in `Referrals to review`, ordered by discharge and first eligible contact-window timing, never inferred clinical risk. +- The aftercare team may accept, return for clarification or decline using structured reasons. Clarification and decline write back to the hospital referral system, which owns referrer notification. +- The referring team retains responsibility until explicit acceptance. Callback must not imply that a pending or returned referral has transferred ownership. +- Accepted referrals enter the team queue and require an explicit coordinator claim or team-lead assignment. There is no automatic round-robin assignment. +- Authorised teammates provide audited coverage during coordinator absence; the named coordinator and any formal reassignment remain visible. +- Eligibility uses objective prerequisites only: adult status, qualifying discharge/referral, pilot-service scope, patient-controlled mobile flag and agreement. Diagnosis, presentation details and risk assessments never drive automated eligibility. +- Search is restricted to referrals and caring-contact episodes belonging to the pilot team. Callback is not a hospital-wide patient directory. + +### Mobile, agreement and patient control + +- Callback imports the current hospital-record mobile number without test SMS, verbal read-back or separate referrer attestation. +- Activation nevertheless requires an explicit source-system flag that the destination is patient-controlled and suitable for discreet SMS. A plain mobile-number field is insufficient. Family, carer and shared destinations are ineligible. +- The agreement interface is a simple `Agreement confirmed: Yes/No`. The audit automatically retains the source referral, referring clinician and received timestamp; no separate Callback agreement ceremony is added. +- A source-system mobile-number change automatically pauses future contacts and creates a coordinator-review exception. Callback never silently switches the destination. +- Patients request timing changes, pause or withdrawal through the named programme phone. It is staffed seven days during every sending window, and any authorised team member can act immediately. +- Withdrawal immediately cancels all unsent contacts, requires no approval, retains immutable history and writes back the milestone. A reason is optional. +- A pause keeps the original discharge-anchored calendar. Contacts falling inside the pause are skipped permanently; explicit resumption begins with the next future contact. + +### Schedule and message policy + +- The schedule is anchored to actual discharge time. The first message uses the next occurrence of the patient's approved sending time. +- Contacts may send on weekends and WA public holidays between 9:00 am and 6:00 pm AWST. +- Each patient plan stores one selected preference: `Morning`, `Afternoon` or `Early evening`, mapping to predictable service times of 10:00 am, 2:00 pm and 5:00 pm respectively. All contacts in that plan derive the same window; the Schedule dashboard may aggregate different patients across the three service windows. +- A missed first or later contact is recorded but never sent retrospectively. The pathway retains its original calendar. +- Coordinators may move a contact only within its scheduled day. A date change requires a reason and team-lead approval. +- Personalisation is structured only: preferred name, neutral team identity, coordinator signature and approved message variants. There is no unrestricted clinician free text or dynamic translation. +- The first pilot uses approved English content only. Interpreter-supported enrolment uses existing service processes; translated pathways require separate professional translation and cultural approval. +- Patient-visible sender and message wording are discreet but recognisable and never expose suicide, crisis or mental-health treatment on a lock screen. +- Use a non-receiving sender. Callback receives, stores, analyses and displays no replies. +- Enrolment and the first SMS provide complete support information. The first SMS includes the programme phone and hours, emergency direction and one approved crisis-support contact in plain text; later messages retain the short no-reply boundary and programme contact. +- Every fully substituted message, including required notices and signature, is limited to two concatenated SMS segments. The UI shows encoding and exact segment count and blocks overflow. + +### Episode, delivery and hospital-status policy + +- A new referral for a patient with an active plan is blocked as a duplicate and routed to review of the existing episode. +- A later qualifying discharge creates a new linked episode after the earlier episode is completed, cancelled or withdrawn. Earlier episodes are never reopened or mutated. +- Hospital readmission automatically pauses future contacts. A later discharge requires a new linked referral and coordinator decision; the old episode never automatically resumes or rebases. +- A recorded death immediately and irreversibly cancels all unsent contacts. A later source correction is an incident and requires a new referral for any future plan. +- Completed, cancelled and withdrawn plans become read-only, leave active worklists and remain available for the formally approved retention period. +- Structured clinical-record write-back covers referral outcome, activation, pause, withdrawal, cancellation, material delivery exception and completion. Detailed transport and access evidence remains in Callback. +- Transient transport failures receive two bounded application retries, for three attempts total, within the original window. The application never retries outside that window. +- A permanent failure pauses future contacts and creates a same-day operational task. It never automatically triggers patient contact or clinical review. +- Provider outage contacts that miss their window are marked missed and never sent late. Future cadence remains unchanged after restoration. +- Active plans keep immutable pathway and message snapshots. An urgent safety retirement pauses affected future contacts for explicit review; ordinary version updates do not rewrite them. + +### Governance, identity, privacy and reporting + +- A clinical programme lead and a lived-experience/content representative both approve new or materially changed pathway/message versions. Privacy or legal review joins when disclosure or agreement changes. +- The pilot proves operational safety, reliability, clinician usability and patient acceptability. It is not a clinical-effectiveness study. +- Patient acceptability uses a separately consented evaluation process outside Callback, with aggregate reporting only. +- WA Health enterprise SSO/MFA and service-managed team groups control access. No Callback-local credentials exist. +- Any device may access Callback only when WA Health SSO/MFA, conditional access and managed-session controls succeed. No patient download or persistent browser-local patient storage is permitted. +- Enterprise policy controls session timeout. Activation, withdrawal, reassignment and any allowed export require fresh authentication. +- The pilot permits no patient-level export. Approved aggregate reporting may include imported clinical-source demographic fields with a governance-configured small-cell threshold and a non-inferable `Suppressed` state. +- Every patient search, view, decision, mutation, write-back and administrative access enters an immutable audit trail. Clinicians see episode-relevant operational history; privacy/security auditors see the complete access trail. +- Today activity shows patient name, exact action, clinician and time to authorised team members, but never phone number, message text or clinical details. +- External WA Health email or managed-push alerts contain no patient identifiers and require authentication. During staffed hours, referrals must be reviewed before their first eligible window, permanent delivery exceptions receive same-day review and unclaimed work escalates to the team lead after 60 minutes. + +### Hosting, incident and pilot controls + +- Real-patient hosting uses a separately contracted PHI-capable Australian-region cloud environment, not the current Clinical KB deployment. +- Identifiers, message content, application data, backups, logs and SMS-provider processing remain in Australia. Overseas support access requires explicit approval and auditing. +- Vendors may manage encryption only through dedicated tenant-scoped Australian-region keys with documented rotation and contractually reviewable privileged access. +- Any confirmed wrong-recipient message, duplicate send, unauthorised content, material privacy/security incident or loss of audit integrity immediately pauses the entire pilot. +- Restart requires joint approval from the incident lead, privacy/security owner and clinical programme lead after reconciliation and remediation. +- Downtime fails closed: no offline patient cache, no new activation and no uncertain send. Staff use the approved service downtime process and reconcile before resuming. +- Provider webhooks are the normal transport-status source. Staff perform manual provider reconciliation when an outage, discrepancy or suspected incident occurs; there is no routine daily full reconciliation. This is a conscious residual risk requiring provider, service and security acceptance before pilot. +- The single-team pilot has no numeric patient cap and accepts every eligible referral while open. This is a conscious exposure choice; strict automatic stopping rules, workload monitoring and the 6–8-week early governance review are mandatory. +- Production access requires assessed simulation of identity review, activation, withdrawal, delivery failure, readmission, downtime and incident handling. +- Lived-experience approval is required at message-content, complete-prototype and pilot-findings gates, and may block progression. +- Go-live receives two weeks of seven-day hypercare with named clinical, service, technical, privacy and incident leads plus daily Callback queue/state review. Provider-side reconciliation remains event-triggered by outage, discrepancy or suspected incident. +- Rollout remains sequential: approved design specification → complete synthetic prototype → secure datastore/tenancy → fake-provider simulation → authorised non-production provider → staged real-patient pilot. + +### Approved visual direction + +- **Today:** guided command centre with `Referrals to review`, distinct `Needs action` and today's sending-window panels, recent activity, then quiet metrics. +- **Activation:** guided split with persistent patient identity, focused stage content and live exact-message preview; phone uses a labelled preview sheet. +- **Continuity:** widening horizontal thread whose close early nodes spread across twelve months, followed immediately by the complete chronological text/list equivalent. +- **Phone navigation:** four-item dock — Today, Patients, Schedule and More. Templates, Team, Guidance and Reports live in the More sheet. Desktop exposes all five primary areas directly. +- **Schedule:** day-led split with a seven-day strip, named exceptions separated from ordinary sending-window lists, and a secondary week inspection. +- **Patient results:** identity-forward action rows with minimum distinguishing identifiers and a separate identity-confirmation step. +- **Delivery exceptions:** contextual desktop resolution drawer; the same content becomes a full-screen phone sheet. +- **Final activation:** sectioned assurance review showing identity, source eligibility, agreement, ownership, exact message, cadence and one-way boundaries together before fresh authentication. + +## 1. Outcome and recommended direction + +Build Callback as a **dedicated caring-contact workspace inside this codebase, with a separate operational shell and a separately approved runtime/data boundary**. + +That choice deliberately separates three things: + +1. **Shared design language** — tokens, typography, controls, overlays, focus behaviour, accessibility, responsive states, iconography and quality gates come from Clinical KB. +2. **Dedicated product navigation** — Today, Patients, Schedule, Templates and More belong to caring-contact coordination, not to the global search composer or the thirteen reference modes. +3. **Patient-data boundary** — the current product and PIA assume no solicited patient-identifiable data. Callback cannot silently widen that assumption by adding a route to the existing RAG deployment. + +The memorable product signature is one restrained **continuity thread**: close early nodes that widen across the approved cadence. It is a schedule and continuity device only. It never changes colour or geometry based on clinical state, inferred risk, delivery success or patient behaviour. + +## 2. Evidence-backed repository design audit + +### 2.1 Source-of-truth order + +Use the repository's declared order, not historical preference: + +1. `AGENTS.md` — execution, UI, search-chrome, wiring, verification and clinical-governance rules. +2. `src/app/ckb-v2-tokens.css` — authoritative v2 roles and values. +3. Committed design-system and browser tests. +4. `.design-sync/conventions.md`. +5. `docs/design-system/README.md`, `SPEC.md`, `TOKENS.md`, `COMPONENTS.md`, `DECISIONS.md`, `GATES.md` and `ADOPTION.md`. + +`docs/design-system.md` is a transitional description, not the current specification. The older `docs/redesign/*` material remains useful rationale, but the `docs/design-system/` set wins when they disagree. `docs/design-system/HANDOVER-2026-08-07.md` is explicitly superseded and must not scope this work. + +### 2.2 Current visual language to inherit + +- True-white clinical canvas with quiet graphite text and chrome. +- Graphite `--command` for the one dominant primary action in a surface. +- Clinical Sky `--clinical-accent` for clinical identity, selected state, focus and the continuity-thread signature. +- Green, amber and red reserved for exact semantic status; never decoration, identity or charts. +- Geist typography, sentence case, Australian English, tabular numerals and typography-led hierarchy. +- Four-pixel spacing rhythm; v2 semantic size, radius and elevation tokens; one edge owner per surface. +- Flat light-mode surfaces with hairline separation and restrained elevation. Glass/blur only where the overlay contract already allows it. +- Sparse cards. Prefer headings, spacing, dividers and rows before adding another panel. +- Five responsive layout states: `compact`, `stacked`, `rail`, `split` and `wide`. +- Page titles wrap; one `

` is owned by `PageHeader`; actions yield before the title does. +- Degraded and exception states say what happened, what it means and what action is available. + +### 2.3 Rendered evidence used + +The repository's six canonical Linux baselines are human-approved hosted-CI artifacts recorded in `tests/__screenshots__/linux/provenance.json` and governed by `docs/design-system/adoption-contract.json`: + +| Rendered surface | Evidence file | Callback lesson | +| ------------------------ | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| Dashboard shell, desktop | `tests/__screenshots__/linux/dashboard-shell.png` | Quiet centred hierarchy, one obvious command and minimal chrome. | +| Dashboard shell, phone | `tests/__screenshots__/linux/dashboard-shell-phone.png` | Compact header, large reachable action and safe-area discipline. | +| Results band, desktop | `tests/__screenshots__/linux/search-results-band.png` | Dense operational metadata belongs in calm rows, not KPI cards. | +| Results band, phone | `tests/__screenshots__/linux/search-results-band-phone.png` | Controls collapse to explicit actions instead of shrinking unreadably. | +| Document viewer | `tests/__screenshots__/linux/document-viewer.png` | Split-pane inspection, progressive disclosure, provenance and action hierarchy. | +| Therapy Compass home | `tests/__screenshots__/linux/therapy-compass-home.png` | Repository-native workflow launcher with a restrained choice set and plain clinical copy. | + +The local app identity was also confirmed through `/api/local-project-id` as `Clinical KB`. The verified server started at the repository-selected port; no assumed localhost port is part of this plan. + +### 2.4 Reuse map + +| Need | Reuse | Rule | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Global theme, fonts, CSP, auth context, announcements, overlay root | `src/app/layout.tsx` | Keep the root layout; do not duplicate providers or create a second overlay root. | +| Tokens | `src/app/ckb-v2-tokens.css`, `src/app/globals.css` | No raw colour, pixel, z-index, duration, radius or shadow values in production components. | +| Actions | `src/components/ui/button.tsx`, design-system `IconButton` | One filled command action per region; verb-first labels. | +| Fields and errors | `FormField`, `TextField`, `SearchField`, `Select`, `Checkbox`, `RadioGroup`, `ErrorSummary` | Labels are persistent; errors are field-connected; autocomplete semantics are reviewed for patient fields. | +| Modal decisions and phone sheets | `src/components/ui/sheet.tsx`, `src/components/ui/confirm-dialog.tsx`, `OverlayRoot` | Desktop dialog/drawer and phone bottom-sheet/full-screen treatment share one focus-safe primitive. | +| Tabs and view choices | `Tabs`, `SegmentedControl` | Tabs change panels; segmented controls change sort/density/view. | +| Status and tags | `Chip`, `InlineNotice`, `StatusMark` where status is the content | Text is the primary status channel; colour and shape are secondary. Create domain vocabularies rather than reusing source-governance status. | +| Page structure | `PageHeader`, `Breadcrumb`, `PanelHeading`, `Disclosure`, `AccessibleTable` | Titles never truncate; compact tables become labelled cards. | +| Feedback | `ToastProvider`, `LiveAnnouncer`, `EmptyState`, `ErrorState`, `LoadingPanel`, `Skeleton` | Toasts contain no patient details. A spinner is never a terminal state. | +| Dates | `DateDisplay` | ISO in, `en-AU`/Perth out; relative dates are secondary. | +| Launcher entry | `src/lib/tools-catalog.ts`, `/tools` | Add one `coordination` destination after production-route approval. Do not add a searchable `AppModeId`. | +| Route reachability | `docs/wiring-conventions.md`, `tests/route-reachability.test.ts` | Every production route has a real inbound path or a documented, tested exception. | + +### 2.5 Components not to extend as Callback foundations + +- `GlobalSearchShell`, `MasterSearchHeader` and the shared composer: Callback is patient-first, not query-first. +- `app-modes.ts`: every current mode declares a search contract; caring-contact coordination is not a search result surface. +- `patient-profile-storage.ts`: browser-local reference context is not an acceptable patient-plan datastore. +- `AsyncButton`: deprecated; use `Button` busy state. +- `FilterBar` and `DataTable`: retired names; use surface-owned filters and `AccessibleTable`. +- Legacy teal, blue-corporate, warm-porcelain, pure-monochrome, glass-heavy and generic-SaaS directions rejected in `docs/redesign/permanent-colour-direction.md`. +- Mockup-specific raw colour or layout exceptions. Promotion to production requires full token adoption. + +### 2.6 Repository maturity conflict that changes the rollout + +The existing product explicitly tells clinicians not to enter patient-identifiable information. Its current persistence is individual-owner scoped, and its PIA describes Clinical KB as a knowledge base rather than a patient record. Callback requires deliberate identity confirmation, mobile details, agreement, team ownership and longitudinal communication history. Therefore: + +- Design work can proceed in this repository with synthetic data. +- Production code can proceed only behind a non-production feature boundary. +- Real-patient use cannot proceed on the current deployment assumptions without a new privacy, security, records, tenancy and hosting decision. +- The current Railway/OpenAI RAG route is irrelevant to message generation and must not receive Callback data. + +## 3. WA clinical, service and evidence grounding + +### 3.1 What the authoritative sources support + +- The WA Mental Health Commission announced an Aftercare Services Program on 15 June 2026 providing brief interventions, psychosocial support and care coordination for people discharged after a suicidal crisis. This supports the cohort and coordination context, not a particular SMS workflow: [Aftercare Services launched](https://www.mhc.wa.gov.au/news-and-resources/latest-news/aftercare-services-launched). +- WA guidance emphasises direct, coordinated post-discharge follow-up, documented clinician responsibility, collaborative discharge planning and local protocols. Callback must remain additive to these arrangements, never their substitute: [Principles and Best Practice for the Care of People Who May Be Suicidal](https://www.health.wa.gov.au/-/media/Files/Corporate/general-documents/Mental-health/PDF/Best-Practice-for-the-Care-of-People-Who-May-Be-Suicidal.pdf). +- NSQHS Action 5.32 requires follow-up arrangements to be developed, communicated and implemented. Callback may coordinate one bounded element of an approved follow-up plan but cannot claim to satisfy the standard by itself: [Comprehensive Care Standard](https://www.safetyandquality.gov.au/national-standards/nsqhs-standards/comprehensive-care-standard). +- EMHS distinguishes clinically relevant community follow-up from administrative contact. Caring-contact delivery must not be counted as clinical follow-up unless the health service's measure owner explicitly defines it that way: [Community follow-up within seven days](https://emhs.health.wa.gov.au/Patient-Care/Safety-and-Quality/Mental-Health/Community-Follow-Up). +- WA Health's consent policy requires collaborative, informed decision-making and consistent documentation. Local governance must decide how the caring-contact agreement maps to treatment consent, communication preference and the clinical record: [Consent to Treatment Policy](https://www.health.wa.gov.au/About-us/Policy-frameworks/Clinical-Governance-Safety-and-Quality/Mandatory-requirements/Consent-to-Treatment-Policy). +- WA Health's Digital Health and Information Security policies make consumer consent, privacy, cyber security, confidentiality, integrity and availability mandatory design inputs: [Digital Health Policy Framework](https://www.health.wa.gov.au/about-us/policy-frameworks/digital-health), [Information Security Policy](https://www.health.wa.gov.au/about-us/policy-frameworks/digital-health/mandatory-requirements/information-security-policy). + +### 3.2 What the research does not establish + +The illustrative cadence comes from a 12-month trial of 11 nondemanding messages in a US military population. Primary outcomes were not significant, while some secondary outcomes favoured the intervention. The protocol monitored replies and was adapted when participants found unresponsive messaging mechanical. The research therefore does **not** validate an unmonitored one-way WA hospital service, the exact cadence, or its channel notice: [Comtois et al. randomised clinical trial](https://pmc.ncbi.nlm.nih.gov/articles/PMC6495345/). + +A systematic review found mixed estimates across outcomes and time points, with a protective one-year estimate for attempts but uncertainty for mortality and emergency presentations/hospitalisation. The product must describe caring contacts as a locally governed service intervention with an evidence base that remains heterogeneous, not as a proven universal suicide-prevention mechanism: [Caring Contacts systematic review and meta-analysis](https://pubmed.ncbi.nlm.nih.gov/35420858/). + +### 3.3 Clinical-language guardrail + +Every design and implementation review must reject copy or visuals that imply: + +- a patient is safe because a message was delivered; +- failure to deliver is a clinical deterioration signal; +- non-response or reply content has been interpreted; +- a pathway is a prescription for all patients; +- the application is monitoring the patient; +- automated contact replaces active follow-up, safety planning, review or emergency care; +- a team has transferred duty of care merely by reassigning a plan in software. + +## 4. Repository-native approaches considered + +### Approach A — add Callback as another shared search mode + +**Shape:** Extend `app-modes.ts`, `GlobalSearchShell` and the shared composer. + +**Benefits:** Lowest shell work; automatic access to mode navigation and search chrome. + +**Costs:** The query-first shell conflicts with patient-first enrolment; it would force a false search contract, invite patient details into the global composer, blur the PHI boundary and make Today/Patients/Schedule/Templates secondary. + +**Disposition:** Reject. + +### Approach B — dedicated route group and operational shell in this repository + +**Shape:** Add `/caring-contacts/**` outside `(search-app)`, inherit the root v2 layer and primitives, launch it from Tools, and use a dedicated five-destination shell. Keep patient APIs and delivery services isolated from RAG and approved separately. + +**Benefits:** Native visual maturity without semantic compromise; clean patient-data and navigation boundaries; direct reuse of accessibility, overlays, tokens and testing; one codebase for maintainers. + +**Costs:** Requires a new shell, team tenancy model, operational routes and an explicit production-data architecture. + +**Disposition:** Recommend. + +### Approach C — separate application repository with copied design assets + +**Shape:** Build Callback independently and manually mirror Clinical KB tokens/components. + +**Benefits:** Strongest operational and deployment isolation. + +**Costs:** Immediate design-system drift, duplicated accessibility work, slower visual convergence and a second governance/tooling estate. + +**Disposition:** Reserve for a future organisational decision if WA hosting, ownership or procurement requires full code isolation. Do not choose it merely for visual separation. + +## 5. Information architecture and route plan + +### 5.1 Primary navigation + +1. **Today** — referrals awaiting review, named exceptions, today's scheduled contacts and recent team activity. +2. **Patients** — patient search, enrolled patients and caring-contact history. +3. **Schedule** — team day/list default with week inspection. +4. **Templates** — approved pathways and message variants. +5. **More** — active team, handover, guidance, help and role-appropriate reporting. + +No Inbox, Messages or Conversations destination exists in the initial product. + +Today keeps the approved action-first order on every viewport: `Referrals to review`, named operational exceptions, today's scheduled contacts, recent team activity, then quiet aggregate service metrics. Responsive layouts may change presentation, but must not promote reporting above work that requires action. + +### 5.2 Production route inventory + +| Route | Responsibility | Primary layout state | +| ---------------------------- | ----------------------------------------------------------- | ------------------------------------------------- | +| `/caring-contacts` | Today dashboard | compact cards; wide action/list split | +| `/caring-contacts/patients` | Pilot-team referral and caring-contact episode search | stacked/rail | +| `/patients/[patientId]` | Patient identity, agreement, plan history and current plan | stacked/split | +| `/plans/new` | Four-stage activation for one accepted referral | compact full-stage; wide stepper + live preview | +| `/plans/[planId]` | Plan detail, future schedule, ownership and audited actions | stacked/split | +| `/caring-contacts/schedule` | Team day/list and week inspection | compact list; wide rail/split | +| `/contacts/[contactId]` | Contact detail and exact delivery state | full page on phone; drawer-capable detail on wide | +| `/caring-contacts/templates` | Approved pathways and message variants | stacked/rail | +| `/templates/[pathwayId]` | Cadence, version, governance and variant preview | stacked/split | +| `/caring-contacts/team` | Active context, team ownership and handover | stacked/rail | +| `/caring-contacts/guidance` | Programme boundaries and contextual help | readable wide/rail | +| `/caring-contacts/reports` | Privacy-conscious aggregate operations | stacked/wide | + +The `More` control opens a navigation sheet on compact layouts and exposes direct links on wide layouts. Patient search is scoped to the pilot team's received referrals and retained Callback episodes. It never queries the hospital-wide directory directly and never writes to browser search history or the Clinical KB search store. + +### 5.3 Overlay and full-screen decision inventory + +Use `Sheet`/`ConfirmDialog`; on phone, promote clinical decisions to a bottom sheet or dedicated full-screen stage: + +1. Verify patient identity. +2. Change patient confirmation. +3. Pathway preview. +4. Message-variant preview. +5. Add optional communication preference. +6. Adjust date/time within policy. +7. Outside permitted contact-time warning. +8. Save draft. +9. Discard unsaved changes. +10. Final activation confirmation. +11. Activation success. +12. Pause remaining contacts. +13. Record withdrawal. +14. Reassign coordinator. +15. Delivery-status detail. +16. Resolve failed delivery. +17. Contact-details-changed block. +18. Template changed or retired. +19. Session-expiry warning. +20. Offline/connection banner. +21. Recoverable error. +22. Permission unavailable. +23. Active-team switcher. +24. Draft/version conflict. + +## 6. Core workflows and safety behaviour + +### 6.1 Review a referral and start a plan + +1. The hospital referral workflow sends a structured referral containing the approved minimum identity, discharge, mobile-source and agreement fields. +2. Today lists the referral under `Referrals to review`, ordered by first eligible contact-window timing without risk ranking. +3. An authorised aftercare clinician chooses `Accept`, `Return for clarification` or `Decline` with a structured reason. The latter two outcomes write back to the source workflow. +4. Until acceptance, the referring team remains responsible and Callback displays `Awaiting handover`; it never shows an aftercare owner. +5. Acceptance moves the referral to the team queue. A coordinator explicitly claims it or a team lead assigns it before activation. +6. Search and selection reveal only the minimum identifiers needed to distinguish this team's referrals and episodes. +7. A deliberate identity-confirmation step repeats the selected referral, source identifiers, patient-controlled-mobile flag and imported agreement status. +8. The selected patient persists as a compact identity header through every stage, with an explicit `Change patient` confirmation. +9. Missing objective eligibility, source-controlled mobile evidence, agreement, owning team or coordinator blocks pathway selection with a named resolution path. + +### 6.2 Choose a pathway + +- Show only current, locally approved versions available to the active team. +- Preview duration, exact cadence, sender, one-way boundary, example tone and approval owner. +- Do not rank pathways, calculate fit or mark one as `best`. +- The clinician makes and owns the selection. + +### 6.3 Personalise + +- Select approved, warm, non-demanding variants. +- Permit only governed substitutions such as preferred name, discreet team display name, coordinator signature and selection among approved message variants. +- Render the exact patient-visible SMS, GSM-7/Unicode segment count and sender identity. +- Block any fully substituted message over two concatenated SMS segments. +- Show the selected `Morning`, `Afternoon` or `Early evening` window and its exact 10:00 am, 2:00 pm or 5:00 pm AWST send time. +- Show the full continuity thread and readable date list. +- Deterministically block missing mandatory wording, reply invitations, appointment/task language, clinical advice and prohibited placeholders. +- Preserve entered work across recoverable errors and session-warning recovery. + +### 6.4 Review and activate + +- Repeat patient identifiers, the imported mobile and source, patient-controlled/discreet-SMS suitability evidence, + agreement, owning team and coordinator. Do not claim separate destination verification or reverification. +- Show exact text, dates, send times, timezone, pathway version, two-segment evidence and one-way/no-monitoring notice. +- Detect stale drafts, template retirement, patient detail changes and competing activation. +- Require fresh WA Health authentication for the object-specific final action: `Activate 10-contact plan`. +- Activation atomically creates immutable contact snapshots and one audit event; retrying the request must not duplicate a plan or contact. + +### 6.5 Manage an active plan + +- Future contacts can be adjusted only within policy and with an audited reason. +- Pause is reversible; withdrawal is a patient preference and terminal for future contact; cancellation is an authorised operational action with a reason. +- Pausing never moves dates; contacts inside the pause are skipped permanently, and explicit resumption begins with the next future scheduled contact. +- Withdrawal immediately cancels every unsent contact and may be recorded by any authorised teammate who receives the request through the staffed programme phone. +- Reassignment changes the coordinator, not the owning team's history. +- A source mobile-number change or hospital readmission automatically pauses future contacts for review. +- A recorded death irreversibly cancels every unsent contact; a corrected source event remains an incident and any future episode requires a new referral. +- A later discharge after readmission requires a new linked referral and never automatically resumes or rebases the earlier episode. +- A contact already claimed by the dispatcher shows `Processing — too late to change` and cannot be silently cancelled. +- Contact-detail changes pause future sends until updated source-system mobile, patient-controlled + and discreet-SMS-suitability evidence is imported and reviewed; no test SMS, read-back or Callback + attestation is added. +- A retired template never silently edits activated message snapshots. Governance defines whether affected future contacts continue, pause for review or require a replacement pathway. + +### 6.6 Delivery exception + +- Delivery states remain transport states: Scheduled → Processing → Sent → Delivered. +- Exact exception states: Not delivered, Number invalid, Contact changed, Status unavailable. +- Each exception names the operational action and owner. +- Apply at most two bounded retries—three attempts total—inside the original sending window; never expose `Retry` as an unbounded send button. +- A permanent failure pauses future contacts and creates a same-day operational task until details are reviewed. +- Attempted replies are never rendered as a conversation. The approved technical path must either prevent inbound SMS or send the approved automatic channel notice. Raw reply content is not analysed, triaged or displayed. +- The selected production path uses a non-receiving sender. A transport failure creates no inferred clinical alert or automatic patient call. +- Provider-outage contacts that miss their approved window are recorded as missed and never sent late. + +## 7. State and invariant model + +### 7.1 Plan lifecycle + +`Draft → Active → Paused → Active → Completed` + +Alternative terminal states: `Withdrawn` and `Cancelled`. + +Referral lifecycle: `Received → Awaiting review → Accepted | Clarification requested | Declined`. + +Invariants: + +- Completed, withdrawn and cancelled plans cannot schedule new contacts. +- Withdrawal cancels every unclaimed future contact in the same transaction. +- Pausing does not alter historical delivery events or rebase future dates; contacts inside the pause become skipped. +- Reactivation cannot recreate already-sent contacts. +- A duplicate referral cannot create a second active plan. +- Readmission pauses; death cancels irreversibly; either event records its source-system provenance. +- Every state change records actor, active team, timestamp, previous state, next state and reason code. + +### 7.2 Contact lifecycle + +`Scheduled → Processing → Sent → Delivered` + +Exception/terminal transitions: + +- Scheduled → Cancelled because plan paused, withdrawn or cancelled. +- Processing → Sent or Not delivered. +- Sent → Delivered, Not delivered or Status unavailable. +- No transition leaves Delivered for a clinical state. + +### 7.3 Template lifecycle + +`Draft → Approved → Retired` + +- Approval produces an immutable version. +- A pathway references exact approved message-variant versions. +- Activation snapshots exact text and schedule policy. +- Retirement blocks new selection and creates a named review task for affected drafts; it never rewrites active history. + +## 8. Patient data, privacy, security and records architecture + +### 8.1 Data minimisation + +Store only what coordination requires: + +- external patient identifier and a minimal identity snapshot for deliberate confirmation; +- imported mobile number, source-system provenance and explicit patient-controlled/suitable-for-SMS flag; +- preferred name and explicitly selected communication preferences; +- imported agreement boolean plus source referral, referring clinician and received timestamp; +- owning team and coordinator identifiers; +- approved pathway/version, exact scheduled contacts and exact message snapshots; +- delivery metadata, operational exception codes and append-only audit events. + +Do not store free-text suicide-risk assessments, prediction features, clinical notes, diagnosis narratives, message-reply interpretation, engagement scores or copied EMR content. + +### 8.2 Runtime boundary + +- Keep `/caring-contacts/**` out of service-worker content caching and browser-local persistence containing PHI. +- Use authenticated server-side access and short-lived responses with `Cache-Control: no-store` for patient routes/APIs. +- Prevent patient identifiers in URLs where a stable opaque plan/patient reference can be used. +- Redact identifiers and phone numbers from application logs, error reports, analytics, metrics labels and webhook diagnostics. +- Never put patient details in toast titles, browser notifications, document titles, telemetry breadcrumbs or screenshot fixtures. +- Disable RAG/OpenAI calls for the entire route group by architecture, not by convention. +- Use WA Health enterprise SSO/MFA, conditional access and managed-session controls on every device. Follow the enterprise idle-expiry policy and require explicit reauthentication for activation, withdrawal, reassignment and export. +- Do not allow patient-level downloads, offline patient caches or persistent browser-local patient data. + +### 8.3 Team tenancy and roles + +The current individual `owner_id` model is insufficient. The approved datastore must represent: + +- `team_member` — view team plans and perform ordinary coordination permitted by role. +- `coordinator` — create, personalise, activate and manage assigned plans. +- `team_lead` — reassign coordinators and approve exceptional operational actions. +- `template_governor` — approve/retire pathways and message variants; cannot gain patient access solely from this role. +- `report_viewer` — access authorised aggregates with small-cell suppression; no patient detail by default. +- `system_operator` — delivery operations without unnecessary clinical identity fields. + +Every read and mutation is team-scoped and deny-by-default. Service-role access is not a substitute for RLS/authorisation. Cross-team switching clears cached patient state before the new context renders. + +### 8.4 Record of truth + +Callback is not the clinical record. The design requires structured milestone write-back while detailed transport and access evidence remains in Callback. Before pilot, the service must approve: + +- which system is authoritative for patient identity and mobile details; +- where agreement, activation, pause, withdrawal, exception resolution and plan completion are recorded clinically; +- the structured referral-outcome, activation, pause, withdrawal, cancellation, material delivery-exception and completion events written to the approved clinical record service; +- retention, deletion, legal hold, audit access and patient-access/correction processes; +- how corrected/deceased/contact-changed patient states stop future messages. + +### 8.5 Delivery architecture + +Use a transactional outbox with lease-fenced dispatch: + +1. Activation writes plan, contacts, message snapshots and audit event atomically. +2. The dispatcher claims due contacts with a lease and unique provider idempotency key. +3. The provider adapter sends only the approved snapshot. +4. A signature-verified, replay-protected webhook records transport events idempotently. +5. Timeout/retry logic cannot create a second send. +6. Pause/withdrawal/cancellation races are resolved against the lease before provider submission. +7. Every provider payload and log is minimised; secrets remain in the approved secret store. + +Provider webhooks are the routine transport-status source. An outage, discrepancy or suspected incident initiates manual reconciliation against provider records; uncertain contacts are never resent automatically. The absence of scheduled daily reconciliation is an explicit residual risk and pilot approval item. + +The adapter interface is implemented first with a deterministic fake provider. A real provider adapter, webhook endpoint, credentials or live call requires explicit provider approval and security/procurement evidence. + +## 9. Governance decision register + +These are not questions about the product model; they are launch gates whose answer must come from the named local owner. + +| Decision | Accountable owner | Evidence required before pilot | Safe default while unresolved | +| ------------------------------------------------------------------------------------ | -------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------- | +| Participating hospital/service and cohort eligibility | Service clinical governance | Approved protocol and inclusion/exclusion rules | Synthetic-only design/build | +| Whether verbal caring-contact agreement is consent, communication preference or both | Clinical governance + legal/privacy | Approved wording and documentation procedure | Label `Agreement confirmed`; no real enrolment | +| Patient identity/mobile source of truth | Health information/integration owner | Interface contract and correction workflow | Fictional directory only | +| Clinical-record write-back | Records owner + service | Approved event set and filing responsibility | Downloadable fictional summary only | +| One-way inbound-reply handling | Clinical governance + lived-experience + SMS owner | Approved non-receiving sender, no-reply wording and support route | Fake provider; no inbound UI | +| Exact pathway cadence and contact windows | Service governance | Versioned pathway using discharge anchor and 10:00/14:00/17:00 AWST | Illustrative twelve-month pathway label | +| Message library and prohibited content | Clinical governance + lived-experience | Approved variants and revision owner | Fictional governed variants | +| Team membership and role model | Service + identity/security owner | Role matrix, joiner/mover/leaver process | Local synthetic roles | +| Hosting, residency, privacy notices and breach response | Privacy + security + legal | Australian-only data flow, tenant-key, PIA, contracts and incident proof | No PHI deployment | +| SMS provider and delivery-status semantics | Procurement + security + service owner | Contract, data-flow review, status mapping and cost approval | Provider-neutral fake | +| Reporting and small-cell suppression | Governance + analytics owner | Imported-field dictionary, configured threshold and access rules | Operational fixture aggregates only | +| Pilot evaluation | Clinical governance + evaluation/lived-experience | Operational-safety/acceptability protocol and independent feedback route | No effectiveness claims | + +## 10. Reusable component plan + +### 10.1 New domain components + +Create domain components under `src/components/caring-contacts/`; promote only genuinely general patterns to `src/components/ui/` through the design-system authoring contract. + +- `caring-contact-shell.tsx` — desktop rail, tablet collapsed rail, phone header/bottom navigation and active-team context. +- `clinical-context-switcher.tsx` — active hospital/service/team, with context-clear behaviour. +- `patient-identity-header.tsx` — minimum identifiers, source-state and change action; do not claim patient verification beyond imported evidence. +- `identity-assurance-checklist.tsx` — deliberate identity confirmation. +- `communication-eligibility-panel.tsx` — mobile, agreement and optional preferences. +- `plan-summary.tsx` — lifecycle, team, coordinator, pathway and next contact. +- `continuity-thread.tsx` — schedule geometry plus accessible ordered-list fallback. +- `activation-stepper.tsx` — four-stage journey with current/completed/error semantics. +- `approved-pathway-card.tsx` and `pathway-selector.tsx`. +- `message-variant-selector.tsx`. +- `sms-preview.tsx` and `sms-segment-count.tsx`. +- `one-way-boundary-notice.tsx`. +- `review-summary.tsx`. +- `plan-state-chip.tsx` and `contact-state-chip.tsx` with closed vocabularies. +- `action-required-row.tsx` and compact card treatment. +- `team-schedule-row.tsx` and labelled compact card. +- `delivery-history.tsx` and `audit-event-list.tsx`. +- `coordinator-selector.tsx`. +- `programme-metric.tsx` for quiet aggregates only; no clinician ranking. + +### 10.2 Approved composition contracts + +- `caring-contact-shell.tsx` exposes all five desktop areas. Compact navigation contains Today, Patients, Schedule and More; the More sheet contains Templates, Team, Guidance and Reports. +- Today uses the guided command-centre composition: referral command, separate action and sending-window panels, recent team activity, then quiet metrics. +- Activation uses a wide split between the current decision and live patient-visible preview. Compact mode keeps identity in flow and opens the preview in a labelled sheet. +- `continuity-thread.tsx` uses close early nodes that widen across the year; a complete chronological list immediately follows and remains the accessible source of truth. +- Schedule defaults to one day with a seven-day strip. Named exceptions remain visually separate from routine morning, afternoon and early-evening lists. +- Patient results use identity-forward rows, not a dense table or card grid. Selection always leads to a distinct identity-assurance step. +- Delivery exception inspection uses a right drawer on wide layouts and full-screen sheet on compact layouts. +- Final activation uses sectioned assurance review and places fresh-authentication activation after the entire review content. + +### 10.3 Component-state specimen requirements + +Each new component specimen covers: + +- default, hover, active, focus-visible, disabled, busy and invalid where operable; +- loading, empty, no-results, offline, partial-data and recoverable-error states; +- long patient names, long service names, 200% text and narrow 320px width; +- dark, forced-colour and reduced-motion modes; +- full keyboard path and screen-reader name/role/state; +- print applicability explicitly stated; +- fictional data only. + +## 11. Responsive behaviour + +### 11.1 Widths to design and verify + +Design explicitly at 320, 390, 430, 768, 1024 and 1440 CSS pixels. Verify 400% zoom at 1280px as equivalent narrow reflow. + +### 11.2 Shell behaviour + +- **320–430 compact:** repository-native phone header, four-item bottom navigation, no persistent side rail. Today begins with `Referrals to review`; Templates, Team, Guidance and Reports live in the More sheet. Identity and current activation stage remain visible in flow without becoming a second fixed header. +- **768 rail:** collapsed desktop navigation and supporting rail. This is not an enlarged phone layout. +- **1024 split:** list/detail and workflow/preview pairs appear when both panes preserve minimum viable widths. +- **1440 wide:** persistent navigation and context with comparison-friendly plan/schedule layouts. + +### 11.3 Overlay behaviour + +- Short reversible decisions: centred desktop dialog, bottom phone sheet. +- Inspection: desktop right drawer, phone dedicated screen or full-height sheet. +- Identity, withdrawal, activation and conflict resolution: dedicated full-screen stage on phone. +- Sticky actions never cover validation, identity, banners, keyboard focus or safe-area navigation. + +### 11.4 Continuity thread + +- Visual nodes show spacing over time, not clinical importance. +- Early nodes are close together and later nodes widen across the twelve-month cadence; this geometry never changes for patient, delivery or clinical state. +- The accessible name is `Caring-contact schedule` and the DOM contains a complete ordered list of dates/messages. +- Compact mode may present a short horizontal overview, but the complete vertical list follows and cannot omit dates or transport-state labels. +- Forced colours use system strokes and text; reduced motion removes path-drawing animation. +- Print shows the exact date list; the decorative line is optional. + +## 12. Screen-generation sequence + +Follow this exact order so the shell and product signature stabilise before edge pages multiply: + +1. Foundation board: tokens, type, iconography, status vocabularies, controls, responsive shell and continuity thread. +2. Today — desktop, phone, tablet risk check. +3. Patient and agreement — desktop and phone. +4. Pathway selection — desktop and phone. +5. Personalisation with exact SMS preview — desktop and phone. +6. Review and activation — desktop and phone. +7. Patient overview — desktop and phone. +8. Schedule — desktop and phone. +9. Patient boundary and exception screens. +10. Template/pathway library and version states. +11. Delivery exception and resolution. +12. Team context, reassignment and handover. +13. Guidance, help and authorised reporting. +14. All 24 overlays/states. +15. Complete component specimens. +16. Dark, forced-colour, reduced-motion and 320px review boards. + +## 13. Delivery programme + +### Phase 1 — repository grounding + +**Deliverable:** approved evidence-backed repository audit and design direction. No application code. + +**Files** + +- Create: `docs/superpowers/specs/2026-08-14-caring-contact-coordination-design.md` — binding product/design specification. +- Create: `docs/caring-contacts/repository-design-audit.md` — source, route, component and rendered-evidence map. +- Create: `docs/caring-contacts/clinical-boundaries.md` — exact claims, prohibited language and service boundary. +- Create: `docs/caring-contacts/governance-decisions.md` — decision register with owners/evidence/status. + +**Steps** + +- [ ] Re-verify `origin/main`, the design-system source ranking and canonical visual provenance at execution time. +- [ ] Record the three approaches in §4 and retain Approach B unless repository or governance evidence materially changes. +- [ ] Trace every proposed component to a current primitive, a new domain component or a justified future design-system promotion. +- [ ] Map every confirmed product decision to a screen, component, state or governance rule. +- [ ] Review the design spec for placeholders, contradictions, reply/inbox drift, risk-language drift and PHI leakage. +- [ ] Run `npm run format:check -- docs/superpowers/specs/2026-08-14-caring-contact-coordination-design.md docs/caring-contacts/*.md`; expect Prettier success. +- [ ] Run `npm run docs:check-links`; expect no broken local or external documentation link. +- [ ] Present the written spec for explicit approval before mockups. + +**Exit gate:** product owner approves the repository-native direction; clinical/governance owners accept the boundary language as suitable for design exploration, not clinical approval. + +### Phase 2 — concept foundation + +**Deliverable:** responsive mockup foundation, complete IA and the continuity-thread signature. + +**Files** + +- Create: `src/app/mockups/caring-contacts/page.tsx` — noindexed visual-suite entry. +- Create: `src/components/caring-contacts/mockups/foundation-board.tsx`. +- Create: `src/components/caring-contacts/mockups/caring-contact-shell-frame.tsx`. +- Create: `src/components/caring-contacts/mockups/continuity-thread-specimen.tsx`. +- Create: `src/components/caring-contacts/mockups/fixtures.ts` — obviously fictional patients, teams, pathways and events. +- Create: `tests/caring-contact-mockups.dom.test.tsx`. +- Create: `tests/ui-caring-contact-mockups.spec.ts`. +- Modify: `package.json` — add repository-wrapped focused mockup verification script. + +**Interfaces** + +- `MockPatient`, `MockTeam`, `MockPathway`, `MockPlan`, `MockContact` live only in mockup fixtures and cannot be imported by production code. +- `ContinuityThreadSpecimen` consumes ordered ISO date strings and labels; geometry never consumes risk or engagement values. + +**Steps** + +- [ ] Write DOM tests for one `

`, five desktop destinations, the four-item compact dock plus More-sheet destinations, no inbox/conversation label, exact one-way notice, widening thread geometry inputs and a complete accessible schedule list. +- [ ] Run the focused DOM test and confirm the new assertions fail before the mockup exists. +- [ ] Build the shell frames at 320, 390, 430, 768, 1024 and 1440 using v2 tokens and registered primitives. +- [ ] Build light/dark, forced-colour and reduced-motion continuity-thread specimens. +- [ ] Run the focused DOM test and expect all assertions to pass. +- [ ] Run the new repository-wrapped Playwright mockup script; expect every viewport to avoid horizontal overflow, clipped focus and covered sticky actions. +- [ ] Run `npm run check:design-system-contract`; expect no new production token violation. +- [ ] Run `npm run format`; inspect and retain only intended changes. +- [ ] Present the foundation board and shell for approval before producing the core suite. + +**Exit gate:** approved shell, continuity thread, status language and responsive model. + +### Phase 3 — core visual suite + +**Deliverable:** complete desktop and phone designs for Today, the four activation stages, patient overview and schedule. + +**Files** + +- Create: `src/components/caring-contacts/mockups/today-screen.tsx`. +- Create: `src/components/caring-contacts/mockups/patient-agreement-screen.tsx`. +- Create: `src/components/caring-contacts/mockups/pathway-selection-screen.tsx`. +- Create: `src/components/caring-contacts/mockups/personalisation-screen.tsx`. +- Create: `src/components/caring-contacts/mockups/review-activation-screen.tsx`. +- Create: `src/components/caring-contacts/mockups/patient-overview-screen.tsx`. +- Create: `src/components/caring-contacts/mockups/schedule-screen.tsx`. +- Modify: `tests/caring-contact-mockups.dom.test.tsx`. +- Modify: `tests/ui-caring-contact-mockups.spec.ts`. + +**Steps** + +- [ ] Add failing assertions for exact screen inventory, `Referrals to review` dominance, identity-forward results, patient-identity repetition, imported agreement, owning team, coordinator, exact 10:00/14:00/17:00 schedule, two-segment SMS preview and one-way boundary. +- [ ] Implement each screen in the sequence from §12 using the eight approved composition contracts in §10.2 at compact and wide widths. +- [ ] Verify tablet treatment for patient selection, personalisation and schedule rather than assuming interpolation. +- [ ] Exercise every interactive mockup control; no enabled inert button is permitted even in mockups. +- [ ] Verify keyboard order, focus return, 200% text, reduced motion and forced colours. +- [ ] Run focused DOM and Playwright mockup scripts; expect all cases to pass. +- [ ] Run `npm run build` only if the focused mockup route changes bundle composition enough to warrant bundle-budget evidence; remove stale `.next` first as required by the repository contract. +- [ ] Hold a design approval checkpoint before completion screens. + +**Exit gate:** the complete core journey is visually approved at desktop, tablet and phone widths. + +### Phase 4 — completion suite + +**Deliverable:** every remaining page, overlay, state and component specimen. + +**Files** + +- Create focused mockup modules under `src/components/caring-contacts/mockups/` for patient boundaries, plan/contact detail, templates, delivery exceptions, team, guidance, reports, overlays and component specimens. +- Modify: `src/app/mockups/caring-contacts/page.tsx` — expose a navigable suite index. +- Modify: focused DOM and Playwright mockup tests. + +**Steps** + +- [ ] Add every item in §§5.2–5.3 to the suite inventory and test the inventory count. +- [ ] Add loading, empty, no-results, offline, session-expiry, partial-data, permission and recoverable-error states. +- [ ] Add template-changed, draft-conflict, processing-too-late, contact-changed, withdrawal, readmission, death-event, corrected-death incident and reassignment states. +- [ ] Prove dialog/drawer/bottom-sheet selection at compact and wide widths. +- [ ] Prove no patient detail appears in toast specimens or page titles. +- [ ] Complete component specimens with default/hover/active/focus/disabled/busy/invalid states. +- [ ] Run focused DOM and browser suites, then `npm run verify:ui` because the completed visual suite spans all responsive/accessibility concerns. +- [ ] Record physical iPhone Safari and installed-PWA review as a separate acceptance item; Chromium cannot close it. + +**Exit gate:** no missing page, overlay, state or responsive treatment remains. + +### Phase 5 — meticulous design and clinical review + +**Deliverable:** approved developer handoff; still no patient-data production implementation. + +**Files** + +- Create: `docs/caring-contacts/design-handoff.md` — screen-to-route/component/state mapping. +- Create: `docs/caring-contacts/content-style-guide.md` — exact labels, prohibited copy and message-governance rules. +- Create: `docs/caring-contacts/accessibility-acceptance.md`. +- Create: `docs/caring-contacts/clinical-language-review.md`. + +**Steps** + +- [ ] Review every screen against `docs/design-system/` and record justified domain-specific exceptions. +- [ ] Scan visible copy for risk prediction, false reassurance, clinical-outcome inference, reply invitation and caring-contact/follow-up confusion. +- [ ] Trace identity, agreement, activation, pause, withdrawal, failed delivery and team handover end to end. +- [ ] Verify continuity across all six target widths and every overlay transition. +- [ ] Complete keyboard, screen-reader, reflow, dark, forced-colour and reduced-motion reviews. +- [ ] Obtain lived-experience approval of message content and again of the complete visual prototype; either gate may block progression. +- [ ] Obtain product, service-clinical, privacy/security and accessibility design sign-off with limitations recorded. +- [ ] Freeze the approved screen/state inventory; later visual changes require a documented decision rather than silent drift. + +**Exit gate:** explicit approval to begin production implementation planning. + +### Phase 6 — secure domain and datastore foundation + +**Deliverable:** independently testable local domain model and approved datastore contract. No SMS provider. + +**Files** + +- Create: `src/lib/caring-contacts/model.ts`. +- Create: `src/lib/caring-contacts/schedule.ts`. +- Create: `src/lib/caring-contacts/message-policy.ts`. +- Create: `src/lib/caring-contacts/permissions.ts`. +- Create: `src/lib/caring-contacts/audit.ts`. +- Create: `src/lib/caring-contacts/repository.ts`. +- Create: `supabase/migrations/20260814000000_caring_contacts_foundation.sql` only if Supabase is approved for this boundary; otherwise create the equivalent migration in the selected datastore's native location. +- Regenerate: `src/lib/supabase/database.types.ts` only after the approved local migration workflow. +- Create focused model, schedule, policy, permission, audit and migration tests under `tests/`. + +**Interfaces** + +- Closed TypeScript unions mirror the plan, contact and template lifecycles in §7. +- `buildApprovedSchedule(pathwayVersion, dischargeAt, sendingPreference): ScheduleResult` is pure, discharge-anchored and deterministic; preferences map to 10:00, 14:00 or 17:00 AWST. +- `validateGovernedMessage(input): ValidationResult` returns exact blocking codes; it never calls a model/provider. +- `applyHospitalStatusEvent(plan, event): PlanTransition` pauses on readmission and irreversibly cancels on death. +- `canPerformCaringContactAction(actor, action, resource): boolean` is deny-by-default and team-aware. +- Repository writes accept an idempotency key and actor/team context; audit creation occurs in the same transaction. + +**Steps** + +- [ ] Write failing referral, lifecycle, readmission, death/correction, discharge-anchor, missed-window, weekend/public-holiday, timezone, leap-date, two-segment, prohibited-copy, permission and idempotency tests. +- [ ] Implement only the pure model/schedule/message-policy layer and make focused tests pass. +- [ ] Review the approved data classification, retention, RLS and audit design before adding schema. +- [ ] Add tables, checks, foreign keys, unique constraints, team-scoped RLS and transactional functions. +- [ ] Add migration tests proving anonymous denial, cross-team denial, role boundaries, atomic withdrawal and duplicate-activation prevention. +- [ ] Regenerate types and prove no manual drift. +- [ ] Run focused Vitest and migration checks, `npm run check:owner-scope`, `npm run check:production-readiness` and `npm run verify:pr-local`. +- [ ] Do not apply a hosted migration without explicit target authorisation. + +**Exit gate:** security/privacy owners approve the datastore boundary; all local deterministic/RLS evidence passes. + +### Phase 7 — production shell and read-only synthetic routes + +**Deliverable:** repository-native production routes backed only by deterministic synthetic fixtures or the approved local repository abstraction. + +**Files** + +- Create: `src/app/(caring-contacts)/caring-contacts/layout.tsx`. +- Create the route files listed in §5.2 with page-specific `loading.tsx`, `error.tsx` and `not-found.tsx` only where the route owns those states. +- Create the domain components listed in §10 under `src/components/caring-contacts/`. +- Create: `src/lib/caring-contacts/fixtures.ts` for non-production synthetic mode, clearly separated from mockup fixtures. +- Modify: `src/lib/tools-catalog.ts` — one Coordination launcher entry. +- Modify: `docs/codebase-index.md`, generated site map/inventory and `tests/route-reachability.test.ts`. +- Create: `tests/ui-caring-contacts.spec.ts` and focused DOM tests. + +**Steps** + +- [ ] Read the repository-installed Next.js 16 route/layout/loading/error guidance before writing route code. +- [ ] Add failing reachability, shell-navigation, no-global-composer and route-access tests. +- [ ] Implement the dedicated shell and synthetic Today route first; prove compact/wide navigation. +- [ ] Add remaining read-only routes one coherent journey at a time. +- [ ] Ensure every patient API response and page is `no-store` and excluded from PWA caching. +- [ ] Verify context switching clears patient state and the browser back path returns to the correct workspace view. +- [ ] Run focused DOM tests and a repository-wrapped Playwright caring-contact journey. +- [ ] Run `npm run docs:update`, review generated diffs, then run `npm run verify:ui` and `npm run verify:pr-local` once for handoff. + +**Exit gate:** the production route structure and responsive UI are complete with synthetic data, with no provider, hosted migration or real-patient path. + +### Phase 8 — governed plan mutations + +**Deliverable:** structured referral adapter, referral decisions, draft, activation, pause, withdrawal, reassignment and exception-resolution workflows against an approved non-production environment. + +**Files** + +- Create API routes under `src/app/api/caring-contacts/` for referrals, referral decisions, plans, contacts, templates, team context and reports. +- Create: `src/lib/caring-contacts/referral-source.ts` — hospital referral/write-back interface plus synthetic adapter; it never exposes a hospital-wide directory search. +- Create: `src/lib/caring-contacts/service.ts` — orchestration; route handlers stay thin. +- Create: `src/lib/caring-contacts/api-contracts.ts` — validated request/response schemas. +- Add focused API, auth, privacy, concurrency and DOM/e2e tests. + +**Steps** + +- [ ] Write failing API contract tests for auth expiry, conditional-access denial, referral accept/clarify/decline write-back, pending ownership, patient-controlled-source flag, duplicate active referral, fresh-auth activation, stale versions, concurrent pause/send, contact changes, readmission/death and redacted errors. +- [ ] Implement thin authenticated routes over the repository/service layer. +- [ ] Add the referral-source interface and synthetic adapter; keep a real PAS/EMR referral/write-back adapter approval-gated. +- [ ] Implement draft recovery and conflict handling without browser PHI persistence. +- [ ] Implement activation and management actions with object-specific confirmations and audit events. +- [ ] Prove no patient fields reach logs, analytics, URL query strings, RAG or OpenAI using static and request-level tests. +- [ ] Run focused tests, owner/team-scope checks, privacy checks, `npm run check:production-readiness` and `npm run verify:pr-local`. + +**Exit gate:** approved test environment supports the full human workflow with synthetic/test patients and no external SMS. + +### Phase 9 — delivery outbox and fake provider + +**Deliverable:** idempotent scheduling/dispatch pipeline using a deterministic fake provider. + +**Files** + +- Create: `src/lib/caring-contacts/sms-provider.ts`. +- Create: `src/lib/caring-contacts/fake-sms-provider.ts`. +- Create: `callback-worker/index.ts`, `callback-worker/run-loop.ts`, `callback-worker/dispatcher.ts` and `callback-worker/types.ts` as a separate responsibility from the ingestion worker. +- Create: `src/app/api/webhooks/caring-contacts/delivery/route.ts` with provider-neutral contract tests. +- Add package scripts and worker/API tests through repository wrappers. + +**Steps** + +- [ ] Write failing tests for lease loss, duplicate claim, provider timeout, duplicate webhook, out-of-order status, pause/withdrawal race, too-late cancellation and redacted logging. +- [ ] Implement lease-fenced claiming and fake-provider sending. +- [ ] Implement signed/replay-safe webhook infrastructure against fake signatures. +- [ ] Run a deterministic accelerated twelve-month simulation and prove exact counts, order and no duplicates. +- [ ] Prove worker shutdown drains claimed work safely and leaves reclaimable leases. +- [ ] Run focused worker/API tests and `npm run verify:pr-local`. + +**Exit gate:** local delivery simulation is deterministic, idempotent and privacy-safe. + +### Phase 10 — approved SMS adapter and non-production end-to-end acceptance + +**Deliverable:** one approved provider adapter in a named non-production account. + +**Prerequisite:** explicit user authorisation for the provider, target, data exposure and likely cost. + +**Steps** + +- [ ] Record the Australian-only provider data flow, tenant-scoped key evidence, discreet sender identity, non-receiving capability, status semantics, rate limits, cost controls and key/secret rotation. +- [ ] Implement the adapter behind the provider-neutral interface without changing domain logic. +- [ ] Verify webhook signatures, replay prevention, status mapping and redacted observability. +- [ ] Run canaries only with approved synthetic/test numbers and an explicit send budget. +- [ ] Prove the selected sender cannot receive replies and that no inbound payload, route, log or user interface exists in Callback. +- [ ] Test webhook-primary status handling plus manual reconciliation after simulated outage, discrepancy and suspected incident; prove uncertain contacts are never resent automatically. +- [ ] Complete `npm run check:production-readiness`, local release checks and the clinical-governance PR preflight. +- [ ] Record hosted evidence separately from local evidence. + +**Exit gate:** service, privacy, security and procurement owners accept the non-production end-to-end evidence. + +### Phase 11 — limited clinical pilot + +**Deliverable:** an explicitly authorised single-team pilot with real patients under a written protocol, no numeric enrolment cap and strict automatic stopping rules. + +**Steps** + +- [ ] Approve the single hospital/service, dedicated aftercare team, 9:00 am–6:00 pm seven-day staffing, enrolment script, imported agreement evidence, 10:00/14:00/17:00 windows, pathway version, message variants, non-receiving sender, structured record write-back, no-cap exposure, monitoring and stopping rules. +- [ ] Require assessed synthetic simulation before production access, covering referral handover, source identity, activation, withdrawal, contact change, failed delivery, readmission, death, downtime and incident handling. +- [ ] Perform accessibility acceptance with clinicians using ward desktops and supported phones, including physical iPhone Safari/PWA boundaries where applicable. +- [ ] Open enrolment to every objectively eligible referral from the one pilot team; monitor queue age and workload continuously because there is no numeric patient cap. +- [ ] Monitor duplicate sends, schedule drift, delivery exceptions, unresolved exceptions, withdrawals processed, access anomalies and privacy/security incidents. +- [ ] Run the separately consented patient-acceptability evaluation outside Callback and review aggregate lived-experience findings on tone, timing, sender identity and the one-way boundary. +- [ ] Run two weeks of seven-day hypercare with named clinical, service, technical, privacy and incident leads plus daily Callback queue/state review; provider-side reconciliation remains event-triggered. +- [ ] Trigger an immediate service-wide pause for any confirmed wrong-recipient send, duplicate send, unauthorised content, material privacy/security incident or loss of audit integrity. +- [ ] Permit restart only after joint incident-lead, privacy/security and clinical-programme approval. +- [ ] Hold the first governance review at 6–8 weeks; permit only a controlled extension, then require longer-term pathway evidence before broad rollout. + +**Exit gate:** governance board accepts pilot evidence and explicitly authorises expansion. + +### Phase 12 — controlled expansion + +**Deliverable:** staged scale-out by team/site with maintained governance. + +**Steps** + +- [ ] Expand one team/site at a time with joiner/mover/leaver checks and pathway ownership. +- [ ] Re-approve message/pathway versions before each material cohort change. +- [ ] Keep operational reporting aggregate and privacy-conscious; suppress small cells and never rank clinicians. +- [ ] Rehearse rollback, provider outage, compromised credential, incorrect template and duplicate-send incident paths. +- [ ] Schedule periodic access, retention, audit, pathway, message, provider and lived-experience reviews. +- [ ] Treat broader mental-health discharge cohorts and two-way messaging as separate governed products with new design, hazard, privacy and operational plans. + +## 14. Verification ladder + +### Design/documentation tranches + +- `npm run format:check` +- `npm run docs:check-links` +- `npm run docs:check-scripts` +- Focused mockup DOM tests +- Repository-wrapped mockup Playwright at all required widths +- `npm run check:design-system-contract` + +### Domain/data tranches + +- Focused pure-function tests for state, schedule, content and permissions +- Migration/RLS/transaction tests +- `npm run check:owner-scope` plus new team-scope proof +- `npm run check:production-readiness` +- `npm run verify:pr-local` + +### UI and workflow tranches + +- Focused DOM journey +- Repository-wrapped caring-contact Playwright journey +- 320/390/430/768/1024/1440, 400% zoom, dark, forced colours and reduced motion +- `npm run verify:ui` once for a complete UI handoff +- Physical Safari/PWA acceptance recorded separately + +### Provider and release tranches + +- Fake-provider deterministic simulation first +- Named non-production provider canary only after explicit approval +- Hosted database/provider evidence kept separate from local evidence +- `npm run verify:release` only for an explicitly authorised release-confidence run +- Clinical governance preflight, PIA/security/records evidence and rollback rehearsal + +Do not stack broad gates after focused proof unless the broader gate catches a distinct plausible failure class. + +## 15. Acceptance criteria + +### Product and clinical boundary + +- No reply/inbox/conversation surface exists. +- No risk score, prediction, urgency detection or clinical advice exists. +- Every caring contact is visibly supplemental to usual care. +- Delivery states never imply safety, wellbeing or engagement. +- Every plan shows owning team, coordinator, agreement and approved pathway/version. + +### Workflow + +- Source identity review and deliberate referral selection precede enrolment; the UI does not overstate the imported mobile number as directly re-verified. +- Missing agreement, patient-controlled-mobile flag, team or coordinator blocks activation with a named remedy. +- Final activation shows exact patient, messages, dates, 10:00/14:00/17:00 send times, AWST timezone, two-segment evidence and one-way boundary. +- Pause, withdrawal, cancellation, reassignment and contact change are distinct and audited. +- Retries and concurrency cannot duplicate a plan or message. + +### Privacy and security + +- No patient data reaches Clinical KB search/RAG/OpenAI or browser-local persistence. +- Team-scoped deny-by-default access is proven. +- WA Health SSO/MFA, conditional access and managed-session controls are proven across supported personal and managed devices. +- Patient data is absent from logs, URLs, analytics, toasts and screenshots. +- Patient data, backups, logs and provider processing remain in Australia under dedicated tenant-scoped vendor-managed keys. +- Retention, record-of-truth, breach and correction processes are approved before pilot. +- Provider secrets, payloads, webhooks and costs are governed. + +### Design and accessibility + +- The workspace visibly belongs to the current v2 Clinical KB system. +- One dominant action per region; no card soup, glassmorphism, marketing gradient or decorative clinical-state colour. +- Every production control is wired or explicitly unavailable with a stated reason. +- Complete keyboard, screen-reader, 320px, 400% zoom, dark, forced-colour and reduced-motion behaviour is proven. +- The continuity thread has a complete text/list equivalent and never carries risk meaning. +- The eight approved composition contracts in §10.2 are proven at desktop and compact widths. + +### Operations and rollout + +- Exact duplicate-send count remains zero in simulation and pilot. +- Delivery exception ownership and resolution age are visible without clinician ranking. +- Provider outage and rollback do not lose plan/audit state or silently send late messages. +- Downtime fails closed without offline patient caching, new activation or uncertain sending. +- The no-cap pilot exposes queue age and workload continuously and pauses automatically on the defined safety-stop events. +- Pilot limitations and stopping rules are documented; no production-readiness or clinical-effectiveness claim exceeds the evidence. + +## 16. Metrics that are safe to use + +Operational metrics may include: + +- plans awaiting activation; +- agreement/contact-detail completeness; +- contacts due, dispatched and delayed; +- exact delivery exceptions by approved transport code; +- time to resolve operational exceptions; +- pauses, withdrawals and cancellations processed; +- duplicate sends and schedule drift; +- template/pathway versions in use; +- approved demographic breakdowns sourced from clinical systems, with governance-configured small-cell suppression; +- access, security and privacy incidents. + +Do not infer or display patient safety, suicide risk, wellbeing, engagement, therapeutic response or clinician performance from these measures. Any clinical-effectiveness evaluation requires a separate protocol, governance review and statistical plan. + +## 17. Rollback model + +- **Design rollback:** retain the approved design spec and revert only the unapproved screen tranche. +- **Feature rollback:** disable the caring-contact launcher and route boundary without changing historical plan/audit data. +- **Delivery rollback:** stop new claims, drain or release existing leases, preserve exact status, and require explicit operator review before resumption. +- **Template rollback:** retire a faulty version; never mutate sent/history snapshots; pause affected future contacts according to the approved policy. +- **Provider rollback:** switch only through the provider interface after reconciliation; never resend uncertain contacts automatically. +- **Pilot rollback:** automatically pause all future contacts for a defined safety-stop event, notify named incident owners through the approved operational channel, preserve records, reconcile provider state and require joint incident/privacy/clinical approval before resumption. + +## 18. Final handoff package + +The programme is ready for implementation handoff only when it contains: + +1. Approved repository-grounded design spec. +2. Complete screen/overlay/state inventory and visual suite. +3. Route/component/data/interface map. +4. Clinical-boundary and content-governance specification. +5. PIA, security, records, hosting, identity and team-tenancy decisions. +6. Approved pathway/message versions and inbound-channel policy. +7. Local deterministic, RLS, UI, accessibility and delivery-simulation evidence. +8. Separate hosted/provider evidence where explicitly authorised. +9. Pilot protocol, training, monitoring, stopping and rollback rules. +10. Honest residual-risk list and named owners. + +This plan does not claim clinical approval, production readiness, WA Health endorsement, provider acceptance, deployment or migration status. diff --git a/docs/superpowers/plans/2026-08-15-caring-contact-design-phase.md b/docs/superpowers/plans/2026-08-15-caring-contact-design-phase.md new file mode 100644 index 0000000000..4a2b9a742d --- /dev/null +++ b/docs/superpowers/plans/2026-08-15-caring-contact-design-phase.md @@ -0,0 +1,311 @@ +# Caring Contact Coordination Design Phase Implementation Plan + +> **Execution note:** This plan is the design-only tranche of +> `2026-08-14-caring-contact-coordination-rollout.md`. It creates a complete synthetic mockup +> suite and developer handoff. It does not create production caring-contact routes, patient-data +> APIs, persistence, provider integrations, migrations, deployments, or real-patient capability. + +**Goal:** Produce the repository-native, clinically bounded, responsive design specification and +complete synthetic visual suite required to approve the Caring Contact Coordination workspace for +later production implementation. + +**Architecture:** Keep every rendered artefact under `/mockups/caring-contacts` and every fixture +under `src/components/caring-contacts/mockups/`. Use the repository root `.ckb-v2` layer, current UI +primitives, Lucide icon vocabulary, `Sheet`/`ConfirmDialog`, and mockup-only browser project. A small +client controller owns screen switching and overlay specimens; individual screens remain focused, +typed components. No mockup module may be imported by production code. + +**Tech stack:** Next.js App Router, React 19, TypeScript, Tailwind utilities backed by existing CSS +tokens, Lucide React, Vitest/Testing Library, repository-wrapped Playwright. + +**Execution constraints:** + +- Use only obviously fictional people, identifiers, phone numbers, teams, messages and events. +- Do not call a provider, Supabase, OpenAI/RAG, a hospital system, or any production API. +- Do not commit, stage, push, deploy or publish; repository authority is required separately. +- Preserve the untracked approved master plan and all unrelated work. +- Use `apply_patch` for file edits and repository wrappers for verification. +- Treat the clinical boundary documents and approved decision lock as binding, not illustrative. +- At each task boundary, run a specification review and a code/visual-quality review before the next + task begins. + +--- + +### Task 1: Freeze the design, clinical and governance contracts + +**Files:** + +- Create: `docs/superpowers/specs/2026-08-15-caring-contact-coordination-design.md` +- Create: `docs/caring-contacts/repository-design-audit.md` +- Create: `docs/caring-contacts/clinical-boundaries.md` +- Create: `docs/caring-contacts/governance-decisions.md` +- Reference: `docs/superpowers/plans/2026-08-14-caring-contact-coordination-rollout.md` +- Reference: `docs/design-system/SPEC.md` +- Reference: `docs/design-system/TOKENS.md` +- Reference: `docs/design-system/COMPONENTS.md` +- Reference: `docs/design-system/GATES.md` + +**Step 1: Write the binding design specification** + +Record the chosen information architecture, action-first Today hierarchy, activation workflow, +patient/episode boundaries, schedule model, desktop/phone navigation, continuity-thread treatment, +screen inventory, overlay inventory, content hierarchy and responsive layout states. Explicitly mark +the suite as a design prototype using synthetic data. + +**Step 2: Record the repository source-of-truth audit** + +Map the new suite to current tokens, UI primitives, mockup routing, browser-project isolation and +rendered visual sources. Name deprecated or unsuitable directions: shared search composer, RAG +routes, browser-local patient storage, decorative clinical colours, risk-ranked queues and copied +Psychbase styling. + +**Step 3: Record clinical-language boundaries** + +Define the exact distinctions the UI must preserve: transport state is not patient safety; a caring +contact is not monitoring, crisis response or replacement clinical care; pending referrals retain +referring-team ownership; delivery cannot imply receipt or wellbeing; objective eligibility never +becomes inferred risk. Include approved sender, one-way, emergency and first-message wording rules. + +**Step 4: Record governance decisions and explicit residual risks** + +Capture the approved pilot team/service, handover acceptance, source-system mobile suitability, +AU-only hosting boundary, audit split, incident stop/restart authority, no numeric pilot cap, webhook +reconciliation choice, reporting suppression and non-production design limit. + +**Step 5: Verify documentation** + +Run: + +```powershell +npm run docs:check-links +npm run docs:check-scripts +npx prettier --check docs/superpowers/specs/2026-08-15-caring-contact-coordination-design.md docs/caring-contacts/repository-design-audit.md docs/caring-contacts/clinical-boundaries.md docs/caring-contacts/governance-decisions.md +``` + +Expected: all commands exit 0; no placeholder tokens remain. + +--- + +### Task 2: Build the mockup foundation, shell and continuity language + +**Files:** + +- Create: `src/app/mockups/caring-contacts/page.tsx` +- Modify: `src/app/mockups/mockups-layout-client.tsx` +- Create: `src/components/caring-contacts/mockups/index.ts` +- Create: `src/components/caring-contacts/mockups/types.ts` +- Create: `src/components/caring-contacts/mockups/fixtures.ts` +- Create: `src/components/caring-contacts/mockups/caring-contact-design-suite.tsx` +- Create: `src/components/caring-contacts/mockups/caring-contact-shell-frame.tsx` +- Create: `src/components/caring-contacts/mockups/foundation-board.tsx` +- Create: `src/components/caring-contacts/mockups/continuity-thread-specimen.tsx` +- Create: `src/components/caring-contacts/mockups/mockup-primitives.tsx` +- Test: `tests/caring-contact-mockups.dom.test.tsx` + +**Step 1: Add failing foundation contract tests** + +Assert an exact synthetic-data marker, five desktop destinations, four phone destinations, More-sheet +destinations, one continuity thread plus chronological alternative, approved schedule times, no +clinical-risk score, and no inbound/reply affordance. + +**Step 2: Create typed fictional fixtures** + +Define mock-only patients, referrals, episodes, pathways, contacts, templates, delivery events, team +members and audit events. Make fiction obvious in visible names and IDs. Export no production model. + +**Step 3: Build a repository-native shell** + +Desktop exposes Today, Patients, Schedule, Templates and More; phone exposes Today, Patients, +Schedule and More. The suite has one `

`, a synthetic-data banner, real buttons for screen changes, +and a labelled More sheet. Suppress the global search composer/chrome only for this mockup family. + +**Step 4: Build the foundation board and continuity specimen** + +Show inherited surface/type/spacing/action/status roles, plain operational vocabulary, component +state examples and the approved widening cadence. The thread is neutral schedule geometry and never +changes for clinical risk, delivery result or patient behaviour. Pair it with an accessible ordered +list. + +**Step 5: Run focused proof** + +Run: + +```powershell +npm test -- --run tests/caring-contact-mockups.dom.test.tsx +npm run typecheck +npx prettier --check src/app/mockups/caring-contacts/page.tsx src/app/mockups/mockups-layout-client.tsx src/components/caring-contacts/mockups tests/caring-contact-mockups.dom.test.tsx +``` + +Expected: focused DOM tests, typecheck and formatting pass. + +--- + +### Task 3: Build the complete core visual suite + +**Files:** + +- Create: `src/components/caring-contacts/mockups/today-screen.tsx` +- Create: `src/components/caring-contacts/mockups/patient-agreement-screen.tsx` +- Create: `src/components/caring-contacts/mockups/pathway-selection-screen.tsx` +- Create: `src/components/caring-contacts/mockups/personalisation-screen.tsx` +- Create: `src/components/caring-contacts/mockups/review-activation-screen.tsx` +- Create: `src/components/caring-contacts/mockups/patient-overview-screen.tsx` +- Create: `src/components/caring-contacts/mockups/schedule-screen.tsx` +- Modify: `src/components/caring-contacts/mockups/caring-contact-design-suite.tsx` +- Modify: `tests/caring-contact-mockups.dom.test.tsx` +- Create: `tests/ui-caring-contact-mockup.spec.ts` +- Modify: `playwright.config.ts` +- Modify: `tests/playwright-project-isolation.test.ts` +- Modify: `package.json` + +**Step 1: Extend failing contracts for the core inventory** + +Assert all seven screens, `Referrals to review` dominance, distinct Needs action and sending-window +sections, pending ownership wording, identity repetition, imported agreement, patient-controlled +mobile provenance, owning team, coordinator, 10:00/14:00/17:00 AWST schedule, exact message preview, +two-segment limit and one-way boundary. + +**Step 2: Implement Today as a guided command centre** + +Place referrals first, then named action exceptions, today's three sending windows, recent activity +and quiet metrics. Order referrals by time to first eligible window, never risk. Use identity-forward +rows and visible ownership states. + +**Step 3: Implement the four activation stages** + +Use the approved split composition on wide screens: persistent patient identity, focused stage and +exact patient-visible preview. On compact screens the identity stays in flow and preview opens in a +labelled sheet. Activation stays blocked until agreement, patient-controlled mobile, owning team and +coordinator are present. + +**Step 4: Implement patient overview and schedule** + +Patient overview leads with identity, active-plan state, owner, agreement, continuity thread and +chronological record. Schedule leads with a day and seven-day strip; named exceptions remain separate +from routine 10:00, 14:00 and 17:00 AWST windows. + +**Step 5: Register focused mockup-browser isolation** + +Add the new spec to the mockup project matcher and top-level matcher, update the isolation contract, +and add a repository-wrapper script named `test:e2e:caring-contact-mockup`. + +**Step 6: Verify the core suite** + +Run: + +```powershell +npm test -- --run tests/caring-contact-mockups.dom.test.tsx tests/playwright-project-isolation.test.ts +npm run typecheck +npm run test:e2e:caring-contact-mockup +``` + +Browser proof must cover 320, 390, 768 and 1440 CSS-pixel widths; no horizontal overflow; usable +keyboard focus; More and preview sheets; desktop/phone schedule; reduced motion; forced colours. + +--- + +### Task 4: Build the completion suite and full component/state specimens + +**Files:** + +- Create: `src/components/caring-contacts/mockups/patient-boundary-screens.tsx` +- Create: `src/components/caring-contacts/mockups/template-screens.tsx` +- Create: `src/components/caring-contacts/mockups/delivery-exception-screens.tsx` +- Create: `src/components/caring-contacts/mockups/team-guidance-reporting-screens.tsx` +- Create: `src/components/caring-contacts/mockups/overlay-specimens.tsx` +- Create: `src/components/caring-contacts/mockups/component-state-specimens.tsx` +- Modify: `src/components/caring-contacts/mockups/caring-contact-design-suite.tsx` +- Modify: `tests/caring-contact-mockups.dom.test.tsx` +- Modify: `tests/ui-caring-contact-mockup.spec.ts` + +**Step 1: Add failing inventory and safety assertions** + +Require search/empty/duplicate/readmission/deceased/wrong-recipient/contact-changed/pause/withdrawal/ +cancel states; plan/contact detail; templates and approvals; delivery drawer/sheet; team; guidance; +suppressed reporting; dialogs/drawers/sheets; loading/empty/error/offline/auth/permission/conflict states; +and reusable component specimens. + +**Step 2: Implement remaining full-page surfaces** + +Keep identity-forward search restricted to pilot-team records. Separate permanent transport failure +from clinical action. Show source write-back, audit and owner visibility without raw patient details +in transient alerts. Render reporting suppression as `Suppressed`, never zero. + +**Step 3: Implement overlay and responsive exception patterns** + +Delivery exceptions use a contextual right drawer on desktop and a full-screen phone sheet. Identity, +withdrawal, activation and conflict decisions use named dialogs or full-screen stages with explicit +focus return. No toast contains patient name, ID, phone number or message. + +**Step 4: Implement component state specimens** + +Cover default, hover, active, focus-visible, disabled/unavailable, busy, invalid, empty, long-content, +compact, dark, forced-colour and reduced-motion states. Use adjacent wording/non-colour marks for +every status. + +**Step 5: Verify completion** + +Run the Task 3 focused DOM, type and browser commands again. Expected: the exact full inventory is +reachable from the suite, every overlay closes and restores focus, and no viewport overflows. + +--- + +### Task 5: Complete clinical, accessibility, responsive and developer handoff review + +**Files:** + +- Create: `docs/caring-contacts/content-style-guide.md` +- Create: `docs/caring-contacts/clinical-language-review.md` +- Create: `docs/caring-contacts/accessibility-acceptance.md` +- Create: `docs/caring-contacts/design-handoff.md` +- Modify: `docs/superpowers/specs/2026-08-15-caring-contact-coordination-design.md` +- Modify only if generated contract requires: design-system adoption/generated documentation + +**Step 1: Review every screen against the clinical boundary** + +Search rendered and source copy for monitoring, safety, risk inference, response/reply, delivery +certainty, transferred ownership before acceptance, crisis-service implication, coercive agreement +and diagnostic eligibility. Record each reviewed phrase and outcome. + +**Step 2: Review accessibility and responsive continuity** + +Record 320px/400% reflow, 390/430/768/1024/1440 widths, keyboard sequence, focus restoration, +screen-reader names/roles/states, status redundancy, text sizing, safe-area behaviour, dark, forced +colours and reduced motion. Record physical iPhone Safari/installed-PWA work as unrun and required +later; Chromium cannot close it. + +**Step 3: Produce the developer handoff** + +Map every approved screen to its future route, domain components, states, fixture, production data +contract and safety invariant. Freeze the screen/state inventory and list explicit non-goals and +future authority gates. + +**Step 4: Run final proportional gates** + +Run: + +```powershell +npm run format +npm test -- --run tests/caring-contact-mockups.dom.test.tsx tests/playwright-project-isolation.test.ts +npm run typecheck +npm run check:design-system-contract +npm run docs:check-links +npm run docs:check-scripts +npm run test:e2e:caring-contact-mockup +npm run verify:ui +npm run check:production-readiness +``` + +Expected: all applicable local commands pass. If an existing broad gate fails, compare with a clean +current-main baseline before classifying it. Do not run provider-backed or live clinical checks. + +**Step 5: Capture final visual evidence** + +Create desktop and phone screenshots for the foundation, Today, activation, patient overview, +schedule, delivery exception and component/state board. Keep screenshot fixtures synthetic and place +temporary evidence outside tracked product paths unless the repository contract explicitly requires +committed baselines. + +**Exit gate:** The design specification, complete desktop/phone synthetic suite, clinical-language +review, accessibility record and developer handoff agree with the approved decision lock and current +repository design system. Production implementation remains a separately authorised phase. diff --git a/docs/superpowers/specs/2026-08-15-caring-contact-coordination-design.md b/docs/superpowers/specs/2026-08-15-caring-contact-coordination-design.md new file mode 100644 index 0000000000..7da1e2b41b --- /dev/null +++ b/docs/superpowers/specs/2026-08-15-caring-contact-coordination-design.md @@ -0,0 +1,185 @@ +# Caring Contact Coordination Workspace — binding design specification + +**Status:** synthetic design prototype only, 15 August 2026 +**Decision source:** [approved rollout plan](../plans/2026-08-14-caring-contact-coordination-rollout.md), especially the Approved decision lock +**Design system:** [SPEC](../../design-system/SPEC.md), [TOKENS](../../design-system/TOKENS.md), [COMPONENTS](../../design-system/COMPONENTS.md) and [GATES](../../design-system/GATES.md) + +## 1. Scope and product boundary + +This specification freezes the design target. It does not authorise production routes, patient data, +APIs, a datastore, SMS, a migration or deployment. Prototype and test screenshots are required to +use clearly fictional synthetic identities and details; real patient information or PHI must never +appear in them. All other fixtures, messages, people, services, identifiers and phone numbers are +fictional too. + +Callback is a dedicated operational workspace inside this repository. It inherits the Clinical KB +v2 visual and accessibility contracts but is not a search mode. Patient or referral information must +not enter shared search, RAG, OpenAI, favourites, recent-search, query-log or analytics paths. + +The initial service is one dedicated hospital aftercare/transition team coordinating one-way caring +contacts for objectively eligible adults discharged after a suicidal crisis. Caring contacts +supplement usual care. They are not monitoring, crisis response, triage, clinical advice or a +replacement for person-to-person follow-up. Transport state never represents patient safety, +wellbeing, receipt, engagement or treatment response. A pending or returned referral remains the +referring team's responsibility until explicit acceptance. + +## 2. Information architecture + +Desktop exposes five primary areas: Today, Patients, Schedule, Templates and More. Compact layouts +use a four-item dock: Today, Patients, Schedule and More; the More sheet contains Templates, Team, +Guidance and Reports. No Inbox, Messages, Conversations or global search composer exists. + +Today keeps this action-first order at every width: + +1. `Referrals to review`, ordered by discharge and first eligible contact-window timing; +2. `Needs action`, where every row names the observable operational condition, remedy and owner; +3. today's Morning, Afternoon and Early evening sending-window panels; +4. recent activity with patient name, exact action, clinician and time, but no phone number, message + text or clinical detail; and +5. quiet aggregate metrics. + +Reporting never moves above actionable work. `Needs action` is not an inferred-risk label. + +## 3. Referral, identity and episode boundaries + +The hospital workflow supplies minimum identity, discharge, mobile provenance, an explicit +patient-controlled/suitable-for-discreet-SMS flag and `Agreement confirmed: Yes/No`. Callback does +not represent the imported mobile as independently re-verified or the agreement as legal/treatment +consent. + +An authorised aftercare clinician Accepts, Returns for clarification or Declines with a structured +reason. Before acceptance, the UI shows `Awaiting handover` and the referring team as responsible. +Acceptance moves the referral into the aftercare queue; a coordinator then explicitly claims it or a +team lead assigns it. There is no automatic round robin. + +Patient search is limited to the active pilot team's referrals and Callback episodes. Results are +identity-forward rows followed by a separate assurance step. The chosen identity remains visible in +flow through activation; `Change patient` requires object-specific confirmation. Team switching +clears patient state before the new context renders. + +- A duplicate referral cannot create a second active plan. +- A later qualifying discharge creates a new linked episode only after the earlier episode closes. +- Readmission pauses the episode; a later discharge needs a new linked referral. +- Recorded death irreversibly cancels unsent contacts; a correction is an incident and any future + plan needs a new referral. +- Completed, cancelled and withdrawn episodes are read-only. + +## 4. Four-stage activation + +Wide layouts use a persistent stepper, focused stage and live exact-message preview. Compact layouts +keep identity and stage in flow and open the preview in a labelled sheet. + +1. **Patient and agreement:** repeat source identity/mobile evidence; require the + patient-controlled-mobile flag, `Agreement confirmed: Yes`, accepted owning team and coordinator; + name the remedy for each blocker. +2. **Choose pathway:** show current locally approved versions with duration, cadence, sender, + one-way boundary and approval ownership. Do not rank or label a pathway `best`. Until local + approval, the cadence reads `Illustrative locally governed pathway`. +3. **Personalise:** allow only preferred name, neutral team identity, coordinator signature and + approved variant choice. Show exact patient-visible text, encoding, segment count, schedule and + continuity thread. No free text, generated authoring or dynamic translation. More than two fully + substituted SMS segments blocks progression. +4. **Review and activate:** section the full assurance review—identity and source, mobile suitability, + agreement, ownership, exact pathway/message versions, exact text and segment evidence, every date + and AWST send time, and one-way/no-monitoring boundaries—before the final action + `Activate 10-contact plan`. Fresh authentication and atomic activation are future implementation + contracts, not prototype capability. + +## 5. Schedule and continuity + +The schedule is anchored to actual discharge time. The first contact uses the next approved service +time: Morning 10:00 am, Afternoon 2:00 pm or Early evening 5:00 pm AWST. Store one selected +preference per plan and derive all 10 planned contacts from it; never rotate one episode through the +three windows. The Schedule dashboard may still aggregate different patients. Weekends and WA public +holidays are permitted within 9:00 am–6:00 pm. The illustrative cadence is day 1, week 1 and months +1, 2, 3, 4, 6, 8, 10 and 12. + +Missed contacts are never sent late. A pause preserves the original calendar and permanently skips +contacts within the pause. Resumption starts with the next future contact. Withdrawal immediately +cancels unsent contacts; cancellation is a distinct authorised action. A coordinator may move a +contact only within its scheduled day; a date change needs a reason and team-lead approval. + +Schedule defaults to one day with a seven-day strip. Named exceptions remain separate from routine +sending-window lists. The continuity thread uses close early nodes widening across the year and +represents elapsed schedule spacing only. Geometry and colour never respond to patient, delivery or +clinical state. A complete chronological ordered list, accessible name `Caring-contact schedule`, +immediately follows and is the source of truth. Forced colours use system strokes; reduced motion +removes path animation; print uses the list. + +## 6. Screen and overlay inventory + +| Screen | Primary composition | +| -------------------------- | --------------------------------------------------------------------------------------- | +| Today | Compact stacked commands; wide action/list split | +| Patients / patient detail | Identity-forward rows, then stacked or split identity/plan detail | +| New plan / plan detail | Compact full-stage plus preview sheet; wide stepper/work/preview or plan/schedule split | +| Schedule / contact detail | Compact day list and dedicated contact page; wide rail/split and drawer-capable detail | +| Templates / pathway detail | Stacked or rail list; metadata/preview split | +| Team / Guidance / Reports | Stacked or rail; readable guidance; aggregate-only reporting | + +The 24 required overlay/state decisions are: verify identity; change patient; pathway preview; +message preview; communication preference; adjust date/time; outside-window warning; save draft; +discard changes; final activation; activation success; pause; withdrawal; reassignment; delivery +detail; resolve failed delivery; contact-changed block; template changed/retired; session expiry; +offline banner; recoverable error; permission unavailable; team switcher; draft/version conflict. + +Use repository `Sheet`, `ConfirmDialog` and `OverlayRoot`. Short decisions are desktop dialogs and +phone bottom sheets; inspection is a wide right drawer and phone full-height sheet/screen; identity, +withdrawal, activation and conflicts become full-screen phone stages. Every overlay is named, +focus-safe, scrollable and leaves validation, focus and safe-area navigation uncovered. +The frozen per-item 24-row modality and dismissal matrix in +`docs/caring-contacts/design-handoff.md` is binding; a generic one-modality Sheet path is not an +acceptable implementation substitute. + +## 7. Content, visual and responsive contract + +- Australian English, sentence case and verb-first, object-specific actions. +- One filled command per region; headings, rows and dividers before another panel. +- Clinical Sky for identity/focus/continuity. Green, amber and red only for exact semantic state, + always with text and a non-colour channel. No card soup, marketing gradient, glass-heavy treatment + or decorative clinical colour. +- Closed transport terms: Scheduled, Processing, Sent, Delivered, Not delivered, Number invalid, + Contact changed, Status unavailable and Missed. None is a patient-state label. +- ISO machine dates; `en-AU`/`Australia/Perth` display; explicit missing-value phrases, never a dash. +- One `

` per page; titles wrap; patient names, ownership and warnings have a full-value path. +- Real patient information or PHI never appears in toasts, URLs, page titles, analytics, logs or + screenshots. Prototype and test screenshots contain clearly fictional synthetic identities and + details so the required visual states remain reviewable without weakening the privacy rule. + +Freeze the responsive width-to-state mapping as follows; 390 and 430 are required compact samples, +not additional layout states: + +| Width | State and required composition | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 320–430 | `compact`: repository-native phone header, four-item dock, no persistent side rail; Today starts with `Referrals to review`; More owns Templates, Team, Guidance and Reports; patient identity and the activation stage stay in flow without a second fixed header | +| 768 | `rail`: collapsed desktop navigation plus a supporting rail; this is not an enlarged phone layout. Today retains its action-first order, while Patients, activation and Schedule may place navigation/context in the rail and keep the working content stacked | +| 1024 | `split` where both panes preserve their minimum viable widths: Today may use action/list, Patients list/detail, activation workflow/preview and Schedule seven-day/day-detail. A composition that cannot preserve both panes falls back to `rail` or `stacked` rather than compressing or clipping | +| 1440 | `wide`: persistent navigation and active-team context with comparison-friendly patient, plan and schedule compositions | + +Review 400% zoom on 1280px as equivalent narrow reflow. Components may use `stacked` between the +frozen shell states when their own container cannot support a rail or split. All controls meet the +repository tap-target, naming, keyboard and focus contracts. Dark mode, forced colours and reduced +motion are first-class. No horizontal page scroll or sticky content covering focus/validation is +permitted. + +## 8. Approval boundary + +This specification is not clinical approval, WA Health endorsement, clinical-effectiveness evidence +or production readiness. Progression requires the complete synthetic prototype, clinical-language +and accessibility review, lived-experience approval of message content and the complete prototype, +privacy/security review, and explicit approval to begin production implementation planning. + +## 9. Final design handoff set + +Implementation must treat this specification together with the following records as one frozen +design handoff: + +- [content style guide](../../caring-contacts/content-style-guide.md); +- [clinical-language review](../../caring-contacts/clinical-language-review.md); +- [accessibility and responsive acceptance](../../caring-contacts/accessibility-acceptance.md); and +- [developer handoff](../../caring-contacts/design-handoff.md). + +The local evidence is synthetic Chromium/source evidence only. Physical iPhone Safari and installed- +PWA acceptance are unrun and required later. None of these documents converts the prototype into a +production route or authorises patient data, provider/API work, SMS, migration, deployment or a +pilot. diff --git a/package.json b/package.json index aa5bc15901..3db9d2ab3b 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "check:npm-ci-dry-run": "npm ci --dry-run --ignore-scripts", "test:e2e:quarantine": "node scripts/run-playwright.mjs --project=chromium --grep @quarantine --pass-with-no-tests", "test:e2e:mockups": "node scripts/run-playwright.mjs --project=chromium-mockups", + "test:e2e:caring-contact-mockup": "node scripts/run-playwright.mjs --project=chromium-mockups tests/ui-caring-contact-mockup.spec.ts", "test:e2e:advisory": "node scripts/run-playwright.mjs --project=chromium --project=chromium-mockups --grep \"@quarantine|@mockup\" --pass-with-no-tests", "test:e2e:chromium": "node scripts/run-playwright.mjs --project=chromium --project=chromium-mockups", "test:e2e:visual": "node scripts/run-playwright.mjs --config=playwright.visual.config.ts", diff --git a/playwright.config.ts b/playwright.config.ts index 4d6ee0fc63..4e0fb17be6 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -25,13 +25,13 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; const productionSpecPattern = /.*(?:answer-progress-ui-smoke|dsm-ui-smoke|ui-(smoke|stress|accessibility|document-canvas|tools|overlap|universal-search|specifiers|formulation(?:-result-cards)?|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/; const mockupSpecPattern = - /.*ui-(document-top-navigation-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-search-mode-mockup|tools-task-directory)\.spec\.ts/; + /.*ui-(caring-contact-mockup|document-top-navigation-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-search-mode-mockup|tools-task-directory)\.spec\.ts/; const mockupTag = /@mockup/; export default defineConfig({ testDir: "./tests", testMatch: - /.*(?:answer-progress-ui-smoke|dsm-ui-smoke|ui-(smoke|stress|accessibility|document-canvas|document-top-navigation-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-search-mode-mockup|tools-task-directory|overlap|universal-search|specifiers|formulation(?:-result-cards)?|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/, + /.*(?:answer-progress-ui-smoke|dsm-ui-smoke|ui-(smoke|stress|accessibility|caring-contact-mockup|document-canvas|document-top-navigation-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-search-mode-mockup|tools-task-directory|overlap|universal-search|specifiers|formulation(?:-result-cards)?|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/, timeout: 60_000, retries: 0, // Fail the run if a stray `test.only` is committed: otherwise it silently diff --git a/src/app/mockups/caring-contacts/page.tsx b/src/app/mockups/caring-contacts/page.tsx new file mode 100644 index 0000000000..2cfe20a97a --- /dev/null +++ b/src/app/mockups/caring-contacts/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { CaringContactDesignSuite } from "@/components/caring-contacts/mockups"; + +export const metadata: Metadata = { + title: "Synthetic caring-contact design suite", + description: "Fictional-only caring-contact coordination design foundation.", +}; + +export default function CaringContactsMockupPage() { + return ; +} diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index 7c4b8ac0f6..4239ac9da4 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -71,6 +71,10 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { // the three directions restructure that band. Shared chrome above them would read as a second, // real header and a second real composer over the study. const isToolsSearchDirectionsMockup = pathname === "/mockups/tools-search-directions"; + // This family draws a complete patient-first operational shell. It is not a + // search mode, and fictional patient details must never enter shared search. + const isCaringContactMockup = + pathname === "/mockups/caring-contacts" || pathname.startsWith("/mockups/caring-contacts/"); return ( {children} diff --git a/src/components/caring-contacts/mockups/activation-workflow.tsx b/src/components/caring-contacts/mockups/activation-workflow.tsx new file mode 100644 index 0000000000..5d2a27d053 --- /dev/null +++ b/src/components/caring-contacts/mockups/activation-workflow.tsx @@ -0,0 +1,453 @@ +"use client"; + +import { + ArrowLeft, + ArrowRight, + CalendarDays, + CheckCircle2, + ChevronRight, + ClipboardCheck, + Eye, + FileCheck2, + IdCard, + Info, + MessageSquareText, + Phone, + ShieldCheck, + Sun, + Sunset, + UserRoundCheck, + Users, +} from "lucide-react"; +import { useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; +import { announce } from "@/components/ui/live-announcer"; +import { Sheet } from "@/components/ui/sheet"; +import { cn } from "@/components/ui-primitives"; + +import { + ROWAN_SELECTED_SENDING_PREFERENCE, + syntheticPathways, + syntheticPatients, + syntheticTeamMembers, + syntheticTemplates, +} from "./fixtures"; +import { + AssuranceRow, + MessagePreviewCard, + OneWayBoundary, + PatientIdentityStrip, + ProductSection, + ScheduleList, + SectionHeading, + StatusChip, + VerifiedSummary, + WorkflowStepper, + productInset, + productSurface, +} from "./product-ui"; + +export type ActivationStage = "agreement" | "pathway" | "personalisation" | "review"; + +const stageNumber: Record = { + agreement: 1, + pathway: 2, + personalisation: 3, + review: 4, +}; + +export function ActivationWorkflow({ + stage, + onStageChange, + onActivated, +}: { + stage: ActivationStage; + onStageChange: (stage: ActivationStage) => void; + onActivated: () => void; +}) { + return ( +
+ + + {stage === "agreement" ? onStageChange("pathway")} /> : null} + {stage === "pathway" ? ( + onStageChange("agreement")} onContinue={() => onStageChange("personalisation")} /> + ) : null} + {stage === "personalisation" ? ( + onStageChange("pathway")} onContinue={() => onStageChange("review")} /> + ) : null} + {stage === "review" ? ( + onStageChange("personalisation")} onActivated={onActivated} /> + ) : null} +
+ ); +} + +function AgreementStage({ onContinue }: { onContinue: () => void }) { + const patient = syntheticPatients[1]; + const coordinator = syntheticTeamMembers[0]; + return ( +
+ + All assurances present} + /> +
+ + + + + +
+
+ + +
+ ); +} + +function PathwayStage({ onBack, onContinue }: { onBack: () => void; onContinue: () => void }) { + const [previewOpen, setPreviewOpen] = useState(false); + const previewTriggerRef = useRef(null); + const pathway = syntheticPathways[0]; + return ( +
+
+ + +
+ + +
+
+ + + Two-person approved} + /> +
+ {[ + ["Duration", "12 months · 10 contacts"], + ["Cadence", "Day 1, week 1, months 1, 2, 3, 4, 6, 8, 10 and 12"], + ["Sender", "Example Aftercare Team · non-receiving"], + ["Approval", "Taylor Fiction and Jordan Example"], + ].map(([term, value]) => ( +
+
{term}
+
{value}
+
+ ))} +
+
+ +
+
+ +
+
+
+ +
+ + +
+ + setPreviewOpen(false)} + title="Preview governed pathway" + description="Current locally approved synthetic version" + closeLabel="Close pathway preview" + returnFocusRef={previewTriggerRef} + mobilePlacement="fullscreen" + contentStyle={{ + position: "fixed", + inset: "0 0 0 auto", + width: "min(100%, 38rem)", + maxWidth: "38rem", + height: "100%", + maxHeight: "100%", + }} + contentClassName="md:rounded-none" + desktopBackdropClassName="md:items-stretch md:justify-end md:p-0" + > +
+ + + + +
+
+
+ ); +} + +const preferenceOptions = [ + { label: "Morning", time: "10:00 am AWST", icon: Sun }, + { label: "Afternoon", time: "2:00 pm AWST", icon: Sun }, + { label: "Early evening", time: "5:00 pm AWST", icon: Sunset }, +] as const; + +function PersonalisationStage({ onBack, onContinue }: { onBack: () => void; onContinue: () => void }) { + return ( +
+
+ + +
+ {[ + ["Preferred name", "Rowan", "Imported from the synthetic referral"], + ["Message variant", "Warm neutral A", "Locally approved current version"], + ["Team identity", "Example Aftercare Team", "Neutral, non-receiving sender"], + ["Coordinator signature", "Alex Example", "Explicitly assigned coordinator"], + ].map(([label, value, note]) => ( +
+
{label}
+
+ {value} + {note} +
+ +
+ ))} +
+
+ Sending preference +

One preference applies to all 10 contacts.

+
+ {preferenceOptions.map(({ label, time, icon: Icon }, index) => ( + + ))} +
+
+
+ +
+ +
+ + +
+
+ ); +} + +function ReviewStage({ onBack, onActivated }: { onBack: () => void; onActivated: () => void }) { + const [confirmOpen, setConfirmOpen] = useState(false); + const patient = syntheticPatients[1]; + const pathway = syntheticPathways[0]; + const template = syntheticTemplates[0]; + + function confirmActivation() { + setConfirmOpen(false); + announce("Prototype activation reviewed. No plan was created and no message was sent.", { + eventId: "caring-contact:activation-review", + }); + onActivated(); + } + + return ( +
+
+ {[ + [IdCard, "Identity and agreement", `${patient.fullName} · Agreement confirmed: Yes`], + [Users, "Ownership", "Example Aftercare Team · Alex Example"], + [FileCheck2, "Approved pathway", `${pathway.name} · ${pathway.version}`], + [Info, "About this service", "One-way scheduled SMS that supplements usual care"], + ].map(([Icon, title, detail]) => { + const CardIcon = Icon as typeof IdCard; + return ( +
+ + +

{title as string}

+

{detail as string}

+
+ ); + })} +
+ +
+ + + +
+ +
+
+
+ + + +
+ + +
+ + setConfirmOpen(false)} + onConfirm={confirmActivation} + title="Final activation assurance" + description="Review the selected identity, agreement, ownership, pathway, exact message and every AWST schedule time. This synthetic action creates no plan and sends no message." + confirmLabel="Confirm activation review" + tone="primary" + /> +
+ ); +} diff --git a/src/components/caring-contacts/mockups/caring-contact-design-suite.tsx b/src/components/caring-contacts/mockups/caring-contact-design-suite.tsx new file mode 100644 index 0000000000..203aa91c26 --- /dev/null +++ b/src/components/caring-contacts/mockups/caring-contact-design-suite.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { ArrowLeft, Plus } from "lucide-react"; +import { useEffect, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { announce } from "@/components/ui/live-announcer"; + +import { ActivationWorkflow, type ActivationStage } from "./activation-workflow"; +import { CaringContactShellFrame } from "./caring-contact-shell-frame"; +import { + DeliveryExceptionDrawer, + GuidanceProductPage, + PatientOverviewProductPage, + PatientsDirectoryPage, + PlanDetailProductPage, + ReportsProductPage, + ScheduleProductPage, + TeamProductPage, + TemplatesProductPage, + TodayProductPage, +} from "./product-pages"; +import type { WorkspaceDestination } from "./types"; + +type PatientSurface = "directory" | "agreement" | "pathway" | "personalisation" | "review" | "overview" | "plan"; + +const workflowTitles: Record = { + agreement: "Patient and agreement", + pathway: "Pathway selection", + personalisation: "Personalisation", + review: "Review and activation", +}; + +const workflowStepLabels: Record = { + agreement: "Step 1 of 4", + pathway: "Step 2 of 4", + personalisation: "Step 3 of 4", + review: "Step 4 of 4", +}; + +export function CaringContactDesignSuite() { + const [activeDestination, setActiveDestination] = useState("Today"); + const [patientSurface, setPatientSurface] = useState("directory"); + const [deliveryExceptionOpen, setDeliveryExceptionOpen] = useState(false); + + const activationStage = + patientSurface === "agreement" || + patientSurface === "pathway" || + patientSurface === "personalisation" || + patientSurface === "review" + ? patientSurface + : null; + + useEffect(() => { + document.documentElement.scrollTop = 0; + document.body.scrollTop = 0; + const title = document.querySelector("[data-caring-contact-page-title]"); + title?.focus({ preventScroll: true }); + announce(`${title?.textContent?.trim() ?? activeDestination} page`, { + eventId: `caring-contact:surface:${activeDestination}:${patientSurface}`, + }); + }, [activeDestination, patientSurface]); + + function selectDestination(destination: WorkspaceDestination) { + setActiveDestination(destination); + if (destination === "Patients") setPatientSurface("directory"); + } + + function openPatientSurface(surface: PatientSurface) { + setActiveDestination("Patients"); + setPatientSurface(surface); + } + + function openScheduleException() { + setActiveDestination("Schedule"); + setDeliveryExceptionOpen(true); + } + + let title: string = activeDestination; + let eyebrow: string | undefined; + let description: string | undefined; + let headerAction; + let content; + + if (activeDestination === "Today") { + title = "Today"; + eyebrow = "Saturday, 15 August 2026"; + description = "Prioritised referrals, operational work and today’s scheduled contacts."; + headerAction = ( + + ); + content = ( + openPatientSurface("agreement")} + onOpenPatients={() => openPatientSurface("directory")} + onOpenSchedule={() => setActiveDestination("Schedule")} + /> + ); + } else if (activeDestination === "Patients") { + if (activationStage) { + title = workflowTitles[activationStage]; + eyebrow = workflowStepLabels[activationStage]; + description = "Create a governed 10-contact plan for the selected fictional patient."; + headerAction = ( + + ); + content = ( + setPatientSurface("overview")} + /> + ); + } else if (patientSurface === "overview") { + title = "Patient overview"; + eyebrow = "Rowan Sample · synthetic patient"; + description = "Identity, ownership, continuity and the complete operational history in one place."; + headerAction = ( + + ); + content = setPatientSurface("plan")} />; + } else if (patientSurface === "plan") { + title = "Plan and contact detail"; + eyebrow = "Rowan Sample · active plan"; + description = "The plan, frozen versions, future schedule, audit trail and governed actions."; + headerAction = ( + + ); + content = setDeliveryExceptionOpen(true)} />; + } else { + title = "Patients"; + eyebrow = "Example Aftercare Team"; + description = "Team-scoped referrals and caring-contact episodes. No global patient search."; + headerAction = ( + + ); + content = ( + setPatientSurface("overview")} + onContinueMira={() => setPatientSurface("agreement")} + /> + ); + } + } else if (activeDestination === "Schedule") { + title = "Schedule"; + eyebrow = "Australia/Perth"; + description = "One-day operational view with routine windows separated from named exceptions."; + content = ; + } else if (activeDestination === "Templates") { + title = "Governed templates"; + eyebrow = "Programme administration"; + description = "Approved pathway and message versions with explicit ownership and lifecycle."; + content = ; + } else if (activeDestination === "Team") { + title = "Team"; + eyebrow = "Ownership and capacity"; + description = "Explicit coordination, unclaimed work and escalation for the active team."; + content = ; + } else if (activeDestination === "Guidance") { + title = "Guidance"; + eyebrow = "Programme boundaries"; + description = "Clear operational guidance for one-way caring-contact coordination."; + content = ; + } else { + title = "Reports"; + eyebrow = "Aggregate operations"; + description = "Quiet service measures without patient-state, engagement or clinical inference."; + content = ; + } + + return ( + openPatientSurface("agreement")} + title={title} + eyebrow={eyebrow} + description={description} + headerAction={headerAction} + > + {content} + setDeliveryExceptionOpen(false)} /> + + ); +} diff --git a/src/components/caring-contacts/mockups/caring-contact-shell-frame.tsx b/src/components/caring-contacts/mockups/caring-contact-shell-frame.tsx new file mode 100644 index 0000000000..1916262fdf --- /dev/null +++ b/src/components/caring-contacts/mockups/caring-contact-shell-frame.tsx @@ -0,0 +1,322 @@ +"use client"; + +import { + Bell, + BookOpen, + CalendarDays, + ChevronDown, + ChevronRight, + CircleHelp, + ClipboardList, + FileText, + HeartHandshake, + LayoutDashboard, + MoreHorizontal, + Plus, + Settings, + Users, +} from "lucide-react"; +import { useEffect, useRef, useState, type ComponentType, type ReactNode, type SVGProps } from "react"; + +import { Button } from "@/components/ui/button"; +import { Sheet } from "@/components/ui/sheet"; +import { cn } from "@/components/ui-primitives"; + +import { FICTIONAL_DATA_MARKER } from "./fixtures"; +import type { PrimaryDestination, WorkspaceDestination } from "./types"; + +type NavigationIcon = ComponentType>; + +const desktopDestinations: readonly { label: PrimaryDestination | "More"; icon: NavigationIcon }[] = [ + { label: "Today", icon: LayoutDashboard }, + { label: "Patients", icon: Users }, + { label: "Schedule", icon: CalendarDays }, + { label: "Templates", icon: FileText }, + { label: "More", icon: MoreHorizontal }, +]; + +const phoneDestinations = desktopDestinations.filter(({ label }) => label !== "Templates"); +const desktopMoreDestinations: readonly WorkspaceDestination[] = ["Team", "Guidance", "Reports"]; +const phoneMoreDestinations: readonly WorkspaceDestination[] = ["Templates", ...desktopMoreDestinations]; + +const moreDestinations: readonly { label: WorkspaceDestination; description: string; icon: NavigationIcon }[] = [ + { label: "Templates", description: "Governed pathways, messages and approval history", icon: FileText }, + { label: "Team", description: "Ownership, capacity and unclaimed work", icon: Users }, + { label: "Guidance", description: "Programme boundaries and operational guidance", icon: BookOpen }, + { label: "Reports", description: "Aggregate operational reporting", icon: ClipboardList }, +]; + +export type CaringContactShellFrameProps = { + activeDestination: WorkspaceDestination; + onDestinationChange: (destination: WorkspaceDestination) => void; + onStartReferral: () => void; + title: string; + eyebrow?: string; + description?: string; + headerAction?: ReactNode; + children: ReactNode; +}; + +export function CaringContactShellFrame({ + activeDestination, + onDestinationChange, + onStartReferral, + title, + eyebrow, + description, + headerAction, + children, +}: CaringContactShellFrameProps) { + const [moreOpen, setMoreOpen] = useState(false); + const [announcement, setAnnouncement] = useState(`${activeDestination} selected`); + const moreTriggerRef = useRef(null); + + useEffect(() => { + setAnnouncement(`${activeDestination} selected`); + }, [activeDestination]); + + function selectDestination(destination: WorkspaceDestination) { + const returningFromMore = moreOpen; + onDestinationChange(destination); + setMoreOpen(false); + setAnnouncement(`${destination} selected`); + if (returningFromMore) { + window.setTimeout(() => { + document.querySelector("[data-caring-contact-page-title]")?.focus({ preventScroll: true }); + }, 250); + } + } + + function handlePrimaryDestination(destination: PrimaryDestination | "More", trigger: HTMLButtonElement) { + if (destination === "More") { + moreTriggerRef.current = trigger; + setMoreOpen(true); + setAnnouncement("More destinations opened"); + return; + } + selectDestination(destination); + } + + return ( +
+ + +
+
+
+
+ + +
+

Callback

+

{title}

+
+
+ + + +
+ + Synthetic prototype + + + + AE + +
+
+
+ +
+
+
+
+ {eyebrow ? ( +

+ {eyebrow} +

+ ) : null} +

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ {headerAction ?
{headerAction}
: null} +
+ + {children} +
+
+
+ + + + setMoreOpen(false)} + title="More" + description="Programme administration and supporting areas" + closeLabel="Close more destinations" + returnFocusRef={moreTriggerRef} + mobileSize="content" + > +
+ {moreDestinations.map(({ label, description: itemDescription, icon: Icon }) => ( + + ))} +
+
+ +

+ {announcement} +

+
+ ); +} diff --git a/src/components/caring-contacts/mockups/component-state-specimens.tsx b/src/components/caring-contacts/mockups/component-state-specimens.tsx new file mode 100644 index 0000000000..a5d6232a00 --- /dev/null +++ b/src/components/caring-contacts/mockups/component-state-specimens.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { AlertCircle, Check, Clock3, LoaderCircle, LockKeyhole, WifiOff } from "lucide-react"; +import { useState } from "react"; + +import { Button } from "@/components/ui/button"; + +import { OperationalStatus } from "./mockup-primitives"; + +function StateTile({ + title, + children, + className = "", +}: { + title: string; + children: React.ReactNode; + className?: string; +}) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +export function ComponentStateSpecimens() { + const [interactionOutcome, setInteractionOutcome] = useState("No interaction specimen selected."); + + return ( +
+

+ Every state carries visible words and a non-colour mark. These specimens demonstrate interaction, content, + system and accessibility modes without patient information. +

+

+ {interactionOutcome} +

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

Reason and remedy remain keyboard-reachable.

+
+ + + + + + +

+

+
+ +

No named exceptions for this day.

+
+ +

+ Exception-owner-with-an-intentionally-long-fictional-display-name@example.invalid · full value wraps and + remains available without clipping. +

+
+ +
+ 10:00 am AWST + Scheduled +
+
+ +

Raised surfaces use the dark luminance ladder.

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+ +

+

+
+
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/continuity-thread-specimen.tsx b/src/components/caring-contacts/mockups/continuity-thread-specimen.tsx new file mode 100644 index 0000000000..e0ae568916 --- /dev/null +++ b/src/components/caring-contacts/mockups/continuity-thread-specimen.tsx @@ -0,0 +1,117 @@ +import { CalendarDays } from "lucide-react"; + +import { + ROWAN_SELECTED_SENDING_PREFERENCE, + SYNTHETIC_SERVICE_SENDING_WINDOWS, + syntheticPathways, + syntheticPlannedContacts, +} from "./fixtures"; +import { OperationalStatus, SpecimenLabel, SpecimenPanel } from "./mockup-primitives"; + +const nodePositions = [3, 8, 15, 23, 32, 42, 55, 69, 84, 97] as const; + +const formatter = new Intl.DateTimeFormat("en-AU", { + day: "numeric", + month: "short", + year: "numeric", + timeZone: "Australia/Perth", +}); + +export function ContinuityThreadSpecimen() { + const pathway = syntheticPathways[0]; + + return ( + +
+
+
+ Schedule geometry +

{pathway.governanceLabel}

+
+

+ The line encodes elapsed schedule spacing only. It does not encode clinical state, transport outcome, + wellbeing or response. +

+
+ + + +
    + {syntheticPlannedContacts.map((contact) => ( +
  1. + {contact.cadenceLabel} + + {contact.windowLabel} + + {contact.transportState} + +
  2. + ))} +
+

+ Delivered means transport receipt only; it does not show that a message was read, helped or reflected the + patient’s state. Every future contact remains Scheduled. +

+
+ +
+ {Object.entries(SYNTHETIC_SERVICE_SENDING_WINDOWS).map(([name, { windowLabel }]) => ( +
+

+ {name}{" "} + {name === ROWAN_SELECTED_SENDING_PREFERENCE.window ? "· selected for this plan" : "· service option"} +

+

{windowLabel}

+
+ ))} +
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/delivery-exception-screens.tsx b/src/components/caring-contacts/mockups/delivery-exception-screens.tsx new file mode 100644 index 0000000000..94671bd33a --- /dev/null +++ b/src/components/caring-contacts/mockups/delivery-exception-screens.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { AlertTriangle, ClipboardCheck, RefreshCcw, ShieldAlert } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Sheet } from "@/components/ui/sheet"; + +import { DefinitionRow, OperationalStatus } from "./mockup-primitives"; + +export function DeliveryExceptionScreens() { + const [open, setOpen] = useState(false); + const [compact, setCompact] = useState(true); + const triggerRef = useRef(null); + + useEffect(() => { + const media = window.matchMedia("(max-width: 767px)"); + const sync = () => setCompact(media.matches); + sync(); + media.addEventListener("change", sync); + return () => media.removeEventListener("change", sync); + }, []); + + return ( +
+
+
+
+ +
+ + setOpen(false)} + title="Permanent delivery exception" + description="Contextual right drawer on desktop; full-screen phone sheet on compact layouts." + closeLabel="Close delivery exception" + returnFocusRef={triggerRef} + mobilePlacement="fullscreen" + placement={compact ? "default" : "left"} + desktopBackdropClassName="justify-end" + contentStyle={compact ? undefined : { marginLeft: "auto" }} + contentClassName={compact ? undefined : "sm:max-w-lg sm:rounded-l-2xl sm:rounded-r-none sm:border-r-0"} + testId="caring-contact-delivery-exception-sheet" + footer={ +
+ + +
+ } + > +
+
+

Contact 4 of 10 · fictional record

+ Not delivered +
+

+ This is a transport exception. It does not indicate patient safety, receipt, wellbeing or response. +

+
+ 5:00:05 pm AWST · transient carrier rejection + 5:02:05 pm AWST · transient carrier rejection + 5:04:05 pm AWST · permanent rejection + + 3 attempts total; no more retries and never outside the original window + + Future contacts paused pending operational review +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/fixtures.ts b/src/components/caring-contacts/mockups/fixtures.ts new file mode 100644 index 0000000000..e1afe70677 --- /dev/null +++ b/src/components/caring-contacts/mockups/fixtures.ts @@ -0,0 +1,218 @@ +import { + FICTIONAL_CONTACTS_BY_ROLE, + type SyntheticAuditEvent, + type SyntheticDeliveryEvent, + type SyntheticEpisode, + type SyntheticPathway, + type SyntheticPatient, + type SyntheticPlannedContact, + type SyntheticReferral, + type SyntheticSendingPreference, + type SyntheticTeamMember, + type SyntheticTemplate, +} from "./types"; + +export const FICTIONAL_DATA_MARKER = "Synthetic prototype — fictional data only"; + +export const SYNTHETIC_SERVICE_SENDING_WINDOWS = { + Morning: { scheduledTime: "10:00:00", windowLabel: "Morning 10:00 am AWST" }, + Afternoon: { scheduledTime: "14:00:00", windowLabel: "Afternoon 2:00 pm AWST" }, + "Early evening": { scheduledTime: "17:00:00", windowLabel: "Early evening 5:00 pm AWST" }, +} as const satisfies Record>; + +export const ROWAN_SELECTED_SENDING_PREFERENCE = { + window: "Morning", + ...SYNTHETIC_SERVICE_SENDING_WINDOWS.Morning, +} as const satisfies SyntheticSendingPreference; + +export const syntheticPatients = [ + { + id: "SYN-PATIENT-001", + fullName: "Mira Example", + preferredName: "Mira", + dateOfBirth: "1992-04-17", + mobile: FICTIONAL_CONTACTS_BY_ROLE.miraPatientMobile, + mobileSource: "Synthetic hospital record", + patientControlledForSms: true, + }, + { + id: "SYN-PATIENT-002", + fullName: "Rowan Sample", + preferredName: "Rowan", + dateOfBirth: "1987-11-03", + mobile: FICTIONAL_CONTACTS_BY_ROLE.rowanPatientMobile, + mobileSource: "Synthetic hospital record", + patientControlledForSms: true, + }, +] satisfies readonly SyntheticPatient[]; + +export const syntheticReferrals = [ + { + id: "SYN-REFERRAL-001", + patientId: "SYN-PATIENT-001", + referringTeam: "Fictional Ward A", + receivedAt: "2026-08-15T08:40:00+08:00", + dischargedAt: "2026-08-15T08:10:00+08:00", + handoverState: "Awaiting handover", + agreementConfirmed: true, + }, + { + id: "SYN-REFERRAL-002", + patientId: "SYN-PATIENT-002", + referringTeam: "Example Transition Unit", + receivedAt: "2026-08-15T09:15:00+08:00", + dischargedAt: "2026-08-15T09:00:00+08:00", + handoverState: "Accepted", + agreementConfirmed: true, + }, +] satisfies readonly SyntheticReferral[]; + +export const syntheticPathways = [ + { + id: "SYN-PATHWAY-12M", + name: "Example twelve-month pathway", + version: "SYN-v0.3", + duration: "12 months", + cadence: [ + "Day 1", + "Week 1", + "Month 1", + "Month 2", + "Month 3", + "Month 4", + "Month 6", + "Month 8", + "Month 10", + "Month 12", + ], + senderLabel: "Example Aftercare Team", + governanceLabel: "Illustrative locally governed pathway", + approvalState: "Locally approved", + lifecycle: "Current", + approvalEvidence: { + clinicalProgrammeLead: "Taylor Fiction · clinical programme lead · approved 14 Aug 2026 at 4:10 pm AWST", + livedExperienceContentReviewer: + "Jordan Example · lived-experience/content reviewer · approved 14 Aug 2026 at 4:24 pm AWST", + }, + }, + { + id: "SYN-PATHWAY-RETIRED", + name: "Example retired twelve-month pathway", + version: "SYN-v0.2-retired", + duration: "12 months", + cadence: [ + "Day 1", + "Week 1", + "Month 1", + "Month 2", + "Month 3", + "Month 4", + "Month 6", + "Month 8", + "Month 10", + "Month 12", + ], + senderLabel: "Example Aftercare Team", + governanceLabel: "Illustrative locally governed pathway", + approvalState: "Locally approved", + lifecycle: "Retired", + approvalEvidence: { + clinicalProgrammeLead: "Taylor Fiction · clinical programme lead · approved 1 Aug 2026 at 2:10 pm AWST", + livedExperienceContentReviewer: + "Jordan Example · lived-experience/content reviewer · approved 1 Aug 2026 at 2:24 pm AWST", + }, + }, +] satisfies readonly SyntheticPathway[]; + +export const syntheticTeamMembers = [ + { id: "SYN-TEAM-001", displayName: "Alex Example", role: "Coordinator" }, + { id: "SYN-TEAM-002", displayName: "Sam Sample", role: "Team lead" }, + { id: "SYN-TEAM-003", displayName: "Taylor Fiction", role: "Authorised clinician" }, +] satisfies readonly SyntheticTeamMember[]; + +export const syntheticEpisodes = [ + { + id: "SYN-EPISODE-001", + patientId: "SYN-PATIENT-002", + referralId: "SYN-REFERRAL-002", + state: "Active", + coordinatorId: "SYN-TEAM-001", + pathwayId: "SYN-PATHWAY-12M", + openedAt: "2026-08-15T09:35:00+08:00", + selectedSendingPreference: ROWAN_SELECTED_SENDING_PREFERENCE, + }, +] satisfies readonly SyntheticEpisode[]; + +export const syntheticPlannedContacts = [ + ["Day 1", "2026-08-15T10:00:00+08:00", "Delivered"], + ["Week 1", "2026-08-22T10:00:00+08:00", "Scheduled"], + ["Month 1", "2026-09-15T10:00:00+08:00", "Scheduled"], + ["Month 2", "2026-10-15T10:00:00+08:00", "Scheduled"], + ["Month 3", "2026-11-15T10:00:00+08:00", "Scheduled"], + ["Month 4", "2026-12-15T10:00:00+08:00", "Scheduled"], + ["Month 6", "2027-02-15T10:00:00+08:00", "Scheduled"], + ["Month 8", "2027-04-15T10:00:00+08:00", "Scheduled"], + ["Month 10", "2027-06-15T10:00:00+08:00", "Scheduled"], + ["Month 12", "2027-08-15T10:00:00+08:00", "Scheduled"], +].map( + ([cadenceLabel, scheduledAt, transportState], index) => + ({ + id: `SYN-CONTACT-${String(index + 1).padStart(2, "0")}`, + episodeId: "SYN-EPISODE-001", + sequence: index + 1, + cadenceLabel, + scheduledAt, + window: ROWAN_SELECTED_SENDING_PREFERENCE.window, + windowLabel: ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel, + transportState, + }) as SyntheticPlannedContact, +); + +export const syntheticTemplates = [ + { + id: "SYN-TEMPLATE-001", + name: "Example first contact", + version: "SYN-copy-v1.0", + variant: "Warm neutral A", + segmentCount: 2, + encoding: "GSM-7", + approvalState: "Locally approved", + lifecycle: "Current", + approvalEvidence: { + clinicalProgrammeLead: "Taylor Fiction · clinical programme lead · approved 14 Aug 2026 at 4:10 pm AWST", + livedExperienceContentReviewer: + "Jordan Example · lived-experience/content reviewer · approved 14 Aug 2026 at 4:24 pm AWST", + }, + }, + { + id: "SYN-TEMPLATE-002", + name: "Example pending contact", + version: "SYN-copy-v0.2", + variant: "Warm neutral draft", + segmentCount: 2, + encoding: "GSM-7", + approvalState: "Illustrative — approval pending", + lifecycle: "Current", + approvalEvidence: null, + }, +] satisfies readonly SyntheticTemplate[]; + +export const syntheticDeliveryEvents = [ + { + id: "SYN-DELIVERY-001", + contactId: "SYN-CONTACT-01", + occurredAt: "2026-08-15T10:00:14+08:00", + state: "Delivered", + operationalNote: "Transport receipt recorded; this does not show that the contact was read.", + }, +] satisfies readonly SyntheticDeliveryEvent[]; + +export const syntheticAuditEvents = [ + { + id: "SYN-AUDIT-001", + occurredAt: "2026-08-15T09:35:00+08:00", + actorId: "SYN-TEAM-003", + action: "Accepted fictional referral", + objectId: "SYN-REFERRAL-002", + }, +] satisfies readonly SyntheticAuditEvent[]; diff --git a/src/components/caring-contacts/mockups/foundation-board.tsx b/src/components/caring-contacts/mockups/foundation-board.tsx new file mode 100644 index 0000000000..0683364f08 --- /dev/null +++ b/src/components/caring-contacts/mockups/foundation-board.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { ArrowRight, Check, Clock3, Layers3, Palette, Type } from "lucide-react"; +import { useState } from "react"; + +import { Button } from "@/components/ui/button"; + +import { syntheticReferrals } from "./fixtures"; +import { DefinitionRow, OperationalStatus, SpecimenLabel, SpecimenPanel } from "./mockup-primitives"; + +export function FoundationBoard() { + const referral = syntheticReferrals[0]; + const [selectedExample, setSelectedExample] = useState<"referral" | "schedule" | null>(null); + + return ( +
+
+ +
+ {[ + ["Canvas", "Primary working surface", "bg-[color:var(--surface)]"], + ["Raised", "Focused content region", "bg-[color:var(--surface-raised)]"], + ["Inset", "Supporting context", "bg-[color:var(--surface-inset)]"], + ].map(([name, description, colour]) => ( +
+ + ))} +
+
+ Type hierarchy +
+ Workspace + Panel heading + Supporting detail +
+
+ + + + Available actions +
+ + +
+ {selectedExample ? ( +

+ {selectedExample === "referral" + ? "Fictional referral assurance selected for review." + : "Approved AWST sending-window specimen selected."} +

+ ) : null} +
+

Named unavailable state

+

+ Activation remains unavailable until every source assurance is complete. The prototype does not perform + activation. +

+
+
+
+ +
+ +
+ Scheduled + Processing + Delivered + Status unavailable + Not delivered +
+
+ + Transport receipt recorded. It does not show that the contact was read. + + + Create the named operational task and keep patient state unstated. + + + The referring team remains responsible until explicit acceptance. + +
+
+ + +
+
+

Patient-controlled mobile flag missing

+ Needs action +
+

+ Request source clarification from {referral.referringTeam}. +

+

Owner: referring team

+
+

+ Fictional reference {referral.id}; agreement is recorded as Yes and is not represented as legal or treatment + consent. +

+
+
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/index.ts b/src/components/caring-contacts/mockups/index.ts new file mode 100644 index 0000000000..6648b3ad21 --- /dev/null +++ b/src/components/caring-contacts/mockups/index.ts @@ -0,0 +1,12 @@ +export { CaringContactDesignSuite } from "./caring-contact-design-suite"; +export { CaringContactShellFrame } from "./caring-contact-shell-frame"; +export { ContinuityThreadSpecimen } from "./continuity-thread-specimen"; +export { FoundationBoard } from "./foundation-board"; +export { FICTIONAL_DATA_MARKER } from "./fixtures"; +export { + APPROVED_CARING_CONTACT_ROUTE_IDENTITIES, + DESIGNATED_FICTIONAL_MOBILE_NUMBERS, + FICTIONAL_CONTACTS_BY_ROLE, + SUPERSEDED_CARING_CONTACT_ROUTE_PATTERNS, +} from "./types"; +export type * from "./types"; diff --git a/src/components/caring-contacts/mockups/mockup-primitives.tsx b/src/components/caring-contacts/mockups/mockup-primitives.tsx new file mode 100644 index 0000000000..55a46c7241 --- /dev/null +++ b/src/components/caring-contacts/mockups/mockup-primitives.tsx @@ -0,0 +1,64 @@ +import type { LucideIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +import { Chip, type ChipStatusTone } from "@/components/ui/chip"; + +export function SpecimenPanel({ + title, + description, + icon: Icon, + children, + className = "", +}: { + title: string; + description?: string; + icon?: LucideIcon; + children: ReactNode; + className?: string; +}) { + return ( +
+
+ {Icon ? ( + + + ) : null} +
+

{title}

+ {description ? ( +

{description}

+ ) : null} +
+
+ {children} +
+ ); +} + +export function SpecimenLabel({ children }: { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +export function OperationalStatus({ children, tone }: { children: ReactNode; tone: ChipStatusTone }) { + return ( + + {children} + + ); +} + +export function DefinitionRow({ term, children }: { term: string; children: ReactNode }) { + return ( +
+
{term}
+
{children}
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/overlay-specimens.tsx b/src/components/caring-contacts/mockups/overlay-specimens.tsx new file mode 100644 index 0000000000..57a7b83c65 --- /dev/null +++ b/src/components/caring-contacts/mockups/overlay-specimens.tsx @@ -0,0 +1,656 @@ +"use client"; + +import { AlertTriangle, CheckCircle2, Layers3, ShieldCheck } from "lucide-react"; +import { useEffect, useRef, useState, type RefObject } from "react"; + +import { Button } from "@/components/ui/button"; +import { Sheet } from "@/components/ui/sheet"; + +import { ROWAN_SELECTED_SENDING_PREFERENCE } from "./fixtures"; +import { PATIENT_VISIBLE_NO_REPLY_NOTICE } from "./personalisation-screen"; + +export type OverlayPhoneModality = "bottom-sheet" | "full-screen-stage" | "session-gate" | "status-banner"; +export type OverlayDesktopModality = "dialog" | "inspection-drawer" | "session-gate" | "status-banner"; +export type OverlayDismissal = "escape-backdrop-close" | "action-only" | "recovery-only"; + +export type OverlayDefinition = { + id: string; + label: string; + title: string; + summary: string; + content: string; + decision: string; + availability: "Available" | "Read only" | "Unavailable until resolved"; + mutatesState: boolean; + phoneModality: OverlayPhoneModality; + desktopModality: OverlayDesktopModality; + dismissal: OverlayDismissal; + tone?: "primary" | "danger"; +}; + +export const completionOverlayDefinitions = [ + { + id: "verify-identity", + label: "Verify identity", + title: "Verify identity before changing patient", + summary: "Compare the selected identity with the fictional source record before continuing.", + content: + "Rowan Sample · SYN-PATIENT-002 · born 3 November 1987. The selected patient remains visible until confirmed.", + decision: "Confirm fictional identity", + availability: "Available", + mutatesState: true, + phoneModality: "full-screen-stage", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "primary", + }, + { + id: "change-patient", + label: "Change patient", + title: "Change selected patient", + summary: "Changing patient clears the current activation draft and all object-specific selections.", + content: "Current fictional selection: Rowan Sample. No patient context carries into the replacement search.", + decision: "Change patient", + availability: "Available", + mutatesState: true, + phoneModality: "full-screen-stage", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "danger", + }, + { + id: "pathway-preview", + label: "Pathway preview", + title: "Preview governed pathway", + summary: "Inspect the current locally approved pathway without ranking it as best or recommended.", + content: "Example twelve-month pathway · SYN-v0.3 · 10 contacts · one-way sender · two-person approval complete.", + decision: "Use this pathway", + availability: "Available", + mutatesState: true, + phoneModality: "full-screen-stage", + desktopModality: "inspection-drawer", + dismissal: "escape-backdrop-close", + }, + { + id: "message-preview", + label: "Message preview", + title: "Preview exact patient-visible message", + summary: "The fully substituted message is visible exactly as the patient would receive it.", + content: `GSM-7 · 272 septets · 2 of 2 SMS segments · ${PATIENT_VISIBLE_NO_REPLY_NOTICE}.`, + decision: "Return to personalisation", + availability: "Read only", + mutatesState: false, + phoneModality: "full-screen-stage", + desktopModality: "inspection-drawer", + dismissal: "escape-backdrop-close", + }, + { + id: "communication-preference", + label: "Communication preference", + title: "Communication preference", + summary: "Record only a governed preference received through the staffed programme phone.", + content: `Current plan preference: ${ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel}. Proposed change: Early evening 5:00 pm AWST. The proposal does not change the original cadence and is not recorded by this specimen.`, + decision: "Record preference", + availability: "Available", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + }, + { + id: "adjust-date-time", + label: "Adjust date/time", + title: "Adjust contact time", + summary: "A coordinator may move a contact only within its already scheduled day.", + content: + "Proposed one-contact exception: move contact 2 from 10:00 am to 2:00 pm AWST on Saturday 22 August 2026. The plan preference remains Morning; a date change needs a reason and team-lead approval.", + decision: "Save time within scheduled day", + availability: "Available", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "primary", + }, + { + id: "outside-window-warning", + label: "Outside-window warning", + title: "Outside approved sending window", + summary: "The requested time is outside 9:00 am–6:00 pm AWST and cannot be scheduled.", + content: "7:30 pm AWST is unavailable. Keep one of the governed Morning, Afternoon or Early evening times.", + decision: "Keep approved time", + availability: "Unavailable until resolved", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "primary", + }, + { + id: "save-draft", + label: "Save draft", + title: "Save activation draft", + summary: "Save the incomplete fictional activation without sending or activating anything.", + content: + "The draft retains the selected patient and governed versions. It remains unavailable for sending until every assurance passes.", + decision: "Save draft", + availability: "Available", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "primary", + }, + { + id: "discard-changes", + label: "Discard changes", + title: "Discard unsaved changes", + summary: "Discard only changes made in this session; the frozen approved versions remain unchanged.", + content: "Preferred-name and timing edits will be removed. Existing plan and audit history are not changed.", + decision: "Discard changes", + availability: "Available", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "danger", + }, + { + id: "final-activation", + label: "Final activation", + title: "Final activation assurance", + summary: "Recheck identity, agreement, ownership, governed versions, exact text and every AWST send time.", + content: + "Fresh authentication and atomic activation are future production contracts; this synthetic action records review only.", + decision: "Confirm activation review", + availability: "Available", + mutatesState: true, + phoneModality: "full-screen-stage", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "primary", + }, + { + id: "activation-success", + label: "Activation success", + title: "Plan activation recorded", + summary: "A privacy-safe outcome confirms the operational event without patient information.", + content: + "Outcome: “Plan activation recorded. View the plan for schedule and audit detail.” No message has been sent by this prototype.", + decision: "View activated plan", + availability: "Read only", + mutatesState: false, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + }, + { + id: "pause", + label: "Pause", + title: "Pause caring-contact plan", + summary: "Pause is reversible but never rebases the original calendar.", + content: + "Contacts inside the pause are permanently skipped. Resumption begins with the next future scheduled contact.", + decision: "Pause future contacts", + availability: "Available", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "danger", + }, + { + id: "withdrawal", + label: "Withdrawal", + title: "Record patient-requested withdrawal", + summary: "Withdrawal immediately cancels every unsent contact and needs no approval.", + content: + "The terminal withdrawal record and immutable history remain visible. Fresh authentication is required before mutation; there is no undo action.", + decision: "Continue to fresh authentication", + availability: "Available", + mutatesState: true, + phoneModality: "full-screen-stage", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "danger", + }, + { + id: "reassignment", + label: "Reassignment", + title: "Reassign plan coordinator", + summary: "Reassignment changes coordination ownership and retains the complete handover history.", + content: + "Current coordinator: Alex Example. Proposed coordinator: Sam Sample. Fresh authentication is required before mutation; duty-of-care claims are not inferred from assignment.", + decision: "Continue to fresh authentication", + availability: "Available", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "primary", + }, + { + id: "delivery-detail", + label: "Delivery detail", + title: "Delivery transport detail", + summary: "Inspect provider-neutral transport evidence without a clinical or patient-state inference.", + content: + "Delivered at 10:00:14 am AWST means transport receipt only. It does not show that the message was read or helped.", + decision: "Close transport detail", + availability: "Read only", + mutatesState: false, + phoneModality: "full-screen-stage", + desktopModality: "inspection-drawer", + dismissal: "escape-backdrop-close", + }, + { + id: "resolve-failed-delivery", + label: "Resolve failed delivery", + title: "Resolve permanent delivery failure", + summary: "Three attempts in the original window are complete; no additional or late retry is allowed.", + content: + "Future contacts remain paused. Record a same-day operational resolution; do not create automatic clinical follow-up.", + decision: "Record operational resolution", + availability: "Available", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "primary", + }, + { + id: "contact-changed-block", + label: "Contact-changed block", + title: "Contact destination changed", + summary: "A source mobile change pauses future contacts for coordinator review.", + content: + "The destination must never switch silently. Review source provenance and keep the plan paused until resolved.", + decision: "Keep plan paused", + availability: "Unavailable until resolved", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "danger", + }, + { + id: "template-changed-retired", + label: "Template changed/retired", + title: "Governed template is no longer current", + summary: "The selected template was retired after this draft opened.", + content: + "The frozen retired version remains readable in history. New activation requires a current, locally approved version.", + decision: "Choose current version", + availability: "Unavailable until resolved", + mutatesState: true, + phoneModality: "full-screen-stage", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "primary", + }, + { + id: "session-expiry", + label: "Session expiry", + title: "Session expired", + summary: "The managed session ended before a protected decision could complete.", + content: + "No mutation was recorded. Sign in through WA Health SSO/MFA before continuing; local credentials are absent.", + decision: "Sign in again", + availability: "Unavailable until resolved", + mutatesState: false, + phoneModality: "session-gate", + desktopModality: "session-gate", + dismissal: "action-only", + }, + { + id: "offline-banner", + label: "Offline banner", + title: "Offline operation unavailable", + summary: "Downtime fails closed and patient data is not cached for offline use.", + content: + "Activation, mutation and uncertain resend are unavailable. Existing synthetic read-only context may remain visible.", + decision: "Try reconnecting", + availability: "Unavailable until resolved", + mutatesState: false, + phoneModality: "status-banner", + desktopModality: "status-banner", + dismissal: "recovery-only", + }, + { + id: "recoverable-error", + label: "Recoverable error", + title: "Operational records could not be loaded", + summary: "The error is recoverable and does not change plan or delivery state.", + content: + "Retry the same read. If the error persists, return to Today; never guess or display stale state as current.", + decision: "Retry loading records", + availability: "Available", + mutatesState: false, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + }, + { + id: "permission-unavailable", + label: "Permission unavailable", + title: "Permission unavailable", + summary: "The current synthetic role cannot perform this protected action.", + content: + "Return to the plan and contact the team lead. Access remains deny-by-default and the attempted access is audited.", + decision: "Return to plan", + availability: "Unavailable until resolved", + mutatesState: false, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + }, + { + id: "team-switcher", + label: "Team switcher", + title: "Switch active team", + summary: "Switching team clears the selected patient and every patient-specific draft before new context renders.", + content: + "Current team: Example Aftercare Team. Destination: Fictional Coverage Team. No patient context crosses teams.", + decision: "Clear context and switch team", + availability: "Available", + mutatesState: true, + phoneModality: "bottom-sheet", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "danger", + }, + { + id: "draft-version-conflict", + label: "Draft/version conflict", + title: "Resolve draft version conflict", + summary: "A governed template changed after this draft opened.", + content: "Compare the frozen draft with the current approved version. Do not silently overwrite either record.", + decision: "Review current version", + availability: "Unavailable until resolved", + mutatesState: false, + phoneModality: "full-screen-stage", + desktopModality: "dialog", + dismissal: "escape-backdrop-close", + tone: "primary", + }, +] satisfies readonly OverlayDefinition[]; + +export const completionMutationOverlayLabels = completionOverlayDefinitions + .filter(({ mutatesState }) => mutatesState) + .map(({ label }) => label); + +function SpecificOverlayContent({ + definition, + modality, +}: { + definition: OverlayDefinition; + modality: OverlayPhoneModality | OverlayDesktopModality; +}) { + return ( +
+

+ {definition.availability === "Available" ? ( +

+

{definition.content}

+

+ Privacy-safe outcome only: no patient name, identifier, phone number or message text enters a toast, page title + or external alert. +

+
+ ); +} + +function SessionExpiryGate({ + open, + definition, + onReauthenticate, + returnFocusRef, + compact, +}: { + open: boolean; + definition: OverlayDefinition; + onReauthenticate: () => void; + returnFocusRef: RefObject; + compact: boolean; +}) { + const actionRef = useRef(null); + + return ( + undefined} + title={definition.title} + description={definition.summary} + closeLabel="Session expired — sign in required" + closeButtonClassName="hidden" + initialFocusRef={actionRef} + returnFocusRef={returnFocusRef} + mobilePlacement="fullscreen" + contentStyle={ + !compact ? { width: "100%", height: "auto", maxHeight: "calc(100dvh - 3rem)", maxWidth: "32rem" } : undefined + } + contentClassName="lg:h-auto lg:max-h-[calc(100dvh-3rem)] lg:max-w-lg lg:rounded-2xl lg:border" + testId="caring-contact-session-expiry-gate" + footer={ +
+ +
+ } + > + +
+ ); +} + +export function OverlaySpecimens({ + offline, + onOfflineChange, +}: { + offline: boolean; + onOfflineChange: (offline: boolean) => void; +}) { + const [openId, setOpenId] = useState(null); + const [compact, setCompact] = useState(true); + const activeTriggerRef = useRef(null); + const active = completionOverlayDefinitions.find(({ id }) => id === openId) ?? null; + const offlineDefinition = completionOverlayDefinitions.find( + ({ phoneModality }) => phoneModality === "status-banner", + )!; + const activeSessionGate = active?.phoneModality === "session-gate" ? active : null; + const activeOverlay = + active && active.phoneModality !== "session-gate" && active.phoneModality !== "status-banner" ? active : null; + const activeModality = activeOverlay ? (compact ? activeOverlay.phoneModality : activeOverlay.desktopModality) : null; + const activeActionUnavailable = offline && Boolean(activeOverlay?.mutatesState); + const activeUsesInspectionDrawer = activeModality === "inspection-drawer"; + const activeUsesFullScreenStage = activeModality === "full-screen-stage"; + + useEffect(() => { + const media = window.matchMedia("(max-width: 767px)"); + const sync = () => setCompact(media.matches); + sync(); + media.addEventListener("change", sync); + return () => media.removeEventListener("change", sync); + }, []); + + return ( +
+
+
+ +
+
+
+
    + {completionOverlayDefinitions.map((definition, index) => { + const unavailableOffline = offline && definition.mutatesState; + return ( +
  1. + {index + 1} + +
  2. + ); + })} +
+
+ + {offline ? ( +
+

{offlineDefinition.title}

+

+ {offlineDefinition.content} Governed mutation is unavailable while offline. +

+
+

Offline status remains visible until connectivity is restored.

+ +
+
+ ) : null} + + setOpenId(null)} + title={activeOverlay?.title ?? "Overlay specimen"} + description={activeOverlay?.summary} + returnFocusRef={activeTriggerRef} + closeLabel="Close overlay specimen" + mobilePlacement={activeUsesFullScreenStage ? "fullscreen" : "bottom"} + desktopBackdropClassName={activeUsesInspectionDrawer ? "lg:!items-stretch lg:!justify-end lg:!p-0" : undefined} + contentStyle={ + activeUsesInspectionDrawer + ? { + position: "fixed", + inset: "0 0 0 auto", + height: "100dvh", + maxHeight: "100dvh", + maxWidth: "32rem", + borderRadius: "1rem 0 0 1rem", + } + : undefined + } + contentClassName={ + activeUsesInspectionDrawer + ? "lg:fixed lg:inset-y-0 lg:right-0 lg:h-dvh lg:max-h-dvh lg:max-w-[32rem] lg:rounded-none lg:rounded-l-2xl lg:border-y-0 lg:border-r-0" + : undefined + } + testId={ + activeModality === "full-screen-stage" + ? "completion-fullscreen-stage" + : activeModality === "inspection-drawer" + ? "completion-inspection-drawer" + : activeModality === "bottom-sheet" + ? "completion-bottom-sheet" + : "completion-desktop-dialog" + } + footer={ + activeOverlay ? ( +
+ +
+ ) : undefined + } + > + {activeOverlay && activeModality ? ( + <> + + {activeActionUnavailable ? ( +

+ Unavailable while offline — reconnect before recording this decision. +

+ ) : null} + + ) : null} +
+ + setOpenId(null)} + returnFocusRef={activeTriggerRef} + compact={compact} + /> +
+ ); +} diff --git a/src/components/caring-contacts/mockups/pathway-selection-screen.tsx b/src/components/caring-contacts/mockups/pathway-selection-screen.tsx new file mode 100644 index 0000000000..2bb2dde4ba --- /dev/null +++ b/src/components/caring-contacts/mockups/pathway-selection-screen.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { ArrowLeft, ArrowRight, CalendarRange, CheckCircle2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; + +import { syntheticPathways, syntheticPatients } from "./fixtures"; +import { DefinitionRow, OperationalStatus } from "./mockup-primitives"; +import { CompactMessagePreview, MessagePreview, PATIENT_VISIBLE_NO_REPLY_NOTICE } from "./personalisation-screen"; + +export function PathwaySelectionScreen({ onBack, onContinue }: { onBack: () => void; onContinue: () => void }) { + const patient = syntheticPatients[1]; + const pathway = syntheticPathways[0]; + + return ( +
+
+

+ Patient identity remains in flow +

+

+ {patient.fullName} · {patient.id} · 3 Nov 1987 +

+
+ +
+
+

+ Stage 2 of 4 +

+

+ Pathway selection +

+

+ Select a locally governed version without ranking or implying clinical effectiveness. +

+ +
+
+
+
+
+

Version {pathway.version}

+
+ Selected +
+
+ + {pathway.governanceLabel} + + {pathway.duration} · 10 contacts + Day 1, week 1, then months 1, 2, 3, 4, 6, 8, 10 and 12 + {pathway.senderLabel} · neutral, non-receiving sender + + One-way caring contacts supplement usual care. {PATIENT_VISIBLE_NO_REPLY_NOTICE}. + + + Two-person approval complete + {pathway.approvalEvidence?.clinicalProgrammeLead} + {pathway.approvalEvidence?.livedExperienceContentReviewer} + +
+

+

+
+ +
+ +
+ + +
+
+
+ + +
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/patient-agreement-screen.tsx b/src/components/caring-contacts/mockups/patient-agreement-screen.tsx new file mode 100644 index 0000000000..28646625dd --- /dev/null +++ b/src/components/caring-contacts/mockups/patient-agreement-screen.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { ArrowRight, CheckCircle2, ClipboardCheck, ShieldCheck } from "lucide-react"; + +import { Button } from "@/components/ui/button"; + +import { syntheticPatients, syntheticReferrals, syntheticTeamMembers } from "./fixtures"; +import { DefinitionRow, OperationalStatus } from "./mockup-primitives"; +import { CompactMessagePreview, MessagePreview } from "./personalisation-screen"; + +export function PatientAgreementScreen({ onContinue }: { onContinue: () => void }) { + const patient = syntheticPatients[1]; + const referral = syntheticReferrals[1]; + const coordinator = syntheticTeamMembers[0]; + + return ( +
+
+

+ Patient identity remains in flow +

+

+ {patient.fullName} · {patient.id} · 3 Nov 1987 +

+
+ +
+
+
+
+

+ Stage 1 of 4 +

+

+ Patient and agreement +

+

+ Repeat the imported identity, mobile suitability, agreement and explicit ownership before pathway + selection. +

+
+ All four assurances present +
+ +
+ + {patient.fullName} · {patient.id} · imported referral {referral.id} + + + + {patient.mobile} · Imported from {patient.mobileSource} + + + + + + + + Agreement confirmed: Yes · imported source + record, not legal or treatment consent + + + Owning team: Example Aftercare Team · + referral explicitly accepted + + + Coordinator: {coordinator.displayName} · + explicitly assigned + +
+ +
+

+

+

+ Agreement, patient-controlled mobile, owning team and coordinator are present. Any missing assurance would + name its remedy and block progression. +

+
+ +
+ + +
+
+ + +
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/patient-boundary-screens.tsx b/src/components/caring-contacts/mockups/patient-boundary-screens.tsx new file mode 100644 index 0000000000..ba02db1bc3 --- /dev/null +++ b/src/components/caring-contacts/mockups/patient-boundary-screens.tsx @@ -0,0 +1,730 @@ +"use client"; + +import { + Ban, + CalendarDays, + CircleOff, + ClipboardList, + FileCheck2, + Link2, + PauseCircle, + Search, + ShieldAlert, + UserRoundSearch, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; +import { Sheet } from "@/components/ui/sheet"; + +import { syntheticPlannedContacts } from "./fixtures"; +import { OverlaySpecimens } from "./overlay-specimens"; +import { DefinitionRow, OperationalStatus } from "./mockup-primitives"; + +const boundaryRecords = [ + { + title: "Duplicate referral blocked", + detail: "Link SYN-REFERRAL-003 to the active episode; a second active plan cannot be created.", + status: "Link referral to active episode", + icon: Link2, + }, + { + title: "Readmission pause", + detail: "Future contacts pause from the source event. A later discharge needs a new linked referral.", + status: "Paused by source event", + icon: PauseCircle, + }, + { + title: "Recorded death — irreversible cancellation", + detail: "All unsent contacts are cancelled. Correction is an incident; any future plan needs a new referral.", + status: "Cancelled irreversibly", + icon: CircleOff, + }, + { + title: "Wrong-recipient incident", + detail: "Pause the entire pilot, preserve evidence and follow the joint incident restart authority.", + status: "Pilot stop", + icon: ShieldAlert, + }, + { + title: "Contact changed", + detail: "Pause future contacts for coordinator review; never silently switch the destination.", + status: "Review required", + icon: Ban, + }, +] as const; + +type ContactDetailState = "Scheduled" | "Processing" | "Sent" | "Delivered" | "Named exception"; + +const contactDetailStates: Record< + ContactDetailState, + { tone: "neutral" | "warning" | "info" | "success" | "danger"; detail: string; evidence: string } +> = { + Scheduled: { + tone: "neutral", + detail: "Saturday 22 August 2026 at 10:00 am AWST. A coordinator may move it only within this scheduled day.", + evidence: "No transport attempt exists. Date changes require a reason and team-lead approval.", + }, + Processing: { + tone: "warning", + detail: "Provider-neutral processing began at 2:00:03 pm AWST. It is too late to change or cancel this contact.", + evidence: "Await a signed transport event. Do not infer receipt, safety, wellbeing or response.", + }, + Sent: { + tone: "info", + detail: "Accepted for transport at 2:00:08 pm AWST. Sent is not a delivery receipt.", + evidence: "No resend occurs while status is uncertain. Retain event and webhook evidence.", + }, + Delivered: { + tone: "success", + detail: "Transport receipt recorded at 2:00:14 pm AWST. Delivered means transport receipt only.", + evidence: "It does not show that the message was read, helped, or reflected the patient’s state.", + }, + "Named exception": { + tone: "danger", + detail: "Not delivered after three attempts in the original window. Future contacts are paused.", + evidence: "Create a same-day operational task. No automatic clinical follow-up or late resend.", + }, +}; + +const perthDateFormatter = new Intl.DateTimeFormat("en-AU", { + timeZone: "Australia/Perth", + weekday: "long", + day: "numeric", + month: "long", + year: "numeric", +}); + +const planOfflineReason = "Unavailable while offline — reconnect before changing this plan."; +const planOfflineTransitionNotice = + "Connectivity was lost. The open plan decision closed without recording a change. Reconnect to continue."; + +function OfflinePlanDecisionReason({ offline }: { offline: boolean }) { + return offline ? ( +

+ {planOfflineReason} +

+ ) : null; +} + +function PlanAndContactDetail({ offline }: { offline: boolean }) { + const [contactState, setContactState] = useState("Scheduled"); + const [coordinator, setCoordinator] = useState("Alex Example"); + const [reassignmentOpen, setReassignmentOpen] = useState(false); + const [reassignmentAuthenticated, setReassignmentAuthenticated] = useState(false); + const [pauseOpen, setPauseOpen] = useState(false); + const [withdrawalOpen, setWithdrawalOpen] = useState(false); + const [cancelOpen, setCancelOpen] = useState(false); + const [compact, setCompact] = useState(true); + const [correctionOpen, setCorrectionOpen] = useState(false); + const [correctionRecorded, setCorrectionRecorded] = useState(false); + const [actionNotice, setActionNotice] = useState(""); + const reassignmentRef = useRef(null); + const pauseRef = useRef(null); + const withdrawalRef = useRef(null); + const cancelRef = useRef(null); + const correctionRef = useRef(null); + const selectedContact = contactDetailStates[contactState]; + + useEffect(() => { + const media = window.matchMedia("(max-width: 767px)"); + const sync = () => setCompact(media.matches); + sync(); + media.addEventListener("change", sync); + return () => media.removeEventListener("change", sync); + }, []); + + useEffect(() => { + if (!offline) return; + const closeDesktopWithdrawal = withdrawalOpen && !compact; + if (!pauseOpen && !cancelOpen && !closeDesktopWithdrawal) return; + setPauseOpen(false); + setCancelOpen(false); + if (closeDesktopWithdrawal) setWithdrawalOpen(false); + setActionNotice(planOfflineTransitionNotice); + }, [cancelOpen, compact, offline, pauseOpen, withdrawalOpen]); + + const offlineMutationProps = (reasonId = "caring-contact-plan-offline-reason") => + offline + ? { + "aria-disabled": true as const, + "aria-describedby": reasonId, + "data-mutates-state": "true", + title: "Unavailable while offline — reconnect to continue", + } + : { "data-mutates-state": "true" }; + + const commitPlanMutation = (mutation: () => void) => { + if (offline) { + setActionNotice(planOfflineTransitionNotice); + return false; + } + mutation(); + return true; + }; + + return ( +
+
+
+ +
+
+

Plan identity and ownership

+
+ Rowan Sample · SYN-PATIENT-002 · born 3 November 1987 + + Active + + Example Aftercare Team + Coordinator: {coordinator} + + 15 August 2026 at 9:35 am AWST · referring-team history retained + +
+
+
+

Frozen governed versions

+
+ Example twelve-month pathway · SYN-v0.3 · locally approved + Example first contact · SYN-copy-v1.0 · frozen snapshot + Example Aftercare Team · non-receiving sender + GSM-7 · 272 septets · 2 of 2 SMS segments +
+
+
+ +
+
+
+
    + {syntheticPlannedContacts.slice(1, 4).map((contact) => ( +
  1. + {contact.cadenceLabel} + + {contact.windowLabel} + + {contact.id} · {contact.transportState} + +
  2. + ))} +
+
+ +
+
+
+
+ +
+

Plan actions

+
+ + + + +
+ {offline ? ( +

+ {planOfflineReason} +

+ ) : null} + {actionNotice ? ( +

+ {actionNotice} +

+ ) : null} +
+ +
+
+

Terminal plan — read only

+

+ Withdrawn 4 September 2026 at 11:12 am AWST · unsent contacts cancelled · immutable history retained. +

+ +
+
+

Partial source data

+

+ Patient-controlled and suitable-for-discreet-SMS evidence: Not supplied. Activation remains unavailable. +

+

+ Remedy: referring team supplies the source flag; Callback does not infer it. +

+
+
+ +
+
+
+

Corrected-death governance

+

+ Recorded death irreversibly cancelled unsent contacts. A correction is an incident, not an undo or + reinstatement. +

+
+ +
+

+ {correctionRecorded + ? "Correction incident recorded in audit. Existing plan remains cancelled; any future plan requires a new referral and approved restart path." + : "No correction incident recorded. Existing plan remains cancelled and read only."} +

+
+ +
+

Contact detail

+

+ General transport-state composition for contact SYN-CONTACT-02. +

+
+ {(Object.keys(contactDetailStates) as ContactDetailState[]).map((state) => ( + + ))} +
+
+
+
Contact 2 of 10
+ {contactState} +
+

{selectedContact.detail}

+

{selectedContact.evidence}

+
+
+ + setPauseOpen(false)} + onConfirm={() => { + commitPlanMutation(() => { + setActionNotice( + "Pause decision recorded for this synthetic review; contacts inside the pause are skipped.", + ); + setPauseOpen(false); + }); + }} + title="Pause caring-contact plan" + description="Pause is reversible, but the original discharge-anchored calendar is never rebased. Contacts inside the pause are permanently skipped." + confirmLabel="Pause future contacts" + cancelLabel="Keep plan active" + tone="danger" + /> + + setWithdrawalOpen(false)} + title="Record patient-requested withdrawal" + description="Withdrawal immediately cancels every unsent contact and has no undo." + returnFocusRef={withdrawalRef} + mobilePlacement="fullscreen" + testId="plan-withdrawal-fullscreen-stage" + footer={ +
+ +
+ } + > +
+

Fresh authentication is required before mutation.

+

After authentication, recheck identity and the patient-requested withdrawal source before recording it.

+

The terminal record and immutable history remain visible; there is no undo control.

+ +
+
+ + setWithdrawalOpen(false)} + onConfirm={() => { + commitPlanMutation(() => { + setActionNotice("Fresh authentication requested; no withdrawal mutation was recorded by this prototype."); + setWithdrawalOpen(false); + }); + }} + title="Record patient-requested withdrawal" + description="Fresh authentication is required before mutation. After authentication, recheck identity and the patient-requested withdrawal source. The terminal record has no undo." + confirmLabel="Continue to fresh authentication" + cancelLabel="Keep plan active" + tone="danger" + /> + + setCancelOpen(false)} + onConfirm={() => { + commitPlanMutation(() => { + setActionNotice("Authorised cancellation decision recorded for this synthetic review."); + setCancelOpen(false); + }); + }} + title="Cancel caring-contact plan" + description="Cancel every future contact for the recorded authorised reason and preserve the complete immutable audit history." + confirmLabel="Cancel future contacts" + cancelLabel="Keep plan active" + tone="danger" + /> + + setReassignmentOpen(false)} + title="Reassign active plan" + description="Change coordination ownership while retaining handover and audit history." + returnFocusRef={reassignmentRef} + mobilePlacement="fullscreen" + footer={ +
+ +
+ } + > +
+ Alex Example + Sam Sample · team lead + Record actor, time, previous and new coordinator + Fresh authentication is required before mutation + + Assignment identifies coordination; it does not independently transfer duty of care + +
+ {reassignmentAuthenticated ? ( +

+ Fresh authentication confirmed for this synthetic decision. Recheck the new coordinator before confirming. +

+ ) : null} + +
+ + setCorrectionOpen(false)} + title="Recorded-death correction governance" + description="A source correction is an incident and cannot reinstate the cancelled plan." + returnFocusRef={correctionRef} + mobilePlacement="fullscreen" + footer={ +
+ +
+ } + > +
+

Preserve the original source event, irreversible cancellation and every audit entry.

+

Reconcile the correction through the incident lead, privacy/security owner and clinical programme lead.

+

Any future caring-contact plan needs a new referral. There is no reinstate or undo control.

+ +
+
+
+ ); +} + +export function PatientBoundaryScreens({ onBack }: { onBack: () => void }) { + const [emptySearch, setEmptySearch] = useState(false); + const [detailsOpen, setDetailsOpen] = useState(false); + const [offline, setOffline] = useState(false); + + useEffect(() => { + const handleOffline = () => setOffline(true); + const handleOnline = () => setOffline(false); + window.addEventListener("offline", handleOffline); + window.addEventListener("online", handleOnline); + return () => { + window.removeEventListener("offline", handleOffline); + window.removeEventListener("online", handleOnline); + }; + }, []); + + return ( +
+
+
+

+ Patient and episode boundaries +

+

+ Boundary and lifecycle specimens +

+

+ Search is limited to the active pilot team’s referrals and Callback episodes. No shared Clinical KB search, + recent-search or analytics path is used. +

+
+ +
+ +
+
+
+ +
+ + + +
+ {emptySearch ? ( +
+

No pilot-team records found

+

+ Check the fictional details or return to the referring service; do not broaden into global search. +

+
+ ) : ( +
+
+

Rowan Sample

+

+ SYN-PATIENT-002 · born 3 November 1987 · active pilot team +

+
+ Identity assurance required +
+ )} +
+ +
+

Boundary states

+
+ {boundaryRecords.map(({ title, detail, status, icon: Icon }) => ( +
+
+ ))} +
+
+ +
+
+

Pause plan

+

+ Reversible. Preserve the calendar, permanently skip contacts inside the pause and resume at the next future + contact. +

+
+
+

Record withdrawal

+

+ Patient preference. Immediately cancel unsent contacts; no approval is required and history is immutable. +

+
+
+

Cancel plan

+

+ A distinct authorised operational action with a recorded reason and audit event. +

+
+
+

Plan and contact detail

+

+ Open the complete plan, terminal, partial-data, corrected-death, reassignment and contact-state + compositions. +

+ +
+
+ + {detailsOpen ? : null} + + +
+ ); +} diff --git a/src/components/caring-contacts/mockups/patient-overview-screen.tsx b/src/components/caring-contacts/mockups/patient-overview-screen.tsx new file mode 100644 index 0000000000..26d5da36cc --- /dev/null +++ b/src/components/caring-contacts/mockups/patient-overview-screen.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { ArrowLeft, CalendarDays, ClipboardCheck, UserRoundCheck } from "lucide-react"; + +import { Button } from "@/components/ui/button"; + +import { ContinuityThreadSpecimen } from "./continuity-thread-specimen"; +import { + ROWAN_SELECTED_SENDING_PREFERENCE, + syntheticAuditEvents, + syntheticEpisodes, + syntheticPatients, + syntheticReferrals, +} from "./fixtures"; +import { DefinitionRow, OperationalStatus } from "./mockup-primitives"; + +export function PatientOverviewScreen({ onBack }: { onBack: () => void }) { + const patient = syntheticPatients[1]; + const referral = syntheticReferrals[1]; + const episode = syntheticEpisodes[0]; + const auditEvent = syntheticAuditEvents[0]; + + return ( +
+
+
+
+

+ Fictional patient record +

+

+ Patient overview +

+

+ {patient.fullName} · {patient.id} +

+

+ Date of birth 3 Nov 1987 · active fictional episode {episode.id} +

+
+ Active plan +
+
+ + Example Aftercare Team · referral accepted from {referral.referringTeam} + + Alex Example · explicitly assigned + Agreement confirmed: Yes · imported source evidence + + Patient-controlled and suitable for discreet SMS · imported from {patient.mobileSource} + + + {ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel} · all 10 planned contacts + +
+
+ +
+
+ + + +
+
+
+
    +
  1. +
  2. +
  3. +
  4. +
+
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/personalisation-screen.tsx b/src/components/caring-contacts/mockups/personalisation-screen.tsx new file mode 100644 index 0000000000..ca89257ea4 --- /dev/null +++ b/src/components/caring-contacts/mockups/personalisation-screen.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { ArrowLeft, ArrowRight, Eye, LockKeyhole, MessageSquareText } from "lucide-react"; +import { useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Sheet } from "@/components/ui/sheet"; + +import { + ROWAN_SELECTED_SENDING_PREFERENCE, + syntheticPatients, + syntheticTeamMembers, + syntheticTemplates, +} from "./fixtures"; +import { DefinitionRow, OperationalStatus } from "./mockup-primitives"; +import { FICTIONAL_CONTACTS_BY_ROLE, type SyntheticPathway, type SyntheticTemplate } from "./types"; + +export const PATIENT_VISIBLE_NO_REPLY_NOTICE = "Replies are not received, stored, analysed or monitored"; + +export const EXACT_PATIENT_VISIBLE_MESSAGE = `Hi Rowan, Alex from Example Aftercare Team is thinking of you. This is a one-way message. ${PATIENT_VISIBLE_NO_REPLY_NOTICE}. For timing changes call ${FICTIONAL_CONTACTS_BY_ROLE.programmeStaffedLine}, 9 am-6 pm. In an emergency call 000. Fictional Support Line: ${FICTIONAL_CONTACTS_BY_ROLE.crisisSupportContact}. - Alex`; + +const GSM_7_BASIC_CHARACTERS = new Set( + "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà", +); +const GSM_7_EXTENSION_CHARACTERS = new Set("\f^{}\\[~]|€"); + +export type Gsm7Evidence = { + valid: boolean; + septets: number; + segments: number; + invalidCharacters: string[]; +}; + +export function calculateGsm7(value: string): Gsm7Evidence { + let septets = 0; + const invalidCharacters: string[] = []; + + for (const character of value) { + if (GSM_7_BASIC_CHARACTERS.has(character)) septets += 1; + else if (GSM_7_EXTENSION_CHARACTERS.has(character)) septets += 2; + else if (!invalidCharacters.includes(character)) invalidCharacters.push(character); + } + + if (invalidCharacters.length > 0) return { valid: false, septets, segments: 0, invalidCharacters }; + const segments = septets === 0 ? 0 : septets <= 160 ? 1 : Math.ceil(septets / 153); + return { valid: true, septets, segments, invalidCharacters }; +} + +export const EXACT_MESSAGE_GSM7 = calculateGsm7(EXACT_PATIENT_VISIBLE_MESSAGE); + +export type ActivationGovernanceState = { + pathway: SyntheticPathway; + template: SyntheticTemplate; +}; + +export type ActivationBlocker = { reason: string; remedy: string }; + +function blockersForVersion(label: "Pathway" | "Message", version: SyntheticPathway | SyntheticTemplate) { + const blockers: ActivationBlocker[] = []; + if (version.lifecycle !== "Current") { + blockers.push({ + reason: `${label} version is retired.`, + remedy: `Select a current, locally approved ${label.toLowerCase()} version with complete two-person approval evidence.`, + }); + } + if (version.approvalState !== "Locally approved") { + blockers.push({ + reason: `${label} version is awaiting two-person approval.`, + remedy: `Select a current, locally approved ${label.toLowerCase()} version with complete two-person approval evidence.`, + }); + } else if (!version.approvalEvidence) { + blockers.push({ + reason: `${label} version has incomplete two-person approval evidence.`, + remedy: `Select a current, locally approved ${label.toLowerCase()} version with complete two-person approval evidence.`, + }); + } + return blockers; +} + +export function getActivationBlockers({ pathway, template }: ActivationGovernanceState) { + return [...blockersForVersion("Pathway", pathway), ...blockersForVersion("Message", template)]; +} + +export function canActivateGovernedVersions(governance: ActivationGovernanceState) { + return getActivationBlockers(governance).length === 0; +} + +export function MessagePreview({ compact = false }: { compact?: boolean }) { + return ( +
+
+

+ Exact patient-visible message +

+ + {EXACT_MESSAGE_GSM7.septets} septets · {EXACT_MESSAGE_GSM7.segments} of 2 SMS segments + +
+
+ {EXACT_PATIENT_VISIBLE_MESSAGE} +
+

+

+
+ ); +} + +export function CompactMessagePreview({ stageName }: { stageName: string }) { + const [previewOpen, setPreviewOpen] = useState(false); + const previewTriggerRef = useRef(null); + + return ( + <> + + setPreviewOpen(false)} + title="Exact patient-visible message" + description={`Fully substituted fictional text for ${stageName}.`} + closeLabel="Close exact message preview" + returnFocusRef={previewTriggerRef} + mobileSize="viewport" + testId="caring-contact-message-preview-sheet" + > + +
+

+

+

{PATIENT_VISIBLE_NO_REPLY_NOTICE}. Caring contacts supplement usual care.

+
+
+ + ); +} + +export function PersonalisationScreen({ onBack, onContinue }: { onBack: () => void; onContinue: () => void }) { + const patient = syntheticPatients[1]; + const coordinator = syntheticTeamMembers[0]; + const template = syntheticTemplates[0]; + + return ( +
+
+

+ Patient identity remains in flow +

+

+ {patient.fullName} · {patient.id} · 3 Nov 1987 +

+
+ +
+
+

+ Stage 3 of 4 +

+

+ Personalisation +

+

+ Choose only governed substitutions. Free text, generated authoring and dynamic translation are absent. +

+ +
+ + {patient.preferredName} · imported from the fictional referral + + Example Aftercare Team · neutral sender label + {coordinator.displayName} + + {template.variant} · {template.version} + + + Two-person approval complete + {template.approvalEvidence?.clinicalProgrammeLead} + {template.approvalEvidence?.livedExperienceContentReviewer} + + + {EXACT_MESSAGE_GSM7.septets} septets · {EXACT_MESSAGE_GSM7.segments} of 2 SMS segments · GSM-7 encoding + + + {ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel} · applies to all 10 planned contacts + + + 15 Aug 2026 · {ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel} + +
+ +
+ +
+ + +
+
+
+ + +
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/product-pages.tsx b/src/components/caring-contacts/mockups/product-pages.tsx new file mode 100644 index 0000000000..0e7928fd36 --- /dev/null +++ b/src/components/caring-contacts/mockups/product-pages.tsx @@ -0,0 +1,1350 @@ +"use client"; + +import { + AlertTriangle, + ArrowRight, + CalendarClock, + CalendarDays, + CheckCircle2, + ChevronLeft, + ChevronRight, + CircleHelp, + ClipboardCheck, + Clock3, + FileCheck2, + FileText, + History, + Info, + ListChecks, + MessageSquareText, + PauseCircle, + Search, + ShieldCheck, + Users, + XCircle, +} from "lucide-react"; +import { useMemo, useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Chip } from "@/components/ui/chip"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; +import { announce } from "@/components/ui/live-announcer"; +import { Sheet } from "@/components/ui/sheet"; +import { cn, fieldControlWithIcon, fieldIcon } from "@/components/ui-primitives"; + +import { + ROWAN_SELECTED_SENDING_PREFERENCE, + syntheticPathways, + syntheticPatients, + syntheticPlannedContacts, + syntheticTeamMembers, + syntheticTemplates, +} from "./fixtures"; +import { + MessagePreviewCard, + OneWayBoundary, + PatientIdentityStrip, + PersonAvatar, + ProductSection, + ScheduleList, + SectionHeading, + StatusChip, + productInset, + productSurface, +} from "./product-ui"; + +type TodayPageProps = { + onReviewReferral: () => void; + onOpenPatients: () => void; + onOpenSchedule: () => void; +}; + +const sendingWindows = [ + { label: "Morning", time: "10:00 am AWST", planned: 8, active: 3 }, + { label: "Afternoon", time: "2:00 pm AWST", planned: 11, active: 5 }, + { label: "Early evening", time: "5:00 pm AWST", planned: 6, active: 1 }, +] as const; + +const recentActivity = [ + { initials: "RS", name: "Rowan Sample", event: "Transport receipt recorded", time: "10:42 am", state: "Delivered" }, + { initials: "TR", name: "Taylor Rivera", event: "Schedule updated", time: "10:15 am", state: "Scheduled" }, + { initials: "JW", name: "Jordan White", event: "Agreement evidence added", time: "9:58 am", state: "Completed" }, + { + initials: "AK", + name: "Avery Khan", + event: "Transport status unavailable", + time: "9:20 am", + state: "Requires action", + }, +] as const; + +export function TodayProductPage({ onReviewReferral, onOpenPatients, onOpenSchedule }: TodayPageProps) { + return ( +
+
+ + 3 new} + /> +
+
+ +
+
+

Rowan Sample

+ Ready for activation +
+

+ Referral accepted 9:35 am · first window 10:00 am AWST +

+

+ Example Aftercare Team owns the referral; Alex Example coordinates the plan. +

+
+
+
+ +
+
+
+ + + 2 items} + /> +
+ + +
+
+
+ + + + View schedule + + } + /> +
+ {sendingWindows.map((window, index) => ( +
0 && "border-t border-[color:var(--border)] sm:border-l sm:border-t-0", + )} + > +
+
+

{window.label}

+

{window.time}

+
+
+
+
+
Planned
+
+ {window.planned} +
+
+
+
Processing
+
+ {window.active} +
+
+
+
+ ))} +
+
+ + + +
+ {recentActivity.map((item) => ( +
+ +

{item.name}

+

{item.event}

+ + + {item.state} + +
+ ))} +
+
+ +
+

+ Operational summary +

+ {[ + ["Active plans", "14", "Across the selected team"], + ["Due today", "25", "All three sending windows"], + ["Named exceptions", "1", "Requires operational review"], + ].map(([label, value, note]) => ( +
+

{label}

+

{value}

+

{note}

+
+ ))} +
+
+ ); +} + +type PatientsDirectoryProps = { + onOpenRowan: () => void; + onContinueMira: () => void; +}; + +const patientRows = [ + { + initials: "ME", + name: "Mira Example", + id: "SYN-PATIENT-001", + status: "Awaiting handover", + owner: "Fictional Ward A", + action: "Continue referral", + }, + { + initials: "RS", + name: "Rowan Sample", + id: "SYN-PATIENT-002", + status: "Active plan", + owner: "Alex Example", + action: "View patient", + }, + { + initials: "SW", + name: "Sam Wilson", + id: "SYN-PATIENT-003", + status: "Needs action", + owner: "Taylor Fiction", + action: "Review exception", + }, +] as const; + +export function PatientsDirectoryPage({ onOpenRowan, onContinueMira }: PatientsDirectoryProps) { + const [query, setQuery] = useState(""); + const filteredRows = useMemo( + () => patientRows.filter((row) => `${row.name} ${row.id}`.toLowerCase().includes(query.trim().toLowerCase())), + [query], + ); + + return ( +
+
+ {[ + ["Active patients", "14", "One current plan each"], + ["Awaiting handover", "3", "Referring team remains responsible"], + ["Needs action", "2", "Named operational conditions"], + ].map(([label, value, note]) => ( +
+

{label}

+

{value}

+

{note}

+
+ ))} +
+ + +
+
+

+ Active-team patients +

+

+ Search is limited to referrals and episodes owned by Example Aftercare Team. +

+
+
+ +
+
+
+ {filteredRows.map((row) => ( +
+ +
+

{row.name}

+

{row.id}

+
+ + {row.status} + +

Owner: {row.owner}

+ +
+ ))} + {filteredRows.length === 0 ? ( +
+

No active-team patients match

+

+ Try a different fictional name or identifier. +

+
+ ) : null} +
+
+
+ ); +} + +export function PatientOverviewProductPage({ onViewPlan }: { onViewPlan: () => void }) { + return ( +
+ + +
+ {[ + ["Active plan", syntheticPathways[0].name, "10 contacts over 12 months"], + ["Next contact", "Week 1 · 22 Aug", ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel], + ["Continuity", "1 delivered · 9 scheduled", "Original cadence preserved"], + ].map(([label, value, note]) => ( +
+

+ {label} +

+

{value}

+

{note}

+
+ ))} +
+ + + + View plan + + } + /> +
+ +

One-way caring contacts · Morning 10:00 am AWST

+
+
+ +
+ + +
+ +
+
+ + +
    + {[ + ["Transport receipt recorded", "15 Aug 2026, 10:00 am AWST", "Delivered"], + ["Coordination claimed", "15 Aug 2026, 9:42 am AWST", "Alex Example"], + ["Referral accepted", "15 Aug 2026, 9:35 am AWST", "Taylor Fiction"], + ].map(([event, time, detail]) => ( +
  1. +
    +
    +
  2. + ))} +
+

+ Delivered is transport receipt only. +

+
+
+
+ ); +} + +export function PlanDetailProductPage({ onOpenException }: { onOpenException: () => void }) { + const [pauseOpen, setPauseOpen] = useState(false); + const [withdrawOpen, setWithdrawOpen] = useState(false); + const [notice, setNotice] = useState(""); + + function recordPrototypeOutcome(message: string) { + setPauseOpen(false); + setWithdrawOpen(false); + setNotice(message); + announce(message, { eventId: "caring-contact:plan-action" }); + } + + return ( +
+
+
+
+ +
+

Rowan Sample

+

SYN-PATIENT-002

+
+
+
+

Plan

+

+ Twelve-month caring contact · 10 contacts +

+

+ One-way SMS · {ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel} +

+
+
+

Status

+
+ Active +
+

Started 15 Aug 2026

+
+
+
+ + + +
+
+ + +
+ {[ + ["Template", `${syntheticPathways[0].name} · ${syntheticPathways[0].version}`], + ["Total contacts", "10"], + ["Cadence", "Day 1 to month 12"], + ["Next contact", "22 Aug 2026"], + ["Time zone", "AWST (UTC+8)"], + ].map(([term, value]) => ( +
+
{term}
+
{value}
+
+ ))} +
+
+ + +
+

Alex Example

+

Coordinator · Example Aftercare Team

+

Backup owner: Sam Sample

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

+ Pause preserves the original calendar and skips contacts that fall inside the pause. +

+ {notice ? ( +

+ {notice} +

+ ) : null} +
+
+
+ + setPauseOpen(false)} + onConfirm={() => recordPrototypeOutcome("Prototype pause reviewed. The synthetic plan was not changed.")} + title="Pause caring-contact plan" + description="Future contacts inside the pause would be skipped permanently. The original cadence would remain unchanged." + confirmLabel="Pause future contacts" + tone="primary" + /> + setWithdrawOpen(false)} + onConfirm={() => recordPrototypeOutcome("Prototype withdrawal reviewed. The synthetic plan was not changed.")} + title="Record patient-requested withdrawal" + description="Withdrawal would permanently cancel every unsent contact. This prototype records no change." + confirmLabel="Continue to fresh authentication" + tone="danger" + /> +
+ ); +} + +type DeliveryExceptionDrawerProps = { open: boolean; onClose: () => void }; + +export function DeliveryExceptionDrawer({ open, onClose }: DeliveryExceptionDrawerProps) { + const closeRef = useRef(null); + const [reviewRecorded, setReviewRecorded] = useState(false); + + function closeDrawer() { + setReviewRecorded(false); + onClose(); + } + + function recordOperationalReview() { + setReviewRecorded(true); + announce("Prototype operational review recorded. No data changed.", { + eventId: "caring-contact:exception-review", + }); + } + + return ( + + Close transport detail + + } + > +
+ Not delivered + +
+
+ +
+

Rowan Sample

+

Synthetic patient · SYN-PATIENT-002

+
+
+
+ {[ + ["Plan", "Twelve-month caring contact · 10 contacts"], + ["Message", "Contact 4 of 10"], + ["Scheduled", "15 Oct 2026 · 10:00 am AWST"], + ].map(([term, value]) => ( +
+
{term}
+
{value}
+
+ ))} +
+
+ +
+ +
    + {["10:00 am", "1:00 pm", "5:00 pm"].map((time, index) => ( +
  1. +
  2. + ))} +
+
+ +
+
+
+
+ +
+ +
+

+ Review the contact record and record an operational outcome today. No automated clinical action is + created. +

+ + {reviewRecorded ? ( +

+ Prototype review recorded. The synthetic contact and plan remain unchanged. +

+ ) : null} +
+
+
+
+ ); +} + +const scheduleCounts = [5, 4, 7, 4, 6, 9, 3] as const; + +const scheduleFullDateFormatter = new Intl.DateTimeFormat("en-AU", { + weekday: "long", + day: "numeric", + month: "long", + year: "numeric", + timeZone: "Australia/Perth", +}); + +const scheduleDayFormatter = new Intl.DateTimeFormat("en-AU", { + weekday: "short", + day: "numeric", + month: "short", + timeZone: "Australia/Perth", +}); + +function scheduleDateFor(dayOffset: number) { + return new Date(Date.UTC(2026, 7, 10 + dayOffset)); +} + +function scheduleDateId(date: Date) { + return date.toISOString().slice(0, 10); +} + +const schedulePatients = { + Morning: ["Rowan Sample", "Taylor Smith", "Jordan Lee", "Morgan Riley"], + Afternoon: ["Casey Brown", "Avery Jones", "Jamie Paterson"], + "Early evening": ["Reese Clark", "Alex Nguyen"], +} as const; + +export function ScheduleProductPage({ onOpenException }: { onOpenException: () => void }) { + const [weekOffset, setWeekOffset] = useState(0); + const [selectedDate, setSelectedDate] = useState("2026-08-15"); + const visibleDays = useMemo( + () => + scheduleCounts.map((count, index) => { + const date = scheduleDateFor(weekOffset * 7 + index); + const [day, dateLabel, month] = scheduleDayFormatter.format(date).replace(",", "").split(" "); + return { count, dateId: scheduleDateId(date), day, dateLabel, month }; + }), + [weekOffset], + ); + const selectedDateValue = new Date(`${selectedDate}T00:00:00+08:00`); + const selectedHasException = selectedDate === "2026-08-15"; + + function changeWeek(delta: number) { + const nextOffset = weekOffset + delta; + setWeekOffset(nextOffset); + setSelectedDate(scheduleDateId(scheduleDateFor(nextOffset * 7 + 5))); + } + + return ( +
+ +
+
+

+ {scheduleFullDateFormatter.format(selectedDateValue)} +

+

Australia/Perth · approved service windows

+
+
+ + +
+
+
+ {visibleDays.map(({ count, dateId, day, dateLabel, month }) => { + const selected = dateId === selectedDate; + return ( + + ); + })} +
+
+ +
+ {(Object.entries(schedulePatients) as Array<[keyof typeof schedulePatients, readonly string[]]>).map( + ([window, patients], index) => ( + +
+
+

+ {window} +

+

+ {index === 0 ? "10:00 am" : index === 1 ? "2:00 pm" : "5:00 pm"} AWST +

+
+ {patients.length} +
+
+ {patients.map((patient) => ( + + ))} +
+
+ ), + )} +
+ + + + {selectedHasException ? ( + + ) : ( +
+
+ )} +
+
+ ); +} + +const governedLibraryRecords = [ + { + id: syntheticPathways[0].id, + kind: "Pathway", + name: syntheticPathways[0].name, + version: syntheticPathways[0].version, + lifecycle: syntheticPathways[0].lifecycle, + approval: syntheticPathways[0].approvalState, + detail: "12 months · 10 contacts", + }, + { + id: syntheticPathways[1].id, + kind: "Pathway", + name: syntheticPathways[1].name, + version: syntheticPathways[1].version, + lifecycle: syntheticPathways[1].lifecycle, + approval: syntheticPathways[1].approvalState, + detail: "12 months · historical record", + }, + { + id: syntheticTemplates[0].id, + kind: "Message", + name: syntheticTemplates[0].name, + version: syntheticTemplates[0].version, + lifecycle: syntheticTemplates[0].lifecycle, + approval: syntheticTemplates[0].approvalState, + detail: `${syntheticTemplates[0].variant} · ${syntheticTemplates[0].segmentCount} segments`, + }, + { + id: syntheticTemplates[1].id, + kind: "Message", + name: syntheticTemplates[1].name, + version: syntheticTemplates[1].version, + lifecycle: "Pending", + approval: syntheticTemplates[1].approvalState, + detail: `${syntheticTemplates[1].variant} · unavailable for activation`, + }, +] as const; + +export function TemplatesProductPage() { + const [selectedRecordId, setSelectedRecordId] = useState(governedLibraryRecords[0].id); + const selectedRecord = governedLibraryRecords.find(({ id }) => id === selectedRecordId) ?? governedLibraryRecords[0]; + const selectedIsCurrent = selectedRecord.lifecycle === "Current" && selectedRecord.approval === "Locally approved"; + const approvalDate = + selectedRecord.lifecycle === "Pending" + ? "Not approved" + : selectedRecord.lifecycle === "Retired" + ? "1 Aug 2026" + : "14 Aug 2026"; + const nextReview = + selectedRecord.lifecycle === "Pending" + ? "Not scheduled" + : selectedRecord.lifecycle === "Retired" + ? "Retired record" + : "14 Aug 2027"; + + return ( +
+ + +
+ {governedLibraryRecords.map((record) => { + const selected = record.id === selectedRecord.id; + return ( + + ); + })} +
+
+ +
+ + + {selectedRecord.lifecycle} + + } + /> +
+ {[ + ["Approval", selectedRecord.approval], + ["Record type", selectedRecord.kind], + ["Programme detail", selectedRecord.detail], + ["Approved", approvalDate], + ["Next review", nextReview], + ["Clinical programme lead", "Taylor Fiction"], + ["Lived-experience reviewer", "Jordan Example"], + ].map(([term, value]) => ( +
+
{term}
+
{value}
+
+ ))} +
+
+ {selectedIsCurrent ? ( + + ) : ( +
+
+ )} +
+
+ ); +} + +export function TeamProductPage() { + return ( +
+ + 12 unclaimed} + /> +
+ {syntheticTeamMembers.map((member, index) => ( +
+
+
+ part[0]) + .join("")} + size="sm" + /> +
+

{member.displayName}

+

{member.id}

+
+
+ {member.role} +
+
+
+
Active plans
+
+ {[8, 18, 7][index]} +
+
+
+
Unclaimed work
+
+ {[2, 4, 1][index]} +
+
+
+
Escalation
+
No escalation
+
+
+
+ ))} +
+
+

Unclaimed work

+ 12 +
+

No owner · escalates after 60 minutes

+
+
+
+ + + + + + + + + + + + {syntheticTeamMembers.map((member, index) => ( + + + + + + + + ))} + + + + + + + + +
Team memberRoleActive plansUnclaimed workEscalation
+
+ part[0]) + .join("")} + size="sm" + /> +
+

{member.displayName}

+

{member.id}

+
+
+
+ {member.role} + {[8, 18, 7][index]}{[2, 4, 1][index]}No escalation
Unclaimed workNo ownerNot applicable12Escalates after 60 minutes
+
+

+ External alerts contain no patient information. +

+
+
+ ); +} + +export function GuidanceProductPage() { + return ( +
+
+ + + +
    + {[ + "Pause sending when a system incident is declared.", + "Do not queue messages beyond the next approved send.", + "Record the incident and notify the owning team.", + "Resume the original schedule only after restoration is confirmed.", + "Do not automatically resend a contact missed during downtime.", + ].map((item) => ( +
  • +
  • + ))} +
+
+
+ +
+ ); +} + +export function ReportsProductPage() { + return ( +
+
+ {[ + ["Due", "14", "Today"], + ["Dispatched", "13", "Today"], + ["Named failures", "1", "Operational review"], + ["Median resolution", "38 min", "Last 7 days"], + ].map(([label, value, note]) => ( +
+

{label}

+

{value}

+

{note}

+
+ ))} +
+ + + +
+ {[ + ["Due and dispatched", "14 due · 13 dispatched", "One contact remains in a named operational state"], + ["Permanent transport failures", "1 named exception", "No automated clinical action"], + ["Resolution time", "Median 38 minutes", "From named exception to recorded outcome"], + ["Approved demographic cell", "Suppressed", "Below the minimum reporting threshold"], + ].map(([title, value, note]) => ( +
+ {title} + {value} + {note} +
+ ))} +
+
+ +
+
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/product-ui.tsx b/src/components/caring-contacts/mockups/product-ui.tsx new file mode 100644 index 0000000000..598ca0a630 --- /dev/null +++ b/src/components/caring-contacts/mockups/product-ui.tsx @@ -0,0 +1,303 @@ +import { Check, CheckCircle2, Info, LockKeyhole, MessageSquareText, ShieldCheck, type LucideIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +import { Chip, type ChipStatusTone } from "@/components/ui/chip"; +import { cn } from "@/components/ui-primitives"; + +import { ROWAN_SELECTED_SENDING_PREFERENCE, syntheticPatients, syntheticPlannedContacts } from "./fixtures"; +import { + EXACT_MESSAGE_GSM7, + EXACT_PATIENT_VISIBLE_MESSAGE, + PATIENT_VISIBLE_NO_REPLY_NOTICE, +} from "./personalisation-screen"; + +export const productSurface = + "min-w-0 rounded-[var(--radius-xl)] border border-[color:var(--border)] bg-[color:var(--surface-raised)]"; + +export const productInset = + "min-w-0 rounded-[var(--radius-lg)] border border-[color:var(--border)] bg-[color:var(--surface-subtle)]"; + +export function ProductSection({ + children, + className, + labelledBy, +}: { + children: ReactNode; + className?: string; + labelledBy?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export function SectionHeading({ + id, + title, + description, + icon: Icon, + action, + compact = false, +}: { + id?: string; + title: string; + description?: string; + icon?: LucideIcon; + action?: ReactNode; + compact?: boolean; +}) { + return ( +
+
+ {Icon ? ( + + + ) : null} +
+

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+
+ {action ?
{action}
: null} +
+ ); +} + +export function PersonAvatar({ initials, size = "md" }: { initials: string; size?: "sm" | "md" | "lg" }) { + return ( + + ); +} + +export function StatusChip({ children, tone }: { children: ReactNode; tone: ChipStatusTone }) { + return ( + + {children} + + ); +} + +export function PatientIdentityStrip({ compact = false }: { compact?: boolean }) { + const patient = syntheticPatients[1]; + return ( +
+ +
+

{patient.fullName}

+

+ DOB 3 Nov 1987 {patient.id} +

+
+ + +
+ ); +} + +const workflowSteps = ["Patient and agreement", "Pathway", "Personalisation", "Review"] as const; + +export function WorkflowStepper({ activeStep }: { activeStep: 1 | 2 | 3 | 4 }) { + return ( + + ); +} + +export function AssuranceRow({ + icon: Icon, + label, + value, + source, +}: { + icon: LucideIcon; + label: string; + value: string; + source: string; +}) { + return ( +
+ + +
+

{label}

+

{source}

+
+
+

{value}

+

Source: {source}

+
+ +
+ ); +} + +export function MessagePreviewCard({ compact = false }: { compact?: boolean }) { + return ( +
+
+
+
+ {EXACT_PATIENT_VISIBLE_MESSAGE} +
+
+
+
+ ); +} + +export function OneWayBoundary({ compact = false }: { compact?: boolean }) { + return ( +
+
+
+
+ ); +} + +const dateFormatter = new Intl.DateTimeFormat("en-AU", { + day: "numeric", + month: "short", + year: "numeric", + timeZone: "Australia/Perth", +}); + +export function ScheduleList({ limit }: { limit?: number }) { + const contacts = limit ? syntheticPlannedContacts.slice(0, limit) : syntheticPlannedContacts; + return ( +
    + {contacts.map((contact) => ( +
  1. + + {contact.sequence} + + {contact.cadenceLabel} + + + {ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel} + + + {contact.transportState} + +
  2. + ))} +
+ ); +} + +export function VerifiedSummary({ children }: { children: ReactNode }) { + return ( +
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/review-activation-screen.tsx b/src/components/caring-contacts/mockups/review-activation-screen.tsx new file mode 100644 index 0000000000..2e0cf3bac9 --- /dev/null +++ b/src/components/caring-contacts/mockups/review-activation-screen.tsx @@ -0,0 +1,275 @@ +"use client"; + +import { ArrowLeft, CheckCircle2, ShieldCheck } from "lucide-react"; +import { useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { announce } from "@/components/ui/live-announcer"; + +import { + ROWAN_SELECTED_SENDING_PREFERENCE, + syntheticPatients, + syntheticPathways, + syntheticPlannedContacts, + syntheticTemplates, +} from "./fixtures"; +import { DefinitionRow, OperationalStatus } from "./mockup-primitives"; +import { + CompactMessagePreview, + EXACT_MESSAGE_GSM7, + EXACT_PATIENT_VISIBLE_MESSAGE, + MessagePreview, + PATIENT_VISIBLE_NO_REPLY_NOTICE, + canActivateGovernedVersions, + getActivationBlockers, +} from "./personalisation-screen"; +import type { SyntheticPathway, SyntheticTemplate } from "./types"; + +const dateFormatter = new Intl.DateTimeFormat("en-AU", { + day: "numeric", + month: "short", + year: "numeric", + timeZone: "Australia/Perth", +}); + +type ReviewActivationScreenProps = { + onBack: () => void; + onOverview: () => void; + pathway?: SyntheticPathway; + template?: SyntheticTemplate; +}; + +export function ReviewActivationScreen({ + onBack, + onOverview, + pathway = syntheticPathways[0], + template = syntheticTemplates[0], +}: ReviewActivationScreenProps) { + const patient = syntheticPatients[1]; + const [activationNotice, setActivationNotice] = useState(""); + const selectedGovernance = { pathway, template }; + const activationBlockers = getActivationBlockers(selectedGovernance); + const activationAllowed = canActivateGovernedVersions(selectedGovernance); + const activationGateDescriptionId = activationAllowed ? undefined : "review-activation-unavailable"; + + function reviewPrototypeActivation() { + if (!activationAllowed) { + announce( + `Activation unavailable. ${activationBlockers.map(({ reason, remedy }) => `${reason} Remedy: ${remedy}`).join(" ")}`, + { eventId: "caring-contact:activation-unavailable" }, + ); + return; + } + const outcome = "Prototype activation reviewed. No plan was created and no message was sent."; + setActivationNotice(outcome); + announce(outcome, { eventId: "caring-contact:activation-review" }); + } + + return ( +
+
+

+ Patient identity remains in flow +

+

+ {patient.fullName} · {patient.id} · 3 Nov 1987 +

+
+ +
+
+
+
+

+ Stage 4 of 4 +

+

+ Review and activation +

+

+ Review every source, owner, version, word and AWST time before the final prototype action. +

+
+ + {activationAllowed ? "Ready for prototype review" : "Action blocked"} + +
+ +
+
+

+ Identity and source +

+
+ + {patient.fullName} · {patient.id} · 3 Nov 1987 + + + + {patient.mobile} · Imported from {patient.mobileSource} + + + Patient-controlled and suitable for discreet SMS + + Agreement confirmed: Yes · imported + evidence + +
+
+ +
+

+ Ownership and versions +

+
+ + Owning team: Example Aftercare Team + + + Coordinator: Alex Example + + + {pathway.name} · {pathway.version} · {pathway.approvalState} · {pathway.lifecycle} + + + {template.name} · {template.version} · {template.variant} · {template.approvalState} ·{" "} + {template.lifecycle} + + + + {activationAllowed ? "Two-person approval complete" : "Selected version unavailable for activation"} + + + Pathway record: {pathway.approvalEvidence?.clinicalProgrammeLead ?? "Approval evidence unavailable"} + + + Pathway record:{" "} + {pathway.approvalEvidence?.livedExperienceContentReviewer ?? "Approval evidence unavailable"} + + + Message record:{" "} + {template.approvalEvidence?.clinicalProgrammeLead ?? "Approval evidence unavailable"} + + + Message record:{" "} + {template.approvalEvidence?.livedExperienceContentReviewer ?? "Approval evidence unavailable"} + + +
+
+ + {!activationAllowed ? ( +
+

+ Activation unavailable +

+
    + {activationBlockers.map(({ reason, remedy }) => ( +
  • +

    {reason}

    +

    + Remedy: {remedy} +

    +
  • + ))} +
+
+ ) : null} + +
+

+ Exact message and schedule +

+
+ {EXACT_PATIENT_VISIBLE_MESSAGE} +
+

+ {EXACT_MESSAGE_GSM7.septets} septets · {EXACT_MESSAGE_GSM7.segments} of 2 SMS segments · GSM-7 encoding +

+

+ 10 contacts use the selected {ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel} preference across the exact + dated schedule. +

+
    + {syntheticPlannedContacts.map((contact) => ( +
  1. + {contact.cadenceLabel} + + {contact.windowLabel} + + {contact.transportState} + +
  2. + ))} +
+
+ +
+

+

+

+ {PATIENT_VISIBLE_NO_REPLY_NOTICE}. Caring contacts do not monitor safety, wellbeing or response and + supplement usual care. +

+
+
+ +
+ +
+ + + +
+
+ {activationNotice ? ( +

+ {activationNotice} +

+ ) : null} +
+ + +
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/schedule-screen.tsx b/src/components/caring-contacts/mockups/schedule-screen.tsx new file mode 100644 index 0000000000..89b36ffd68 --- /dev/null +++ b/src/components/caring-contacts/mockups/schedule-screen.tsx @@ -0,0 +1,237 @@ +"use client"; + +import { CalendarDays, Clock3, TriangleAlert } from "lucide-react"; +import { useState } from "react"; + +import { announce } from "@/components/ui/live-announcer"; + +import { ContinuityThreadSpecimen } from "./continuity-thread-specimen"; +import { OperationalStatus } from "./mockup-primitives"; + +type ScheduleDay = { + id: string; + label: string; + tabDetail: string; + dateLabel: string; + note: string; + routineWindows: readonly { name: string; time: string; count: number }[]; + exceptions: readonly { title: string; state: string; detail: string; owner: string }[]; +}; + +const windowRecords = (morning: number, afternoon: number, earlyEvening: number) => + [ + { name: "Morning", time: "10:00 am AWST", count: morning }, + { name: "Afternoon", time: "2:00 pm AWST", count: afternoon }, + { name: "Early evening", time: "5:00 pm AWST", count: earlyEvening }, + ] as const; + +const scheduleDays: readonly ScheduleDay[] = [ + { + id: "2026-08-15", + label: "15 Aug", + tabDetail: "Today", + dateLabel: "Saturday 15 August 2026", + note: "Weekend contacts remain inside the approved service windows.", + routineWindows: windowRecords(2, 3, 1), + exceptions: [ + { + title: "Carrier status unavailable", + state: "Status unavailable", + detail: + "Retrieve transport detail for SYN-CONTACT-07. This system state does not imply safety, receipt or wellbeing.", + owner: "Alex Example", + }, + ], + }, + { + id: "2026-08-16", + label: "16 Aug", + tabDetail: "Sun", + dateLabel: "Sunday 16 August 2026", + note: "Weekend contacts remain inside the approved service windows.", + routineWindows: windowRecords(1, 1, 0), + exceptions: [], + }, + { + id: "2026-08-17", + label: "17 Aug", + tabDetail: "Mon", + dateLabel: "Monday 17 August 2026", + note: "Routine service day in Australia/Perth.", + routineWindows: windowRecords(1, 2, 1), + exceptions: [], + }, + { + id: "2026-08-18", + label: "18 Aug", + tabDetail: "Tue", + dateLabel: "Tuesday 18 August 2026", + note: "Routine service day in Australia/Perth.", + routineWindows: windowRecords(1, 1, 1), + exceptions: [], + }, + { + id: "2026-08-19", + label: "19 Aug", + tabDetail: "Wed", + dateLabel: "Wednesday 19 August 2026", + note: "Routine service day in Australia/Perth.", + routineWindows: windowRecords(2, 2, 1), + exceptions: [], + }, + { + id: "2026-08-20", + label: "20 Aug", + tabDetail: "Thu", + dateLabel: "Thursday 20 August 2026", + note: "Routine service day in Australia/Perth.", + routineWindows: windowRecords(1, 1, 0), + exceptions: [], + }, + { + id: "2026-08-21", + label: "21 Aug", + tabDetail: "Fri", + dateLabel: "Friday 21 August 2026", + note: "Routine service day in Australia/Perth.", + routineWindows: windowRecords(1, 1, 1), + exceptions: [], + }, +]; + +function routineCount(day: ScheduleDay) { + return day.routineWindows.reduce((sum, window) => sum + window.count, 0); +} + +function scheduledContactLabel(count: number) { + return `${count} scheduled ${count === 1 ? "contact" : "contacts"}`; +} + +export function ScheduleScreen() { + const [selectedDayId, setSelectedDayId] = useState(scheduleDays[0].id); + const selectedDay = scheduleDays.find((day) => day.id === selectedDayId) ?? scheduleDays[0]; + const selectedRoutineCount = routineCount(selectedDay); + + function selectDay(day: ScheduleDay) { + setSelectedDayId(day.id); + announce( + `${day.dateLabel}. ${routineCount(day)} routine contacts. ${day.exceptions.length === 0 ? "No named exceptions." : `${day.exceptions.length} named exception.`}`, + { eventId: `caring-contact:schedule:${day.id}` }, + ); + } + + return ( +
+
+
+
+ +
    + {scheduleDays.map((day) => { + const selected = selectedDay.id === day.id; + return ( +
  • + +
  • + ); + })} +
+

+ Showing {selectedDay.label} · {selectedRoutineCount} routine contacts ·{" "} + {selectedDay.exceptions.length === 0 + ? "No named exceptions" + : `${selectedDay.exceptions.length} named exception`} +

+
+ +
+
+
+
+
    + {selectedDay.routineWindows.map((window) => ( +
  1. + {window.name} + {window.time} + {scheduledContactLabel(window.count)} +
  2. + ))} +
+
+ +
+
+
+ {selectedDay.exceptions.length === 0 ? ( +

+ No named exceptions for this day. +

+ ) : ( + selectedDay.exceptions.map((exception) => ( +
+
+

{exception.title}

+ {exception.state} +
+

{exception.detail}

+

Owner: {exception.owner}

+
+ )) + )} +
+
+ + +
+ ); +} diff --git a/src/components/caring-contacts/mockups/team-guidance-reporting-screens.tsx b/src/components/caring-contacts/mockups/team-guidance-reporting-screens.tsx new file mode 100644 index 0000000000..ce075d9002 --- /dev/null +++ b/src/components/caring-contacts/mockups/team-guidance-reporting-screens.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { BarChart3, BookOpen, ClockAlert, ShieldCheck, Users } from "lucide-react"; +import { useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Sheet } from "@/components/ui/sheet"; + +import { ComponentStateSpecimens } from "./component-state-specimens"; +import { syntheticTeamMembers } from "./fixtures"; +import { DefinitionRow, OperationalStatus } from "./mockup-primitives"; +import { PATIENT_VISIBLE_NO_REPLY_NOTICE } from "./personalisation-screen"; + +export function TeamScreen() { + return ( +
+
+
+
+
+

Example Aftercare Team

+
    + {syntheticTeamMembers.map((member) => ( +
  • + {member.displayName} + + {member.role} · {member.id} + +
  • + ))} +
+
+
+
+
+

+ Alert: “Accepted referral remains unclaimed. Open Callback.” +

+
+
+
+ ); +} + +export function GuidanceScreen() { + return ( +
+
+
+
+
+

One-way programme boundary

+
    +
  • Caring contacts supplement usual care and person-to-person follow-up.
  • +
  • {PATIENT_VISIBLE_NO_REPLY_NOTICE}.
  • +
  • Delivered is a transport receipt only; it does not prove the message was read.
  • +
  • Timing changes, pause and withdrawal use the staffed programme phone.
  • +
+
+
+

Incident and downtime help

+
    +
  • + Wrong recipient, duplicate send, unauthorised content or lost audit integrity pauses the entire pilot. +
  • +
  • Restart needs joint incident, privacy/security and clinical programme approval.
  • +
  • Offline operation fails closed with no patient cache, activation or uncertain resend.
  • +
+
+
+
+ ); +} + +export function ReportsScreen() { + const [statesOpen, setStatesOpen] = useState(false); + const triggerRef = useRef(null); + + return ( +
+
+
+
+ +
+
+
+

Pilot operations snapshot

+ Synthetic aggregates +
+
+ 14 due · 13 dispatched + 1 named exception + Median 38 minutes + + + + +
+

+ Small cells always render as “Suppressed”, never zero. The threshold is owned by governance and analytics and + is not invented in this design. +

+
+ + setStatesOpen(false)} + title="Component and state specimens" + description="Reusable interaction, content, system and accessibility states." + closeLabel="Close component and state specimens" + returnFocusRef={triggerRef} + mobilePlacement="fullscreen" + contentClassName="lg:!max-w-6xl" + > + + +
+ ); +} diff --git a/src/components/caring-contacts/mockups/template-screens.tsx b/src/components/caring-contacts/mockups/template-screens.tsx new file mode 100644 index 0000000000..8517f803e5 --- /dev/null +++ b/src/components/caring-contacts/mockups/template-screens.tsx @@ -0,0 +1,121 @@ +import { CheckCircle2, Clock3, FileText, History, Users } from "lucide-react"; + +import { syntheticTemplates } from "./fixtures"; +import { DefinitionRow, OperationalStatus } from "./mockup-primitives"; +import { PATIENT_VISIBLE_NO_REPLY_NOTICE } from "./personalisation-screen"; + +const retiredTemplate = { + ...syntheticTemplates[0], + id: "SYN-TEMPLATE-RETIRED", + name: "Example retired first contact", + version: "SYN-copy-v0.9-retired", + lifecycle: "Retired" as const, +}; + +export function TemplateScreens() { + const current = syntheticTemplates[0]; + const pending = syntheticTemplates[1]; + + return ( +
+
+
+ +
+
+

Version library

+
+ {[ + { template: current, label: "Current", tone: "success" as const }, + { template: pending, label: "Approval pending", tone: "warning" as const }, + { template: retiredTemplate, label: "Retired", tone: "neutral" as const }, + ].map(({ template, label, tone }) => ( +
+
+

{template.name}

+ {label} +
+

+ {template.version} · {template.variant} · {template.segmentCount} of 2 SMS segments ·{" "} + {template.encoding} +

+
+ ))} +
+
+ +
+
+
+

+ Template detail +

+

{current.name}

+
+ Locally approved +
+
+ {current.version} + Example Aftercare Team · non-receiving sender + {PATIENT_VISIBLE_NO_REPLY_NOTICE} + + {current.segmentCount} of 2 SMS segments · {current.encoding} + +
+
+
+
+
    +
  • +
  • +
  • +
  • +
+
+
+
+ +
+
+
+
+

+ Remedy: complete both approvals, freeze the exact content snapshot and select the newly approved version. +

+
+
+
+
+

+ Existing audit history keeps the frozen snapshot. New plans must use a current locally approved version. +

+
+
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/today-screen.tsx b/src/components/caring-contacts/mockups/today-screen.tsx new file mode 100644 index 0000000000..eee1dd871d --- /dev/null +++ b/src/components/caring-contacts/mockups/today-screen.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { ArrowRight, CalendarClock, CheckCircle2, Clock3, FileWarning, History } from "lucide-react"; +import { useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { announce } from "@/components/ui/live-announcer"; + +import { syntheticAuditEvents, syntheticPatients, syntheticReferrals } from "./fixtures"; +import { OperationalStatus } from "./mockup-primitives"; + +const sendingWindows = [ + { name: "Morning", label: "Morning 10:00 am AWST", count: "2 scheduled" }, + { name: "Afternoon", label: "Afternoon 2:00 pm AWST", count: "3 scheduled" }, + { name: "Early evening", label: "Early evening 5:00 pm AWST", count: "1 scheduled" }, +] as const; + +export function TodayScreen() { + const pendingReferral = syntheticReferrals[0]; + const patient = syntheticPatients.find(({ id }) => id === pendingReferral.patientId)!; + const auditEvent = syntheticAuditEvents[0]; + const [announcement, setAnnouncement] = useState(""); + + function reviewFictionalReferral() { + const outcome = "Fictional referral assurance selected for review."; + setAnnouncement(outcome); + announce(outcome, { eventId: "caring-contact:referral-review" }); + } + + return ( +
+
+
+
+

+ First actionable region +

+

Referrals to review

+

+ Ordered by time to the first eligible sending window after discharge, never by inferred patient state. +

+
+ 1 awaiting review +
+ +
+
+
+

{patient.fullName}

+ {pendingReferral.handoverState} +
+

+ {patient.id} · discharged 15 Aug 2026 at 8:10 am · first eligible window 10:00 am AWST +

+

+ {pendingReferral.referringTeam} remains responsible until explicit acceptance. +

+
+ +
+ {announcement ? ( +

+ {announcement} +

+ ) : null} +
+ +
+
+
+

Needs action

+

+ Observable operational conditions with a named remedy and owner. +

+
+
+
+
+

Template approval owner not recorded

+ Approval evidence missing +
+

+ Record the local approval owner before this template version can be selected. +

+

Owner: Sam Sample, team lead

+
+
+ +
+
+
+
+ {sendingWindows.map((window) => ( +
+

+ {window.name} +

+

{window.label}

+

{window.count}

+
+ ))} +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
Active fictional plans
+
14
+
+
+
Due today
+
6
+
+
+
Named exceptions
+
1
+
+
+
+
+ ); +} diff --git a/src/components/caring-contacts/mockups/types.ts b/src/components/caring-contacts/mockups/types.ts new file mode 100644 index 0000000000..f45480c129 --- /dev/null +++ b/src/components/caring-contacts/mockups/types.ts @@ -0,0 +1,157 @@ +export type PrimaryDestination = "Today" | "Patients" | "Schedule" | "Templates"; +export type MoreDestination = "Team" | "Guidance" | "Reports"; +export type WorkspaceDestination = PrimaryDestination | MoreDestination; + +export const APPROVED_CARING_CONTACT_ROUTE_IDENTITIES = { + today: "/caring-contacts", + patients: "/caring-contacts/patients", + patient: "/patients/[patientId]", + newPlan: "/plans/new", + plan: "/plans/[planId]", + schedule: "/caring-contacts/schedule", + contact: "/contacts/[contactId]", + templates: "/caring-contacts/templates", + pathwayTemplate: "/templates/[pathwayId]", + team: "/caring-contacts/team", + guidance: "/caring-contacts/guidance", + reports: "/caring-contacts/reports", +} as const; + +export const SUPERSEDED_CARING_CONTACT_ROUTE_PATTERNS = [ + "/caring-contacts/patients/[episodeId]/activate/*", + "/caring-contacts/patients/[episodeId]/plan", + "/caring-contacts/patients/[episodeId]/contacts/[contactId]", + "/caring-contacts/patients/[episodeId]", + "/caring-contacts/templates/[versionId]", +] as const; + +export const FICTIONAL_CONTACTS_BY_ROLE = { + miraPatientMobile: "+61 491 570 006", + rowanPatientMobile: "+61 491 570 156", + programmeStaffedLine: "+61 491 570 157", + crisisSupportContact: "+61 491 570 158", +} as const; + +export type FictionalContactRole = keyof typeof FICTIONAL_CONTACTS_BY_ROLE; +export const DESIGNATED_FICTIONAL_MOBILE_NUMBERS = [ + FICTIONAL_CONTACTS_BY_ROLE.miraPatientMobile, + FICTIONAL_CONTACTS_BY_ROLE.rowanPatientMobile, + FICTIONAL_CONTACTS_BY_ROLE.programmeStaffedLine, + FICTIONAL_CONTACTS_BY_ROLE.crisisSupportContact, +] as const; +export type DesignatedFictionalMobileNumber = (typeof DESIGNATED_FICTIONAL_MOBILE_NUMBERS)[number]; +export type SyntheticPatientMobile = + (typeof FICTIONAL_CONTACTS_BY_ROLE)["miraPatientMobile"] | (typeof FICTIONAL_CONTACTS_BY_ROLE)["rowanPatientMobile"]; + +export type ContactWindow = "Morning" | "Afternoon" | "Early evening"; +export type SyntheticSendingPreference = { + window: ContactWindow; + scheduledTime: string; + windowLabel: string; +}; +export type TransportState = + | "Scheduled" + | "Processing" + | "Sent" + | "Delivered" + | "Not delivered" + | "Number invalid" + | "Contact changed" + | "Status unavailable" + | "Missed"; + +export type SyntheticPatient = { + id: string; + fullName: string; + preferredName: string; + dateOfBirth: string; + mobile: SyntheticPatientMobile; + mobileSource: string; + patientControlledForSms: boolean; +}; + +export type SyntheticReferral = { + id: string; + patientId: string; + referringTeam: string; + receivedAt: string; + dischargedAt: string; + handoverState: "Awaiting handover" | "Accepted" | "Clarification requested"; + agreementConfirmed: boolean; +}; + +export type SyntheticEpisode = { + id: string; + patientId: string; + referralId: string; + state: "Awaiting activation" | "Active" | "Paused" | "Completed" | "Withdrawn" | "Cancelled"; + coordinatorId: string; + pathwayId: string; + openedAt: string; + selectedSendingPreference: SyntheticSendingPreference; +}; + +export type GovernanceApprovalState = "Illustrative — approval pending" | "Locally approved"; +export type GovernanceLifecycle = "Current" | "Retired"; +export type SyntheticTwoPersonApprovalEvidence = { + clinicalProgrammeLead: string; + livedExperienceContentReviewer: string; +}; + +export type SyntheticPathway = { + id: string; + name: string; + version: string; + duration: string; + cadence: readonly string[]; + senderLabel: string; + governanceLabel: "Illustrative locally governed pathway"; + approvalState: GovernanceApprovalState; + lifecycle: GovernanceLifecycle; + approvalEvidence: SyntheticTwoPersonApprovalEvidence | null; +}; + +export type SyntheticPlannedContact = { + id: string; + episodeId: string; + sequence: number; + cadenceLabel: string; + scheduledAt: string; + window: ContactWindow; + windowLabel: string; + transportState: TransportState; +}; + +export type SyntheticTemplate = { + id: string; + name: string; + version: string; + variant: string; + segmentCount: 1 | 2; + encoding: "GSM-7" | "Unicode"; + approvalState: GovernanceApprovalState; + lifecycle: GovernanceLifecycle; + approvalEvidence: SyntheticTwoPersonApprovalEvidence | null; +}; + +export type SyntheticDeliveryEvent = { + id: string; + contactId: string; + occurredAt: string; + state: TransportState; + operationalNote: string; +}; + +export type SyntheticTeamMember = { + id: string; + displayName: string; + role: "Coordinator" | "Team lead" | "Authorised clinician"; +}; + +export type SyntheticAuditEvent = { + id: string; + occurredAt: string; + actorId: string; + action: string; + objectId: string; +}; diff --git a/tests/caring-contact-mockups.dom.test.tsx b/tests/caring-contact-mockups.dom.test.tsx new file mode 100644 index 0000000000..6cc45c0a9b --- /dev/null +++ b/tests/caring-contact-mockups.dom.test.tsx @@ -0,0 +1,258 @@ +import { useState } from "react"; +import { render, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; + +import { CaringContactDesignSuite } from "@/components/caring-contacts/mockups"; +import { + ROWAN_SELECTED_SENDING_PREFERENCE, + syntheticPathways, + syntheticPatients, + syntheticPlannedContacts, + syntheticTemplates, +} from "@/components/caring-contacts/mockups/fixtures"; +import { + completionMutationOverlayLabels, + completionOverlayDefinitions, + OverlaySpecimens, +} from "@/components/caring-contacts/mockups/overlay-specimens"; +import { + calculateGsm7, + canActivateGovernedVersions, + EXACT_MESSAGE_GSM7, + EXACT_PATIENT_VISIBLE_MESSAGE, +} from "@/components/caring-contacts/mockups/personalisation-screen"; +import { ReviewActivationScreen } from "@/components/caring-contacts/mockups/review-activation-screen"; +import { + APPROVED_CARING_CONTACT_ROUTE_IDENTITIES, + DESIGNATED_FICTIONAL_MOBILE_NUMBERS, + FICTIONAL_CONTACTS_BY_ROLE, + SUPERSEDED_CARING_CONTACT_ROUTE_PATTERNS, +} from "@/components/caring-contacts/mockups/types"; + +const expectedOverlayLabels = [ + "Verify identity", + "Change patient", + "Pathway preview", + "Message preview", + "Communication preference", + "Adjust date/time", + "Outside-window warning", + "Save draft", + "Discard changes", + "Final activation", + "Activation success", + "Pause", + "Withdrawal", + "Reassignment", + "Delivery detail", + "Resolve failed delivery", + "Contact-changed block", + "Template changed/retired", + "Session expiry", + "Offline banner", + "Recoverable error", + "Permission unavailable", + "Team switcher", + "Draft/version conflict", +] as const; + +function OverlayHarness() { + const [offline, setOffline] = useState(false); + return ; +} + +describe("Caring Contact governance contracts", () => { + it("keeps every fictional contact role distinct and every plan contact in one selected window", () => { + expect(FICTIONAL_CONTACTS_BY_ROLE).toEqual({ + miraPatientMobile: "+61 491 570 006", + rowanPatientMobile: "+61 491 570 156", + programmeStaffedLine: "+61 491 570 157", + crisisSupportContact: "+61 491 570 158", + }); + expect(DESIGNATED_FICTIONAL_MOBILE_NUMBERS).toEqual([ + "+61 491 570 006", + "+61 491 570 156", + "+61 491 570 157", + "+61 491 570 158", + ]); + expect(new Set(Object.values(FICTIONAL_CONTACTS_BY_ROLE)).size).toBe(4); + expect(syntheticPatients.every((patient) => DESIGNATED_FICTIONAL_MOBILE_NUMBERS.includes(patient.mobile))).toBe( + true, + ); + expect(syntheticPatients.map(({ mobile }) => mobile)).toEqual([ + FICTIONAL_CONTACTS_BY_ROLE.miraPatientMobile, + FICTIONAL_CONTACTS_BY_ROLE.rowanPatientMobile, + ]); + expect(syntheticPlannedContacts).toHaveLength(10); + expect(syntheticPlannedContacts.every(({ window }) => window === ROWAN_SELECTED_SENDING_PREFERENCE.window)).toBe( + true, + ); + expect( + syntheticPlannedContacts.every( + ({ windowLabel }) => windowLabel === ROWAN_SELECTED_SENDING_PREFERENCE.windowLabel, + ), + ).toBe(true); + expect(syntheticPlannedContacts[0]?.transportState).toBe("Delivered"); + expect(syntheticPlannedContacts.slice(1).every(({ transportState }) => transportState === "Scheduled")).toBe(true); + }); + + it("freezes the approved future routes and marks nested episode/version routes superseded", () => { + expect(APPROVED_CARING_CONTACT_ROUTE_IDENTITIES).toEqual({ + today: "/caring-contacts", + patients: "/caring-contacts/patients", + patient: "/patients/[patientId]", + newPlan: "/plans/new", + plan: "/plans/[planId]", + schedule: "/caring-contacts/schedule", + contact: "/contacts/[contactId]", + templates: "/caring-contacts/templates", + pathwayTemplate: "/templates/[pathwayId]", + team: "/caring-contacts/team", + guidance: "/caring-contacts/guidance", + reports: "/caring-contacts/reports", + }); + expect(SUPERSEDED_CARING_CONTACT_ROUTE_PATTERNS).toEqual([ + "/caring-contacts/patients/[episodeId]/activate/*", + "/caring-contacts/patients/[episodeId]/plan", + "/caring-contacts/patients/[episodeId]/contacts/[contactId]", + "/caring-contacts/patients/[episodeId]", + "/caring-contacts/templates/[versionId]", + ]); + }); + + it("derives exact GSM-7 septets and blocks unapproved or retired versions", () => { + const pendingTemplate = syntheticTemplates.find( + ({ approvalState }) => approvalState === "Illustrative — approval pending", + )!; + + expect(EXACT_MESSAGE_GSM7).toEqual({ invalidCharacters: [], segments: 2, septets: 272, valid: true }); + expect(EXACT_PATIENT_VISIBLE_MESSAGE).toContain(FICTIONAL_CONTACTS_BY_ROLE.programmeStaffedLine); + expect(EXACT_PATIENT_VISIBLE_MESSAGE).toContain(FICTIONAL_CONTACTS_BY_ROLE.crisisSupportContact); + expect(EXACT_PATIENT_VISIBLE_MESSAGE).not.toContain(FICTIONAL_CONTACTS_BY_ROLE.miraPatientMobile); + expect(EXACT_PATIENT_VISIBLE_MESSAGE).not.toContain(FICTIONAL_CONTACTS_BY_ROLE.rowanPatientMobile); + expect(calculateGsm7("A^B")).toEqual({ invalidCharacters: [], segments: 1, septets: 4, valid: true }); + expect(calculateGsm7("Hello 🙂")).toMatchObject({ invalidCharacters: ["🙂"], segments: 0, valid: false }); + expect(calculateGsm7("A".repeat(160)).segments).toBe(1); + expect(calculateGsm7("A".repeat(161)).segments).toBe(2); + expect(canActivateGovernedVersions({ pathway: syntheticPathways[0], template: syntheticTemplates[0] })).toBe(true); + expect(canActivateGovernedVersions({ pathway: syntheticPathways[0], template: pendingTemplate })).toBe(false); + expect(canActivateGovernedVersions({ pathway: syntheticPathways[1], template: syntheticTemplates[0] })).toBe(false); + }); + + it("renders a pending selected message as unavailable with a named approval remedy", async () => { + const user = userEvent.setup(); + const pendingTemplate = syntheticTemplates.find( + ({ approvalState }) => approvalState === "Illustrative — approval pending", + )!; + render( + undefined} + onOverview={() => undefined} + />, + ); + + expect(screen.getByText("Activation unavailable", { exact: true })).toBeInTheDocument(); + expect(screen.getByText(/Message version is awaiting two-person approval/i)).toBeInTheDocument(); + expect(screen.getByText(/select a current, locally approved message version/i)).toBeInTheDocument(); + const activate = screen.getByRole("button", { name: "Activate 10-contact plan" }); + expect(activate).toHaveAttribute("aria-disabled", "true"); + await user.click(activate); + expect(screen.queryByText(/Prototype activation reviewed/i)).not.toBeInTheDocument(); + }); + + it("renders a retired selected pathway as unavailable with a named replacement remedy", () => { + render( + undefined} + onOverview={() => undefined} + />, + ); + + expect(screen.getByText("Selected version unavailable for activation", { exact: true })).toBeInTheDocument(); + expect(screen.getByText("Activation unavailable", { exact: true })).toBeInTheDocument(); + expect(screen.getByText(/Pathway version is retired/i)).toBeInTheDocument(); + expect(screen.getByText(/select a current, locally approved pathway version/i)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Activate 10-contact plan" })).toHaveAttribute("aria-disabled", "true"); + }); + + it("keeps the complete overlay decision matrix explicit and internally consistent", () => { + expect(completionOverlayDefinitions.map(({ label }) => label)).toEqual(expectedOverlayLabels); + expect(new Set(completionOverlayDefinitions.map(({ id }) => id)).size).toBe(24); + expect(completionOverlayDefinitions).toHaveLength(24); + expect(completionMutationOverlayLabels).toHaveLength(16); + expect(completionOverlayDefinitions.filter(({ mutatesState }) => mutatesState).map(({ label }) => label)).toEqual( + completionMutationOverlayLabels, + ); + for (const definition of completionOverlayDefinitions) { + expect(definition.title).not.toBe(""); + expect(definition.summary).not.toBe(""); + expect(definition.content).not.toBe(""); + expect(definition.decision).not.toBe(""); + expect(definition.content).not.toMatch(/monitor(?:ed|ing)? replies|patient is safe|risk score/i); + } + }); + + it("keeps overlay focus restoration and offline mutation guards outside the product page catalogue", async () => { + const user = userEvent.setup(); + render(); + + const inventory = screen.getByRole("list", { name: "Complete overlay inventory" }); + expect(within(inventory).getAllByRole("listitem")).toHaveLength(24); + + const identityTrigger = within(inventory).getByRole("button", { name: "Open Verify identity" }); + await user.click(identityTrigger); + expect(screen.getByRole("dialog", { name: "Verify identity before changing patient" })).toBeInTheDocument(); + await user.keyboard("{Escape}"); + await waitFor(() => expect(identityTrigger).toHaveFocus()); + + await user.click(within(inventory).getByRole("button", { name: "Open Offline banner" })); + expect(screen.getByRole("status", { name: "Offline status" })).toBeInTheDocument(); + expect(identityTrigger).toHaveAttribute("aria-disabled", "true"); + + const readOnlyPreview = within(inventory).getByRole("button", { name: "Open Message preview" }); + expect(readOnlyPreview).not.toHaveAttribute("aria-disabled"); + await user.click(readOnlyPreview); + const preview = screen.getByRole("dialog", { name: "Preview exact patient-visible message" }); + expect(within(preview).getByRole("button", { name: "Return to personalisation" })).not.toHaveAttribute( + "aria-disabled", + ); + await user.keyboard("{Escape}"); + + await user.click(screen.getByRole("button", { name: "Try reconnecting" })); + expect(screen.queryByRole("status", { name: "Offline status" })).not.toBeInTheDocument(); + expect(identityTrigger).not.toHaveAttribute("aria-disabled"); + }); + + it("does not introduce clinical scoring or two-way messaging affordances in any destination", async () => { + const user = userEvent.setup(); + render(); + + function expectNoProhibitedAffordance() { + expect(screen.queryByText(/clinical risk|risk score|wellbeing score|engagement score/i)).not.toBeInTheDocument(); + expect(screen.queryByRole("textbox", { name: /reply|message/i })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /reply|inbox|conversation/i })).not.toBeInTheDocument(); + expect(screen.queryByRole("navigation", { name: /inbox|messages|conversations/i })).not.toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: /reply|inbox|messages|conversations/i })).not.toBeInTheDocument(); + } + + const navigation = screen.getByRole("navigation", { name: "Desktop workspace" }); + expectNoProhibitedAffordance(); + for (const destination of ["Patients", "Schedule", "Templates", "Today"]) { + await user.click(within(navigation).getByRole("button", { name: destination })); + expectNoProhibitedAffordance(); + } + + for (const destination of ["Team", "Guidance", "Reports"] as const) { + await user.click(within(navigation).getByRole("button", { name: "More" })); + const more = screen.getByRole("dialog", { name: "More" }); + await user.click(within(more).getByRole("button", { name: new RegExp(`^${destination}`) })); + expect(screen.getByRole("heading", { level: 1, name: destination })).toBeInTheDocument(); + expectNoProhibitedAffordance(); + } + }); +}); diff --git a/tests/caring-contact-product-redesign.dom.test.tsx b/tests/caring-contact-product-redesign.dom.test.tsx new file mode 100644 index 0000000000..972c315d29 --- /dev/null +++ b/tests/caring-contact-product-redesign.dom.test.tsx @@ -0,0 +1,164 @@ +import { render, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; + +import { CaringContactDesignSuite } from "@/components/caring-contacts/mockups"; +import { EXACT_PATIENT_VISIBLE_MESSAGE } from "@/components/caring-contacts/mockups/personalisation-screen"; + +function desktopNavigation() { + return screen.getByRole("navigation", { name: "Desktop workspace" }); +} + +describe("Caring Contact product redesign", () => { + it("presents one focused product page in the repository workspace shell", () => { + render(); + + expect(screen.getByRole("heading", { name: "Today", level: 1 })).toBeInTheDocument(); + expect(screen.getAllByRole("heading", { level: 1 })).toHaveLength(1); + expect(screen.queryByRole("heading", { name: "Foundation board" })).not.toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: /specimen/i })).not.toBeInTheDocument(); + expect(screen.queryByTestId("caring-contact-screen-patient-agreement")).not.toBeInTheDocument(); + expect(screen.getByTestId("caring-contact-synthetic-marker")).toHaveAttribute( + "title", + "Synthetic prototype — fictional data only", + ); + + expect( + within(desktopNavigation()) + .getAllByRole("button") + .map((button) => button.textContent), + ).toEqual(["Today", "Patients", "Schedule", "Templates", "More"]); + expect( + within(screen.getByRole("navigation", { name: "Phone workspace" })) + .getAllByRole("button") + .map((button) => button.textContent), + ).toEqual(["Today", "Patients", "Schedule", "More"]); + }); + + it("completes the governed activation workflow and lands on the patient overview", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("button", { name: "Review Rowan Sample plan setup" })); + + expect(screen.getByRole("heading", { name: "Patient and agreement", level: 1 })).toBeInTheDocument(); + expect(screen.getByText("Step 1 of 4")).toBeInTheDocument(); + expect(screen.getAllByText("Agreement confirmed: Yes").length).toBeGreaterThanOrEqual(2); + expect(screen.getByText("Patient-controlled and suitable for discreet SMS")).toBeInTheDocument(); + expect(screen.getByText("Example Aftercare Team", { selector: "p" })).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Continue to pathway selection" })); + expect(screen.getByRole("heading", { name: "Pathway selection", level: 1 })).toBeInTheDocument(); + expect(screen.getByRole("navigation", { name: "Plan activation progress" })).toHaveTextContent( + "Patient and agreement", + ); + + const previewTrigger = screen.getByRole("button", { name: "Preview pathway" }); + await user.click(previewTrigger); + const preview = screen.getByRole("dialog", { name: "Preview governed pathway" }); + expect(within(preview).getByRole("list", { name: "Caring-contact schedule" })).toBeInTheDocument(); + expect(within(preview).getAllByRole("listitem")).toHaveLength(10); + await user.keyboard("{Escape}"); + await waitFor(() => expect(previewTrigger).toHaveFocus()); + + await user.click(screen.getByRole("button", { name: "Continue to personalisation" })); + expect(screen.getByRole("heading", { name: "Personalisation", level: 1 })).toBeInTheDocument(); + expect(screen.getByRole("radio", { name: /Morning/ })).toBeChecked(); + expect(screen.getByText(EXACT_PATIENT_VISIBLE_MESSAGE)).toBeInTheDocument(); + expect(screen.getByText(/one preference applies to all 10 contacts/i)).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Continue to review and activation" })); + expect(screen.getByRole("heading", { name: "Review and activation", level: 1 })).toBeInTheDocument(); + const activationSchedule = screen.getByRole("list", { name: "Caring-contact schedule" }); + expect(within(activationSchedule).getAllByRole("listitem")).toHaveLength(10); + expect(screen.getAllByText(/Replies are not received, stored, analysed or monitored/i).length).toBeGreaterThan(0); + + await user.click(screen.getByRole("button", { name: "Activate 10-contact plan" })); + const activation = screen.getByRole("dialog", { name: "Final activation assurance" }); + expect(activation).toHaveTextContent(/creates no plan and sends no message/i); + await user.click(within(activation).getByRole("button", { name: "Confirm activation review" })); + + const overviewHeading = screen.getByRole("heading", { name: "Patient overview", level: 1 }); + await waitFor(() => expect(overviewHeading).toHaveFocus()); + expect(screen.getByTestId("caring-contact-screen-patient-overview")).toBeInTheDocument(); + expect(screen.getByText("1 delivered · 9 scheduled")).toBeInTheDocument(); + }); + + it("moves from the team-scoped patient directory into the plan and its governed actions", async () => { + const user = userEvent.setup(); + render(); + + await user.click(within(desktopNavigation()).getByRole("button", { name: "Patients" })); + expect(screen.getByRole("heading", { name: "Patients", level: 1 })).toBeInTheDocument(); + + const search = screen.getByRole("searchbox", { name: "Search active team patients" }); + await user.type(search, "Rowan"); + expect(screen.getByText("Rowan Sample")).toBeInTheDocument(); + expect(screen.queryByText("Mira Example")).not.toBeInTheDocument(); + await user.clear(search); + + await user.click(screen.getByRole("button", { name: "View patient" })); + await user.click(screen.getByRole("button", { name: "View active plan" })); + expect(screen.getByRole("heading", { name: "Plan and contact detail", level: 1 })).toBeInTheDocument(); + expect(screen.getByText(/Example twelve-month pathway · SYN-v0.3/)).toBeInTheDocument(); + + const pauseTrigger = screen.getByRole("button", { name: "Pause plan" }); + await user.click(pauseTrigger); + const pauseDialog = screen.getByRole("dialog", { name: "Pause caring-contact plan" }); + await user.click(within(pauseDialog).getAllByRole("button", { name: "Cancel" })[1]!); + await waitFor(() => expect(pauseTrigger).toHaveFocus()); + + await user.click(screen.getByRole("button", { name: "Open delivery exception" })); + const exception = screen.getByRole("dialog", { name: "Permanent delivery exception" }); + expect(exception).toHaveTextContent(/does not indicate patient safety, receipt, wellbeing or response/i); + await user.click(within(exception).getByRole("button", { name: "Record operational review" })); + expect(within(exception).getByRole("status")).toHaveTextContent(/synthetic contact and plan remain unchanged/i); + await user.click(within(exception).getByRole("button", { name: "Close transport detail" })); + }); + + it("makes the Schedule date state, week navigation and named exception honest", async () => { + const user = userEvent.setup(); + render(); + + await user.click(within(desktopNavigation()).getByRole("button", { name: "Schedule" })); + expect(screen.getByRole("heading", { name: "Saturday 15 August 2026", level: 2 })).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: /Sun 16 Aug, 3 scheduled contacts/ })); + expect(screen.getByRole("heading", { name: "Sunday 16 August 2026", level: 2 })).toBeInTheDocument(); + expect(screen.getByText("No named exceptions for this day")).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Next week" })); + expect(screen.getByRole("heading", { name: "Saturday 22 August 2026", level: 2 })).toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "Previous week" })); + expect(screen.getByRole("heading", { name: "Saturday 15 August 2026", level: 2 })).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: /Sam Wilson.*Transport status unavailable/ })); + expect(screen.getByRole("dialog", { name: "Permanent delivery exception" })).toBeInTheDocument(); + }); + + it("keeps supporting destinations out of the primary phone dock and gives each a focused page", async () => { + const user = userEvent.setup(); + render(); + const phoneNavigation = screen.getByRole("navigation", { name: "Phone workspace" }); + + await user.click(within(phoneNavigation).getByRole("button", { name: "More" })); + let more = screen.getByRole("dialog", { name: "More" }); + for (const destination of ["Templates", "Team", "Guidance", "Reports"]) { + expect(within(more).getByRole("button", { name: new RegExp(`^${destination}`) })).toBeInTheDocument(); + } + + await user.click(within(more).getByRole("button", { name: /^Templates/ })); + expect(screen.getByRole("heading", { name: "Governed templates", level: 1 })).toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: /Example retired twelve-month pathway/ })); + expect(screen.getByText("Unavailable for new activation")).toBeInTheDocument(); + + for (const destination of ["Team", "Guidance", "Reports"] as const) { + await user.click(within(phoneNavigation).getByRole("button", { name: "More" })); + more = screen.getByRole("dialog", { name: "More" }); + await user.click(within(more).getByRole("button", { name: new RegExp(`^${destination}`) })); + expect(screen.getByRole("heading", { name: destination, level: 1 })).toBeInTheDocument(); + } + + expect(screen.getByText(/No clinical outcome, response or engagement inference/i)).toBeInTheDocument(); + }); +}); diff --git a/tests/playwright-project-isolation.test.ts b/tests/playwright-project-isolation.test.ts index 31ae1c8a99..7945caa083 100644 --- a/tests/playwright-project-isolation.test.ts +++ b/tests/playwright-project-isolation.test.ts @@ -35,6 +35,28 @@ describe("Playwright production-project isolation", () => { expect(source).toMatch(/name: ["']chromium-mockups["'],\s+testMatch: mockupSpecPattern,\s+grep: mockupTag,/m); }); + it("collects the caring-contact mockup only in the advisory mockup project", () => { + const source = readFileSync(resolve(process.cwd(), "playwright.config.ts"), "utf8"); + const productionSpecPattern = configPattern(source, "productionSpecPattern"); + const mockupSpecPattern = configPattern(source, "mockupSpecPattern"); + const testMatch = source.match(/testMatch:\s*(\/.*\/),/); + expect(testMatch, "playwright.config.ts: could not read the top-level testMatch regex").not.toBeNull(); + const testMatchPattern = new RegExp(testMatch![1].slice(1, -1)); + const spec = "tests/ui-caring-contact-mockup.spec.ts"; + + expect(existsSync(resolve(process.cwd(), spec)), `${spec} is missing`).toBe(true); + expect(testMatchPattern.test(spec), `${spec} is not collected by top-level testMatch`).toBe(true); + expect(mockupSpecPattern.test(spec), `${spec} is not collected by chromium-mockups`).toBe(true); + expect(productionSpecPattern.test(spec), `${spec} leaked into required production projects`).toBe(false); + + const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8")) as { + scripts?: Record; + }; + expect(packageJson.scripts?.["test:e2e:caring-contact-mockup"]).toBe( + "node scripts/run-playwright.mjs --project=chromium-mockups tests/ui-caring-contact-mockup.spec.ts", + ); + }); + /** * The phone-scroll coverage is split across sibling spec files so no single * file can dominate one `--shard`. That split only holds if every sibling is diff --git a/tests/ui-caring-contact-mockup.spec.ts b/tests/ui-caring-contact-mockup.spec.ts new file mode 100644 index 0000000000..140dbff99f --- /dev/null +++ b/tests/ui-caring-contact-mockup.spec.ts @@ -0,0 +1,329 @@ +import { mkdirSync, readdirSync, rmSync } from "node:fs"; +import { resolve } from "node:path"; + +import { expect, test, type Locator, type Page } from "playwright/test"; + +const mockupPath = "/mockups/caring-contacts"; +const requiredWidths = [320, 390, 430, 768, 1024, 1440] as const; +const captureMajorPages = process.env.CARING_CONTACT_CAPTURE_MAJOR_PAGES === "1"; +const captureDirectory = resolve(process.cwd(), ".local", "caring-contact-product-redesign", "2026-08-15"); + +async function gotoMockup(page: Page) { + await page.goto(mockupPath, { waitUntil: "domcontentloaded" }); + const marker = page.getByTestId("caring-contact-synthetic-marker").first(); + await expect(marker).toHaveAttribute("title", "Synthetic prototype — fictional data only", { timeout: 15_000 }); + await expect(page.getByRole("heading", { level: 1, name: "Today" })).toBeVisible(); +} + +async function expectNoHorizontalOverflow(page: Page) { + const overflow = await page.evaluate( + () => Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0) - window.innerWidth, + ); + expect(overflow).toBeLessThanOrEqual(2); +} + +function activeNavigation(page: Page) { + return page.getByRole("navigation", { + name: page.viewportSize()!.width < 768 ? "Phone workspace" : "Desktop workspace", + }); +} + +async function chooseDestination(page: Page, destination: "Today" | "Patients" | "Schedule") { + await activeNavigation(page).getByRole("button", { name: destination, exact: true }).click(); +} + +async function chooseSupportingDestination(page: Page, destination: "Templates" | "Team" | "Guidance" | "Reports") { + const compact = page.viewportSize()!.width < 768; + if (!compact && destination === "Templates") { + await activeNavigation(page).getByRole("button", { name: destination, exact: true }).click(); + return; + } + + await activeNavigation(page).getByRole("button", { name: "More", exact: true }).click(); + await page + .getByRole("dialog", { name: "More" }) + .getByRole("button", { name: new RegExp(`^${destination}`) }) + .click(); +} + +async function expectPhoneDockClearance(page: Page, control: Locator) { + if (page.viewportSize()!.width >= 768) return; + + await control.evaluate((element) => element.scrollIntoView({ block: "center" })); + const [controlBox, dockBox] = await Promise.all([ + control.boundingBox(), + page.getByRole("navigation", { name: "Phone workspace" }).boundingBox(), + ]); + expect(controlBox).not.toBeNull(); + expect(dockBox).not.toBeNull(); + expect(controlBox!.y + controlBox!.height).toBeLessThanOrEqual(dockBox!.y); +} + +async function continueWorkflow(page: Page, buttonName: string, nextHeading: string) { + const button = page.getByRole("button", { name: buttonName, exact: true }); + await expectPhoneDockClearance(page, button); + await button.click(); + await expect(page.getByRole("heading", { level: 1, name: nextHeading, exact: true })).toBeFocused(); + await expect.poll(() => page.evaluate(() => window.scrollY)).toBeLessThanOrEqual(1); + await expectNoHorizontalOverflow(page); +} + +async function reachPlan(page: Page) { + await chooseDestination(page, "Patients"); + await page.getByRole("button", { name: "View patient", exact: true }).click(); + await expect(page.getByRole("heading", { level: 1, name: "Patient overview" })).toBeFocused(); + await page.getByRole("button", { name: "View active plan", exact: true }).click(); + await expect(page.getByRole("heading", { level: 1, name: "Plan and contact detail" })).toBeFocused(); +} + +test.describe("@mockup Caring Contact product redesign", () => { + test("product shell is responsive at every required handoff width", async ({ page }) => { + for (const width of requiredWidths) { + await page.setViewportSize({ width, height: width < 768 ? 844 : 1000 }); + await gotoMockup(page); + await expectNoHorizontalOverflow(page); + + const desktopRail = page.locator("aside").first(); + const phoneDock = page.getByRole("navigation", { name: "Phone workspace" }); + if (width < 768) { + await expect(desktopRail).toBeHidden(); + await expect(phoneDock).toBeVisible(); + await expect(phoneDock.getByRole("button")).toHaveCount(4); + await expectPhoneDockClearance(page, page.getByRole("button", { name: "Review Rowan Sample plan setup" })); + } else { + await expect(desktopRail).toBeVisible(); + await expect(phoneDock).toBeHidden(); + const [railBox, mainBox] = await Promise.all([desktopRail.boundingBox(), page.locator("main").boundingBox()]); + expect(railBox).not.toBeNull(); + expect(mainBox).not.toBeNull(); + expect(mainBox!.x).toBeGreaterThanOrEqual(railBox!.x + railBox!.width - 1); + expect(railBox!.width).toBeGreaterThanOrEqual(78); + expect(railBox!.width).toBeLessThanOrEqual(82); + } + } + }); + + for (const width of [390, 1440] as const) { + test(`governed activation is coherent and focus-safe at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: width < 768 ? 844 : 1000 }); + await gotoMockup(page); + + await page.getByRole("button", { name: "Review Rowan Sample plan setup" }).click(); + await expect(page.getByRole("heading", { level: 1, name: "Patient and agreement" })).toBeFocused(); + await expect(page.getByText("Agreement confirmed: Yes").first()).toBeVisible(); + await expect.poll(() => page.evaluate(() => window.scrollY)).toBeLessThanOrEqual(1); + + await continueWorkflow(page, "Continue to pathway selection", "Pathway selection"); + const previewTrigger = page.getByRole("button", { name: "Preview pathway", exact: true }); + await previewTrigger.click(); + const preview = page.getByRole("dialog", { name: "Preview governed pathway" }); + await expect(preview).toBeVisible(); + await expect(preview.getByRole("listitem")).toHaveCount(10); + const previewBox = await preview.boundingBox(); + expect(previewBox).not.toBeNull(); + if (width < 768) { + expect(previewBox!.x).toBeLessThanOrEqual(1); + expect(previewBox!.width).toBeGreaterThanOrEqual(width - 2); + } else { + expect(previewBox!.width).toBeLessThanOrEqual(640); + expect(previewBox!.x + previewBox!.width).toBeGreaterThanOrEqual(width - 2); + } + await page.keyboard.press("Escape"); + await expect(previewTrigger).toBeFocused(); + + await continueWorkflow(page, "Continue to personalisation", "Personalisation"); + await expect(page.getByRole("radio", { name: /Morning/ })).toBeChecked(); + await expect(page.getByText(/Hi Rowan, Alex from Example Aftercare Team is thinking of you/)).toBeVisible(); + + await continueWorkflow(page, "Continue to review and activation", "Review and activation"); + const schedule = page.getByRole("list", { name: "Caring-contact schedule" }); + await expect(schedule.getByRole("listitem")).toHaveCount(10); + await expect(schedule).toContainText("Morning 10:00 am AWST"); + + const activate = page.getByRole("button", { name: "Activate 10-contact plan", exact: true }); + await expectPhoneDockClearance(page, activate); + await activate.click(); + const confirmation = page.getByRole("dialog", { name: "Final activation assurance" }); + await expect(confirmation).toContainText("creates no plan and sends no message"); + await confirmation.getByRole("button", { name: "Confirm activation review" }).click(); + await expect(page.getByRole("heading", { level: 1, name: "Patient overview" })).toBeFocused(); + await expect(page.getByText("1 delivered · 9 scheduled")).toBeVisible(); + await expectNoHorizontalOverflow(page); + }); + } + + test("patient overview, plan actions and delivery exception work at phone and desktop widths", async ({ page }) => { + for (const width of [390, 1440] as const) { + await page.setViewportSize({ width, height: width < 768 ? 844 : 1000 }); + await gotoMockup(page); + await reachPlan(page); + + const pauseTrigger = page.getByRole("button", { name: "Pause plan", exact: true }); + await expectPhoneDockClearance(page, pauseTrigger); + await pauseTrigger.click(); + await expect(page.getByRole("dialog", { name: "Pause caring-contact plan" })).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(pauseTrigger).toBeFocused(); + + const exceptionTrigger = page.getByRole("button", { name: "Open delivery exception", exact: true }); + await expectPhoneDockClearance(page, exceptionTrigger); + await exceptionTrigger.click(); + const exception = page.getByRole("dialog", { name: "Permanent delivery exception" }); + await expect(exception).toContainText("does not indicate patient safety, receipt, wellbeing or response"); + const box = await exception.boundingBox(); + expect(box).not.toBeNull(); + if (width < 768) expect(box!.width).toBeGreaterThanOrEqual(width - 2); + else { + expect(box!.width).toBeLessThanOrEqual(560); + expect(box!.x + box!.width).toBeGreaterThanOrEqual(width - 2); + } + await exception.getByRole("button", { name: "Record operational review" }).click(); + await expect(exception.getByRole("status")).toContainText("synthetic contact and plan remain unchanged"); + await exception.getByRole("button", { name: "Close transport detail" }).click(); + await expectNoHorizontalOverflow(page); + } + }); + + test("Schedule changes date and week state without conflating named exceptions", async ({ page }) => { + for (const width of [390, 1440] as const) { + await page.setViewportSize({ width, height: width < 768 ? 844 : 1000 }); + await gotoMockup(page); + await chooseDestination(page, "Schedule"); + + const weekDays = page.locator('button[aria-label$="scheduled contacts"]'); + await expect(weekDays).toHaveCount(7); + const weekDayBoxes = await weekDays.evaluateAll((elements) => + elements.map((element) => { + const box = element.getBoundingClientRect(); + return { left: box.left, top: box.top, width: box.width }; + }), + ); + expect( + Math.max(...weekDayBoxes.map(({ top }) => top)) - Math.min(...weekDayBoxes.map(({ top }) => top)), + ).toBeLessThanOrEqual(2); + expect(weekDayBoxes.every(({ width: dayWidth }) => dayWidth >= 40)).toBe(true); + expect(weekDayBoxes.every(({ left }, index) => index === 0 || left > weekDayBoxes[index - 1]!.left)).toBe(true); + + await page.getByRole("button", { name: "Sun 16 Aug, 3 scheduled contacts" }).click(); + await expect(page.getByRole("heading", { level: 2, name: "Sunday 16 August 2026" })).toBeVisible(); + await expect(page.getByText("No named exceptions for this day")).toBeVisible(); + await page.getByRole("button", { name: "Next week" }).click(); + await expect(page.getByRole("heading", { level: 2, name: "Saturday 22 August 2026" })).toBeVisible(); + await page.getByRole("button", { name: "Previous week" }).click(); + await expect(page.getByRole("heading", { level: 2, name: "Saturday 15 August 2026" })).toBeVisible(); + + const exceptionTrigger = page.getByRole("button", { name: /Sam Wilson.*Transport status unavailable/ }); + await expectPhoneDockClearance(page, exceptionTrigger); + await exceptionTrigger.click(); + await expect(page.getByRole("dialog", { name: "Permanent delivery exception" })).toBeVisible(); + await page.keyboard.press("Escape"); + await expectNoHorizontalOverflow(page); + } + }); + + test("supporting destinations remain focused, governed product pages", async ({ page }) => { + for (const width of [390, 1440] as const) { + await page.setViewportSize({ width, height: width < 768 ? 844 : 1000 }); + await gotoMockup(page); + + await chooseSupportingDestination(page, "Templates"); + await expect(page.getByRole("heading", { level: 1, name: "Governed templates" })).toBeFocused(); + await page.getByRole("button", { name: /Example retired twelve-month pathway/ }).click(); + await expect(page.getByText("Unavailable for new activation")).toBeVisible(); + + for (const destination of ["Team", "Guidance", "Reports"] as const) { + await chooseSupportingDestination(page, destination); + await expect(page.getByRole("heading", { level: 1, name: destination })).toBeFocused(); + if (destination === "Team") { + if (width < 768) { + await expect(page.getByTestId("team-mobile-roster")).toBeVisible(); + await expect(page.getByTestId("team-desktop-table")).toBeHidden(); + } else { + await expect(page.getByTestId("team-mobile-roster")).toBeHidden(); + await expect(page.getByTestId("team-desktop-table")).toBeVisible(); + } + } + await expectNoHorizontalOverflow(page); + } + await expect(page.getByText(/No clinical outcome, response or engagement inference/i)).toBeVisible(); + } + }); + + test("reduced motion and forced colours retain visible focus and usable navigation", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.emulateMedia({ reducedMotion: "reduce", forcedColors: "active" }); + await gotoMockup(page); + const review = page.getByRole("button", { name: "Review Rowan Sample plan setup" }); + await review.focus(); + await expect(review).toBeFocused(); + const outlineStyle = await review.evaluate((element) => getComputedStyle(element).outlineStyle); + expect(outlineStyle).not.toBe("none"); + await expectNoHorizontalOverflow(page); + }); + + async function captureSurface(page: Page, name: string, target: Locator = page.locator("main")) { + await page.evaluate(() => document.documentElement.setAttribute("data-caring-contact-capture", "true")); + await target.screenshot({ path: resolve(captureDirectory, name), animations: "disabled" }); + await page.evaluate(() => document.documentElement.removeAttribute("data-caring-contact-capture")); + } + + test("capture the complete major-page atlas", async ({ page }) => { + test.skip(!captureMajorPages, "Set CARING_CONTACT_CAPTURE_MAJOR_PAGES=1 to refresh the product atlas."); + test.setTimeout(300_000); + rmSync(captureDirectory, { recursive: true, force: true }); + mkdirSync(captureDirectory, { recursive: true }); + + for (const [device, width, height] of [ + ["phone", 390, 844], + ["desktop", 1440, 1000], + ] as const) { + await page.setViewportSize({ width, height }); + await gotoMockup(page); + await page.addStyleTag({ + content: + 'html[data-caring-contact-capture="true"] nav[aria-label="Phone workspace"] { display: none !important; }', + }); + await captureSurface(page, `${device}-01-today.png`); + + await chooseDestination(page, "Patients"); + await captureSurface(page, `${device}-02-patients.png`); + await page.getByRole("button", { name: "Continue referral", exact: true }).click(); + await captureSurface(page, `${device}-03-patient-agreement.png`); + await continueWorkflow(page, "Continue to pathway selection", "Pathway selection"); + await captureSurface(page, `${device}-04-pathway-selection.png`); + await continueWorkflow(page, "Continue to personalisation", "Personalisation"); + await captureSurface(page, `${device}-05-personalisation.png`); + await continueWorkflow(page, "Continue to review and activation", "Review and activation"); + await captureSurface(page, `${device}-06-review-activation.png`); + await page.getByRole("button", { name: "Activate 10-contact plan" }).click(); + await page + .getByRole("dialog", { name: "Final activation assurance" }) + .getByRole("button", { name: "Confirm activation review" }) + .click(); + await captureSurface(page, `${device}-07-patient-overview.png`); + await page.getByRole("button", { name: "View active plan" }).click(); + await captureSurface(page, `${device}-08-plan-contact-detail.png`); + + await chooseDestination(page, "Schedule"); + await captureSurface(page, `${device}-09-schedule.png`); + await page.getByRole("button", { name: /Sam Wilson.*Transport status unavailable/ }).click(); + await captureSurface( + page, + `${device}-10-delivery-exception.png`, + page.getByRole("dialog", { name: "Permanent delivery exception" }), + ); + await page.keyboard.press("Escape"); + + await chooseSupportingDestination(page, "Templates"); + await captureSurface(page, `${device}-11-templates.png`); + await chooseSupportingDestination(page, "Team"); + await captureSurface(page, `${device}-12-team.png`); + await chooseSupportingDestination(page, "Guidance"); + await captureSurface(page, `${device}-13-guidance.png`); + await chooseSupportingDestination(page, "Reports"); + await captureSurface(page, `${device}-14-reports.png`); + } + + expect(readdirSync(captureDirectory).filter((name) => name.endsWith(".png"))).toHaveLength(28); + }); +});