# LIS Smart Report — Phase 1 implementation plan (التقرير الذكي)

> **Status:** PLAN — owner-approved direction, code NOT started. Grounded in a full BE+FE code study (4 探 agents, 2026-06-30).
> **Canonical topic:** [[lis-smart-report]] · **Owner-facing visual plan:** `public_html/smart-report-phase1-plan.html` · **Reference sample:** `public_html/1777288186381.pdf` (Dr Sulaiman Al Habib style).
> **Branch/deploy:** work on `hazemdev`, deploy FE → moonui `/app`, BE `local-deploy` → `moonui_dev_be`; ship to clients via MoonStack release→update. NEVER cp to staging/clients ([[feedback_no_direct_deploy_to_staging_or_clients]]).

---

## 1. What we're building (locked decisions)

A **separate, second patient report** named **«التقرير الذكي»**, printed for the patient and downloadable on the patient portal, styled like the reference (cards + severity bars + trend charts + Arabic descriptions + status icons). Built in **two phases**:

| | Phase 1 (THIS plan) | Phase 2 (later) |
|---|---|---|
| Layer | **Visual report — NO AI.** Deterministic, offline, zero PHI-leaves-system. | **AI assessment box** (DeepSeek via existing AI infra). |
| Content | Header band · overall-severity banner · per-test cards (value + status icon + 5-zone severity gradient bar + historical trend sparkline + Arabic description) · footer/disclaimer. | A worded severity narrative + "see a doctor" recommendation, rule-computed severity, anonymized facts, best-effort + cache. |
| Trigger | On-demand button **«تحميل التقرير الذكي»** (staff print/preview + portal download). | Same report, AI box added. |
| Risk | Low. No external calls, no key dependency. | Needs the **MAC bug fix** (below) + privacy sign-off. |

**Locked sub-decisions:**
- 3-level overall severity (rule-based): 🔴 **عاجل** (any critical) · 🟠 **يحتاج انتباه** (any out-of-range non-critical) · 🟢 **طبيعي** (all normal).
- Static per-test Arabic description source = **new catalog field `description_ar` + built-in default map fallback**.
- Two lab settings: `lis.smart_report_enabled` (default **on**) · `lis.smart_report_ai_enabled` (default **off**, Phase 2 only).
- Rendering = **FE renders HTML** as a new `'smart'` report template; charts are **inline SVG**; print/preview/portal reuse the existing engine. (jsPDF rejected — can't shape Arabic; project norm is HTML→print.)
- **Bilingual (عربي + English).** The report renders in **both languages**, following the same convention as the existing reports: chrome/field labels follow the lab's `ReportSettings.report_label_lang` (`'ar'|'en'`), test **names show Arabic + English** (`name_ar`/`name_en`, as in the reference), the **description follows the report language** (`description_ar`/`description_en` with fallback to the other), and every fixed string we add (severity banner, status, disclaimer, footer) ships with **both `*_ar` and `*_en`**. Severity-scale zone captions stay English (Crit Low · Low · Normal · High · Crit High — clinical universal). Reuse `buildHeaderFields`/`HEADER_LABELS_AR` + `LanguageService.currentLang()`.

---

## 2. Architecture

```
[Staff Validation / Requests screen]  or  [Patient Portal]
        │ click «تحميل التقرير الذكي»
        ▼
FE LisPrintReportService.smartRequest(reqId)  ──GET──►  BE  GET /api/lis/requests/{id}/smart-report
        │  (payload: header + sections[] + per-test {value,flag,zone,bar_position,range,trend[]} + overall severity)
        ▼
FE LisHtmlReportService.render('smart', payload)  →  HTML string
        │
        ├─ print:   printReport() → window.open + win.print()      (existing mechanism)
        ├─ preview:  Blob → iframe in existing p-dialog            (existing mechanism)
        └─ portal:   PpPrintService.printSmart() + trackDownloaded (existing mechanism)
```

**Severity is computed on the BE** (single source of truth → reused by Phase 2 AI). The FE only renders.

---

## 3. Data contract — `GET /api/lis/requests/{id}/smart-report`

Auth `auth:sanctum`, company-scoped (`->where('company_id', auth()->user()->company_id)->findOrFail($id)`), permission `lis.result-reports.generate`, gated on `lis.smart_report_enabled`. Portal twin: `GET /api/lis/portal/requests/{id}/smart-report` (portal token auth, ownership-checked, `trackDownloaded`).

```jsonc
{
  "request": { "number": "LR-2026-00016", "date": "2026-06-30", "priority": "routine", "doctor": "...", "branch": "..." },
  "patient": { "name": "...", "mrn": "...", "age": 34, "sex": "female" },   // FE header ONLY (never sent to AI in Phase 2)
  "overall": {
    "level": "attention",                  // urgent | attention | normal
    "label_ar": "يحتاج انتباه", "label_en": "Needs attention",
    "color": "#d97706",
    "guidance_ar": "يُنصح بمراجعة الطبيب لمناقشة النتائج",
    "guidance_en": "Please consult your doctor to discuss these results.",
    "counts": { "normal": 18, "abnormal": 3, "critical": 0, "qualitative": 1 }
  },
  "sections": [{
    "name": "أمراض الدم",
    "tests": [{
      "investigation_id": 12, "code": "HGB", "name_ar": "الهيموجلوبين", "name_en": "Hemoglobin",
      "description_ar": "يقيس مستوى الهيموجلوبين لتشخيص ومتابعة فقر الدم.",
      "description_en": "Measures blood hemoglobin to diagnose and monitor anemia.",
      "value": "9.1", "unit": "g/dL", "is_numeric": true,
      "flag": "low", "zone": "low", "status": "warn",           // ok | warn | crit | neutral
      "normal_min": 11.8, "normal_max": 14.8, "critical_low": 7, "critical_high": 20,
      "reference_text": "11.8 – 14.8",
      "bar_position": 0.34,                                     // 0..1 across the 5-zone scale
      "trend": [ { "value": 10.2, "date": "2026-01-04", "min": 11.8, "max": 14.8 }, /* … chronological */ ]
    }]
  }],
  "ai": null,                                                  // Phase 2 fills this
  "meta": { "smart_report_enabled": true, "ai_enabled": false, "generated_at": "2026-06-30T12:00:00Z" }
}
```

---

## 4. Severity engine (rule — BE, authoritative)

Reuse the system's existing per-result flag — **do not invent new medical logic**.

- Per result: read stored `LabResult.abnormal_flag` (`Modules/LIS/app/Enums/AbnormalFlag.php`: `normal|low|high|critical_low|critical_high|abnormal`), or recompute via `LabResultService::calculateAbnormalFlag()` (`app/Services/LabResultService.php:667`). `AbnormalFlag::isCritical()` → critical_low/high.
  - `critical_low|critical_high` → zone `critical_low|critical_high`, status `crit`.
  - `low|high` → zone `low|high`, status `warn`.
  - `abnormal` → status `warn` (generic out-of-range).
  - `normal` → zone `normal`, status `ok`.
  - structured types (culture/histopath/file → `abnormal_flag = null`) → status `neutral`, excluded from the numeric severity vote.
- **Overall (3-level):** any `crit` → `urgent`; else any `warn` → `attention`; else `normal`.

**Bar position** (`0..1` across **5 equal segments**, each width 0.2 — `[criticalLow | low | normal | high | criticalHigh]`), inputs CL=`critical_low`, NL=`normal_min`, NH=`normal_max`, CH=`critical_high`, value `v`. Find the segment, then linear-interpolate inside it:
- `v < CL` → segment 0 → clamp ≈ `0.10`
- `CL ≤ v < NL` → segment 1 (low) → `0.20 + (v-CL)/(NL-CL) * 0.20`
- `NL ≤ v ≤ NH` → segment 2 (normal) → `0.40 + (v-NL)/(NH-NL) * 0.20`
- `NH < v ≤ CH` → segment 3 (high) → `0.60 + (v-NH)/(CH-NH) * 0.20`
- `v > CH` → segment 4 → clamp ≈ `0.90`
- **Graceful degradation** when thresholds missing: no criticals → collapse to a 3-segment bar (Low·Normal·High); no range at all → hide the bar, show value + status only.

Implement in a new `Modules/LIS/app/Services/LisSmartReportService.php`:
- `build(LabRequest $req): array` — loads released results (mirror `LabReportPdfService::generateRequestReport()` load at `app/Services/LabReportPdfService.php:35`: `results` where `status=ResultStatus::Released` + `results.investigation.section`), maps tests, attaches descriptions + trend, computes overall, returns the §3 payload.
- `overallSeverity(array $tests): array` · `barPosition(float $v, ?float $cl, ?float $nl, ?float $nh, ?float $ch): float` · `trendSeries(int $patientId, int $investigationId, int $companyId): array` · `describe(LabInvestigation $inv): ?string` (catalog field → default map).

---

## 5. Phase-1 BACKEND work packages

**WP-B1 — Catalog description field** (`Modules/LIS`)
- Migration `database/migrations/2026_06_30_120000_add_descriptions_to_lab_investigations.php` — `Schema::table('lab_investigations', …)` add nullable `text description_ar`, `text description_en` (guard with `Schema::hasColumn`, `->after('method_ar')`, with `down()` dropColumn). Template: `2026_05_25_170100_add_investigation_revamp_columns.php`.
- `LabInvestigation::$fillable` (`app/Models/LabInvestigation.php:26-88`) += `description_ar`, `description_en` (plain text, no cast).
- Validation rules: `app/Http/Requests/StoreLabInvestigationRequest.php` (~line 70) + `UpdateLabInvestigationRequest.php` (~line 67) += `'description_ar' => ['nullable','string','max:500']`, same for `description_en`.
- `LabInvestigationResource` += the two fields (so the editor + report can read them).

**WP-B2 — Trend perf index** (same or sibling migration)
- `lab_results` has only `index(['patient_id'])` + `index(['company_id','status'])`. Add `index(['patient_id','investigation_id','status'])` for the hot per-test trend lookup. Additive, safe.

**WP-B3 — `LisSmartReportService`** (new, §4). Default Arabic description map = a static array keyed by common codes (HGB, GLU, CREA, …) as fallback when `description_ar` is empty.

**WP-B4 — Endpoints**
- `Modules/LIS/routes/api.php`: `Route::get('requests/{request}/smart-report', [LabRequestController::class, 'smartReport'])->name('requests.smart-report');` (or a dedicated `LisSmartReportController`). Company-scoped, permission `lis.result-reports.generate`, 404/empty if `lis.smart_report_enabled` is off.
- Portal twin in the `api/lis/portal` group (token auth, ownership-checked) reusing `LisSmartReportService::build()`; call the existing track endpoint on download.

**WP-B5 — Settings definitions** (`database/seeders/LabSettingDefinitionSeeder.php::getDefinitions()`)
- `lis.smart_report_enabled`: `value_type=boolean`, `default_value='1'`, `display_group='lis_reports'`, `is_visible=true`, labels «التقرير الذكي» / "Smart Report".
- `lis.smart_report_ai_enabled`: `value_type=boolean`, `default_value='0'`, `display_group='lis_reports'`, `is_visible=true`, labels «تقييم الذكاء الاصطناعي» / "AI assessment" (inert until Phase 2).
- Read at runtime: `app(SettingsService::class)->get('lis.smart_report_enabled', $companyId)` → real bool. Surfaces automatically on the Lab Settings screen via `LabSettingsController::index` (no controller change).

**WP-B6 — (optional) seed descriptions** from `Modules/LIS/database/data/nafis_catalog.csv` `ref_long_desc` column (already present, unmapped) via `NafisCatalogSeeder` → `description_en`. Nice-to-have, not blocking.

**WP-B7 — Tests (Pest, sqlite)**: severity rule (each flag → zone/overall), bar-position math (incl. missing-threshold degradation), trend query shape, endpoint company-scoping + settings-gate, structured-type exclusion.

---

## 6. Phase-1 FRONTEND work packages (`moon-erp/src/app`)

**WP-F1 — Register the template**
- `core/services/lis-lab-info.service.ts:21` — add `'smart'` to the `ReportTemplate` union + `REPORT_TEMPLATES` + `normalizeReportTemplate`.

**WP-F2 — `smart` renderer** in `core/services/lis-html-report.service.ts`
- Add a `smart` case in `render()` (`:104-109`) → new `smart(data, lab, logo, opts): string` (copy `editorial()` `:543-686` structure). Reuse `sharedHead()` (Cairo for Arabic, `@page A4`), `buildHeaderFields`/`HEADER_LABELS_AR`, `dir="auto"`.
- **Bilingual:** pick language from `merged.labelLang` (`report_label_lang`) like the other templates; render test name in both `name_ar`+`name_en`, description in the report language (fallback to the other), and use the `*_ar`/`*_en` strings from the payload for the banner/status/disclaimer. Don't hardcode Arabic-only.
- New inline-SVG helpers (next to `rangeBar()` `:416-440`):
  - `severityBar(test): string` — 5-zone gradient strip + positioned pointer (uses `bar_position`).
  - `sparkline(trend): string` — small SVG polyline + shaded normal band (uses `trend[]`).
  - `severityBanner(overall): string` — full-width colored status strip.
  - `smartTestCard(test): string` — name + description_ar + value/unit + status icon + severityBar + sparkline + reference.
- Charts = inline SVG only (canvas/chart.js don't survive `window.open`+`document.write`/blob-iframe).

**WP-F3 — Orchestrator** `core/services/lis-print-report.service.ts`
- Add `smartRequest(reqId)` + `previewSmartRequest(reqId)`: fetch `GET /lis/requests/{id}/smart-report`, feed the self-contained payload to the `smart` renderer (lean `_renderSmart()`), then reuse `printReport('smart', …)` / return `{kind:'html', html}` for preview.

**WP-F4 — Staff buttons**
- `features/lis/validation-worklist/validation-worklist.component.*` — add a «تحميل التقرير الذكي / Smart Report» `<p-button>` next to Preview/Print (html ~`:390-422`; handler near `printReport()` `:3401` / `previewReport()` `:3428`). Gate on `selectedRequestStatus()?.released` + `lis.smart_report_enabled`.
- `features/lis/requests/lis-requests.component.*` — sibling kebab button (html `:258`, handler near `printReport(item)` `~:970`).

**WP-F5 — Patient portal**
- `features/lis/patient-portal-public/pp-print.service.ts` — add `printSmart(request, patient)` (fetch portal smart endpoint → `htmlReportService.printReport('smart', …)` + `trackDownloaded`).
- `pp-request-detail.component.*` — second button near `:205-209` (gate on `releasedCount() > 0`).
- `core/services/patient-portal.service.ts` — add the smart fetch (`/lis/portal/...`).

**WP-F6 — i18n** keys for the new buttons (`assets/i18n/ar.json`/`en.json`: `LIS.SMART_REPORT`, `PPORTAL.SMART_REPORT`).

**WP-F7 — FE checks**: preview renders Arabic correctly in the iframe; print A4 layout; portal download tracked; popup-blocked handled (existing pattern).

---

## 7. Visual design (frontend-design skill)

- **Palette:** brand teal `#0d9488` (+ indigo `#4f46e5`); ink `#0f2540`, muted `#7689a3`, line `#e4ebf3`, card `#fff`, page `#f4f7fb`. Severity: green `#16a34a`/`#e9f8ee`, amber `#d97706`/`#fdf3e3`, red `#dc2626`/`#fdeaea`. Gradient bar stops: red→amber→green→amber→red.
- **Type:** Cairo (Arabic display/body, already loaded by the report engine) + Inter (Latin) + tabular numerals for values/units/ranges (crisp alignment).
- **Layout (A4, RTL):** header band (lab identity + accreditation | patient block) → full-width **overall-severity banner** → per-section **test cards** (name + 1-line Arabic description + big value/unit + status icon + 5-zone severity bar w/ pointer + trend sparkline + reference) → footer (accreditation logos + «التقرير الذكي» identity + signature/QR + note).
- **Signature element:** the **severity gradient bar with a precisely-positioned pointer** — the one content-true, memorable device; everything else stays quiet/clinical. No JS animation (print-safe).

---

## 8. Rollout

1. Implement on `hazemdev` with the per-part review gate (Codex blocked here → native `code-reviewer` + apply findings) [[feedback_codex_review_after_each_task]].
2. BE: `local-deploy` to `moonui_dev_be`; **run the new migration `--path=… --force` on `moonui_dev_be` in the SAME step** (schema-dependent code crashes otherwise — [[feedback_run_migration_on_moonui_after_schema_code]]) — **needs owner permission** (migrate on dev DB was DENY'd before). Re-run `LabSettingDefinitionSeeder` (idempotent) so the 2 settings appear.
3. FE: build + deploy to moonui `/app` only.
4. CHANGELOG entry, push `hazemdev`→`main`; **owner** cuts the MoonStack release and updates clients (staging `s-elmadina` first, then live). NEVER cp to staging/clients.
5. Never `migrate:fresh` / never test on `moonui_dev_be` (not binlogged) — [[feedback_never_test_on_real_db]]; Pest = sqlite.

---

## 9. Phase 2 preview (not in this plan)

AI assessment box: `LisReportAiService` reuses `AiChatService::complete($companyId, $userId, $system, $user, ['provider'=>'deepseek'])` (`Modules/Core/app/Services/AiChatService.php:212`, never throws, logs usage — pass a real user id). Rule-computed severity (already built in Phase 1) → anonymized facts (age+sex+values only, NO name/MRN) → DeepSeek narrative, best-effort + timeout + fallback to the rule text, cached by results-hash + audited. Mandatory disclaimer «ملخّص آلي للتوعية وليس تشخيصًا طبيًا».

**🔴 Prerequisite — fix the "MAC is invalid" 500.** APP_KEY was rotated; the stored DeepSeek ciphertext can't be decrypted, and `$settings->update()` 500s in Eloquent's dirty-check (`AiSettingsService::updateSettings()` `Modules/Core/app/Services/AiSettingsService.php:35-42`; encrypted casts `Models/AiSetting.php:42-46`). Also `AiChatService::chat()` reads keys unguarded (`:63,72,87`). **Fix:** make the write + chat paths decryption-safe (catch `DecryptException`, reset the stale original raw value before update) — code-only, no DB surgery needed; mirrors the already-safe `getMaskedKey()`. The Smart Report's `complete()` path is already resilient (falls back), so this only blocks the owner from saving a working key. (Diagnosed 2026-06-30.)

---

## 10. Out of scope / risks
- **Out of scope (Phase 1):** the AI box; emailing the report; per-test reference-interval graphs beyond the sparkline; editing descriptions UI polish (field is editable via the existing investigation form once added to the resource/request).
- **Risks:** trend sparkline empty for first-time tests (degrade to "no history"); descriptions sparse until seeded/entered (default map covers common tests); A4 pagination of many cards (test print with a large request).
