2025-11-06 12:34:29 +00:00
|
|
|
"use client";
|
|
|
|
|
|
2025-11-07 11:53:00 +00:00
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
|
import {
|
|
|
|
|
Calendar,
|
|
|
|
|
Clock,
|
|
|
|
|
User,
|
|
|
|
|
Video,
|
|
|
|
|
FileText,
|
|
|
|
|
MoreVertical,
|
2025-11-23 22:28:02 +00:00
|
|
|
Search,
|
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-23 22:28:02 +00:00
|
|
|
import { listAppointments } from "@/lib/actions/appointments";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
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-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-13 11:42:56 +00:00
|
|
|
const { theme } = useAppTheme();
|
|
|
|
|
const isDark = theme === "dark";
|
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();
|
|
|
|
|
console.log("Fetched appointments:", data);
|
|
|
|
|
console.log("Appointments count:", data?.length);
|
|
|
|
|
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-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>
|
|
|
|
|
</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"}`}
|
|
|
|
|
/>
|
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-13 11:42:56 +00:00
|
|
|
className={`transition-colors ${isDark ? "hover:bg-gray-700" : "hover:bg-gray-50"}`}
|
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">
|
|
|
|
|
<div className="flex items-center justify-end gap-1 sm:gap-2">
|
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>
|
|
|
|
|
)}
|
2025-11-23 22:28:02 +00:00
|
|
|
{appointment.reason && (
|
2025-11-07 11:53:00 +00:00
|
|
|
<button
|
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-23 22:28:02 +00:00
|
|
|
title={appointment.reason}
|
2025-11-07 11:53:00 +00:00
|
|
|
>
|
|
|
|
|
<FileText className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2025-11-13 11:42:56 +00:00
|
|
|
<button 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
|
|
|
<MoreVertical className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-06 12:34:29 +00:00
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|