Implement start and end meeting features in the appointment detail component. Introduce new API endpoints for starting and ending meetings, and update the appointment model to include meeting status fields. Enhance UI to provide buttons for starting and ending meetings, improving user interaction and experience.
784 lines
25 KiB
TypeScript
784 lines
25 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;
|
|
}
|
|
|
|
export async function startMeeting(id: string): Promise<Appointment> {
|
|
const tokens = getStoredTokens();
|
|
if (!tokens.access) {
|
|
throw new Error("Authentication required.");
|
|
}
|
|
|
|
const response = await fetch(API_ENDPOINTS.meetings.startMeeting(id), {
|
|
method: "POST",
|
|
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 endMeeting(id: string): Promise<Appointment> {
|
|
const tokens = getStoredTokens();
|
|
if (!tokens.access) {
|
|
throw new Error("Authentication required.");
|
|
}
|
|
|
|
const response = await fetch(API_ENDPOINTS.meetings.endMeeting(id), {
|
|
method: "POST",
|
|
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;
|
|
}
|