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() {