# W2-FS-3 report: vitals card packs, modes, round I/O

Status: **done**. BE tests pass (15/15), the FE build is green, and there is no commit or deploy from me.
Covers review rows A #31 #32, B #55, D N15 / G9 / I6, and D bugs 2–3.

## ⚠ Commit note for the orchestrator
FE commit `10d02f96` already includes my **shell binding** (visit-shell.component.html: `[clinicalType] [context] [mode] (openCard)`) and my **i18n keys** (ar/en.json). The panel and service files that define those inputs are **still uncommitted**. HEAD on its own does not compile, so commit the FE files below next.

## What it does
- «العلامات الحيوية» now shows the 8 core vitals plus «باقة <التخصص>» plus «باقة الإقامة — جولة التمريض» (or «باقة الطوارئ»). The packs sit in the same card with the same input style and write to the same observation set as the core vitals. The server resolves the pack from the encounter: department `clinical_type` + `encounter.kind` (inpatient/day_case → inpatient, emergency → emergency). That is why the **classic consultation screen, the vitals station and dental «المزيد» get the packs too**, without any change to those screens. A type with no pack (general, dental) still shows exactly the 8 fields.
  - nutrition: waist, hip, **WHR**. WHR is computed on the server, 2 dp, and also shown live while typing. The risk flag depends on gender: F > 0.85, M > 0.90, and unknown gender uses the male threshold. The thresholds live in the definition's `meta.risk_gt`.
  - gyn_ob: **LMP** and **GA** are read-only and come from the open pregnancy (`PregnancyService::forEncounter` → `gyn_pregnancy_lmp` + header `ga_at_visit`/`ga`). They are not duplicated, and LMP has a «افتح الحمل» link (`openCard` → shell `jump('gyn_pregnancy')`). The pack also has fundal height, FHR and **urine albumin** (a select: سلبي/أثر/+/++/+++, with ++ and +++ flagged as danger).
  - inpatient/day case/emergency: RR, AVPU/ACVPU (select), pain 0–10, on O₂ (yes/no), intake ml / output ml (with the live «الرصيد ±n مل» balance) and **NEWS2**. NEWS2 is read-only and computed through `ComputedRegistry` from the pack plus core vitals. It is re-scored whenever any input changes, including a core-only pulse PATCH.
- Labels, units, ranges and options come from the observation definitions, so owners can edit them. A deactivated or missing definition just drops out of the pack.
- Panel `mode`: `full` (default), `compact` (one row of chips — weight · BMI · BP · pulse · NEWS2/WHR/GA — plus «عرض الكل»/«تصغير») and `readonly` (no inputs and no writes, but no «معتمد» badge). The shell passes `c.props.mode`.
- Hospital vital round (bug 3): intake and output inputs added. The BE already accepted them. The readings table gets a «داخل/خارج» column, and a «سوائل آخر ٢٤ ساعة: داخل · خارج · الرصيد» line is summed from the chart's rounds. The BE gives no aggregate, so the FE computes it from the rows the BE returns.

## API
GET/PATCH `/api/clinic/encounters/{id}/vitals`:
- The response adds `pack: {clinical_type, context, fields:[{code, label_ar, label_en, unit, input: number|select|yesno|date|text, options, min, max, integer, normal_*/danger_*, danger_values, computed, readonly, group: specialty|context, source, value, is_danger, is_abnormal, previous, risk_gt?}]}`.
- The PATCH body accepts `pack: {code: value}`. Each value is checked against the definitions: code allowed for this visit, not computed, number within bounds and an integer where required, select value in options, yes/no as a boolean. A failure returns 422 `pack.<code>`, and nothing is written. `null` clears a code.
- Optional `?clinical_type=` and `?context=inpatient|emergency|none` override what the encounter implies. The FE sends them only when the host sets them.
- The core whitelist and sign lock are unchanged. Core and pack are written in one DB transaction.

## Files
BE (`/home/moonui3/moon-erp-be`):
- new `Modules/Clinic/app/Support/VitalsPacks.php` (codes per type/context, bounds, computed map)
- new `Modules/Clinic/app/Services/VitalsPackService.php` (resolve / payload / validate / write)
- `Modules/Clinic/app/Http/Controllers/VitalsController.php` (show/upsert/payload + `pack`)
- `Modules/Clinic/app/Http/Requests/UpdateVitalsRequest.php` (`pack`, `clinical_type`, `context` keys)
- `Modules/Clinic/app/Support/ComputedRegistry.php` (new `whr` key; small additive edit to a shared file)
- `Modules/Clinic/database/seeders/ClinicObservationDefinitionSeeder.php` (+ `nut_whr` computed, + `gyn_urine_albumin` select with options; still create-only)
- new migration `Modules/Clinic/database/migrations/2026_11_20_330001_seed_vitals_pack_definitions.php` (re-runs that seeder so existing installs get the new definitions)
- `Modules/Clinic/lang/{ar,en}/observations.php` (4 pack validation messages)
- new test `Modules/Clinic/tests/Feature/W2fs3VitalsPacksTest.php`
- No route changes: the existing vitals routes are reused.

FE (`/home/moonui3/public_html/moon-erp/src`):
- `app/features/clinic/services/clinic-vitals.service.ts` (pack types; `get/upsert` take `pack` + scope)
- `app/features/clinic/encounter/vitals-panel/vitals-panel.component.{ts,html,scss}` (packs, modes, `openCard` output, `clinicalType`/`context`/`mode` inputs)
- `app/features/hospital/nursing/vital-round.component.{ts,html}` (I/O inputs, column, 24 h balance; column grid set in component `styles`, shared nursing.scss untouched)
- `app/features/clinic/visit-shell/visit-shell.component.html`: **one edit** adding the bindings `[clinicalType]="type()" [context]="context()?.key ?? null" [mode]="c.props?.['mode'] ?? 'full'" (openCard)="jump($event)"` (W1-FE-1's file; already committed in 10d02f96)
- `assets/i18n/{ar,en}.json`: CLINIC_VITALS.PACK_* / WHR_RISK_HINT / IO_BALANCE / EXPAND / COLLAPSE / COMPACT_EMPTY, HOSPITAL.VITAL.intake_ml/output_ml, HOSPITAL.NURSE.IO_24H/IO. I18N_VERSION is not bumped.

## Dev DB
- `artisan migrate --force` ran `2026_11_20_330001_seed_vitals_pack_definitions` (DONE). Company 1 now has `nut_whr` (computed, meta risk_gt) and `gyn_urine_albumin` (single_select).
- I ran `optimize:clear` and `dump-autoload` (ea-php82 CLI), and chowned the BE files to moonui3.
- On real dev MySQL encounters (nutrition 43, gyn_ob 62, inpatient 27), I ran payload, validate and write inside a transaction that was **rolled back**. I checked afterwards that 0 rows were left. Results:
  - waist 90 / hip 100 → WHR 0.9, flagged.
  - urine 2+ → danger.
  - The inpatient pack resolved correctly.

## Tests (run)
- `Modules/Clinic/tests/Feature/W2fs3VitalsPacksTest.php`: **15 passed (152 assertions)**. Covers the seeder, the whr registry key, the pack per type/context, the overrides plus 422 on a bad context, a deactivated definition, the WHR store and gender flag, NEWS2 scoring and re-scoring, NEWS2 not stamped outpatient, validation (foreign code, computed code, range, integer, option, non-array, extra key, nothing written on 422), the sign lock, GYN urine/FHR with LMP not writable, and LMP + GA read from a real booked pregnancy.
- Regression: VitalsTest + VitalsFoldInTest + ClinicVitalsFieldEngineTest: 32 passed. D1ObservationsTest: 12 passed. Hospital D4VitalRoundsObservationsTest: 8 passed.
- VitalsFoldInTest fails when run **alone** because it uses a global helper defined in VitalsTest.php. That is a pre-existing problem, and it passes when the two files run together.

## Could not verify
- No browser check of the rendered card, compact mode or the round I/O, because there is no deploy. The `ng build` is green and has no warnings in my files.
- The «افتح الحمل» link only works when `gyn_pregnancy` is in the visit's layout, since `jump()` ignores cards that are not there.

## Decisions / deviations
- `pack` is a **nested object** in the PATCH body rather than flat keys, so the core whitelist stays strict and each pack code is checked against its definition.
- The context pack also applies to **emergency** encounters. This matches the seeder's existing `show_if` (inpatient, emergency); day case maps to inpatient, as in the FE context.
- LMP and GA are **read-only**. LMP is a record answer on the pregnancy episode, and writing it from the vitals card would have meant creating or editing the pregnancy. The field links to the pregnancy card instead.
- The I/O balance is computed client-side: the panel uses intake − output for the visit, and the round uses the 24 h sum of rows. Neither the BE vitals nor the nursing chart returns an aggregate.
- CHANGELOG (`docs/moonstack/CHANGELOG.md`) is not edited, because it is a shared file. Suggested bullet: «العلامات الحيوية: باقة التخصص (تغذية: خصر/ورك/نسبة · نسا: LMP/عمر الحمل/قاع الرحم/نبض الجنين/زلال) وباقة الإقامة (تنفس/وعي/ألم/أكسجين/سوائل/NEWS2) داخل نفس الكارت؛ وجولة التمريض فيها السوائل الداخلة/الخارجة والرصيد.»
