diff --git a/app/(admin)/admin/booking/[id]/page.tsx b/app/(admin)/admin/booking/[id]/page.tsx new file mode 100644 index 0000000..da9587d --- /dev/null +++ b/app/(admin)/admin/booking/[id]/page.tsx @@ -0,0 +1,800 @@ +"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, +} from "lucide-react"; +import { useAppTheme } from "@/components/ThemeProvider"; +import { getAppointmentDetail, scheduleAppointment, rejectAppointment } from "@/lib/actions/appointments"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { DatePicker } from "@/components/DatePicker"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +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 [scheduledDate, setScheduledDate] = useState(undefined); + const [scheduledTime, setScheduledTime] = useState("09:00"); + const [scheduledDuration, setScheduledDuration] = useState(60); + const [rejectionReason, setRejectionReason] = useState(""); + const [isScheduling, setIsScheduling] = useState(false); + const [isRejecting, setIsRejecting] = useState(false); + const { theme } = useAppTheme(); + const isDark = theme === "dark"; + + useEffect(() => { + const fetchAppointment = async () => { + if (!appointmentId) return; + + setLoading(true); + try { + const data = await getAppointmentDetail(appointmentId); + setAppointment(data); + } catch (error) { + console.error("Failed to fetch appointment details:", 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 timeSlots = Array.from({ length: 24 }, (_, i) => { + const hour = i.toString().padStart(2, "0"); + return `${hour}:00`; + }); + + 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) { + console.error("Failed to schedule appointment:", error); + 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) { + console.error("Failed to reject appointment:", error); + toast.error(error.message || "Failed to reject appointment"); + } finally { + setIsRejecting(false); + } + }; + + const copyToClipboard = (text: string, label: string) => { + navigator.clipboard.writeText(text); + toast.success(`${label} copied to clipboard`); + }; + + if (loading) { + return ( +
+
+ +

Loading appointment details...

+
+
+ ); + } + + if (!appointment) { + return ( +
+
+

Appointment not found

+ +
+
+ ); + } + + return ( +
+
+ {/* 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 +

+
+
+
+
+ +
+
+

+ {formatDate(appointment.scheduled_datetime)} +

+
+
+ +

+ {formatTime(appointment.scheduled_datetime)} +

+
+ {appointment.scheduled_duration && ( +
+ +

+ {appointment.scheduled_duration} minutes +

+
+ )} +
+
+
+
+
+ )} + + {/* Preferred Dates & Times */} + {(appointment.preferred_dates?.length > 0 || appointment.preferred_time_slots?.length > 0) && ( +
+
+

+ Preferred Availability +

+
+
+ {appointment.preferred_dates && appointment.preferred_dates.length > 0 && ( +
+

+ Preferred Dates +

+
+ {appointment.preferred_dates.map((date, idx) => ( + + {formatShortDate(date)} + + ))} +
+
+ )} + {appointment.preferred_time_slots && appointment.preferred_time_slots.length > 0 && ( +
+

+ Preferred Time Slots +

+
+ {appointment.preferred_time_slots.map((slot, idx) => ( + + {slot} + + ))} +
+
+ )} +
+
+ )} + + {/* Reason */} + {appointment.reason && ( +
+
+

+ + Reason for Appointment +

+
+
+

+ {appointment.reason} +

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

+ Rejection Reason +

+
+
+

+ {appointment.rejection_reason} +

+
+
+ )} + + {/* Meeting Information */} + {appointment.jitsi_meet_url && ( +
+
+

+

+
+
+ {appointment.jitsi_room_id && ( +
+

+ Meeting Room ID +

+
+

+ {appointment.jitsi_room_id} +

+ +
+
+ )} +
+

+ Meeting Link +

+ +
+ {appointment.can_join_meeting !== undefined && ( +
+
+

+ {appointment.can_join_meeting ? "Meeting is active - You can join now" : "Meeting is not available yet"} +

+
+ )} +
+
+ )} +
+ + {/* 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)} + +
+
+
+ + {/* Action Buttons */} + {appointment.status === "pending_review" && ( +
+
+ + +
+
+ )} + + {/* Join Meeting Button (if scheduled) */} + {appointment.status === "scheduled" && appointment.jitsi_meet_url && ( + + )} +
+
+
+ + {/* Google Meet Style Schedule Dialog */} + + + + + Schedule Appointment + + + Set date and time for {appointment.first_name} {appointment.last_name}'s appointment + + + +
+ {/* Date Selection */} +
+ +
+ +
+
+ + {/* Time Selection */} +
+ +
+ +
+
+ + {/* Duration Selection */} +
+ +
+
+ {[30, 60, 90, 120].map((duration) => ( + + ))} +
+
+
+ + {/* Preview */} + {scheduledDate && ( +
+

+ Appointment Preview +

+
+

+ {formatDate(scheduledDate.toISOString())} +

+

+ {new Date(`2000-01-01T${scheduledTime}`).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + })} • {scheduledDuration} minutes +

+
+
+ )} +
+ + + + + +
+
+ + {/* Reject Appointment Dialog */} + + + + + Reject Appointment Request + + + Reject appointment request from {appointment.first_name} {appointment.last_name} + + + +
+
+ +