"use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { Calendar, Clock, Video, Search, CalendarCheck, X, Loader2, User, Settings, Check, } from "lucide-react"; import { useAppTheme } from "@/components/ThemeProvider"; import { listAppointments, scheduleAppointment, rejectAppointment } from "@/lib/actions/appointments"; import { useAppointments } from "@/hooks/useAppointments"; import { Input } from "@/components/ui/input"; 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 Booking() { const router = useRouter(); const [appointments, setAppointments] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [scheduleDialogOpen, setScheduleDialogOpen] = useState(false); const [rejectDialogOpen, setRejectDialogOpen] = useState(false); const [selectedAppointment, setSelectedAppointment] = useState(null); 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"; // Availability management const { adminAvailability, isLoadingAdminAvailability, updateAvailability, isUpdatingAvailability, refetchAdminAvailability, } = useAppointments(); const [selectedDays, setSelectedDays] = useState([]); const [availabilityDialogOpen, setAvailabilityDialogOpen] = useState(false); const [dayTimeSlots, setDayTimeSlots] = useState>({}); const daysOfWeek = [ { value: 0, label: "Monday" }, { value: 1, label: "Tuesday" }, { value: 2, label: "Wednesday" }, { value: 3, label: "Thursday" }, { value: 4, label: "Friday" }, { value: 5, label: "Saturday" }, { value: 6, label: "Sunday" }, ]; // Time slots will be loaded from API in the useEffect below // Initialize selected days and time slots when availability is loaded useEffect(() => { // Try new format first (availability_schedule) if (adminAvailability?.availability_schedule) { const schedule = adminAvailability.availability_schedule; const days = Object.keys(schedule).map(Number); setSelectedDays(days); // Convert schedule format to dayTimeSlots format const initialSlots: Record = {}; days.forEach((day) => { initialSlots[day] = schedule[day.toString()] || []; }); setDayTimeSlots(initialSlots); } // Fallback to legacy format else if (adminAvailability?.available_days) { setSelectedDays(adminAvailability.available_days); // Load saved time slots or use defaults const savedTimeSlots = localStorage.getItem("adminAvailabilityTimeSlots"); let initialSlots: Record = {}; if (savedTimeSlots) { try { const parsed = JSON.parse(savedTimeSlots); // Only use saved slots for days that are currently available adminAvailability.available_days.forEach((day) => { // Map old time slot names to new ones const oldSlots = parsed[day] || []; initialSlots[day] = oldSlots.map((slot: string) => { if (slot === "lunchtime") return "afternoon"; if (slot === "afternoon") return "evening"; return slot; }).filter((slot: string) => ["morning", "afternoon", "evening"].includes(slot)); // If no valid slots after mapping, use defaults if (initialSlots[day].length === 0) { initialSlots[day] = ["morning", "afternoon"]; } }); } catch (error) { // If parsing fails, use defaults adminAvailability.available_days.forEach((day) => { initialSlots[day] = ["morning", "afternoon"]; }); } } else { // No saved slots, use defaults adminAvailability.available_days.forEach((day) => { initialSlots[day] = ["morning", "afternoon"]; }); } setDayTimeSlots(initialSlots); } }, [adminAvailability]); const timeSlotOptions = [ { value: "morning", label: "Morning" }, { value: "afternoon", label: "Lunchtime" }, { value: "evening", label: "Evening" }, ]; const handleDayToggle = (day: number) => { setSelectedDays((prev) => { const newDays = prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day].sort(); // Initialize time slots for newly added day if (!prev.includes(day) && !dayTimeSlots[day]) { setDayTimeSlots((prevSlots) => ({ ...prevSlots, [day]: ["morning", "afternoon"], })); } // Remove time slots for removed day if (prev.includes(day)) { setDayTimeSlots((prevSlots) => { const newSlots = { ...prevSlots }; delete newSlots[day]; return newSlots; }); } return newDays; }); }; const handleTimeSlotToggle = (day: number, slot: string) => { setDayTimeSlots((prev) => { const currentSlots = prev[day] || []; const newSlots = currentSlots.includes(slot) ? currentSlots.filter((s) => s !== slot) : [...currentSlots, slot]; return { ...prev, [day]: newSlots, }; }); }; const handleSaveAvailability = async () => { if (selectedDays.length === 0) { toast.error("Please select at least one available day"); return; } // Validate all time slots for (const day of selectedDays) { const timeSlots = dayTimeSlots[day]; if (!timeSlots || timeSlots.length === 0) { toast.error(`Please select at least one time slot for ${daysOfWeek.find(d => d.value === day)?.label}`); return; } // Validate time slots are valid const validSlots = ["morning", "afternoon", "evening"]; const invalidSlots = timeSlots.filter(slot => !validSlots.includes(slot)); if (invalidSlots.length > 0) { toast.error(`Invalid time slots: ${invalidSlots.join(", ")}. Only morning, afternoon, and evening are allowed.`); return; } } try { // Build availability_schedule format: {"0": ["morning", "evening"], "1": ["afternoon"]} const availabilitySchedule: Record = {}; selectedDays.forEach(day => { const timeSlots = dayTimeSlots[day]; if (timeSlots && timeSlots.length > 0) { // Ensure only valid time slots and remove duplicates const validSlots = timeSlots .filter((slot): slot is "morning" | "afternoon" | "evening" => ["morning", "afternoon", "evening"].includes(slot) ) .filter((slot, index, self) => self.indexOf(slot) === index); // Remove duplicates if (validSlots.length > 0) { availabilitySchedule[day.toString()] = validSlots; } } }); // Validate we have at least one day with slots if (Object.keys(availabilitySchedule).length === 0) { toast.error("Please select at least one day with valid time slots"); return; } // Send in new format await updateAvailability({ availability_schedule: availabilitySchedule }); // Also save to localStorage for backwards compatibility localStorage.setItem("adminAvailabilityTimeSlots", JSON.stringify(dayTimeSlots)); toast.success("Availability updated successfully!"); // Refresh availability data if (refetchAdminAvailability) { await refetchAdminAvailability(); } setAvailabilityDialogOpen(false); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to update availability"; toast.error(`Failed to update availability: ${errorMessage}`, { duration: 5000, }); } }; const handleOpenAvailabilityDialog = () => { // Try new format first (availability_schedule) if (adminAvailability?.availability_schedule) { const schedule = adminAvailability.availability_schedule; const days = Object.keys(schedule).map(Number); setSelectedDays(days); // Convert schedule format to dayTimeSlots format const initialSlots: Record = {}; days.forEach((day) => { initialSlots[day] = schedule[day.toString()] || ["morning", "afternoon"]; }); setDayTimeSlots(initialSlots); } // Fallback to legacy format else if (adminAvailability?.available_days) { setSelectedDays(adminAvailability.available_days); // Initialize time slots for each day const initialSlots: Record = {}; adminAvailability.available_days.forEach((day) => { initialSlots[day] = dayTimeSlots[day] || ["morning", "afternoon"]; }); setDayTimeSlots(initialSlots); } else { // No existing availability, start fresh setSelectedDays([]); setDayTimeSlots({}); } setAvailabilityDialogOpen(true); }; useEffect(() => { const fetchBookings = async () => { setLoading(true); try { const data = await listAppointments(); setAppointments(data || []); } catch (error) { toast.error("Failed to load appointments. Please try again."); setAppointments([]); } finally { setLoading(false); } }; fetchBookings(); }, []); const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString("en-US", { month: "short", 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 getStatusColor = (status: string) => { const normalized = status.toLowerCase(); if (isDark) { switch (normalized) { case "scheduled": return "bg-blue-500/20 text-blue-200"; case "completed": return "bg-green-500/20 text-green-200"; case "rejected": case "cancelled": return "bg-red-500/20 text-red-200"; case "pending_review": case "pending": return "bg-yellow-500/20 text-yellow-200"; default: return "bg-gray-700 text-gray-200"; } } switch (normalized) { case "scheduled": return "bg-blue-100 text-blue-700"; case "completed": return "bg-green-100 text-green-700"; case "rejected": case "cancelled": return "bg-red-100 text-red-700"; case "pending_review": case "pending": return "bg-yellow-100 text-yellow-700"; default: return "bg-gray-100 text-gray-700"; } }; const formatStatus = (status: string) => { return status.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase()); }; const handleViewDetails = (appointment: Appointment) => { router.push(`/admin/booking/${appointment.id}`); }; const handleScheduleClick = (appointment: Appointment) => { setSelectedAppointment(appointment); setScheduledDate(undefined); setScheduledTime("09:00"); setScheduledDuration(60); setScheduleDialogOpen(true); }; const handleRejectClick = (appointment: Appointment) => { setSelectedAppointment(appointment); setRejectionReason(""); setRejectDialogOpen(true); }; const handleSchedule = async () => { if (!selectedAppointment) { toast.error("No appointment selected"); return; } if (!scheduledDate) { toast.error("Please select a date"); return; } if (!scheduledTime) { toast.error("Please select a time"); return; } setIsScheduling(true); try { // Combine date and time into ISO datetime string const [hours, minutes] = scheduledTime.split(":"); const datetime = new Date(scheduledDate); datetime.setHours(parseInt(hours, 10), parseInt(minutes, 10), 0, 0); const isoString = datetime.toISOString(); await scheduleAppointment(selectedAppointment.id, { scheduled_datetime: isoString, scheduled_duration: scheduledDuration, }); toast.success("Appointment scheduled successfully!"); setScheduleDialogOpen(false); setScheduledDate(undefined); setScheduledTime("09:00"); setScheduledDuration(60); // Refresh appointments list const data = await listAppointments(); setAppointments(data || []); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to schedule appointment"; toast.error(errorMessage); } finally { setIsScheduling(false); } }; const handleReject = async () => { if (!selectedAppointment) { return; } setIsRejecting(true); try { await rejectAppointment(selectedAppointment.id, { rejection_reason: rejectionReason || undefined, }); toast.success("Appointment rejected successfully"); setRejectDialogOpen(false); // Refresh appointments list const data = await listAppointments(); setAppointments(data || []); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to reject appointment"; toast.error(errorMessage); } finally { setIsRejecting(false); } }; const filteredAppointments = appointments.filter( (appointment) => appointment.first_name .toLowerCase() .includes(searchTerm.toLowerCase()) || appointment.last_name .toLowerCase() .includes(searchTerm.toLowerCase()) || appointment.email.toLowerCase().includes(searchTerm.toLowerCase()) || (appointment.phone && appointment.phone.toLowerCase().includes(searchTerm.toLowerCase())) ); return (
{/* Main Content */}
{/* Page Header */}

Bookings

Manage and view all appointment bookings

{/* Search Bar */}
setSearchTerm(e.target.value)} className={`pl-10 ${isDark ? "bg-gray-800 border-gray-700 text-white placeholder:text-gray-400" : "bg-white border-gray-200 text-gray-900 placeholder:text-gray-500"}`} />
{/* Available Days Display Card */} {adminAvailability && (

Weekly Availability

{(adminAvailability.availability_schedule || (adminAvailability.available_days_display && adminAvailability.available_days_display.length > 0)) ? (
{(() => { // Try new format first if (adminAvailability.availability_schedule) { return Object.keys(adminAvailability.availability_schedule).map((dayKey) => { const dayNum = parseInt(dayKey); const dayName = daysOfWeek.find(d => d.value === dayNum)?.label || `Day ${dayNum}`; const timeSlots = adminAvailability.availability_schedule![dayKey] || []; const slotLabels = timeSlots.map((slot: string) => { const option = timeSlotOptions.find(opt => opt.value === slot); return option ? option.label : slot; }); return (
{dayName} {slotLabels.length > 0 && ( ({slotLabels.join(", ")}) )}
); }); } // Fallback to legacy format else if (adminAvailability.available_days && adminAvailability.available_days.length > 0) { return adminAvailability.available_days.map((dayNum, index) => { const dayName = daysOfWeek.find(d => d.value === dayNum)?.label || adminAvailability.available_days_display?.[index]; const timeSlots = dayTimeSlots[dayNum] || []; const slotLabels = timeSlots.map(slot => { const option = timeSlotOptions.find(opt => opt.value === slot); return option ? option.label : slot; }); return (
{dayName} {slotLabels.length > 0 && ( ({slotLabels.join(", ")}) )}
); }); } return null; })()}
) : (

No availability set. Click "Manage Availability" to set your available days.

)}
)} {loading ? (
) : filteredAppointments.length === 0 ? (

No bookings found

{searchTerm ? "Try adjusting your search terms" : "No appointments have been created yet"}

) : (
{filteredAppointments.map((appointment) => ( handleViewDetails(appointment)} > ))}
Patient Status Actions
{appointment.first_name} {appointment.last_name}
{appointment.phone && ( )} {appointment.scheduled_datetime && (
{formatDate(appointment.scheduled_datetime)}
)}
{appointment.scheduled_datetime ? ( <>
{formatDate(appointment.scheduled_datetime)}
{formatTime(appointment.scheduled_datetime)}
) : (
Not scheduled
)}
{formatStatus(appointment.status)}
e.stopPropagation()}> {appointment.status === "pending_review" && ( <> )} {appointment.moderator_join_url && ( )}
)}
{/* Schedule Appointment Dialog */} { setScheduleDialogOpen(open); if (!open) { setScheduledDate(undefined); setScheduledTime("09:00"); setScheduledDuration(60); } }} appointment={selectedAppointment} scheduledDate={scheduledDate} setScheduledDate={setScheduledDate} scheduledTime={scheduledTime} setScheduledTime={setScheduledTime} scheduledDuration={scheduledDuration} setScheduledDuration={setScheduledDuration} onSchedule={handleSchedule} isScheduling={isScheduling} isDark={isDark} /> {/* Reject Appointment Dialog */} Reject Appointment {selectedAppointment && ( <>Reject appointment request from {selectedAppointment.first_name} {selectedAppointment.last_name} )}