2025-11-23 21:43:13 +00:00
|
|
|
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,
|
2025-11-25 21:25:53 +00:00
|
|
|
UserAppointmentStats,
|
2025-11-23 21:43:13 +00:00
|
|
|
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<Appointment> {
|
|
|
|
|
const tokens = getStoredTokens();
|
|
|
|
|
|
|
|
|
|
if (!tokens.access) {
|
|
|
|
|
throw new Error("Authentication required. Please log in to book an appointment.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 11:42:31 +00:00
|
|
|
// 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);
|
|
|
|
|
|
2025-11-23 21:43:13 +00:00
|
|
|
const response = await fetch(API_ENDPOINTS.meetings.createAppointment, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
Authorization: `Bearer ${tokens.access}`,
|
|
|
|
|
},
|
2025-11-26 11:42:31 +00:00
|
|
|
body: JSON.stringify(payload),
|
2025-11-23 21:43:13 +00:00
|
|
|
});
|
|
|
|
|
|
2025-11-26 11:42:31 +00:00
|
|
|
// 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[^>]*>(.*?)<\/pre>/is) ||
|
|
|
|
|
responseText.match(/<h1[^>]*>(.*?)<\/h1>/is) ||
|
|
|
|
|
responseText.match(/<title[^>]*>(.*?)<\/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);
|
|
|
|
|
}
|
2025-11-23 21:43:13 +00:00
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-23 21:43:13 +00:00
|
|
|
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
|
2025-11-25 21:04:22 +00:00
|
|
|
export async function getAvailableDates(): Promise<AvailableDatesResponse> {
|
2025-11-23 21:43:13 +00:00
|
|
|
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) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-23 21:43:13 +00:00
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 21:04:22 +00:00
|
|
|
// If API returns array directly, wrap it in response object
|
2025-11-23 21:43:13 +00:00
|
|
|
if (Array.isArray(data)) {
|
2025-11-25 21:04:22 +00:00
|
|
|
return {
|
|
|
|
|
dates: data,
|
|
|
|
|
};
|
2025-11-23 21:43:13 +00:00
|
|
|
}
|
2025-11-25 21:04:22 +00:00
|
|
|
|
|
|
|
|
return data as AvailableDatesResponse;
|
2025-11-23 21:43:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// List appointments (Admin sees all, users see their own)
|
|
|
|
|
export async function listAppointments(email?: string): Promise<Appointment[]> {
|
|
|
|
|
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}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
const data = await response.json();
|
2025-11-23 21:43:13 +00:00
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-23 21:43:13 +00:00
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
// 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 [];
|
2025-11-23 21:43:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get user appointments
|
|
|
|
|
export async function getUserAppointments(): Promise<Appointment[]> {
|
|
|
|
|
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}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
const data = await response.json();
|
2025-11-23 21:43:13 +00:00
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-23 21:43:13 +00:00
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
// 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 [];
|
2025-11-23 21:43:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get appointment detail
|
|
|
|
|
export async function getAppointmentDetail(id: string): Promise<Appointment> {
|
|
|
|
|
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) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-23 21:43:13 +00:00
|
|
|
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<Appointment> {
|
|
|
|
|
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),
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-26 11:42:31 +00:00
|
|
|
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 || {};
|
|
|
|
|
}
|
2025-11-23 21:43:13 +00:00
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2025-11-26 11:42:31 +00:00
|
|
|
// 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,
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-23 21:43:13 +00:00
|
|
|
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<Appointment> {
|
|
|
|
|
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) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-23 21:43:13 +00:00
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.appointment) {
|
|
|
|
|
return data.appointment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data as unknown as Appointment;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 21:04:22 +00:00
|
|
|
// Get admin availability (public version - tries without auth first)
|
|
|
|
|
export async function getPublicAvailability(): Promise<AdminAvailability | null> {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 21:43:13 +00:00
|
|
|
// Get admin availability
|
|
|
|
|
export async function getAdminAvailability(): Promise<AdminAvailability> {
|
|
|
|
|
const tokens = getStoredTokens();
|
|
|
|
|
|
|
|
|
|
if (!tokens.access) {
|
|
|
|
|
throw new Error("Authentication required.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 20:15:37 +00:00
|
|
|
const response = await fetch(API_ENDPOINTS.meetings.adminAvailability, {
|
2025-11-23 21:43:13 +00:00
|
|
|
method: "GET",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
Authorization: `Bearer ${tokens.access}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-25 20:15:37 +00:00
|
|
|
const data: any = await response.json();
|
2025-11-23 21:43:13 +00:00
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-23 21:43:13 +00:00
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 20:15:37 +00:00
|
|
|
// 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;
|
2025-11-23 21:43:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update admin availability
|
|
|
|
|
export async function updateAdminAvailability(
|
|
|
|
|
input: UpdateAvailabilityInput
|
|
|
|
|
): Promise<AdminAvailability> {
|
|
|
|
|
const tokens = getStoredTokens();
|
|
|
|
|
|
|
|
|
|
if (!tokens.access) {
|
|
|
|
|
throw new Error("Authentication required.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 20:38:37 +00:00
|
|
|
// 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
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-25 20:15:37 +00:00
|
|
|
const response = await fetch(API_ENDPOINTS.meetings.adminAvailability, {
|
2025-11-23 21:43:13 +00:00
|
|
|
method: "PUT",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
Authorization: `Bearer ${tokens.access}`,
|
|
|
|
|
},
|
2025-11-25 20:38:37 +00:00
|
|
|
body: JSON.stringify(payload),
|
2025-11-23 21:43:13 +00:00
|
|
|
});
|
|
|
|
|
|
2025-11-25 20:38:37 +00:00
|
|
|
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");
|
|
|
|
|
}
|
2025-11-23 21:43:13 +00:00
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-25 20:38:37 +00:00
|
|
|
console.error("Availability update error:", {
|
|
|
|
|
status: response.status,
|
|
|
|
|
statusText: response.statusText,
|
|
|
|
|
data,
|
|
|
|
|
payload
|
|
|
|
|
});
|
2025-11-23 21:43:13 +00:00
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 20:38:37 +00:00
|
|
|
// 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;
|
2025-11-23 21:43:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get appointment stats (Admin only)
|
|
|
|
|
export async function getAppointmentStats(): Promise<AppointmentStats> {
|
|
|
|
|
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) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-23 21:43:13 +00:00
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 21:25:53 +00:00
|
|
|
// Get user appointment stats
|
|
|
|
|
export async function getUserAppointmentStats(): Promise<UserAppointmentStats> {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 21:43:13 +00:00
|
|
|
// Get Jitsi meeting info
|
|
|
|
|
export async function getJitsiMeetingInfo(id: string): Promise<JitsiMeetingInfo> {
|
|
|
|
|
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) {
|
2025-11-24 16:38:09 +00:00
|
|
|
const errorMessage = extractErrorMessage(data as unknown as ApiError);
|
2025-11-23 21:43:13 +00:00
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|