"use client"; import { CalendarClock, CalendarDays, Clock, Plus, Search, Stethoscope, Users, } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { type ReactNode, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { AiBadge } from "@/components/ai-badge"; import { AppointmentDetailSheet } from "@/components/appointments/appointment-detail-sheet"; import { AddAppointmentDialog, type NewAppointment, } from "@/components/appointments/add-appointment-dialog"; import { CalendarDialog } from "@/components/appointments/calendar-dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { type Appointment, createAppointment, listAppointments, } from "@/lib/appointments"; import { notify } from "@/lib/toast"; import { cn } from "@/lib/utils"; export type { Appointment } from "@/lib/appointments"; // 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")}`; // "Today" is the real current date — appointments are persisted, not seeded, so // the schedule and calendar anchor to now. ISO YYYY-MM-DD. export const TODAY = keyOf(new Date()); // Sunday-anchored week window [start, end] as ISO keys, for the "This week" KPI. function weekRange(): { start: string; end: string } { const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - now.getDay()); const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 6); return { start: keyOf(start), end: keyOf(end) }; } type ApptStatus = Appointment["status"]; const statusVariant: Record< ApptStatus, "default" | "secondary" | "destructive" | "outline" > = { "checked-in": "default", confirmed: "secondary", completed: "outline", cancelled: "destructive", }; // "2026-06-05" -> "Wednesday, June 5" function formatDayKey(key: string): string { return new Date(`${key}T00:00:00`).toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", }); } function byTime(a: Appointment, b: Appointment) { return a.time.localeCompare(b.time); } function Kpi({ label, value, icon: Icon, }: { label: string; value: string; icon: typeof CalendarClock; }) { return (
{label} {value}
); } function ApptRow({ appt, onOpen, }: { appt: Appointment; onOpen?: (appt: Appointment) => void; }) { const { t } = useTranslation(); const interactive = Boolean(onOpen); return (
onOpen?.(appt) : undefined} onKeyDown={ interactive ? (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onOpen?.(appt); } } : undefined } role={interactive ? "button" : undefined} tabIndex={interactive ? 0 : undefined} > {appt.time} {appt.initials}
{appt.name} {appt.type} · {appt.provider}
{t(`appointments.status.${appt.status}`)}
); } export function ScheduleList({ items, onOpen, }: { items: Appointment[]; onOpen?: (appt: Appointment) => void; }) { return (
{items.map((appt) => ( ))}
); } function Section({ title, description, children, }: { title: string; description?: string; children: ReactNode; }) { return (

{title}

{description && (

{description}

)}
{children}
); } export function AppointmentsView() { const { t } = useTranslation(); // Deep-link from the AI chat appointment card: `?calendar=1&date=YYYY-MM-DD` // opens the month calendar on that date. const searchParams = useSearchParams(); const dateParam = searchParams.get("date"); const [addOpen, setAddOpen] = useState(false); const [calendarOpen, setCalendarOpen] = useState( () => searchParams.get("calendar") != null, ); const [appointments, setAppointments] = useState([]); const [query, setQuery] = useState(""); const [selectedAppt, setSelectedAppt] = useState(null); const [sheetOpen, setSheetOpen] = useState(false); const openAppt = (appt: Appointment) => { setSelectedAppt(appt); setSheetOpen(true); }; useEffect(() => { let active = true; listAppointments() .then((data) => { if (active) setAppointments(data); }) .catch(() => { /* api-client redirects on 401; otherwise leave the list empty */ }); return () => { active = false; }; }, []); // Persist a new appointment, then add the saved record to the list. const addAppointment = async (appt: NewAppointment) => { try { const created = await createAppointment({ fileNumber: appt.fileNumber, name: appt.name, initials: appt.initials, date: appt.date, time: appt.time, type: appt.type, provider: appt.provider, }); setAppointments((prev) => [...prev, created]); } catch { notify.error( t("appointments.addFailedTitle"), t("appointments.addFailedBody"), ); } }; const kpis = useMemo(() => { const { start, end } = weekRange(); const today = appointments.filter((a) => a.date === TODAY); const week = appointments.filter((a) => a.date >= start && a.date <= end); return [ { label: t("appointments.kpi.today"), value: String(today.length), icon: CalendarClock }, { label: t("appointments.kpi.thisWeek"), value: String(week.length), icon: Users }, { label: t("appointments.kpi.checkedIn"), value: String(today.filter((a) => a.status === "checked-in").length), icon: Stethoscope, }, { label: t("appointments.kpi.completed"), value: String(today.filter((a) => a.status === "completed").length), icon: Clock, }, ]; }, [appointments, t]); const todayItems = useMemo( () => appointments.filter((a) => a.date === TODAY).sort(byTime), [appointments], ); // Group future dates (after TODAY) into day sections, soonest first. const upcoming = useMemo(() => { const keys = [ ...new Set(appointments.map((a) => a.date).filter((d) => d > TODAY)), ].sort(); return keys.map((key) => ({ key, items: appointments.filter((a) => a.date === key).sort(byTime), })); }, [appointments]); const search = query.trim().toLowerCase(); // While searching, match name/type/provider across every date and group the // hits by date (soonest first) so each section keeps its day header. const results = useMemo(() => { if (!search) return []; const hits = appointments.filter( (a) => a.name.toLowerCase().includes(search) || a.type.toLowerCase().includes(search) || a.provider.toLowerCase().includes(search), ); const keys = [...new Set(hits.map((a) => a.date))].sort(); return keys.map((key) => ({ key, items: hits.filter((a) => a.date === key).sort(byTime), })); }, [appointments, search]); return (

{t("appointments.title")}

{t("appointments.subtitle")}

setQuery(event.target.value)} placeholder={t("appointments.searchPlaceholder")} value={query} />
{search ? ( results.length > 0 ? ( results.map((group) => (
)) ) : (

{t("appointments.noMatches")}

) ) : ( <>
{kpis.map((k) => ( ))}
{todayItems.length > 0 ? ( ) : (

{t("appointments.nothingToday")}

)}
{upcoming.map((group) => (
))} )} setAppointments((prev) => prev.filter((a) => a.id !== id)) } onOpenChange={setSheetOpen} onSaved={(updated) => setAppointments((prev) => prev.map((a) => (a.id === updated.id ? updated : a)), ) } open={sheetOpen} />
); }