medOS ultra

Perioperative Status Kit

Auto-calc + severity-badge kit for the perioperative nursing record: NPO, duration, Apgar, sign-in completeness.

6 min read diagramsUpdated 2026-05-25docs/architecture/perioperative-status-kit.md

Status: Live Module: web/packages/periops-kit/src/operating-room/components/list-or-patient/table-data/dialog/perioperative-status/ Form: FM-OPR01-007 (แบบบันทึกการพยาบาลผู้ป่วยผ่าตัด · Perioperative Nursing Record) Surfaces: Sign-In dialog · Time-Out (Intraoperative) dialog · Time-Stamp dialog · FM-OPR01-007 print template

1. What it is

A shared hook + badge kit that auto-derives four clinical safety signals from the perioperative form state and surfaces them next to the relevant inputs (and on the printed record) with severity-coloured icon badges. It replaces eyeballing checklists / clock arithmetic with at-a-glance, role-readable status indicators.

Four signals, one tick clock, three surfaces (live form · time-stamp dialog · printed FM-OPR01-007 PDF).

2. The four signals

Hook Input Output Banding
useNpoStatus(active, npoAt) NPO checkbox + start time { hours, severity, label } ok ≤ 12 h · warn 12–24 h · danger > 24 h
useDurationTicker(start, end?, warn=180m, danger=300m) Anaesthesia / Procedure / any time range { minutes, label, running, severity } ok < 3 h · warn 3–5 h · danger > 5 h · ticks while end is null
useApgarTotal(rawScore) Apgar field (string or number) { score, severity, label } ok 7–10 · warn 4–6 · danger 0–3 · idle when blank
useSignInCompleteness(preop) Whole Sign-In checklist { doneCount, total, missingCritical[], severity, percent } derives 16 fields · escalates to danger when any critical is missing OR NPO > 24 h

3. The shared clock

All four hooks share one module-level setInterval(30_000) via useNow.ts:

┌─────────────────────────────────────────────────────────────┐
│ useNow.ts                                                   │
│                                                             │
│   const subscribers = new Set<() => void>()                 │
│   let timer: ReturnType<typeof setInterval> | null = null   │
│                                                             │
│   ensureTimer()  ← starts the 30 s tick on first subscribe  │
│   teardownIfIdle() ← stops the timer when last unmounts     │
└─────────────────────────────────────────────────────────────┘
              │ broadcasts now=Date.now() every 30s
              ▼
┌──────────────┬───────────────┬───────────────────────────────┐
│ useNpoStatus │ useDuration   │ useSignInCompleteness         │
│              │ Ticker        │  (consumes useNpoStatus)      │
└──────────────┴───────────────┴───────────────────────────────┘
              │ all badges re-render on the same tick
              ▼
        NPO clock · live procedure ticker · completeness ring
        (lock-step; zero drift between them)

This is the sync guarantee — turning the OR clock 30 s forward updates every badge in the same React commit, so the NPO clock can’t disagree with the procedure ticker. No per-component intervals; no fanout drift.

4. The two visual components

``

Chip-style severity indicator. Icon auto-selected from severity (success / warning / error / hourglass) or override with icon="clock". Palette comes from MUI theme so it follows light/dark mode. Used inline in form rows and as endAdornment on text fields.

``

SVG circular-progress ring around a percentage. Stroke color follows status.severity. Hover tooltip lists every missing-critical field. Used in the FM-OPR01-007 header banner.

5. Where it’s wired

Surface Location What it shows
Sign-In header banner PreoperativeChecklistReview.tsx next to FM-OPR01-007 CompletenessRing with 12/16 (75%) + tooltip listing missing items
Sign-In NPO row PreoperativeChecklistReview.tsx above the NPO checkbox StatusBadge showing NPO 14.2 h with clock icon
Intraoperative Apgar field Implant.tsx (C/S section), Apgar Score endAdornment StatusBadge showing Apgar 7/10 in severity colour
Time-Stamp dialog DialogTimeStamp.tsx, next to Anest Time + Procedure Time result fields StatusBadge showing live 1h 23m once start is set; disappears when finish is recorded
Print template (FM-OPR01-007) PerioperativeNursingRecordPrint.tsx SIGN IN row prints Completeness 12/16 (75%) ⚠ · NPO chip · severity-coloured Apgar · “· 2h 15m” suffix on completed ranges
Print trigger DialogSignIn.tsx footer พิมพ์ FM-OPR01-007 button between draft-save and final-save
Save guard DialogSignIn.tsx save button useConfirmation modal lists every missing critical before allowing submit; [กลับไปแก้ไข] [บันทึกต่อ]

The print template uses the same hooks, so what prints == what’s on screen.

6. The Sign-In save guard

Wraps handleSubmit in guardSignInSubmit:

const guardSignInSubmit = () => {
  setIsError(true);
  if (state.page === 'sign-in' && signInCompleteness.missingCritical.length > 0) {
    confirm({
      title: 'รายการ Sign-In ยังไม่ครบ',
      description: `ก่อนยืนยันบันทึก กรุณาตรวจสอบรายการต่อไปนี้:\n• ${missing.join('\n• ')}`,
      leftLabel: 'กลับไปแก้ไข',
      rightLabel: 'บันทึกต่อ',
      onSubmit: () => handleSubmit(),
    });
    return;
  }
  handleSubmit();
};

Critical fields (any one missing → guard fires):

  • Patient ID confirmed
  • Site verified
  • Allergies verified
  • Consent verified
  • Procedure verified
  • NPO time + npoAt date set
  • Synthetic: NPO > 24 h (from useNpoStatus)

Non-critical fields contribute to the X/Y count but don’t trigger the guard. The autosave path (onAutoUpdate) is unaffected so drafts keep saving silently.

7. Severity colours

Pulled from MUI palette so they follow theme mode:

Severity Light Dark Meaning
ok palette.success.main #2e7d32 brighter green within safe operating range
warn palette.warning.main #ed6c02 amber escalate / flag to anaesthetic team
danger palette.error.main #d32f2f crimson immediate clinical attention required
idle palette.text.disabled #9e9e9e grey input not present yet

Print template uses a small inline SEVERITY_COLOR map (no ThemeProvider in the print window) — same hex values.

8. Verification — sandbox Playwright smoke

Target: web/sandbox/targets/PerioperativeStatusKitTarget.tsx Spec: web/e2e/perioperative-status-kit.sandbox.spec.ts

pnpm e2e:sandbox -- perioperative-status-kit
# 5 tests, 5 passed (~4 s)
#   ✓ all four hooks render without crashing
#   ✓ NPO severity escalates by hours        (6h ok · 18h warn · 30h danger)
#   ✓ Apgar score banding 9 / 5 / 2 / null
#   ✓ Duration ticker + running-vs-stopped
#   ✓ Completeness ring lists missing criticals

The target mounts every hook + visual with deterministic synthetic data — useful both as a regression smoke and as a visual reference for designers / clinicians.

9. File inventory

File Purpose
web/packages/periops-kit/src/operating-room/components/list-or-patient/table-data/dialog/perioperative-status/useNow.ts Shared 30 s ticker (module-level subscribe pattern)
…/perioperative-status/useNpoStatus.ts NPO hours + severity
…/perioperative-status/useDurationTicker.ts Live elapsed ticker for any start/end range
…/perioperative-status/useApgarTotal.ts Apgar parse + severity
…/perioperative-status/useSignInCompleteness.ts Sign-In done/total + missing-critical aggregator
…/perioperative-status/StatusBadge.tsx Severity-coloured chip with icon + tooltip
…/perioperative-status/CompletenessRing.tsx SVG progress ring with tooltip
…/perioperative-status/index.ts Barrel
…/sign-in/PreoperativeChecklistReview.tsx Wires CompletenessRing + NPO badge
…/time-out/Implant.tsx Wires Apgar badge
…/time-stamp/DialogTimeStamp.tsx Wires live duration badges
…/print-perioperative-record/PerioperativeNursingRecordPrint.tsx Print template (consumes same hooks)
…/DialogSignIn.tsx Save guard + Print FM-OPR01-007 button
web/sandbox/targets/PerioperativeStatusKitTarget.tsx Sandbox harness
web/e2e/perioperative-status-kit.sandbox.spec.ts Playwright smoke (5 cases)

10. Extending to other modules

The kit is module-agnostic; only useSignInCompleteness is Sign-In-specific. To reuse elsewhere:

  • Recovery (PACU) NPO post-op: drop useNpoStatus into PACU vitals form with a different threshold table
  • Labour-ward partogram: use useDurationTicker for first-stage / second-stage labour ranges with obstetric-specific thresholds
  • Imaging contrast wait time: useDurationTicker(injectedAt, scanStart) with contrast-specific bands
  • Any checklist surface: write a thin useXyzCompleteness(form) that picks fields + critical set, reuse + as-is

To add a new severity-banded scalar (e.g. blood loss, transfused units), the pattern is useMemo(() => severity-classify-the-number) + `` — about 30 lines.

11. Out of scope / future work

  • CDS hook: feed NPO > 24 h or Apgar ≤ 3 into the cds_rules engine so they raise a CdsAlertSurface toast / inline badge (see cds-vital-signs-rules.md). Currently visual-only; no backend alert.
  • Backend persistence: the calculated values are derived on the client every render. If we ever need them queryable (analytics, FHIR write-back), persist apgarSeverity, npoHours, procedureDurationMinutes as denormalised columns on the OR request.
  • Acknowledgement integration: when useSignInCompleteness returns severity === 'danger', optionally raise an AcknowledgementRequest to the anaesthesia team (see acknowledgement-system.md). Currently the save guard is the only gate.
Ask Anything