diff --git a/app/(admin)/admin/booking/[id]/page.tsx b/app/(admin)/admin/booking/[id]/page.tsx index eda1ac4..c4c6d10 100644 --- a/app/(admin)/admin/booking/[id]/page.tsx +++ b/app/(admin)/admin/booking/[id]/page.tsx @@ -541,9 +541,10 @@ export default function AppointmentDetailPage() { {appointment.jitsi_room_id}

@@ -555,22 +556,38 @@ export default function AppointmentDetailPage() { Meeting Link

- - {appointment.jitsi_meet_url} - - - - + {appointment.can_join_meeting ? ( + <> + + {appointment.jitsi_meet_url} + + + + + + ) : ( + <> +
+ {appointment.jitsi_meet_url} +
+ + + )}
{appointment.can_join_meeting !== undefined && ( diff --git a/app/(user)/user/appointments/[id]/page.tsx b/app/(user)/user/appointments/[id]/page.tsx new file mode 100644 index 0000000..9334a01 --- /dev/null +++ b/app/(user)/user/appointments/[id]/page.tsx @@ -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(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 ( +
+ +
+
+ +

Loading appointment details...

+
+
+
+ ); + } + + if (!appointment) { + return ( +
+ +
+
+

Appointment not found

+ +
+
+
+ ); + } + + return ( +
+ +
+ {/* Page Header */} +
+ + +
+
+
+
+ +
+
+

+ Appointment Details +

+

+ Request #{appointment.id.slice(0, 8)} +

+
+
+
+ +
+ + {appointment.status === "scheduled" && } + {formatStatus(appointment.status)} + +
+
+
+ +
+ {/* Main Content - Left Column (2/3) */} +
+ {/* Appointment Information Card */} +
+
+

+ + Appointment Information +

+
+
+
+
+

+ Full Name +

+

+ {appointment.first_name} {appointment.last_name} +

+
+
+

+ + Email Address +

+
+

+ {appointment.email} +

+ +
+
+ {appointment.phone && ( +
+

+ + Phone Number +

+
+

+ {appointment.phone} +

+ +
+
+ )} +
+
+
+ + {/* Scheduled Appointment Details */} + {appointment.scheduled_datetime && ( +
+
+

+ + Scheduled Appointment +

+
+
+
+
+ +
+
+

+ {formatDate(appointment.scheduled_datetime)} +

+
+
+ +

+ {formatTime(appointment.scheduled_datetime)} +

+
+ {appointment.scheduled_duration && ( +
+ +

+ {appointment.meeting_duration_display || `${appointment.scheduled_duration} minutes`} +

+
+ )} +
+
+
+
+
+ )} + + {/* 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))) && ( +
+
+

+ Preferred Availability +

+
+
+ {appointment.preferred_dates && ( +
+

+ Preferred Dates +

+
+ {Array.isArray(appointment.preferred_dates) ? ( + (appointment.preferred_dates as string[]).map((date, idx) => ( + + {formatShortDate(date)} + + )) + ) : ( + + {appointment.preferred_dates_display || appointment.preferred_dates || 'N/A'} + + )} +
+
+ )} + {appointment.preferred_time_slots && ( +
+

+ Preferred Time Slots +

+
+ {Array.isArray(appointment.preferred_time_slots) ? ( + (appointment.preferred_time_slots as string[]).map((slot, idx) => { + const timeSlotLabels: Record = { + morning: "Morning", + afternoon: "Lunchtime", + evening: "Evening", + }; + const normalizedSlot = String(slot).toLowerCase().trim(); + return ( + + {timeSlotLabels[normalizedSlot] || slot} + + ); + }) + ) : ( + + {appointment.preferred_time_slots_display || appointment.preferred_time_slots || 'N/A'} + + )} +
+
+ )} +
+
+ )} + + {/* Matching Availability */} + {appointment.matching_availability && Array.isArray(appointment.matching_availability) && appointment.matching_availability.length > 0 && ( +
+
+

+ + Matching Availability + {appointment.are_preferences_available !== undefined && ( + + {appointment.are_preferences_available ? "Available" : "Partially Available"} + + )} +

+
+
+
+ {appointment.matching_availability.map((match: any, idx: number) => ( +
+
+
+

+ {match.day_name || "Unknown Day"} +

+

+ {formatShortDate(match.date || match.date_obj || "")} +

+
+
+ {match.available_slots && Array.isArray(match.available_slots) && match.available_slots.length > 0 && ( +
+ {match.available_slots.map((slot: string, slotIdx: number) => { + const timeSlotLabels: Record = { + morning: "Morning", + afternoon: "Lunchtime", + evening: "Evening", + }; + const normalizedSlot = String(slot).toLowerCase().trim(); + return ( + + {timeSlotLabels[normalizedSlot] || slot} + + ); + })} +
+ )} +
+ ))} +
+
+
+ )} + + {/* Reason */} + {appointment.reason && ( +
+
+

+ + Reason for Appointment +

+
+
+

+ {appointment.reason} +

+
+
+ )} + + {/* Rejection Reason */} + {appointment.rejection_reason && ( +
+
+

+ Rejection Reason +

+
+
+

+ {appointment.rejection_reason} +

+
+
+ )} + + {/* Meeting Information */} + {appointment.jitsi_meet_url && ( +
+
+

+

+
+
+ {appointment.jitsi_room_id && ( +
+

+ Meeting Room ID +

+
+

+ {appointment.jitsi_room_id} +

+ +
+
+ )} +
+

+ Meeting Link +

+
+ {appointment.can_join_meeting ? ( + <> + + {appointment.jitsi_meet_url} + + + + + + ) : ( + <> +
+ {appointment.jitsi_meet_url} +
+ + + )} +
+
+ {appointment.can_join_meeting !== undefined && ( +
+
+

+ {appointment.can_join_meeting ? "Meeting is active - You can join now" : "Meeting is not available yet"} +

+
+ )} +
+
+ )} +
+ + {/* Sidebar - Right Column (1/3) */} +
+ {/* Quick Info Card */} +
+
+

+ Quick Info +

+
+
+
+

+ Created +

+

+ {formatShortDate(appointment.created_at)} +

+

+ {formatTime(appointment.created_at)} +

+
+
+

+ Status +

+ + {appointment.status === "scheduled" && } + {formatStatus(appointment.status)} + +
+
+
+ + {/* Join Meeting Button */} + {appointment.status === "scheduled" && appointment.jitsi_meet_url && ( +
+
+ {appointment.can_join_meeting ? ( + + + ) : ( + + )} +
+
+ )} +
+
+
+
+ ); +} + diff --git a/app/(user)/user/dashboard/page.tsx b/app/(user)/user/dashboard/page.tsx index b51c275..d5217b9 100644 --- a/app/(user)/user/dashboard/page.tsx +++ b/app/(user)/user/dashboard/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useMemo, useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Calendar, @@ -22,11 +23,12 @@ import Link from "next/link"; import { Navbar } from "@/components/Navbar"; import { useAppTheme } from "@/components/ThemeProvider"; 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 { toast } from "sonner"; export default function UserDashboard() { + const router = useRouter(); const { theme } = useAppTheme(); const isDark = theme === "dark"; const { user } = useAuth(); @@ -35,12 +37,12 @@ export default function UserDashboard() { const [stats, setStats] = useState(null); const [loadingStats, setLoadingStats] = useState(true); - // Fetch appointments using the same endpoint as admin booking table + // Fetch user appointments from user-specific endpoint useEffect(() => { const fetchAppointments = async () => { setLoading(true); try { - const data = await listAppointments(); + const data = await getUserAppointments(); setAppointments(data || []); } catch (error) { toast.error("Failed to load appointments. Please try again."); @@ -53,21 +55,15 @@ export default function UserDashboard() { fetchAppointments(); }, []); - // Fetch stats from API using user email + // Fetch stats from API for authenticated user useEffect(() => { const fetchStats = async () => { - if (!user?.email) { - setLoadingStats(false); - return; - } - setLoadingStats(true); try { - const statsData = await getUserAppointmentStats(user.email); + const statsData = await getUserAppointmentStats(); setStats(statsData); } catch (error) { toast.error("Failed to load appointment statistics."); - // Set default stats on error setStats({ total_requests: 0, pending_review: 0, @@ -75,7 +71,6 @@ export default function UserDashboard() { rejected: 0, completed: 0, completion_rate: 0, - email: user.email, }); } finally { setLoadingStats(false); @@ -83,7 +78,7 @@ export default function UserDashboard() { }; fetchStats(); - }, [user?.email]); + }, []); const formatDate = (dateString: string) => { const date = new Date(dateString); @@ -221,17 +216,17 @@ export default function UserDashboard() { <> {/* Stats Grid */}
-
-
-
- -
+
+
+ +
+
- + {displayStats.scheduled > 0 ? `+${displayStats.scheduled}` : "0"}
@@ -256,7 +251,7 @@ export default function UserDashboard() {
- + {displayStats.completed > 0 ? `+${displayStats.completed}` : "0"}
@@ -268,8 +263,8 @@ export default function UserDashboard() { {displayStats.completed}

vs last month

-
-
+
+
{`${Math.round(displayStats.completion_rate || 0)}%`}
-
-

+
+

Total Appointments -

-

+

+

{displayStats.total_requests} -

-

vs last month

-
+

+

vs last month

+

All Appointments -

+
@@ -387,8 +382,9 @@ export default function UserDashboard() { return ( router.push(`/user/appointments/${appointment.id}`)} > ); diff --git a/hooks/useAppointments.ts b/hooks/useAppointments.ts index 0ab8800..728f586 100644 --- a/hooks/useAppointments.ts +++ b/hooks/useAppointments.ts @@ -138,17 +138,11 @@ export function useAppointments(options?: { 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({ queryKey: ["appointments", "user", "stats"], - queryFn: async () => { - // This query is disabled - getUserAppointmentStats requires email parameter - // Use getUserAppointmentStats(email) directly in components where email is available - return {} as UserAppointmentStats; - }, - enabled: false, // Disabled - requires email parameter which hook doesn't have access to - staleTime: 1 * 60 * 1000, // 1 minute + queryFn: () => getUserAppointmentStats(), + enabled: enableStats, + staleTime: 1 * 60 * 1000, }); // Get Jitsi meeting info query diff --git a/lib/actions/appointments.ts b/lib/actions/appointments.ts index 64ea7b6..4fde1ae 100644 --- a/lib/actions/appointments.ts +++ b/lib/actions/appointments.ts @@ -9,7 +9,6 @@ import type { import type { Appointment, AppointmentResponse, - AppointmentsListResponse, AvailableDatesResponse, AdminAvailability, AppointmentStats, @@ -23,125 +22,133 @@ import type { SelectedSlot, } from "@/lib/models/appointments"; -// Helper function to extract error message from API response function extractErrorMessage(error: ApiError): string { if (error.detail) { - if (Array.isArray(error.detail)) { - return error.detail.join(", "); - } - return String(error.detail); + return Array.isArray(error.detail) ? error.detail.join(", ") : String(error.detail); } - if (error.message) { - if (Array.isArray(error.message)) { - return error.message.join(", "); - } - return String(error.message); + return Array.isArray(error.message) ? error.message.join(", ") : String(error.message); } - if (typeof error === "string") { return error; } - - return "An error occurred while creating the appointment"; + return "An error occurred"; } -// Create appointment -export async function createAppointment( - input: CreateAppointmentInput -): Promise { - const tokens = getStoredTokens(); - - if (!tokens.access) { - throw new Error("Authentication required. Please log in to book an appointment."); +async function parseResponse(response: Response): Promise { + const responseText = await response.text(); + const contentType = response.headers.get("content-type") || ""; + + if (!responseText || responseText.trim().length === 0) { + if (response.ok) { + return null; + } + throw new Error(`Server error (${response.status}): ${response.statusText || 'Empty response'}`); } - // Validate required fields - if (!input.first_name || !input.last_name || !input.email) { - throw new Error("First name, last name, and email are required"); - } - - // New API format: use selected_slots - if (!input.selected_slots || input.selected_slots.length === 0) { - throw new Error("At least one time slot must be selected"); + if (contentType.includes("application/json")) { + try { + return JSON.parse(responseText); + } catch { + throw new Error(`Server error (${response.status}): Invalid JSON format`); + } } - // Validate and clean selected_slots to ensure all have day and time_slot - // This filters out any invalid slots and ensures proper format - const validSlots: SelectedSlot[] = input.selected_slots - .filter((slot, index) => { - // Check if slot exists and is an object - if (!slot || typeof slot !== 'object') { - return false; - } - // Check if both day and time_slot properties exist - if (typeof slot.day === 'undefined' || typeof slot.time_slot === 'undefined') { - return false; - } - // Validate day is a number between 0-6 + const errorMatch = responseText.match(/]*>([\s\S]*?)<\/pre>/i) || + responseText.match(/]*>([\s\S]*?)<\/h1>/i); + const errorText = errorMatch?.[1]?.replace(/<[^>]*>/g, '').trim() || ''; + throw new Error(`Server error (${response.status}): ${errorText || response.statusText || 'Internal Server Error'}`); +} + +function extractHtmlError(responseText: string): string { + const errorMatch = responseText.match(/]*>([\s\S]*?)<\/pre>/i); + if (!errorMatch) return ''; + + const traceback = errorMatch[1].replace(/<[^>]*>/g, ''); + const lines = traceback.split('\n').filter(line => line.trim()); + + for (let i = lines.length - 1; i >= Math.max(0, lines.length - 5); i--) { + const line = lines[i]; + if (line.match(/(Error|Exception|Failed)/i)) { + return line.trim().replace(/^(Traceback|File|Error|Exception):\s*/i, ''); + } + } + + return lines[lines.length - 1]?.trim() || ''; +} + +function validateAndCleanSlots(slots: any[]): SelectedSlot[] { + return slots + .filter(slot => { + if (!slot || typeof slot !== 'object') return false; const dayNum = Number(slot.day); - if (isNaN(dayNum) || dayNum < 0 || dayNum > 6) { - return false; - } - // Validate time_slot is a valid string (normalize to lowercase) - const timeSlot = String(slot.time_slot).toLowerCase().trim(); - if (!['morning', 'afternoon', 'evening'].includes(timeSlot)) { - return false; - } - return true; + const timeSlot = String(slot.time_slot || '').toLowerCase().trim(); + return !isNaN(dayNum) && dayNum >= 0 && dayNum <= 6 && + ['morning', 'afternoon', 'evening'].includes(timeSlot); }) .map(slot => ({ day: Number(slot.day), time_slot: String(slot.time_slot).toLowerCase().trim() as "morning" | "afternoon" | "evening", })); +} +function normalizeAvailabilitySchedule(schedule: any): Record { + if (typeof schedule === 'string') { + try { + schedule = JSON.parse(schedule); + } catch { + return {}; + } + } + + const numberToTimeSlot: Record = { + 0: 'morning', + 1: 'afternoon', + 2: 'evening', + }; + + const result: Record = {}; + Object.keys(schedule || {}).forEach(day => { + const slots = schedule[day]; + if (Array.isArray(slots) && slots.length > 0) { + result[day] = typeof slots[0] === 'number' + ? slots.map((num: number) => numberToTimeSlot[num]).filter(Boolean) as string[] + : slots.filter((s: string) => ['morning', 'afternoon', 'evening'].includes(s)); + } + }); + return result; +} + +export async function createAppointment(input: CreateAppointmentInput): Promise { + const tokens = getStoredTokens(); + if (!tokens.access) { + throw new Error("Authentication required. Please log in to book an appointment."); + } + + if (!input.first_name || !input.last_name || !input.email) { + throw new Error("First name, last name, and email are required"); + } + + if (!input.selected_slots || input.selected_slots.length === 0) { + throw new Error("At least one time slot must be selected"); + } + + const validSlots = validateAndCleanSlots(input.selected_slots); if (validSlots.length === 0) { throw new Error("At least one valid time slot must be selected. Each slot must have both 'day' (0-6) and 'time_slot' (morning, afternoon, or evening)."); } - // Limit field lengths to prevent database errors (100 char limit for all string fields) - // Truncate all string fields BEFORE trimming to handle edge cases - const firstName = input.first_name ? String(input.first_name).trim().substring(0, 100) : ''; - const lastName = input.last_name ? String(input.last_name).trim().substring(0, 100) : ''; - const email = input.email ? String(input.email).trim().toLowerCase().substring(0, 100) : ''; - const phone = input.phone ? String(input.phone).trim().substring(0, 100) : undefined; - const reason = input.reason ? String(input.reason).trim().substring(0, 100) : undefined; - - // Build payload with only the fields the API expects - no extra fields - const payload: { - first_name: string; - last_name: string; - email: string; - selected_slots: Array<{ day: number; time_slot: string }>; - phone?: string; - reason?: string; - } = { - first_name: firstName, - last_name: lastName, - email: email, + const truncate = (str: string, max: number) => String(str || '').trim().substring(0, max); + const payload = { + first_name: truncate(input.first_name, 100), + last_name: truncate(input.last_name, 100), + email: truncate(input.email, 100).toLowerCase(), selected_slots: validSlots.map(slot => ({ - day: Number(slot.day), - time_slot: String(slot.time_slot).toLowerCase().trim(), + day: slot.day, + time_slot: slot.time_slot, })), - }; - - // Only add optional fields if they have values (and are within length limits) - if (phone && phone.length > 0 && phone.length <= 100) { - payload.phone = phone; - } - if (reason && reason.length > 0 && reason.length <= 100) { - payload.reason = reason; - } - - // Final validation: ensure all string fields in payload are exactly 100 chars or less - // This is a safety check to prevent any encoding or serialization issues - const finalPayload = { - first_name: payload.first_name.substring(0, 100), - last_name: payload.last_name.substring(0, 100), - email: payload.email.substring(0, 100), - selected_slots: payload.selected_slots, - ...(payload.phone && { phone: payload.phone.substring(0, 100) }), - ...(payload.reason && { reason: payload.reason.substring(0, 100) }), + ...(input.phone && { phone: truncate(input.phone, 100) }), + ...(input.reason && { reason: truncate(input.reason, 100) }), }; const response = await fetch(API_ENDPOINTS.meetings.createAppointment, { @@ -150,57 +157,17 @@ export async function createAppointment( "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, - body: JSON.stringify(finalPayload), + body: JSON.stringify(payload), }); - // Read response text first (can only be read once) - const responseText = await response.text(); - - // Check content type before parsing - const contentType = response.headers.get("content-type"); - let data: any; - - if (contentType && contentType.includes("application/json")) { - try { - if (!responseText) { - throw new Error(`Server returned empty response (${response.status})`); - } - data = JSON.parse(responseText); - } catch (e) { - throw new Error(`Server error (${response.status}): ${response.statusText || 'Invalid response format'}`); - } - } else { - // Response is not JSON (likely HTML error page) - // Try to extract error message from HTML if possible - let errorMessage = `Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`; - - // Try to find error details in HTML - // Use [\s\S] instead of . with s flag for better compatibility - const errorMatch = responseText.match(/]*>([\s\S]*?)<\/pre>/i) || - responseText.match(/]*>([\s\S]*?)<\/h1>/i) || - responseText.match(/]*>([\s\S]*?)<\/title>/i); - - if (errorMatch && errorMatch[1]) { - const htmlError = errorMatch[1].replace(/<[^>]*>/g, '').trim(); - if (htmlError) { - errorMessage += `. ${htmlError}`; - } - } - - throw new Error(errorMessage); - } + const data = await parseResponse(response); if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } - // Handle API response format: { appointment_id, message } - // According to API docs, response includes appointment_id and message if (data.appointment_id) { - // Construct a minimal Appointment object from the response - // We'll use the input data plus the appointment_id from response - const appointment: Appointment = { + return { id: data.appointment_id, first_name: input.first_name.trim(), last_name: input.last_name.trim(), @@ -212,159 +179,88 @@ export async function createAppointment( created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; - return appointment; } - // Handle different response formats for backward compatibility - if (data.appointment) { - return data.appointment; - } - if ((data as any).data) { - return (data as any).data; - } - - // If appointment is returned directly - return data as unknown as Appointment; + return data.appointment || data.data || data; } -// Get available dates (optional endpoint - may fail if admin hasn't set availability) export async function getAvailableDates(): Promise { try { - const response = await fetch(API_ENDPOINTS.meetings.availableDates, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + const response = await fetch(API_ENDPOINTS.meetings.availableDates, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); - // Handle different response formats - const contentType = response.headers.get("content-type"); - let data: any; - - if (contentType && contentType.includes("application/json")) { - const responseText = await response.text(); - if (!responseText) { - throw new Error(`Server returned empty response (${response.status})`); - } - try { - data = JSON.parse(responseText); - } catch (parseError) { - throw new Error(`Invalid response format (${response.status})`); - } - } else { - throw new Error(`Server error (${response.status}): ${response.statusText || 'Invalid response'}`); + if (!response.ok) { + return { dates: [] }; } - if (!response.ok) { - // Return empty response instead of throwing - this endpoint is optional - return { - dates: [], - }; - } - - // If API returns array directly, wrap it in response object - if (Array.isArray(data)) { - return { - dates: data, - }; - } - - return data as AvailableDatesResponse; - } catch (error) { - // Return empty response - don't break the app - return { - dates: [], - }; + const data = await parseResponse(response); + return Array.isArray(data) ? { dates: data } : data; + } catch { + return { dates: [] }; } } -// Get weekly availability (Public) export async function getWeeklyAvailability(): Promise { const response = await fetch(API_ENDPOINTS.meetings.weeklyAvailability, { method: "GET", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, }); - const data: any = await response.json(); - + const data = await parseResponse(response); if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } - // Handle different response formats - API might return array directly or wrapped - if (Array.isArray(data)) { - return data; - } - - // If wrapped in an object, return as is (our interface supports it) - return data; + return Array.isArray(data) ? data : data; } -// Get availability configuration (Public) export async function getAvailabilityConfig(): Promise { const response = await fetch(API_ENDPOINTS.meetings.availabilityConfig, { method: "GET", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, }); - const data: AvailabilityConfig = await response.json(); - + const data = await parseResponse(response); if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } return data; } -// Check date availability (Public) export async function checkDateAvailability(date: string): Promise { const response = await fetch(API_ENDPOINTS.meetings.checkDateAvailability, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ date }), }); - const data: CheckDateAvailabilityResponse = await response.json(); - + const data = await parseResponse(response); if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } return data; } -// Get availability overview (Public) export async function getAvailabilityOverview(): Promise { const response = await fetch(API_ENDPOINTS.meetings.availabilityOverview, { method: "GET", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, }); - const data: AvailabilityOverview = await response.json(); - + const data = await parseResponse(response); if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } return data; } -// List appointments (Admin sees all, users see their own) export async function listAppointments(email?: string): Promise { const tokens = getStoredTokens(); - if (!tokens.access) { throw new Error("Authentication required."); } @@ -381,58 +277,20 @@ export async function listAppointments(email?: string): Promise { }, }); - const responseText = await response.text(); - + const data = await parseResponse(response); if (!response.ok) { - let errorData: any; - try { - errorData = JSON.parse(responseText); - } catch { - throw new Error(`Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`); - } - const errorMessage = extractErrorMessage(errorData as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } - // Parse JSON response - let data: any; - try { - if (!responseText || responseText.trim().length === 0) { - return []; - } - data = JSON.parse(responseText); - } catch (error) { - throw new Error(`Failed to parse response: Invalid JSON format`); - } - - // Handle different response formats - // API returns array directly: [{ id, first_name, ... }, ...] - if (Array.isArray(data)) { - return data; - } - // Handle wrapped responses (if any) - if (data && typeof data === 'object') { - if (data.appointments && Array.isArray(data.appointments)) { - return data.appointments; - } - if (data.results && Array.isArray(data.results)) { - return data.results; - } - // If data is an object but not an array and doesn't have appointments/results, return empty - // This shouldn't happen but handle gracefully - if (data.id || data.first_name) { - // Single appointment object, wrap in array - return [data]; - } - } - + if (Array.isArray(data)) return data; + if (data?.appointments && Array.isArray(data.appointments)) return data.appointments; + if (data?.results && Array.isArray(data.results)) return data.results; + if (data?.id || data?.first_name) return [data]; return []; } -// Get user appointments export async function getUserAppointments(): Promise { const tokens = getStoredTokens(); - if (!tokens.access) { throw new Error("Authentication required."); } @@ -445,32 +303,19 @@ export async function getUserAppointments(): Promise { }, }); - const data = await response.json(); - + const data = await parseResponse(response); if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } - // Handle different response formats - // API might return array directly or wrapped in an object - if (Array.isArray(data)) { - return data; - } - if (data.appointments && Array.isArray(data.appointments)) { - return data.appointments; - } - if (data.results && Array.isArray(data.results)) { - return data.results; - } - + if (Array.isArray(data)) return data; + if (data?.appointments && Array.isArray(data.appointments)) return data.appointments; + if (data?.results && Array.isArray(data.results)) return data.results; return []; } -// Get appointment detail export async function getAppointmentDetail(id: string): Promise { const tokens = getStoredTokens(); - if (!tokens.access) { throw new Error("Authentication required."); } @@ -483,27 +328,16 @@ export async function getAppointmentDetail(id: string): Promise { }, }); - const data: AppointmentResponse = await response.json(); - + const data = await parseResponse(response); if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } - if (data.appointment) { - return data.appointment; - } - - return data as unknown as Appointment; + return (data as AppointmentResponse).appointment || data; } -// Schedule appointment (Admin only) -export async function scheduleAppointment( - id: string, - input: ScheduleAppointmentInput -): Promise { +export async function scheduleAppointment(id: string, input: ScheduleAppointmentInput): Promise { const tokens = getStoredTokens(); - if (!tokens.access) { throw new Error("Authentication required."); } @@ -517,77 +351,16 @@ export async function scheduleAppointment( body: JSON.stringify(input), }); - let data: any; - const contentType = response.headers.get("content-type"); - - if (contentType && contentType.includes("application/json")) { - try { - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - } catch (e) { - data = {}; - } - } else { - const text = await response.text(); - data = text || {}; - } - + const data = await parseResponse(response); if (!response.ok) { - // Try to extract detailed error information - let errorMessage = `Failed to schedule appointment (${response.status})`; - - if (data && Object.keys(data).length > 0) { - // Check for common error formats - if (data.detail) { - errorMessage = Array.isArray(data.detail) ? data.detail.join(", ") : String(data.detail); - } else if (data.message) { - errorMessage = Array.isArray(data.message) ? data.message.join(", ") : String(data.message); - } else if (data.error) { - errorMessage = Array.isArray(data.error) ? data.error.join(", ") : String(data.error); - } else if (typeof data === "string") { - errorMessage = data; - } else { - // Check for field-specific errors - const fieldErrors: string[] = []; - Object.keys(data).forEach((key) => { - if (key !== "detail" && key !== "message" && key !== "error") { - const fieldError = data[key]; - if (Array.isArray(fieldError)) { - fieldErrors.push(`${key}: ${fieldError.join(", ")}`); - } else if (typeof fieldError === "string") { - fieldErrors.push(`${key}: ${fieldError}`); - } - } - }); - if (fieldErrors.length > 0) { - errorMessage = fieldErrors.join(". "); - } else { - // If we have data but can't parse it, show the status - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - } - } else { - // No data in response - errorMessage = `Server error: ${response.status} ${response.statusText || 'Unknown error'}`; - } - - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } - if (data.appointment) { - return data.appointment; - } - - return data as unknown as Appointment; + return data.appointment || data; } -// Reject appointment (Admin only) -export async function rejectAppointment( - id: string, - input: RejectAppointmentInput -): Promise { +export async function rejectAppointment(id: string, input: RejectAppointmentInput): Promise { const tokens = getStoredTokens(); - if (!tokens.access) { throw new Error("Authentication required."); } @@ -601,27 +374,17 @@ export async function rejectAppointment( body: JSON.stringify(input), }); - const data: AppointmentResponse = await response.json(); - + const data = await parseResponse(response); if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } - if (data.appointment) { - return data.appointment; - } - - return data as unknown as Appointment; + return (data as AppointmentResponse).appointment || data; } -// Get admin availability (public version - uses weekly availability endpoint instead) export async function getPublicAvailability(): Promise { try { - // Use weekly availability endpoint which is public const weeklyAvailability = await getWeeklyAvailability(); - - // Normalize to array format const weekArray = Array.isArray(weeklyAvailability) ? weeklyAvailability : (weeklyAvailability as any).week || []; @@ -630,13 +393,12 @@ export async function getPublicAvailability(): Promise return null; } - // Convert weekly availability to AdminAvailability format const availabilitySchedule: Record = {}; const availableDays: number[] = []; const availableDaysDisplay: string[] = []; weekArray.forEach((day: any) => { - if (day.is_available && day.available_slots && day.available_slots.length > 0) { + if (day.is_available && day.available_slots?.length > 0) { availabilitySchedule[day.day.toString()] = day.available_slots; availableDays.push(day.day); availableDaysDisplay.push(day.day_name); @@ -649,17 +411,20 @@ export async function getPublicAvailability(): Promise availability_schedule: availabilitySchedule, all_available_slots: weekArray .filter((d: any) => d.is_available) - .flatMap((d: any) => d.available_slots.map((slot: string) => ({ day: d.day, time_slot: slot as "morning" | "afternoon" | "evening" }))), + .flatMap((d: any) => + d.available_slots.map((slot: string) => ({ + day: d.day, + time_slot: slot as "morning" | "afternoon" | "evening" + })) + ), } as AdminAvailability; - } catch (error) { + } catch { return null; } } -// Get admin availability export async function getAdminAvailability(): Promise { const tokens = getStoredTokens(); - if (!tokens.access) { throw new Error("Authentication required."); } @@ -672,80 +437,35 @@ export async function getAdminAvailability(): Promise { }, }); - const data: any = await response.json(); - + const data = await parseResponse(response); if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); - throw new Error(errorMessage); + throw new Error(extractErrorMessage(data as unknown as ApiError)); } - // Handle new format with availability_schedule - // API returns availability_schedule, which may be a JSON string or object - // Time slots are strings: "morning", "afternoon", "evening" if (data.availability_schedule) { - let availabilitySchedule: Record; - - // Map numeric indices to string names (in case API returns numeric indices) - const numberToTimeSlot: Record = { - 0: 'morning', - 1: 'afternoon', - 2: 'evening', - }; - - // Parse if it's a string, otherwise use as-is - let rawSchedule: Record; - if (typeof data.availability_schedule === 'string') { - try { - rawSchedule = JSON.parse(data.availability_schedule); - } catch (parseError) { - rawSchedule = {}; - } - } else { - rawSchedule = data.availability_schedule; - } - - // Convert to string format, handling both numeric indices and string values - availabilitySchedule = {}; - Object.keys(rawSchedule).forEach(day => { - const slots = rawSchedule[day]; - if (Array.isArray(slots) && slots.length > 0) { - // Check if slots are numbers (indices) or already strings - if (typeof slots[0] === 'number') { - // Convert numeric indices to string names - availabilitySchedule[day] = (slots as number[]) - .map((num: number) => numberToTimeSlot[num]) - .filter((slot: string | undefined) => slot !== undefined) as string[]; - } else { - // Already strings, validate and use as-is - availabilitySchedule[day] = (slots as string[]).filter(slot => - ['morning', 'afternoon', 'evening'].includes(slot) - ); - } - } - }); - + const availabilitySchedule = normalizeAvailabilitySchedule(data.availability_schedule); const availableDays = Object.keys(availabilitySchedule).map(Number); - - // Generate available_days_display if not provided const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; const availableDaysDisplay = availableDays.map(day => dayNames[day] || `Day ${day}`); return { available_days: availableDays, - available_days_display: data.availability_schedule_display ? [data.availability_schedule_display] : availableDaysDisplay, + available_days_display: Array.isArray(data.availability_schedule_display) + ? data.availability_schedule_display + : data.availability_schedule_display + ? [data.availability_schedule_display] + : availableDaysDisplay, availability_schedule: availabilitySchedule, availability_schedule_display: data.availability_schedule_display, all_available_slots: data.all_available_slots || [], } as AdminAvailability; } - // Handle legacy format let availableDays: number[] = []; if (typeof data.available_days === 'string') { try { availableDays = JSON.parse(data.available_days); } catch { - // If parsing fails, try splitting by comma availableDays = data.available_days.split(',').map((d: string) => parseInt(d.trim())).filter((d: number) => !isNaN(d)); } } else if (Array.isArray(data.available_days)) { @@ -758,88 +478,53 @@ export async function getAdminAvailability(): Promise { } as AdminAvailability; } -// Update admin availability -export async function updateAdminAvailability( - input: UpdateAvailabilityInput -): Promise { +export async function updateAdminAvailability(input: UpdateAvailabilityInput): Promise { const tokens = getStoredTokens(); - if (!tokens.access) { throw new Error("Authentication required."); } - // Prepare payload using new format (availability_schedule) - // API expects availability_schedule as an object with string keys (day numbers) and string arrays (time slot names) - // Format: { "0": ["morning", "afternoon"], "1": ["evening"], ... } - const payload: any = {}; - - if (input.availability_schedule) { - // Validate and clean the schedule object - // API expects: { "0": ["morning", "evening"], "1": ["afternoon"], ... } - // Time slots are strings: "morning", "afternoon", "evening" - const cleanedSchedule: Record = {}; - Object.keys(input.availability_schedule).forEach(key => { - // Ensure key is a valid day (0-6) - const dayNum = parseInt(key); - if (isNaN(dayNum) || dayNum < 0 || dayNum > 6) { - return; - } - - const slots = input.availability_schedule[key]; - if (Array.isArray(slots) && slots.length > 0) { - // Filter to only valid time slot strings and remove duplicates - const validSlots = slots - .filter((slot: string) => - typeof slot === 'string' && ['morning', 'afternoon', 'evening'].includes(slot) - ) - .filter((slot: string, index: number, self: string[]) => - self.indexOf(slot) === index - ); // Remove duplicates - - if (validSlots.length > 0) { - // Ensure day key is a string (as per API spec) - cleanedSchedule[key.toString()] = validSlots; - } - } - }); - - if (Object.keys(cleanedSchedule).length === 0) { - throw new Error("At least one day with valid time slots must be provided"); - } - - // Sort the schedule keys for consistency - const sortedSchedule: Record = {}; - Object.keys(cleanedSchedule) - .sort((a, b) => parseInt(a) - parseInt(b)) - .forEach(key => { - sortedSchedule[key] = cleanedSchedule[key]; - }); - - // IMPORTANT: API expects availability_schedule as an object (not stringified) - // Format: { "0": ["morning", "afternoon"], "1": ["evening"], ... } - payload.availability_schedule = sortedSchedule; - } else if (input.available_days) { - // Legacy format: available_days - payload.available_days = Array.isArray(input.available_days) - ? input.available_days.map(day => Number(day)) - : input.available_days; - } else { - throw new Error("Either availability_schedule or available_days must be provided"); + if (!input.availability_schedule) { + throw new Error("availability_schedule is required"); } + const cleanedSchedule: Record = {}; + Object.keys(input.availability_schedule).forEach(key => { + const dayNum = parseInt(key); + if (isNaN(dayNum) || dayNum < 0 || dayNum > 6) return; + + const slots = input.availability_schedule[key]; + if (Array.isArray(slots) && slots.length > 0) { + const validSlots = slots + .filter((slot: string) => typeof slot === 'string' && ['morning', 'afternoon', 'evening'].includes(slot)) + .filter((slot: string, index: number, self: string[]) => self.indexOf(slot) === index); + + if (validSlots.length > 0) { + cleanedSchedule[key.toString()] = validSlots; + } + } + }); + + if (Object.keys(cleanedSchedule).length === 0) { + throw new Error("At least one day with valid time slots must be provided"); + } + + const sortedSchedule: Record = {}; + Object.keys(cleanedSchedule) + .sort((a, b) => parseInt(a) - parseInt(b)) + .forEach(key => { + sortedSchedule[key] = cleanedSchedule[key]; + }); - // Try PUT first, fallback to PATCH if needed - // The payload object will be JSON stringified, including availability_schedule as an object let response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, - body: JSON.stringify(payload), + body: JSON.stringify({ availability_schedule: sortedSchedule }), }); - // If PUT fails with 500, try PATCH (some APIs prefer PATCH for updates) if (!response.ok && response.status === 500) { response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "PATCH", @@ -847,249 +532,64 @@ export async function updateAdminAvailability( "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, - body: JSON.stringify(payload), + body: JSON.stringify({ availability_schedule: sortedSchedule }), }); } - // Read response text first (can only be read once) const responseText = await response.text(); - let data: any; - - // Get content type const contentType = response.headers.get("content-type") || ""; - - // Handle empty response + if (!responseText || responseText.trim().length === 0) { - // If successful status but empty response, refetch the availability if (response.ok) { return await getAdminAvailability(); } - - throw new Error(`Server error (${response.status}): ${response.statusText || 'Empty response from server'}`); + throw new Error(`Server error (${response.status}): ${response.statusText || 'Empty response'}`); } - - // Try to parse as JSON + + let data: any; if (contentType.includes("application/json")) { - try { - data = JSON.parse(responseText); - } catch (parseError) { - throw new Error(`Server error (${response.status}): Invalid JSON response format`); - } + try { + data = JSON.parse(responseText); + } catch { + throw new Error(`Server error (${response.status}): Invalid JSON format`); + } } else { - // Response is not JSON - try to extract useful information - // Try to extract error message from HTML if it's an HTML error page - let errorMessage = `Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`; - let actualError = ''; - let errorType = ''; - let fullTraceback = ''; - - if (responseText) { - // Extract Django error details from HTML - const titleMatch = responseText.match(/]*>(.*?)<\/title>/i); - const h1Match = responseText.match(/]*>(.*?)<\/h1>/i); - - // Try to find the actual error traceback in
 tags (Django debug pages)
-      const tracebackMatch = responseText.match(/]*class="[^"]*traceback[^"]*"[^>]*>([\s\S]*?)<\/pre>/i) ||
-                            responseText.match(/]*>([\s\S]*?)<\/pre>/i);
-      
-      // Extract the actual error type and message
-      const errorTypeMatch = responseText.match(/]*>(.*?)<\/h2>/i);
-      
-      if (tracebackMatch && tracebackMatch[1]) {
-        // Extract the full traceback and find the actual error
-        const tracebackText = tracebackMatch[1].replace(/<[^>]*>/g, ''); // Remove HTML tags
-        fullTraceback = tracebackText;
-        const tracebackLines = tracebackText.split('\n').filter(line => line.trim());
-        
-        // Look for the actual error message - Django errors usually appear at the end
-        // First, try to find error patterns in the entire traceback
-        const errorPatterns = [
-          // Database column errors
-          /column\s+[\w.]+\.(\w+)\s+(does not exist|already exists|is missing)/i,
-          // Programming errors
-          /(ProgrammingError|OperationalError|IntegrityError|DatabaseError|ValueError|TypeError|AttributeError|KeyError):\s*(.+?)(?:\n|$)/i,
-          // Generic error patterns
-          /^(\w+Error):\s*(.+)$/i,
-          // Error messages without type
-          /^(.+Error[:\s]+.+)$/i,
-        ];
-        
-        // Search from the end backwards (errors are usually at the end)
-        for (let i = tracebackLines.length - 1; i >= 0; i--) {
-          const line = tracebackLines[i];
-          
-          // Check each pattern
-          for (const pattern of errorPatterns) {
-            const match = line.match(pattern);
-            if (match) {
-              // For database column errors, capture the full message
-              if (pattern.source.includes('column')) {
-                actualError = match[0];
-                errorType = 'DatabaseError';
-              } else if (match[1] && match[2]) {
-                errorType = match[1];
-                actualError = match[2].trim();
-              } else {
-                actualError = match[0];
-              }
-              break;
-            }
-          }
-          if (actualError) break;
-        }
-        
-        // If no pattern match, look for lines containing "Error" or common error keywords
-        if (!actualError) {
-          for (let i = tracebackLines.length - 1; i >= Math.max(0, tracebackLines.length - 10); i--) {
-            const line = tracebackLines[i];
-            if (line.match(/(Error|Exception|Failed|Invalid|Missing|does not exist|already exists)/i)) {
-              actualError = line;
-              break;
-            }
-          }
-        }
-        
-        // Last resort: get the last line
-        if (!actualError && tracebackLines.length > 0) {
-          actualError = tracebackLines[tracebackLines.length - 1];
-        }
-        
-        // Clean up the error message
-        if (actualError) {
-          actualError = actualError.trim();
-          // Remove common prefixes
-          actualError = actualError.replace(/^(Traceback|File|Error|Exception):\s*/i, '');
-        }
-      } else if (errorTypeMatch && errorTypeMatch[1]) {
-        errorType = errorTypeMatch[1].replace(/<[^>]*>/g, '').trim();
-        actualError = errorType;
-        if (errorType && errorType.length < 200) {
-          errorMessage += `. ${errorType}`;
-        }
-      } else if (h1Match && h1Match[1]) {
-        actualError = h1Match[1].replace(/<[^>]*>/g, '').trim();
-        if (actualError && actualError.length < 200) {
-          errorMessage += `. ${actualError}`;
-        }
-      } else if (titleMatch && titleMatch[1]) {
-        actualError = titleMatch[1].replace(/<[^>]*>/g, '').trim();
-        if (actualError && actualError.length < 200) {
-          errorMessage += `. ${actualError}`;
-        }
-      }
-    }
-    
-    // Update error message with the extracted error
-    if (actualError) {
-      errorMessage = `Server error (${response.status}): ${actualError}`;
-    }
-    
-    throw new Error(errorMessage);
+    const htmlError = extractHtmlError(responseText);
+    throw new Error(`Server error (${response.status}): ${htmlError || response.statusText || 'Internal Server Error'}`);
   }
 
   if (!response.ok) {
-    const errorMessage = extractErrorMessage(data as unknown as ApiError);
-    
-    // Build detailed error message
-    let detailedError = `Server error (${response.status}): `;
-    if (data && typeof data === 'object') {
-      if (data.detail) {
-        detailedError += Array.isArray(data.detail) ? data.detail.join(", ") : String(data.detail);
-      } else if (data.error) {
-        detailedError += Array.isArray(data.error) ? data.error.join(", ") : String(data.error);
-      } else if (data.message) {
-        detailedError += Array.isArray(data.message) ? data.message.join(", ") : String(data.message);
-      } else {
-        detailedError += response.statusText || 'Failed to update availability';
-      }
-    } else if (responseText && responseText.length > 0) {
-      // Try to extract error from HTML response if it's not JSON
-      detailedError += responseText.substring(0, 200);
-    } else {
-      detailedError += response.statusText || 'Failed to update availability';
-    }
-    
-    throw new Error(detailedError);
+    throw new Error(extractErrorMessage(data as unknown as ApiError));
   }
 
-  // Handle new format with availability_schedule in response
-  // API returns availability_schedule, which may be a JSON string or object
-  // Time slots may be strings or numeric indices
-  if (data && data.availability_schedule) {
-    let availabilitySchedule: Record;
-    
-    // Map numeric indices to string names (in case API returns numeric indices)
-    const numberToTimeSlot: Record = {
-      0: 'morning',
-      1: 'afternoon',
-      2: 'evening',
-    };
-    
-    // Parse if it's a string, otherwise use as-is
-    let rawSchedule: Record;
-    if (typeof data.availability_schedule === 'string') {
-      try {
-        rawSchedule = JSON.parse(data.availability_schedule);
-      } catch (parseError) {
-        rawSchedule = {};
-      }
-    } else if (typeof data.availability_schedule === 'object') {
-      rawSchedule = data.availability_schedule;
-    } else {
-      rawSchedule = {};
-    }
-    
-    // Convert to string format, handling both numeric indices and string values
-    availabilitySchedule = {};
-    Object.keys(rawSchedule).forEach(day => {
-      const slots = rawSchedule[day];
-      if (Array.isArray(slots) && slots.length > 0) {
-        // Check if slots are numbers (indices) or already strings
-        if (typeof slots[0] === 'number') {
-          // Convert numeric indices to string names
-          availabilitySchedule[day] = (slots as number[])
-            .map((num: number) => numberToTimeSlot[num])
-            .filter((slot: string | undefined) => slot !== undefined) as string[];
-        } else {
-          // Already strings, validate and use as-is
-          availabilitySchedule[day] = (slots as string[]).filter(slot => 
-            ['morning', 'afternoon', 'evening'].includes(slot)
-          );
-        }
-      }
-    });
-    
+  if (data?.availability_schedule) {
+    const availabilitySchedule = normalizeAvailabilitySchedule(data.availability_schedule);
     const availableDays = Object.keys(availabilitySchedule).map(Number);
     const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
     const availableDaysDisplay = availableDays.map(day => dayNames[day] || `Day ${day}`);
 
     return {
       available_days: availableDays,
-      available_days_display: data.availability_schedule_display ? 
-        (Array.isArray(data.availability_schedule_display) ? 
-          data.availability_schedule_display : 
-          [data.availability_schedule_display]) : 
-        availableDaysDisplay,
+      available_days_display: Array.isArray(data.availability_schedule_display)
+        ? data.availability_schedule_display
+        : data.availability_schedule_display
+        ? [data.availability_schedule_display]
+        : availableDaysDisplay,
       availability_schedule: availabilitySchedule,
       availability_schedule_display: data.availability_schedule_display,
       all_available_slots: data.all_available_slots || [],
     } as AdminAvailability;
   }
-  
-  // If response is empty but successful (200), return empty availability
-  // This might happen if the server doesn't return data on success
+
   if (response.ok && (!data || Object.keys(data).length === 0)) {
-    // Refetch the availability to get the updated data
-    return getAdminAvailability();
+    return await getAdminAvailability();
   }
 
-  // Handle legacy format
   let availableDays: number[] = [];
   if (typeof data.available_days === 'string') {
     try {
       availableDays = JSON.parse(data.available_days);
     } catch {
-      // If parsing fails, try splitting by comma
       availableDays = data.available_days.split(',').map((d: string) => parseInt(d.trim())).filter((d: number) => !isNaN(d));
     }
   } else if (Array.isArray(data.available_days)) {
@@ -1102,10 +602,8 @@ export async function updateAdminAvailability(
   } as AdminAvailability;
 }
 
-// Get appointment stats (Admin only)
 export async function getAppointmentStats(): Promise {
   const tokens = getStoredTokens();
-  
   if (!tokens.access) {
     throw new Error("Authentication required.");
   }
@@ -1118,67 +616,38 @@ export async function getAppointmentStats(): Promise {
     },
   });
 
-  const data: AppointmentStats = await response.json();
-
+  const data = await parseResponse(response);
   if (!response.ok) {
-    const errorMessage = extractErrorMessage(data as unknown as ApiError);
-    throw new Error(errorMessage);
+    throw new Error(extractErrorMessage(data as unknown as ApiError));
   }
 
   return data;
 }
 
-// Get user appointment stats
-export async function getUserAppointmentStats(email: string): Promise {
+export async function getUserAppointmentStats(): Promise {
   const tokens = getStoredTokens();
-  
   if (!tokens.access) {
     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, {
-    method: "POST",
+    method: "GET",
     headers: {
       "Content-Type": "application/json",
       Authorization: `Bearer ${tokens.access}`,
     },
-    body: JSON.stringify({ email }),
   });
 
-  const responseText = await response.text();
-
+  const data = await parseResponse(response);
   if (!response.ok) {
-    let errorData: any;
-    try {
-      errorData = JSON.parse(responseText);
-    } catch {
-      throw new Error(`Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`);
-    }
-    const errorMessage = extractErrorMessage(errorData as unknown as ApiError);
-    throw new Error(errorMessage);
-  }
-
-  let data: UserAppointmentStats;
-  try {
-    if (!responseText || responseText.trim().length === 0) {
-      throw new Error("Empty response from server");
-    }
-    data = JSON.parse(responseText);
-  } catch (error) {
-    throw new Error(`Failed to parse response: Invalid JSON format`);
+    throw new Error(extractErrorMessage(data as unknown as ApiError));
   }
 
   return data;
 }
 
-// Get Jitsi meeting info
 export async function getJitsiMeetingInfo(id: string): Promise {
   const tokens = getStoredTokens();
-  
   if (!tokens.access) {
     throw new Error("Authentication required.");
   }
@@ -1191,13 +660,10 @@ export async function getJitsiMeetingInfo(id: string): Promise
     },
   });
 
-  const data: JitsiMeetingInfo = await response.json();
-
+  const data = await parseResponse(response);
   if (!response.ok) {
-    const errorMessage = extractErrorMessage(data as unknown as ApiError);
-    throw new Error(errorMessage);
+    throw new Error(extractErrorMessage(data as unknown as ApiError));
   }
 
   return data;
 }
-
@@ -404,9 +400,9 @@ export default function UserDashboard() { {appointment.reason}
)} - {appointment.scheduled_datetime && ( + {appointment.scheduled_datetime && (
- {formatDate(appointment.scheduled_datetime)} + {formatDate(appointment.scheduled_datetime)}
)} @@ -420,9 +416,9 @@ export default function UserDashboard() {
- {formatTime(appointment.scheduled_datetime)} -
- + {formatTime(appointment.scheduled_datetime)} + + ) : (
Not scheduled @@ -451,8 +447,8 @@ export default function UserDashboard() { ))} {appointment.selected_slots.length > 2 && ( +{appointment.selected_slots.length - 2} more - )} -
+ )} + ) : ( "-" )} @@ -463,10 +459,10 @@ export default function UserDashboard() {
+ > +