# LIS — 8-point batch: portals, report comments, culture catalog (2026-06-26)

**Status:** ✅ SHIPPED on `hazemdev` + FF'd to `main`. FE deployed to `/app`. Reaches elmadina + every lab via the normal MoonStack update.
**Commits:** BE `1b85a9278` · FE `0258c1a`.
**Plan + analysis:** `https://moonui.elbaset.com/lab-8-fixes-plan.html` (5-agent read-only investigation, one card per point).
**Reviews:** native code-reviewer ×2 (Codex blocked on server). Verdicts APPROVE + WARN; all findings applied (2 MEDIUM on the P3 migration, several LOW).

> 🔴 **The two data migrations run on the CLIENT via update — NOT on `moonui_dev_be`** (not binlogged + classifier-blocked). After the owner updates elmadina, verify there (see §Verify-on-elmadina).

## The 8 points

**P1 — password eye toggle** (FE). External-lab add/edit form: `input[type=password]` → PrimeNG `p-password [toggleMask][feedback]=false`. The B2B portal login already had it. Files: `external-labs/lis-external-labs.component.{ts,html}`.

**P2 — B2B portal prints the request invoice** (BE+FE). New `ExternalLabPortalRequestController::invoice()` (scoped by `findOwnedRequest` → no cross-lab IDOR; reviewer-confirmed) + route `GET external-lab-portal/requests/{id}/invoice` + lang `invoice_not_available`. Returns the **client lab as buyer** (`client` block). FE: `external-lab-portal.service.getClientRequestInvoice()` + `client-request-detail.viewInvoice()` + "Print invoice" button → renders via `LisTaxInvoiceService` (`a4html`) with the new optional `buyer` block.

**P3 — internal/process comments stopped leaking onto the report** (BE + migration). Root cause: one overloaded `comment` column held clinical + system + process text; the report printed it. **Writer-level fix** (not heuristics): retest reason → `defer_comment`; `Auto-calculated` & machine `Auto-verify:` → `internal_note`; `rerunHistory` reads `defer_comment ?: comment`. **Cleanup migration** `2026_06_27_000001_*`: lossless backup `comment`→`internal_note`, strip known system fragments, clear `No reason`, and clear a retest `comment` **only** when it's provably preserved in `defer_comment` AND the row has no entered `result_value`/`result_text` (so a clinical comment can never be lost). The FE `auto-calculated` band-aid regex is KEPT as defense-in-depth. Files: `LabResultController` (retest/rerunHistory), `LabResultService::applyCalculatedValue`, `LabMachineResultController` (auto-verify).

**P4 — portals honor full report settings** (BE+FE). Root cause: both portal `reportConfigPayload()` copies (drifted) emitted only scalar flags, dropping the 3 JSON blocks → portals re-showed hidden header fields. Fix: shared trait **`BuildsPortalReportConfig`** (one source of truth) emits `header_config` / `print_comment_config` / `histo_section_order` + `show_printed_by`; both `PatientPortalController` + `ExternalLabPortalRequestController` `use` it. FE: `client-report-print.service` + `pp-print.service` set those blocks on `ReportData` (both engines pick them up).

**P5 — sample-reasons default seeder** (BE seeder). `LabRejectionReason` (`/lab/sample-reasons`) had a seeder for `deferral` only. Extended `LabDeferralReasonSeeder` (kept the name — already wired in installer+updater) to also seed clean canonical `rejection` + `result` sets, idempotent `firstOrCreate(company_id,code)`. Reaches elmadina via `sync-reference` on update. (Decision: canonical clean set, NOT a copy of moonui's duplicate-laden data.) ⚠️ unique key is `(company_id, code)` — codes must be globally unique per company across all categories.

**P6 — portal invoice Arabic was garbled** (FE). Root cause: **NOT a font issue** — jsPDF does no Arabic shaping/bidi; the patient portal forced `a4` (jsPDF). Fix: new `LisTaxInvoiceService.printHtmlA4()` renders the A4 invoice as **browser-printed HTML** (correct shaping, like the thermal path) + `InvoicePrintMode 'a4html'`; patient portal switched `a4`→`a4html`. B2B invoice (P2) uses `a4html` too.

**P7 — barcode panel shows members** → **already fixed**; elmadina's live FE (4.0.15) is byte-identical to moonui and contains the 2026-05-29 fix (`3039dd05`/`3b5ad56e`). The reported PDF was from an older onboarding build. **No code change** — verify on the current build. (Optional future hardening: the print path trusts the list payload for `panel_members`.)

**P8 — culture tests mistyped in the seeded catalog** (DATA + migration + seeder). Root cause: catalog built from NAFIS (numeric/text only); only 1 real culture test was typed `culture`. Fix: (a) **32 true bacterial/fungal C&S tests** retyped `text`/`memo`→`culture` in `house_catalog.json` (curated explicit code list — excludes Gram stain/PCR/antigen/molecular-typing/tissue-culture); junk rows `ntest`/`test2`/`test3` removed; `culture_config`+`file_settings` added to `LisCatalogService::CORE_FIELDS` so culture defaults travel on export/import. (b) idempotent corrective migration `2026_06_27_000002_*` retypes by code with a **history guard** (skips any investigation that already has an entered `result_value`/`result_text` — never hides an existing value behind the culture cell; logs counts). (c) new **`LabAntibioticSeeder`** (~28-drug default AST panel) wired into installer + updater + `LISDatabaseSeeder`. What `result_type=culture` unlocks: the organism + S/I/R + MIC culture cell (reads `culture_config` + `lab_antibiotics`).

### The 32 retyped culture codes
BLCU CSCU EACU NACU SECU SPCU STCU THCU VACU WOCU URCU · AEROBCX AEROBCX2 ANAEROB ANAEROBC ANAEROBC2 · AFBCULT MTBCULT CHLAMCX MYCOPCUL · CSPUS GCCS URETHCS VAGCX SPUTCX UCULTISO SALSHIGSTL GBSCS GBSSCR · FUNGBCUL FUNGSKIN MOLDCX YEASTCX. (URCU was already culture.)

## Follow-up (same day) — histopathology/culture report rendering + preview on both worklists
**Commits:** FE `6b1d305` · CHANGELOG `c593519ec`. (Triggered by the owner's `Report Histopathology 26.6.pdf` + the B2B portal screenshot.)

**Bug:** narrative/structured results (histopathology, culture) were rendered as a normal numeric row — the full diagnosis dumped into the "Result" value cell (giant overflowing text on staff print, long-line overflow on the portal), a bogus **"Normal"** flag, and counted in the section "1 normal" badge — **while also** rendering the proper structured block (Diagnosis/Gross/Microscopic…) below. Only the **HTML** report engine had this; the classic jsPDF engine already skipped structured rows from the table (`lis-report-pdf.service.ts` ~`if (r.histopathPayload||r.culturePayload||r.filePayload) continue`).

**Fix (root = the report-row build + the HTML engine):**
- `lis-report-section-builder.ts` (shared, both portals) `toRow()` + `lis-print-report.service.ts` `_render()`: for a structured row **gated on the PAYLOAD being present** (`histopath`/`resultPayload`) — clear `result`/`unit`/`referenceRange`/`flag`. Gating on payload (not just `result_type`) means an *unfilled* histopath never hides its value, and keeps the staff path in sync with the HTML engine's payload-based suppression. The rich block carries the content; the value cell shows `—`.
- `lis-html-report.service.ts`: `sectionStats()` skips rows with `histopathPayload||culturePayload` (not counted normal/abnormal); all **4** `flagPill(r.flag)` call sites (editorial/bento/monochrome/spectrum) render no pill for structured rows; the section normal/abnormal **badges hide when there are no numeric results** (spectrum `stat-pill` + the other three) so a histopath-only section never shows "1 normal". The `document`/narrative template already pre-filters structured rows. **`flagClass` does `flag||'normal'`** — so an empty flag would otherwise read "Normal"; that's why the guards are **payload-based, not flag-based**. Normal numeric rows (flag `''`→normal) are unaffected.

**Preview button now on BOTH worklists:** the on-screen report preview (iframe via canonical `previewRequest`) was added to `dept-worklist` (result-entry Worklist) — it was only on `validation-worklist`. Same blob-URL lifecycle (revoke on close/destroy + closed-during-render race guard).

**Note on "preview shows nothing for histopath":** same engine + data as the print path (which rendered it), so the blank was most likely the giant value row breaking the on-screen layout — expected to resolve with the value-cell fix. If it still reproduces after the update, capture the iframe/console to diagnose the iframe specifically.

Review: native code-reviewer APPROVE (0 critical/high/medium); LOW fixes applied (payload-gated detection to avoid hiding unfilled values; dropped an `as any`). The unrelated `src/assets/config.json` apiUrl flip (moonui2→moonui) in the working tree was deliberately **NOT** committed with this batch.

## ✅ VERIFIED on elmadina (2026-06-26, after the owner's update) + a P5 hotfix
The owner ran the update; read-only checks on `elmadina_db` (company 1) confirmed all code reached it (new files present, FE build 22:52, both `2026_06_27` migrations recorded). State: **P8 culture=33 · antibiotics=28 · P3 leftover system fragments in printed comment = 0**. All good — EXCEPT P5.

🔴 **P5 BUG found + fixed (commit `caf80c87e`):** `lab_rejection_reasons.code` is **`varchar(10)`**. Six of the new canonical codes exceeded it (`INSUFFICIENT`/`WRONG_CONTAINER`/`CONTAMINATED`/`EXPIRED_TUBE`/`IMPROPER_STORAGE`/`RC_CRITICAL`). Under **MySQL strict mode** the first oversize insert threw *"Data too long"* and **aborted the whole seeder mid-run** → elmadina got only the 2 short codes created before the throw (`HEMOLYZED`,`CLOTTED`) and **0 result** reasons. **Lesson:** the idempotency test runs on **sqlite, which does NOT enforce varchar length**, so it passed despite the bug — only MySQL strict caught it. **Always keep `lab_rejection_reasons.code` ≤ 10 chars** (or widen the column). Fixed by shortening to `QNS/WRONG_TUBE/CONTAM/EXP_TUBE/BADSTORE/RC_CRIT/MISLABEL`.
  - **Backfilled both DBs** by copying the fixed seeder to each and running `php artisan db:seed --class=…LabDeferralReasonSeeder --force` (idempotent/additive). Final: **elmadina deferral=10 · rejection=13 · result=6**; moonui populated per-company.

🔧 **Release CHANGELOG ownership (recurring [[release-chown-moonui]]):** the owner's `moonstack:ship` failed — *"could NOT write …/CHANGELOG.md — [Unreleased] not cleared"* — because the file was **root-owned** (Claude edits BE as root). Fixed with `chown -R moonui:moonui /home/moonui/moon-erp-be` + `/app`. The failed ship did NOT partially promote (no duplicate dated section; `[Unreleased]` holds each bullet once) → re-running ship is safe. **Always `chown -R moonui:moonui` the BE tree + `/app` before any release.**

## Verify-on-elmadina (the in-app checks the owner should still eyeball)
1. **P8 migration log** → `[P8] retype_culture_investigations` shows `retyped`/`skipped_with_results`. Open `/lab` result entry for e.g. `SPUTCX`/`BLCU` → the **culture cell** (organism + AST grid) renders, antibiotic list populated (from the seeder). Print → culture layout in the report.
2. **P3** → create a NEW retest with a reason + trigger a formula auto-calc → release → report shows NO reason/Auto-calculated; the doctor's clinical comment still prints. Rerun-history dialog still shows the reason.
3. **P4** → hide a header field + set comment style in Lab Settings → print from patient & B2B portal → field hidden / style applied (matches internal).
4. **P2** → B2B portal → open a request → "Print invoice" → A4 with the client lab as buyer + items + VAT + QR.
5. **P6** → patient/B2B portal invoice → Arabic renders connected/RTL.
6. **P5** → `/lab/sample-reasons` → all 3 tabs populated.
7. **P1** → external-lab form password eye toggles.
