Ports the vanilla ROS / Physical Exam / ICD-10 diagnosis cards from
public/js/shadess.js (@be14578) — renderRosRows, wireRosContainer,
renderDxComponent — which the earlier React port had collapsed into
plain-text textareas.
Clinical data → shared/clinical/ros-pe-dx.ts:
• ROS_SYSTEMS (15 systems, each with label + detail hint)
• PE_SYSTEMS (17 systems)
• COMMON_DX (28 pediatric quick-pick ICD-10 codes)
• formatRosForAI / formatDxForAI — byte-identical to vanilla so
the server-side prompt context is unchanged.
Tests: ros-pe-dx.test.ts asserts the table counts, ordering, and
format strings — catches the class of bug where an LLM silently
drops an entry or re-orders the clinical reference during a port.
New React components:
client/src/components/RosPeTable.tsx — tri-state row (WNL/Abnormal/
Not reviewed) with auto-revealed note field on Abnormal.
rosAllWnl / rosClear helpers mirror the "All WNL" / "Clear"
buttons from vanilla. Accepts a btnLabels prop so the same
component renders ROS ("WNL"/"Not reviewed") and PE ("Normal"/
"Not examined").
client/src/components/DxPicker.tsx — ICD-10 live search via NLM
Clinical Tables API (free, no auth, CORS-OK) with 280ms debounce
+ AbortController; chip-style selected tags with × remove;
28-entry common-diagnosis quick-pick grid. Enter key adds the
top result.
Wired into WellVisit / VisitNote.tsx and SickVisit.tsx:
• Submit now sends ros / physicalExam / diagnoses as formatted
strings to /api/well-visit/note and /api/sick-visit/note.
• State persists through the Save/Load encounter flow via
partialData → the rosData/peData/diagnoses round-trip.
130 lines
7.4 KiB
TypeScript
130 lines
7.4 KiB
TypeScript
// ============================================================
|
|
// ROS / PE systems + common ICD-10 diagnoses + AI formatters.
|
|
// Ported verbatim from public/js/shadess.js (@be14578):
|
|
// ROS_SYSTEMS — 15 systems
|
|
// PE_SYSTEMS — 17 systems
|
|
// COMMON_DX — 28 pediatric diagnoses quick-pick
|
|
// formatRosForAI / formatDxForAI — string format the AI receives.
|
|
// These tables are clinical reference data — do NOT re-order or
|
|
// silently "simplify" them during maintenance. Copy from the vanilla
|
|
// source when it changes.
|
|
// ============================================================
|
|
|
|
export interface SystemEntry {
|
|
key: string;
|
|
label: string;
|
|
detail: string;
|
|
}
|
|
|
|
export const ROS_SYSTEMS: ReadonlyArray<SystemEntry> = [
|
|
{ key: 'constitutional', label: 'Constitutional', detail: 'fever, fatigue, weight loss/gain, appetite' },
|
|
{ key: 'eyes', label: 'Eyes', detail: 'vision changes, discharge, redness' },
|
|
{ key: 'ent', label: 'ENT', detail: 'ear pain, congestion, sore throat, mouth sores' },
|
|
{ key: 'cardiovascular', label: 'Cardiovascular', detail: 'chest pain, palpitations, murmur' },
|
|
{ key: 'respiratory', label: 'Respiratory', detail: 'cough, wheeze, dyspnea' },
|
|
{ key: 'gastrointestinal', label: 'Gastrointestinal', detail: 'nausea, vomiting, diarrhea, constipation, abdominal pain' },
|
|
{ key: 'genitourinary', label: 'Genitourinary', detail: 'dysuria, frequency, discharge, menstrual' },
|
|
{ key: 'musculoskeletal', label: 'Musculoskeletal', detail: 'joint pain, swelling, gait issues, back pain' },
|
|
{ key: 'skin', label: 'Skin', detail: 'rash, lesions, itch, hair/nail changes' },
|
|
{ key: 'neurological', label: 'Neurological', detail: 'headache, dizziness, seizures, numbness' },
|
|
{ key: 'psychiatric', label: 'Psychiatric', detail: 'mood, anxiety, sleep, behavior changes' },
|
|
{ key: 'endocrine', label: 'Endocrine', detail: 'polyuria/polydipsia, heat/cold intolerance, growth concerns' },
|
|
{ key: 'hematologic', label: 'Hematologic/Lymphatic', detail: 'bruising, bleeding, lymph nodes' },
|
|
{ key: 'allergic', label: 'Allergic/Immunologic', detail: 'allergic reactions, frequent infections' },
|
|
{ key: 'developmental', label: 'Developmental/Behavioral', detail: 'milestones, school, social' },
|
|
];
|
|
|
|
export const PE_SYSTEMS: ReadonlyArray<SystemEntry> = [
|
|
{ key: 'general', label: 'General Appearance', detail: 'WDWN, alert, NAD, etc.' },
|
|
{ key: 'peEyes', label: 'Eyes', detail: 'PERRL, conjunctiva, EOM' },
|
|
{ key: 'ears', label: 'Ears', detail: 'TMs, canals, hearing' },
|
|
{ key: 'nose', label: 'Nose', detail: 'mucosa, septum, turbinates' },
|
|
{ key: 'mouthThroat', label: 'Mouth/Throat', detail: 'lips, gums, teeth, tonsils, pharynx' },
|
|
{ key: 'neck', label: 'Neck', detail: 'lymph nodes, thyroid, masses' },
|
|
{ key: 'chestLungs', label: 'Chest/Lungs', detail: 'air entry, wheezing, retractions' },
|
|
{ key: 'peCv', label: 'Cardiovascular', detail: 'S1/S2, murmur, pulses, cap refill' },
|
|
{ key: 'abdomen', label: 'Abdomen', detail: 'soft, tenderness, organomegaly, BS' },
|
|
{ key: 'guTanner', label: 'Genitourinary/Tanner', detail: 'Tanner stage, genitalia, hernias' },
|
|
{ key: 'msk', label: 'Musculoskeletal', detail: 'strength, ROM, gait, spine' },
|
|
{ key: 'extremities', label: 'Extremities', detail: 'tone, reflexes, clubbing, edema' },
|
|
{ key: 'peSkin', label: 'Skin', detail: 'rashes, lesions, cafe au lait, bruises' },
|
|
{ key: 'peNeuro', label: 'Neurological', detail: 'CN intact, DTRs, coordination' },
|
|
{ key: 'lymphatic', label: 'Lymphatic', detail: 'cervical, axillary, inguinal nodes' },
|
|
{ key: 'breast', label: 'Breast', detail: 'if indicated by age/Tanner' },
|
|
{ key: 'pePsych', label: 'Psychiatric', detail: 'affect, mood, behavior during exam' },
|
|
];
|
|
|
|
export interface DxEntry { code: string; name: string }
|
|
|
|
export const COMMON_DX: ReadonlyArray<DxEntry> = [
|
|
{ code: 'Z00.129', name: 'Routine child health exam, no abnormal findings' },
|
|
{ code: 'Z00.121', name: 'Routine child health exam, with abnormal findings' },
|
|
{ code: 'J06.9', name: 'Upper Respiratory Infection, NOS' },
|
|
{ code: 'J02.9', name: 'Acute pharyngitis, NOS' },
|
|
{ code: 'J03.90', name: 'Acute tonsillitis, NOS' },
|
|
{ code: 'H66.90', name: 'Otitis media, NOS' },
|
|
{ code: 'J20.9', name: 'Acute bronchitis' },
|
|
{ code: 'J45.20', name: 'Mild intermittent asthma, uncomplicated' },
|
|
{ code: 'J45.30', name: 'Mild persistent asthma' },
|
|
{ code: 'A09', name: 'Gastroenteritis/colitis' },
|
|
{ code: 'L20.9', name: 'Atopic dermatitis, NOS' },
|
|
{ code: 'L30.9', name: 'Dermatitis NOS' },
|
|
{ code: 'R50.9', name: 'Fever NOS' },
|
|
{ code: 'R10.9', name: 'Abdominal pain NOS' },
|
|
{ code: 'R05.9', name: 'Cough' },
|
|
{ code: 'B34.9', name: 'Viral infection NOS' },
|
|
{ code: 'Z23', name: 'Immunization encounter' },
|
|
{ code: 'Z41.9', name: 'Procedure for non-remedial reason' },
|
|
{ code: 'K21.0', name: 'GERD with esophagitis' },
|
|
{ code: 'K21.9', name: 'GERD without esophagitis' },
|
|
{ code: 'F90.0', name: 'ADHD, predominantly inattentive' },
|
|
{ code: 'F90.2', name: 'ADHD, combined type' },
|
|
{ code: 'F41.1', name: 'Generalized anxiety disorder' },
|
|
{ code: 'F32.9', name: 'Major depressive episode NOS' },
|
|
{ code: 'E11.9', name: 'Type 2 diabetes' },
|
|
{ code: 'E03.9', name: 'Hypothyroidism NOS' },
|
|
{ code: 'M54.5', name: 'Low back pain' },
|
|
{ code: 'G43.909', name: 'Migraine NOS' },
|
|
];
|
|
|
|
// Per-system status — undefined/'' = Not Reviewed → omit when formatted.
|
|
export type RosStatus = '' | 'wnl' | 'abnormal' | 'notrev';
|
|
|
|
export interface RosCell { status?: RosStatus; note?: string }
|
|
export type RosData = Record<string, RosCell>;
|
|
|
|
// Format ROS/PE for the AI. Mirrors window.formatRosForAI exactly —
|
|
// tells the AI which systems to expand with pertinent negatives vs
|
|
// which to treat as abnormal findings to describe.
|
|
export function formatRosForAI(
|
|
systems: ReadonlyArray<SystemEntry>,
|
|
data: RosData,
|
|
heading: string,
|
|
): string {
|
|
const lines: string[] = [heading + ':'];
|
|
let hasAny = false;
|
|
for (const sys of systems) {
|
|
const cell = data[sys.key] || {};
|
|
if (!cell.status) continue;
|
|
hasAny = true;
|
|
if (cell.status === 'wnl') {
|
|
lines.push(' ' + sys.label + ': NORMAL [domain: ' + sys.detail + ']');
|
|
} else if (cell.status === 'abnormal') {
|
|
lines.push(' ' + sys.label + ': ABNORMAL' + (cell.note ? ' — ' + cell.note : '') + ' [domain: ' + sys.detail + ']');
|
|
} else {
|
|
lines.push(' ' + sys.label + ': Not reviewed');
|
|
}
|
|
}
|
|
return hasAny ? lines.join('\n') : '';
|
|
}
|
|
|
|
// Format diagnoses list for the AI. Mirrors window.formatDxForAI.
|
|
export function formatDxForAI(dx: ReadonlyArray<DxEntry>, freetext?: string): string {
|
|
const lines: string[] = [];
|
|
for (const d of dx) {
|
|
lines.push((d.code ? d.code + ' ' : '') + d.name);
|
|
}
|
|
const t = (freetext || '').trim();
|
|
if (t) lines.push(t);
|
|
return lines.length ? 'Diagnoses:\n' + lines.map((l) => ' - ' + l).join('\n') : '';
|
|
}
|