Edit patient records + fix Add-patient dialog scrollbar

- Generalize add-patient-dialog → shared PatientFormDialog (create | edit).
  Edit mode prefills from the patient; both modes now cover all sections
  with add/remove rows: identity, vitals, allergies, meds, problems, labs,
  visits. Save writes to the store (addPatient overwrites by file #).
- Fix the dialog scrollbar: fixed header + footer (Save/Cancel pinned),
  only the field body scrolls, scrollbar hidden (no-scrollbar).
- patient-cards: "Edit record" button on the Summary card opens the editor
  (stopPropagation so it doesn't open the detail dialog); PatientResult
  holds the editor state and exposes onPatientUpdated.
- chat-panel: onPatientUpdated replaces the message's patient in place, so
  saved edits update the on-screen cards (no duplicate message, no requery).
- chat-input: Add pill now uses PatientFormDialog mode="create".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-01 21:17:43 +03:00
parent 01e345c3e1
commit 750622e0db
5 changed files with 573 additions and 409 deletions
@@ -1,403 +0,0 @@
"use client";
import { Plus, RefreshCw, X } from "lucide-react";
import { type FormEvent, type ReactNode, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import {
addPatient,
type AllergySeverity,
generateFileNumber,
type Patient,
} from "@/lib/patients";
type AddPatientDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
onCreated: (fileNumber: string) => void;
};
type AllergyDraft = { substance: string; reaction: string; severity: AllergySeverity };
type MedicationDraft = { name: string; dose: string; frequency: string };
const controlClass =
"h-9 w-full rounded-3xl border border-transparent bg-input/50 px-3 text-sm text-foreground outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30";
function Field({ label, children }: { label: string; children: ReactNode }) {
return (
<label className="flex flex-col gap-1.5">
<span className="text-xs text-muted-foreground">{label}</span>
{children}
</label>
);
}
function SectionHeader({
label,
onAdd,
}: {
label: string;
onAdd: () => void;
}) {
return (
<div className="flex items-center justify-between">
<span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
{label}
</span>
<Button onClick={onAdd} size="sm" type="button" variant="ghost">
<Plus className="size-4" />
Add
</Button>
</div>
);
}
function initialsFromName(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) {
return "?";
}
return parts
.slice(0, 2)
.map((part) => part[0])
.join("")
.toUpperCase();
}
const today = () =>
new Date().toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
});
export function AddPatientDialog({
open,
onOpenChange,
onCreated,
}: AddPatientDialogProps) {
// Lazily generate the file number on mount. The dialog is remounted (via a
// `key`) each time it opens, so this gives a fresh number + cleared form
// without a reset effect.
const [fileNumber, setFileNumber] = useState(generateFileNumber);
const [name, setName] = useState("");
const [age, setAge] = useState("");
const [sex, setSex] = useState<Patient["sex"]>("F");
const [status, setStatus] = useState<Patient["status"]>("active");
const [pcp, setPcp] = useState("");
const [bp, setBp] = useState("");
const [hr, setHr] = useState("");
const [temp, setTemp] = useState("");
const [spo2, setSpo2] = useState("");
const [allergies, setAllergies] = useState<AllergyDraft[]>([]);
const [medications, setMedications] = useState<MedicationDraft[]>([]);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!name.trim()) {
return;
}
const patient: Patient = {
fileNumber,
name: name.trim(),
age: Number(age) || 0,
sex,
pcp: pcp.trim() || "—",
status,
initials: initialsFromName(name),
allergies: allergies.filter((a) => a.substance.trim()),
alerts: [],
medications: medications.filter((m) => m.name.trim()),
problems: [],
vitals: {
bp: bp.trim() || "—",
hr: hr.trim() || "—",
temp: temp.trim() || "—",
spo2: spo2.trim() || "—",
takenAt: today(),
},
vitalsTrend: { label: "Heart rate", unit: "bpm", points: [] },
labs: [],
labTrend: { label: "—", unit: "", points: [] },
encounters: [],
};
addPatient(patient);
onCreated(fileNumber);
onOpenChange(false);
};
return (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent className="max-h-[85dvh] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Add patient</DialogTitle>
<DialogDescription>
Create a new chart. A file number has been generated for you.
</DialogDescription>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<Field label="File number">
<div className="flex items-center gap-2">
<Input readOnly value={fileNumber} />
<Button
aria-label="Regenerate file number"
onClick={() => setFileNumber(generateFileNumber())}
size="icon"
type="button"
variant="outline"
>
<RefreshCw className="size-4" />
</Button>
</div>
</Field>
<Field label="Full name">
<Input
autoFocus
onChange={(event) => setName(event.target.value)}
placeholder="e.g. Jordan Pierce"
required
value={name}
/>
</Field>
<div className="grid grid-cols-3 gap-3">
<Field label="Age">
<Input
inputMode="numeric"
onChange={(event) => setAge(event.target.value)}
placeholder="—"
value={age}
/>
</Field>
<Field label="Sex">
<select
className={controlClass}
onChange={(event) => setSex(event.target.value as Patient["sex"])}
value={sex}
>
<option value="F">Female</option>
<option value="M">Male</option>
</select>
</Field>
<Field label="Status">
<select
className={controlClass}
onChange={(event) =>
setStatus(event.target.value as Patient["status"])
}
value={status}
>
<option value="active">Active</option>
<option value="inpatient">Inpatient</option>
<option value="discharged">Discharged</option>
</select>
</Field>
</div>
<Field label="Primary care">
<Input
onChange={(event) => setPcp(event.target.value)}
placeholder="e.g. Dr. Lena Ortiz"
value={pcp}
/>
</Field>
<div className="flex flex-col gap-1.5">
<span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
Current vitals
</span>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Input
aria-label="Blood pressure"
onChange={(event) => setBp(event.target.value)}
placeholder="BP"
value={bp}
/>
<Input
aria-label="Heart rate"
onChange={(event) => setHr(event.target.value)}
placeholder="HR"
value={hr}
/>
<Input
aria-label="Temperature"
onChange={(event) => setTemp(event.target.value)}
placeholder="Temp"
value={temp}
/>
<Input
aria-label="Oxygen saturation"
onChange={(event) => setSpo2(event.target.value)}
placeholder="SpO₂"
value={spo2}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<SectionHeader
label="Allergies"
onAdd={() =>
setAllergies((prev) => [
...prev,
{ substance: "", reaction: "", severity: "mild" },
])
}
/>
{allergies.map((allergy, index) => (
<div className="flex items-center gap-2" key={index}>
<Input
aria-label="Substance"
onChange={(event) =>
setAllergies((prev) =>
prev.map((row, i) =>
i === index
? { ...row, substance: event.target.value }
: row
)
)
}
placeholder="Substance"
value={allergy.substance}
/>
<Input
aria-label="Reaction"
onChange={(event) =>
setAllergies((prev) =>
prev.map((row, i) =>
i === index
? { ...row, reaction: event.target.value }
: row
)
)
}
placeholder="Reaction"
value={allergy.reaction}
/>
<select
aria-label="Severity"
className={cn(controlClass, "w-auto")}
onChange={(event) =>
setAllergies((prev) =>
prev.map((row, i) =>
i === index
? {
...row,
severity: event.target.value as AllergySeverity,
}
: row
)
)
}
value={allergy.severity}
>
<option value="mild">Mild</option>
<option value="moderate">Moderate</option>
<option value="severe">Severe</option>
</select>
<button
aria-label="Remove allergy"
className="text-muted-foreground transition-colors hover:text-foreground"
onClick={() =>
setAllergies((prev) => prev.filter((_, i) => i !== index))
}
type="button"
>
<X className="size-4" />
</button>
</div>
))}
</div>
<div className="flex flex-col gap-2">
<SectionHeader
label="Medications"
onAdd={() =>
setMedications((prev) => [
...prev,
{ name: "", dose: "", frequency: "" },
])
}
/>
{medications.map((med, index) => (
<div className="flex items-center gap-2" key={index}>
<Input
aria-label="Medication name"
onChange={(event) =>
setMedications((prev) =>
prev.map((row, i) =>
i === index ? { ...row, name: event.target.value } : row
)
)
}
placeholder="Name"
value={med.name}
/>
<Input
aria-label="Dose"
onChange={(event) =>
setMedications((prev) =>
prev.map((row, i) =>
i === index ? { ...row, dose: event.target.value } : row
)
)
}
placeholder="Dose"
value={med.dose}
/>
<Input
aria-label="Frequency"
onChange={(event) =>
setMedications((prev) =>
prev.map((row, i) =>
i === index
? { ...row, frequency: event.target.value }
: row
)
)
}
placeholder="Frequency"
value={med.frequency}
/>
<button
aria-label="Remove medication"
className="text-muted-foreground transition-colors hover:text-foreground"
onClick={() =>
setMedications((prev) => prev.filter((_, i) => i !== index))
}
type="button"
>
<X className="size-4" />
</button>
</div>
))}
</div>
<DialogFooter>
<DialogClose render={<Button type="button" variant="outline" />}>
Cancel
</DialogClose>
<Button disabled={!name.trim()} type="submit">
Save patient
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
+3 -2
View File
@@ -23,7 +23,7 @@ import {
useState,
} from "react";
import { AddPatientDialog } from "@/components/chat/add-patient-dialog";
import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -343,8 +343,9 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
</div>
</form>
<AddPatientDialog
<PatientFormDialog
key={addKey}
mode="create"
onCreated={(fileNumber) => onSubmit(`/patient ${fileNumber}`)}
onOpenChange={setAddOpen}
open={addOpen}
+11
View File
@@ -134,6 +134,17 @@ export function ChatPanel() {
<MessageContent className="w-full">
<PatientResult
fileNumber={message.fileNumber}
onPatientUpdated={(updated) =>
setMessages((prev) =>
prev.map((m) =>
m.id === message.id &&
m.role === "assistant" &&
m.kind === "patient"
? { ...m, patient: updated, status: "ready" }
: m
)
)
}
patient={message.patient}
status={message.status}
/>
+47 -4
View File
@@ -1,6 +1,7 @@
"use client";
import type { ReactNode } from "react";
import { Pencil } from "lucide-react";
import { type ReactNode, useState } from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
@@ -21,6 +22,7 @@ import {
} from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
import { Sparkline } from "@/components/chat/sparkline";
import { cn } from "@/lib/utils";
import type { AllergySeverity, LabFlag, Patient, Trend } from "@/lib/patients";
@@ -31,6 +33,7 @@ type PatientResultProps = {
status: "loading" | "ready" | "not-found";
fileNumber: string;
patient?: Patient;
onPatientUpdated?: (patient: Patient) => void;
};
const severityVariant: Record<AllergySeverity, BadgeVariant> = {
@@ -183,7 +186,13 @@ function ExpandableCard({
);
}
function SummaryCard({ patient }: { patient: Patient }) {
function SummaryCard({
patient,
onEdit,
}: {
patient: Patient;
onEdit?: () => void;
}) {
const idLine = `${patient.age} · ${sexLabel[patient.sex]} · MRN ${patient.fileNumber}`;
return (
<ExpandableCard
@@ -233,6 +242,17 @@ function SummaryCard({ patient }: { patient: Patient }) {
<Stat label="Open problems" value={patient.problems.length} />
</div>
<AlertBadges alerts={patient.alerts} />
<button
className="mt-auto flex items-center justify-center gap-1.5 rounded-2xl border border-border/60 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
onClick={(event) => {
event.stopPropagation();
onEdit?.();
}}
type="button"
>
<Pencil className="size-4" />
Edit record
</button>
</CardContent>
</ExpandableCard>
);
@@ -542,7 +562,16 @@ function LoadingCards() {
);
}
export function PatientResult({ status, fileNumber, patient }: PatientResultProps) {
export function PatientResult({
status,
fileNumber,
patient,
onPatientUpdated,
}: PatientResultProps) {
const [editOpen, setEditOpen] = useState(false);
// Bumped on open so the editor remounts with the latest patient data.
const [editKey, setEditKey] = useState(0);
if (status === "not-found") {
return (
<Card size="sm">
@@ -561,13 +590,27 @@ export function PatientResult({ status, fileNumber, patient }: PatientResultProp
<LoadingCards />
) : (
<>
<SummaryCard patient={patient} />
<SummaryCard
onEdit={() => {
setEditKey((k) => k + 1);
setEditOpen(true);
}}
patient={patient}
/>
<VitalsCard patient={patient} />
<LabsCard patient={patient} />
<MedicationsCard patient={patient} />
<ProblemsCard patient={patient} />
<AllergiesCard patient={patient} />
<VisitsCard patient={patient} />
<PatientFormDialog
key={editKey}
mode="edit"
onOpenChange={setEditOpen}
onSaved={(updated) => onPatientUpdated?.(updated)}
open={editOpen}
patient={patient}
/>
</>
)}
</div>
@@ -0,0 +1,512 @@
"use client";
import { Plus, RefreshCw, X } from "lucide-react";
import { type FormEvent, type ReactNode, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import {
addPatient,
type AllergySeverity,
generateFileNumber,
type LabFlag,
type Patient,
} from "@/lib/patients";
type PatientFormDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
mode: "create" | "edit";
patient?: Patient;
onCreated?: (fileNumber: string) => void;
onSaved?: (patient: Patient) => void;
};
type AllergyDraft = { substance: string; reaction: string; severity: AllergySeverity };
type MedicationDraft = { name: string; dose: string; frequency: string };
type ProblemDraft = { label: string; since: string };
type LabDraft = { name: string; value: string; flag: LabFlag; takenAt: string };
type VisitDraft = { type: string; date: string; provider: string; summary: string };
const controlClass =
"h-9 w-full rounded-3xl border border-transparent bg-input/50 px-3 text-sm text-foreground outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30";
function Field({ label, children }: { label: string; children: ReactNode }) {
return (
<label className="flex flex-col gap-1.5">
<span className="text-xs text-muted-foreground">{label}</span>
{children}
</label>
);
}
function SectionList<T>({
label,
rows,
blank,
onChange,
render,
}: {
label: string;
rows: T[];
blank: T;
onChange: (rows: T[]) => void;
render: (row: T, set: (patch: Partial<T>) => void) => ReactNode;
}) {
return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
{label}
</span>
<Button
onClick={() => onChange([...rows, blank])}
size="sm"
type="button"
variant="ghost"
>
<Plus className="size-4" />
Add
</Button>
</div>
{rows.map((row, index) => (
<div className="flex items-center gap-2" key={index}>
{render(row, (patch) =>
onChange(rows.map((r, i) => (i === index ? { ...r, ...patch } : r)))
)}
<button
aria-label={`Remove ${label} row`}
className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
onClick={() => onChange(rows.filter((_, i) => i !== index))}
type="button"
>
<X className="size-4" />
</button>
</div>
))}
</div>
);
}
function initialsFromName(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) {
return "?";
}
return parts
.slice(0, 2)
.map((part) => part[0])
.join("")
.toUpperCase();
}
const today = () =>
new Date().toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
});
export function PatientFormDialog({
open,
onOpenChange,
mode,
patient,
onCreated,
onSaved,
}: PatientFormDialogProps) {
const isEdit = mode === "edit";
const [fileNumber, setFileNumber] = useState(() =>
isEdit && patient ? patient.fileNumber : generateFileNumber()
);
const [name, setName] = useState(patient?.name ?? "");
const [age, setAge] = useState(patient ? String(patient.age) : "");
const [sex, setSex] = useState<Patient["sex"]>(patient?.sex ?? "F");
const [status, setStatus] = useState<Patient["status"]>(
patient?.status ?? "active"
);
const [pcp, setPcp] = useState(patient?.pcp ?? "");
const [bp, setBp] = useState(patient?.vitals.bp ?? "");
const [hr, setHr] = useState(patient?.vitals.hr ?? "");
const [temp, setTemp] = useState(patient?.vitals.temp ?? "");
const [spo2, setSpo2] = useState(patient?.vitals.spo2 ?? "");
const [allergies, setAllergies] = useState<AllergyDraft[]>(
() => patient?.allergies.map((a) => ({ ...a })) ?? []
);
const [medications, setMedications] = useState<MedicationDraft[]>(
() => patient?.medications.map((m) => ({ ...m })) ?? []
);
const [problems, setProblems] = useState<ProblemDraft[]>(
() => patient?.problems.map((p) => ({ ...p })) ?? []
);
const [labs, setLabs] = useState<LabDraft[]>(
() => patient?.labs.map((l) => ({ ...l })) ?? []
);
const [visits, setVisits] = useState<VisitDraft[]>(
() =>
patient?.encounters.map((e) => ({
type: e.type,
date: e.date,
provider: e.provider,
summary: e.summary,
})) ?? []
);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!name.trim()) {
return;
}
const built: Patient = {
fileNumber,
name: name.trim(),
age: Number(age) || 0,
sex,
pcp: pcp.trim() || "—",
status,
initials: initialsFromName(name),
allergies: allergies.filter((a) => a.substance.trim()),
alerts: patient?.alerts ?? [],
medications: medications.filter((m) => m.name.trim()),
problems: problems.filter((p) => p.label.trim()),
vitals: {
bp: bp.trim() || "—",
hr: hr.trim() || "—",
temp: temp.trim() || "—",
spo2: spo2.trim() || "—",
takenAt: isEdit ? (patient?.vitals.takenAt ?? today()) : today(),
},
vitalsTrend: patient?.vitalsTrend ?? {
label: "Heart rate",
unit: "bpm",
points: [],
},
labs: labs
.filter((l) => l.name.trim())
.map((l) => ({ ...l, takenAt: l.takenAt.trim() || today() })),
labTrend: patient?.labTrend ?? { label: "—", unit: "", points: [] },
encounters: visits
.filter((v) => v.type.trim() || v.summary.trim())
.map((v) => ({
type: v.type.trim() || "Visit",
date: v.date.trim() || today(),
provider: v.provider,
summary: v.summary,
})),
};
addPatient(built);
if (isEdit) {
onSaved?.(built);
} else {
onCreated?.(fileNumber);
}
onOpenChange(false);
};
return (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent className="flex max-h-[85dvh] flex-col gap-4 overflow-hidden sm:max-w-lg">
<DialogHeader>
<DialogTitle>{isEdit ? "Edit record" : "Add patient"}</DialogTitle>
<DialogDescription>
{isEdit
? `Update ${patient?.name ?? "this"}'s chart and add new data.`
: "Create a new chart. A file number has been generated for you."}
</DialogDescription>
</DialogHeader>
<form
className="flex min-h-0 flex-1 flex-col gap-4"
onSubmit={handleSubmit}
>
<div className="no-scrollbar flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto">
<Field label="File number">
<div className="flex items-center gap-2">
<Input readOnly value={fileNumber} />
{!isEdit && (
<Button
aria-label="Regenerate file number"
onClick={() => setFileNumber(generateFileNumber())}
size="icon"
type="button"
variant="outline"
>
<RefreshCw className="size-4" />
</Button>
)}
</div>
</Field>
<Field label="Full name">
<Input
autoFocus={!isEdit}
onChange={(event) => setName(event.target.value)}
placeholder="e.g. Jordan Pierce"
required
value={name}
/>
</Field>
<div className="grid grid-cols-3 gap-3">
<Field label="Age">
<Input
inputMode="numeric"
onChange={(event) => setAge(event.target.value)}
placeholder="—"
value={age}
/>
</Field>
<Field label="Sex">
<select
className={controlClass}
onChange={(event) =>
setSex(event.target.value as Patient["sex"])
}
value={sex}
>
<option value="F">Female</option>
<option value="M">Male</option>
</select>
</Field>
<Field label="Status">
<select
className={controlClass}
onChange={(event) =>
setStatus(event.target.value as Patient["status"])
}
value={status}
>
<option value="active">Active</option>
<option value="inpatient">Inpatient</option>
<option value="discharged">Discharged</option>
</select>
</Field>
</div>
<Field label="Primary care">
<Input
onChange={(event) => setPcp(event.target.value)}
placeholder="e.g. Dr. Lena Ortiz"
value={pcp}
/>
</Field>
<div className="flex flex-col gap-1.5">
<span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
Current vitals
</span>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Input
aria-label="Blood pressure"
onChange={(event) => setBp(event.target.value)}
placeholder="BP"
value={bp}
/>
<Input
aria-label="Heart rate"
onChange={(event) => setHr(event.target.value)}
placeholder="HR"
value={hr}
/>
<Input
aria-label="Temperature"
onChange={(event) => setTemp(event.target.value)}
placeholder="Temp"
value={temp}
/>
<Input
aria-label="Oxygen saturation"
onChange={(event) => setSpo2(event.target.value)}
placeholder="SpO₂"
value={spo2}
/>
</div>
</div>
<SectionList<AllergyDraft>
blank={{ substance: "", reaction: "", severity: "mild" }}
label="Allergies"
onChange={setAllergies}
render={(row, set) => (
<>
<Input
aria-label="Substance"
onChange={(event) => set({ substance: event.target.value })}
placeholder="Substance"
value={row.substance}
/>
<Input
aria-label="Reaction"
onChange={(event) => set({ reaction: event.target.value })}
placeholder="Reaction"
value={row.reaction}
/>
<select
aria-label="Severity"
className={cn(controlClass, "w-auto")}
onChange={(event) =>
set({ severity: event.target.value as AllergySeverity })
}
value={row.severity}
>
<option value="mild">Mild</option>
<option value="moderate">Moderate</option>
<option value="severe">Severe</option>
</select>
</>
)}
rows={allergies}
/>
<SectionList<MedicationDraft>
blank={{ name: "", dose: "", frequency: "" }}
label="Medications"
onChange={setMedications}
render={(row, set) => (
<>
<Input
aria-label="Medication name"
onChange={(event) => set({ name: event.target.value })}
placeholder="Name"
value={row.name}
/>
<Input
aria-label="Dose"
onChange={(event) => set({ dose: event.target.value })}
placeholder="Dose"
value={row.dose}
/>
<Input
aria-label="Frequency"
onChange={(event) => set({ frequency: event.target.value })}
placeholder="Frequency"
value={row.frequency}
/>
</>
)}
rows={medications}
/>
<SectionList<ProblemDraft>
blank={{ label: "", since: "" }}
label="Problems"
onChange={setProblems}
render={(row, set) => (
<>
<Input
aria-label="Problem"
onChange={(event) => set({ label: event.target.value })}
placeholder="Diagnosis"
value={row.label}
/>
<Input
aria-label="Since"
className="w-28 shrink-0"
onChange={(event) => set({ since: event.target.value })}
placeholder="Since"
value={row.since}
/>
</>
)}
rows={problems}
/>
<SectionList<LabDraft>
blank={{ name: "", value: "", flag: "normal", takenAt: "" }}
label="Labs"
onChange={setLabs}
render={(row, set) => (
<>
<Input
aria-label="Lab name"
onChange={(event) => set({ name: event.target.value })}
placeholder="Test"
value={row.name}
/>
<Input
aria-label="Value"
onChange={(event) => set({ value: event.target.value })}
placeholder="Value"
value={row.value}
/>
<select
aria-label="Flag"
className={cn(controlClass, "w-auto")}
onChange={(event) => set({ flag: event.target.value as LabFlag })}
value={row.flag}
>
<option value="normal">Normal</option>
<option value="low">Low</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</>
)}
rows={labs}
/>
<SectionList<VisitDraft>
blank={{ type: "", date: "", provider: "", summary: "" }}
label="Visits"
onChange={setVisits}
render={(row, set) => (
<div className="flex w-full flex-col gap-2">
<div className="flex items-center gap-2">
<Input
aria-label="Visit type"
onChange={(event) => set({ type: event.target.value })}
placeholder="Type"
value={row.type}
/>
<Input
aria-label="Visit date"
className="w-32 shrink-0"
onChange={(event) => set({ date: event.target.value })}
placeholder="Date"
value={row.date}
/>
</div>
<Input
aria-label="Provider"
onChange={(event) => set({ provider: event.target.value })}
placeholder="Provider"
value={row.provider}
/>
<Input
aria-label="Summary"
onChange={(event) => set({ summary: event.target.value })}
placeholder="Summary"
value={row.summary}
/>
</div>
)}
rows={visits}
/>
</div>
<DialogFooter>
<DialogClose render={<Button type="button" variant="outline" />}>
Cancel
</DialogClose>
<Button disabled={!name.trim()} type="submit">
{isEdit ? "Save changes" : "Save patient"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}