Recovery Room Request Linkage
Wiring RecoveryRoomRequest (PACU) into the central OR request lifecycle.
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 foror_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-recovery → web/src/containers/operating-room/list-or-recovery/index.tsx → RecoveryRoomRequest (@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 only — GET/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.tsx→pacu_discharge_decision(PAR ≥ 9 gate).- Orchestrator
handlePacuAdmitted.ts/handlePacuDischarged.ts→pacu_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
RecoveryRoomRequestbelongs to exactly oneOperatingRoomRequestviaoperatingRoomRequestRef, and inheritspatientRef/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-recovery → web/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!: Ref — REQUIRED. But:
- Labour deliveries (normal and caesarean done in LR) have a
labourRoomRequest, not anOperatingRoomRequest— 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/roomRefquery filters, which is convention, not contract.
Consolidation verdict — polymorphic parent (Option A)
- Make
operatingRoomRequestRefoptional (deprecation window; backfill existing rows) and addparentRef?: { type: 'operatingRoom' | 'labour' | 'endoscopy' | 'cath' | 'sedation'; id: Uuid }. - Extend
listRecoveryRoomRequestApiwith aparentTypefilter so each worklist scopes by contract, not by room-filter convention. - One UI component set — keep the operating-room copy as canonical, parameterize by
parentType, delete the labour duplicate (have/labour-room/list-lr-recoveryimport the OR set), delete the dead copies (§1.3). - Endoscopy: on case completion, orchestrator creates a
RecoveryRoomRequest(parentRef={type:'endoscopy'}); wireEndoscopyAldreteRecoveryscores into it (it currently persists nothing). - CCL stays siloed for now (billing-domain-specific); the new general
recovery_stay_log(Step 5 below) coexists withccl_recovery_stay_log, both feeding the ledger. - Labour: remove the Supabase dual-source fallback in
useLabourRoomRecovery.tsonce 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
operatingRoomRequestRefoptional + add the polymorphicparentRefinpackages/platform-api-schema/src/medication/recoveryRoomRequest/, backfill existing rows withparentRef={type:'operatingRoom'}, and add theparentTypelist 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
handleRecoveryRoomRequesttoinfrastructure/medbase/functions/encounter-orchestrator/(sibling ofhandlePacuAdmitted). On recovery create/update → upsertpacu_state_cache(encounter_id,status,pacu_admitted_at,pacu_discharged_at,last_par_score) and projectencounter_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; mirrorweb/packages/miniapps/scoring-forms/definitions/aldrete.tsauto-calc, or standardize on Aldrete). - On PAR save → write
pacu_discharge_decision(last_par_score,second_last_par_score,eligibility_first_met_at) viaweb/src/services/medbase/pacuDischarge.medbase.tscomputeEligibility(). - Mount
PacuDischargeCardinsideDialogRecoveryRoomRequest(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. OnlastStatusset, stampendDate/endTime+pacu_discharged_at, and compute + displayduration_minutes(port theccl_recovery_stay_logpattern; 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_logbilling pattern to general-OR recovery (arecovery_stay_logwithbilling_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-recoveryto import the operating-room component (single source). - Decide
RecoveryRoomMiniApp.tsx: register inDynamicCoreAppor 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
RecoveryRoomRequestlinks toOperatingRoomRequestviaoperatingRoomRequestRef(exists). - [ ] Status state machine documented + enforced (§2.4); discharge advances parent OR status.
- [ ] Events
RECOVERY_ADMITTED/RECOVERY_DISCHARGED→normalizeEventType(add to_shared/event-contract.ts). - [ ] Orchestrator
handleRecoveryRoomRequestprojectspacu_state_cache+encounter_journey_cache.surgical_status. - [ ] Queue placement (optional): PACU bed availability in
department_queues(dept_typepacu_arrival). - [ ] Acknowledgement (optional): notify ward charge nurse on
sentBackToWard. - [ ] Policy gate (optional):
discharge_from_pacurequiring 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=OrTorCoverageshows the audit; add a recovery-dialog target) + Playwright, perweb/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_statusprojection.