import dayjs from 'dayjs'; import useSWR, { useSWRConfig } from 'swr'; import { useMemo, useCallback } from 'react'; import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; import { type AppointmentsFetchResponse, BackendAppointmentStates, BackendAppointmentStateIds } from '../types'; import { useSelectedDateContext } from './selected-date-context'; // Hook to fetch available appointment states from backend export const useAppointmentStates = () => { const url = `${restBaseUrl}/mohappointment/appointmentstate`; const { data, error, isLoading } = useSWR(url, (url) => openmrsFetch(url).then((res) => res.data)); return { appointmentStates: data?.results || [], isLoading, error, }; }; // Backward-compatible overload - supports both (appointmentStatus, date) and (appointmentState, startIndex) export const useAppointmentList = (appointmentState: string, startIndexOrDate: number | string = 0) => { const { selectedDate } = useSelectedDateContext(); // Determine if using old API (string date) or new API (number startIndex) const isLegacyAPI = typeof startIndexOrDate === 'string'; const startIndex = isLegacyAPI ? 0 : (startIndexOrDate as number); const dateOverride = isLegacyAPI ? (startIndexOrDate as string) : null; const validAppointmentState = useMemo(() => { if (!appointmentState || typeof appointmentState !== 'string') { return 'WAITING'; } return appointmentState.trim(); }, [appointmentState]); const formattedDate = useMemo(() => { const targetDate = dateOverride || selectedDate; if (!targetDate) { return dayjs().format('YYYY-MM-DDTHH:mm:ss.SSSZZ'); } // If it's already in the correct format, use it directly if (targetDate.includes('T') && (targetDate.includes('Z') || targetDate.includes('+'))) { return targetDate; } const parsed = dayjs(targetDate); if (!parsed.isValid()) { return dayjs().format('YYYY-MM-DDTHH:mm:ss.SSSZZ'); } // Use consistent date format matching the constants return parsed.format('YYYY-MM-DDTHH:mm:ss.SSSZZ'); }, [selectedDate, dateOverride]); const url = `${restBaseUrl}/mohappointment/all?forDate=${encodeURIComponent(formattedDate)}`; const { data, error, isLoading, mutate } = useSWR( // Use an array key to ensure cache invalidation when date changes [url, formattedDate, validAppointmentState], () => openmrsFetch(url), { errorRetryCount: 2, revalidateOnFocus: true, revalidateOnReconnect: true, refreshInterval: 30000, dedupingInterval: 5000, // Ensure we revalidate when the URL changes (when date changes) revalidateIfStale: true, }, ); const results = useMemo(() => { if (!data?.data) { return []; } const apiData = data.data as any[]; const filteredAppointments = Array.isArray(apiData) ? apiData.filter((appointment) => { // Calculate dates once for efficiency const appointmentDate = dayjs(appointment.appointmentDate); const selectedDateObj = dayjs(formattedDate); const isSelectedDate = appointmentDate.isSame(selectedDateObj, 'day'); // Only show appointments for the selected date (critical for calendar integration) if (!isSelectedDate) { return false; } const state = appointment.appointmentState; // Extract appointment state value - handle both string and object types let appointmentStateValue = state; let appointmentStateId = null; if (appointmentStateValue && typeof appointmentStateValue === 'object') { appointmentStateId = (appointmentStateValue as any).appointmentStateId; appointmentStateValue = (appointmentStateValue as any).description || (appointmentStateValue as any).name || (appointmentStateValue as any).value; } // Backend state matching - prioritize ID-based matching as it's more reliable if (appointmentStateId !== null) { // Map frontend states to backend state IDs (based on Java backend) const stateIdMapping: Record = { SCHEDULED: [ BackendAppointmentStateIds.UPCOMING, BackendAppointmentStateIds.CONFIRMED, BackendAppointmentStateIds.NULL, ], WAITING: [ BackendAppointmentStateIds.NULL, BackendAppointmentStateIds.UPCOMING, BackendAppointmentStateIds.CONFIRMED, ], CHECKEDIN: [BackendAppointmentStateIds.WAITING], COMPLETED: [BackendAppointmentStateIds.ATTENDED], CANCELLED: [BackendAppointmentStateIds.RETIRED], MISSED: [BackendAppointmentStateIds.EXPIRED], }; const expectedStateIds = stateIdMapping[validAppointmentState.toUpperCase()]; if (expectedStateIds && expectedStateIds.includes(appointmentStateId)) { return true; } } // Fallback to string-based matching for legacy support if (typeof appointmentStateValue === 'string') { if (appointmentStateValue.toUpperCase() === validAppointmentState.toUpperCase()) { return true; } } // Backend appointment states mapping for specific legacy status names (string-based fallback) if (validAppointmentState === 'Scheduled') { // Primary state for scheduled appointments is UPCOMING (ID: 3) if (appointmentStateValue === BackendAppointmentStates.UPCOMING) { return true; } // Also include CONFIRMED (ID: 2) for assigned appointments if (appointmentStateValue === BackendAppointmentStates.CONFIRMED) { return true; } // Include NULL (ID: 1) as default state for new appointments if (appointmentStateValue === BackendAppointmentStates.NULL || appointmentStateValue === null) { return true; } } if (validAppointmentState === 'CheckedIn' && appointmentStateValue === BackendAppointmentStates.WAITING) { return true; } if ( validAppointmentState === 'Completed' && (appointmentStateValue === BackendAppointmentStates.ATTENDED || appointment.attended === true) ) { return true; } if ( validAppointmentState === 'Cancelled' && (appointmentStateValue === BackendAppointmentStates.RETIRED || appointment.voided === true) ) { return true; } if (validAppointmentState === 'Missed' && appointmentStateValue === BackendAppointmentStates.EXPIRED) { return true; } // WAITING state special handling if ( validAppointmentState.toUpperCase() === 'WAITING' && (!appointmentStateValue || appointmentStateValue === 'NULL' || appointmentStateValue === null) ) { return true; } // Enhanced legacy state mapping aligned with backend states const legacyStateMapping: Record = { WAITING: [ 'NULL', 'SCHEDULED', 'PENDING', BackendAppointmentStates.NULL, BackendAppointmentStates.UPCOMING, BackendAppointmentStates.CONFIRMED, ], ATTENDED: ['CHECKEDIN', 'ARRIVED', BackendAppointmentStates.ATTENDED], SCHEDULED: [ 'SCHEDULED', 'PENDING', BackendAppointmentStates.UPCOMING, BackendAppointmentStates.CONFIRMED, BackendAppointmentStates.NULL, ], CHECKEDIN: ['ATTENDED', 'CHECKEDIN', 'ARRIVED', BackendAppointmentStates.WAITING], COMPLETED: [BackendAppointmentStates.ATTENDED], CANCELLED: [BackendAppointmentStates.RETIRED], EXPIRED: [BackendAppointmentStates.EXPIRED], MISSED: [BackendAppointmentStates.EXPIRED], }; const mappedStates = legacyStateMapping[validAppointmentState.toUpperCase()]; if (mappedStates && typeof appointmentStateValue === 'string') { return ( mappedStates.includes(appointmentStateValue.toUpperCase()) || mappedStates.includes(appointmentStateValue) ); } // Direct match for backend state names if (appointmentStateValue === validAppointmentState) { return true; } return false; }) : []; const endIndex = startIndex + 50; return filteredAppointments.slice(startIndex, endIndex); }, [data, validAppointmentState, startIndex, formattedDate]); return { appointmentList: results, isLoading, error, mutate, refresh: () => mutate(), }; }; export const useWaitingAppointments = (startIndex = 0) => { return useAppointmentList('WAITING', startIndex); }; export const useAttendedAppointments = (startIndex = 0) => { return useAppointmentList('ATTENDED', startIndex); }; export const useCompletedAppointments = (startIndex = 0) => { return useAppointmentList('COMPLETED', startIndex); }; export const useExpiredAppointments = (startIndex = 0) => { return useAppointmentList('EXPIRED', startIndex); }; export const useCancelledAppointments = (startIndex = 0) => { return useAppointmentList('CANCELLED', startIndex); }; export const useScheduledAppointments = (startIndex = 0) => { return useAppointmentList('SCHEDULED', startIndex); }; export const useEarlyAppointmentList = (startDate?: string) => { const { selectedDate } = useSelectedDateContext(); const forDate = startDate || selectedDate; const url = `${restBaseUrl}/appointment/earlyAppointment?forDate=${forDate}`; const { data, error, isLoading } = useSWR(url, openmrsFetch, { errorRetryCount: 2, refreshInterval: 30000, }); return { earlyAppointmentList: data?.data ?? [], isLoading, error, }; }; export const useAppointmentsByDateRange = (startDate?: string, endDate?: string) => { const { selectedDate } = useSelectedDateContext(); const searchStartDate = startDate || selectedDate; const searchEndDate = endDate || selectedDate; const url = `${restBaseUrl}/mohappointment/search`; const { data, error, isLoading, mutate } = useSWR( [url, searchStartDate, searchEndDate], () => openmrsFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ startDate: `${searchStartDate}T00:00:00.000Z`, endDate: `${searchEndDate}T23:59:59.999Z`, }), }), { errorRetryCount: 2, revalidateOnFocus: true, }, ); const allAppointments = useMemo(() => (data?.data as any) ?? [], [data?.data]); const getAppointmentsByState = useCallback( (state: string) => { return allAppointments.filter((apt) => { const stateDescription = apt.appointmentState?.description; const stateId = apt.appointmentState?.appointmentStateId?.toString(); return stateDescription === state || stateId === state; }); }, [allAppointments], ); return { allAppointments, getAppointmentsByState, isLoading, error, mutate, }; }; export const useAppointmentMutations = () => { const { mutate } = useSWRConfig(); const invalidateAppointments = useCallback(() => { mutate((key) => typeof key === 'string' && key.includes('/mohappointment/all'), undefined, { revalidate: true, }); }, [mutate]); const invalidateAppointmentsByDate = useCallback( (date: string) => { mutate( (key) => typeof key === 'string' && key.includes('/mohappointment/all') && key.includes(encodeURIComponent(date)), undefined, { revalidate: true }, ); }, [mutate], ); const invalidateAppointmentsByState = useCallback( (state: string) => { mutate((key) => typeof key === 'string' && key.includes('/mohappointment/all'), undefined, { revalidate: true }); }, [mutate], ); return { invalidateAppointments, invalidateAppointmentsByDate, invalidateAppointmentsByState, }; }; export const useAppointmentStatesDebug = () => { const { selectedDate } = useSelectedDateContext(); const url = `${restBaseUrl}/mohappointment/search`; const { data, error } = useSWR( [url, selectedDate, 'debug'], () => openmrsFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startDate: `${selectedDate}T00:00:00.000Z`, endDate: `${selectedDate}T23:59:59.999Z`, }), }), { errorRetryCount: 1, revalidateOnFocus: false, }, ); const appointments = useMemo(() => (data?.data as any) ?? [], [data?.data]); const uniqueStates = useMemo(() => { const stateMap = new Map(); appointments.forEach((apt) => { const state = apt.appointmentState; if (state) { const key = state.appointmentStateId; stateMap.set(key, { id: state.appointmentStateId, name: state.description, count: (stateMap.get(key)?.count || 0) + 1, }); } }); return Array.from(stateMap.values()); }, [appointments]); return { uniqueStates, appointments, totalCount: appointments.length, error, }; }; export default useAppointmentList;