# W2-BE-C — report (layout settings API · facility type · complaint duration/HPI · diagnosis text mode)

Status: DONE (BE). Not committed. Migration run on dev. Tests written and run (results below).

## Files

New
- `Modules/Clinic/app/Support/VisitLayoutRegistry.php` — the allowed keys per clinical type (fields + their supported attributes, elements, features); published in GET as `registry`.
- `Modules/Clinic/app/Services/VisitLayoutService.php` — reads/normalises both levels, sanitises PUTs against the registry, resolves `effective` (per-key merge, locks, sources), dental write-through.
- `Modules/Clinic/app/Support/FacilityType.php` — presets, `Setting::saved` hook, after-commit reconcile, `describe()`.
- `Modules/Clinic/database/migrations/2026_11_20_500001_add_hpi_and_complaint_duration_to_encounters.php`
- `Modules/Clinic/tests/Feature/W2becVisitLayoutSettingsTest.php` (13 tests, helpers `w2bec_`)

Changed (mine)
- `Modules/Clinic/app/Http/Controllers/VisitLayoutController.php` — rewritten (thin; uses the service). New action `updateFacilityType`.
- `Modules/Clinic/database/seeders/ClinicSettingDefinitionSeeder.php` — `clinic.facility_type` (enum clinic|center|hospital|custom, default `custom`, group clinic_modes, visible, display_order 0).
- `Modules/Clinic/app/Http/Requests/UpdateEncounterRequest.php` — `complaint_duration` (≤60), `hpi` (≤10000).
- `Modules/Clinic/lang/{ar,en}/errors.php` — 5 keys `visit_layout_*`.
- `Modules/Clinic/tests/Feature/S1VisitLayoutApiTest.php` — one test now uses registry-valid keys (`occupation` seeded as a history question, `exam_heart` region, `shell`), because unknown keys now 422.

Shared files touched minimally (other agents have uncommitted edits there — kept)
- `Modules/Clinic/routes/api.php` — one route: `PUT clinic/visit-layout/facility-type` (`permission:clinic.layout.manage`; controller also requires `clinic.settings.manage`).
- `Modules/Clinic/app/Providers/ClinicServiceProvider.php` — one `Setting::saved` hook line (next to the ClinicRevenueSettings one).
- `Modules/Clinic/app/Models/Encounter.php` — `complaint_duration`, `hpi` fillable (W1-BE-A's `shell_state`/`signer` kept).
- `Modules/Clinic/app/Http/Resources/EncounterResource.php` — `complaint_duration`, `hpi` after `chief_complaint` (W1-BE-A's signer kept).
- `Modules/Clinic/app/Http/Controllers/EncounterController.php` — `update()`: the null-filter now lets `complaint_duration` / `hpi` be cleared with null (one line). W1-BE-A's `show()` edit kept.
- `Modules/Clinic/tests/Feature/ClinicSettingsBackboneTest.php` — the clinic_modes definition count 10 → 11 (the new definition).
- DiagnosisController / DiagnosisService: NOT touched.

## Contract implemented (for W3-FE-4)

`GET clinic/visit-layout?clinical_type=` returns (old keys unchanged + new):
```
clinical_type, visit_shell_enabled, allow_doctor_override, can_manage,
can_manage_settings,                      // clinic.settings.manage (the «عام للعيادة» block)
facility_type,                            // stored value, default 'custom'
facility: { type, current: {clinic.ordering_mode, clinic.lab_mode, clinic.rad_mode, clinic.pharmacy_source},
            presets: { clinic|center|hospital|custom: {label_ar, label_en, desc_ar, desc_en, values: {4 keys}|null} } },
client:  { layout, fields: {key: {visible?, required?, input?, locked?}}, elements: {key: {on, locked?}}, features: {feature: bool} },
doctor:  { layout, fields: {key: {input}}, elements: {key: bool} },        // empty when override is off
effective: { fields:   {key: {...rule, locked: bool, source: 'client'|'doctor'}},   // only keys with a stored rule
             elements: {key: {on, locked, source: 'default'|'client'|'doctor'}},   // every registry element
             features: {feature: {on, source: 'default'|'client'}} },             // every feature of the type
registry: { fields: {static key: {attrs, inputs}}, dynamic_fields: {vital, exam_region{keys}, history{keys}},
            elements: [...], features: [...], notes: {diagnosis_text_not_for_insured: true} }
```
- `PUT clinic/visit-layout/client` — `layout`, `fields`, `elements`, `features` each optional (omitted = kept), `null` = reset that section. Each section present REPLACES that section (send the whole draft). Unknown key / unsupported attribute / bad value → 422 with `errors["fields.<key>"]`, `errors["fields.<key>.<attr>"]`, `errors["elements.<key>"]`, `errors["features.<key>"]`; nothing is written on a 422. Element value: `bool` (shorthand) or `{on, locked?}`; stored normalised `{on, locked?}`.
- `PUT clinic/visit-layout/doctor` — `layout`, `fields`, `elements` (403 when override is off, as before). Only `input` is personal: `visible/required/locked` in a doctor field rule and a `features` section are ignored. A key the client `locked`: same value as the client's → no-op; different → 422 `fields.<key>` / `elements.<key>` (`visit_layout_rule_locked`). `effective` ignores a doctor value for a key the client locked later. All three sections null → the doctor's entry for the type is removed («رجّع افتراضيي»).
- `PUT clinic/visit-layout/facility-type` `{facility_type, clinical_type?}` → returns the payload for `clinical_type` (default general).

Keys (registry):
- Fields: `chief_complaint` (required, input), `complaint_duration` (visible, required), `diagnosis` (required, input = select|text only), `followup_date` (required); `vital.<code>` (visible, required, pattern-checked only); exam region = the region's `clinical_lookups.code` itself (e.g. `exam_chest`; visible, required, input); any company `history_questions.code` of the type or core (visible, required).
- Elements (spec §2.3 dotted keys): `hdr.insurance hdr.lastVisit hdr.doctor complaint.quick complaint.hpi complaint.note exam.normal exam.notes dx.secondary lab.prev rad.prev rx.interact`. Stored per clinical type (hdr too).
- Features: gyn_ob → `fertility obstetrics urogyn sexual oncology operations` + `shell`; dental → `surfaces plan_signature perio lab imaging recall note_templates ortho` + `shell`; general/nutrition → `shell`. `gyn.feature.x` / `dental.feature.x` accepted and normalised to `x`. Shell default: general/gyn_ob true, nutrition/dental false (matches the consultation redirect).
- Dental features are written through to the `dental.feature.*` settings (one source of truth; only `shell` is stored in `clinic.visit_layouts.dental.features`); GET reads their real values back, so `/clinic/settings → الأسنان` and the sheet show the same thing. `features: null` on dental puts them back to their definition defaults.
- Gyn features stay in `clinic.visit_layouts.gyn_ob.features`, the store `GynFeatures::for()` reads (checked in a test); other entry keys (`discharge_presets`) survive saves. The consultation redirect still reads `client.features.shell` (unchanged shape).
- Legacy reads: the old field rules `complaint_quick` / `chief_complaint_note` are read back as elements `complaint.quick` / `complaint.note` (and dropped from `fields`); the old doctor shapes `{type: {layout}}` and `{type: <bare layout>}` still read.

## Facility type (Decision 3)
- Presets (real enum values): clinic → immediate · standalone · standalone · standalone_formulary; center = hospital → intent · integrated · integrated · products; custom → leaves the four.
- One code path for both doors: a `Setting::saved` hook. Saving the type applies its preset; changing one of the four so it no longer matches the current type's preset flips the type to `custom`. The decision runs after commit, once per company, from raw rows — so the `/clinic/settings` bulk save (which sends the new type together with the four OLD values) ends on the new type's preset, and a bulk save that changes nothing doesn't trigger an extra cache flush (kept `ClinicSettingsBackboneTest` #13 green).
- Default `custom`, so existing installs keep their four values until someone picks a type.
- Dev: seeder run; company 1 set to **hospital** (its four were the defaults = center/hospital preset; dev has 9 hospital admissions). The hook wrote the four rows explicitly (same values).

## Complaint duration + HPI (B #56)
- Columns `encounters.complaint_duration` varchar(60) null, `encounters.hpi` text null (migration run on dev: DONE). Edited through the endpoint the complaint card already uses: `PATCH clinic/encounters/{id}` (`{complaint_duration?, hpi?}`), `assertEditable()` = the sign lock (422 once signed). null clears them. Returned in EncounterResource.

## Diagnosis text mode (C #22)
- No BE change needed: `POST encounters/{e}/diagnoses` already accepts `{description}` with no `code` (cash visit → code null). On an insured visit the endpoint already refuses a code-less row (422, `DiagnosisController::store`), and `blockSignForInsured` still needs a coded primary. Documented in VisitLayoutRegistry. So the FE must force ICD on insured visits (it gets a 422 otherwise).

## Tests (run with the ea-php82 CLI, only these files)
- `Modules/Clinic/tests/Feature/W2becVisitLayoutSettingsTest.php` — 13 passed (148 assertions): registry field keys (static/vital/exam code + both aliases/history; unknown, gyn-only-on-general, unsupported attr, bad input → 422, nothing written); elements/features validation + normalisation; section independence + null reset; legacy complaint rules → elements; doctor per-key merge, locks (422 / same-value no-op / late lock wins / override off); old doctor shapes + all-null removal; gyn features through GynFeatures + discharge_presets kept; dental write-through both ways + reset; facility preset + flip to custom + equal-value no flip; bulk settings save (type wins; one changed mode → custom); facility door permissions/validation; HPI/duration patch/clear/max/signed lock; diagnosis text mode cash vs insured.
- `S1VisitLayoutApiTest.php` — 6 passed (after the key update).
- `ClinicSettingsBackboneTest.php` — 17 passed (after the count update).
- `PermPlatformRoutesTest.php --filter="visit layout"` — 1 passed.
- `D3IntakeTest.php` + `Modules/Gyn/tests/Feature/G1aGynFoundationTest.php` — 28 passed, **1 failed, not mine**: G1a «the clinic summary…» expects section order `profile, obhx, menstrual`, gets `profile, alerts, obhx` — an `alerts` section now appears in the summary. Traced to W1-BE-A's uncommitted `PatientFactsReader::alerts()` change (#4, derived Rh-negative info alert); this WP doesn't touch the summary. Reported, not fixed — route to W1-BE-A.

## What I could not verify / deviations / decisions
- `effective.fields` lists only keys that have a stored rule (client or doctor); a missing key = `source: 'default'` with the FE's default rule. `effective.elements` / `effective.features` list every registry key.
- The client PUT runs in one DB transaction (dental write-through settings + the layout row land together).
- FacilityType keeps a per-process pending map; a rolled-back transaction clears it (afterRollBack), one afterCommit callback per save (later ones are no-ops).
- Exam field key: spec §1.3/§2.3 governs → the key is the region code itself (`exam_chest`); the brief's `exam_finding_<code>` and the current FE's `exam_finding_<id>` are accepted and normalised to the code. FE must re-key the exam card by code.
- Element keys: spec dotted keys (`hdr.insurance`, `hdr.lastVisit`…) are canonical; the brief's underscore spellings are accepted as aliases. `complaint_duration` is a field, not an element.
- Strictness: an attribute a key doesn't support (e.g. `chief_complaint.visible`, `diagnosis.visible`, `followup_date.visible`) is a 422, not ignored (spec §2.8 "never silently drop"). The FE popover must send only the attributes in the §2.3 table / `registry`.
- `vital.<code>` keys are pattern-checked only (W2-FS-3 is still changing the packs); codes are not checked against the observation definitions.
- center and hospital presets are identical (as the brief's table says) — the type differs in name only for now. `his.mode` (single_clinic|center, scheduling) is related but deliberately not part of the preset — owner/orchestrator may want them linked.
- The four mode keys and `clinic.facility_type` sit in the same `clinic_modes` group on `/clinic/settings`; the existing settings screen does not re-fetch after a save, so after picking a type there the four rows show the new values only after reload (FE concern).
- Dental write-through requires the Dental module's classes (guarded with `class_exists`); writes go through `SettingsService::set` (permission = the layout PUT's `clinic.layout.manage`, not `dental.*`).
- CHANGELOG `[Unreleased]` bullet not added (left to the orchestrator's commit, per _COMMON). Suggested: «شاشة الزيارة: إعدادات الحقول والعناصر ومجموعات التخصص ونوع المنشأة من جوه الشاشة؛ مدة الشكوى وHPI على الزيارة / Visit screen: field rules, elements, specialty groups and facility type set in place; complaint duration + HPI on the visit».
- Not checked live over HTTPS with a real token; checked on dev via an in-process request (GET dental → 200, dental features read back, facility `hospital`, 8 exam region codes listed).
