740 lines
24 KiB
TypeScript
740 lines
24 KiB
TypeScript
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,
|
|
AvailableDatesResponse,
|
|
AdminAvailability,
|
|
AppointmentStats,
|
|
UserAppointmentStats,
|
|
JitsiMeetingInfo,
|
|
ApiError,
|
|
WeeklyAvailabilityResponse,
|
|
AvailabilityConfig,
|
|
CheckDateAvailabilityResponse,
|
|
AvailabilityOverview,
|
|
SelectedSlot,
|
|
} from "@/lib/models/appointments";
|
|
|
|
function extractErrorMessage(error: ApiError): string {
|
|
if (error.detail) {
|
|
return Array.isArray(error.detail) ? error.detail.join(", ") : String(error.detail);
|
|
}
|
|
if (error.message) {
|
|
return Array.isArray(error.message) ? error.message.join(", ") : String(error.message);
|
|
}
|
|
if (typeof error === "string") {
|
|
return error;
|
|
}
|
|
return "An error occurred";
|
|
}
|
|
|
|
async function parseResponse(response: Response): Promise<any> {
|
|
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'}`);
|
|
}
|
|
|
|
if (contentType.includes("application/json")) {
|
|
try {
|
|
return JSON.parse(responseText);
|
|
} catch {
|
|
throw new Error(`Server error (${response.status}): Invalid JSON format`);
|
|
}
|
|
}
|
|
|
|
const errorMatch = responseText.match(/<pre[^>]*>([\s\S]*?)<\/pre>/i) ||
|
|
responseText.match(/<h1[^>]*>([\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(/<pre[^>]*>([\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);
|
|
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<string, string[]> {
|
|
if (typeof schedule === 'string') {
|
|
try {
|
|
schedule = JSON.parse(schedule);
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
const numberToTimeSlot: Record<number, string> = {
|
|
0: 'morning',
|
|
1: 'afternoon',
|
|
2: 'evening',
|
|
};
|
|
|
|
const result: Record<string, string[]> = {};
|
|
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<Appointment> {
|
|
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).");
|
|
}
|
|
|
|
// Explicitly exclude legacy fields - API doesn't need preferred_dates or preferred_time_slots
|
|
// We only use selected_slots format
|
|
|
|
const truncate = (str: string, max: number) => String(str || '').trim().substring(0, max);
|
|
|
|
const selectedSlotsForPayload = validSlots.map(slot => ({
|
|
day: slot.day,
|
|
time_slot: slot.time_slot,
|
|
}));
|
|
|
|
// Build payload with ONLY the fields the API requires/accepts
|
|
// DO NOT include preferred_dates or preferred_time_slots - the API doesn't need them
|
|
const payload: {
|
|
first_name: string;
|
|
last_name: string;
|
|
email: string;
|
|
selected_slots: Array<{ day: number; time_slot: string }>;
|
|
phone?: string;
|
|
reason?: string;
|
|
} = {
|
|
first_name: truncate(input.first_name, 100),
|
|
last_name: truncate(input.last_name, 100),
|
|
email: truncate(input.email, 100).toLowerCase(),
|
|
selected_slots: selectedSlotsForPayload,
|
|
};
|
|
|
|
// Only add optional fields if they exist
|
|
if (input.phone && input.phone.length > 0) {
|
|
payload.phone = truncate(input.phone, 100);
|
|
}
|
|
if (input.reason && input.reason.length > 0) {
|
|
payload.reason = truncate(input.reason, 100);
|
|
}
|
|
|
|
const requestBody = JSON.stringify(payload);
|
|
|
|
const response = await fetch(API_ENDPOINTS.meetings.createAppointment, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${tokens.access}`,
|
|
},
|
|
body: requestBody,
|
|
});
|
|
|
|
const data = await parseResponse(response);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
// Build clean response - explicitly exclude preferred_dates and preferred_time_slots
|
|
// Backend may return these legacy fields, but we only use selected_slots format (per API spec)
|
|
const rawResponse: any = data.appointment || data.data || data;
|
|
|
|
// Build appointment object from scratch with ONLY the fields we want
|
|
// Explicitly DO NOT include preferred_dates, preferred_time_slots, or their display variants
|
|
const appointmentResponse: any = {
|
|
id: data.appointment_id || rawResponse.id || '',
|
|
first_name: rawResponse.first_name || input.first_name.trim(),
|
|
last_name: rawResponse.last_name || input.last_name.trim(),
|
|
email: rawResponse.email || input.email.trim().toLowerCase(),
|
|
phone: rawResponse.phone || input.phone?.trim(),
|
|
reason: rawResponse.reason || input.reason?.trim(),
|
|
// Use selected_slots from our original input (preserve the format we sent - per API spec)
|
|
selected_slots: validSlots,
|
|
status: rawResponse.status || "pending_review",
|
|
created_at: rawResponse.created_at || new Date().toISOString(),
|
|
updated_at: rawResponse.updated_at || new Date().toISOString(),
|
|
// Include other useful fields from response
|
|
...(rawResponse.jitsi_meet_url && { jitsi_meet_url: rawResponse.jitsi_meet_url }),
|
|
...(rawResponse.jitsi_room_id && { jitsi_room_id: rawResponse.jitsi_room_id }),
|
|
...(rawResponse.matching_availability && { matching_availability: rawResponse.matching_availability }),
|
|
...(rawResponse.are_preferences_available !== undefined && { are_preferences_available: rawResponse.are_preferences_available }),
|
|
...(rawResponse.available_slots_info && { available_slots_info: rawResponse.available_slots_info }),
|
|
// Explicitly EXCLUDED: preferred_dates, preferred_time_slots, preferred_dates_display, preferred_time_slots_display
|
|
};
|
|
|
|
// Explicitly delete preferred_dates and preferred_time_slots from response object
|
|
// These are backend legacy fields - we only use selected_slots format
|
|
if ('preferred_dates' in appointmentResponse) {
|
|
delete appointmentResponse.preferred_dates;
|
|
}
|
|
if ('preferred_time_slots' in appointmentResponse) {
|
|
delete appointmentResponse.preferred_time_slots;
|
|
}
|
|
if ('preferred_dates_display' in appointmentResponse) {
|
|
delete appointmentResponse.preferred_dates_display;
|
|
}
|
|
if ('preferred_time_slots_display' in appointmentResponse) {
|
|
delete appointmentResponse.preferred_time_slots_display;
|
|
}
|
|
|
|
return appointmentResponse;
|
|
}
|
|
|
|
export async function getAvailableDates(): Promise<AvailableDatesResponse> {
|
|
try {
|
|
const response = await fetch(API_ENDPOINTS.meetings.availableDates, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return { dates: [] };
|
|
}
|
|
|
|
const data = await parseResponse(response);
|
|
return Array.isArray(data) ? { dates: data } : data;
|
|
} catch {
|
|
return { dates: [] };
|
|
}
|
|
}
|
|
|
|
export async function getWeeklyAvailability(): Promise<WeeklyAvailabilityResponse> {
|
|
const response = await fetch(API_ENDPOINTS.meetings.weeklyAvailability, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
const data = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return Array.isArray(data) ? data : data;
|
|
}
|
|
|
|
export async function getAvailabilityConfig(): Promise<AvailabilityConfig> {
|
|
const response = await fetch(API_ENDPOINTS.meetings.availabilityConfig, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
const data = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
export async function checkDateAvailability(date: string): Promise<CheckDateAvailabilityResponse> {
|
|
const response = await fetch(API_ENDPOINTS.meetings.checkDateAvailability, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ date }),
|
|
});
|
|
|
|
const data = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
export async function getAvailabilityOverview(): Promise<AvailabilityOverview> {
|
|
const response = await fetch(API_ENDPOINTS.meetings.availabilityOverview, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
const data = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
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}`,
|
|
},
|
|
});
|
|
|
|
const data = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
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 [];
|
|
}
|
|
|
|
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}`,
|
|
},
|
|
});
|
|
|
|
const data = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
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 [];
|
|
}
|
|
|
|
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 = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return (data as AppointmentResponse).appointment || data;
|
|
}
|
|
|
|
export async function scheduleAppointment(id: string, input: ScheduleAppointmentInput): Promise<Appointment> {
|
|
const tokens = getStoredTokens();
|
|
if (!tokens.access) {
|
|
throw new Error("Authentication required.");
|
|
}
|
|
|
|
// Build payload with defaults
|
|
const payload: any = {
|
|
...input,
|
|
// Default create_jitsi_meeting to true if not specified
|
|
create_jitsi_meeting: input.create_jitsi_meeting !== undefined ? input.create_jitsi_meeting : true,
|
|
};
|
|
|
|
// Remove undefined fields
|
|
Object.keys(payload).forEach(key => {
|
|
if (payload[key] === undefined) {
|
|
delete payload[key];
|
|
}
|
|
});
|
|
|
|
const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/schedule/`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${tokens.access}`,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
const data = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return data.appointment || data;
|
|
}
|
|
|
|
export async function rejectAppointment(id: string, input: RejectAppointmentInput): Promise<Appointment> {
|
|
const tokens = getStoredTokens();
|
|
if (!tokens.access) {
|
|
throw new Error("Authentication required.");
|
|
}
|
|
|
|
// Build payload - only include rejection_reason if provided
|
|
const payload: any = {};
|
|
if (input.rejection_reason) {
|
|
payload.rejection_reason = input.rejection_reason;
|
|
}
|
|
|
|
const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/reject/`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${tokens.access}`,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
const data = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return (data as AppointmentResponse).appointment || data;
|
|
}
|
|
|
|
export async function getPublicAvailability(): Promise<AdminAvailability | null> {
|
|
try {
|
|
const weeklyAvailability = await getWeeklyAvailability();
|
|
const weekArray = Array.isArray(weeklyAvailability)
|
|
? weeklyAvailability
|
|
: (weeklyAvailability as any).week || [];
|
|
|
|
if (!weekArray || weekArray.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const availabilitySchedule: Record<string, string[]> = {};
|
|
const availableDays: number[] = [];
|
|
const availableDaysDisplay: string[] = [];
|
|
|
|
weekArray.forEach((day: any) => {
|
|
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);
|
|
}
|
|
});
|
|
|
|
return {
|
|
available_days: availableDays,
|
|
available_days_display: availableDaysDisplay,
|
|
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"
|
|
}))
|
|
),
|
|
} as AdminAvailability;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getAdminAvailability(): Promise<AdminAvailability> {
|
|
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 = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
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: 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export async function updateAdminAvailability(input: UpdateAvailabilityInput): Promise<AdminAvailability> {
|
|
const tokens = getStoredTokens();
|
|
if (!tokens.access) {
|
|
throw new Error("Authentication required.");
|
|
}
|
|
|
|
if (!input.availability_schedule) {
|
|
throw new Error("availability_schedule is required");
|
|
}
|
|
|
|
const cleanedSchedule: Record<string, string[]> = {};
|
|
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<string, string[]> = {};
|
|
Object.keys(cleanedSchedule)
|
|
.sort((a, b) => parseInt(a) - parseInt(b))
|
|
.forEach(key => {
|
|
sortedSchedule[key] = cleanedSchedule[key];
|
|
});
|
|
|
|
let response = await fetch(API_ENDPOINTS.meetings.adminAvailability, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${tokens.access}`,
|
|
},
|
|
body: JSON.stringify({ availability_schedule: sortedSchedule }),
|
|
});
|
|
|
|
if (!response.ok && response.status === 500) {
|
|
response = await fetch(API_ENDPOINTS.meetings.adminAvailability, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${tokens.access}`,
|
|
},
|
|
body: JSON.stringify({ availability_schedule: sortedSchedule }),
|
|
});
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
const contentType = response.headers.get("content-type") || "";
|
|
|
|
if (!responseText || responseText.trim().length === 0) {
|
|
if (response.ok) {
|
|
return await getAdminAvailability();
|
|
}
|
|
throw new Error(`Server error (${response.status}): ${response.statusText || 'Empty response'}`);
|
|
}
|
|
|
|
let data: any;
|
|
if (contentType.includes("application/json")) {
|
|
try {
|
|
data = JSON.parse(responseText);
|
|
} catch {
|
|
throw new Error(`Server error (${response.status}): Invalid JSON format`);
|
|
}
|
|
} else {
|
|
const htmlError = extractHtmlError(responseText);
|
|
throw new Error(`Server error (${response.status}): ${htmlError || response.statusText || 'Internal Server Error'}`);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
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: 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.ok && (!data || Object.keys(data).length === 0)) {
|
|
return await getAdminAvailability();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
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 = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
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 = await parseResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
|
}
|
|
|
|
return data;
|
|
}
|