Enhance appointment detail and user dashboard components to conditionally render meeting links and buttons based on availability. Update appointment stats fetching logic to remove email dependency, improving user experience and API interaction. Refactor UI elements for better accessibility and clarity.
This commit is contained in:
parent
a864d909a8
commit
cea4747da5
@ -541,9 +541,10 @@ export default function AppointmentDetailPage() {
|
|||||||
{appointment.jitsi_room_id}
|
{appointment.jitsi_room_id}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => copyToClipboard(appointment.jitsi_room_id!, "Room ID")}
|
onClick={() => appointment.can_join_meeting && copyToClipboard(appointment.jitsi_room_id!, "Room ID")}
|
||||||
className={`p-2 rounded-lg hover:bg-opacity-80 transition-colors ${isDark ? "hover:bg-gray-700" : "hover:bg-gray-100"}`}
|
disabled={!appointment.can_join_meeting}
|
||||||
title="Copy room ID"
|
className={`p-2 rounded-lg transition-colors ${appointment.can_join_meeting ? (isDark ? "hover:bg-gray-700" : "hover:bg-gray-100") : (isDark ? "opacity-50 cursor-not-allowed" : "opacity-50 cursor-not-allowed")}`}
|
||||||
|
title={appointment.can_join_meeting ? "Copy room ID" : "Meeting not available"}
|
||||||
>
|
>
|
||||||
<Copy className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} />
|
<Copy className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} />
|
||||||
</button>
|
</button>
|
||||||
@ -555,6 +556,8 @@ export default function AppointmentDetailPage() {
|
|||||||
Meeting Link
|
Meeting Link
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{appointment.can_join_meeting ? (
|
||||||
|
<>
|
||||||
<a
|
<a
|
||||||
href={appointment.jitsi_meet_url}
|
href={appointment.jitsi_meet_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -571,6 +574,20 @@ export default function AppointmentDetailPage() {
|
|||||||
>
|
>
|
||||||
<ExternalLink className="w-4 h-4" />
|
<ExternalLink className="w-4 h-4" />
|
||||||
</a>
|
</a>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800/50 text-gray-500 border border-gray-700" : "bg-gray-100 text-gray-400 border border-gray-300"}`}>
|
||||||
|
{appointment.jitsi_meet_url}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium cursor-not-allowed ${isDark ? "bg-gray-700 text-gray-500" : "bg-gray-300 text-gray-500"}`}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{appointment.can_join_meeting !== undefined && (
|
{appointment.can_join_meeting !== undefined && (
|
||||||
|
|||||||
623
app/(user)/user/appointments/[id]/page.tsx
Normal file
623
app/(user)/user/appointments/[id]/page.tsx
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
User,
|
||||||
|
Video,
|
||||||
|
CalendarCheck,
|
||||||
|
Loader2,
|
||||||
|
ArrowLeft,
|
||||||
|
Mail,
|
||||||
|
Phone as PhoneIcon,
|
||||||
|
MessageSquare,
|
||||||
|
CheckCircle2,
|
||||||
|
ExternalLink,
|
||||||
|
Copy,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
|
import { getAppointmentDetail } from "@/lib/actions/appointments";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Navbar } from "@/components/Navbar";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { Appointment } from "@/lib/models/appointments";
|
||||||
|
|
||||||
|
export default function UserAppointmentDetailPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const appointmentId = params.id as string;
|
||||||
|
|
||||||
|
const [appointment, setAppointment] = useState<Appointment | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { theme } = useAppTheme();
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAppointment = async () => {
|
||||||
|
if (!appointmentId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await getAppointmentDetail(appointmentId);
|
||||||
|
setAppointment(data);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to load appointment details");
|
||||||
|
router.push("/user/dashboard");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAppointment();
|
||||||
|
}, [appointmentId, router]);
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
weekday: "long",
|
||||||
|
month: "long",
|
||||||
|
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 formatShortDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const normalized = status.toLowerCase();
|
||||||
|
if (isDark) {
|
||||||
|
switch (normalized) {
|
||||||
|
case "scheduled":
|
||||||
|
return "bg-blue-500/20 text-blue-300 border-blue-500/30";
|
||||||
|
case "completed":
|
||||||
|
return "bg-green-500/20 text-green-300 border-green-500/30";
|
||||||
|
case "rejected":
|
||||||
|
case "cancelled":
|
||||||
|
return "bg-red-500/20 text-red-300 border-red-500/30";
|
||||||
|
case "pending_review":
|
||||||
|
case "pending":
|
||||||
|
return "bg-yellow-500/20 text-yellow-300 border-yellow-500/30";
|
||||||
|
default:
|
||||||
|
return "bg-gray-700 text-gray-200 border-gray-600";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (normalized) {
|
||||||
|
case "scheduled":
|
||||||
|
return "bg-blue-50 text-blue-700 border-blue-200";
|
||||||
|
case "completed":
|
||||||
|
return "bg-green-50 text-green-700 border-green-200";
|
||||||
|
case "rejected":
|
||||||
|
case "cancelled":
|
||||||
|
return "bg-red-50 text-red-700 border-red-200";
|
||||||
|
case "pending_review":
|
||||||
|
case "pending":
|
||||||
|
return "bg-yellow-50 text-yellow-700 border-yellow-200";
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-700 border-gray-300";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatStatus = (status: string) => {
|
||||||
|
return status.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string, label: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
toast.success(`${label} copied to clipboard`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
|
||||||
|
<Navbar />
|
||||||
|
<div className="min-h-[calc(100vh-4rem)] flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className={`w-12 h-12 animate-spin mx-auto mb-4 ${isDark ? "text-rose-400" : "text-rose-600"}`} />
|
||||||
|
<p className={`text-sm ${isDark ? "text-gray-400" : "text-gray-600"}`}>Loading appointment details...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appointment) {
|
||||||
|
return (
|
||||||
|
<div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
|
||||||
|
<Navbar />
|
||||||
|
<div className="min-h-[calc(100vh-4rem)] flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className={`text-lg mb-4 ${isDark ? "text-gray-400" : "text-gray-600"}`}>Appointment not found</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push("/user/dashboard")}
|
||||||
|
className="bg-rose-600 hover:bg-rose-700 text-white"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Back to Dashboard
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
|
||||||
|
<Navbar />
|
||||||
|
<main className="container mx-auto px-4 sm:px-6 lg:px-8 space-y-6 pt-20 sm:pt-24 pb-8">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="flex flex-col gap-3 sm:gap-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => router.push("/user/dashboard")}
|
||||||
|
className={`flex items-center gap-2 w-fit ${isDark ? "text-gray-300 hover:bg-gray-800 hover:text-white" : "text-gray-600 hover:bg-gray-100"}`}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Back to Dashboard
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className={`h-12 w-12 sm:h-16 sm:w-16 rounded-full flex items-center justify-center text-xl sm:text-2xl font-bold ${isDark ? "bg-gradient-to-br from-rose-500 to-pink-600 text-white" : "bg-gradient-to-br from-rose-100 to-pink-100 text-rose-600"}`}>
|
||||||
|
<CalendarCheck className="w-6 h-6 sm:w-8 sm:h-8" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className={`text-2xl sm:text-3xl lg:text-4xl font-bold ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
Appointment Details
|
||||||
|
</h1>
|
||||||
|
<p className={`text-xs sm:text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
Request #{appointment.id.slice(0, 8)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
className={`px-3 sm:px-4 py-2 inline-flex items-center gap-2 text-xs sm:text-sm font-semibold rounded-full border ${getStatusColor(
|
||||||
|
appointment.status
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{appointment.status === "scheduled" && <CheckCircle2 className="w-3 h-3 sm:w-4 sm:h-4" />}
|
||||||
|
{formatStatus(appointment.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Main Content - Left Column (2/3) */}
|
||||||
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
{/* Appointment Information Card */}
|
||||||
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||||
|
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||||
|
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
<User className={`w-5 h-5 ${isDark ? "text-rose-400" : "text-rose-600"}`} />
|
||||||
|
Appointment Information
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className={`text-xs font-medium uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
Full Name
|
||||||
|
</p>
|
||||||
|
<p className={`text-base font-medium ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
{appointment.first_name} {appointment.last_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className={`text-xs font-medium uppercase tracking-wider flex items-center gap-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
<Mail className="w-3 h-3" />
|
||||||
|
Email Address
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className={`text-base font-medium ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
{appointment.email}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => copyToClipboard(appointment.email, "Email")}
|
||||||
|
className={`p-1.5 rounded-lg hover:bg-opacity-80 transition-colors ${isDark ? "hover:bg-gray-700" : "hover:bg-gray-100"}`}
|
||||||
|
title="Copy email"
|
||||||
|
>
|
||||||
|
<Copy className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{appointment.phone && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className={`text-xs font-medium uppercase tracking-wider flex items-center gap-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
<PhoneIcon className="w-3 h-3" />
|
||||||
|
Phone Number
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className={`text-base font-medium ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
{appointment.phone}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => copyToClipboard(appointment.phone!, "Phone")}
|
||||||
|
className={`p-1.5 rounded-lg hover:bg-opacity-80 transition-colors ${isDark ? "hover:bg-gray-700" : "hover:bg-gray-100"}`}
|
||||||
|
title="Copy phone"
|
||||||
|
>
|
||||||
|
<Copy className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scheduled Appointment Details */}
|
||||||
|
{appointment.scheduled_datetime && (
|
||||||
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||||
|
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||||
|
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
<Calendar className={`w-5 h-5 ${isDark ? "text-rose-400" : "text-rose-600"}`} />
|
||||||
|
Scheduled Appointment
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className={`p-4 rounded-xl ${isDark ? "bg-blue-500/10 border border-blue-500/20" : "bg-blue-50 border border-blue-100"}`}>
|
||||||
|
<Calendar className={`w-6 h-6 ${isDark ? "text-blue-400" : "text-blue-600"}`} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className={`text-2xl font-bold mb-1 ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
{formatDate(appointment.scheduled_datetime)}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 mt-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} />
|
||||||
|
<p className={`text-base ${isDark ? "text-gray-300" : "text-gray-700"}`}>
|
||||||
|
{formatTime(appointment.scheduled_datetime)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{appointment.scheduled_duration && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`text-base ${isDark ? "text-gray-400" : "text-gray-500"}`}>•</span>
|
||||||
|
<p className={`text-base ${isDark ? "text-gray-300" : "text-gray-700"}`}>
|
||||||
|
{appointment.meeting_duration_display || `${appointment.scheduled_duration} minutes`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Preferred Dates & Times */}
|
||||||
|
{((appointment.preferred_dates && (Array.isArray(appointment.preferred_dates) ? appointment.preferred_dates.length > 0 : appointment.preferred_dates)) ||
|
||||||
|
(appointment.preferred_time_slots && (Array.isArray(appointment.preferred_time_slots) ? appointment.preferred_time_slots.length > 0 : appointment.preferred_time_slots))) && (
|
||||||
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||||
|
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||||
|
<h2 className={`text-lg font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
Preferred Availability
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{appointment.preferred_dates && (
|
||||||
|
<div>
|
||||||
|
<p className={`text-sm font-medium mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
Preferred Dates
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Array.isArray(appointment.preferred_dates) ? (
|
||||||
|
(appointment.preferred_dates as string[]).map((date, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium ${isDark ? "bg-gray-700 text-gray-200 border border-gray-600" : "bg-gray-100 text-gray-700 border border-gray-200"}`}
|
||||||
|
>
|
||||||
|
{formatShortDate(date)}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium ${isDark ? "bg-gray-700 text-gray-200 border border-gray-600" : "bg-gray-100 text-gray-700 border border-gray-200"}`}
|
||||||
|
>
|
||||||
|
{appointment.preferred_dates_display || appointment.preferred_dates || 'N/A'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{appointment.preferred_time_slots && (
|
||||||
|
<div>
|
||||||
|
<p className={`text-sm font-medium mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
Preferred Time Slots
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Array.isArray(appointment.preferred_time_slots) ? (
|
||||||
|
(appointment.preferred_time_slots as string[]).map((slot, idx) => {
|
||||||
|
const timeSlotLabels: Record<string, string> = {
|
||||||
|
morning: "Morning",
|
||||||
|
afternoon: "Lunchtime",
|
||||||
|
evening: "Evening",
|
||||||
|
};
|
||||||
|
const normalizedSlot = String(slot).toLowerCase().trim();
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium capitalize ${isDark ? "bg-rose-500/20 text-rose-300 border border-rose-500/30" : "bg-rose-50 text-rose-700 border border-rose-200"}`}
|
||||||
|
>
|
||||||
|
{timeSlotLabels[normalizedSlot] || slot}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium capitalize ${isDark ? "bg-rose-500/20 text-rose-300 border border-rose-500/30" : "bg-rose-50 text-rose-700 border border-rose-200"}`}
|
||||||
|
>
|
||||||
|
{appointment.preferred_time_slots_display || appointment.preferred_time_slots || 'N/A'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Matching Availability */}
|
||||||
|
{appointment.matching_availability && Array.isArray(appointment.matching_availability) && appointment.matching_availability.length > 0 && (
|
||||||
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||||
|
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||||
|
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
<CalendarCheck className={`w-5 h-5 ${isDark ? "text-green-400" : "text-green-600"}`} />
|
||||||
|
Matching Availability
|
||||||
|
{appointment.are_preferences_available !== undefined && (
|
||||||
|
<span className={`ml-auto px-3 py-1 text-xs font-medium rounded-full ${appointment.are_preferences_available ? (isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200") : (isDark ? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30" : "bg-yellow-50 text-yellow-700 border border-yellow-200")}`}>
|
||||||
|
{appointment.are_preferences_available ? "Available" : "Partially Available"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{appointment.matching_availability.map((match: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`p-4 rounded-xl border ${isDark ? "bg-gray-700/50 border-gray-600" : "bg-gray-50 border-gray-200"}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<p className={`text-base font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
{match.day_name || "Unknown Day"}
|
||||||
|
</p>
|
||||||
|
<p className={`text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
{formatShortDate(match.date || match.date_obj || "")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{match.available_slots && Array.isArray(match.available_slots) && match.available_slots.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
|
{match.available_slots.map((slot: string, slotIdx: number) => {
|
||||||
|
const timeSlotLabels: Record<string, string> = {
|
||||||
|
morning: "Morning",
|
||||||
|
afternoon: "Lunchtime",
|
||||||
|
evening: "Evening",
|
||||||
|
};
|
||||||
|
const normalizedSlot = String(slot).toLowerCase().trim();
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={slotIdx}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium ${isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200"}`}
|
||||||
|
>
|
||||||
|
{timeSlotLabels[normalizedSlot] || slot}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reason */}
|
||||||
|
{appointment.reason && (
|
||||||
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||||
|
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||||
|
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
<MessageSquare className={`w-5 h-5 ${isDark ? "text-rose-400" : "text-rose-600"}`} />
|
||||||
|
Reason for Appointment
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<p className={`text-base leading-relaxed ${isDark ? "text-gray-300" : "text-gray-700"}`}>
|
||||||
|
{appointment.reason}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Rejection Reason */}
|
||||||
|
{appointment.rejection_reason && (
|
||||||
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-red-900/20 border-red-800/50" : "bg-red-50 border-red-200"}`}>
|
||||||
|
<div className={`px-6 py-4 border-b ${isDark ? "border-red-800/50" : "border-red-200"}`}>
|
||||||
|
<h2 className={`text-lg font-semibold ${isDark ? "text-red-300" : "text-red-900"}`}>
|
||||||
|
Rejection Reason
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<p className={`text-base leading-relaxed ${isDark ? "text-red-200" : "text-red-800"}`}>
|
||||||
|
{appointment.rejection_reason}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Meeting Information */}
|
||||||
|
{appointment.jitsi_meet_url && (
|
||||||
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}>
|
||||||
|
<div className={`px-6 py-4 border-b ${isDark ? "border-blue-800/30" : "border-blue-200"}`}>
|
||||||
|
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
<Video className={`w-5 h-5 ${isDark ? "text-blue-400" : "text-blue-600"}`} />
|
||||||
|
Video Meeting
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
{appointment.jitsi_room_id && (
|
||||||
|
<div>
|
||||||
|
<p className={`text-xs font-medium mb-2 uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
Meeting Room ID
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className={`text-sm font-mono px-3 py-2 rounded-lg ${isDark ? "bg-gray-800 text-gray-200" : "bg-white text-gray-900 border border-gray-200"}`}>
|
||||||
|
{appointment.jitsi_room_id}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => appointment.can_join_meeting && copyToClipboard(appointment.jitsi_room_id!, "Room ID")}
|
||||||
|
disabled={!appointment.can_join_meeting}
|
||||||
|
className={`p-2 rounded-lg transition-colors ${appointment.can_join_meeting ? (isDark ? "hover:bg-gray-700" : "hover:bg-gray-100") : (isDark ? "opacity-50 cursor-not-allowed" : "opacity-50 cursor-not-allowed")}`}
|
||||||
|
title={appointment.can_join_meeting ? "Copy room ID" : "Meeting not available"}
|
||||||
|
>
|
||||||
|
<Copy className={`w-4 h-4 ${isDark ? "text-gray-400" : "text-gray-500"}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className={`text-xs font-medium mb-2 uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
Meeting Link
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{appointment.can_join_meeting ? (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href={appointment.jitsi_meet_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800 text-blue-400 hover:bg-gray-700" : "bg-white text-blue-600 hover:bg-gray-50 border border-gray-200"}`}
|
||||||
|
>
|
||||||
|
{appointment.jitsi_meet_url}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={appointment.jitsi_meet_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${isDark ? "bg-blue-600 hover:bg-blue-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white"}`}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800/50 text-gray-500 border border-gray-700" : "bg-gray-100 text-gray-400 border border-gray-300"}`}>
|
||||||
|
{appointment.jitsi_meet_url}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium cursor-not-allowed ${isDark ? "bg-gray-700 text-gray-500" : "bg-gray-300 text-gray-500"}`}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{appointment.can_join_meeting !== undefined && (
|
||||||
|
<div className={`flex items-center gap-2 px-4 py-3 rounded-lg ${appointment.can_join_meeting ? (isDark ? "bg-green-500/20 border border-green-500/30" : "bg-green-50 border border-green-200") : (isDark ? "bg-gray-800 border border-gray-700" : "bg-gray-50 border border-gray-200")}`}>
|
||||||
|
<div className={`h-2 w-2 rounded-full ${appointment.can_join_meeting ? (isDark ? "bg-green-400" : "bg-green-600") : (isDark ? "bg-gray-500" : "bg-gray-400")}`} />
|
||||||
|
<p className={`text-sm font-medium ${appointment.can_join_meeting ? (isDark ? "text-green-300" : "text-green-700") : (isDark ? "text-gray-400" : "text-gray-500")}`}>
|
||||||
|
{appointment.can_join_meeting ? "Meeting is active - You can join now" : "Meeting is not available yet"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar - Right Column (1/3) */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Quick Info Card */}
|
||||||
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||||
|
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||||
|
<h2 className={`text-lg font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
Quick Info
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className={`text-xs font-medium mb-1 uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
Created
|
||||||
|
</p>
|
||||||
|
<p className={`text-sm font-medium ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
|
{formatShortDate(appointment.created_at)}
|
||||||
|
</p>
|
||||||
|
<p className={`text-xs mt-0.5 ${isDark ? "text-gray-500" : "text-gray-500"}`}>
|
||||||
|
{formatTime(appointment.created_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<p className={`text-xs font-medium mb-1 uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||||
|
Status
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-semibold rounded-lg border ${getStatusColor(
|
||||||
|
appointment.status
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{appointment.status === "scheduled" && <CheckCircle2 className="w-4 h-4" />}
|
||||||
|
{formatStatus(appointment.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Join Meeting Button */}
|
||||||
|
{appointment.status === "scheduled" && appointment.jitsi_meet_url && (
|
||||||
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200"}`}>
|
||||||
|
<div className="p-6">
|
||||||
|
{appointment.can_join_meeting ? (
|
||||||
|
<a
|
||||||
|
href={appointment.jitsi_meet_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={`flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 text-white h-12 rounded-lg text-base font-medium transition-colors`}
|
||||||
|
>
|
||||||
|
<Video className="w-5 h-5" />
|
||||||
|
Join Meeting
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className={`flex items-center justify-center gap-2 w-full cursor-not-allowed h-12 rounded-lg text-base font-medium transition-colors ${isDark ? "bg-gray-700 text-gray-500" : "bg-gray-300 text-gray-500"}`}
|
||||||
|
>
|
||||||
|
<Video className="w-5 h-5" />
|
||||||
|
Meeting Not Available Yet
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useEffect, useState } from "react";
|
import { useMemo, useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
@ -22,11 +23,12 @@ import Link from "next/link";
|
|||||||
import { Navbar } from "@/components/Navbar";
|
import { Navbar } from "@/components/Navbar";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { listAppointments, getUserAppointmentStats } from "@/lib/actions/appointments";
|
import { getUserAppointments, getUserAppointmentStats } from "@/lib/actions/appointments";
|
||||||
import type { Appointment, UserAppointmentStats } from "@/lib/models/appointments";
|
import type { Appointment, UserAppointmentStats } from "@/lib/models/appointments";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export default function UserDashboard() {
|
export default function UserDashboard() {
|
||||||
|
const router = useRouter();
|
||||||
const { theme } = useAppTheme();
|
const { theme } = useAppTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@ -35,12 +37,12 @@ export default function UserDashboard() {
|
|||||||
const [stats, setStats] = useState<UserAppointmentStats | null>(null);
|
const [stats, setStats] = useState<UserAppointmentStats | null>(null);
|
||||||
const [loadingStats, setLoadingStats] = useState(true);
|
const [loadingStats, setLoadingStats] = useState(true);
|
||||||
|
|
||||||
// Fetch appointments using the same endpoint as admin booking table
|
// Fetch user appointments from user-specific endpoint
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAppointments = async () => {
|
const fetchAppointments = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await listAppointments();
|
const data = await getUserAppointments();
|
||||||
setAppointments(data || []);
|
setAppointments(data || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to load appointments. Please try again.");
|
toast.error("Failed to load appointments. Please try again.");
|
||||||
@ -53,21 +55,15 @@ export default function UserDashboard() {
|
|||||||
fetchAppointments();
|
fetchAppointments();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Fetch stats from API using user email
|
// Fetch stats from API for authenticated user
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
if (!user?.email) {
|
|
||||||
setLoadingStats(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingStats(true);
|
setLoadingStats(true);
|
||||||
try {
|
try {
|
||||||
const statsData = await getUserAppointmentStats(user.email);
|
const statsData = await getUserAppointmentStats();
|
||||||
setStats(statsData);
|
setStats(statsData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to load appointment statistics.");
|
toast.error("Failed to load appointment statistics.");
|
||||||
// Set default stats on error
|
|
||||||
setStats({
|
setStats({
|
||||||
total_requests: 0,
|
total_requests: 0,
|
||||||
pending_review: 0,
|
pending_review: 0,
|
||||||
@ -75,7 +71,6 @@ export default function UserDashboard() {
|
|||||||
rejected: 0,
|
rejected: 0,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
completion_rate: 0,
|
completion_rate: 0,
|
||||||
email: user.email,
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingStats(false);
|
setLoadingStats(false);
|
||||||
@ -83,7 +78,7 @@ export default function UserDashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchStats();
|
fetchStats();
|
||||||
}, [user?.email]);
|
}, []);
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
@ -388,7 +383,8 @@ export default function UserDashboard() {
|
|||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={appointment.id}
|
key={appointment.id}
|
||||||
className={`transition-colors ${isDark ? "hover:bg-gray-700" : "hover:bg-gray-50"}`}
|
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">
|
<td className="px-3 sm:px-4 md:px-6 py-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@ -478,6 +474,7 @@ export default function UserDashboard() {
|
|||||||
}`}
|
}`}
|
||||||
title={appointment.can_join_meeting ? "Join Meeting" : "Meeting Not Available"}
|
title={appointment.can_join_meeting ? "Join Meeting" : "Meeting Not Available"}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
if (!appointment.can_join_meeting) {
|
if (!appointment.can_join_meeting) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,17 +138,11 @@ export function useAppointments(options?: {
|
|||||||
staleTime: 1 * 60 * 1000, // 1 minute
|
staleTime: 1 * 60 * 1000, // 1 minute
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get user appointment stats query - disabled because it requires email parameter
|
|
||||||
// Use getUserAppointmentStats(email) directly where email is available
|
|
||||||
const userAppointmentStatsQuery = useQuery<UserAppointmentStats>({
|
const userAppointmentStatsQuery = useQuery<UserAppointmentStats>({
|
||||||
queryKey: ["appointments", "user", "stats"],
|
queryKey: ["appointments", "user", "stats"],
|
||||||
queryFn: async () => {
|
queryFn: () => getUserAppointmentStats(),
|
||||||
// This query is disabled - getUserAppointmentStats requires email parameter
|
enabled: enableStats,
|
||||||
// Use getUserAppointmentStats(email) directly in components where email is available
|
staleTime: 1 * 60 * 1000,
|
||||||
return {} as UserAppointmentStats;
|
|
||||||
},
|
|
||||||
enabled: false, // Disabled - requires email parameter which hook doesn't have access to
|
|
||||||
staleTime: 1 * 60 * 1000, // 1 minute
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get Jitsi meeting info query
|
// Get Jitsi meeting info query
|
||||||
|
|||||||
@ -624,45 +624,26 @@ export async function getAppointmentStats(): Promise<AppointmentStats> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserAppointmentStats(email: string): Promise<UserAppointmentStats> {
|
export async function getUserAppointmentStats(): Promise<UserAppointmentStats> {
|
||||||
const tokens = getStoredTokens();
|
const tokens = getStoredTokens();
|
||||||
if (!tokens.access) {
|
if (!tokens.access) {
|
||||||
throw new Error("Authentication required.");
|
throw new Error("Authentication required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!email) {
|
|
||||||
throw new Error("Email is required to fetch user appointment stats.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(API_ENDPOINTS.meetings.userAppointmentStats, {
|
const response = await fetch(API_ENDPOINTS.meetings.userAppointmentStats, {
|
||||||
method: "POST",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${tokens.access}`,
|
Authorization: `Bearer ${tokens.access}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseText = await response.text();
|
const data = await parseResponse(response);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorData: any;
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
||||||
try {
|
|
||||||
errorData = JSON.parse(responseText);
|
|
||||||
} catch {
|
|
||||||
throw new Error(`Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`);
|
|
||||||
}
|
|
||||||
throw new Error(extractErrorMessage(errorData as unknown as ApiError));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return data;
|
||||||
if (!responseText || responseText.trim().length === 0) {
|
|
||||||
throw new Error("Empty response from server");
|
|
||||||
}
|
|
||||||
return JSON.parse(responseText);
|
|
||||||
} catch {
|
|
||||||
throw new Error("Failed to parse response: Invalid JSON format");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getJitsiMeetingInfo(id: string): Promise<JitsiMeetingInfo> {
|
export async function getJitsiMeetingInfo(id: string): Promise<JitsiMeetingInfo> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user