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 ,
AvailableDatesResponse ,
AdminAvailability ,
AppointmentStats ,
2025-11-25 21:25:53 +00:00
UserAppointmentStats ,
2025-11-23 21:43:13 +00:00
JitsiMeetingInfo ,
ApiError ,
2025-11-27 19:18:59 +00:00
WeeklyAvailabilityResponse ,
AvailabilityConfig ,
CheckDateAvailabilityResponse ,
AvailabilityOverview ,
SelectedSlot ,
2025-11-23 21:43:13 +00:00
} from "@/lib/models/appointments" ;
function extractErrorMessage ( error : ApiError ) : string {
if ( error . detail ) {
2025-11-27 19:53:35 +00:00
return Array . isArray ( error . detail ) ? error . detail . join ( ", " ) : String ( error . detail ) ;
2025-11-23 21:43:13 +00:00
}
if ( error . message ) {
2025-11-27 19:53:35 +00:00
return Array . isArray ( error . message ) ? error . message . join ( ", " ) : String ( error . message ) ;
2025-11-23 21:43:13 +00:00
}
if ( typeof error === "string" ) {
return error ;
}
2025-11-27 19:53:35 +00:00
return "An error occurred" ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:53:35 +00:00
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' } ` ) ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:53:35 +00:00
if ( contentType . includes ( "application/json" ) ) {
try {
return JSON . parse ( responseText ) ;
} catch {
throw new Error ( ` Server error ( ${ response . status } ): Invalid JSON format ` ) ;
}
2025-11-26 11:42:31 +00:00
}
2025-11-27 19:53:35 +00:00
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 '' ;
2025-11-27 19:18:59 +00:00
2025-11-27 19:53:35 +00:00
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 , '' ) ;
}
2025-11-26 11:42:31 +00:00
}
2025-11-27 19:53:35 +00:00
return lines [ lines . length - 1 ] ? . trim ( ) || '' ;
}
2025-11-26 11:42:31 +00:00
2025-11-27 19:53:35 +00:00
function validateAndCleanSlots ( slots : any [ ] ) : SelectedSlot [ ] {
return slots
. filter ( slot = > {
if ( ! slot || typeof slot !== 'object' ) return false ;
2025-11-27 19:18:59 +00:00
const dayNum = Number ( slot . day ) ;
2025-11-27 19:53:35 +00:00
const timeSlot = String ( slot . time_slot || '' ) . toLowerCase ( ) . trim ( ) ;
return ! isNaN ( dayNum ) && dayNum >= 0 && dayNum <= 6 &&
[ 'morning' , 'afternoon' , 'evening' ] . includes ( timeSlot ) ;
2025-11-27 19:18:59 +00:00
} )
. map ( slot = > ( {
day : Number ( slot . day ) ,
time_slot : String ( slot . time_slot ) . toLowerCase ( ) . trim ( ) as "morning" | "afternoon" | "evening" ,
} ) ) ;
2025-11-27 19:53:35 +00:00
}
2025-11-27 19:18:59 +00:00
2025-11-27 19:53:35 +00:00
function normalizeAvailabilitySchedule ( schedule : any ) : Record < string , string [ ] > {
if ( typeof schedule === 'string' ) {
try {
schedule = JSON . parse ( schedule ) ;
} catch {
return { } ;
}
2025-11-26 11:42:31 +00:00
}
2025-11-27 19:53:35 +00:00
const numberToTimeSlot : Record < number , string > = {
0 : 'morning' ,
1 : 'afternoon' ,
2 : 'evening' ,
2025-11-26 11:42:31 +00:00
} ;
2025-11-27 19:53:35 +00:00
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." ) ;
2025-11-26 11:42:31 +00:00
}
2025-11-27 19:53:35 +00:00
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" ) ;
2025-12-01 17:35:28 +00:00
}
2025-11-27 19:53:35 +00:00
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)." ) ;
2025-11-26 11:42:31 +00:00
}
2025-12-03 11:03:01 +00:00
// Explicitly exclude legacy fields - API doesn't need preferred_dates or preferred_time_slots
// We only use selected_slots format
2025-11-27 19:53:35 +00:00
const truncate = ( str : string , max : number ) = > String ( str || '' ) . trim ( ) . substring ( 0 , max ) ;
2025-12-03 11:03:01 +00:00
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 ;
} = {
2025-11-27 19:53:35 +00:00
first_name : truncate ( input . first_name , 100 ) ,
last_name : truncate ( input . last_name , 100 ) ,
email : truncate ( input . email , 100 ) . toLowerCase ( ) ,
2025-12-03 11:03:01 +00:00
selected_slots : selectedSlotsForPayload ,
2025-11-27 19:18:59 +00:00
} ;
2025-11-26 11:42:31 +00:00
2025-12-03 11:03:01 +00:00
// 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 ) ;
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-12-03 11:03:01 +00:00
body : requestBody ,
2025-11-23 21:43:13 +00:00
} ) ;
2025-12-03 11:03:01 +00:00
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-23 21:43:13 +00:00
}
2025-12-03 11:03:01 +00:00
// 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 ;
2025-11-27 19:18:59 +00:00
}
2025-12-03 11:03:01 +00:00
return appointmentResponse ;
2025-11-23 21:43:13 +00:00
}
2025-11-25 21:04:22 +00:00
export async function getAvailableDates ( ) : Promise < AvailableDatesResponse > {
2025-11-27 19:18:59 +00:00
try {
2025-12-03 11:03:01 +00:00
const response = await fetch ( API_ENDPOINTS . meetings . availableDates , {
2025-11-27 19:53:35 +00:00
method : "GET" ,
headers : { "Content-Type" : "application/json" } ,
} ) ;
2025-11-27 19:18:59 +00:00
2025-11-27 19:53:35 +00:00
if ( ! response . ok ) {
return { dates : [ ] } ;
2025-11-27 19:18:59 +00:00
}
2025-11-23 21:43:13 +00:00
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
return Array . isArray ( data ) ? { dates : data } : data ;
} catch {
return { dates : [ ] } ;
2025-11-27 19:18:59 +00:00
}
}
export async function getWeeklyAvailability ( ) : Promise < WeeklyAvailabilityResponse > {
const response = await fetch ( API_ENDPOINTS . meetings . weeklyAvailability , {
method : "GET" ,
2025-11-27 19:53:35 +00:00
headers : { "Content-Type" : "application/json" } ,
2025-11-27 19:18:59 +00:00
} ) ;
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-27 19:18:59 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-12-01 17:35:28 +00:00
}
2025-11-27 19:18:59 +00:00
2025-11-27 19:53:35 +00:00
return Array . isArray ( data ) ? data : data ;
2025-11-27 19:18:59 +00:00
}
export async function getAvailabilityConfig ( ) : Promise < AvailabilityConfig > {
const response = await fetch ( API_ENDPOINTS . meetings . availabilityConfig , {
method : "GET" ,
2025-11-27 19:53:35 +00:00
headers : { "Content-Type" : "application/json" } ,
2025-11-27 19:18:59 +00:00
} ) ;
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-27 19:18:59 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-27 19:18:59 +00:00
}
return data ;
}
export async function checkDateAvailability ( date : string ) : Promise < CheckDateAvailabilityResponse > {
const response = await fetch ( API_ENDPOINTS . meetings . checkDateAvailability , {
method : "POST" ,
2025-11-27 19:53:35 +00:00
headers : { "Content-Type" : "application/json" } ,
2025-11-27 19:18:59 +00:00
body : JSON.stringify ( { date } ) ,
} ) ;
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-27 19:18:59 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-12-01 17:35:28 +00:00
}
2025-11-27 19:18:59 +00:00
return data ;
}
export async function getAvailabilityOverview ( ) : Promise < AvailabilityOverview > {
const response = await fetch ( API_ENDPOINTS . meetings . availabilityOverview , {
method : "GET" ,
2025-11-27 19:53:35 +00:00
headers : { "Content-Type" : "application/json" } ,
2025-11-27 19:18:59 +00:00
} ) ;
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-27 19:18:59 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-27 19:18:59 +00:00
}
2025-12-01 17:35:28 +00:00
2025-11-27 19:18:59 +00:00
return data ;
2025-11-23 21:43:13 +00:00
}
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-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:53:35 +00:00
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 ] ;
2025-11-23 22:28:02 +00:00
return [ ] ;
2025-11-23 21:43:13 +00:00
}
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-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:53:35 +00:00
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 ;
2025-11-23 22:28:02 +00:00
return [ ] ;
2025-11-23 21:43:13 +00:00
}
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 } ` ,
} ,
} ) ;
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:53:35 +00:00
return ( data as AppointmentResponse ) . appointment || data ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:53:35 +00:00
export async function scheduleAppointment ( id : string , input : ScheduleAppointmentInput ) : Promise < Appointment > {
2025-11-23 21:43:13 +00:00
const tokens = getStoredTokens ( ) ;
if ( ! tokens . access ) {
throw new Error ( "Authentication required." ) ;
}
2025-12-03 18:50:45 +00:00
// 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 ] ;
}
} ) ;
2025-11-23 21:43:13 +00:00
const response = await fetch ( ` ${ API_ENDPOINTS . meetings . listAppointments } ${ id } /schedule/ ` , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ tokens . access } ` ,
} ,
2025-12-03 18:50:45 +00:00
body : JSON.stringify ( payload ) ,
2025-11-23 21:43:13 +00:00
} ) ;
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:53:35 +00:00
return data . appointment || data ;
2025-12-01 17:35:28 +00:00
}
2025-11-23 21:43:13 +00:00
2025-11-27 19:53:35 +00:00
export async function rejectAppointment ( id : string , input : RejectAppointmentInput ) : Promise < Appointment > {
2025-11-23 21:43:13 +00:00
const tokens = getStoredTokens ( ) ;
if ( ! tokens . access ) {
throw new Error ( "Authentication required." ) ;
}
2025-12-03 18:50:45 +00:00
// Build payload - only include rejection_reason if provided
const payload : any = { } ;
if ( input . rejection_reason ) {
payload . rejection_reason = input . rejection_reason ;
}
2025-11-23 21:43:13 +00:00
const response = await fetch ( ` ${ API_ENDPOINTS . meetings . listAppointments } ${ id } /reject/ ` , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ tokens . access } ` ,
} ,
2025-12-03 18:50:45 +00:00
body : JSON.stringify ( payload ) ,
2025-11-23 21:43:13 +00:00
} ) ;
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:53:35 +00:00
return ( data as AppointmentResponse ) . appointment || data ;
2025-11-23 21:43:13 +00:00
}
2025-11-25 21:04:22 +00:00
export async function getPublicAvailability ( ) : Promise < AdminAvailability | null > {
try {
2025-11-27 19:18:59 +00:00
const weeklyAvailability = await getWeeklyAvailability ( ) ;
const weekArray = Array . isArray ( weeklyAvailability )
? weeklyAvailability
: ( weeklyAvailability as any ) . week || [ ] ;
2025-12-01 17:35:28 +00:00
2025-11-27 19:18:59 +00:00
if ( ! weekArray || weekArray . length === 0 ) {
2025-11-25 21:04:22 +00:00
return null ;
}
2025-11-27 19:18:59 +00:00
const availabilitySchedule : Record < string , string [ ] > = { } ;
const availableDays : number [ ] = [ ] ;
const availableDaysDisplay : string [ ] = [ ] ;
2025-11-25 21:04:22 +00:00
2025-11-27 19:18:59 +00:00
weekArray . forEach ( ( day : any ) = > {
2025-11-27 19:53:35 +00:00
if ( day . is_available && day . available_slots ? . length > 0 ) {
2025-11-27 19:18:59 +00:00
availabilitySchedule [ day . day . toString ( ) ] = day . available_slots ;
availableDays . push ( day . day ) ;
availableDaysDisplay . push ( day . day_name ) ;
2025-12-01 17:35:28 +00:00
}
2025-11-27 19:18:59 +00:00
} ) ;
2025-11-25 21:04:22 +00:00
return {
available_days : availableDays ,
2025-11-27 19:18:59 +00:00
available_days_display : availableDaysDisplay ,
availability_schedule : availabilitySchedule ,
all_available_slots : weekArray
. filter ( ( d : any ) = > d . is_available )
2025-11-27 19:53:35 +00:00
. flatMap ( ( d : any ) = >
d . available_slots . map ( ( slot : string ) = > ( {
day : d.day ,
time_slot : slot as "morning" | "afternoon" | "evening"
} ) )
) ,
2025-11-25 21:04:22 +00:00
} as AdminAvailability ;
2025-11-27 19:53:35 +00:00
} catch {
2025-11-25 21:04:22 +00:00
return null ;
}
}
2025-11-23 21:43:13 +00:00
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-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:18:59 +00:00
if ( data . availability_schedule ) {
2025-11-27 19:53:35 +00:00
const availabilitySchedule = normalizeAvailabilitySchedule ( data . availability_schedule ) ;
2025-11-27 19:18:59 +00:00
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 ,
2025-11-27 19:53:35 +00:00
available_days_display : Array.isArray ( data . availability_schedule_display )
? data . availability_schedule_display
: data . availability_schedule_display
? [ data . availability_schedule_display ]
: availableDaysDisplay ,
2025-11-27 19:18:59 +00:00
availability_schedule : availabilitySchedule ,
availability_schedule_display : data.availability_schedule_display ,
all_available_slots : data.all_available_slots || [ ] ,
} as AdminAvailability ;
}
2025-11-25 20:15:37 +00:00
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 ;
2025-11-23 21:43:13 +00:00
}
2025-11-27 19:53:35 +00:00
export async function updateAdminAvailability ( input : UpdateAvailabilityInput ) : Promise < AdminAvailability > {
2025-11-23 21:43:13 +00:00
const tokens = getStoredTokens ( ) ;
if ( ! tokens . access ) {
throw new Error ( "Authentication required." ) ;
}
2025-11-27 19:53:35 +00:00
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 ) ;
2025-11-27 19:18:59 +00:00
2025-11-27 19:53:35 +00:00
if ( validSlots . length > 0 ) {
cleanedSchedule [ key . toString ( ) ] = validSlots ;
2025-11-27 19:18:59 +00:00
}
}
2025-11-27 19:53:35 +00:00
} ) ;
if ( Object . keys ( cleanedSchedule ) . length === 0 ) {
throw new Error ( "At least one day with valid time slots must be provided" ) ;
2025-11-27 19:18:59 +00:00
}
2025-11-25 20:38:37 +00:00
2025-11-27 19:53:35 +00:00
const sortedSchedule : Record < string , string [ ] > = { } ;
Object . keys ( cleanedSchedule )
. sort ( ( a , b ) = > parseInt ( a ) - parseInt ( b ) )
. forEach ( key = > {
sortedSchedule [ key ] = cleanedSchedule [ key ] ;
} ) ;
2025-11-27 19:18:59 +00:00
let 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-27 19:53:35 +00:00
body : JSON.stringify ( { availability_schedule : sortedSchedule } ) ,
2025-11-23 21:43:13 +00:00
} ) ;
2025-11-27 19:18:59 +00:00
if ( ! response . ok && response . status === 500 ) {
response = await fetch ( API_ENDPOINTS . meetings . adminAvailability , {
method : "PATCH" ,
headers : {
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ tokens . access } ` ,
} ,
2025-11-27 19:53:35 +00:00
body : JSON.stringify ( { availability_schedule : sortedSchedule } ) ,
2025-12-01 17:35:28 +00:00
} ) ;
2025-11-27 19:18:59 +00:00
}
const responseText = await response . text ( ) ;
const contentType = response . headers . get ( "content-type" ) || "" ;
2025-11-27 19:53:35 +00:00
2025-11-27 19:18:59 +00:00
if ( ! responseText || responseText . trim ( ) . length === 0 ) {
if ( response . ok ) {
return await getAdminAvailability ( ) ;
}
2025-11-27 19:53:35 +00:00
throw new Error ( ` Server error ( ${ response . status } ): ${ response . statusText || 'Empty response' } ` ) ;
2025-11-27 19:18:59 +00:00
}
2025-11-27 19:53:35 +00:00
let data : any ;
2025-11-27 19:18:59 +00:00
if ( contentType . includes ( "application/json" ) ) {
2025-12-01 17:35:28 +00:00
try {
2025-11-27 19:53:35 +00:00
data = JSON . parse ( responseText ) ;
} catch {
throw new Error ( ` Server error ( ${ response . status } ): Invalid JSON format ` ) ;
2025-11-27 19:18:59 +00:00
}
2025-11-27 19:53:35 +00:00
} else {
const htmlError = extractHtmlError ( responseText ) ;
throw new Error ( ` Server error ( ${ response . status } ): ${ htmlError || response . statusText || 'Internal Server Error' } ` ) ;
2025-11-25 20:38:37 +00:00
}
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-27 19:18:59 +00:00
}
2025-11-27 19:53:35 +00:00
if ( data ? . availability_schedule ) {
const availabilitySchedule = normalizeAvailabilitySchedule ( data . availability_schedule ) ;
2025-11-27 19:18:59 +00:00
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 ,
2025-11-27 19:53:35 +00:00
available_days_display : Array.isArray ( data . availability_schedule_display )
? data . availability_schedule_display
: data . availability_schedule_display
? [ data . availability_schedule_display ]
: availableDaysDisplay ,
2025-11-27 19:18:59 +00:00
availability_schedule : availabilitySchedule ,
availability_schedule_display : data.availability_schedule_display ,
all_available_slots : data.all_available_slots || [ ] ,
} as AdminAvailability ;
}
2025-11-27 19:53:35 +00:00
2025-11-27 19:18:59 +00:00
if ( response . ok && ( ! data || Object . keys ( data ) . length === 0 ) ) {
2025-11-27 19:53:35 +00:00
return await getAdminAvailability ( ) ;
2025-11-23 21:43:13 +00:00
}
2025-11-25 20:38:37 +00:00
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 ;
2025-11-23 21:43:13 +00:00
}
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 } ` ,
} ,
} ) ;
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-23 21:43:13 +00:00
}
return data ;
}
2025-11-27 20:35:26 +00:00
export async function getUserAppointmentStats ( ) : Promise < UserAppointmentStats > {
2025-11-25 21:25:53 +00:00
const tokens = getStoredTokens ( ) ;
if ( ! tokens . access ) {
throw new Error ( "Authentication required." ) ;
}
const response = await fetch ( API_ENDPOINTS . meetings . userAppointmentStats , {
2025-11-27 20:35:26 +00:00
method : "GET" ,
2025-11-25 21:25:53 +00:00
headers : {
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ tokens . access } ` ,
} ,
} ) ;
2025-11-27 20:35:26 +00:00
const data = await parseResponse ( response ) ;
2025-11-25 21:25:53 +00:00
if ( ! response . ok ) {
2025-11-27 20:35:26 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-25 21:25:53 +00:00
}
2025-11-27 20:35:26 +00:00
return data ;
2025-11-25 21:25:53 +00:00
}
2025-11-23 21:43:13 +00:00
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 } ` ,
} ,
} ) ;
2025-11-27 19:53:35 +00:00
const data = await parseResponse ( response ) ;
2025-11-23 21:43:13 +00:00
if ( ! response . ok ) {
2025-11-27 19:53:35 +00:00
throw new Error ( extractErrorMessage ( data as unknown as ApiError ) ) ;
2025-11-23 21:43:13 +00:00
}
return data ;
}
2025-12-04 15:36:14 +00:00
2025-12-04 16:14:24 +00:00
export async function startMeeting (
id : string ,
options ? : {
metadata? : string ;
recording_url? : string ;
}
) : Promise < Appointment > {
2025-12-04 15:36:14 +00:00
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 } ` ,
} ,
2025-12-04 16:14:24 +00:00
body : JSON.stringify ( {
action : "start" ,
metadata : options?.metadata ,
recording_url : options?.recording_url ,
} ) ,
2025-12-04 15:36:14 +00:00
} ) ;
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 ;
}