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.
97 lines
3.9 KiB
TypeScript
97 lines
3.9 KiB
TypeScript
// ============================================================
|
|
// RosPeTable — structured ROS or PE input. Per-system row with
|
|
// WNL / Abnormal / Not reviewed toggle + a note field that reveals
|
|
// when "Abnormal" is selected. Mirrors window.renderRosRows /
|
|
// wireRosContainer from public/js/shadess.js (@be14578).
|
|
//
|
|
// Consumers (WellVisit Note, Sick Visit Note) pass systems list +
|
|
// data object + label set so the same component renders both
|
|
// review-of-systems and physical-exam tables.
|
|
// ============================================================
|
|
|
|
import type { RosData, RosStatus, SystemEntry } from '@shared/clinical/ros-pe-dx';
|
|
|
|
interface BtnLabels { wnl: string; abnormal: string; notrev: string }
|
|
|
|
interface Props {
|
|
systems: ReadonlyArray<SystemEntry>;
|
|
data: RosData;
|
|
onChange: (next: RosData) => void;
|
|
btnLabels?: BtnLabels;
|
|
testIdPrefix?: string;
|
|
}
|
|
|
|
export function rosAllWnl(systems: ReadonlyArray<SystemEntry>, data: RosData): RosData {
|
|
const next: RosData = { ...data };
|
|
for (const s of systems) next[s.key] = { status: 'wnl', note: next[s.key]?.note };
|
|
return next;
|
|
}
|
|
export function rosClear(data: RosData, systems: ReadonlyArray<SystemEntry>): RosData {
|
|
const next: RosData = { ...data };
|
|
for (const s of systems) next[s.key] = {};
|
|
return next;
|
|
}
|
|
|
|
const DEFAULT_LABELS: BtnLabels = { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' };
|
|
|
|
function statusClass(active: boolean, kind: RosStatus) {
|
|
if (!active) return 'bg-background border-border hover:bg-muted';
|
|
if (kind === 'wnl') return 'bg-green-100 text-green-800 border-green-300';
|
|
if (kind === 'abnormal') return 'bg-red-100 text-red-800 border-red-300';
|
|
return 'bg-muted text-muted-foreground border-border';
|
|
}
|
|
|
|
export default function RosPeTable({
|
|
systems, data, onChange, btnLabels, testIdPrefix = 'ros',
|
|
}: Props) {
|
|
const labels = btnLabels || DEFAULT_LABELS;
|
|
|
|
function setStatus(key: string, status: RosStatus) {
|
|
const cur = data[key]?.status || '';
|
|
const nextStatus: RosStatus = cur === status ? '' : status;
|
|
onChange({ ...data, [key]: { status: nextStatus, note: data[key]?.note || '' } });
|
|
}
|
|
function setNote(key: string, note: string) {
|
|
onChange({ ...data, [key]: { status: data[key]?.status || '', note } });
|
|
}
|
|
|
|
return (
|
|
<div className="divide-y divide-border" data-testid={testIdPrefix + '-table'}>
|
|
{systems.map((sys) => {
|
|
const cell = data[sys.key] || {};
|
|
const active = cell.status || '';
|
|
return (
|
|
<div key={sys.key} className="flex flex-wrap items-center gap-2 px-2 py-1.5" data-testid={testIdPrefix + '-row-' + sys.key}>
|
|
<div className="flex-1 min-w-[180px]" title={sys.detail}>
|
|
<span className="text-sm font-medium">{sys.label}</span>
|
|
<span className="ml-1 text-[10px] text-muted-foreground">({sys.detail})</span>
|
|
</div>
|
|
<div className="flex gap-1 shrink-0">
|
|
{(['wnl', 'abnormal', 'notrev'] as const).map((kind) => (
|
|
<button
|
|
key={kind}
|
|
type="button"
|
|
onClick={() => setStatus(sys.key, kind)}
|
|
className={'text-[10px] uppercase tracking-wider px-2 py-1 rounded border ' + statusClass(active === kind, kind)}
|
|
data-testid={testIdPrefix + '-btn-' + sys.key + '-' + kind}
|
|
>
|
|
{labels[kind]}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{active === 'abnormal' && (
|
|
<input
|
|
type="text"
|
|
value={cell.note || ''}
|
|
onChange={(e) => setNote(sys.key, e.target.value)}
|
|
placeholder="Describe finding…"
|
|
className="flex-1 min-w-[200px] rounded-md border border-input bg-background px-2 py-1 text-xs"
|
|
data-testid={testIdPrefix + '-note-' + sys.key}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|