ER System Master Contract
Master source of truth for the Emergency Room system in medOS-ultra.
Master source of truth for the Emergency Room system in medOS-ultra. Mirrors
admission-to-ward-unified-contract.mdandipd-medication-order-master-contracts.mdin scope and shape.Read this before any ER worklist, ER request, triage, or fast-track work.
0. The Core Insight
ER is BOTH an Encounter AND a Request. This is the same dual nature as Admission (encounter+request), Blood Bank (request only, no separate encounter), and Operating Room (request only). What makes ER special is that both layers already exist in the backend — they just aren’t wired through the central order system or the Supabase read model layer yet.
| Layer | Entity | Purpose | Status |
|---|---|---|---|
| Encounter | Encounter with encounterClass='EMER' |
The patient’s clinical episode in the ER | ✅ Working |
| Request | EmergencyMedicalService (emergency_medical_service collection) |
The operational ER service ticket | ✅ Entity exists, ⚠️ not wired through orderRequest |
| Resource | EmergencyRoom (emergency_room collection) |
Physical bay/zone (resus, trauma, fast-track) with ESI, devices, attending | ✅ Entity exists |
| Read model | department_queues with dept_type='screening' |
Operational queue surface | ⚠️ Works, but ER mixed with OPD triage |
| Manifest | encounter_journey_cache.encounter_class='EMER' |
Macro view per encounter | ✅ Working |
The implication: any clinical flow can spawn an ER request, same way any flow can spawn a lab order or imaging request. The patient may already be in OPD (deteriorating walk-in), IPD (code blue / RRT), in transit (EMS pre-arrival), or external (inbound referral). The ER request is the universal escalation primitive.
1. Three Worlds, Six Entry Points
┌──────────────────────────────┐
│ EmergencyMedicalService │ ← canonical request
│ (emergency_medical_service) │ patient, encounter,
│ │ severity, status,
│ │ orderRequestRef ─┐
└──────────────┬─────────────────┘ │
│ │
│ Created by ANY of │ links
│ 6 entry points ↓ │ to
│ ▼
┌──────────┬──────────┬────────┴────────┬──────────┬──────────┐ ┌────────────┐
│ │ │ │ │ │ │ orderRequest│
▼ ▼ ▼ ▼ ▼ ▼ │ (billing, │
Direct OPD IPD Inbound EMS Other│ events, │
Walk-in Escalate Code Blue/RRT Referral Pre-arrival │ webhooks) │
│ │ │ │ │ │ └────────────┘
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
NEW LINK to LINK to LINK to NEW LINK to
EMER existing existing IPD new EMER EMER existing
encounter OPD enc encounter enc encounter encounter
+request +request +request +request +request +request
Entry point matrix
| # | Source | Encounter | ER Request | Trigger | Status today |
|---|---|---|---|---|---|
| 1 | Direct walk-in | NEW EMER |
NEW | DialogCreateVisit with emergencyDetails.emergencyPatient=true |
✅ Works |
| 2 | OPD escalation | LINK existing AMB→EMER reclass, or NEW EMER |
NEW | OPD doctor “ส่ง ER” menu action | ❌ No UI |
| 3 | IPD Code Blue / RRT | LINK existing IMP (keep IPD encounter) |
NEW (special type=emergency_response) |
IPD nurse “Code Blue” button | ❌ No UI |
| 4 | Inbound referral | NEW EMER |
NEW | /referral accept → spawn EMER encounter |
✅ Path exists, not wired |
| 5 | EMS pre-arrival | Optional pre-registered EMER |
NEW (PENDING, no encounter yet) | Public API POST /api/ems/notify |
❌ Endpoint doesn’t exist |
| 6 | Mass casualty | Multiple NEW EMER |
Multiple, batch-spawned | Mass casualty incident dispatch | ✅ Tab exists, partial |
Why ER must be both layers
| Why an Encounter? | Why a Request? |
|---|---|
| Patient needs a clinical record (HN, AN, EN) | OPD/IPD/Referral patients already HAVE an encounter — need an ER “ticket” hanging off it |
| Billing rolls up to one encounter | Multiple ER touches per encounter (initial visit, repeat visit same day) |
| Diagnosis codes attach to encounter | ESI level / severity is per-event, not per-encounter |
| Discharge disposition is encounter-level | Multiple severity escalations within one encounter are valid |
| Per-encounter LOS / arrival timestamp | Operational SLA clock (door-to-doctor) is per-request |
Rule: the ER encounter is the patient’s clinical record; the ER request is the operational unit of work. One encounter, many requests possible.
2. Entity Contracts
2.1 EmergencyMedicalService (MongoDB — write truth)
Source: packages/platform-api-schema/src/administration/emergencyMedicalService/entity/EmergencyMedicalService.ts
| Field | Type | Notes |
|---|---|---|
_id |
UUID | MongoDB |
patientRef |
UUID | FK → Patient |
encounterRef |
UUID | FK → Encounter (REQUIRED — every request has an encounter) |
status |
EmergencyMedicalServiceStatus |
pending / in_progress / completed |
emergencyMedicalServiceType |
EmergencyMedicalServiceType |
diagnosis / treatment / transfer / other |
severityLevel |
EmergencyMedicalServiceSeverityLevel |
normal / urgent / emergency |
serviceDate |
Date | When service began |
clinicRef / subClinicRef / locationRef |
UUID | Where |
responsibleProvider |
UUID | FK → User |
chiefComplaint |
string | TH or EN |
treatmentHistory |
string | |
testResults |
string | |
notes |
string | |
transferDate |
Date | When transferred out (if disposition=transfer) |
orderRequestRef |
UUID | FK → OrderRequest (declared, not auto-populated yet) |
Gaps in current model (proposed extensions in §6):
- No
esiLevel: 1..5(currently lives onEncounteronly) - No
zone: red\|yellow\|green\|black(lives onscreeningDetails) - No
arrivalAt/triageAt/doctorAssignedAt/dispositionAtSLA timestamps - No
disposition: discharged\|admitted\|referred\|expired\|lwbs - No
triggerSource: walkin\|opd_escalation\|ipd_code\|referral\|ems\|mass_casualty - No
parentEncounterReffor OPD→ER or IPD code-blue cases
2.2 EmergencyRoom (MongoDB — physical resource)
Source: packages/platform-api-schema/src/administration/emergencyRoom/entity/EmergencyRoom.ts
| Field | Notes |
|---|---|
status |
EmergencyRoomStatus (available / occupied / cleaning) |
emergencyRoomType |
EmergencyRoomType (resus / trauma / fast-track / observation) |
roomNumber |
display label |
patient |
current occupant |
encounter |
linked encounter |
esiLevel |
current ESI of occupant (1-5) |
attendingStaff[] |
doctor + nurses |
device[] |
available devices in bay |
medication[] |
crash cart contents |
lastCleaned |
turnover timestamp |
2.3 Encounter (existing, write truth)
The Encounter entity already supports ER. Relevant fields:
| Field | Notes |
|---|---|
encounterClass |
'EMER' for ER (also accepts 'AMB', 'IMP', 'OBSENC', 'VR') |
screeningDetails.screeningInfo.esiLevel |
ESI 1-5 |
screeningDetails.screeningInfo.zone |
Red/Yellow/Green/Black |
screeningDetails.screeningInfo.activateFastTrack |
chest pain / stroke / sepsis / trauma flags |
emergencyDetails.emergencyPatient |
boolean flag — the gate that sets encounterClass='EMER' |
emergencyDetails.isMassCasualtyAccident |
mass casualty incident link |
emergencyDetails.transporterName / transporterTelContact |
EMS / ambulance |
consultationWorkflowState |
'er-wait-screening', 'er-in-process', etc. |
parentEncounter |
(proposed) FK to OPD encounter when ER spawned from OPD escalation |
2.4 Supabase read models
encounter_journey_cache — already supports EMER. Existing columns:
| Column | EMER usage |
|---|---|
encounter_class |
'EMER' |
clinical_context.encounter_context.esiLevel |
1-5 |
clinical_context.encounter_context.emergencyType |
trauma / non-trauma / fast-track |
current_physical_location |
ER bay / triage / resus |
pending_tickets |
{ "screening": [...], "consultation": [...], "lab": [...], ... } |
Proposed new columns (migration xxxx_er_journey_columns.sql):
| Column | Purpose |
|---|---|
er_arrival_at |
hoisted from encounter for SLA queries |
er_triage_at |
door-to-triage |
er_doctor_at |
door-to-doctor |
er_disposition |
discharged | admitted | referred | expired | lwbs |
er_disposition_at |
door-to-disposition |
er_chief_complaint |
hoisted for column display |
er_zone |
red / yellow / green / black |
department_queues — works, but ER currently uses dept_type='screening'
which mixes ER triage with OPD triage. Proposed:
| Current dept_type | Proposed dept_type for EMER | Why |
|---|---|---|
screening |
er_triage |
Isolate ER triage queue from OPD pre-screening |
consultation |
er_consultation |
Isolate ER doctor queue from OPD clinic doctors |
wait-result |
er_observation |
Observation bay separate from OPD wait-result |
This is the single most important change — it lets the frontend filter
dept_type IN ('er_triage','er_consultation','er_observation') and get
a clean ER worklist.
New table: emergency_service_request_cache (Supabase mirror of
EmergencyMedicalService) — same pattern as referral_cache (migration 052).
CREATE TABLE emergency_service_request_cache (
id UUID PRIMARY KEY,
mongo_ref TEXT, -- EmergencyMedicalService._id
encounter_id TEXT NOT NULL,
patient_id TEXT NOT NULL,
parent_encounter_id TEXT, -- OPD/IPD link for escalations
trigger_source TEXT NOT NULL, -- walkin/opd_escalation/ipd_code/referral/ems/mass_casualty
service_type TEXT, -- diagnosis/treatment/transfer/other
severity_level TEXT, -- normal/urgent/emergency
esi_level SMALLINT CHECK (esi_level BETWEEN 1 AND 5),
zone TEXT, -- red/yellow/green/black
status TEXT NOT NULL, -- pending/in_progress/completed
chief_complaint TEXT,
arrival_at TIMESTAMPTZ,
triage_at TIMESTAMPTZ,
doctor_at TIMESTAMPTZ,
disposition TEXT, -- discharged/admitted/referred/expired/lwbs
disposition_at TIMESTAMPTZ,
responsible_provider_id TEXT,
responsible_provider_name TEXT,
emergency_room_id TEXT, -- EmergencyRoom._id
emergency_room_label TEXT, -- "Resus-1", "Trauma-A"
order_request_id TEXT, -- canonical orderRequest (billing)
ack_request_id TEXT, -- AcknowledgementRequest for SLA
fast_track_flags JSONB, -- { chestPain, stroke, sepsis, trauma }
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_emer_request_encounter ON emergency_service_request_cache(encounter_id);
CREATE INDEX idx_emer_request_status ON emergency_service_request_cache(status);
CREATE INDEX idx_emer_request_severity ON emergency_service_request_cache(severity_level, esi_level);
CREATE INDEX idx_emer_request_trigger ON emergency_service_request_cache(trigger_source);
3. Status Enums & Progressions
3.1 EmergencyMedicalServiceStatus
┌─────────────┐
│ pending │ Request created, not yet started
└──────┬──────┘ (e.g. EMS pre-arrival, OPD escalation queued)
│
▼
┌─────────────┐
│ in_progress │ Patient in ER, treatment underway
└──────┬──────┘ (most of ER lifecycle)
│
▼
┌─────────────┐
│ completed │ Disposition reached (discharge/admit/refer/expired/lwbs)
└─────────────┘
3.2 EmergencyMedicalServiceType
| Value | Use |
|---|---|
diagnosis |
Diagnostic-only visit (e.g. CT, ECG, then discharge) |
treatment |
Active treatment (IV fluids, stitching, reduction, etc.) |
transfer |
Refer-out from ER to another hospital |
other |
Catch-all (observation, fast-track, social admit) |
3.3 EmergencyMedicalServiceSeverityLevel
| Value | Maps to ESI | Color zone |
|---|---|---|
emergency |
1–2 | Red |
urgent |
3 | Yellow |
normal |
4–5 | Green |
3.4 ER Encounter Workflow States (existing)
From nurse-emergency-workflow.json:
er-wait-screening (initial)
│ ESI triage
▼
er-screened
│ assign doctor
▼
er-wait-doctor
│ start treatment
▼
┌──────────────── er-in-process ────────────────┐
│ │ │
│ [send results] │ [observe] [discharge]│
▼ ▼ ▼
er-wait-result er-observe-symptoms er-finished
│ │ │
│ [return] │ [finish observe] ├─ discharge → er-discharged
└────► er-in-process └────► er-finished ├─ admit → er-admitted
└─ refer → er-referred
4. Event Choreography (manifest events)
ER spans 3 services. All events flow through hospital_events →
encounter-orchestrator.
4.1 Events currently emitted
| Event | Emitted by | Payload |
|---|---|---|
clinical.emergency.created |
emergencyMedicalService.controller.mixin.ts |
{ emergencyMedicalServiceId, encounter, patient, status, severityLevel } |
manifest.encounter.seeded |
encounter create (any class) | full encounter context |
manifest.encounter.updated |
encounter update | partial context |
manifest.queue.workflow_transition |
workflow action handler | { encounter, fromState, toState, deptType } |
4.2 Events to add (proposed)
| Event | When | Payload |
|---|---|---|
manifest.emergency.created |
ER request created | { emergencyServiceId, encounterId, patientId, triggerSource, parentEncounterId, severityLevel, esiLevel, chiefComplaint, arrivalAt } |
manifest.emergency.triaged |
ESI assigned, screening complete | { emergencyServiceId, esiLevel, zone, fastTrackFlags, triageAt } |
manifest.emergency.escalated |
OPD/IPD → ER trigger | { parentEncounterId, newEncounterId, triggerSource, severityLevel, escalatedBy } |
manifest.emergency.disposed |
Disposition reached | { emergencyServiceId, encounterId, disposition, dispositionAt } |
manifest.emergency.lwbs |
Left without being seen | { emergencyServiceId, encounterId, lastSeenAt } |
manifest.emergency.boarding |
Admitted but no bed > 4h | { emergencyServiceId, encounterId, admittedAt, boardingDuration } |
4.3 Orchestrator handler additions
In infrastructure/medbase/functions/encounter-orchestrator/:
// New handler: handleEmergencyCreated
async function handleEmergencyCreated(event: HospitalEvent) {
const { emergencyServiceId, encounterId, patientId, triggerSource,
parentEncounterId, severityLevel, esiLevel, chiefComplaint,
arrivalAt } = event.payload;
// 1. Upsert emergency_service_request_cache
await supabase.from('emergency_service_request_cache').upsert({
id: emergencyServiceId,
encounter_id: encounterId,
patient_id: patientId,
parent_encounter_id: parentEncounterId,
trigger_source: triggerSource,
severity_level: severityLevel,
esi_level: esiLevel,
chief_complaint: chiefComplaint,
arrival_at: arrivalAt,
status: 'pending',
});
// 2. Place in department_queues as dept_type='er_triage'
await upsertQueueRow({
encounterId, patientId,
dept_type: 'er_triage',
ticket_id: emergencyServiceId,
status: 'WAITING',
priority: severityLevel === 'emergency' ? 'STAT'
: severityLevel === 'urgent' ? 'URGENT' : 'ROUTINE',
metadata: { triggerSource, parentEncounterId, fastTrackFlags },
});
// 3. Hoist ER columns to encounter_journey_cache
await supabase.from('encounter_journey_cache').update({
er_arrival_at: arrivalAt,
er_chief_complaint: chiefComplaint,
er_disposition: null,
}).eq('encounter_id', encounterId);
// 4. Create AcknowledgementRequest with SLA expiry based on ESI
const slaMinutes = { 1: 0, 2: 10, 3: 30, 4: 60, 5: 120 }[esiLevel] ?? 30;
await createAcknowledgementRequest({
kind: 'er_triage_sla',
targetUserRole: 'er_triage_nurse',
expiresAt: addMinutes(arrivalAt, slaMinutes),
encounterId,
metadata: { emergencyServiceId, esiLevel },
});
}
// New handler: handleEmergencyTriaged
async function handleEmergencyTriaged(event: HospitalEvent) {
// Move from dept_type='er_triage' to dept_type='er_consultation'
// Update emergency_service_request_cache.triage_at + esi_level + zone
// Update encounter_journey_cache.er_triage_at + er_zone
// Resolve er_triage_sla ack, create er_doctor_sla ack
}
// New handler: handleEmergencyEscalated
async function handleEmergencyEscalated(event: HospitalEvent) {
// OPD/IPD → ER promotion
// Either reclass existing encounter (encounter_class AMB→EMER)
// OR spawn new EMER encounter with parentEncounterRef
// Place in er_triage queue
}
// New handler: handleEmergencyDisposed
async function handleEmergencyDisposed(event: HospitalEvent) {
// Move dept_type='er_*' rows to completed_tickets
// Update emergency_service_request_cache.disposition + disposition_at
// Update encounter_journey_cache.er_disposition + er_disposition_at
// If disposition='admitted' → cascade to handleAdmissionUpdated
// If disposition='referred' → cascade to handleReferralCreated
}
5. Frontend Architecture
5.1 The three surfaces
| Surface | Route | Data source | Status |
|---|---|---|---|
| ER Worklist (nurse) | /emergency/list-patient |
Legacy: REST + Socket.IO. Migrate to: Supabase department_queues + emergency_service_request_cache realtime |
⚠️ Legacy |
| ER Doctor View | /emergency/doctor |
Same migration path | ⚠️ Legacy |
| ER Hub | /emergency/hub |
Config landing, links to admin pages | ✅ New |
5.2 New components needed
| Component | Purpose | Where to mount |
|---|---|---|
DialogCreateEmergencyRequest |
Universal “ส่ง ER” dialog — spawn ER request from any context | DoctorActionBar, OPD doctor menu, IPD code blue button |
useERWorklistRealtime |
Supabase realtime subscription hook (like useCommandCenterRealtime for IPD) |
TableAllPatientEr replacement |
useERRequest(encounterId) |
React Query hook for the active ER request | EmergencyDoctor, ER profile panel |
ERSLABadge |
Door-to-triage / door-to-doctor SLA indicator | Queue rows |
CodeBlueButton |
One-click IPD → ER rapid response | IPD command center, ward view |
EMSPreArrivalPanel |
Pre-arrival notification surface | ER triage worklist |
5.3 Universal entry point modal
DialogCreateEmergencyRequest is the universal entry. Its behavior depends on
the source context:
| Source | Pre-fills | Encounter behavior |
|---|---|---|
| Walk-in (Registration) | Empty | Creates new EMER encounter |
| OPD doctor | Patient + current encounter | Either reclasses OPD→EMER or spawns new EMER linked via parentEncounter (admin choice) |
| IPD nurse (code blue) | Patient + IPD encounter | Keeps IPD encounter, creates ER request with serviceType=treatment and severityLevel=emergency |
| Inbound referral | From referral payload | Creates new EMER encounter, links referralRequestId |
| EMS pre-arrival | EMS payload (national ID, chief complaint) | Creates ER request with status=pending and no encounter; encounter spawned on arrival |
| Mass casualty | Incident ID | Batch-create with shared incidentId |
6. Migration Plan — 6 Phases
Phase 0 — Backend Wiring (preparation, no schema)
Effort: 1 day
- [ ] Add ER request to gateway whitelist (
administration.emergencyMedicalService.*) - [ ] On
EmergencyMedicalService.create, auto-create parentorderRequestwithcategory='emergency', populateorderRequestRef - [ ] Emit
manifest.emergency.createdevent (in addition to existingclinical.emergency.createdwebhook) - [ ] Add
triggerSource,parentEncounterRef,arrivalAt,triageAt,doctorAt,dispositionAt,disposition,zone,esiLevelto entity
Phase 1 — Supabase Read Model (orchestrator + tables)
Effort: 1-2 days
- [ ] Migration:
emergency_service_request_cachetable (schema in §2.4) - [ ] Migration: hoist
er_arrival_at,er_triage_at,er_doctor_at,er_disposition,er_disposition_at,er_chief_complaint,er_zonetoencounter_journey_cache - [ ] Migration: add
dept_type='er_triage','er_consultation','er_observation'values to dept_type enum or whitelist - [ ] Orchestrator: add 5 new handlers (
handleEmergencyCreated,handleEmergencyTriaged,handleEmergencyEscalated,handleEmergencyDisposed,handleEmergencyLwbs) - [ ] Orchestrator: update
handleQueueWorkflowTransitionto emitdept_type='er_*'whenencounter_class='EMER'
Phase 2 — Cron Jobs for SLA Monitoring
Effort: ½ day
- [ ]
er-triage-sla-evaluator(every 1 min): escalate WAITING rows whoseack_requestexpired (ESI-2 > 10min, ESI-3 > 30min, ESI-4 > 60min) - [ ]
er-doctor-assignment-sla(every 2 min): alert ifer_triage_atset buter_doctor_atnull > 45 min - [ ]
er-lwbs-detector(daily): flag encounters closed ater-wait-screening - [ ]
er-boarding-alert(every 15 min): admitted ER patients still in ER bay > 4h - [ ] All registered via
cron_jobstable (seecron-jobs-registry.md)
Phase 3 — Universal DialogCreateEmergencyRequest
Effort: 1-2 days
- [ ] Build
DialogCreateEmergencyRequestcomponent inweb/packages/healthops-kit/src/er-system/components/dialogs/ - [ ] 6 modes: walk-in, opd_escalation, ipd_code, referral, ems, mass_casualty
- [ ] Register as
DIALOG_CREATE_EMERGENCYin modalRegistry - [ ] Wire into:
- OPD doctor workflow JSON menu (new
er-escalateaction) - IPD command center as
CodeBlueButton - Referral inbound accept flow (auto-spawn)
- Public API for EMS pre-arrival
DoctorActionBarquick actionQuickOrderDock
- OPD doctor workflow JSON menu (new
Phase 4 — Migrate ER Worklist to Supabase Realtime
Effort: 1-2 days
- [ ] Build
useERWorklistRealtime(locationId)hook (mirror ofuseCommandCenterRealtimefor IPD) - [ ] Replace
TableAllPatientErdata source: REST →supabase.from('department_queues').filter(...).filter('dept_type', 'in', ['er_triage','er_consultation','er_observation']) - [ ] Remove Socket.IO connection (
allPatientOpdScreeningSocketIOClient) - [ ] Wire workflow JSON actions to
WorkflowBasedTabs/workflowActionHandlerpipeline (so the menus we added — e-MAR, nursing assessment, scoring, referral — actually open from the ER row context) - [ ] Keep legacy screening dialog (
DialogScreening) — it works fine; just trigger from the new realtime row
Phase 5 — Configurable Escalation Rules
Effort: 1 day
- [ ] Extend
encounter_class_rulestable (already exists fromclaude/patient-status-emer-entry) withescalation_rules:- Auto-escalate AMB→EMER if EWS ≥ 7 in OPD
- Auto-escalate IMP→EMER short-circuit when code blue triggered
- Auto-fast-track based on chief complaint keywords
- [ ] Admin UI at
/admin/encounter-class-configalready exists — extend with escalation conditions
Phase 6 — Reporting / Analytics
Effort: 1 day
- [ ] Door-to-doctor view (
v_er_door_to_doctor) — % within target by ESI - [ ] LWBS rate view (
v_er_lwbs_rate) — daily/weekly - [ ] Boarding hours view (
v_er_boarding_hours) — admit-to-bed delay - [ ] Disposition mix view (
v_er_dispositions) — discharge/admit/refer rates - [ ] Dashboard at
/emergency/analyticsconsuming the views
7. Cross-System Cascades
When ER request → admitted
handleEmergencyDisposedfires withdisposition='admitted'- Cascades to
handleAdmissionUpdated(existing IPD pipeline) - New admission row created, bed assignment workflow starts
- ER queue row closed, IPD queue row opened
- Encounter stays the same (
encounter_classmay flipEMER→IMP, or remainEMERwithparentForAdmission)
When ER request → referred out
handleEmergencyDisposedfires withdisposition='referred'- Cascades to
handleReferralReceived(existing referral pipeline) - New row in
referral_request(outbound) - Same encounter;
er_disposition='referred'
When ER request → discharged home
handleEmergencyDisposedfires withdisposition='discharged'handleEncounterClosedfires- Billing finalizes via
orderRequest→saleOrder→claim - Encounter moves to
completed_tickets
When OPD doctor escalates AMB→EMER
manifest.emergency.escalatedfires- Two options based on admin config:
- Reclass: Same encounter,
encounter_classAMB→EMER, OPD queue row closed, ER queue row opened - Spawn: New EMER encounter with
parentEncounter, OPD encounter stays open until OPD doctor closes it, ER encounter independent
- Reclass: Same encounter,
- ER request created linked to the (possibly new) encounter
- Patient appears in ER worklist with
triggerSource='opd_escalation'
When IPD code blue
manifest.emergency.escalatedwithtriggerSource='ipd_code'- IPD encounter unchanged (still
encounter_class='IMP') - ER request created with
serviceType='treatment',severityLevel='emergency' - Patient does NOT move from IPD bed; instead ER team comes to ward
- Special EmergencyRoom assignment: virtual room representing the ward bed
- On resolution: ER request
completed, IPD encounter continues OR escalates to ICU
8. The 9 Invariants
Same convention as
admission-to-ward-unified-contract.md§10.
- Every ER request has an encounter.
encounterRefis required. No exceptions — even EMS pre-arrival spawns an encounter shell first. - Encounter class is the gate.
encounter_class='EMER'is the only way to appear in ER worklist queries. ER requests linked to non-EMER encounters (IPD code blue) appear in IPD worklists with an “ER active” badge, not in ER worklist. - One encounter, many requests. Multiple
EmergencyMedicalServicerows per encounter are valid (e.g. patient returns same day, or multiple severity escalations within the visit). - The request is the SLA clock. Door-to-triage and door-to-doctor measure on the request, not the encounter. Encounter LOS may include pre-ER OPD time when escalated.
orderRequestRefis the billing bridge. Without it, the ER visit has nosaleOrder, no claim, no revenue. Phase 0 fix.dept_typenamespacing is the queue gate.screeningis OPD,er_triageis ER. Frontend filters cleanly when this is enforced.- The encounter-orchestrator is the only writer to read models.
Frontend never writes to
emergency_service_request_cache,department_queues, orencounter_journey_cache. Always through backend → events → orchestrator. - Disposition is terminal. Once
dispositionis set on the cache, the request iscompletedand queues are closed. Cascading effects (admission, referral, discharge) flow fromhandleEmergencyDisposed. - Cron-driven SLA is the safety net. No event-driven SLA — every wait threshold is enforced by a cron job that scans WAITING rows and escalates via AcknowledgementRequest, never by inline triggers.
9. Decision: Is ER an Order System or an Encounter EMR?
Both. Same answer as Admission, Operating Room, Blood Bank, Imaging.
- The encounter EMR layer (
encounter_class='EMER') is the patient’s clinical record — the same primitive as OPD/IPD. - The order/request layer (
EmergencyMedicalService→orderRequest) is the operational unit that:- Surfaces in queues
- Drives billing
- Has SLA timers
- Cascades to admission / referral / discharge
- Can be created from ANY context (walk-in, OPD, IPD, referral, EMS)
This matches the existing pattern in central-order-system.md — ER is one of
the disconnected order types that needs to follow the same Blood Bank
recipe. The backend entity already exists with orderRequestRef declared.
Wire it through, mirror it in Supabase, expose the universal
DialogCreateEmergencyRequest, and ER becomes a first-class citizen of the
unified order system while keeping its full encounter semantics.
10. File References
| Topic | Path |
|---|---|
| Existing entity | packages/platform-api-schema/src/administration/emergencyMedicalService/entity/EmergencyMedicalService.ts |
| Backend service | services/administration/src/api/administration/modules/emergencyMedicalService/ |
| EmergencyRoom resource | packages/platform-api-schema/src/administration/emergencyRoom/entity/EmergencyRoom.ts |
| ER workflow JSON | web/packages/medical-kit/src/medical-worklist/defaults/nurse-emergency-workflow.json |
| Legacy worklist (to migrate) | web/packages/medical-kit/src/emergency-system/emergency-list-patient/components/all-patient-emergency/TableAllPatientEr.tsx |
| Routes | web/src/routes/EmergencyRoutes.tsx |
| Encounter class rules (recently merged) | web/src/services/ever-administration/encounterClassRules.service.ts |
| Emergency Hub (recently merged) | web/src/containers/emergency-hub/page.tsx |
| Screening dialog (legacy, still used) | web/packages/patient-roster/src/hospital-outpatient-screeninglist/components/dialog-screening/DialogScreening.tsx |
| Screening V2 (modern) | web/packages/patient-roster/src/hospital-outpatient-patientlist/components/workflows/patient-check-in/menu-tab/screening-v2/ |
| Central order system spec | docs/architecture/central-order-system.md |
| Admission contract reference | docs/architecture/admission-to-ward-unified-contract.md |
| Orchestrator master ref | docs/architecture/encounter-orchestrator-triggers.md |
| Cron registry | docs/architecture/cron-jobs-registry.md |
| Acknowledgement system | docs/architecture/acknowledgement-system.md |
| Referral system master | docs/architecture/referral-system-master.md (TBD — companion doc) |