import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { format } from "date-fns"; import { apiRequest } from "@/lib/queryClient"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Clock } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { useAuth } from "@/hooks/use-auth"; import { useDebounce } from "use-debounce"; import { Appointment, InsertAppointment, insertAppointmentSchema, Patient, Staff, UpdateAppointment, } from "@repo/db/types"; import { DateInputField } from "@/components/ui/dateInputField"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { toast } from "@/hooks/use-toast"; interface AppointmentFormProps { appointment?: Appointment; onSubmit: (data: InsertAppointment | UpdateAppointment) => void; onDelete?: (id: number) => void; onOpenChange?: (open: boolean) => void; isLoading?: boolean; } export function AppointmentForm({ appointment, onSubmit, onDelete, onOpenChange, isLoading = false, }: AppointmentFormProps) { const { user } = useAuth(); const inputRef = useRef(null); const [prefillPatient, setPrefillPatient] = useState(null); useEffect(() => { const timeout = setTimeout(() => { inputRef.current?.focus(); }, 50); // small delay ensures content is mounted return () => clearTimeout(timeout); }, []); const { data: staffMembersRaw = [] as Staff[] } = useQuery({ queryKey: ["/api/staffs/"], queryFn: async () => { const res = await apiRequest("GET", "/api/staffs/"); return res.json(); }, enabled: !!user, }); const colorMap: Record = { "Dr. Kai Gao": "bg-blue-600", "Dr. Jane Smith": "bg-emerald-600", }; const staffMembers = staffMembersRaw.map((staff) => ({ ...staff, color: colorMap[staff.name] || "bg-gray-400", })); // Get the stored data from session storage const storedDataString = sessionStorage.getItem("newAppointmentData"); let parsedStoredData = null; // Try to parse it if it exists if (storedDataString) { try { parsedStoredData = JSON.parse(storedDataString); } catch (error) { console.error("Error parsing stored appointment data:", error); } } // Format the date and times for the form const defaultValues: Partial = appointment ? { userId: user?.id, patientId: appointment.patientId, title: appointment.title, date: parseLocalDate(appointment.date), startTime: appointment.startTime || "09:00", // Default "09:00" endTime: appointment.endTime || "09:30", // Default "09:30" type: appointment.type, notes: appointment.notes || "", status: appointment.status || "scheduled", staffId: typeof appointment.staffId === "number" ? appointment.staffId : undefined, } : parsedStoredData ? { userId: user?.id, patientId: Number(parsedStoredData.patientId), date: parsedStoredData.date ? parseLocalDate(parsedStoredData.date) : parseLocalDate(new Date()), title: parsedStoredData.title || "", startTime: parsedStoredData.startTime, endTime: parsedStoredData.endTime, type: parsedStoredData.type || "checkup", status: parsedStoredData.status || "scheduled", notes: parsedStoredData.notes || "", staffId: typeof parsedStoredData.staff === "number" ? parsedStoredData.staff : (staffMembers?.[0]?.id ?? undefined), } : { userId: user?.id ?? 0, date: new Date(), title: "", startTime: "09:00", endTime: "09:30", type: "checkup", status: "scheduled", staffId: staffMembers?.[0]?.id ?? undefined, }; const form = useForm({ resolver: zodResolver(insertAppointmentSchema), defaultValues, }); // ----------------------------- // PATIENT SEARCH (simple inline search) // ----------------------------- const [selectOpen, setSelectOpen] = useState(false); const [patientSearchTerm, setPatientSearchTerm] = useState(""); const [debouncedPatientSearch] = useDebounce(patientSearchTerm, 300); const searchKeyPart = debouncedPatientSearch.trim() || "recent"; const queryFn = async (): Promise => { const trimmed = debouncedPatientSearch.trim(); const url = trimmed ? `/api/patients/search?name=${encodeURIComponent(trimmed)}&limit=50&offset=0` : `/api/patients/recent?limit=50&offset=0`; const res = await apiRequest("GET", url); if (!res.ok) { const err = await res.json().catch(() => ({ message: "Failed to fetch patients" })); throw new Error(err.message || "Failed to fetch patients"); } const payload = await res.json(); return Array.isArray(payload) ? payload : (payload.patients ?? []); }; const { data: patients = [], isFetching: isFetchingPatients, refetch: refetchPatients, } = useQuery({ queryKey: ["patients-dropdown", searchKeyPart], queryFn, enabled: selectOpen || debouncedPatientSearch.trim().length > 0, }); useEffect(() => { if (selectOpen && patients.length === 0) { refetchPatients(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectOpen]); // Force form field values to update and clean up storage useEffect(() => { if (!parsedStoredData) return; // set times/staff/date as before if (parsedStoredData.startTime) form.setValue("startTime", parsedStoredData.startTime); if (parsedStoredData.endTime) form.setValue("endTime", parsedStoredData.endTime); if (parsedStoredData.staff) form.setValue("staffId", parsedStoredData.staff); if (parsedStoredData.date) { form.setValue("date", parseLocalDate(parsedStoredData.date)); } // ---- patient prefill: check main cache, else fetch once ---- if (parsedStoredData.patientId) { const pid = Number(parsedStoredData.patientId); if (!Number.isNaN(pid)) { // ensure the form value is set form.setValue("patientId", pid); // fetch single patient record (preferred) (async () => { try { const res = await apiRequest("GET", `/api/patients/${pid}`); if (res.ok) { const patientRecord = await res.json(); setPrefillPatient(patientRecord); } else { // non-OK response: show toast with status / message let msg = `Failed to load patient (status ${res.status})`; try { const body = await res.json().catch(() => null); if (body && body.message) msg = body.message; } catch {} toast({ title: "Could not load patient", description: msg, variant: "destructive", }); } } catch (err) { toast({ title: "Error fetching patient", description: (err as Error)?.message || "An unknown error occurred while fetching patient details.", variant: "destructive", }); } finally { // remove the one-time transport sessionStorage.removeItem("newAppointmentData"); } })(); } } else { // no patientId in storage — still remove to avoid stale state sessionStorage.removeItem("newAppointmentData"); } }, [form]); // When editing an appointment, ensure we prefill the patient so SelectValue can render useEffect(() => { if (!appointment?.patientId) return; const pid = Number(appointment.patientId); if (Number.isNaN(pid)) return; // set form value immediately so the select has a value form.setValue("patientId", pid); // fetch the single patient record and set prefill (async () => { try { const res = await apiRequest("GET", `/api/patients/${pid}`); if (res.ok) { const patientRecord = await res.json(); setPrefillPatient(patientRecord); } else { let msg = `Failed to load patient (status ${res.status})`; try { const body = await res.json().catch(() => null); if (body && body.message) msg = body.message; } catch {} toast({ title: "Could not load patient", description: msg, variant: "destructive", }); } } catch (err) { toast({ title: "Error fetching patient", description: (err as Error)?.message || "An unknown error occurred while fetching patient details.", variant: "destructive", }); } })(); // note: we intentionally do NOT remove prefillPatientd here; it will be cleared when dropdown opens and main list contains the patient }, [appointment?.patientId]); const handleSubmit = (data: InsertAppointment) => { // Make sure patientId is a number const patientId = typeof data.patientId === "string" ? parseInt(data.patientId, 10) : data.patientId; // Auto-create title if it's empty let title = data.title; if (!title || title.trim() === "") { // Format: "April 19" - just the date title = format(data.date, "MMMM d"); } let notes = data.notes || ""; const selectedStaff = staffMembers.find((staff) => staff.id?.toString() === data.staffId) || staffMembers[0]; if (!selectedStaff) { console.error("No staff selected and no available staff in the list"); return; // Handle this case as well } // If there's no staff information in the notes, add it if (!notes.includes("Appointment with")) { notes = notes ? `${notes}\nAppointment with ${selectedStaff?.name}` : `Appointment with ${selectedStaff?.name}`; } const formattedDate = formatLocalDate(data.date); onSubmit({ ...data, userId: Number(user?.id), title, notes, patientId, date: formattedDate, startTime: data.startTime, endTime: data.endTime, }); }; return (
{ handleSubmit(data); }, (errors) => { console.error("Validation failed:", errors); } )} className="space-y-6" > ( Patient setPatientSearchTerm(e.target.value)} onClick={(e) => e.stopPropagation()} />
{/* Prefill patient only if main list does not already include them */} {prefillPatient && !patients.some( (p) => Number(p.id) === Number(prefillPatient.id) ) && (
{prefillPatient.firstName}{" "} {prefillPatient.lastName} DOB:{" "} {prefillPatient.dateOfBirth ? new Date( prefillPatient.dateOfBirth ).toLocaleDateString() : ""}{" "} • {prefillPatient.phone ?? ""}
)}
{isFetchingPatients ? (
Loading...
) : patients && patients.length > 0 ? ( patients.map((patient) => (
{patient.firstName} {patient.lastName} DOB:{" "} {new Date( patient.dateOfBirth ).toLocaleDateString()}{" "} • {patient.phone}
)) ) : (
No patients found
)}
)} /> ( Appointment Title{" "} (optional) )} />
( Start Time
)} /> ( End Time
)} />
( Appointment Type )} /> ( Status )} /> ( Doctor/Hygienist )} /> ( Notes