# Scout A — Clinic Services CRUD surface map (م1 backbone)

Read-only. BE: `/home/moonui/moon-erp-be/Modules/Clinic`. FE: `/home/moonui/public_html/moon-erp/src/app/features/clinic`.

> Cross-checked against existing KB `topics/clinic-service-settings.md` (owner-authored, 2026-07-02,
> already has an F1-F10 gap table with file:line evidence). All claims below independently re-verified
> against the actual code in this session — KB is accurate. Read that file for the deepest narrative;
> this map is the condensed CRUD-focused version for م1 execution.

---

## 1) What EXISTS today

### BE — Services CRUD
- **Controller** `Modules/Clinic/app/Http/Controllers/ClinicServiceController.php`
  - `index` (25-55): paginated (25/page), filters `service_type`, `department_id`, `is_active`, `q` (LIKE on name_ar/name_en/code), `DataScope::apply` for branch visibility, orders by `sort_order,name_ar`.
  - `store` (57-66) → `UpsertClinicService::create`.
  - `show` (68-73).
  - `update` (75-85) → `UpsertClinicService::update`.
  - `destroy` (87-94) → `$service->delete()` — **soft delete** (model uses `SoftDeletes`, migration has `softDeletes()`). No explicit enable/disable endpoint; toggling `is_active` is done via `update`.
  - `assertSameCompany` (96-98) — 404 guard, company-scoped.
- **Routes** `Modules/Clinic/routes/api.php:96-100` — full REST (`GET /services`, `POST /services`, `GET/{service}`, `PUT/{service}`, `DELETE/{service}`), gated `clinic.service.view` (read) / `clinic.service.manage` (write) — only 2 perms, no separate delete perm.
- **Model** `Modules/Clinic/app/Models/ClinicService.php`
  - `$fillable` (24-42): `company_id, code, name_ar, name_en, service_type, department_id, base_price, requires_doctor, default_duration, lis_investigation_id, rad_procedure_id, sbscs_code, product_id, is_active, sort_order, created_by, updated_by`.
  - Casts (44-54): `service_type→ServiceType`, `base_price→decimal:3`, `requires_doctor/is_active→bool`, `sort_order/default_duration→int`.
  - `use SoftDeletes` (20). Relations: `department()` BelongsTo Department (64-67), `product()` BelongsTo Product (72-75), `prices()` HasMany ServicePrice (80-83), `contractPrices()` HasMany ContractServicePrice (88-91), `splitScheme()` HasOne RevenueSplitScheme with axes null (121-128).
  - Implements `FhirServiceItemSource` (`sbscsCode/loincCode(null)/displayName/unitPrice`).
  - **No `doctor_id` column. No `category`/`category_id` column. No `parent_service_id` column.**
- **Migration** `database/migrations/2026_06_22_010001_create_clinic_services_table.php` — columns as above; `unique(company_id,code)`; indexes on `(company_id,service_type,is_active)`, `(company_id,department_id)`, `lis_investigation_id`, `sbscs_code`. `lis_investigation_id`/`rad_procedure_id` are explicitly comment-marked "Soft cross-module references (no DB FK)".
- **Action** `Modules/Clinic/app/Actions/UpsertClinicService.php` — `create()` (19-35) allocates `code` via `SequenceService::generateNext(company_id,'clinic','service')`, fires `ClinicServiceCreated`; `update()` (42-49) fires `ClinicServiceUpdated`. These events wire base-price seeding (`SeedBaseServicePrice`/`SyncBaseServicePrice` — writes `clinic_service_prices` scope=base row, `EventServiceProvider.php:69-74`).
- **Enum** `Modules/Clinic/app/Enums/ServiceType.php` — confirmed 4 values: `Consultation='consultation'`, `ClinicService='clinic_service'`, `Lab='lab'`, `Radiology='radiology'`. `label()` via i18n key `clinic::clinic.service_type.{value}`.
- **Validation** `StoreClinicServiceRequest.php` / `UpdateClinicServiceRequest.php`
  - `name_ar` required, `name_en` nullable, `service_type` required(store)/sometimes(update) enum, `department_id` nullable exists-in-company, `base_price` numeric≥0, `requires_doctor` bool, `default_duration` 1-1440, `lis_investigation_id`/`rad_procedure_id` nullable int, `sbscs_code` ≤40, `product_id` nullable exists-in-company, `is_active` bool, `sort_order` int≥0.
  - **Bridge guard** (`guardBridgeIds`, Store:58-81 / Update:47-71): exactly one of `lis_investigation_id`/`rad_procedure_id`, and only when `service_type` matches (lis→lab, rad→radiology) — else `abort(422,...)`. This guard is pure input-shape validation; it does **not** check the referenced investigation/procedure actually exists (soft ref, no FK, cross-module).
- **Resource** `ClinicServiceResource.php` — flat passthrough of all columns + ISO timestamps. No nested `department` object embedded by default in list (FE resolves department name client-side from a separately-loaded department list — see FE section).
- **Tests** `tests/Feature/ClinicServiceCatalogTest.php` — covers create (code auto-gen, 201), base-price seeding into `clinic_service_prices`. (Only skimmed first 60 lines — CRUD basics are tested, doctor/category assignment is not, because those features don't exist.)

### FE — Services list/CRUD
- **Component** `services/clinic-service-list.component.ts`
  - Full CRUD UI: table (PrimeNG `p-table` via `DataTableComponent`), create/edit dialog (`FormDialogComponent`), delete with `ConfirmationService`.
  - Filters: `service_type`, `department_id`, `is_active`, free-text `q` — **all filters (including `q`) trigger `onFilterChange()→loadAll()` on every event, including on every keystroke in the search input** (`clinic-service-list.component.html:79-84`, `(input)="onFilterChange()"`) — **no debounce**.
  - `loadAll()` calls `svc.listAll(...)` which auto-paginates ALL pages via `forkJoin` (service.ts:77-103) — i.e. **every keystroke re-fetches the entire filtered result set page-by-page**, not a single lightweight query. Confirmed pattern named in the task ("listAll-per-keystroke") is accurate.
  - Department picker (`departments()` signal, loaded once via `svc.departments()` in `ngOnInit`, also an auto-paginated `listAll`-style fetch) IS a real `p-select` with filter (`[filter]="true"`) — a proper picker, not a raw ID.
  - `lis_investigation_id` / `rad_procedure_id` fields (HTML:320-341) are **raw `p-inputNumber` numeric inputs** (min=1, placeholder+hint text) — i.e. the user must know/type the LIS investigation or rad-procedure numeric ID by hand. No picker/search component wired for these — confirms "raw IDs vs pickers" for the bridge fields specifically (department picker itself is fine).
  - Client-side mirror of the BE bridge guard (`onSave():256-266`) + zeroes out the non-matching bridge id before submit (276-277).
  - `getTypeSeverity`/`getTypeLabel` map the 4 `ServiceType` values to PrimeNG tag severities/labels for display.
  - No category field, no doctor-assignment field, no parent/variant field anywhere in the form (`initForm()` 132-148) or template.
- **Service** `services/clinic-service.service.ts` — typed `ClinicService`/`CreateClinicService` interfaces mirror the BE columns 1:1 (including `lis_investigation_id`, `rad_procedure_id`); `list/listAll/getById/create/update/delete` + `departments()` lookup. No search-debounce logic lives here (that would be a component-level `RxJS debounceTime` fix, not present).

---

## 2) The 3 assignment possibilities for a كشف (consultation) service — current state

| Mode | Owner's model | Code status |
|---|---|---|
| **(أ) Specific doctor with own hours, chosen at service creation** | Doctor bound to the service itself | **❌ MISSING.** `ClinicService` has no `doctor_id` column at all. The only per-doctor artifacts that exist are: (1) `ServicePrice` with `scope='doctor'` (a price axis, not an assignment — `clinic_service_prices` migration:15-17, columns `scope/department_id/doctor_id`), and (2) `DoctorScheduleSlot` (`doctor_schedule_slots` table: `doctor_id, department_id, weekday, start_time, end_time, capacity` — `2026_06_22_105001_create_doctor_schedule_slots_table.php`), which ties a doctor's weekly hours to a **department**, not to a specific **service**. There is no join anywhere that says "service X is provided by doctor Y on their schedule." This is documented as gap **F9** in the KB. |
| **(ب) Department + doctor chosen at booking time** | Department-only booking, doctor picked when appointment is booked | **✅ SUPPORTED.** `appointments` table requires `department_id`, `doctor_id` is nullable (`BookAppointment.php:48-56`). Reception/booking flow can create an appointment against a department without a doctor pre-selected. |
| **(ج) Whole department provides it; whoever opens the case takes it** | Auto-assignment to any available doctor in the department at arrival | **✅ SUPPORTED via `DoctorAssignmentService`.** `pickDoctor()` (`Services/DoctorAssignmentService.php:38-108`) scores all `DoctorScheduleSlot`s in the department for the weekday/date by most seats-left → fewest live-queue → lowest grade rank, and picks the best. `pickAnyActiveDoctor()` (118-155) is the walk-in fallback when no schedule exists at all (`NoScheduleException` → `MarkArrived.php` catches and falls back). The assigned doctor then drives `RevenueSplitScheme.doctor_id` for revenue-split attribution. |

**Net:** `ClinicService` today has zero columns that couple a service to a doctor or an assignment-mode. Modes (ب)/(ج) work at the *department/appointment/schedule* layer independent of the service row; mode (أ) has no supporting column/relation anywhere. To support all 3 as a first-class per-service choice, the service needs an explicit assignment-mode concept.

---

## 3) Service categories — current state

**❌ MISSING entirely.** Grepped the whole `Modules/Clinic` app tree for `category`/`Category` — the only hits are unrelated (`Icd10Code.category` — an ICD-10 chapter code, `Icd10CodeResource`, and unrelated NPHIES claim-adjudication `category.coding` JSON paths). `ClinicService` has no `category`/`category_id` column, no relation, no seeder. The KB's target design (§4, F-item under "الوضع المستقل") explicitly calls for adding a category field to lab-type services (seeded from imported catalog categories) to drive future pick-grids — this is greenfield, no partial implementation to build on.

---

## 4) Bridge columns `lis_investigation_id` / `rad_procedure_id` — consumed or not?

**Confirmed NOT consumed anywhere in the ordering flow (dead data today).**
- Grep across the entire BE (`Modules/*`) for these two column names outside the Clinic catalog layer itself returned **zero hits**.
- The only readers/writers are: `$fillable` on the model, the two FormRequest guards (validate shape only, never resolve the referenced row), `ClinicServiceResource` (API output), and the factory (tests).
- `OrderLabFromEncounter.php` (65-80) builds its LIS payload from `input['investigations'][].investigation_id` passed directly by the caller — never reads `ClinicService.lis_investigation_id`.
- `CreateRadOrder.php` (59-62) resolves `RadProcedure::where('id', $data['procedure_id'])` directly from the caller's payload — never reads `ClinicService.rad_procedure_id`.
- **Consequence:** defining a service with `service_type=lab` and a `lis_investigation_id` produces no linkage to any real order today; it's stored/validated/displayed but functionally inert. This matches KB gap **F1** exactly ("الجسر LIS/Rad غير مستهلك تمامًا (مؤكد)").

---

## 5) What's MISSING for م1's goal (full CRUD + 4 types + 3 assignment modes + categories)

| Gap | Detail | Blocks |
|---|---|---|
| **G1 — No doctor assignment on service (F9)** | No `doctor_id`/`schedule` link from `ClinicService` to a specific doctor+hours. | Assignment mode (أ). |
| **G2 — No assignment-mode flag** | Nothing records *which* of the 3 modes a given كشف/إعادة service uses; today (ب)/(ج) are indistinguishable at the service level — they're just emergent from whether an appointment happens to have a `doctor_id` at booking vs. relying on auto-assign. | Making mode an explicit, owner-visible per-service choice. |
| **G3 — No category/grouping on service** | No column, no seeder, no UI. | Category-based pick-grids planned for later phases; CSV/catalog import categorization. |
| **G4 — Bridge columns unconsumed (F1)** | `lis_investigation_id`/`rad_procedure_id` stored but not read by any order-creation path. | Standalone-mode lab/rad services can't actually generate a real request from the catalog row; integrated-mode services carrying these IDs are inert/misleading. |
| **G5 — No `parent_service_id` / variant concept (F5-adjacent, referenced in KB gyna reference)** | "Follow-up/re-visit" as a cheaper linked variant of a base service doesn't exist — no column, no UI, no logic. Owner explicitly wants this modeled as a *linked* variant (not a duplicate flat catalog row like the gyna legacy system). | Revisit/follow-up pricing (fuller scope is م8, but the *column* is a م1 catalog concern per the KB plan line 54: "خدمة الإعادة كـ variant مرتبط (parent_service_id)"). |
| **G6 — Search/pickers UX debt** | Search box has no debounce (fires `loadAll()`→full auto-paginated refetch on every keystroke); `lis_investigation_id`/`rad_procedure_id` are raw numeric inputs, not search/pick components. | Usability at catalog scale (500+ lab tests in standalone mode per KB import plan). |
| **G7 — No one-click catalog import for lab/rad in standalone mode** | No reuse of the LIS Setup Wizard catalog sources (curated catalog / NAFIS 1,666 / CSV) to seed `clinic_services(service_type=lab)` names+categories. Nothing in `Modules/Clinic` references wizard catalog sources. | Owner's explicit م1 deliverable: "استيراد كتالوج تحاليل/أشعة بنقرة للوضع المستقل". |
| **G8 — No explicit enable/disable action distinct from full update** | `is_active` toggling only happens through the generic `update()` PUT with full validation payload; there's no lightweight `PATCH .../toggle` endpoint. Not a hard blocker (FE could just PUT `{is_active}`), but worth noting since the task asked specifically about disable/enable. | Minor — UX/endpoint-count nicety only. |
| **G9 — `clinic_service` not in ClinicalIntent's `catalog_ref_type` whitelist (F2, adjacent but relevant)** | `StoreClinicalIntentRequest.php:50` whitelists only `lis_investigation,lis_package,rad_procedure` — a standalone-mode lab/rad `ClinicService` can't be referenced by a clinical intent at all yet. This is really a م4 (execution) gap, but the *service catalog* needs to be shaped (G3/G4) before this can be closed, so it's listed here as a dependency to keep in mind while designing the م1 schema. | Standalone end-to-end execution (M4), not م1 itself, but shapes what م1 must produce. |

---

## 6) Smallest changes to close each gap (no migration unless truly required)

- **G1 (doctor assignment)** — **requires migration.** Add nullable `doctor_id` (FK→`lab_doctors`, nullOnDelete) to `clinic_services`. Smallest version: just the FK column + relation `doctor()` on the model + fillable/cast/validation/resource/FE field. Do NOT try to inline hours on the service row — reuse the existing `doctor_schedule_slots` table (already has `doctor_id`+hours); a service with `doctor_id` set simply means "this service's schedule = that doctor's `DoctorScheduleSlot` rows," so no new hours storage is needed, only the join.
- **G2 (assignment-mode flag)** — **requires migration** (small: one column). Add `assignment_mode` enum/string (`specific_doctor|department_choose_at_booking|department_pool`) nullable/default on `clinic_services`, only meaningful when `service_type=consultation`. Pure additive column + enum class + validation + FE select — no data migration needed since it's new.
- **G3 (category)** — **requires migration** (small: one/two columns). Simplest: add nullable `category` string column (mirrors how `Icd10Code.category` is a plain string, not a separate table) to avoid needing a new categories table/CRUD in م1. A full `clinic_service_categories` table is a bigger option if the owner wants managed categories with bilingual names — flag this as a design choice for the owner, not something to decide unilaterally.
- **G4 (bridge consumption)** — **no schema change**, pure logic/wiring change (belongs more to م4 per KB's own phase tagging, but the *validation* that the referenced ID actually exists could be tightened in م1's FormRequests with an `exists`-style check against LIS/RIS tables, since those are soft cross-module refs without FK — needs a raw query check, not `Rule::exists` across modules the normal way).
- **G5 (parent_service_id)** — **requires migration** (small: one nullable self-referencing FK column `parent_service_id` on `clinic_services`, nullOnDelete). Additive only, no backfill needed (no existing rows use it).
- **G6 (search/pickers UX)** — **no BE change.** FE-only: add `debounceTime(300)`/`distinctUntilChanged()` (RxJS `Subject` on `filterQ`) before calling `loadAll()`; swap the two bridge `p-inputNumber` fields for a proper autocomplete/pick component once G7's catalog import exists (or point them at existing LIS/RIS search endpoints if available — not checked in this scout, would need a separate look at LIS investigation search endpoints).
- **G7 (catalog import)** — **no ClinicService schema change** (create/store already accepts bulk-ish single-row creates); needs a new BE endpoint/action that reads the LIS Setup Wizard's catalog sources and bulk-creates `ClinicService(service_type=lab|radiology)` rows, plus a FE "Import" trigger UI. This is the largest G-item — likely deserves its own action class (e.g. `ImportLisCatalogAsClinicServices`) rather than reusing `UpsertClinicService` per-row for bulk. Where the wizard catalog sources actually live (curated catalog / NAFIS list / CSV parser) was NOT located in this scout — needs a follow-up look at the LIS module's Setup Wizard actions before implementation.
- **G8 (toggle endpoint)** — **no migration.** Optional convenience `PATCH /clinic/services/{service}/toggle` calling `UpsertClinicService::update($service, ['is_active' => !$service->is_active])`, or just leave as-is and have FE do it via a partial PUT (already works today, no gap in capability, only in ergonomics).
- **G9 (intent whitelist)** — **no migration** (column is already generic `catalog_ref_type` string + `catalog_ref_id` int on `clinical_intents`). Pure validation whitelist change (`in:lis_investigation,lis_package,rad_procedure,clinic_service`) + a new branch in `DecideClinicalIntent`. Flagged only as a downstream dependency on how G3/G4 shape the catalog — not a م1 action item per the KB's own phase tagging (KB puts this at م4/L4).

**Flag for the owner:** G1, G2, G3, G5 all need small additive migrations (nullable columns only, no backfill, no breaking change to existing rows/queries) on `clinic_services`. None of them touch existing data or require a data migration — all are purely additive nullable columns/FKs, safe to run on `moonui_dev_be` per the standing dev-DB convention. G3 in particular has an open design choice (flat string column vs. a real categories table) that should be confirmed with the owner before implementation, since the KB's own plan mentions categories should later drive "pick-grids" — a real table might be worth it if the owner wants managed/orderable categories rather than free-text.

---

## Files referenced (file:line summary)

- BE: `Modules/Clinic/app/Http/Controllers/ClinicServiceController.php`
- BE: `Modules/Clinic/app/Models/ClinicService.php`
- BE: `Modules/Clinic/app/Actions/UpsertClinicService.php`
- BE: `Modules/Clinic/app/Enums/ServiceType.php`
- BE: `Modules/Clinic/database/migrations/2026_06_22_010001_create_clinic_services_table.php`
- BE: `Modules/Clinic/database/migrations/2026_06_22_020001_create_clinic_service_prices_table.php`
- BE: `Modules/Clinic/database/migrations/2026_06_22_105001_create_doctor_schedule_slots_table.php`
- BE: `Modules/Clinic/app/Http/Requests/StoreClinicServiceRequest.php` / `UpdateClinicServiceRequest.php`
- BE: `Modules/Clinic/app/Http/Resources/ClinicServiceResource.php`
- BE: `Modules/Clinic/app/Services/DoctorAssignmentService.php`
- BE: `Modules/Clinic/app/Models/DoctorScheduleSlot.php`
- BE: `Modules/Clinic/app/Actions/OrderLabFromEncounter.php`
- BE: `Modules/Clinic/app/Actions/CreateRadOrder.php`
- BE: `Modules/Clinic/app/Http/Requests/StoreClinicalIntentRequest.php` (whitelist line 50)
- BE: `Modules/Clinic/app/Models/ServiceOrderLine.php` (fillable incl. `clinic_service_id`/`catalog_ref_type`/`catalog_ref_id`)
- BE: `Modules/Clinic/routes/api.php` (lines 96-100 services CRUD routes)
- BE: `Modules/Clinic/tests/Feature/ClinicServiceCatalogTest.php`
- FE: `features/clinic/services/clinic-service-list.component.ts`
- FE: `features/clinic/services/clinic-service-list.component.html` (filter bar 39-95, bridge fields 313-343)
- FE: `features/clinic/services/clinic-service.service.ts`
- KB (canonical, owner-authored, cross-checked): `knowledge-base/topics/clinic-service-settings.md` (F1-F10 gap table §3, target logic §4)
- KB (execution plan, م1 scope quote): `knowledge-base/topics/clinic-execution-phases.md` (line 54)
