- Updated the appointment detail page to adjust the CSS class for the Video component, improving layout consistency. - Added handling for "cancelled" and "canceled" statuses in the user dashboard, ensuring accurate representation of appointment states and enhancing user experience.
595 lines
29 KiB
TypeScript
595 lines
29 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Calendar,
|
|
Clock,
|
|
User,
|
|
Mail,
|
|
Phone,
|
|
Heart,
|
|
CalendarPlus,
|
|
Video,
|
|
CheckCircle2,
|
|
XCircle,
|
|
CalendarCheck,
|
|
ArrowUpRight,
|
|
Settings,
|
|
Loader2,
|
|
} from "lucide-react";
|
|
import Link from "next/link";
|
|
import { Navbar } from "@/components/Navbar";
|
|
import { useAppTheme } from "@/components/ThemeProvider";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { getUserAppointments, getUserAppointmentStats } from "@/lib/actions/appointments";
|
|
import type { Appointment, UserAppointmentStats } from "@/lib/models/appointments";
|
|
import { toast } from "sonner";
|
|
|
|
export default function UserDashboard() {
|
|
const router = useRouter();
|
|
const { theme } = useAppTheme();
|
|
const isDark = theme === "dark";
|
|
const { user } = useAuth();
|
|
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [stats, setStats] = useState<UserAppointmentStats | null>(null);
|
|
const [loadingStats, setLoadingStats] = useState(true);
|
|
|
|
// Fetch user appointments from user-specific endpoint
|
|
useEffect(() => {
|
|
const fetchAppointments = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await getUserAppointments();
|
|
setAppointments(data || []);
|
|
} catch (error) {
|
|
toast.error("Failed to load appointments. Please try again.");
|
|
setAppointments([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchAppointments();
|
|
}, []);
|
|
|
|
// Fetch stats from API for authenticated user
|
|
useEffect(() => {
|
|
const fetchStats = async () => {
|
|
setLoadingStats(true);
|
|
try {
|
|
const statsData = await getUserAppointmentStats();
|
|
setStats(statsData);
|
|
} catch (error) {
|
|
toast.error("Failed to load appointment statistics.");
|
|
setStats({
|
|
total_requests: 0,
|
|
pending_review: 0,
|
|
scheduled: 0,
|
|
rejected: 0,
|
|
completed: 0,
|
|
completion_rate: 0,
|
|
});
|
|
} finally {
|
|
setLoadingStats(false);
|
|
}
|
|
};
|
|
|
|
fetchStats();
|
|
}, []);
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString("en-US", {
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
};
|
|
|
|
const formatTime = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleTimeString("en-US", {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
});
|
|
};
|
|
|
|
const formatMemberSince = (dateString?: string) => {
|
|
if (!dateString) return "N/A";
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString("en-US", {
|
|
month: "long",
|
|
year: "numeric",
|
|
});
|
|
};
|
|
|
|
// Filter appointments by status
|
|
const upcomingAppointments = useMemo(() => {
|
|
return appointments.filter(
|
|
(appointment) => appointment.status === "scheduled"
|
|
);
|
|
}, [appointments]);
|
|
|
|
const pendingAppointments = useMemo(() => {
|
|
return appointments.filter(
|
|
(appointment) => appointment.status === "pending_review"
|
|
);
|
|
}, [appointments]);
|
|
|
|
const completedAppointments = useMemo(() => {
|
|
return appointments.filter(
|
|
(appointment) => appointment.status === "completed"
|
|
);
|
|
}, [appointments]);
|
|
|
|
const rejectedAppointments = useMemo(() => {
|
|
return appointments.filter(
|
|
(appointment) => appointment.status === "rejected"
|
|
);
|
|
}, [appointments]);
|
|
|
|
// Sort appointments by created_at (newest first)
|
|
const allAppointments = useMemo(() => {
|
|
return [...appointments].sort((a, b) => {
|
|
const dateA = new Date(a.created_at).getTime();
|
|
const dateB = new Date(b.created_at).getTime();
|
|
return dateB - dateA;
|
|
});
|
|
}, [appointments]);
|
|
|
|
// Use stats from API, fallback to calculated stats if API stats not available
|
|
const displayStats = useMemo(() => {
|
|
if (stats) {
|
|
return {
|
|
scheduled: stats.scheduled || 0,
|
|
scheduled_pct: stats.scheduled_pct,
|
|
completed: stats.completed || 0,
|
|
completed_pct: stats.completed_pct,
|
|
pending_review: stats.pending_review || 0,
|
|
pending_review_pct: stats.pending_review_pct,
|
|
rejected: stats.rejected || 0,
|
|
rejected_pct: stats.rejected_pct,
|
|
total_requests: stats.total_requests || 0,
|
|
completion_rate: stats.completion_rate || 0,
|
|
};
|
|
}
|
|
// Fallback: calculate from appointments if stats not loaded yet
|
|
// Note: Percentage values (_pct) are only available from API, not calculated
|
|
const scheduled = appointments.filter(a => a.status === "scheduled").length;
|
|
const completed = appointments.filter(a => a.status === "completed").length;
|
|
const pending_review = appointments.filter(a => a.status === "pending_review").length;
|
|
const rejected = appointments.filter(a => a.status === "rejected").length;
|
|
const total_requests = appointments.length;
|
|
const completion_rate = total_requests > 0 ? (scheduled / total_requests) * 100 : 0;
|
|
|
|
return {
|
|
scheduled,
|
|
scheduled_pct: undefined, // Only from API
|
|
completed,
|
|
completed_pct: undefined, // Only from API
|
|
pending_review,
|
|
pending_review_pct: undefined, // Only from API
|
|
rejected,
|
|
rejected_pct: undefined, // Only from API
|
|
total_requests,
|
|
completion_rate,
|
|
};
|
|
}, [stats, appointments]);
|
|
|
|
return (
|
|
<div className={`min-h-screen ${isDark ? 'bg-gray-900' : 'bg-gray-50'}`}>
|
|
<Navbar />
|
|
|
|
{/* Main Content */}
|
|
<main className="container mx-auto px-4 sm:px-6 lg:px-8 space-y-6 pt-20 sm:pt-24 pb-8">
|
|
{/* Welcome Section */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
|
<div>
|
|
<h1 className={`text-2xl font-semibold mb-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
Welcome Back!
|
|
</h1>
|
|
<p className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
|
Here's an overview of your appointments
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Link href="/user/settings" className="flex-1 sm:flex-initial">
|
|
<Button
|
|
variant="outline"
|
|
className={`w-full sm:w-auto ${isDark ? 'hover:bg-gray-800 border-gray-700 text-gray-300' : 'hover:bg-gray-100'}`}
|
|
>
|
|
<Settings className="w-4 h-4 mr-2" />
|
|
Settings
|
|
</Button>
|
|
</Link>
|
|
<Link href="/book-now" className="flex-1 sm:flex-initial">
|
|
<Button
|
|
className="w-full sm:w-auto bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
|
|
>
|
|
<CalendarPlus className="w-4 h-4 mr-2" />
|
|
Book Appointment
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className={`w-8 h-8 animate-spin ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4">
|
|
<div
|
|
className={`rounded-lg border p-4 sm:p-5 md:p-6 hover:shadow-md transition-shadow ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}
|
|
>
|
|
<div className="flex items-start justify-between mb-3 sm:mb-4">
|
|
<div className={`p-2 sm:p-2.5 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
|
|
<CalendarCheck className={`w-4 h-4 sm:w-5 sm:h-5 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
|
|
</div>
|
|
{displayStats.scheduled_pct !== undefined && (
|
|
<div
|
|
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${isDark ? "bg-green-900/30 text-green-400" : "bg-green-50 text-green-700"}`}
|
|
>
|
|
<ArrowUpRight className="w-3 h-3" />
|
|
<span>{`${Math.round(displayStats.scheduled_pct)}%`}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h3 className={`text-xs font-medium mb-1 sm:mb-2 uppercase tracking-wider ${isDark ? 'text-rose-400' : 'text-rose-600'}`}>
|
|
Upcoming Appointments
|
|
</h3>
|
|
<p className={`text-xl sm:text-2xl font-bold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
{displayStats.scheduled}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={`rounded-lg border p-4 sm:p-5 md:p-6 hover:shadow-md transition-shadow ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}
|
|
>
|
|
<div className="flex items-start justify-between mb-3 sm:mb-4">
|
|
<div className={`p-2 sm:p-2.5 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
|
|
<CheckCircle2 className={`w-4 h-4 sm:w-5 sm:h-5 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
|
|
</div>
|
|
{displayStats.completed_pct !== undefined && (
|
|
<div
|
|
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${isDark ? "bg-green-900/30 text-green-400" : "bg-green-50 text-green-700"}`}
|
|
>
|
|
<ArrowUpRight className="w-3 h-3" />
|
|
<span>{`${Math.round(displayStats.completed_pct)}%`}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h3 className={`text-xs font-medium mb-1 sm:mb-2 uppercase tracking-wider ${isDark ? 'text-rose-400' : 'text-rose-600'}`}>
|
|
Completed Sessions
|
|
</h3>
|
|
<p className={`text-xl sm:text-2xl font-bold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
{displayStats.completed}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={`rounded-lg border p-4 sm:p-5 md:p-6 hover:shadow-md transition-shadow ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}
|
|
>
|
|
<div className="flex items-start justify-between mb-3 sm:mb-4">
|
|
<div className={`p-2 sm:p-2.5 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
|
|
<Calendar className={`w-4 h-4 sm:w-5 sm:h-5 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
|
|
</div>
|
|
{/* No percentage badge for total appointments */}
|
|
</div>
|
|
<div>
|
|
<h3 className={`text-xs font-medium mb-1 sm:mb-2 uppercase tracking-wider ${isDark ? 'text-rose-400' : 'text-rose-600'}`}>
|
|
Total Appointments
|
|
</h3>
|
|
<p className={`text-xl sm:text-2xl font-bold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
{displayStats.total_requests}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={`rounded-lg border p-4 sm:p-5 md:p-6 hover:shadow-md transition-shadow ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}
|
|
>
|
|
<div className="flex items-start justify-between mb-3 sm:mb-4">
|
|
<div className={`p-2 sm:p-2.5 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
|
|
<Calendar className={`w-4 h-4 sm:w-5 sm:h-5 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
|
|
</div>
|
|
{displayStats.pending_review_pct !== undefined && (
|
|
<div
|
|
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${isDark ? "bg-yellow-900/30 text-yellow-400" : "bg-yellow-50 text-yellow-700"}`}
|
|
>
|
|
<ArrowUpRight className="w-3 h-3" />
|
|
<span>{`${Math.round(displayStats.pending_review_pct)}%`}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h3 className={`text-xs font-medium mb-1 sm:mb-2 uppercase tracking-wider ${isDark ? 'text-rose-400' : 'text-rose-600'}`}>
|
|
Pending Review
|
|
</h3>
|
|
<p className={`text-xl sm:text-2xl font-bold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
{displayStats.pending_review}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* All Appointments Section */}
|
|
{allAppointments.length > 0 ? (
|
|
<div className={`rounded-lg border overflow-hidden ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
|
|
<div className={`px-4 sm:px-5 md:px-6 py-4 border-b ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
|
|
<h2 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
All Appointments
|
|
</h2>
|
|
</div>
|
|
<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"}`}>
|
|
Appointment
|
|
</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 Times
|
|
</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"}`}>
|
|
{allAppointments.map((appointment) => {
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case "scheduled":
|
|
return isDark ? 'bg-green-900/30 text-green-400' : 'bg-green-50 text-green-700';
|
|
case "pending_review":
|
|
return isDark ? 'bg-yellow-900/30 text-yellow-400' : 'bg-yellow-50 text-yellow-700';
|
|
case "completed":
|
|
return isDark ? 'bg-blue-900/30 text-blue-400' : 'bg-blue-50 text-blue-700';
|
|
case "rejected":
|
|
case "cancelled":
|
|
case "canceled":
|
|
return isDark ? 'bg-red-900/30 text-red-400' : 'bg-red-50 text-red-700';
|
|
default:
|
|
return isDark ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-700';
|
|
}
|
|
};
|
|
|
|
const formatStatus = (status: string) => {
|
|
return status.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
};
|
|
|
|
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
const timeSlotLabels: Record<string, string> = {
|
|
morning: 'Morning',
|
|
afternoon: 'Lunchtime',
|
|
evening: 'Evening',
|
|
};
|
|
|
|
return (
|
|
<tr
|
|
key={appointment.id}
|
|
className={`transition-colors cursor-pointer ${isDark ? "hover:bg-gray-700" : "hover:bg-gray-50"}`}
|
|
onClick={() => router.push(`/user/appointments/${appointment.id}`)}
|
|
>
|
|
<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>
|
|
{appointment.reason && (
|
|
<div className={`text-xs sm:text-sm truncate mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
|
{appointment.reason}
|
|
</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 hidden lg:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
|
{appointment.selected_slots && appointment.selected_slots.length > 0 ? (
|
|
<div className="flex flex-col gap-1">
|
|
{appointment.selected_slots.slice(0, 2).map((slot, idx) => (
|
|
<span key={idx} className="text-xs sm:text-sm">
|
|
{dayNames[slot.day]} - {timeSlotLabels[slot.time_slot] || slot.time_slot}
|
|
</span>
|
|
))}
|
|
{appointment.selected_slots.length > 2 && (
|
|
<span className="text-xs">+{appointment.selected_slots.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">
|
|
{appointment.participant_join_url && (
|
|
<a
|
|
href={appointment.participant_join_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={`p-1.5 sm:p-2 rounded-lg transition-colors ${
|
|
appointment.can_join_as_participant
|
|
? isDark
|
|
? "bg-blue-600 hover:bg-blue-700 text-white"
|
|
: "bg-blue-600 hover:bg-blue-700 text-white"
|
|
: isDark
|
|
? "text-gray-400 hover:text-gray-300 hover:bg-gray-700"
|
|
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
|
}`}
|
|
title={appointment.can_join_as_participant ? "Join Meeting" : "Meeting Not Available"}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
if (!appointment.can_join_as_participant) {
|
|
e.preventDefault();
|
|
}
|
|
}}
|
|
>
|
|
<Video className="w-4 h-4" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
) : !loading && (
|
|
<div className={`rounded-lg border p-4 sm:p-5 md:p-6 ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
|
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
<CalendarCheck className={`w-12 h-12 mb-4 ${isDark ? 'text-gray-500' : 'text-gray-400'}`} />
|
|
<p className={`text-lg font-medium mb-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
|
|
No Appointments
|
|
</p>
|
|
<p className={`text-sm mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
|
You don't have any appointments yet. Book an appointment to get started.
|
|
</p>
|
|
<Link href="/book-now">
|
|
<Button className="bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white">
|
|
<CalendarPlus className="w-4 h-4 mr-2" />
|
|
Book Appointment
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Account Information */}
|
|
{user && (
|
|
<div className={`rounded-lg border p-4 sm:p-5 md:p-6 ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
|
|
<h2 className={`text-lg font-semibold mb-4 ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
Account Information
|
|
</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`p-2 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
|
|
<User className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
|
|
</div>
|
|
<div>
|
|
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
|
Full Name
|
|
</p>
|
|
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
{user.first_name} {user.last_name}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className={`p-2 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
|
|
<Mail className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
|
|
</div>
|
|
<div>
|
|
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
|
Email
|
|
</p>
|
|
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
{user.email}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{user.phone_number && (
|
|
<div className="flex items-center gap-3">
|
|
<div className={`p-2 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
|
|
<Phone className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
|
|
</div>
|
|
<div>
|
|
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
|
Phone
|
|
</p>
|
|
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
{user.phone_number}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{user.date_joined && (
|
|
<div className="flex items-center gap-3">
|
|
<div className={`p-2 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
|
|
<Calendar className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
|
|
</div>
|
|
<div>
|
|
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
|
Member Since
|
|
</p>
|
|
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
{formatMemberSince(user.date_joined)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|