# D3 — first-visit intake: patient_intakes, shared + specialty wizard, «ملف ناقص» badge, periodic confirmation, separate stations

Read `_COMMON.md` first. Spec: `../design-data-layers.md` §5 (whole section — the flow 1–8 is the contract), §3.1 rows `fill_role` / `in_intake` / `storage`, §4 «الإصلاحات الستة» item (6), §7 step 10, §8 D3 row; `../design-visit-shell.md` §2 (`intakePack`/`intakeRefs`), §6, §8 («الاستقبال الأول»); master-plan D3 row. Migration range `2026_11_03_210000`–`210099`.

## Depends on (check before starting — if one is missing STOP and report)
- **D2** — `patient_alerts` (incl. an explicit «no known allergies» form), structured chronic conditions, home meds (`prescriptions.kind=home`). Read D2's brief in `tasks/` and its commits in `../LEDGER.md`; use exactly the tables/columns D2 shipped.
- **E1e** — answers store (`repeat_index`, `value_json`, `source`, private fields enforced on read+save). **S1** — the shell (`features/clinic/visit-shell/`) with its patient header, where the intake badge lives.
- Columns already in place since D0 (`2026_11_03_100004`): `history_questions.clinical_type`, `fill_role` (reception|nurse|assistant|doctor|patient, default doctor), `in_intake` (bool), `storage`, plus E1 `show_if`, `is_private`, `module_key`. Setting `clinic.visit_stations` = `merged|split` exists (`ClinicSettingDefinitionSeeder` ~line 503) and today has no effect.

## Code reality
- There is **no `ClinicalTypeRegistry` class** (the docs name one). The type list is `Modules/Clinic/app/Enums/ClinicalType.php` (general, nutrition, dental, gyn_ob) with `profilePackCode()` / `fieldScope()`. You add the intake declarations in a new class (Do §3), not in the enum.
- Writes already exist and stay the only write paths: patient-scope answers `PUT clinic/patients/{p}/profile-fields` (`PatientProfileFieldController::update` → `HistoryService::savePatientAnswers`, accepts `?clinical_type=`), encounter answers `PUT encounters/{e}/history` (`EncounterHistoryController::upsert` → `HistoryService::saveAnswers`), plus D2's alert/condition/home-med endpoints. **HistoryService is a hot file — do not edit it.**
- Queue badges: `QueueController::baseQuery()` builds `has_vitals` / `has_sheet` as correlated sub-selects; exposed by `QueueEntryResource` (lines ~71-72) and mirrored in `ClinicDashboardService::queue()` (~126/166). D1 rewrote `has_vitals` there — read the current file and add only your block. FE chips: `features/clinic/shared/visit-queue/visit-queue.component.html`.
- Registration: `features/clinic/shared/patient-form/clinic-patient-form.component.ts` (+ `config/profile-form-registry.ts`). Stations: `features/clinic/stations/{station-shell,vitals-station,sheet-station}.component.ts`.

## Decisions already made (do not re-litigate)
- `patient_intakes` stores **state only** (status, confirmation, skip, attestations); completeness is **derived** from definitions + stored data and recomputed on write. Unique per (company, patient, layer); layer `''` = shared (L1), `<clinical_type>` = specialty (L2). Use a NOT NULL `clinical_type_key` string (`''` for shared) — MySQL treats NULLs as distinct in a unique index (same trick as `lab_patients.ext_scope_key`).
- Required set of a layer = definitions with `in_intake=true AND is_required=true AND is_active=true`, `clinical_type IS NULL` (shared) or `= X` (specialty), visible for this client (`clinic.patient_form_fields` rule + `is_active`) and whose `show_if` passes for this patient (use the E1 `ShowIfEvaluator` with the patient context) — **plus** the shared minimum: allergies, chronic conditions, current medications — each satisfied by ≥1 row in D2's store OR an explicit «لا يوجد» attestation.
- «لا يوجد» attestation: use D2's mechanism if it has one (e.g. an `nka` alert); otherwise `patient_intakes.attestations` json `{no_known_allergies, no_chronic_conditions, no_home_meds}` with `at/by`, auto-cleared when a real row is later added.
- Status: `pending` (nothing) → `partial` → `complete` (all required present; `completed_at/by` = the write that completed it) ; `skipped` (reason, perm `clinic.intake.skip`). Shared layer only: `needs_reconfirm` when `confirmed_at` (or `completed_at`) is older than `clinic.intake.reconfirm_days` (default 365). Specialty layer: one-time unless the type declares `reintake_days`.
- The specialty step triggers on the patient's first encounter in a department whose `clinical_type = X` while layer X is not complete. Shared fields already answered are never asked again.
- **Stations (enforced in BE, only when `clinic.visit_stations=split`):** a non-doctor user may write a field only if they hold the permission of its `fill_role`: reception → `clinic.patients.update`, nurse → `clinic.vitals.record`, assistant → `clinic.history.record`, patient → never from staff screens. A user linked to a `LabDoctor` (`lab_doctors.user_id`) may write every field (the doctor reviews and edits). Refusal = 422 listing the refused codes (`clinic::intake.fill_role_refused`), nothing saved from that request. `merged` = today's behaviour exactly.

## Do (BE)
1. Migration `2026_11_03_210000_create_patient_intakes_table.php`: `id`, `company_id`, `patient_id` (idx), `clinical_type_key` string(24) default `''`, `status` string(10) default `pending`, `missing` json nullable (snapshot of missing codes/groups), `attestations` json nullable, `completed_at`/`completed_by`, `confirmed_at`/`confirmed_by`, `skipped_at`/`skipped_by`, `skip_reason` string(255) nullable, timestamps. Unique `(company_id, patient_id, clinical_type_key)` (`patient_intakes_co_pt_layer_uq`). Model `PatientIntake` (BaseModel is fine — edits logged; soft-deletes unused) with `saving()` keeping `clinical_type_key` consistent and a `layer()` helper.
2. Backfill `2026_11_03_210001_backfill_patient_intakes.php` (idempotent, chunked 500): for each internal patient that has ≥1 encounter, compute the shared layer with `IntakeCompleteness` (§4) and upsert the row (`complete` with `completed_at` = latest relevant answer/alert time, else `partial`/`pending`); specialty layers only for types the patient has encounters in. Patients without any encounter get no row (they'll be computed on first touch). Must run on MySQL in reasonable time — no per-patient N+1 beyond a few queries; report the measured count on dev if the orchestrator gives you numbers.
3. `Modules/Clinic/app/Clinical/IntakeRegistry.php` (new): per ClinicalType value → `{required_codes: [], optional_codes: [], reintake_days: ?int}` overrides on top of the definition-driven set (nutrition: lifestyle + prefs codes `nut_*` that are `in_intake`; gyn_ob: cycle / G-P-A / spouse codes once G1 pack exists — read `ClinicProfilePacks` and only reference codes that exist; dental: dental alert chips). Default `[]` = definitions only.
4. `Modules/Clinic/app/Services/Intake/IntakeCompleteness.php`: `evaluate(Patient $p, ?string $clinicalType): IntakeResult {status, missing: [{code, label_ar, label_en, fill_role, group}], required_count, answered_count}`; groups `profile` (fill_role reception), `medical` (nurse/assistant), `specialty`. A named extension point `storageChecks()` for the D2 minimum (allergies / conditions / home meds) so later stores plug in. `IntakeService`: `statusFor(patientId, ?type)` (evaluate + persist + return), `confirm`, `skip`, `attest`, `forQueue(array $patientIds, ?type)` (bulk, no N+1).
5. Recompute triggers without touching HistoryService: model observers (registered in the Clinic service provider — one `observe()` line each; report if you can't edit the provider) on `PatientHistoryAnswer` saved/deleted (patient-scope or `in_intake` questions only) and on D2's alert/condition/home-Rx models → `IntakeService::statusFor()` after commit (`DB::afterCommit`), debounced per request (static array of touched patient ids, flushed once).
6. Station enforcement: `Modules/Clinic/app/Support/Intake/FillRoleGuard.php` (`assertCanWrite(User, Collection $questions)`), called from `UpsertPatientProfileFieldsRequest` and `UpsertEncounterHistoryRequest` `after()` validation hooks (you own these two request files; if `../LEDGER.md` shows an open 🔄 WP touching them, STOP and report). Load questions by the submitted ids/codes in ONE query.
7. Endpoints (new `PatientIntakeController`; report these lines for `Modules/Clinic/routes/api.php`):
   - `GET patients/{patient}/intake?clinical_type=` → `{shared: IntakeResult+{confirmed_at, needs_reconfirm, attestations}, specialty: IntakeResult|null, steps: [{key: 'profile'|'medical'|'specialty', fill_role, fields: [definition rows ready for the dynamic field renderer, already filtered by visibility + show_if + private permission], values}]}` — perm `clinic.patients.view`.
   - `POST patients/{patient}/intake/confirm {clinical_type?}` — `clinic.history.record|clinic.patients.update`.
   - `POST patients/{patient}/intake/attest {no_known_allergies?, no_chronic_conditions?, no_home_meds?}` — `clinic.history.record`.
   - `POST patients/{patient}/intake/skip {clinical_type?, reason}` — `clinic.intake.skip`.
8. Queue badge: in `QueueController::baseQuery()` add one sub-select `intake_status` from `patient_intakes` for (appointment.patient_id, layer `''`) and one for the appointment department's `clinical_type` (join departments); in `QueueEntryResource` expose `intake: 'ok' | 'missing_shared' | 'missing_specialty' | 'reconfirm'` (missing row = `missing_shared` only when the patient has no prior done appointment — new patient; otherwise `ok` to avoid flooding old patients). Mirror in `ClinicDashboardService::queue()`. Keep `has_vitals`/`has_sheet` untouched.
9. Encounter payload hint: if cheap, add `intake` (same enum) to the encounter header payload the shell reads (`PatientController@header` or `EncounterResource` — read which one S1's header uses; edit only that one, a few lines).
10. Settings (report definitions — orchestrator owns `ClinicSettingDefinitionSeeder`): `clinic.intake.enabled` (bool, default true), `clinic.intake.reconfirm_days` (int, 365), `clinic.intake.block_sign_when_incomplete` (bool, default false — when true the doctor's sign shows a soft warning only; never a hard block in D3). Permission (report): `clinic.intake.skip` (preset: doctor, nurse).
11. Lang `Modules/Clinic/lang/{ar,en}/intake.php`.

## Do (FE)
- New `src/app/features/clinic/intake/`: `intake-wizard.component` (dialog on desktop, full-screen on mobile; `[patientId] [clinicalType] [step?] [(visible)]`): stepper «١ بيانات التسجيل» (fill_role reception) → «٢ التاريخ الطبي» (nurse/assistant: alerts/allergies, chronic chips, surgeries, family, home meds, smoking — host D2's existing components/cards for those stores, don't rebuild them; «لا يوجد» buttons call `attest`) → «٣ خطوة التخصص» (only when `specialty` is present). Fields render with the existing dynamic field renderer and save through the existing endpoints (autosave per field, debounce, like history-panel); required marked `*`; fields the user's station can't write are read-only with a lock hint when stations=split. Footer: progress «٧ من ٩», «تخطّي» (with reason, when permitted), «تم».
- `intake-badge.component` (`[status]`): chip «ملف ناقص» (amber) / «ناقص: التخصص» / «أكّد البيانات» (blue) / nothing when ok; click opens the wizard on the right step.
- Entry points (small targeted edits only): (a) `clinic-patient-form` — after a successful create, if `clinic.intake.enabled`, open the wizard on step 1 (reception); (b) `stations/sheet-station` (and the merged consultation screen's history area via the badge) — open step 2; (c) visit-shell patient header — place `<app-intake-badge>`; if S1's header component is still being edited by another agent, report the one-line hookup instead; (d) patient file header; (e) `shared/visit-queue/visit-queue.component.html` — the badge next to the `has_sheet` chip. Reconfirm banner in the patient header: «آخر تأكيد للبيانات من ١٤ شهر — تأكيد / تعديل».
- Service `features/clinic/services/clinic-intake.service.ts`. i18n under `CLINIC_INTAKE.*` (ar + en) via small targeted edits; do NOT bump I18N_VERSION. `ng build --base-href /app/` green.
- **Fable consult (mandatory, ONE focused consult):** give a Fable agent (`model: "fable"`) screenshots of the registration form, the sheet station and the visit-shell header (prototype `../visit-shell-prototype.html`) and ask once: the wizard's stepper/layout (incl. read-only station fields and «لا يوجد» buttons), the queue badge states and the reconfirm banner. Apply; don't iterate.

## Tests
`Modules/Clinic/tests/Feature/D3IntakeTest.php` (prefix `d3_`): new patient → pending; answering all shared required → complete with completed_by; a hidden (client `visible:false`) or show_if-false required field doesn't count; private field counted only by presence (value never returned to a user without `clinic.fields.private`); attestation «no allergies» satisfies the minimum and is cleared when an allergy is added; specialty layer appears only for a nutrition encounter and only asks nutrition fields; reconfirm after N days; skip needs permission; queue returns `intake` per row without N+1 (assert query count bound); stations=split: nurse writing a doctor field → 422 with codes, nurse writing a nurse field ok, doctor writes all, merged → no enforcement; backfill marks a fully-answered patient complete. Run also `Af2IsolationTest.php`, any `*PatientProfileField*Test.php`, `*EncounterHistory*Test.php`, `*Queue*Test.php`, `*Dashboard*Test.php`, D1/D2 tests in Modules/Clinic/tests — keep green. List every new column your queries read for the orchestrator's MySQL check (SQLite hides missing columns).

## Files you must NOT edit (report instead)
`Modules/Clinic/routes/api.php`, `RolePermissionSeeder.php`, `ClinicSettingDefinitionSeeder.php`, `HistoryService.php`, `ClinicProfilePacks.php` / `ClinicProfilePackService.php`, D2's models/services (call them), `visit-shell/card-catalog.ts`, the consultation component (badge only via the shared components it already hosts — report if a direct edit is needed), CHANGELOG (give 2 lines EN + AR).

## Out of scope
Patient-portal intake (`fill_role=patient`, later), triage station (D4), new definitions/packs content (G1 owns the gyn_ob intake codes), hard-blocking signing.

## Report
Files, test output, route lines, setting definitions, permission, observer/provider lines, hookups you could not make, anything not done.
