2025-11-06 12:34:29 +00:00
|
|
|
"use client";
|
|
|
|
|
|
2025-11-07 11:53:00 +00:00
|
|
|
import { useState, useEffect } from "react";
|
2025-11-24 16:04:39 +00:00
|
|
|
import { useRouter } from "next/navigation";
|
2025-11-07 11:53:00 +00:00
|
|
|
import {
|
|
|
|
|
Calendar,
|
|
|
|
|
Clock,
|
|
|
|
|
Video,
|
2025-11-23 22:28:02 +00:00
|
|
|
Search,
|
2025-11-24 16:04:39 +00:00
|
|
|
CalendarCheck,
|
|
|
|
|
X,
|
|
|
|
|
Loader2,
|
|
|
|
|
User,
|
2025-11-25 20:15:37 +00:00
|
|
|
Settings,
|
|
|
|
|
Check,
|
2025-11-07 11:53:00 +00:00
|
|
|
} from "lucide-react";
|
2025-11-13 11:42:56 +00:00
|
|
|
import { useAppTheme } from "@/components/ThemeProvider";
|
2025-11-24 16:04:39 +00:00
|
|
|
import { listAppointments, scheduleAppointment, rejectAppointment } from "@/lib/actions/appointments";
|
2025-11-25 20:15:37 +00:00
|
|
|
import { useAppointments } from "@/hooks/useAppointments";
|
2025-11-23 22:28:02 +00:00
|
|
|
import { Input } from "@/components/ui/input";
|
2025-11-24 16:04:39 +00:00
|
|
|
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";
|
2025-11-23 22:28:02 +00:00
|
|
|
import { toast } from "sonner";
|
|
|
|
|
import type { Appointment } from "@/lib/models/appointments";
|
2025-11-06 12:34:29 +00:00
|
|
|
|
|
|
|
|
export default function Booking() {
|
2025-11-24 16:04:39 +00:00
|
|
|
const router = useRouter();
|
2025-11-23 22:28:02 +00:00
|
|
|
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
2025-11-07 11:53:00 +00:00
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState("");
|
2025-11-24 16:04:39 +00:00
|
|
|
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);
|
2025-11-13 11:42:56 +00:00
|
|
|
const { theme } = useAppTheme();
|
|
|
|
|
const isDark = theme === "dark";
|
2025-11-25 20:15:37 +00:00
|
|
|
|
|
|
|
|
// Availability management
|
|
|
|
|
const { adminAvailability, isLoadingAdminAvailability, updateAdminAvailability, isUpdatingAvailability } = useAppointments();
|
|
|
|
|
const [selectedDays, setSelectedDays] = useState<number[]>([]);
|
|
|
|
|
const [availabilityDialogOpen, setAvailabilityDialogOpen] = useState(false);
|
|
|
|
|
const [startTime, setStartTime] = useState<string>("09:00");
|
|
|
|
|
const [endTime, setEndTime] = useState<string>("17:00");
|
|
|
|
|
|
|
|
|
|
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" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Initialize selected days when availability is loaded
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (adminAvailability?.available_days) {
|
|
|
|
|
setSelectedDays(adminAvailability.available_days);
|
|
|
|
|
}
|
|
|
|
|
}, [adminAvailability]);
|
|
|
|
|
|
|
|
|
|
// Generate time slots for time picker
|
|
|
|
|
const generateTimeSlots = () => {
|
|
|
|
|
const slots = [];
|
|
|
|
|
for (let hour = 0; hour < 24; hour++) {
|
|
|
|
|
for (let minute = 0; minute < 60; minute += 30) {
|
|
|
|
|
const timeString = `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`;
|
|
|
|
|
slots.push(timeString);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return slots;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const timeSlotsForPicker = generateTimeSlots();
|
|
|
|
|
|
|
|
|
|
const handleDayToggle = (day: number) => {
|
|
|
|
|
setSelectedDays((prev) =>
|
|
|
|
|
prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day].sort()
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSaveAvailability = async () => {
|
|
|
|
|
if (selectedDays.length === 0) {
|
|
|
|
|
toast.error("Please select at least one available day");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (startTime >= endTime) {
|
|
|
|
|
toast.error("End time must be after start time");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await updateAdminAvailability({ available_days: selectedDays });
|
|
|
|
|
toast.success("Availability updated successfully!");
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
setAvailabilityDialogOpen(true);
|
|
|
|
|
};
|
2025-11-07 11:53:00 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchBookings = async () => {
|
|
|
|
|
setLoading(true);
|
2025-11-23 22:28:02 +00:00
|
|
|
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);
|
|
|
|
|
}
|
2025-11-07 11:53:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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) => {
|
2025-11-13 11:42:56 +00:00
|
|
|
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";
|
2025-11-23 22:28:02 +00:00
|
|
|
case "rejected":
|
2025-11-13 11:42:56 +00:00
|
|
|
case "cancelled":
|
|
|
|
|
return "bg-red-500/20 text-red-200";
|
2025-11-23 22:28:02 +00:00
|
|
|
case "pending_review":
|
2025-11-13 11:42:56 +00:00
|
|
|
case "pending":
|
|
|
|
|
return "bg-yellow-500/20 text-yellow-200";
|
|
|
|
|
default:
|
|
|
|
|
return "bg-gray-700 text-gray-200";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
switch (normalized) {
|
2025-11-07 11:53:00 +00:00
|
|
|
case "scheduled":
|
|
|
|
|
return "bg-blue-100 text-blue-700";
|
|
|
|
|
case "completed":
|
|
|
|
|
return "bg-green-100 text-green-700";
|
2025-11-23 22:28:02 +00:00
|
|
|
case "rejected":
|
2025-11-07 11:53:00 +00:00
|
|
|
case "cancelled":
|
|
|
|
|
return "bg-red-100 text-red-700";
|
2025-11-23 22:28:02 +00:00
|
|
|
case "pending_review":
|
2025-11-07 11:53:00 +00:00
|
|
|
case "pending":
|
|
|
|
|
return "bg-yellow-100 text-yellow-700";
|
|
|
|
|
default:
|
|
|
|
|
return "bg-gray-100 text-gray-700";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
const formatStatus = (status: string) => {
|
|
|
|
|
return status.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
2025-11-07 11:53:00 +00:00
|
|
|
};
|
|
|
|
|
|
2025-11-24 16:04:39 +00:00
|
|
|
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 || !scheduledDate) {
|
|
|
|
|
toast.error("Please select a date and 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), parseInt(minutes), 0, 0);
|
|
|
|
|
const isoString = datetime.toISOString();
|
|
|
|
|
|
|
|
|
|
await scheduleAppointment(selectedAppointment.id, {
|
|
|
|
|
scheduled_datetime: isoString,
|
|
|
|
|
scheduled_duration: scheduledDuration,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
toast.success("Appointment scheduled successfully!");
|
|
|
|
|
setScheduleDialogOpen(false);
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
const filteredAppointments = appointments.filter(
|
|
|
|
|
(appointment) =>
|
|
|
|
|
appointment.first_name
|
2025-11-07 11:53:00 +00:00
|
|
|
.toLowerCase()
|
|
|
|
|
.includes(searchTerm.toLowerCase()) ||
|
2025-11-23 22:28:02 +00:00
|
|
|
appointment.last_name
|
2025-11-07 11:53:00 +00:00
|
|
|
.toLowerCase()
|
|
|
|
|
.includes(searchTerm.toLowerCase()) ||
|
2025-11-23 22:28:02 +00:00
|
|
|
appointment.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
(appointment.phone && appointment.phone.toLowerCase().includes(searchTerm.toLowerCase()))
|
2025-11-07 11:53:00 +00:00
|
|
|
);
|
|
|
|
|
|
2025-11-06 12:34:29 +00:00
|
|
|
return (
|
2025-11-13 11:42:56 +00:00
|
|
|
<div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
|
2025-11-06 12:34:29 +00:00
|
|
|
|
2025-11-07 13:45:14 +00:00
|
|
|
{/* Main Content */}
|
|
|
|
|
<main className="p-3 sm:p-4 md:p-6 lg:p-8">
|
2025-11-07 11:53:00 +00:00
|
|
|
{/* Page Header */}
|
2025-11-23 22:28:02 +00:00
|
|
|
<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>
|
2025-11-25 20:15:37 +00:00
|
|
|
<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>
|
2025-11-23 22:28:02 +00:00
|
|
|
</div>
|
2025-11-25 20:15:37 +00:00
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
{/* 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"}`}
|
|
|
|
|
/>
|
2025-11-07 11:53:00 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="flex items-center justify-center py-12">
|
2025-11-13 11:42:56 +00:00
|
|
|
<div className={`animate-spin rounded-full h-8 w-8 border-b-2 ${isDark ? "border-gray-600" : "border-gray-400"}`}></div>
|
2025-11-07 11:53:00 +00:00
|
|
|
</div>
|
2025-11-23 22:28:02 +00:00
|
|
|
) : filteredAppointments.length === 0 ? (
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`}>
|
2025-11-07 11:53:00 +00:00
|
|
|
{searchTerm
|
|
|
|
|
? "Try adjusting your search terms"
|
2025-11-23 22:28:02 +00:00
|
|
|
: "No appointments have been created yet"}
|
2025-11-07 11:53:00 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2025-11-13 11:42:56 +00:00
|
|
|
<div className={`rounded-lg border overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
2025-11-07 11:53:00 +00:00
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
<table className="w-full">
|
2025-11-13 11:42:56 +00:00
|
|
|
<thead className={`${isDark ? "bg-gray-800 border-b border-gray-700" : "bg-gray-50 border-b border-gray-200"}`}>
|
2025-11-07 11:53:00 +00:00
|
|
|
<tr>
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`}>
|
2025-11-07 11:53:00 +00:00
|
|
|
Patient
|
|
|
|
|
</th>
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`}>
|
2025-11-07 11:53:00 +00:00
|
|
|
Date & Time
|
|
|
|
|
</th>
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`}>
|
2025-11-07 11:53:00 +00:00
|
|
|
Duration
|
|
|
|
|
</th>
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`}>
|
2025-11-07 11:53:00 +00:00
|
|
|
Status
|
|
|
|
|
</th>
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`}>
|
2025-11-23 22:28:02 +00:00
|
|
|
Preferred Dates
|
2025-11-07 11:53:00 +00:00
|
|
|
</th>
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`}>
|
2025-11-23 22:28:02 +00:00
|
|
|
Created
|
2025-11-07 11:53:00 +00:00
|
|
|
</th>
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`}>
|
2025-11-07 11:53:00 +00:00
|
|
|
Actions
|
|
|
|
|
</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
2025-11-13 11:42:56 +00:00
|
|
|
<tbody className={`${isDark ? "bg-gray-800 divide-gray-700" : "bg-white divide-gray-200"}`}>
|
2025-11-23 22:28:02 +00:00
|
|
|
{filteredAppointments.map((appointment) => (
|
2025-11-07 11:53:00 +00:00
|
|
|
<tr
|
2025-11-23 22:28:02 +00:00
|
|
|
key={appointment.id}
|
2025-11-24 16:04:39 +00:00
|
|
|
className={`transition-colors cursor-pointer ${isDark ? "hover:bg-gray-700" : "hover:bg-gray-50"}`}
|
|
|
|
|
onClick={() => handleViewDetails(appointment)}
|
2025-11-07 11:53:00 +00:00
|
|
|
>
|
2025-11-07 13:45:14 +00:00
|
|
|
<td className="px-3 sm:px-4 md:px-6 py-4">
|
2025-11-07 11:53:00 +00:00
|
|
|
<div className="flex items-center">
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`} />
|
2025-11-07 11:53:00 +00:00
|
|
|
</div>
|
2025-11-07 13:45:14 +00:00
|
|
|
<div className="ml-2 sm:ml-4 min-w-0">
|
2025-11-13 11:42:56 +00:00
|
|
|
<div className={`text-xs sm:text-sm font-medium truncate ${isDark ? "text-white" : "text-gray-900"}`}>
|
2025-11-23 22:28:02 +00:00
|
|
|
{appointment.first_name} {appointment.last_name}
|
2025-11-07 11:53:00 +00:00
|
|
|
</div>
|
2025-11-13 11:42:56 +00:00
|
|
|
<div className={`text-xs sm:text-sm truncate hidden sm:block ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
2025-11-23 22:28:02 +00:00
|
|
|
{appointment.email}
|
2025-11-07 13:45:14 +00:00
|
|
|
</div>
|
2025-11-23 22:28:02 +00:00
|
|
|
{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>
|
|
|
|
|
)}
|
2025-11-07 11:53:00 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
2025-11-07 13:45:14 +00:00
|
|
|
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap hidden md:table-cell">
|
2025-11-23 22:28:02 +00:00
|
|
|
{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>
|
|
|
|
|
)}
|
2025-11-07 11:53:00 +00:00
|
|
|
</td>
|
2025-11-13 11:42:56 +00:00
|
|
|
<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"}`}>
|
2025-11-23 22:28:02 +00:00
|
|
|
{appointment.scheduled_duration ? `${appointment.scheduled_duration} min` : "-"}
|
2025-11-07 11:53:00 +00:00
|
|
|
</td>
|
2025-11-07 13:45:14 +00:00
|
|
|
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap">
|
2025-11-07 11:53:00 +00:00
|
|
|
<span
|
|
|
|
|
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(
|
2025-11-23 22:28:02 +00:00
|
|
|
appointment.status
|
2025-11-07 11:53:00 +00:00
|
|
|
)}`}
|
|
|
|
|
>
|
2025-11-23 22:28:02 +00:00
|
|
|
{formatStatus(appointment.status)}
|
2025-11-07 11:53:00 +00:00
|
|
|
</span>
|
|
|
|
|
</td>
|
2025-11-23 22:28:02 +00:00
|
|
|
<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>
|
|
|
|
|
) : (
|
|
|
|
|
"-"
|
|
|
|
|
)}
|
2025-11-07 11:53:00 +00:00
|
|
|
</td>
|
2025-11-23 22:28:02 +00:00
|
|
|
<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)}
|
2025-11-07 11:53:00 +00:00
|
|
|
</td>
|
2025-11-07 13:45:14 +00:00
|
|
|
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
2025-11-24 16:04:39 +00:00
|
|
|
<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>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-11-23 22:28:02 +00:00
|
|
|
{appointment.jitsi_meet_url && (
|
2025-11-07 11:53:00 +00:00
|
|
|
<a
|
2025-11-23 22:28:02 +00:00
|
|
|
href={appointment.jitsi_meet_url}
|
2025-11-07 11:53:00 +00:00
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
2025-11-13 11:42:56 +00:00
|
|
|
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"}`}
|
2025-11-07 11:53:00 +00:00
|
|
|
title="Join Meeting"
|
|
|
|
|
>
|
|
|
|
|
<Video className="w-4 h-4" />
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-06 12:34:29 +00:00
|
|
|
</main>
|
2025-11-24 16:04:39 +00:00
|
|
|
|
|
|
|
|
{/* 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}
|
|
|
|
|
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>
|
|
|
|
|
|
2025-11-25 20:15:37 +00:00
|
|
|
{/* 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>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{/* Current Availability Display */}
|
|
|
|
|
{adminAvailability?.available_days_display && adminAvailability.available_days_display.length > 0 && (
|
|
|
|
|
<div className={`p-3 rounded-lg border ${isDark ? "bg-blue-900/20 border-blue-800" : "bg-blue-50 border-blue-200"}`}>
|
|
|
|
|
<p className={`text-sm ${isDark ? "text-blue-200" : "text-blue-900"}`}>
|
|
|
|
|
<span className="font-medium">Current availability:</span> {adminAvailability.available_days_display.join(", ")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Days Selection */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className={`text-sm font-medium mb-3 block ${isDark ? "text-gray-300" : "text-gray-700"}`}>
|
|
|
|
|
Available Days *
|
|
|
|
|
</label>
|
|
|
|
|
<p className={`text-xs mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
|
|
|
|
Select the days of the week when you accept appointment requests
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex flex-wrap gap-2 sm:gap-3">
|
|
|
|
|
{daysOfWeek.map((day) => (
|
|
|
|
|
<button
|
|
|
|
|
key={day.value}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => handleDayToggle(day.value)}
|
|
|
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${
|
|
|
|
|
selectedDays.includes(day.value)
|
|
|
|
|
? 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"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{selectedDays.includes(day.value) && (
|
|
|
|
|
<Check className="w-4 h-4" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="text-sm font-medium">{day.label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Time Selection */}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label className={`text-sm font-medium mb-3 block ${isDark ? "text-gray-300" : "text-gray-700"}`}>
|
|
|
|
|
Available Hours
|
|
|
|
|
</label>
|
|
|
|
|
<p className={`text-xs mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
|
|
|
|
Set the time range when appointments can be scheduled
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
{/* Start Time */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
|
|
|
|
|
Start Time
|
|
|
|
|
</label>
|
|
|
|
|
<Select value={startTime} onValueChange={setStartTime}>
|
|
|
|
|
<SelectTrigger className={isDark ? "bg-gray-700 border-gray-600 text-white" : "bg-white border-gray-300"}>
|
|
|
|
|
<SelectValue placeholder="Select start time" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent className={isDark ? "bg-gray-800 border-gray-700" : "bg-white"}>
|
|
|
|
|
{timeSlotsForPicker.map((time) => (
|
|
|
|
|
<SelectItem
|
|
|
|
|
key={time}
|
|
|
|
|
value={time}
|
|
|
|
|
className={isDark ? "text-white hover:bg-gray-700" : ""}
|
|
|
|
|
>
|
|
|
|
|
{new Date(`2000-01-01T${time}`).toLocaleTimeString("en-US", {
|
|
|
|
|
hour: "numeric",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
hour12: true,
|
|
|
|
|
})}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* End Time */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
|
|
|
|
|
End Time
|
|
|
|
|
</label>
|
|
|
|
|
<Select value={endTime} onValueChange={setEndTime}>
|
|
|
|
|
<SelectTrigger className={isDark ? "bg-gray-700 border-gray-600 text-white" : "bg-white border-gray-300"}>
|
|
|
|
|
<SelectValue placeholder="Select end time" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent className={isDark ? "bg-gray-800 border-gray-700" : "bg-white"}>
|
|
|
|
|
{timeSlotsForPicker.map((time) => (
|
|
|
|
|
<SelectItem
|
|
|
|
|
key={time}
|
|
|
|
|
value={time}
|
|
|
|
|
className={isDark ? "text-white hover:bg-gray-700" : ""}
|
|
|
|
|
>
|
|
|
|
|
{new Date(`2000-01-01T${time}`).toLocaleTimeString("en-US", {
|
|
|
|
|
hour: "numeric",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
hour12: true,
|
|
|
|
|
})}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Time Range Display */}
|
|
|
|
|
<div className={`p-3 rounded-lg border ${isDark ? "bg-gray-700/50 border-gray-600" : "bg-gray-50 border-gray-200"}`}>
|
|
|
|
|
<p className={`text-sm ${isDark ? "text-gray-300" : "text-gray-700"}`}>
|
|
|
|
|
<span className="font-medium">Available hours:</span>{" "}
|
|
|
|
|
{new Date(`2000-01-01T${startTime}`).toLocaleTimeString("en-US", {
|
|
|
|
|
hour: "numeric",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
hour12: true,
|
|
|
|
|
})}{" "}
|
|
|
|
|
-{" "}
|
|
|
|
|
{new Date(`2000-01-01T${endTime}`).toLocaleTimeString("en-US", {
|
|
|
|
|
hour: "numeric",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
hour12: true,
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
</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 || startTime >= endTime}
|
|
|
|
|
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>
|
|
|
|
|
|
2025-11-06 12:34:29 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|