import React from "react"; import { Avatar, AvatarFallback } from "./avatar"; import { Badge } from "./badge"; import { Button } from "./button"; import { Separator } from "./separator"; import { Sheet, SheetContent } from "./sheet"; import { AppointmentConfirmAction, AppointmentConfirmDialog, AppointmentRescheduleDialog, } from "./appointment-action-dialogs"; import type { AppointmentMeetingFormat, AppointmentStatus, AppointmentTimeSlot, } from "./appointment-time-slot-picker"; import { AlertCircle, Calendar, CalendarClock, Check, Circle, CircleCheck, CircleUser, Clock, ExternalLink, FileText, Mail, MapPin, Phone, RefreshCw, Sparkles, Users, Video, X, } from "lucide-react"; // Re-export so consumers who import these types from this module still work export type { AppointmentStatus, AppointmentMeetingFormat, } from "./appointment-time-slot-picker"; export interface AppointmentMeetingActionItem { label: string; done?: boolean; } /** AI-generated recap of a completed meeting, stored on the booking. */ export interface AppointmentMeetingSummary { /** Short recap of what was discussed. */ summary?: string; /** Follow-up items, optionally marked done. */ actionItems?: AppointmentMeetingActionItem[]; } export interface AppointmentDetailItem { id: string; status: AppointmentStatus; clientName: string; clientAvatarInitials: string; /** Client ID — used to pre-select client when rebooking after cancellation */ clientId?: string; date: string; timeStart: string; timeEnd: string; notes?: string; /** Reason provided when the appointment was cancelled */ cancelReason?: string; /** How the meeting is conducted */ meetingFormat?: AppointmentMeetingFormat; /** Formatted location string — shown for offline meetings */ meetingLocation?: string; /** Phone number — shown for call meetings */ callPhone?: string; /** Join URL — shown for google-meet and microsoft-teams meetings */ meetingLink?: string; /** AI meeting summary — shown once the meeting has been recorded and summarised */ meetingSummary?: AppointmentMeetingSummary; } export type LoanApplicationStatus = | "finished" | "in-progress" | "sent-request" | "not-start"; /** Extra client profile fields not stored on the appointment itself */ export interface AppointmentClientProfile { phone?: string; email?: string; accountType?: "Individual" | "Joint"; loanApplicationStatus?: LoanApplicationStatus; } export interface AppointmentDetailSheetProps { appointment: AppointmentDetailItem | undefined; open: boolean; onOpenChange: (v: boolean) => void; clientProfile?: AppointmentClientProfile; amSlots: AppointmentTimeSlot[]; pmSlots: AppointmentTimeSlot[]; /** * Fired when the user selects a different date in the reschedule dialog. * Use this to fetch fresh `amSlots`/`pmSlots` for the new date. */ onDateChange?: (date: Date) => void; /** * Advisor's weekly availability schedule. Days with `enabled: false` are * disabled in the reschedule date picker. */ schedule?: { day: string; enabled: boolean }[]; /** True while slots are being fetched for the selected date. */ isLoadingSlots?: boolean; onAccept?: (id: string) => void; onDecline?: (id: string) => void; onReschedule?: ( id: string, date: Date, slot: AppointmentTimeSlot, note: string ) => void; /** Called when "Book a New Appointment" is clicked on a cancelled appointment */ onBookNew?: (clientId?: string) => void; } // --------------------------------------------------------------------------- // Status config // --------------------------------------------------------------------------- const STATUS_CONFIG: Record< AppointmentStatus, { variant: "warning" | "success" | "destructive" | "info"; label: string; icon: React.ReactNode; } > = { pending: { variant: "warning", label: "Pending", icon: , }, confirmed: { variant: "success", label: "Confirmed", icon: , }, cancelled: { variant: "destructive", label: "Cancelled", icon: , }, rescheduled: { variant: "info", label: "Rescheduled", icon: , }, }; // --------------------------------------------------------------------------- // Meeting format config // --------------------------------------------------------------------------- const ICON_CLASS = "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground"; const MEETING_FORMAT_META: Record< AppointmentMeetingFormat, { icon: React.ReactNode; label: string } > = { call: { icon: , label: "Phone Call" }, "google-meet": { icon: , label: "Google Meet", }, "microsoft-teams": { icon: , label: "Microsoft Teams", }, online: { icon: , label: "Online Meeting" }, offline: { icon: , label: "In Person" }, }; /** Formats that use a join link rather than a phone number or physical location */ const ONLINE_FORMATS = new Set([ "google-meet", "microsoft-teams", "online", ]); // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function AppointmentDetailSheet({ appointment, open, onOpenChange, clientProfile, amSlots, pmSlots, onDateChange, schedule, isLoadingSlots, onAccept, onDecline, onReschedule, onBookNew, }: AppointmentDetailSheetProps) { const [confirmAction, setConfirmAction] = React.useState(null); const [rescheduleOpen, setRescheduleOpen] = React.useState(false); if (!appointment) return null; const { variant, label, icon } = STATUS_CONFIG[appointment.status]; const isCancelled = appointment.status === "cancelled"; const isConfirmed = appointment.status === "confirmed"; const fmt = appointment.meetingFormat; const meetingMeta = fmt ? MEETING_FORMAT_META[fmt] : undefined; return ( <> {/* ── Header: avatar + name/badge + action buttons ── */} {appointment.clientAvatarInitials} {appointment.clientName} {icon} {label} {/* Action buttons */} {isCancelled ? ( onBookNew?.(appointment.clientId)} > Book a New Appointment ) : ( <> setConfirmAction("accept")} className="flex-1 gap-1.5" > Accept setConfirmAction("decline")} className="flex-1 gap-1.5 text-destructive hover:text-destructive" > Decline setRescheduleOpen(true)} className="flex-1 gap-1.5" > Reschedule > )} {/* ── Appointment details ── */} {appointment.date} {appointment.timeStart} – {appointment.timeEnd} {fmt && meetingMeta && ( {meetingMeta.icon} {meetingMeta.label} {fmt === "call" && appointment.callPhone && ( {appointment.callPhone} )} {ONLINE_FORMATS.has(fmt) && appointment.meetingLink && ( Join Meeting )} {fmt === "offline" && appointment.meetingLocation && ( {appointment.meetingLocation} )} )} {/* ── Client profile ── */} {clientProfile && (clientProfile.phone !== undefined || clientProfile.email !== undefined || clientProfile.accountType !== undefined || clientProfile.loanApplicationStatus !== undefined) && ( <> Client Profile {clientProfile.phone && ( {clientProfile.phone} )} {clientProfile.email && ( {clientProfile.email} )} {clientProfile.accountType !== undefined && ( Account type {clientProfile.accountType === "Joint" ? ( ) : ( )} {clientProfile.accountType} )} {clientProfile.loanApplicationStatus !== undefined && ( Loan application {clientProfile.loanApplicationStatus === "finished" && ( Finished )} {clientProfile.loanApplicationStatus === "in-progress" && ( In Progress )} {clientProfile.loanApplicationStatus === "sent-request" && ( Sent Request )} {clientProfile.loanApplicationStatus === "not-start" && ( Not Started )} )} > )} {/* ── Meeting Summary ── */} {appointment.meetingSummary && (appointment.meetingSummary.summary || (appointment.meetingSummary.actionItems?.length ?? 0) > 0) && ( <> Meeting Summary {appointment.meetingSummary.summary && ( {appointment.meetingSummary.summary} )} {(appointment.meetingSummary.actionItems?.length ?? 0) > 0 && ( Action items {appointment.meetingSummary.actionItems?.map( (item, i) => ( {item.done ? ( ) : ( )} {item.label} ) )} )} > )} {/* ── Notes ── */} {appointment.notes && ( <> Notes {appointment.notes} > )} {/* ── Cancellation Reason ── */} {appointment.cancelReason && ( <> Cancellation Reason {appointment.cancelReason} > )} {/* Dialogs rendered outside Sheet to avoid z-index issues */} !v && setConfirmAction(null)} action={confirmAction ?? "accept"} clientName={appointment.clientName} onConfirm={() => { if (confirmAction === "accept") { onAccept?.(appointment.id); } else { onDecline?.(appointment.id); } setConfirmAction(null); onOpenChange(false); }} /> { onReschedule?.(appointment.id, date, slot, note); setRescheduleOpen(false); }} /> > ); }
{appointment.clientName}
Client Profile
Meeting Summary
{appointment.meetingSummary.summary}
Action items
Notes
{appointment.notes}
Cancellation Reason
{appointment.cancelReason}