/** * Appointment Management Hooks */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import apiClient from '../api/client'; import { Appointment, AppointmentStatus } from '../types'; import { format } from 'date-fns'; interface AppointmentFilters { resource?: string; status?: AppointmentStatus; startDate?: Date; endDate?: Date; } /** * Hook to fetch appointments with optional filters */ export const useAppointments = (filters?: AppointmentFilters) => { return useQuery({ queryKey: ['appointments', filters], queryFn: async () => { const params = new URLSearchParams(); if (filters?.resource) params.append('resource', filters.resource); if (filters?.status) params.append('status', filters.status); // Send full ISO datetime strings to avoid timezone issues // The backend will compare datetime fields properly if (filters?.startDate) { // Start of day in local timezone, converted to ISO const startOfDay = new Date(filters.startDate); startOfDay.setHours(0, 0, 0, 0); params.append('start_date', startOfDay.toISOString()); } if (filters?.endDate) { // End of day (or start of next day) in local timezone, converted to ISO const endOfDay = new Date(filters.endDate); endOfDay.setHours(0, 0, 0, 0); params.append('end_date', endOfDay.toISOString()); } const { data } = await apiClient.get(`/api/appointments/?${params}`); // Transform backend format to frontend format return data.map((a: any) => ({ id: String(a.id), resourceId: a.resource_id ? String(a.resource_id) : null, customerId: String(a.customer_id || a.customer), customerName: a.customer_name || '', serviceId: String(a.service_id || a.service), startTime: new Date(a.start_time), durationMinutes: a.duration_minutes || calculateDuration(a.start_time, a.end_time), status: a.status as AppointmentStatus, notes: a.notes || '', })); }, }); }; /** * Calculate duration in minutes from start and end times */ function calculateDuration(startTime: string, endTime: string): number { const start = new Date(startTime); const end = new Date(endTime); return Math.round((end.getTime() - start.getTime()) / (1000 * 60)); } /** * Hook to get a single appointment */ export const useAppointment = (id: string) => { return useQuery({ queryKey: ['appointments', id], queryFn: async () => { const { data } = await apiClient.get(`/api/appointments/${id}/`); return { id: String(data.id), resourceId: data.resource_id ? String(data.resource_id) : null, customerId: String(data.customer_id || data.customer), customerName: data.customer_name || '', serviceId: String(data.service_id || data.service), startTime: new Date(data.start_time), durationMinutes: data.duration_minutes || calculateDuration(data.start_time, data.end_time), status: data.status as AppointmentStatus, notes: data.notes || '', }; }, enabled: !!id, }); }; /** * Hook to create an appointment */ export const useCreateAppointment = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (appointmentData: Omit) => { const startTime = appointmentData.startTime; const endTime = new Date(startTime.getTime() + appointmentData.durationMinutes * 60000); const backendData: Record = { service: parseInt(appointmentData.serviceId), resource: appointmentData.resourceId ? parseInt(appointmentData.resourceId) : null, start_time: startTime.toISOString(), end_time: endTime.toISOString(), notes: appointmentData.notes || '', }; // Include customer if provided (for business-created appointments) if (appointmentData.customerId) { backendData.customer = parseInt(appointmentData.customerId); } const { data } = await apiClient.post('/api/appointments/', backendData); return data; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['appointments'] }); }, }); }; /** * Hook to update an appointment with optimistic updates for instant UI feedback */ export const useUpdateAppointment = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ id, updates }: { id: string; updates: Partial }) => { const backendData: any = {}; if (updates.serviceId) backendData.service = parseInt(updates.serviceId); if (updates.resourceId !== undefined) { backendData.resource = updates.resourceId ? parseInt(updates.resourceId) : null; } if (updates.startTime) { backendData.start_time = updates.startTime.toISOString(); // Calculate end_time if we have duration, otherwise backend will keep existing duration if (updates.durationMinutes) { const endTime = new Date(updates.startTime.getTime() + updates.durationMinutes * 60000); backendData.end_time = endTime.toISOString(); } } else if (updates.durationMinutes) { // If only duration changed, we need to get the current appointment to calculate new end time // For now, just send duration and let backend handle it // This case is handled by the resize logic which sends both startTime and durationMinutes } if (updates.status) backendData.status = updates.status; if (updates.notes !== undefined) backendData.notes = updates.notes; const { data } = await apiClient.patch(`/api/appointments/${id}/`, backendData); return data; }, // Optimistic update: update UI immediately before API call completes onMutate: async ({ id, updates }) => { // Cancel any outgoing refetches so they don't overwrite our optimistic update await queryClient.cancelQueries({ queryKey: ['appointments'] }); // Get all appointment queries and update them optimistically const queryCache = queryClient.getQueryCache(); const appointmentQueries = queryCache.findAll({ queryKey: ['appointments'] }); const previousData: { queryKey: unknown[]; data: Appointment[] | undefined }[] = []; appointmentQueries.forEach((query) => { const data = queryClient.getQueryData(query.queryKey); if (data) { previousData.push({ queryKey: query.queryKey, data }); queryClient.setQueryData(query.queryKey, (old) => { if (!old) return old; return old.map((apt) => apt.id === id ? { ...apt, ...updates } : apt ); }); } }); // Return context with the previous values for rollback return { previousData }; }, // If mutation fails, rollback to the previous values onError: (error, _variables, context) => { console.error('Failed to update appointment', error); if (context?.previousData) { context.previousData.forEach(({ queryKey, data }) => { queryClient.setQueryData(queryKey, data); }); } }, // Always refetch after error or success to ensure server state onSettled: () => { queryClient.invalidateQueries({ queryKey: ['appointments'] }); }, }); }; /** * Hook to delete an appointment with optimistic updates */ export const useDeleteAppointment = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (id: string) => { await apiClient.delete(`/api/appointments/${id}/`); return id; }, // Optimistic update: remove from UI immediately onMutate: async (id) => { await queryClient.cancelQueries({ queryKey: ['appointments'] }); // Get all appointment queries and update them optimistically const queryCache = queryClient.getQueryCache(); const appointmentQueries = queryCache.findAll({ queryKey: ['appointments'] }); const previousData: { queryKey: unknown[]; data: Appointment[] | undefined }[] = []; appointmentQueries.forEach((query) => { const data = queryClient.getQueryData(query.queryKey); if (data) { previousData.push({ queryKey: query.queryKey, data }); queryClient.setQueryData(query.queryKey, (old) => { if (!old) return old; return old.filter((apt) => apt.id !== id); }); } }); return { previousData }; }, onError: (error, _id, context) => { console.error('Failed to delete appointment', error); if (context?.previousData) { context.previousData.forEach(({ queryKey, data }) => { queryClient.setQueryData(queryKey, data); }); } }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['appointments'] }); }, }); }; /** * Hook to reschedule an appointment (update start time and resource) */ export const useRescheduleAppointment = () => { const updateMutation = useUpdateAppointment(); return useMutation({ mutationFn: async ({ id, newStartTime, newResourceId, }: { id: string; newStartTime: Date; newResourceId?: string | null; }) => { const appointment = await apiClient.get(`/api/appointments/${id}/`); const durationMinutes = appointment.data.duration_minutes; return updateMutation.mutateAsync({ id, updates: { startTime: newStartTime, durationMinutes, resourceId: newResourceId !== undefined ? newResourceId : undefined, }, }); }, }); };