# W5-BE-E report — BE follow-ups (hidden labels · restore clash · mirror freshness · settings refresh)

Status: DONE. Not committed. No migrations. Tests written and run (results below). FE build green.

## 1. Hidden values render for everyone (W2-FE-2 gap)
Choice: **(a) `include_inactive=1` for every reader** — no new endpoint. It matches the existing list call, and the FE already caches per key+specialty.
- BE `LookupCatalogController::index`: `include_inactive` is no longer manager-gated. `with_usage` stays manager-only. Without the flag the list is still active-only for everyone. Company scoping is unchanged. Docblocks updated (controller + `LookupCatalogService::list`).
- FE `shared/components/clinical-picker/clinical-lookup.service.ts`:
  - `list()` now sends `include_inactive=1`. The pick-list cache (`rows`, so `list()` / `snapshot()`) stays active-only.
  - A second cache `all` (hidden rows included) is kept in step by `patchAll` / `upsertAll`. A hide keeps the row there, which is the point.
  - New `resolve(lookupKey, code, specialty?)` finds a stored code among active and hidden rows: the key+specialty list first, then any loaded list of the key. It reads `changed`, so computeds follow renames and hides.
- **Call sites switched to `resolve()` (shared files outside the brief's "service only" allowance; each is a 1–3 line edit; none was modified by another agent per `git status`):**
  - `features/clinic/encounter/history-panel/history-lookup-picker.component.ts` (picks label)
  - `features/gyn/form/gyn-lookup-field.component.ts` (picks label)
  - `features/gyn/form/gyn-display.ts` (`snapshot().find() ?? resolve()`)
  - `features/gyn/state/gyn-visit.store.ts` (`labelEn`, same fallback)
  - `features/clinic/clinical/team-caseload.component.ts` and `team-referral-card.component.ts` (`teamLabel`: a hidden team on old referrals)
- In those components a hidden value keeps `lookupId: -1` and still renders as the dashed `.chip.off`, now with its real label. `clinical-picker-box.isOff()` is unchanged and still correct, because the box's rows stay active-only.
- Soft-deleted rows are not returned. That is deliberate: a delete is refused while a value is used, so no answer can point at a trashed row.

## 2. Restore label clash (W1-BE-B)
- `LookupCatalogService::restore()` calls the new private `assertNoSiblingClash()` before it un-deletes or re-activates a row (only when the state actually changes). The check is: another live, active row of the same company + key + parent with the same `value_ar` → **422 `errors.value_ar`**, and nothing changes. `update()` now uses the same helper, so rename/move and restore follow one rule.
- **Decision:** the clash is on `value_ar` only, the same as the existing rename rule. The brief said "value_ar/value_en", but I did not add a second, different rule for `value_en`.
- FE: when the picker's undo gets a 422 it shows the generic «ماقدرناش نحفظ…» toast (`runUndo` → `MANAGE.FAIL`). I did not change this (it is in the dialog component, outside my files). A specific «الاسم موجود بالفعل» on the undo would be a small follow-up.

## 3. Mirror freshness (W3-BE-D)
- New `AnswerValueMirror::deleteAnswers(Builder)` (forget mirror rows + forceDelete). It is the one call for writers outside HistoryService.
- (a) `Modules/Core/app/Console/Commands/Sec3PiiEncryptCommand.php`: after `privateAnswers($mode)` it runs `clinic:history:mirror-values`, in encrypt and decrypt modes but not `--bidx-only`, and only if that command is registered. Encrypt prunes the plaintext codes of now-encrypted answers. Decrypt re-mirrors them.
- (b) Writers outside HistoryService:
  - `Modules/Gyn/app/Services/GynOperationService.php::writeDirect`: **the real gap W3-BE-D missed.** It writes multi_select record answers (procedure mirror and histology). Now `sync()` runs after save, and clearing goes through `deleteAnswers`. The question was loaded with `first(['id','code','input_type'])`, which would have hidden `lookup_key` / `is_private` from `sync()`; it now loads the full row.
  - `Modules/Dental/app/Services/DentalMedicalService.php::saveExam`: clearing an emptied exam answer goes through `deleteAnswers`.
  - `Modules/Gyn/app/Support/GynObstetricHooks.php::resolveOwn` (~:119): clearing the derived VTE score goes through `deleteAnswers`.
  - `Modules/Gyn/app/Services/PregnancyService.php::markPregnant` (~:635): `sync()` runs after save, and the question is now loaded in full (it was `first(['id'])`).
  - Note: the VTE score is numeric and «pregnant» is boolean, so `sync()` does nothing for them today. They are wired as the brief asked.
  - `VitalsService:285` was not touched. It only writes numeric vitals questions, which `AnswerValueMirror::mirrors()` skips.
  - The other `PatientHistoryAnswer::query()` hits from the grep are reads.
  - Also: on MySQL (and on SQLite in tests) the `history_answer_id` FK `cascadeOnDelete` already removes mirror rows on a hard delete. The explicit forget is belt-and-braces. The real fix in (b) is the save-path `sync()` in `writeDirect` (and `markPregnant`).
- (c) `Modules/Clinic/app/Providers/ClinicServiceProvider.php::registerCommandSchedules`: `clinic:history:mirror-values` runs `dailyAt('03:50')->withoutOverlapping()`, after the 03:30 / 03:40 jobs. Confirmed with `schedule:list` on dev: `50 3 * * *`. This is a shared file; the edit is one line.

## 4. Settings screen refresh (W2-BE-C)
- **No change needed. Verified by a test.** The W2-BE-C note ("the settings screen does not re-fetch after a save") is wrong. `clinic-settings.component.ts::save()` already calls `load()` after a successful bulk save (committed June/July).
- On the BE, `PUT /core/settings/bulk` runs: transaction → `Setting::saved` → afterCommit `FacilityType::apply` (its `set()` flushes the cache) → `flushCompanyCache`. The test warms the cache, sends ONLY `clinic.facility_type` (what the screen sends), then reads `GET /core/settings?module=clinic` with no manual flush. It gets the type and all four preset values.
- I did not change the response shape: the FE ignores the bulk response, so extra fields would never be read.
- **Not done (recommendation):** a pre-save preview where the four rows follow the type you picked. If someone builds it, the local type must flip to `custom` when a mode is then edited. Otherwise the BE's "type wins" reconcile would overwrite that edit with the preset.

## Files changed
BE (all `chown moonui3:moonui3`; `optimize:clear` run; no new classes, so no dump-autoload):
- `Modules/Clinic/app/Http/Controllers/LookupCatalogController.php`
- `Modules/Clinic/app/Services/LookupCatalogService.php`
- `Modules/Clinic/app/Services/History/AnswerValueMirror.php`
- `Modules/Clinic/app/Providers/ClinicServiceProvider.php` (one schedule line)
- `Modules/Core/app/Console/Commands/Sec3PiiEncryptCommand.php`
- `Modules/Dental/app/Services/DentalMedicalService.php`
- `Modules/Gyn/app/Services/GynOperationService.php`
- `Modules/Gyn/app/Services/PregnancyService.php`
- `Modules/Gyn/app/Support/GynObstetricHooks.php`
- Tests: new `Modules/Clinic/tests/Feature/W5beeTidyTest.php` (helpers `w5bee_`). `Modules/Clinic/tests/Feature/W1bCatalogManagementTest.php`: the one `include_inactive` test (title + the non-manager now gets 2 rows, the 2nd `is_active:false`). That test asserted the old manager-only behaviour.

FE:
- `src/app/shared/components/clinical-picker/clinical-lookup.service.ts`, plus the 6 call sites listed in §1.
- No i18n keys added.

Pint `--test`: pass on all touched BE files.

## Tests (ea-php82 CLI, only these files)
- `W5beeTidyTest.php`: **8 passed (62 assertions)**:
  - include_inactive for a non-manager; with_usage still manager-only; cross-company empty.
  - restore 422 after delete+re-add and after hide+re-add; no clash under another parent or with a hidden twin; plain undo is 200.
  - Dental clear: the mirror spy sees the answer id, and the rows are gone.
  - Gyn `writeDirect`: save mirrors (`record_type` / `record_id`), update re-syncs, clear forgets.
  - `markPregnant` with a choice question mirrors; VTE `resolveOwn` forgets.
  - `sec3:pii-encrypt` (`--table=patient_history_answers`) drops the private answer's rows and keeps the non-private ones; `--decrypt` brings them back.
  - The schedule exists at `50 3 * * *`.
  - Settings bulk save of facility_type only, then GET, returns the preset values.
  - The delete-path tests use a recording `AnswerValueMirror` subclass bound in the container, because the FK cascade would otherwise hide a missing forget.
- Regression, all passed:
  - `W1bCatalogManagementTest`: 18 passed
  - `LookupCatalogTest`: 6
  - `W3HistoryAnswerValuesTest`: 5
  - `Modules/Core/tests/Feature/Sec3PiiEncryptionTest`: 21
  - `Modules/Gyn/tests/Feature/G4aGynOperationsTest`: 11
  - `G5aGynObstetricsTest`: 10
  - `Modules/Dental/tests/Feature/DentalConsultTest`: 4
  - `C1aPermissionsTest --filter=nightly`: 1
- FE: `npx ng build --base-href /app/` → "Application bundle generation complete", no errors (only the existing warnings). Not deployed.

## Could not verify
- Not checked live in the browser or over HTTPS (dashed chip with its label for a non-manager; team labels).
- The Sec3 hook was tested on SQLite only. On a real install the reconcile runs over all companies after the conversion, which takes time on a big table (it is chunked).

## Suggested CHANGELOG `[Unreleased]` bullet (for the orchestrator)
«القيم الموقوفة في الكتالوج بتظهر باسمها لكل المستخدمين في الإجابات القديمة؛ استرجاع قيمة اسمها اتاخد بيرفض بدل ما يكرر الاسم / Hidden catalog values keep their label on old answers for every user; restoring a value whose label was taken is refused instead of duplicating it.»
