"use client"; import { CalendarDays } from "lucide-react"; import { type FormEvent, type ReactNode, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { TODAY } from "@/components/appointments/appointments-view"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { Combobox, type ComboboxOption } from "@/components/ui/combobox"; import { Dialog, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Popover, PopoverPopup, PopoverTrigger } from "@/components/ui/popover"; import { listPatients, type Patient } from "@/lib/patients"; import { listProviders, type Provider } from "@/lib/staff"; import { notify } from "@/lib/toast"; export type NewAppointment = { fileNumber: string; name: string; initials: string; date: string; // ISO YYYY-MM-DD time: string; type: string; provider: string; }; // Local-date ISO key (avoids UTC drift from toISOString). const keyOf = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String( d.getDate(), ).padStart(2, "0")}`; const TYPES = [ "Follow-up", "New patient", "Consultation", "Lab review", "Vaccination", ]; 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"; // Local start-of-day so the calendar can disable days strictly before today // (appointments can't be scheduled in the past). const startOfToday = () => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; }; function Field({ label, children }: { label: string; children: ReactNode }) { return ( ); } // Compact "New appointment" dialog. The patient and provider are chosen via // searchable comboboxes (arrow keys + Enter); the rest is the slot. The new // entry is handed back to the page via onAdd, which persists it through the API. export function AddAppointmentDialog({ open, onOpenChange, onAdd, }: { open: boolean; onOpenChange: (open: boolean) => void; onAdd: (appt: NewAppointment) => void; }) { const { t } = useTranslation(); const [patients, setPatients] = useState([]); const [providers, setProviders] = useState([]); const [selected, setSelected] = useState(null); const [date, setDate] = useState(() => new Date(`${TODAY}T00:00:00`)); const [dateOpen, setDateOpen] = useState(false); const [time, setTime] = useState("09:00"); const [type, setType] = useState(TYPES[0]); const [provider, setProvider] = useState(""); const [providerQuery, setProviderQuery] = useState(""); // Load patients + providers lazily when the dialog opens (for the searches). useEffect(() => { if (!open) return; let active = true; listPatients() .then((data) => active && setPatients(data)) .catch(() => { /* search just stays empty */ }); listProviders() .then((data) => active && setProviders(data)) .catch(() => { /* falls back to the patient's PCP on submit */ }); return () => { active = false; }; }, [open]); const patientOptions = useMemo( () => patients.map((p) => ({ value: p.fileNumber, // Fold the file number into the label so it's type-to-searchable. label: `${p.name} ${p.fileNumber}`, node: ( {p.name} #{p.fileNumber} ), })), [patients], ); const providerOptions = useMemo( () => providers.map((pr) => ({ value: pr.name, label: pr.name, node: ( {pr.name} {pr.role} ), })), [providers], ); const reset = () => { setSelected(null); setDate(new Date(`${TODAY}T00:00:00`)); setDateOpen(false); setTime("09:00"); setType(TYPES[0]); setProvider(""); setProviderQuery(""); }; const submit = (event: FormEvent) => { event.preventDefault(); if (!selected) { notify.error( t("appointments.dialog.pickPatientTitle"), t("appointments.dialog.pickPatientBody"), ); return; } onAdd({ fileNumber: selected.fileNumber, name: selected.name, initials: selected.initials, date: keyOf(date), time, type, provider: provider.trim() || selected.pcp, }); notify.success( t("appointments.dialog.addedTitle"), `${selected.name} ยท ${time}`, ); reset(); onOpenChange(false); }; return ( { onOpenChange(o); if (!o) reset(); }} open={open} > {t("appointments.dialog.title")} {t("appointments.dialog.description")}
{selected ? (
{selected.name} {t("appointments.dialog.fileNumber", { number: selected.fileNumber, })}
) : ( { const p = patients.find((x) => x.fileNumber === fileNumber); if (p) setSelected(p); }} options={patientOptions} placeholder={t("appointments.dialog.searchPlaceholder")} /> )}
{t("appointments.dialog.date")} {date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", })} } /> { if (d) { setDate(d); setDateOpen(false); } }} selected={date} />
setTime(event.target.value)} type="time" value={time} />
{ setProvider(name); setProviderQuery(name); }} onValueChange={setProviderQuery} options={providerOptions} placeholder={t("appointments.dialog.providerPlaceholder")} value={providerQuery} />
}> {t("appointments.dialog.cancel")}
); }