medOS ultra

Recovery Room Request Linkage

Wiring RecoveryRoomRequest (PACU) into the central OR request lifecycle.

9 min read diagramsUpdated 2026-06-10docs/architecture/recovery-room-request-flow-linkage.md

Purpose: a self-contained handoff so a fresh session can wire RecoveryRoomRequest (ห้องพักฟื้น / PACU) into the central OR request lifecycle defined in or-request-approval-contract.md, de-fragment the older recovery flow, and complete the 7 Recovery TOR points to UAT.

Read first (in order): (1) or-request-approval-contract.md — the parent contract this links into; (2) the TOR-coverage UAT page /operating-room/tor-coverage (web/src/containers/operating-room/tor-coverage/) — the Recovery Room Deep-Audit accordion is the live status of every surface named below; (3) or-feature-suite-20260515.md for or_state_cache.

One-line problem: recovery is an older flow that fragmented into 4+ copies; the live page is REST-only and the newer Supabase PACU surfaces (SBAR handoff, discharge card) are a disconnected island. This handoff makes recovery a proper child lifecycle of the OR request, on one read model.


1. Current state (verified 2026-06-10)

1.1 What’s LIVE

Route /operating-room/list-or-recoveryweb/src/containers/operating-room/list-or-recovery/index.tsxRecoveryRoomRequest (@periops-kit/operating-room/components/list-or-recovery/RecoveryRoomRequest.tsx) → RecoveryRoomRequestDetail.tsx (table via listRecoveryRoomRequestApi) → DialogRecoveryRoomRequest.tsx (tabs: VitalSignDetail + ParScoreDetail + CoditionDetail + PatientSurgeryFormDetail). Data source: REST onlyGET/PUT /v2/medication/recoveryRoomRequests (NestJS / Mongo) via web/src/services/ever-medication/recoveryRoomRequest.service.ts. No Supabase on the live list.

1.2 The disconnected Supabase island (newer, May 2026)

  • web/packages/miniapps/pacu-handoff/PacuHandoff.tsx (modules.PACURequest) → pacu_handoff_sbar (SBAR + vitals_on_arrival + drains + lines + post-op orders).
  • web/packages/medical-kit/src/pacu-discharge/PacuDischargeCard.tsxpacu_discharge_decision (PAR ≥ 9 gate).
  • Orchestrator handlePacuAdmitted.ts / handlePacuDischarged.tspacu_state_cache. None of these are mounted in /operating-room/list-or-recovery.

1.3 Dead / duplicate copies (delete or consolidate — see §6)

Path Status
web/packages/miniapps/anesthesia/recovery-room/RecoveryRoomMiniApp.tsx (oldest, initial commit) dead — 0 importers, unregistered
web/packages/periops-kit/src/labour-room/labour-room/components/list-or-recovery/* (3rd copy) dead — 0 importers
web/packages/patient-roster/src/hospital-labour-room-worklist/components/recoveryroom-supabase/* (4th copy) dead — 0 importers
web/src/services/supabase/recoveryRoomRequest.supabase.service.ts (recovery_room_requests) dead — defined, never imported
web/packages/periops-kit/src/labour-room/components/list-or-recovery/* stale — used by /labour-room/list-lr-recovery
web/packages/patient-roster/.../recoveryroom-operation-supabase/* stale — lazy fallback in RecoveryRoomRequestWrapper

2. Target entity contract — RecoveryRoomRequest

Backend: packages/platform-api-schema/src/medication/recoveryRoomRequest/ · Mongo recovery_room_request Frontend service: web/src/services/ever-medication/recoveryRoomRequest.service.ts

2.1 Status enum (EXISTS)

REQUESTED='requested'  DRAFT='draft'  PENDING='pending'  COMPLETED='completed'
SENT_BACK_TO_WARD='sentBackToWard'  SENT_TO_ICU='sentToICU'  DISCHARGED='discharged'

2.2 Fields (real)

Group Fields
Link to OR operatingRoomRequestRef: Ref ← the join into the parent contract
Identity encounterRef?, patientRef (required)
Timing (R1) startDate?, startTime?, endDate?, endTime?
Location bed?, anesthesiaPlace?[]
Personnel admittedRN?, dischargedRN?
Assessment (R3) condition?, parScore?: ParScoreItem[], vitalSign?: RecoveredVitalSign[], startTimeOfAnesthesia?, recoveryTimeFromAnesthesia?, anestheticEvaluationFormRef?
Disposition `lastStatus?: sentBackToWard

2.3 Endpoints (exist)

GET  /v2/medication/recoveryRoomRequests           list (buildingRef, roomRef, date, populate)
GET  /v2/medication/recoveryRoomRequests/{id}      get
POST /v2/medication/recoveryRoomRequests           create
PUT  /v2/medication/recoveryRoomRequests/{id}      update
PUT  /v2/medication/recoveryRoomRequests/{id}/lastStatus   disposition

2.4 Lifecycle (as a child of the OR request)

OperatingRoomRequest reaches OPERATING/COMPLETED (roomOut → PACU)
        │  emit OR_CASE_STARTED / case-completed
        ▼
RecoveryRoomRequest  [requested] ──admit (startDate/startTime)──▶ [pending] ──assess (PAR/vitals)──▶ [completed]
   operatingRoomRequestRef = <OR id>                                                                     │ discharge (endDate/endTime)
   inherits patientRef + encounterRef                                                                    ▼
                                                                          lastStatus ∈ {sentBackToWard, sentToICU, discharged}
                                                                          → OperatingRoomRequest.status = recovered/sentBack/sentToICU

Linkage invariant: a RecoveryRoomRequest belongs to exactly one OperatingRoomRequest via operatingRoomRequestRef, and inherits patientRef/encounterRef/origin. Discharging the recovery request advances the parent OR request’s terminal status (recovered/sentBackToWard/sentToICU) — mirror the parent contract’s §10 invariant 7.


2.5 Cross-flow consumers — recovery is NOT an OR-only stage (audit 2026-06-10)

A repo-wide sweep found every flow that consumes (or should consume) a post-procedure recovery stage. This changes the consolidation design: the shared entity is already multi-tenant across flows, but its schema assumes OR-only.

Flow Route / surface Recovery implementation Data store Scoring
Operating Room /operating-room/list-or-recovery shared RecoveryRoomRequest entity (live UI copy #1) REST v2/medication/recoveryRoomRequests PAR (manual)
Labour Room /labour-room/list-lr-recoveryweb/src/containers/labour-room/list-lr-recovery/index.tsx@periops-kit/labour-room/components/list-or-recovery/* SAME shared entity, duplicated UI copy #2; plus a Supabase count fallback via web/packages/patient-roster/src/hospital-labour-room-worklist/hooks/useLabourRoomRecovery.ts REST (same endpoint) + Supabase fallback PAR (manual)
Endoscopy no route — web/packages/periops-kit/src/endoscopy/components/EndoscopyAldreteRecovery.tsx isolated component, local React state only, zero persistence none Aldrete (auto-sum)
Cath Lab (CCL) CCL case flow own table ccl_recovery_stay_log (billable stay phases — the only recovery flow with charges wired) Supabase none (duration/billing)
PACU island modules.PACURequest miniapp + unmounted PacuDischargeCard own Supabase artifacts, no FK to RecoveryRoomRequest pacu_handoff_sbar, pacu_discharge_decision, pacu_state_cache PAR ≥ 9 gate
Dialysis / ABP / Holter /dialysis/* etc. no recovery stage (HD flowsheet ends at discharge vitals)
Imaging/NM sedation, ERCP no recovery stage built (sedation recovery is a real clinical gap)

⚠️ The schema blocker

packages/platform-api-schema/src/medication/recoveryRoomRequest/enity/RecoveryRoomRequest.ts declares operatingRoomRequestRef!: RefREQUIRED. But:

  • Labour deliveries (normal and caesarean done in LR) have a labourRoomRequest, not an OperatingRoomRequest — yet the labour recovery list writes the same collection. Rows either carry a bogus/absent ref or bypass validation.
  • Endoscopy/ERCP/CCL parents would be procedureRequest / cath case — also not OR requests.
  • There is no discriminator column: OR vs labour recovery rows are separated only by buildingRef/roomRef query filters, which is convention, not contract.

Consolidation verdict — polymorphic parent (Option A)

  1. Make operatingRoomRequestRef optional (deprecation window; backfill existing rows) and add parentRef?: { type: 'operatingRoom' | 'labour' | 'endoscopy' | 'cath' | 'sedation'; id: Uuid }.
  2. Extend listRecoveryRoomRequestApi with a parentType filter so each worklist scopes by contract, not by room-filter convention.
  3. One UI component set — keep the operating-room copy as canonical, parameterize by parentType, delete the labour duplicate (have /labour-room/list-lr-recovery import the OR set), delete the dead copies (§1.3).
  4. Endoscopy: on case completion, orchestrator creates a RecoveryRoomRequest(parentRef={type:'endoscopy'}); wire EndoscopyAldreteRecovery scores into it (it currently persists nothing).
  5. CCL stays siloed for now (billing-domain-specific); the new general recovery_stay_log (Step 5 below) coexists with ccl_recovery_stay_log, both feeding the ledger.
  6. Labour: remove the Supabase dual-source fallback in useLabourRoomRecovery.ts once the orchestrator projection (Step 1) exists.

Rejected alternatives: per-domain recovery tables (N+1 maintenance; CCL precedent doesn’t generalize) and “current shape + projection only” (doesn’t fix the required-ref break for labour, which is already live traffic on the shared collection).


3. The linkage tasks (do in order)

Step 0 (prerequisite, from §2.5): make operatingRoomRequestRef optional + add the polymorphic parentRef in packages/platform-api-schema/src/medication/recoveryRoomRequest/, backfill existing rows with parentRef={type:'operatingRoom'}, and add the parentType list filter. Everything below assumes recovery can belong to OR or labour or endoscopy.

Step 1 — Pick ONE source of truth, then bridge

Decision to make: keep REST canonical (Mongo via NestJS) and project to Supabase read-model, OR migrate the live list to read-model. Recommended: keep REST as write truth (consistent with the parent contract §10.4) and add an orchestrator projection so the live list and the PACU island share state.

  • Add handleRecoveryRoomRequest to infrastructure/medbase/functions/encounter-orchestrator/ (sibling of handlePacuAdmitted). On recovery create/update → upsert pacu_state_cache (encounter_id, status, pacu_admitted_at, pacu_discharged_at, last_par_score) and project encounter_journey_cache.surgical_status='in_pacu' (powers the parent contract §9 indicator).
  • Emit events RECOVERY_ADMITTED / RECOVERY_DISCHARGED (normalize → manifest.surgery.recovery_*).

Step 2 — Wire PAR → discharge decision (R3, the #1 critical gap)

  • In ParScoreDetail.tsx: auto-sum the 5 PAR items → total 0–10 (today it’s manual; mirror web/packages/miniapps/scoring-forms/definitions/aldrete.ts auto-calc, or standardize on Aldrete).
  • On PAR save → write pacu_discharge_decision (last_par_score, second_last_par_score, eligibility_first_met_at) via web/src/services/medbase/pacuDischarge.medbase.ts computeEligibility().
  • Mount PacuDischargeCard inside DialogRecoveryRoomRequest (it currently floats with no host) so the ≥9 eligibility gate is visible where the nurse enters the score.

Step 3 — Start/end time + duration (R1)

  • The form already has startDate/startTime/endDate/endTime. On lastStatus set, stamp endDate/endTime + pacu_discharged_at, and compute + display duration_minutes (port the ccl_recovery_stay_log pattern; general-OR has no stay log today). Surface duration in the list row.

Step 4 — Lab / Radiology ordering (R4 / R6) — already PARTIAL

The “ออเดอร์” row action already opens /patient-profile/{id}?tab=order (real orders: lab/path v2/medication/orderRequests, radiology v2/medication/imagingRequests, searchable). Optional polish: embed an inline order widget (QuickOrderDock) in DialogRecoveryRoomRequest so the nurse needn’t leave the screen. Not required for UAT pass — document as reachable.

Step 5 — Charges → financial (R7)

  • Today: EMAR drugs → mar_audit; CCL recovery → ccl_recovery_stay_log (billable). General-OR recovery supply/service charges are not captured.
  • Port the ccl_recovery_stay_log billing pattern to general-OR recovery (a recovery_stay_log with billing_product_id/charge_amount) and feed it into the ledger (billable_ledger / createFromLedger — see universal-facility-stay-billing.md). Recovery becomes a billable stay phase, not swallowed into case cost.

Step 6 — “In PACU now” indicator

Falls out of Step 1’s encounter_journey_cache.surgical_status='in_pacu' projection → reuse the parent contract §9 banner chip (PatientHeaderCard.tsx).

Step 7 — Consolidation (de-fragment)

  • Delete the 3 dead copies + dead Supabase service (§1.3 rows marked dead) — confirm 0 importers with grep first (guardrail: never delete code you didn’t author without confirming it’s truly orphaned).
  • Consolidate /labour-room/list-lr-recovery to import the operating-room component (single source).
  • Decide RecoveryRoomMiniApp.tsx: register in DynamicCoreApp or delete.

4. Acceptance — the 7 Recovery TOR points → done

TOR Done when…
R1 start + end time captured and duration auto-computed + shown in list and dialog
R2 recovery list shows transferred patients with anesthesia method as a column + live OR/PACU status from the read-model projection
R3 PAR auto-totals, writes pacu_discharge_decision, and PacuDischargeCard eligibility (≥9) is visible in the dialog
R4 lab orderable from recovery (reachable via order tab today; inline widget = stretch)
R5 lab requisition printable grouped by test type (today: generic DialogPrintGuideSheet)
R6 radiology orderable from recovery (reachable via order tab today; inline = stretch)
R7 recovery supply/service charges captured + auto-linked to financial (ledger/SalesOrder)

5. Contract checklist (per the parent contract’s required shapes)

  • [ ] Entity RecoveryRoomRequest links to OperatingRoomRequest via operatingRoomRequestRef (exists).
  • [ ] Status state machine documented + enforced (§2.4); discharge advances parent OR status.
  • [ ] Events RECOVERY_ADMITTED/RECOVERY_DISCHARGEDnormalizeEventType (add to _shared/event-contract.ts).
  • [ ] Orchestrator handleRecoveryRoomRequest projects pacu_state_cache + encounter_journey_cache.surgical_status.
  • [ ] Queue placement (optional): PACU bed availability in department_queues (dept_type pacu_arrival).
  • [ ] Acknowledgement (optional): notify ward charge nurse on sentBackToWard.
  • [ ] Policy gate (optional): discharge_from_pacu requiring PAR ≥ 9 (reuse engine; fail-open).

6. File inventory (what a session will touch)

Area File(s)
Live recovery UI web/packages/periops-kit/src/operating-room/components/list-or-recovery/{RecoveryRoomRequest,RecoveryRoomRequestDetail,DialogRecoveryRoomRequest,ParScoreDetail,VitalSignDetail,CoditionDetail}.tsx
REST service web/src/services/ever-medication/recoveryRoomRequest.service.ts
PACU island (to integrate) web/packages/miniapps/pacu-handoff/*, web/packages/medical-kit/src/pacu-discharge/PacuDischargeCard.tsx, web/src/services/medbase/{pacuHandoff,pacuDischarge}.medbase.ts
Orchestrator infrastructure/medbase/functions/encounter-orchestrator/ (new handleRecoveryRoomRequest), infrastructure/medbase/functions/inpatient-handlers/handlePacu{Admitted,Discharged}.ts, _shared/event-contract.ts
Read-model tables pacu_state_cache, pacu_discharge_decision, pacu_handoff_sbar, ccl_recovery_stay_log (083/20260528 migrations); new recovery_stay_log for R7
Scoring web/packages/miniapps/scoring-forms/definitions/aldrete.ts (standardize)
Indicator web/src/containers/ipd-command-center/components/cards/PatientHeaderCard.tsx (chip)
Delete/consolidate the §1.3 dead/stale paths

7. Notes for the implementing session

  • Guardrails: never delete a file without confirming 0 importers (grep -r); the dead list in §1.3 was verified 2026-06-10 but re-verify. Never write read-model tables from the frontend — go through the orchestrator. Seed must be bilingual (TH/EN).
  • Verify via the sandbox (?target=OrTorCoverage shows the audit; add a recovery-dialog target) + Playwright, per web/CLAUDE.md — preview MCP is broken in this repo.
  • The parent contract (or-request-approval-contract.md) phases P4–P6 assume this doc; build P4 (OR orchestrator handler) before Step 1 here so both share the surgical_status projection.
Ask Anything