"use client"; import { useState, useEffect } from "react"; import { useParams, useRouter } from "next/navigation"; import { Calendar, Clock, User, Video, CalendarCheck, X, Loader2, ArrowLeft, Mail, Phone as PhoneIcon, MessageSquare, CheckCircle2, ExternalLink, Copy, MapPin, Pencil, } from "lucide-react"; import { useAppTheme } from "@/components/ThemeProvider"; import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments, startMeeting, endMeeting, rescheduleAppointment, cancelAppointment } from "@/lib/actions/appointments"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ScheduleAppointmentDialog } from "@/components/ScheduleAppointmentDialog"; import { toast } from "sonner"; import type { Appointment } from "@/lib/models/appointments"; export default function AppointmentDetailPage() { const params = useParams(); const router = useRouter(); const appointmentId = params.id as string; const [appointment, setAppointment] = useState(null); const [loading, setLoading] = useState(true); const [scheduleDialogOpen, setScheduleDialogOpen] = useState(false); const [rejectDialogOpen, setRejectDialogOpen] = useState(false); const [rescheduleDialogOpen, setRescheduleDialogOpen] = useState(false); const [scheduledDate, setScheduledDate] = useState(undefined); const [scheduledTime, setScheduledTime] = useState("09:00"); const [scheduledDuration, setScheduledDuration] = useState(60); const [rescheduleDate, setRescheduleDate] = useState(undefined); const [rescheduleTime, setRescheduleTime] = useState("09:00"); const [rescheduleDuration, setRescheduleDuration] = useState(60); const [rejectionReason, setRejectionReason] = useState(""); const [isScheduling, setIsScheduling] = useState(false); const [isRejecting, setIsRejecting] = useState(false); const [isRescheduling, setIsRescheduling] = useState(false); const [isStartingMeeting, setIsStartingMeeting] = useState(false); const [isEndingMeeting, setIsEndingMeeting] = useState(false); const [cancelDialogOpen, setCancelDialogOpen] = useState(false); const [isCancelling, setIsCancelling] = useState(false); const { theme } = useAppTheme(); const isDark = theme === "dark"; useEffect(() => { const fetchAppointment = async () => { if (!appointmentId) return; setLoading(true); try { // Fetch both detail and list to get selected_slots from list endpoint const [detailData, listData] = await Promise.all([ getAppointmentDetail(appointmentId), listAppointments().catch(() => []) // Fallback to empty array if list fails ]); // Find matching appointment in list to get selected_slots const listAppointment = Array.isArray(listData) ? listData.find((apt: Appointment) => apt.id === appointmentId) : null; // Merge selected_slots from list into detail data if (listAppointment && listAppointment.selected_slots && Array.isArray(listAppointment.selected_slots) && listAppointment.selected_slots.length > 0) { detailData.selected_slots = listAppointment.selected_slots; } setAppointment(detailData); } catch (error) { toast.error("Failed to load appointment details"); router.push("/admin/booking"); } finally { setLoading(false); } }; fetchAppointment(); }, [appointmentId, router]); const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric", }); }; const formatTime = (dateString: string) => { const date = new Date(dateString); return date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, }); }; const formatShortDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }); }; const getStatusColor = (status: string) => { const normalized = status.toLowerCase(); if (isDark) { switch (normalized) { case "scheduled": return "bg-blue-500/20 text-blue-300 border-blue-500/30"; case "completed": return "bg-green-500/20 text-green-300 border-green-500/30"; case "rejected": case "cancelled": return "bg-red-500/20 text-red-300 border-red-500/30"; case "pending_review": case "pending": return "bg-yellow-500/20 text-yellow-300 border-yellow-500/30"; default: return "bg-gray-700 text-gray-200 border-gray-600"; } } switch (normalized) { case "scheduled": return "bg-blue-50 text-blue-700 border-blue-200"; case "completed": return "bg-green-50 text-green-700 border-green-200"; case "rejected": case "cancelled": return "bg-red-50 text-red-700 border-red-200"; case "pending_review": case "pending": return "bg-yellow-50 text-yellow-700 border-yellow-200"; default: return "bg-gray-100 text-gray-700 border-gray-300"; } }; const formatStatus = (status: string) => { return status.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase()); }; const handleSchedule = async () => { if (!appointment || !scheduledDate) return; setIsScheduling(true); try { const dateTime = new Date(scheduledDate); const [hours, minutes] = scheduledTime.split(":").map(Number); dateTime.setHours(hours, minutes, 0, 0); await scheduleAppointment(appointment.id, { scheduled_datetime: dateTime.toISOString(), scheduled_duration: scheduledDuration, }); toast.success("Appointment scheduled successfully"); setScheduleDialogOpen(false); // Refresh appointment data const updated = await getAppointmentDetail(appointment.id); setAppointment(updated); } catch (error: any) { toast.error(error.message || "Failed to schedule appointment"); } finally { setIsScheduling(false); } }; const handleReject = async () => { if (!appointment) return; setIsRejecting(true); try { await rejectAppointment(appointment.id, { rejection_reason: rejectionReason || undefined, }); toast.success("Appointment rejected successfully"); setRejectDialogOpen(false); // Refresh appointment data const updated = await getAppointmentDetail(appointment.id); setAppointment(updated); } catch (error: any) { toast.error(error.message || "Failed to reject appointment"); } finally { setIsRejecting(false); } }; const handleReschedule = async () => { if (!appointment || !rescheduleDate) return; setIsRescheduling(true); try { const dateTime = new Date(rescheduleDate); const [hours, minutes] = rescheduleTime.split(":").map(Number); dateTime.setHours(hours, minutes, 0, 0); const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; await rescheduleAppointment(appointment.id, { new_scheduled_datetime: dateTime.toISOString(), new_scheduled_duration: rescheduleDuration, timezone: userTimezone, }); toast.success("Appointment rescheduled successfully"); setRescheduleDialogOpen(false); // Refresh appointment data const updated = await getAppointmentDetail(appointment.id); setAppointment(updated); } catch (error: any) { toast.error(error.message || "Failed to reschedule appointment"); } finally { setIsRescheduling(false); } }; const handleCancelAppointment = async () => { if (!appointment) return; setIsCancelling(true); try { await cancelAppointment(appointment.id); toast.success("Appointment cancelled successfully"); setCancelDialogOpen(false); // Refetch appointment to get updated status const updatedAppointment = await getAppointmentDetail(appointment.id); setAppointment(updatedAppointment); router.push("/admin/booking"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to cancel appointment"; toast.error(errorMessage); } finally { setIsCancelling(false); } }; const copyToClipboard = (text: string, label: string) => { navigator.clipboard.writeText(text); toast.success(`${label} copied to clipboard`); }; const handleStartMeeting = async () => { if (!appointment) return; setIsStartingMeeting(true); try { const updated = await startMeeting(appointment.id); setAppointment(updated); toast.success("Meeting started successfully"); } catch (error: any) { toast.error(error.message || "Failed to start meeting"); } finally { setIsStartingMeeting(false); } }; const handleEndMeeting = async () => { if (!appointment) return; setIsEndingMeeting(true); try { const updated = await endMeeting(appointment.id); setAppointment(updated); toast.success("Meeting ended successfully"); } catch (error: any) { toast.error(error.message || "Failed to end meeting"); } finally { setIsEndingMeeting(false); } }; if (loading) { return (

Loading appointment details...

); } if (!appointment) { return (

Appointment not found

); } return (
{/* Page Header */}
{appointment.first_name[0]}{appointment.last_name[0]}

{appointment.first_name} {appointment.last_name}

Appointment Request

{appointment.status === "scheduled" && } {formatStatus(appointment.status)}
{/* Main Content - Left Column (2/3) */}
{/* Patient Information Card */}

Patient Information

Full Name

{appointment.first_name} {appointment.last_name}

Email Address

{appointment.email}

{appointment.phone && (

Phone Number

{appointment.phone}

)}
{/* Appointment Details Card */} {appointment.scheduled_datetime && (

Scheduled Appointment

{appointment.status === "scheduled" && ( )}

{formatDate(appointment.scheduled_datetime)}

{formatTime(appointment.scheduled_datetime)}

{appointment.scheduled_duration && (

{appointment.scheduled_duration} minutes

)}
)} {/* Selected Slots */} {appointment.selected_slots && Array.isArray(appointment.selected_slots) && appointment.selected_slots.length > 0 && (

Selected Time Slots {appointment.are_preferences_available !== undefined && ( {appointment.are_preferences_available ? "All Available" : "Partially Available"} )}

{(() => { const dayNames: Record = { 0: "Monday", 1: "Tuesday", 2: "Wednesday", 3: "Thursday", 4: "Friday", 5: "Saturday", 6: "Sunday", }; const timeSlotLabels: Record = { morning: "Morning", afternoon: "Lunchtime", evening: "Evening", }; // Time slot order: morning, afternoon (lunchtime), evening const timeSlotOrder: Record = { morning: 0, afternoon: 1, evening: 2, }; // Group slots by date const slotsByDate: Record = {}; appointment.selected_slots.forEach((slot: any) => { const date = slot.date || ""; if (!slotsByDate[date]) { slotsByDate[date] = []; } slotsByDate[date].push(slot); }); // Sort dates and slots within each date const sortedDates = Object.keys(slotsByDate).sort((a, b) => { return new Date(a).getTime() - new Date(b).getTime(); }); return (
{sortedDates.map((date) => { // Sort slots within this date by time slot order const slots = slotsByDate[date].sort((a: any, b: any) => { const aSlot = String(a.time_slot).toLowerCase().trim(); const bSlot = String(b.time_slot).toLowerCase().trim(); const aOrder = timeSlotOrder[aSlot] ?? 999; const bOrder = timeSlotOrder[bSlot] ?? 999; return aOrder - bOrder; }); return (

{formatShortDate(date)}

{slots.length > 0 && slots[0]?.day !== undefined && (

{dayNames[slots[0].day] || `Day ${slots[0].day}`}

)}
{slots.map((slot: any, idx: number) => { const timeSlot = String(slot.time_slot).toLowerCase().trim(); const timeLabel = timeSlotLabels[timeSlot] || slot.time_slot; return ( {timeLabel} ); })}
); })}
); })()}
)} {/* Reason */} {appointment.reason && (

Reason for Appointment

{appointment.reason}

)} {/* Rejection Reason */} {appointment.rejection_reason && (

Rejection Reason

{appointment.rejection_reason}

)} {/* Meeting Information */} {appointment.moderator_join_url && (

{appointment.jitsi_room_id && (

Meeting Room ID

{appointment.jitsi_room_id}

)}

Moderator Meeting Link

{appointment.moderator_join_url}
{appointment.can_join_as_moderator !== undefined && (

{appointment.can_join_as_moderator ? "Meeting is active - You can join as moderator" : appointment.scheduled_datetime ? `Meeting would be available to join starting at ${formatTime(appointment.scheduled_datetime)}` : "Meeting would be available shortly"}

)}
)}
{/* Sidebar - Right Column (1/3) */}
{/* Quick Info Card */}

Quick Info

Created

{formatShortDate(appointment.created_at)}

{formatTime(appointment.created_at)}

Status

{appointment.status === "scheduled" && } {formatStatus(appointment.status)}
{appointment.scheduled_datetime && (

Meeting Information

Meeting Start Time:

{formatDate(appointment.scheduled_datetime)} at {formatTime(appointment.scheduled_datetime)}

How to Access:

Up to 10 minutes before the meeting is scheduled to begin, click the "Start Meeting" button below to begin the session. Once started, participants can join using the meeting link. You can also use the{" "} {appointment.moderator_join_url ? ( moderator link ) : ( "moderator link" )}{" "} to join directly.

)}
{/* Action Buttons */} {appointment.status === "pending_review" && (
)} {/* Cancel Appointment Button */} {appointment.status === "scheduled" && (
)} {/* Meeting Button (if scheduled) */} {appointment.status === "scheduled" && appointment.moderator_join_url && (
{(() => { // Check if meeting has ended const endedAt = appointment.meeting_ended_at; const hasEnded = endedAt != null && endedAt !== ""; // If meeting has ended, show "Meeting has ended" if (hasEnded) { return ( ); } // Check if can join as moderator (handle both boolean and string values) const canJoinAsModerator = appointment.can_join_as_moderator === true || appointment.can_join_as_moderator === "true"; // Check if meeting has started (handle both field names) const startedAt = appointment.started_at || appointment.meeting_started_at; const hasStarted = startedAt != null && startedAt !== ""; // If can_join_as_moderator != true, display "Meeting would be available shortly" if (!canJoinAsModerator) { const meetingTime = appointment.scheduled_datetime ? formatTime(appointment.scheduled_datetime) : "the scheduled time"; return ( ); } // If can_join_as_moderator == true && started_at != null, show "Join Now" button if (hasStarted) { return ( <> ); } // If can_join_as_moderator == true && started_at == null, show "Start Meeting" button return ( ); })()}
)}
{/* Schedule Appointment Dialog */} {/* Reschedule Appointment Dialog */} {/* Reject Appointment Dialog */} Reject Appointment Request Reject appointment request from {appointment.first_name} {appointment.last_name}