pediatric-ai-scribe-v3/shared/clinical/visit-status.ts
Daniel 8253b34dc5 feat(wellvisit): port the 4 sub-tabs vanilla had — By Visit / Milestones / SSHADESS / Note
Earlier WellVisit was a single-pane skeleton — the comment in that
file even admitted "Milestones and SSHADESS sub-tabs land in a
follow-up". This commit lands them, faithful to public/components/
wellvisit.html and the three modules behind it (wellVisit.js,
milestones.js, shadess.js, all @be14578).

WellVisit.tsx is now an 83-line shell that lazy-loads four panels:

  client/src/pages/wellvisit/
    ByVisitAge.tsx  — per-visit AAP Bright Futures recommendations
                       (billing codes, measurements, vaccines,
                       sensory/dev/proc/oral screens, growth +
                       feeding, expected reflexes, BMI table,
                       notes). Status buttons mirror vanilla
                       (Given/Refused/Deferred/Already-Done for
                       vaccines; Done/Refused/N-A for screens).
                       Statuses persist to localStorage under
                       ped_visit_statuses (same key as vanilla).
    Milestones.tsx  — checklist per age group, three-state toggle
                       (✓/✗/blank=not assessed), All-Yes/Clear,
                       Generate → /api/generate-milestone-narrative,
                       3-sentence summary → /api/generate-milestone-
                       summary, Copy-to-Note bridge.
    Shadess.tsx     — 8 SSHADESS domains verbatim from shadess.js
                       (Strengths, School, Home, Activities, Drugs,
                       Emotions/Eating, Sexuality, Safety) with
                       concern_if auto-flag, skip toggle, manual
                       concern toggle, optional Listen-In recorder,
                       Generate → /api/well-visit/shadess.
    VisitNote.tsx   — pre-existing note generator + carry-over
                       pickup from sessionStorage so the user can
                       flow Milestones → SSHADESS → Note without
                       retyping. Now also includes the recorder.

Server: GET /api/schedule-data now also returns wellVisitCodes,
growthReference, reflexesReference, and bmiClassification so the
React side has everything it needs without a second round trip.

Tests:
  shared/clinical/visit-status.ts (+ .test.ts) — pure helpers
  for the visitId → growth/reflex key mapping (the vanilla
  tables collapse 6y/7y/8y/9y/10y onto one window, etc.) and
  the reflex-status color rules. Lives in shared/ so the root
  vitest config picks it up; ByVisitAge.tsx imports from there.
2026-04-24 05:24:09 +02:00

80 lines
3.8 KiB
TypeScript

// ============================================================
// Visit-status helpers — pure functions used by the WellVisit
// "By Visit Age" pane to map AAP visit IDs onto growth/reflex
// reference keys, color-code reflex chips, and read tri-state
// status values out of the localStorage cache.
//
// Mirrors public/js/wellVisit.js (@be14578) getGrowthDataForVisit /
// getReflexDataForVisit / reflexStatusColor. Lives in shared/ so
// the root vitest config can pick the tests up.
// ============================================================
export type VisitStatusVal = string | { status: string; note: string };
// Map a visit ID (e.g. "7y") to its GROWTH_REFERENCE key (e.g. "6y_to_10y").
// Returns null when no match — caller should hide the growth section.
export function mapToGrowthKey<T>(visitId: string, refs: Record<string, T>): string | null {
if (refs[visitId]) return visitId;
const m: Record<string, string> = {
'newborn': 'newborn', '3-5d': 'newborn',
'1mo': '1mo', '2mo': '2mo', '4mo': '4mo', '6mo': '6mo', '9mo': '9mo',
'12mo': '12mo', '15mo': '15mo', '18mo': '18mo',
'24mo': '24mo', '30mo': '30mo',
'3y': '3y', '4y': '4y', '5y': '5y',
'6y': '6y_to_10y', '7y': '6y_to_10y', '8y': '6y_to_10y', '9y': '6y_to_10y', '10y': '6y_to_10y',
'11y': '11y_to_14y', '12y': '11y_to_14y', '13y': '11y_to_14y', '14y': '11y_to_14y',
'15y': '15y_to_21y', '16y': '15y_to_21y', '17y': '15y_to_21y', '18y': '15y_to_21y',
'19y': '15y_to_21y', '20y': '15y_to_21y', '21y': '15y_to_21y',
};
const k = m[visitId];
return k && refs[k] ? k : null;
}
// REFLEXES_REFERENCE shares 3y..5y, not split by year — see vanilla.
export function mapToReflexKey<T>(visitId: string, refs: Record<string, T>): string | null {
if (refs[visitId]) return visitId;
const m: Record<string, string> = {
'newborn': 'newborn', '3-5d': 'newborn',
'1mo': '1mo', '2mo': '2mo', '4mo': '4mo', '6mo': '6mo', '9mo': '9mo',
'12mo': '12mo', '15mo': '15mo', '18mo': '18mo',
'24mo': '24mo', '30mo': '30mo',
'3y': '3y_to_5y', '4y': '3y_to_5y', '5y': '3y_to_5y',
'6y': '6y_to_10y', '7y': '6y_to_10y', '8y': '6y_to_10y', '9y': '6y_to_10y', '10y': '6y_to_10y',
'11y': '11y_to_14y', '12y': '11y_to_14y', '13y': '11y_to_14y', '14y': '11y_to_14y',
'15y': '15y_to_21y', '16y': '15y_to_21y', '17y': '15y_to_21y', '18y': '15y_to_21y',
'19y': '15y_to_21y', '20y': '15y_to_21y', '21y': '15y_to_21y',
};
const k = m[visitId];
return k && refs[k] ? k : null;
}
// Color a reflex status chip. Greens = expected resolved/lifelong state,
// blues = present and expected, purple = emerging, ambers = transitional,
// slate = unspecified. Mirrors reflexStatusColor() exactly.
export function reflexStatusColor(status: string): string {
const s = (status || '').toLowerCase();
if (s.includes('absent / integrated') || s === 'absent' || s.includes('integrated')) return '#059669';
if (s.includes('present') && s.includes('lifelong')) return '#059669';
if (s.includes('present')) return '#2563eb';
if (s.includes('peak')) return '#2563eb';
if (s.includes('emerging')) return '#7c3aed';
if (s.includes('fading') || s.includes('transition')) return '#d97706';
if (s.includes('down-going')) return '#059669';
if (s.includes('up-going')) return '#d97706';
if (s.includes('2+')) return '#2563eb';
return '#475569';
}
// A visit-status value can be a bare string (legacy) or { status, note }.
export function getStatusValue(s: VisitStatusVal | undefined): string {
if (!s) return '';
return typeof s === 'object' ? s.status : s;
}
// SSHADESS sub-tab visibility — vanilla shows it only for these visits.
export const SSHADESS_VISITS: ReadonlyArray<string> = [
'12y','13y','14y','15y','16y','17y','18y','19y','20y','21y',
];
export function isSshadessVisit(visitId: string): boolean {
return SSHADESS_VISITS.includes(visitId);
}