# Fable review — FINAL: verification of the fix pass vs the approved analysis («شبه التحليل؟»)

**Verdict: APPROVE WITH FIXES** — every P0 and all but one P1 from `FABLE-review-FULL.md` are fixed in code and in the deployed bundle; the one residual (P1-4, the specialty-card ⚙ is blank for a layout manager who lacks `clinic.history.manage` — on dev that is the `admin` role) and one new P1 of the same root are fixes inside shapes that already exist. No redesign. The settings layer, the picker, the vitals station, the nutrition board and the inpatient chip/round now match the prototype's design.

Reviewer: Fable (design). Date: 2026-10-24. Review only — no product code edited, nothing written to `moonui3_dev_be` (all DB checks were `SELECT`s through tinker).
Scope of this pass: **code/diff-verified**, not a screenshot pass. Verified: FE `887145ee` (HEAD, tree clean) and BE `674c1e134 · 007409760 · d3070f8df · c5ed456c9` (+ `f008e9e47` for the FIN items) against the current files, the dev DB state, and the deployed `/app` bundle (`index.html` → `main-DEDSDMZY.js`, HTTP 200; the lazy chunks carry the fix strings `etb-req`, `PACK_FROM_MEASURE`, `ROUND_RECORD`, `INSURED_OK`, `closest(".frame")`, `ar-EG-u-nu-latn`). The ✅ rows of FULL §5 (settings layer, picker manage, diagnosis/rad pickers, header alerts-only, stay chip, phone strip…) were confirmed live at FULL on `main-UGLV4HWO.js`; the files behind them were touched since only by the fixes below, so they are carried forward, not re-asserted as freshly seen.

---

## 1. FULL review P0 / P1 — status

| # | Finding (FULL) | Status | Evidence |
|---|---|---|---|
| **P0-1** | `is_insured` never reaches the shell | **✅ fixed** | BE `Modules/Clinic/app/Http/Resources/EncounterResource.php:54` `'is_insured' => app(DiagnosisService::class)->isInsured($this->resource)` — the same resolver as the sign gate (`DiagnosisService.php:140-146`, a `service_orders.encounter_id` row with `payer_contract_id`). Test `FinPrimaryDiagnosisTest.php:122-132` (insured → true, cash → false). FE unchanged and already wired: `visit-shell.component.ts:264` `isInsured`, `:346-352` `cardDefs()` forces `required` on `requiredWhen:'insured'`, `:276` `dxInputMode` = `select` when insured, `:507-511` `insuredOk` → finish panel positive line «مريض مؤمّن — التشخيص المطلوب للمطالبة موجود: {{code}}» (`visit-finish-panel.component.ts:33-36`), `visit-settings.service.ts:222-223` forced row «مفروض: مريض مؤمّن». **Stays included:** `Folio.php:49/96` stamps `encounter_id` on the stay folio (dev: 8/8 admission folios carry the admission's encounter_id), so an insured stay reads `is_insured:true` too. Still unverified live: dev has no insured patient (RESUME «Not verified»). |
| P1-1 | Header ⚙ popover clipped by the app sidebar | ✅ fixed | `visit-card-settings.component.ts:394-413` `place()` clamps to `el.closest('.frame').getBoundingClientRect()` (left+4 … right−4), RTL inline-end aligned to the trigger (`left = r.right − w`), width `max(240, min(380, frame))`, `maxH` from the room below/above; the header ⚙ is inside `.frame` (`visit-shell.component.html:12` frame → `:16-36` `app-visit-header` → `:30` `app-visit-card-settings variant="hdr"`). Phone bottom-sheet rule keeps `!important` overrides (`:226`). |
| P1-2 | Lab compact picker empty, blames the admin; `lis/investigations` 404 | ✅ fixed | BE `ClinicCatalogMode.php:41-47` `labMode()` → `standalone` when `EnforceEnabledModules::enabledFor('lis')` is false (new method `EnforceEnabledModules.php:89-110`); test `W5Fe7LabModeAndPregnancyTest` (encounter resource reports `lab_mode: standalone`). **Live on dev:** `labMode(1)=standalone`, `lis enabled=false`, stored setting still `integrated` (documented in RESUME). FE `lab-orders.component.ts:80-141` and `rad-orders.component.ts:191-243`: per-source `catchError → []`, `catalogFailed` only on a critical source; `catalog-pick-dialog.component.ts:85-92` distinguishes «تعذّر تحميل الكتالوج — جرّب تاني» (`role=alert` + retry) from «الكتالوج فاضي — أضف أول عنصر من تحت» (with `createFn`) from the no-add copy; `CLINICAL_PICKER.EMPTY_CATALOG` exists (`ar.json:24408`). Standalone + `createFn` → inline add now reaches the lab picker. Dev has 1 active `service_type=lab` clinic service, so the owner will see a near-empty-but-addable catalog, which is the correct message. |
| P1-3 | GYN complaints buried under the general list, in English | ✅ fixed | BE `GynComplaintCatalog.php` (20 Arabic labels, category `cc_cat_gyn` «شكاوى النسا والتوليد», create-only, never resurrects a deleted one), migration `2026_11_20_600001` repairs existing installs without touching clinic renames; `LookupCatalogService.php:49-66` own-specialty rows first (after favourites); `GynFieldPackSeeder` seeds `ar` + `parent_id`; `d3070f8df` pack rerun `withTrashed()` (no unique-index crash). **Dev DB:** category id 2611 active, 21 gyn_ob complaint rows, 20/20 under the category, 21/21 Arabic. FE `clinical-picker.model.ts:45-48` `catalogOrder(specialty)` applied to categories and leaves (`clinical-picker-dialog.component.ts:131`, `:586`); the complaint card passes `[specialty]` (`visit-complaint-card.component.ts:58`). Tests `W6bfGynComplaintCatalogTest` ×5 + `ComplaintCatalogTest` 3b (picker label reuses the coded row). |
| P1-4 | Specialty-card fields have no ⚙ | **⚠ partly fixed** — see **NEW-1** | Done: `card-rules.ts:40-46` `gyn_first_visit` (prefixes `gyn_profile_`, `gyn_menstrual_`), `dent_exam` (section `dental_exam`) get `history` rules; `visit-card-settings.component.ts:299-320` `historyRows()` = registry `dynamic_fields.history.keys` ∩ the type's questions, labelled from `VisitHistoryDefsService` (new, `visit-history-defs.service.ts`); consumers wired end-to-end: GYN `withFields` → `gyn-first-visit-card.component.ts:69` `[rules]="fields()"` → `gyn-module-form.component.ts:149/244` (hide / `*`); dental `dental.cards.ts:72` `fields` → `dental-exam.component.ts:39-45,120` (hide unless answered / `*`). Dev has the data (gyn_profile ×5 + gyn_menstrual ×22 on `gyn_ob`, dental_exam ×12 on `dental`). Nutrition `nut_quest`/`nut_lifestyle` = note + in-place sheet link (orchestrator decision, acceptable — their questions are the nutrition intake form). **Residual:** the labels come from `GET clinic/history-questions?scope=all`, which `HistoryQuestionController.php:58-59` gates on **`clinic.history.manage`**; on dev the `admin` role has `clinic.layout.manage` but not `clinic.history.manage` → an admin opening the GYN first-visit / dental exam ⚙ sees only «مقدرناش نجيب أسئلة الكارت ده…» and no rows. |
| P1-5 | Inpatient two entry forms + intake wizard on a stay | ✅ fixed | Intake: `visit-shell.component.ts:1063-1066` auto-open only for `kind` `outpatient`/`remote` (kinds: `clinic-encounter.service.ts:105`). One round form: `vitals-panel.component.ts` `roundMode` input (+ `ROUND_CORE`/`ROUND_PACK` mapping, `roundBody()`, `recordRound()` → `roundSubmit`, round fields start empty, NEWS2 dropped from the typed pack), shell `roundMode` = inpatient ctx + admission + in bed + nurse permission (`:2137-2139`), `vitalsRound` 'shell' | 'dental' (`:2141-2145`), `recordRound()` → `InpatientStayService.recordRound()` → `POST hospital/admissions/{id}/vitals` (`inpatient-stay.service.ts:1276-1283`); `inpatient.cards.ts:1298` `showForm: !c.vitalsRound`; `vital-round.component.html:5-13` table + «＋ جولة جديدة» → `bus.jump('vitals')`; dental stays: `dental-more-card.component.ts:38-40,100-108` hosts the round in «المزيد» and listens to the jump. Finish hard rule reads the latest round for T°/BP/… (`:2171-2178`). Behaviour decision recorded (round not autosaved; hint «فيه جولة متكتبة ولسه متسجّلتش»). |
| P1-6 | «إلزامي» tag absent | ✅ fixed | `visit-card.component.ts:1784-1786` `reqKey()` (insured dx → `FORCED_INSURED`, else `FROM.REQUIRED`); head tag `visit-card.component.html:1696-1700` (both modes; suppressed only when the edit-mode source tag already says it); bare cards (dental jaw) `:1688` `.etb-req` beside the greyed ✕ (`dental.cards.ts:31` `required: true`). |

### FIN W1-BE-A2 P1s (`FABLE-FIN-W1-BE-A2.md`) — current code

| # | Finding | Status | Evidence |
|---|---|---|---|
| FIN P1-1 | Cash folio never re-splits moved lines | ✅ | `RepairAdmissionOrdersCommand.php:212-223` always `splitForOrder($folio, $contract)` with `$contract` null for a cash folio (`folioContract()` :178-186, company-guarded); report line prints stray vs folio payer (`:105-106`). |
| FIN P1-2 | "Stray" could swallow a reception order | ✅ | `:152-157` skip when `appointment_id` / `payer_contract_id` set or created before the encounter («looks like a reception order … left for manual review»); lines move only with `--move-lines` (`:54,161-162,206-207`), facts (lines, subtotal, coverage, patient_due) printed in dry-run. |
| FIN P1-3 | Cancelled folio fell back to a cancelled stray | ✅ | `EncounterServiceOrder.php` `admissionFolioFor()` returns the folio **any status**; legacy fallback adds `status != cancelled` + `orderBy('id')`; `hospitalEnabled()` uses the `Module::has && isEnabled` convention (P2-2 too). |
| FIN P1-4 | Dental `ChargeService::orderFor` bypassed the resolver | ✅ | `Modules/Dental/app/Services/ChargeService.php:77-83` asks `admissionFolioFor()` first. |

### W1 review carry-overs (FULL §4) — now

| W1 item | Now |
|---|---|
| P1-1 «إلزامي» tag | ✅ (above) |
| P1-2 vitals tiles / compact unreachable | ✅ the shell renders `layout="grid"` (`visit-shell.component.html:2061`): prototype `.fld/.inp` 30px boxes, unit inside, read-only mint boxes, danger/abnormal tints, packs as titled rows, «آخر قياس» line + folded «آخر 5 زيارات», 44px on `vs-touch` (`vitals-panel.component.scss:396-626`); classic consultation keeps the tiles. Collapsed head / rail line now from the panel's live `summaryLine()` («84 كجم · BMI 31.7 · 135/85 · نبض 88 · NEWS2 3») via `summaryChange` → `vitalsSummary` (`visit-shell.component.ts:606`). Danger count moved into the head badge (P2-8). |
| P1-3 nutrition pack surface | ⚠ **accepted** — `NUTRITION_DEFAULT_LAYOUT` (`nutrition.cards.ts:105-128`) still has no `vitals` card, so the WHR pack appears only when the card is added; but waist/hip are now **read-only from the measurements table** («من جدول القياسات» + «قياس جديد ↗» → `nut_measure`, `vitals-panel.component.ts:837-850`), so there is one source of truth either way. Optional: add `{ id:'vitals', size:'S', collapsed:true }` to the «الكشف» tab if the dietitian wants WHR on the board. |
| P1-4 lis 404 | ✅ (P1-2 above) |
| P1-5 two forms | ✅ (P1-5 above) |
| P2-2 English dates | ✅ `shared/pipes/clinic-date.pipe.ts` (`ar-EG-u-nu-latn`, company tz, no seconds) used in header / rails / meds / signed bar / amendments / alert strip; `toLocaleDateString` call sites switched to Latin digits. |
| P2-3 LMP tile | ✅ source line + link on its own line (`vitals-panel.component.html:311-315`). |
| P2-4 antenatal on non-OB | ✅ `pack.pregnancy_open` (BE `VitalsPackService.php`), FE `noPregnancy()` → one line «مفيش حمل مفتوح» + «افتح الحمل ↗». |
| P2-7 footer count per tab | ✅ `fullLayout()` count. |
| P2-8 danger pill | ✅ head badge. |

### FULL P2 polish — landed in the same commit (not re-verified visually)
P2-1 two-line rule rows + sticky «تم» · P2-2 note → sheet link (`noteSheet`) · P2-3 bare-card toolbar on its own row · P2-4 «العيادة» wording · P2-5 one duration on GYN · P2-6 «أدوية من الروشتات» / «أدوية من خارج العيادة» · P2-7 p-datepicker + «→ date» text · P2-8 finish scroll under the header · P2-9 «لا شيء مسجّل» on empty collapsed lines · P2-10 icon-only toolbars ≤1180 · P2-11 `stay-strip` on no-rail boards · P2-12 «عام للعيادة» open when no groups · P2-13 manage-row spacing. All present in the diff; none blocks.

---

## 2. Prototype / analysis match — final pass (design)

| Area | Prototype ref | Now | Note |
|---|---|---|---|
| Layout engine: flow stations numbered, board tabs, collapse-to-line, edit mode with two-row purple bar, «＋ إضافة كارت», layer colouring, source tags | 1113-1157, 557-562 | ✅ | confirmed at FULL; fixes only touched tags / toolbar density |
| In-place settings: per-card ⚙ (ظاهر/إلزامي/Select⟷Text/🔒/source/dirty dot), header ⚙, «إعدادات التخصص» sheet (groups, shell switch, allow-doctor, facility radio-cards + inline confirm), scope «للعيادة كلها | لي أنا», level strip, one save | 1245-1265, 526-527, 163 | ✅ (⚠ specialty cards for `admin`, NEW-1) | spec §2 fully built; popover now clamps to the frame, footer sticky |
| Picker: ★, search, category pills, sections, inline «＋ إضافة», «✎ إدارة» (rename/hide/reorder/move/delete-if-unused/undo) | 727-801 | ✅ | GYN category + Arabic rows first on GYN visits; manage rows 28px targets |
| Vitals: 6-col labelled inputs with unit suffix, «آخر قياس» chip, specialty pack row, inpatient «باقة الإقامة — جولة تمريض» row, summary «كجم · BMI · BP · نبض · NEWS2» | 803-811 | ✅ | grid layout = the prototype grammar; prototype's «يملؤها: التمريض أو الطبيب» who-line not rendered (P2, optional) |
| Nutrition board: 14 spec cards, 3-col, classic identical, products sold | 448-461 | ✅ | 15 cards (classic 0px diff per W2-FE-6); WHR pack lives with the measurements |
| Inpatient: chip «🛏 bed · ward · day N/N» + attending, MAR / round / notes / discharge ctx cards, «حساب الإقامة» rail (+ strip on no-rail boards), one round entry, no intake wizard | 1172, 530, 1231 | ✅ | |
| Insured visit: forced dx card, disabled ✕ with «مفروض: مريض مؤمّن», ICD-only input, forced ⚙ row, hard block «لازم قبل التوقيع», positive line «مطلوب للمطالبة … ✓» | 1209, 463 | ✅ (code) | reachable now that `is_insured` is sent; still needs one live insured visit to be seen |
| Header alerts-only, insurance kv, last visit, doctor, «ملف المريض», Rh/pregnancy pills, dates in Arabic | 581, 1171 | ✅ | |
| Finish: hard/soft, per-line «افتح المحطة», sign + print menu | 1209-1210 | ✅ | |

---

## 3. Remaining P0 / P1 — new or residual (with fix instructions)

**No P0.**

### NEW-1 (P1) · Specialty-card ⚙ is empty for the role that configures the screen
**Where:** `moon-erp/src/app/features/clinic/visit-shell/visit-history-defs.service.ts:30-33` calls `GET /api/clinic/history-questions?scope=all&clinical_type=<type>`; `moon-erp-be/Modules/Clinic/app/Http/Controllers/HistoryQuestionController.php:58-59` `abort_unless(can('clinic.history.manage'), 403)` for any scope other than `encounter`. The `catchError → []` then renders «مقدرناش نجيب أسئلة الكارت ده (محتاجة صلاحية إدارة الحقول السريرية…)» (`visit-card-settings.component.ts:1503-1507`) and **no rows**.
**Who hits it:** dev role `admin` = `clinic.layout.manage` + `clinic.lookups.manage` without `clinic.history.manage` (owner / «طبيب» have both). On a client install the layout manager is typically the clinic admin, not the field-registry admin. This is the owner's «مش من شاشات تانية» rule failing for exactly the configuring role, on the two cards (GYN first visit, dental exam) the FULL review asked to fix.
**Fix (prefer A; B is the 2-line fallback):**
- **A (BE + FE, no new endpoint):** `Modules/Clinic/app/Support/VisitLayoutRegistry.php::describe()` already queries `HistoryQuestion` for `dynamic_fields.history.keys` (`historyCodes()`); extend it to also return `dynamic_fields.history.defs = [{code, label_ar, label_en, section}]` (select those four columns in the same query — `clinical_type IS NULL OR = type`, active only). FE `VisitHistoryDefsService.defs(type)` → first return `svc.payload().registry.dynamic_fields.history.defs` when present (no HTTP), keep the HTTP call only as a fallback for an older BE. `historyRows()` then needs no permission beyond the registry read the ⚙ already has. Add a BE test asserting `defs` carries `gyn_profile_*` with Arabic labels for `gyn_ob`, and a FE check (`w3-settings` style) that the GYN first-visit ⚙ lists rows from the registry alone.
- **B (BE only):** `HistoryQuestionController::index` line 59 → `abort_unless($user->can('clinic.history.manage') || $user->can('clinic.layout.manage'), 403)` for `scope=all|patient` **read** (store/update/destroy stay on `history.manage`). Update the popover note to drop the permission clause.
Either way, change `VISIT_SHELL.SET.HISTORY_NONE` to the data-only case («مفيش أسئلة متعرّفة للكارت ده»).

### Carried P1 (accepted by the orchestrator, listed so the owner sees them)
- **W1 P1-3 nutrition pack surface** (⚠ above) — one-line fix if wanted: add `vitals` (size `S`, `collapsed: true`) to the «الكشف» tab in `NUTRITION_DEFAULT_LAYOUT`; the pack is already read-only for waist/hip, so no double entry.
- **Live verification debt** (RESUME «Not verified»): an insured visit end to end, a saved client + doctor layout/rules PUT, the sheet saves (facility type confirm), a recorded round, catalog manage actions, sign + print. Code-verified only; the owner should click through once on `/app`.

---

## 4. P2 noticed in this pass (do not block)
- `EncounterResource::is_insured` adds one `exists()` query per encounter in list endpoints (`EncounterResource::collection`, 25/page) — same class as FIN P2-1; short-circuit when `$this->resource->relationLoaded('serviceOrders')` or compute only on `show`.
- `ClinicDatePipe` is `pure: false` (re-formats every CD cycle); fine at the shell's row counts, but make it pure with an explicit `lang`/`tz` arg if it spreads to lists.
- Dental «المزيد» vitals keep the classic tiles (`dental-more-card.component.ts:40` has no `layout="grid"`); pass `[layout]="'grid'"` for parity with the shell card.
- Vitals grid: the prototype's «يملؤها: التمريض أو الطبيب» who-line is absent — optional.
- A typed, unrecorded round is lost on leaving the visit (hint only) — a `beforeunload`/route-guard prompt when `roundDirty()`.
- Settings screen may display `clinic.lab_mode = integrated` on a LIS-less install while the effective mode is standalone — show the effective value with a note «LIS مش متفعل → مستقل».

---

## 5. Evidence log (commands, read-only)
- `git -C moon-erp show 887145ee --stat` (49 files) · `git -C moon-erp-be show 674c1e134 007409760 d3070f8df c5ed456c9 f008e9e47 --stat`.
- Dev DB (tinker, SELECT only): `ClinicCatalogMode::labMode(1)=standalone`, `enabledFor('lis',1)=false`, `system.enabled_modules` has no `lis`; GYN complaint category 2611 (active), 21 gyn_ob rows / 20 under it / 21 Arabic; `history_questions`: gyn_profile_* ×5 (gyn_ob), gyn_menstrual_* ×22, dental_exam section ×12 (dental); 8/8 admission folios carry the encounter_id; roles: owner/طبيب have layout+history+lookups manage, **admin has layout+lookups only**; `migrate:status` 0 pending.
- `/app/index.html` → `main-DEDSDMZY.js` (HTTP 200); fix strings present in the chunks.
