"use client"; import { CalendarIcon, Plus, RefreshCw, X } from "lucide-react"; import { type FormEvent, type ReactNode, useState } from "react"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { Popover, PopoverPopup, PopoverTrigger, } from "@/components/ui/popover"; import { Dialog, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { type AllergySeverity, createPatient, generateFileNumber, type LabFlag, type Patient, updatePatient, } from "@/lib/patients"; import { notify } from "@/lib/toast"; 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 ( ); } function SectionList({ label, rows, blank, onChange, render, }: { label: string; rows: T[]; blank: T; onChange: (rows: T[]) => void; render: (row: T, set: (patch: Partial) => void) => ReactNode; }) { return (
{label}
{rows.map((row, index) => (
{render(row, (patch) => onChange(rows.map((r, i) => (i === index ? { ...r, ...patch } : r))) )}
))}
); } 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 formatDate = (date: Date) => date.toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "numeric", }); const today = () => formatDate(new Date()); // Patient dates are stored as formatted strings (e.g. "Jun 02, 2026"); parse one // back to a Date so the calendar can highlight the current selection. function parseDate(value: string): Date | undefined { const trimmed = value.trim(); if (!trimmed) { return undefined; } const parsed = new Date(trimmed); return Number.isNaN(parsed.getTime()) ? undefined : parsed; } // Calendar-backed date field that reads/writes the same formatted string the rest // of the form uses. function DatePicker({ value, onChange, ariaLabel, placeholder = "Pick a date", className, }: { value: string; onChange: (value: string) => void; ariaLabel: string; placeholder?: string; className?: string; }) { const [open, setOpen] = useState(false); return ( } > {value || placeholder} { onChange(date ? formatDate(date) : ""); setOpen(false); }} selected={parseDate(value)} /> ); } export function PatientFormDialog({ open, onOpenChange, mode, patient, onCreated, onSaved, }: PatientFormDialogProps) { const isEdit = mode === "edit"; const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); 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 ?? "F"); const [status, setStatus] = useState( 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( () => patient?.allergies.map((a) => ({ ...a })) ?? [] ); const [medications, setMedications] = useState( () => patient?.medications.map((m) => ({ ...m })) ?? [] ); const [problems, setProblems] = useState( () => patient?.problems.map((p) => ({ ...p })) ?? [] ); const [labs, setLabs] = useState( () => patient?.labs.map((l) => ({ ...l })) ?? [] ); const [visits, setVisits] = useState( () => patient?.encounters.map((e) => ({ type: e.type, date: e.date, provider: e.provider, summary: e.summary, })) ?? [] ); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (!name.trim() || submitting) { 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, })), }; setSubmitting(true); setError(null); try { const saved = isEdit ? await updatePatient(built) : await createPatient(built); if (isEdit) { onSaved?.(saved); notify.success("Record updated", `${saved.name}'s chart was saved.`); } else { onCreated?.(saved.fileNumber); notify.success("Patient added", `${saved.name} (${saved.fileNumber}).`); } onOpenChange(false); } catch (err) { const message = err instanceof Error ? err.message : "Could not save the patient."; setError(message); notify.error("Couldn't save patient", message); } finally { setSubmitting(false); } }; return ( {isEdit ? "Edit record" : "Add patient"} {isEdit ? `Update ${patient?.name ?? "this"}'s chart and add new data.` : "Create a new chart. A file number has been generated for you."}
{!isEdit && ( )}
setName(event.target.value)} placeholder="e.g. Jordan Pierce" required value={name} />
setAge(event.target.value)} placeholder="—" value={age} />
setPcp(event.target.value)} placeholder="e.g. Dr. Lena Ortiz" value={pcp} />
Current vitals
setBp(event.target.value)} placeholder="BP" value={bp} /> setHr(event.target.value)} placeholder="HR" value={hr} /> setTemp(event.target.value)} placeholder="Temp" value={temp} /> setSpo2(event.target.value)} placeholder="SpO₂" value={spo2} />
blank={{ substance: "", reaction: "", severity: "mild" }} label="Allergies" onChange={setAllergies} render={(row, set) => ( <> set({ substance: event.target.value })} placeholder="Substance" value={row.substance} /> set({ reaction: event.target.value })} placeholder="Reaction" value={row.reaction} /> )} rows={allergies} /> blank={{ name: "", dose: "", frequency: "" }} label="Medications" onChange={setMedications} render={(row, set) => ( <> set({ name: event.target.value })} placeholder="Name" value={row.name} /> set({ dose: event.target.value })} placeholder="Dose" value={row.dose} /> set({ frequency: event.target.value })} placeholder="Frequency" value={row.frequency} /> )} rows={medications} /> blank={{ label: "", since: "" }} label="Problems" onChange={setProblems} render={(row, set) => ( <> set({ label: event.target.value })} placeholder="Diagnosis" value={row.label} /> set({ since })} placeholder="Since" value={row.since} /> )} rows={problems} /> blank={{ name: "", value: "", flag: "normal", takenAt: "" }} label="Labs" onChange={setLabs} render={(row, set) => ( <> set({ name: event.target.value })} placeholder="Test" value={row.name} /> set({ value: event.target.value })} placeholder="Value" value={row.value} /> )} rows={labs} /> blank={{ type: "", date: "", provider: "", summary: "" }} label="Visits" onChange={setVisits} render={(row, set) => (
set({ type: event.target.value })} placeholder="Type" value={row.type} /> set({ date })} placeholder="Date" value={row.date} />
set({ provider: event.target.value })} placeholder="Provider" value={row.provider} /> set({ summary: event.target.value })} placeholder="Summary" value={row.summary} />
)} rows={visits} />
{error && (

{error}

)} }> Cancel
); }