import React from "react"; import { Calendar as CalendarIcon, CalendarCheck, Clock, ExternalLink, } from "lucide-react"; import { Avatar, AvatarFallback } from "./avatar"; import { Badge } from "./badge"; import { Button } from "./button"; import { Calendar as CalendarPicker } from "./calendar"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "./dialog"; import { Label } from "./label"; import { Separator } from "./separator"; import { Textarea } from "./textarea"; import { safeWindowOpen } from "@/lib/safe-url"; import { AppointmentSlotSection, type AppointmentStatus, type AppointmentTimeSlot, } from "./appointment-time-slot-picker"; // Re-export so consumers who import these types from this module still work export type { AppointmentStatus } from "./appointment-time-slot-picker"; export type { AppointmentTimeSlot as AppointmentUpcomingSlot } from "./appointment-time-slot-picker"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- /** @deprecated Use AppointmentStatus from appointment-time-slot-picker */ export type AppointmentUpcomingStatus = AppointmentStatus; export interface AppointmentUpcomingAdvisor { name: string; role: string; company: string; avatarInitials: string; } export interface AppointmentUpcomingData { status: AppointmentStatus; advisor: AppointmentUpcomingAdvisor; /** Human-readable appointment type — shown as the card header title */ appointmentType?: string; /** Display date string — e.g. "2026-04-22" */ date: string; /** Display start time — e.g. "10:00 AM" */ timeStart: string; /** Display end time — e.g. "10:30 AM" */ timeEnd: string; /** Optional Google Calendar / iCal link shown on Confirmed appointments */ calendarLink?: string; /** Original date before advisor rescheduled — only set when status is "rescheduled" */ originalDate?: string; originalTimeStart?: string; originalTimeEnd?: string; } export interface AppointmentUpcomingCardProps { /** Pass null or undefined to show the empty state with a Book CTA */ appointment?: AppointmentUpcomingData | null; /** AM time slots shown in the reschedule dialog */ amSlots?: AppointmentTimeSlot[]; /** PM time slots shown in the reschedule dialog */ pmSlots?: AppointmentTimeSlot[]; onBookAppointment?: () => void; /** * Called when the client submits a reschedule request. * @param date The preferred date the client chose. * @param slot The preferred time slot (undefined when no slot data is provided). * @param note Optional note from the client. */ onReschedule?: ( date: Date, slot: AppointmentTimeSlot | undefined, note: string, ) => void; /** * Called when the client confirms cancellation. * @param reason Optional cancellation reason entered by the client. */ onCancel?: (reason: string) => void; onAcceptReschedule?: () => void; } // --------------------------------------------------------------------------- // AppointmentUpcomingCard // --------------------------------------------------------------------------- /** * Client-facing upcoming appointment card. * * Five states: * - **Empty** — no appointment; prompts user to book * - **Pending** — client booked, waiting for advisor confirmation * - **Confirmed** — advisor confirmed; shows Add to Calendar if link provided * - **Rescheduled** — advisor proposed a new time; Accept or Request Another Time * - **Cancelled** — advisor cancelled; prompts user to rebook */ export function AppointmentUpcomingCard({ appointment, amSlots = [], pmSlots = [], onBookAppointment, onReschedule, onCancel, onAcceptReschedule, }: AppointmentUpcomingCardProps) { // ── Dialog state ───────────────────────────────────────────────────────────── const [calendarDialogOpen, setCalendarDialogOpen] = React.useState(false); const [acceptDialogOpen, setAcceptDialogOpen] = React.useState(false); const [rescheduleDialogOpen, setRescheduleDialogOpen] = React.useState(false); const [cancelDialogOpen, setCancelDialogOpen] = React.useState(false); // Reschedule form const [rescheduleDate, setRescheduleDate] = React.useState( new Date(), ); const [rescheduleSlot, setRescheduleSlot] = React.useState< AppointmentTimeSlot | undefined >(undefined); const [rescheduleNote, setRescheduleNote] = React.useState(""); // Cancel form const [cancelReason, setCancelReason] = React.useState(""); const resetRescheduleForm = () => { setRescheduleDate(new Date()); setRescheduleSlot(undefined); setRescheduleNote(""); }; const hasSlotsData = amSlots.length > 0 || pmSlots.length > 0; const canSubmitReschedule = hasSlotsData ? !!rescheduleDate && !!rescheduleSlot : !!rescheduleDate; // ── Empty state ───────────────────────────────────────────────────────────── if (!appointment) { return (

No upcoming appointments

Schedule time with your adviser to discuss your financial goals.

); } const isPending = appointment.status === "pending"; const isRescheduled = appointment.status === "rescheduled"; const isCancelled = appointment.status === "cancelled"; // ── Status badge ───────────────────────────────────────────────────────────── const statusBadge = (() => { if (isPending) return Pending; if (isRescheduled) return Rescheduled; if (isCancelled) return Cancelled; return Confirmed; })(); // ── Status notice banner ────────────────────────────────────────────────────── const noticeBanner = (() => { if (isPending) { return (

Your booking request has been sent. Your adviser will confirm the appointment shortly.

); } if (isRescheduled) { return (

Your adviser has proposed a new time. Please review and respond below.

); } if (isCancelled) { return (

This appointment has been cancelled by your adviser.

); } return null; })(); const headerTitle = appointment.appointmentType ? appointment.appointmentType.toUpperCase() : "UPCOMING APPOINTMENT"; // ── Filled state ────────────────────────────────────────────────────────────── return ( <>
{/* Header */}

{headerTitle}

{statusBadge}
{/* Advisor info */}
{appointment.advisor.avatarInitials}

{appointment.advisor.name}

{appointment.advisor.role}

{appointment.advisor.company}

{/* Date/time */}
{isRescheduled ? ( <>

Original time

{appointment.originalDate ?? appointment.date}
{appointment.originalTimeStart ?? appointment.timeStart} –{" "} {appointment.originalTimeEnd ?? appointment.timeEnd}

New proposed time

{appointment.date}
{appointment.timeStart} – {appointment.timeEnd}
) : ( <>
{appointment.date}
{appointment.timeStart} – {appointment.timeEnd}
)}
{/* Status notice */} {noticeBanner && ( <> {noticeBanner} )} {/* Actions */}
{/* Confirmed — add to calendar */} {!isPending && !isRescheduled && !isCancelled && appointment.calendarLink && ( )} {/* Rescheduled — accept or request another time */} {isRescheduled && ( <> )} {/* Cancelled — rebook */} {isCancelled && ( )} {/* Pending / Confirmed — reschedule + cancel */} {!isRescheduled && !isCancelled && ( <> )}
{/* ── Accept New Time dialog ────────────────────────────────────────── */} Accept New Time Confirm you want to accept{" "} {appointment.date} {" "} from{" "} {appointment.timeStart} – {appointment.timeEnd} {" "} as your new appointment time. Your adviser will be notified. }> Keep Waiting {/* ── Add to Calendar dialog ─────────────────────────────────────────── */} Add to Google Calendar You'll be taken to Google Calendar to save this appointment. Make sure you're signed in to the correct account. }> Cancel {/* ── Request Reschedule dialog ──────────────────────────────────────── */} { if (!next) resetRescheduleForm(); setRescheduleDialogOpen(next); }} > Request Reschedule Choose your preferred date and time. Your adviser will review the request and confirm a new slot. {/* Current booking */}

Current booking

{appointment.date} {appointment.timeStart} – {appointment.timeEnd}
{/* Date picker */}
{ setRescheduleDate(d); setRescheduleSlot(undefined); }} captionLayout="label" fromDate={new Date()} disabled={{ before: new Date() }} className="border border-border" />
{/* Time slots */}
{hasSlotsData ? ( rescheduleDate ? ( <>

Preferred time

) : (

Preferred time

Pick a date on the left to see available slots.

) ) : null}
{/* Note — full width */}