From c7871cfb4636e0091cc4287927061b9e740ad560 Mon Sep 17 00:00:00 2001 From: iamkiddy Date: Mon, 24 Nov 2025 16:04:39 +0000 Subject: [PATCH] Enhance Booking component with appointment scheduling and rejection functionality. Integrate dialogs for scheduling and rejecting appointments, improving user interaction. Update layout to suppress hydration warnings and refine appointment data handling in BookNowPage for consistent ID management. --- app/(admin)/admin/booking/[id]/page.tsx | 800 ++++++++++++++++++++++++ app/(admin)/admin/booking/page.tsx | 324 +++++++++- app/(pages)/book-now/page.tsx | 10 +- app/layout.tsx | 2 +- 4 files changed, 1113 insertions(+), 23 deletions(-) create mode 100644 app/(admin)/admin/booking/[id]/page.tsx 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} + + + +
+
+ +