From bdd1b6ec3e7740d12a0221e67c59e6013596a7eb Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 24 Apr 2026 05:38:13 +0200 Subject: [PATCH] feat(notes): structured HospitalCourse + ChartReview match vanilla MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both pages were partial ports — HospitalCourse had a single "notes textarea separated by blank lines" instead of the dynamic note cards vanilla shipped, and ChartReview lacked per-visit type / specialist / specialty fields plus the Additional Labs block. These commits close the gap. HospitalCourse (ports public/components/hospital.html + public/js/hospitalCourse.js @be14578): client/src/pages/hospital/DictatableCard.tsx — reusable card (title + date + meta children + content + per-card Recorder). Each note gets its own recorder so dictating into one card doesn't interrupt another in progress. client/src/pages/hospital/LabsList.tsx — dynamic (date, values) rows with add/remove. client/src/pages/hospital/ClarifyButton.tsx — "What's Missing?" → POST /api/hospital-course-clarify, renders the returned questions inline. client/src/pages/HospitalCourse.tsx — rewritten: ED Note card (date + ED labs + content + dictate), H&P card (date + content + dictate), Progress Notes as dynamic cards (date + type select matching vanilla's 6 options + content + per-card dictate + remove), separate Labs list, instructions, EditableResult output, ClarifyButton. Save/Load round-trips the entire structured note-set via JSON in the transcript column. ChartReview (ports public/components/chart.html + public/js/chartReview.js @be14578): client/src/pages/ChartReview.tsx — rewritten: each visit now has its own date + visit-type select (outpatient/subspecialty/ed), and when subspecialty is selected the specialist-name + specialty fields appear inline. Per-visit labs textarea. New Additional Labs block (reuses hospital/LabsList) for labs not tied to a visit. Submit splits visits by type into the server's visits / subspecialty / edVisits arrays, matching src/routes/chartReview.ts. --- client/src/pages/ChartReview.tsx | 156 +++++++---- client/src/pages/HospitalCourse.tsx | 257 ++++++++++++++----- client/src/pages/hospital/ClarifyButton.tsx | 54 ++++ client/src/pages/hospital/DictatableCard.tsx | 73 ++++++ client/src/pages/hospital/LabsList.tsx | 55 ++++ 5 files changed, 481 insertions(+), 114 deletions(-) create mode 100644 client/src/pages/hospital/ClarifyButton.tsx create mode 100644 client/src/pages/hospital/DictatableCard.tsx create mode 100644 client/src/pages/hospital/LabsList.tsx diff --git a/client/src/pages/ChartReview.tsx b/client/src/pages/ChartReview.tsx index ae25ee5..1f605e9 100644 --- a/client/src/pages/ChartReview.tsx +++ b/client/src/pages/ChartReview.tsx @@ -1,5 +1,13 @@ // ============================================================ -// CHART REVIEW — /api/generate-chart-review +// CHART REVIEW — faithful port of public/components/chart.html + +// public/js/chartReview.js (@be14578): +// • Demographics bar (age, gender, PMH, review type) +// • Save/Load encounter toolbar +// • Dynamic Visit cards — each with date + type + optional +// specialist/specialty (subspecialty) + content + per-visit labs +// • Additional Labs block — dynamic (date, values) list +// • Instructions textarea +// • Refine / Shorter / Read / Nextcloud export / editable body // ============================================================ import { useState } from 'react'; @@ -8,30 +16,49 @@ import { api, ApiError } from '@/lib/api'; import type { ChartReviewOk } from '@/shared/types'; import EncounterToolbar from '@/components/EncounterToolbar'; import EditableResult from '@/components/EditableResult'; +import LabsList, { type LabRow, emptyLab } from './hospital/LabsList'; type ReviewType = 'outpatient' | 'subspecialty' | 'ed'; +type VisitType = 'outpatient' | 'subspecialty' | 'ed'; const TYPE = 'chart' as const; -interface VisitInput { date: string; content: string; labs: string } +interface Visit { + date: string; + visitType: VisitType; + content: string; + labs: string; + specialistName?: string; + specialty?: string; +} +function emptyVisit(): Visit { + return { date: '', visitType: 'outpatient', content: '', labs: '' }; +} -function emptyVisit(): VisitInput { return { date: '', content: '', labs: '' }; } +const input = 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm'; +const inputSm = 'rounded-md border border-input bg-background px-2 py-1 text-sm'; +const btn = 'inline-flex items-center gap-1 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium hover:bg-muted disabled:opacity-50'; + +// Per-visit payload pieces for the server. Matches src/routes/chartReview.ts. +interface OutVisitPayload { date: string; content: string; labs: string } +interface SubspecialtyPayload extends OutVisitPayload { specialistName?: string; specialty?: string } export default function ChartReview() { const [label, setLabel] = useState(''); - const [type, setType] = useState('outpatient'); + const [reviewType, setReviewType] = useState('outpatient'); const [patientAge, setPatientAge] = useState(''); const [patientGender, setPatientGender] = useState(''); const [pmh, setPmh] = useState(''); - const [visits, setVisits] = useState([emptyVisit()]); + const [visits, setVisits] = useState([emptyVisit()]); + const [extraLabs, setExtraLabs] = useState([emptyLab()]); const [additionalInstructions, setAdditionalInstructions] = useState(''); const [result, setResult] = useState(null); - const generate = useMutation({ + const generate = useMutation>({ mutationFn: (body) => api.post('/api/generate-chart-review', body), onSuccess: (data) => setResult(data.review), }); - function updateVisit(i: number, patch: Partial) { + function updateVisit(i: number, patch: Partial) { setVisits((vs) => vs.map((v, idx) => (idx === i ? { ...v, ...patch } : v))); } @@ -39,41 +66,52 @@ export default function ChartReview() { e.preventDefault(); setResult(null); const filled = visits.filter((v) => v.content.trim()); + const toOut = (v: Visit): OutVisitPayload => ({ date: v.date, content: v.content, labs: v.labs }); + const toSub = (v: Visit): SubspecialtyPayload => ({ + date: v.date, content: v.content, labs: v.labs, + specialistName: v.specialistName, specialty: v.specialty, + }); generate.mutate({ - type, + type: reviewType, patientAge, patientGender, pmh, - visits: type === 'outpatient' ? filled : undefined, - subspecialty: type === 'subspecialty' ? filled : undefined, - edVisits: type === 'ed' ? filled : undefined, + visits: filled.filter((v) => v.visitType === 'outpatient').map(toOut), + subspecialty: filled.filter((v) => v.visitType === 'subspecialty').map(toSub), + edVisits: filled.filter((v) => v.visitType === 'ed').map(toOut), + labs: extraLabs.filter((l) => l.values.trim()), additionalInstructions: additionalInstructions || undefined, }); } - const input = 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm'; - return (

Chart Review

- Past visits → summary for pre-charting. + Past visits + labs → summary for pre-charting. Mix of outpatient, subspecialty, and ED visits supported.

{ try { - const parsedVisits = enc.transcript ? JSON.parse(enc.transcript) as VisitInput[] : [emptyVisit()]; - setVisits(parsedVisits.length ? parsedVisits : [emptyVisit()]); - } catch { setVisits([emptyVisit()]); } + const parsed = enc.transcript ? JSON.parse(enc.transcript) : null; + if (Array.isArray(parsed?.visits) && parsed.visits.length) setVisits(parsed.visits); + if (Array.isArray(parsed?.extraLabs) && parsed.extraLabs.length) setExtraLabs(parsed.extraLabs); + } catch { + // Legacy saved encounters used a bare array — fall back to that shape. + try { + const legacy = enc.transcript ? JSON.parse(enc.transcript) as Visit[] : null; + if (Array.isArray(legacy)) setVisits(legacy); + } catch { /* ignore */ } + } setResult(enc.generated_note || null); try { const pd = enc.partial_data ? JSON.parse(enc.partial_data) : null; - if (pd?.type) setType(pd.type); + if (pd?.reviewType) setReviewType(pd.reviewType); if (pd?.age) setPatientAge(pd.age); if (pd?.gender) setPatientGender(pd.gender); if (pd?.pmh) setPmh(pd.pmh); @@ -82,8 +120,8 @@ export default function ChartReview() { setLabel(enc.label || ''); }} onClear={() => { - setVisits([emptyVisit()]); setResult(null); - setType('outpatient'); setPatientAge(''); setPatientGender(''); + setVisits([emptyVisit()]); setExtraLabs([emptyLab()]); setResult(null); + setReviewType('outpatient'); setPatientAge(''); setPatientGender(''); setPmh(''); setAdditionalInstructions(''); }} /> @@ -92,7 +130,7 @@ export default function ChartReview() {