# S1b — exam findings + complaint catalog links: a real server store for the visit shell

Read `_COMMON.md` first. Migration range `2026_11_03_280000`–`280099`. Owner directive: multi-select = normalized link rows (never CSV). Orchestrator-owned: routes files, RolePermissionSeeder (report lines instead).

## Why
The visit shell (S1, commit a20910a2) stores exam findings as a browser draft (`localStorage` key `visit-shell:exam:<encId>` = `{picks:[{id, lookupId, label, cat: regionId}], text:{[regionId]: string}, note}`) because no endpoint exists, and links complaints picked from the lookup catalog to `clinic_complaints` by Arabic name. Design: `../design-visit-shell.md` §2.6 (exam = one catalog `exam.finding` with group = region, `encounter_findings` link rows) and §2.6 guards.

## Do (BE, Modules/Clinic)
1. Migration: `encounter_findings` (id, company_id, encounter_id, region_key string(48) — the parent clinical_lookups row's code/id of `exam.finding`, lookup_id nullable (clinical_lookups row), text nullable (free text when the region's input=text or as a note), sort_order, created_by, timestamps, softDeletes; idx (encounter_id), (company_id)). Plus `encounters.exam_note` text nullable if no suitable column exists (check `encounters` + `clinical_examinations` first — reuse an existing exam-note column if there is one).
2. `encounter_complaints` link: read how complaints are linked to encounters today (`Encounter::complaints()` belongsToMany). Add a nullable `lookup_id` to that pivot (or the complaints table — choose the place that keeps existing history/prints working) so a complaint picked from the catalog keeps its catalog id; keep the name-based fallback.
3. `EncounterFindingController`: `GET /clinic/encounters/{e}/findings` → `{findings:[{id, region_key, lookup_id, label_ar, label_en, text}], note}`; `PUT /clinic/encounters/{e}/findings` replaces the set atomically (`{findings:[{region_key, lookup_id?, text?}], note?}`) — `Encounter::assertEditable()` first; validate lookup_ids belong to the company and to `lookup_key='exam.finding'`. Permissions: view = clinic.encounters.view, write = clinic.encounters.update. Report the two route lines.
4. Include findings in the encounter summary readers that print/show the exam if any exist (grep for where clinical_examinations notes are printed — keep it minimal; report what you changed).

## Do (FE) — `src/app/features/clinic/visit-shell/cards/visit-exam-card.*` and `visit-shell/visit-data.service.ts` only (+ i18n targeted edits, no I18N_VERSION bump)
- Load/save findings through the new endpoints (debounced autosave, like the other cards); on first load, if a localStorage draft exists and the server set is empty, push the draft once and clear it; drop the «مسودة على الجهاز» tag when saved.
- Complaint card: when picking from the catalog, send the lookup id too (if the BE pivot carries it).

## Tests
`Modules/Clinic/tests/Feature/S1bEncounterFindingsTest.php` (helpers `s1b_`): PUT replaces the set; signed encounter 422; foreign/other-key lookup_id 422; GET returns labels; complaint link keeps lookup_id. `ng build --base-href /app/` green.

Out of scope: history/vitals/diagnosis, D2 notes (clinical_notes is D2's), other cards.
