# م5 — عقد Fable + ليدجر التقدم (2026-07-07)

## ✅ م5 مكتملة — منشورة /app + hazemdev (بانتظار تست المالك)
**9 WP ✅** native+Codex+Fable. نشر /app `main-GGBADCV5.js`؛ push hazemdev (BE `2731ba769`/FE `4e80511`)؛ CHANGELOG م5؛ **585 نجاح/1 pre-existing** (ClinicRoomTest HRM — مش من م5)؛ صفر regression؛ visit_stations مزروع. SHAs: WP1 `4487a3272` · WP2 `3dc3e3a40` · WP3 `e98953b98`/`814eee9` · WP4 `6957102` · WP5+6 `185daa3` · WP7 `edcb8e1` · WP8 `14d708c66`/`4e80511`. اتسلّم: طابور حي (auto-close/waited/urgent/no-show/readiness/reorder) · encounter-on-demand · محطتا القياسات+الشيت · now-serving display · إعداد visit_stations. **⛔ المالك: merge→main + ship + تست.**


> العقد المُلزِم من Fable. الخريطتان في `RESUME.md`. **اقرأ الليدجر أول حاجة عند العودة.**

## 📊 ليدجر
| WP | المحتوى | يعتمد | مراجعة | الحالة |
|----|---------|-------|--------|--------|
| WP1 | Queue liveness (BE): auto-close events+listeners · symmetric cancel · arrived→no_show · waited_since→arrived_at + waited_minutes · urgent sort · dept/doctor filter · encounter_id+has_vitals+has_sheet في resource | — | native+Codex | ✅ |
| WP2 | ✅ Encounter-on-demand (BE): `POST appts/{id}/encounter` (ensureForAppointment، idempotent) + `beginConsult` flag على EncounterStarted + `POST encounters/{id}/begin` | WP1 | native+Codex | ✅ |
| WP3 | `clinic.visit_stations` setting (merged\|split، default merged، nav-gate فقط) | — | single | ✅ |
| WP4 | ✅ Queue screen (FE): إنقاذ `VisitQueueComponent` (field-map للـresource الحقيقي + urgent badge + ticking waited + ✓✓ + cancel/no-show + dept/doctor filter + 30s poll) · route `clinic/queue` · استبدال لوحة الداشبورد بيه | WP1 | single | ✅ |
| WP5 | Vitals station (FE): route `clinic/stations/vitals` — queue list → ensure → mount `vitals-panel` | WP2+WP4 | single | ✅ |
| WP6 | Sheet station (FE): نفس الـshell + `history-panel` (يسيب profile history زي ما هو) | WP2+WP4 | single | ✅ |
| WP7 | Now-serving display (FE): route `clinic/queue/display` — أرقام كبيرة per dept/doctor + waiting، 10s poll، بلا أفعال، logged-in (مفيش kiosk auth) | WP1 | single | ✅ |
| WP8 | Reorder (BE+FE): `queue_position` migration + `PATCH appts/{id}/queue-position` + drag/buttons — **قابل للقص** | WP1 | native (BE) | ✅ |
| WP9 | Integration + deploy /app + push hazemdev + CHANGELOG + owner-test script | الكل | self+smoke | ✅ |

**DAG:** WP1 ‖ WP3 (ابدأ). WP2 بعد WP1؛ WP4 بعد WP1؛ WP7 بعد WP1. WP5+WP6 بعد WP2+WP4. WP8 آخرًا (قابل للقص). WP9 نشر.

## القرارات المُلزِمة (Fable)
- **a. Auto-close:** event جديد `EncounterCancelled` (dispatch آخر `EncounterService::cancel`) + listenerان: `EncounterSigned→CloseAppointmentOnSign` (arrived/in_consultation→done) · `EncounterCancelled→CancelAppointmentOnEncounterCancel` (→cancelled). **يكتبوا مباشرة (مش عبر `TransitionAppointmentStatus` اللي يمنع arrived→done ويطلب User)** بنمط `SyncAppointmentStatus`. walk-in (appointment_id=null) = no-op. **symmetric:** إلغاء/no-show الموعد (`POST appts/{id}/status`) على موعد له encounter مفتوح → يلغي الـencounter كمان.
- **b. Encounter-on-demand (المحور):** **ما تعملش auto-create عند الوصول.** `POST clinic/appointments/{id}/encounter` idempotent (لو encounter_id موجود رجّعه؛ وإلا `EncounterService::ensureForAppointment` يفتح من حقول الموعد). perm = OR(`vitals.record`,`history.record`,`encounters.create`). guard: الموعد arrived/in_consultation. **فصل الانتقال:** فلاج `beginConsult=true` (param تاني على `EncounterStarted`، additive)؛ `SyncAppointmentStatus` **دايمًا يختم encounter_id** بس ينقل arrived→in_consultation فقط لو `beginConsult`. مسار ensure يبعت `false` (التمريض ≠ المريض في الغرفة، الطابور يفضل «waiting»). `POST clinic/encounters/{id}/begin` (perm `encounters.create`, idempotent) يقلب arrived→in_consultation؛ الـFE consultation ينده عليه عند الـmount. مسار الدكتور الحالي (create-encounter) default `beginConsult=true` = byte-identical.
- **Readiness:** `has_vitals`=`EXISTS(clinical_examinations WHERE encounter_id=appts.encounter_id)` · `has_sheet`=`EXISTS(patient_history_answers JOIN history_questions scope=encounter WHERE encounter_id=appts.encounter_id)` عبر selectSub/withExists (استعلام واحد). null encounter_id → false. + expose `encounter_id`.
- **c. visit_stations:** seeder `clinic_operations`، select `merged|split`، default **merged**. merged=كله على شاشة الكشف، routes المحطات مخفية من nav. split=routes المحطات تظهر لأصحاب الصلاحية. **يحكم ظهور nav فقط** — الأمان يفضل perms (`appCan`)، مايتعملش double-gate. شاشة الكشف تحتفظ باللوحات في الوضعين.
- **d. Sheet station:** يسيب الاتنين (شيت الزيارة + profile history) بلا refactor، بلا mode input.
- **e. no_show from arrived:** سطر في `TransitionAppointmentStatus::ALLOWED`: `'arrived'=>['in_consultation','cancelled','no_show']`. (timeout auto-no-show مؤجّل.)
- **f. waited:** `arrived_at` (مش updated_at) + `waited_minutes` (للـwaiting=للآن؛ للـin_room=arrived_at→encounter.started_at). FE ticking. sort: `urgent desc → queue_position(WP8، مؤقتًا queue_no) → id`.

## مؤجّلات م5 (Fable)
- تحويل مصدر الطابور Appointment→Encounter = **مُلغى نهائيًا** (Appointment صح مع auto-close+on-demand)؛ حدّث تعليق `QueueController`.
- kiosk/anonymous auth للشاشة = مؤجّل. timeout auto-no-show = مؤجّل. drag gesture = قابل للتحويل لأزرار.
