import { API_ENDPOINTS } from "@/lib/api_urls"; import { getStoredTokens } from "./auth"; import type { CreateAppointmentInput, ScheduleAppointmentInput, RejectAppointmentInput, UpdateAvailabilityInput, } from "@/lib/schema/appointments"; import type { Appointment, AppointmentResponse, AppointmentsListResponse, AvailableDatesResponse, AdminAvailability, AppointmentStats, UserAppointmentStats, JitsiMeetingInfo, ApiError, } 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); } if (error.message) { if (Array.isArray(error.message)) { return error.message.join(", "); } return String(error.message); } if (typeof error === "string") { return error; } return "An error occurred while creating the appointment"; } // 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."); } // Validate required fields if (!input.first_name || !input.last_name || !input.email) { throw new Error("First name, last name, and email are required"); } if (!input.preferred_dates || input.preferred_dates.length === 0) { throw new Error("At least one preferred date is required"); } if (!input.preferred_time_slots || input.preferred_time_slots.length === 0) { throw new Error("At least one preferred time slot is required"); } // Validate date format (YYYY-MM-DD) const dateRegex = /^\d{4}-\d{2}-\d{2}$/; for (const date of input.preferred_dates) { if (!dateRegex.test(date)) { throw new Error(`Invalid date format: ${date}. Expected YYYY-MM-DD format.`); } } // Validate time slots const validTimeSlots = ["morning", "afternoon", "evening"]; for (const slot of input.preferred_time_slots) { if (!validTimeSlots.includes(slot)) { throw new Error(`Invalid time slot: ${slot}. Must be one of: ${validTimeSlots.join(", ")}`); } } // Prepare the payload exactly as the API expects // Only include fields that the API accepts - no jitsi_room_id or other fields const payload: { first_name: string; last_name: string; email: string; preferred_dates: string[]; preferred_time_slots: string[]; phone?: string; reason?: string; } = { first_name: input.first_name.trim(), last_name: input.last_name.trim(), email: input.email.trim().toLowerCase(), preferred_dates: input.preferred_dates, preferred_time_slots: input.preferred_time_slots, }; // Only add optional fields if they have values if (input.phone && input.phone.trim()) { payload.phone = input.phone.trim(); } if (input.reason && input.reason.trim()) { payload.reason = input.reason.trim(); } // Log the payload for debugging console.log("Creating appointment with payload:", JSON.stringify(payload, null, 2)); console.log("API endpoint:", API_ENDPOINTS.meetings.createAppointment); const response = await fetch(API_ENDPOINTS.meetings.createAppointment, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, 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) { // If JSON parsing fails, log the actual response console.error("Failed to parse JSON response:", { status: response.status, statusText: response.statusText, contentType, url: API_ENDPOINTS.meetings.createAppointment, preview: responseText.substring(0, 500) }); 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 const errorMatch = responseText.match(/]*>(.*?)<\/pre>/is) || responseText.match(/]*>(.*?)<\/h1>/is) || responseText.match(/]*>(.*?)<\/title>/is); if (errorMatch && errorMatch[1]) { const htmlError = errorMatch[1].replace(/<[^>]*>/g, '').trim(); if (htmlError) { errorMessage += `. ${htmlError}`; } } console.error("Non-JSON response received:", { status: response.status, statusText: response.statusText, contentType, url: API_ENDPOINTS.meetings.createAppointment, payload: input, preview: responseText.substring(0, 1000) }); throw new Error(errorMessage); } if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } // Handle different response formats 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; } // Get available dates export async function getAvailableDates(): Promise { const response = await fetch(API_ENDPOINTS.meetings.availableDates, { method: "GET", headers: { "Content-Type": "application/json", }, }); const data: AvailableDatesResponse | string[] = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } // If API returns array directly, wrap it in response object if (Array.isArray(data)) { return { dates: data, }; } return data as AvailableDatesResponse; } // 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."); } const url = email ? `${API_ENDPOINTS.meetings.listAppointments}?email=${encodeURIComponent(email)}` : API_ENDPOINTS.meetings.listAppointments; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } // 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; } return []; } // Get user appointments export async function getUserAppointments(): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(API_ENDPOINTS.meetings.userAppointments, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } // 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; } return []; } // Get appointment detail export async function getAppointmentDetail(id: string): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data: AppointmentResponse = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } if (data.appointment) { return data.appointment; } return data as unknown as Appointment; } // Schedule appointment (Admin only) export async function scheduleAppointment( id: string, input: ScheduleAppointmentInput ): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/schedule/`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, 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 || {}; } 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'}`; } console.error("Schedule appointment error:", { status: response.status, statusText: response.statusText, data, errorMessage, }); throw new Error(errorMessage); } if (data.appointment) { return data.appointment; } return data as unknown as Appointment; } // Reject appointment (Admin only) export async function rejectAppointment( id: string, input: RejectAppointmentInput ): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/reject/`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, body: JSON.stringify(input), }); const data: AppointmentResponse = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } if (data.appointment) { return data.appointment; } return data as unknown as Appointment; } // Get admin availability (public version - tries without auth first) export async function getPublicAvailability(): Promise { try { const response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "GET", headers: { "Content-Type": "application/json", }, }); if (!response.ok) { return null; } const data: any = await response.json(); // Handle both string and array formats for available_days let availableDays: number[] = []; if (typeof data.available_days === 'string') { try { availableDays = JSON.parse(data.available_days); } catch { availableDays = data.available_days.split(',').map((d: string) => parseInt(d.trim())).filter((d: number) => !isNaN(d)); } } else if (Array.isArray(data.available_days)) { availableDays = data.available_days; } return { available_days: availableDays, available_days_display: data.available_days_display || [], } as AdminAvailability; } catch (error) { return null; } } // Get admin availability export async function getAdminAvailability(): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data: any = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } // Handle both string and array formats for available_days 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)) { availableDays = data.available_days; } return { available_days: availableDays, available_days_display: data.available_days_display || [], } as AdminAvailability; } // Update admin availability export async function updateAdminAvailability( input: UpdateAvailabilityInput ): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } // Ensure available_days is an array of numbers const payload = { available_days: Array.isArray(input.available_days) ? input.available_days.map(day => Number(day)) : input.available_days }; const response = await fetch(API_ENDPOINTS.meetings.adminAvailability, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, body: JSON.stringify(payload), }); let data: any; try { data = await response.json(); } catch (parseError) { // If response is not JSON, use status text throw new Error(response.statusText || "Failed to update availability"); } if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); console.error("Availability update error:", { status: response.status, statusText: response.statusText, data, payload }); throw new Error(errorMessage); } // Handle both string and array formats for available_days in response 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)) { availableDays = data.available_days; } return { available_days: availableDays, available_days_display: data.available_days_display || [], } as AdminAvailability; } // Get appointment stats (Admin only) export async function getAppointmentStats(): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}stats/`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data: AppointmentStats = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } return data; } // Get user appointment stats export async function getUserAppointmentStats(): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(API_ENDPOINTS.meetings.userAppointmentStats, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data: UserAppointmentStats = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } return data; } // Get Jitsi meeting info export async function getJitsiMeetingInfo(id: string): Promise { const tokens = getStoredTokens(); if (!tokens.access) { throw new Error("Authentication required."); } const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/jitsi-meeting/`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, }); const data: JitsiMeetingInfo = await response.json(); if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); throw new Error(errorMessage); } return data; }