website/app/(admin)/admin/booking/page.tsx

965 lines
42 KiB
TypeScript
Raw Normal View History

"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 { 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 Booking() {
const router = useRouter();
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [scheduleDialogOpen, setScheduleDialogOpen] = useState(false);
const [rejectDialogOpen, setRejectDialogOpen] = useState(false);
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | null>(null);
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
const [scheduledTime, setScheduledTime] = useState<string>("09:00");
const [scheduledDuration, setScheduledDuration] = useState<number>(60);
const [rejectionReason, setRejectionReason] = useState<string>("");
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<number[]>([]);
const [availabilityDialogOpen, setAvailabilityDialogOpen] = useState(false);
const [dayTimeSlots, setDayTimeSlots] = useState<Record<number, string[]>>({});
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" },
];
// Load time slots from localStorage on mount
useEffect(() => {
const savedTimeSlots = localStorage.getItem("adminAvailabilityTimeSlots");
if (savedTimeSlots) {
try {
const parsed = JSON.parse(savedTimeSlots);
setDayTimeSlots(parsed);
} catch (error) {
console.error("Failed to parse saved time slots:", error);
}
}
}, []);
// Initialize selected days and time slots when availability is loaded
useEffect(() => {
if (adminAvailability?.available_days) {
setSelectedDays(adminAvailability.available_days);
// Load saved time slots or use defaults
const savedTimeSlots = localStorage.getItem("adminAvailabilityTimeSlots");
let initialSlots: Record<number, string[]> = {};
if (savedTimeSlots) {
try {
const parsed = JSON.parse(savedTimeSlots);
// Only use saved slots for days that are currently available
adminAvailability.available_days.forEach((day) => {
initialSlots[day] = parsed[day] || ["morning", "lunchtime", "afternoon"];
});
} catch (error) {
// If parsing fails, use defaults
adminAvailability.available_days.forEach((day) => {
initialSlots[day] = ["morning", "lunchtime", "afternoon"];
});
}
} else {
// No saved slots, use defaults
adminAvailability.available_days.forEach((day) => {
initialSlots[day] = ["morning", "lunchtime", "afternoon"];
});
}
setDayTimeSlots(initialSlots);
}
}, [adminAvailability]);
const timeSlotOptions = [
{ value: "morning", label: "Morning" },
{ value: "lunchtime", label: "Lunchtime" },
{ value: "afternoon", 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", "lunchtime", "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;
}
}
try {
// Ensure selectedDays is an array of numbers
const daysToSave = selectedDays.map(day => Number(day)).sort();
await updateAvailability({ available_days: daysToSave });
// Save time slots to localStorage
localStorage.setItem("adminAvailabilityTimeSlots", JSON.stringify(dayTimeSlots));
toast.success("Availability updated successfully!");
// Refresh availability data
if (refetchAdminAvailability) {
await refetchAdminAvailability();
}
setAvailabilityDialogOpen(false);
} catch (error) {
console.error("Failed to update availability:", error);
const errorMessage = error instanceof Error ? error.message : "Failed to update availability";
toast.error(errorMessage);
}
};
const handleOpenAvailabilityDialog = () => {
if (adminAvailability?.available_days) {
setSelectedDays(adminAvailability.available_days);
// Initialize time slots for each day
const initialSlots: Record<number, string[]> = {};
adminAvailability.available_days.forEach((day) => {
initialSlots[day] = dayTimeSlots[day] || ["morning", "lunchtime", "afternoon"];
});
setDayTimeSlots(initialSlots);
}
setAvailabilityDialogOpen(true);
};
useEffect(() => {
const fetchBookings = async () => {
setLoading(true);
try {
const data = await listAppointments();
setAppointments(data || []);
} catch (error) {
console.error("Failed to fetch appointments:", 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) {
console.error("Failed to schedule appointment:", 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) {
console.error("Failed to reject appointment:", error);
const errorMessage = error instanceof Error ? error.message : "Failed to reject appointment";
toast.error(errorMessage);
} finally {
setIsRejecting(false);
}
};
// Generate time slots
const timeSlots = [];
for (let hour = 8; hour <= 18; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
const timeString = `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`;
timeSlots.push(timeString);
}
}
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 (
<div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
{/* Main Content */}
<main className="p-3 sm:p-4 md:p-6 lg:p-8">
{/* Page Header */}
<div className="mb-4 sm:mb-6 flex flex-col gap-3 sm:gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div>
<h1 className={`text-xl sm:text-2xl font-semibold mb-1 ${isDark ? "text-white" : "text-gray-900"}`}>
Bookings
</h1>
<p className={`text-xs sm:text-sm ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Manage and view all appointment bookings
</p>
</div>
<Button
onClick={handleOpenAvailabilityDialog}
variant="outline"
className={`flex items-center gap-2 ${isDark ? "border-gray-700 hover:bg-gray-800" : "border-gray-300 hover:bg-gray-50"}`}
>
<Settings className="w-4 h-4" />
Manage Availability
</Button>
</div>
{/* Search Bar */}
<div className="relative">
<Search className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} />
<Input
type="text"
placeholder="Search by name, email, or phone..."
value={searchTerm}
onChange={(e) => 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"}`}
/>
</div>
</div>
{/* Available Days Display Card */}
{adminAvailability && (
<div className={`mb-4 sm:mb-6 rounded-lg border p-4 sm:p-5 ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
<div className="flex items-start gap-3">
<div className={`p-2 rounded-lg ${isDark ? "bg-rose-500/20" : "bg-rose-50"}`}>
<CalendarCheck className={`w-5 h-5 ${isDark ? "text-rose-400" : "text-rose-600"}`} />
</div>
<div className="flex-1 min-w-0">
<h3 className={`text-sm font-semibold mb-3 ${isDark ? "text-white" : "text-gray-900"}`}>
Weekly Availability
</h3>
{adminAvailability.available_days_display && adminAvailability.available_days_display.length > 0 ? (
<div className="flex flex-wrap gap-2">
{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 (
<div
key={dayNum}
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-sm whitespace-nowrap ${
isDark
? "bg-rose-500/10 text-rose-200 border border-rose-500/20"
: "bg-rose-50 text-rose-700 border border-rose-200"
}`}
>
<Check className={`w-3.5 h-3.5 shrink-0 ${isDark ? "text-rose-400" : "text-rose-600"}`} />
<span className="font-medium shrink-0">{dayName}</span>
{slotLabels.length > 0 && (
<span className={`text-sm shrink-0 ${isDark ? "text-rose-300" : "text-rose-600"}`}>
({slotLabels.join(", ")})
</span>
)}
</div>
);
})}
</div>
) : (
<p className={`text-sm ${isDark ? "text-gray-400" : "text-gray-500"}`}>
No availability set. Click "Manage Availability" to set your available days.
</p>
)}
</div>
</div>
</div>
)}
{loading ? (
<div className="flex items-center justify-center py-12">
<div className={`animate-spin rounded-full h-8 w-8 border-b-2 ${isDark ? "border-gray-600" : "border-gray-400"}`}></div>
</div>
) : filteredAppointments.length === 0 ? (
<div className={`rounded-lg border p-12 text-center ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
<Calendar className={`w-12 h-12 mx-auto mb-4 ${isDark ? "text-gray-500" : "text-gray-400"}`} />
<p className={`font-medium mb-1 ${isDark ? "text-gray-200" : "text-gray-600"}`}>No bookings found</p>
<p className={`text-sm ${isDark ? "text-gray-400" : "text-gray-500"}`}>
{searchTerm
? "Try adjusting your search terms"
: "No appointments have been created yet"}
</p>
</div>
) : (
<div className={`rounded-lg border overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
<div className="overflow-x-auto">
<table className="w-full">
<thead className={`${isDark ? "bg-gray-800 border-b border-gray-700" : "bg-gray-50 border-b border-gray-200"}`}>
<tr>
<th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Patient
</th>
<th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider hidden md:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Date & Time
</th>
<th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider hidden lg:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Duration
</th>
<th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Status
</th>
<th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider hidden lg:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Preferred Dates
</th>
<th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider hidden xl:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Created
</th>
<th className={`px-3 sm:px-4 md:px-6 py-3 text-right text-xs font-medium uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Actions
</th>
</tr>
</thead>
<tbody className={`${isDark ? "bg-gray-800 divide-gray-700" : "bg-white divide-gray-200"}`}>
{filteredAppointments.map((appointment) => (
<tr
key={appointment.id}
className={`transition-colors cursor-pointer ${isDark ? "hover:bg-gray-700" : "hover:bg-gray-50"}`}
onClick={() => handleViewDetails(appointment)}
>
<td className="px-3 sm:px-4 md:px-6 py-4">
<div className="flex items-center">
<div className={`shrink-0 h-8 w-8 sm:h-10 sm:w-10 rounded-full flex items-center justify-center ${isDark ? "bg-gray-700" : "bg-gray-100"}`}>
<User className={`w-4 h-4 sm:w-5 sm:h-5 ${isDark ? "text-gray-200" : "text-gray-600"}`} />
</div>
<div className="ml-2 sm:ml-4 min-w-0">
<div className={`text-xs sm:text-sm font-medium truncate ${isDark ? "text-white" : "text-gray-900"}`}>
{appointment.first_name} {appointment.last_name}
</div>
<div className={`text-xs sm:text-sm truncate hidden sm:block ${isDark ? "text-gray-400" : "text-gray-500"}`}>
{appointment.email}
</div>
{appointment.phone && (
<div className={`text-xs truncate hidden sm:block ${isDark ? "text-gray-500" : "text-gray-400"}`}>
{appointment.phone}
</div>
)}
{appointment.scheduled_datetime && (
<div className={`text-xs sm:hidden mt-0.5 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
{formatDate(appointment.scheduled_datetime)}
</div>
)}
</div>
</div>
</td>
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap hidden md:table-cell">
{appointment.scheduled_datetime ? (
<>
<div className={`text-xs sm:text-sm ${isDark ? "text-white" : "text-gray-900"}`}>
{formatDate(appointment.scheduled_datetime)}
</div>
<div className={`text-xs sm:text-sm flex items-center gap-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
<Clock className="w-3 h-3" />
{formatTime(appointment.scheduled_datetime)}
</div>
</>
) : (
<div className={`text-xs sm:text-sm ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Not scheduled
</div>
)}
</td>
<td className={`px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-xs sm:text-sm hidden lg:table-cell ${isDark ? "text-white" : "text-gray-900"}`}>
{appointment.scheduled_duration ? `${appointment.scheduled_duration} min` : "-"}
</td>
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(
appointment.status
)}`}
>
{formatStatus(appointment.status)}
</span>
</td>
<td className={`px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-xs sm:text-sm hidden lg:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
{appointment.preferred_dates && appointment.preferred_dates.length > 0 ? (
<div className="flex flex-col gap-1">
{appointment.preferred_dates.slice(0, 2).map((date, idx) => (
<span key={idx}>{formatDate(date)}</span>
))}
{appointment.preferred_dates.length > 2 && (
<span className="text-xs">+{appointment.preferred_dates.length - 2} more</span>
)}
</div>
) : (
"-"
)}
</td>
<td className={`px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-xs sm:text-sm hidden xl:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
{formatDate(appointment.created_at)}
</td>
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end gap-1 sm:gap-2" onClick={(e) => e.stopPropagation()}>
{appointment.status === "pending_review" && (
<>
<button
onClick={() => handleScheduleClick(appointment)}
className={`px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-lg transition-colors ${
isDark
? "bg-blue-600 hover:bg-blue-700 text-white"
: "bg-blue-600 hover:bg-blue-700 text-white"
}`}
title="Schedule Appointment"
>
<span className="hidden sm:inline">Schedule</span>
<CalendarCheck className="w-4 h-4 sm:hidden" />
</button>
<button
onClick={() => handleRejectClick(appointment)}
className={`px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-lg transition-colors ${
isDark
? "bg-red-600 hover:bg-red-700 text-white"
: "bg-red-600 hover:bg-red-700 text-white"
}`}
title="Reject Appointment"
>
<span className="hidden sm:inline">Reject</span>
<X className="w-4 h-4 sm:hidden" />
</button>
</>
)}
{appointment.jitsi_meet_url && (
<a
href={appointment.jitsi_meet_url}
target="_blank"
rel="noopener noreferrer"
className={`p-1.5 sm:p-2 rounded-lg transition-colors ${isDark ? "text-gray-300 hover:text-white hover:bg-gray-700" : "text-gray-600 hover:text-gray-900 hover:bg-gray-100"}`}
title="Join Meeting"
>
<Video className="w-4 h-4" />
</a>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</main>
{/* Schedule Appointment Dialog */}
<Dialog open={scheduleDialogOpen} onOpenChange={setScheduleDialogOpen}>
<DialogContent className={`${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
<DialogHeader>
<DialogTitle className={isDark ? "text-white" : "text-gray-900"}>
Schedule Appointment
</DialogTitle>
<DialogDescription className={isDark ? "text-gray-400" : "text-gray-500"}>
{selectedAppointment && (
<>Schedule appointment for {selectedAppointment.first_name} {selectedAppointment.last_name}</>
)}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Date *
</label>
<DatePicker
date={scheduledDate}
setDate={setScheduledDate}
/>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Time *
</label>
<Select value={scheduledTime} onValueChange={setScheduledTime}>
<SelectTrigger className={isDark ? "bg-gray-700 border-gray-600 text-white" : "bg-white border-gray-300"}>
<SelectValue placeholder="Select time" />
</SelectTrigger>
<SelectContent className={isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}>
{timeSlots.map((time) => (
<SelectItem
key={time}
value={time}
className={isDark ? "focus:bg-gray-700" : ""}
>
{new Date(`2000-01-01T${time}`).toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
})}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Duration (minutes)
</label>
<Select
value={scheduledDuration.toString()}
onValueChange={(value) => setScheduledDuration(parseInt(value))}
>
<SelectTrigger className={isDark ? "bg-gray-700 border-gray-600 text-white" : "bg-white border-gray-300"}>
<SelectValue placeholder="Select duration" />
</SelectTrigger>
<SelectContent className={isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}>
<SelectItem value="30" className={isDark ? "focus:bg-gray-700" : ""}>30 minutes</SelectItem>
<SelectItem value="60" className={isDark ? "focus:bg-gray-700" : ""}>60 minutes</SelectItem>
<SelectItem value="90" className={isDark ? "focus:bg-gray-700" : ""}>90 minutes</SelectItem>
<SelectItem value="120" className={isDark ? "focus:bg-gray-700" : ""}>120 minutes</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setScheduleDialogOpen(false)}
disabled={isScheduling}
className={isDark ? "border-gray-700 text-gray-300 hover:bg-gray-700" : ""}
>
Cancel
</Button>
<Button
onClick={handleSchedule}
disabled={isScheduling || !scheduledDate || !scheduledTime}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
{isScheduling ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Scheduling...
</>
) : (
"Schedule Appointment"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Reject Appointment Dialog */}
<Dialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}>
<DialogContent className={`${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
<DialogHeader>
<DialogTitle className={isDark ? "text-white" : "text-gray-900"}>
Reject Appointment
</DialogTitle>
<DialogDescription className={isDark ? "text-gray-400" : "text-gray-500"}>
{selectedAppointment && (
<>Reject appointment request from {selectedAppointment.first_name} {selectedAppointment.last_name}</>
)}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Rejection Reason (Optional)
</label>
<textarea
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
placeholder="Enter reason for rejection..."
rows={4}
className={`w-full rounded-md border px-3 py-2 text-sm ${
isDark
? "bg-gray-700 border-gray-600 text-white placeholder:text-gray-400"
: "bg-white border-gray-300 text-gray-900 placeholder:text-gray-500"
}`}
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setRejectDialogOpen(false)}
disabled={isRejecting}
className={isDark ? "border-gray-700 text-gray-300 hover:bg-gray-700" : ""}
>
Cancel
</Button>
<Button
onClick={handleReject}
disabled={isRejecting}
className="bg-red-600 hover:bg-red-700 text-white"
>
{isRejecting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Rejecting...
</>
) : (
"Reject Appointment"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Availability Management Dialog */}
<Dialog open={availabilityDialogOpen} onOpenChange={setAvailabilityDialogOpen}>
<DialogContent className={`max-w-2xl max-h-[90vh] overflow-y-auto ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
<DialogHeader>
<DialogTitle className={`text-xl sm:text-2xl font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
Manage Weekly Availability
</DialogTitle>
</DialogHeader>
<div className="space-y-6 py-4">
{isLoadingAdminAvailability ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-rose-600" />
</div>
) : (
<>
{/* Days Selection with Time Ranges */}
<div className="space-y-4">
<div>
<label className={`text-sm font-medium mb-3 block ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Available Days & Times *
</label>
<p className={`text-xs mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Select days and choose time slots (Morning, Lunchtime, Evening) for each day
</p>
</div>
<div className="space-y-3">
{daysOfWeek.map((day) => {
const isSelected = selectedDays.includes(day.value);
return (
<div
key={day.value}
className={`rounded-lg border p-4 transition-all ${
isSelected
? isDark
? "bg-rose-500/10 border-rose-500/30"
: "bg-rose-50 border-rose-200"
: isDark
? "bg-gray-700/50 border-gray-600"
: "bg-gray-50 border-gray-200"
}`}
>
<div className="flex items-start justify-between gap-4 mb-3">
<button
type="button"
onClick={() => handleDayToggle(day.value)}
className={`flex items-center gap-2 flex-1 ${
isSelected ? "cursor-pointer" : "cursor-pointer"
}`}
>
<div
className={`w-5 h-5 rounded border-2 flex items-center justify-center flex-shrink-0 ${
isSelected
? isDark
? "bg-rose-600 border-rose-500"
: "bg-rose-500 border-rose-500"
: isDark
? "border-gray-500"
: "border-gray-300"
}`}
>
{isSelected && <Check className="w-3 h-3 text-white" />}
</div>
<span className={`text-sm font-medium ${isDark ? "text-white" : "text-gray-900"}`}>
{day.label}
</span>
</button>
</div>
{isSelected && (
<div className="mt-3 pt-3 border-t border-gray-300 dark:border-gray-600">
<label className={`text-xs font-medium mb-2 block ${isDark ? "text-gray-400" : "text-gray-600"}`}>
Available Time Slots
</label>
<div className="flex flex-wrap gap-2">
{timeSlotOptions.map((slot) => {
const isSelectedSlot = dayTimeSlots[day.value]?.includes(slot.value) || false;
return (
<button
key={slot.value}
type="button"
onClick={() => handleTimeSlotToggle(day.value, slot.value)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${
isSelectedSlot
? isDark
? "bg-rose-600 border-rose-500 text-white"
: "bg-rose-500 border-rose-500 text-white"
: isDark
? "bg-gray-700 border-gray-600 text-gray-300 hover:border-rose-500"
: "bg-white border-gray-300 text-gray-700 hover:border-rose-500"
}`}
>
{slot.label}
</button>
);
})}
</div>
</div>
)}
</div>
);
})}
</div>
</div>
</>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setAvailabilityDialogOpen(false);
if (adminAvailability?.available_days) {
setSelectedDays(adminAvailability.available_days);
}
}}
disabled={isUpdatingAvailability}
className={isDark ? "border-gray-700 text-gray-300 hover:bg-gray-700" : ""}
>
Cancel
</Button>
<Button
onClick={handleSaveAvailability}
disabled={isUpdatingAvailability || selectedDays.length === 0}
className="bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
>
{isUpdatingAvailability ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Saving...
</>
) : (
<>
<Check className="w-4 h-4 mr-2" />
Save Availability
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}