/** * React Query Hooks - Availability * Hooks para gerenciamento de disponibilidade com cache inteligente * @version 1.0 */ import { useQuery, useMutation, useQueryClient, UseQueryOptions, } from "@tanstack/react-query"; import { availabilityService } from "../services"; import type { DoctorAvailability, CreateAvailabilityInput, UpdateAvailabilityInput, } from "../services/availability/types"; import toast from "react-hot-toast"; // ============================================================================ // QUERY KEYS // ============================================================================ export const availabilityKeys = { all: ["availability"] as const, lists: () => [...availabilityKeys.all, "list"] as const, list: (doctorId?: string) => [...availabilityKeys.lists(), doctorId] as const, details: () => [...availabilityKeys.all, "detail"] as const, detail: (id: string) => [...availabilityKeys.details(), id] as const, slots: (doctorId: string, date: string) => [...availabilityKeys.all, "slots", doctorId, date] as const, }; // ============================================================================ // QUERY HOOKS // ============================================================================ /** * Hook para buscar disponibilidade de um médico */ export function useAvailability( doctorId: string | undefined, options?: Omit, "queryKey" | "queryFn"> ) { return useQuery({ queryKey: availabilityKeys.list(doctorId), queryFn: async () => { if (!doctorId) throw new Error("Doctor ID é obrigatório"); return await availabilityService.list({ doctor_id: doctorId }); }, enabled: !!doctorId, ...options, }); } /** * Hook para buscar uma disponibilidade específica */ export function useAvailabilityById(id: string | undefined) { return useQuery({ queryKey: availabilityKeys.detail(id!), queryFn: async () => { if (!id) throw new Error("ID é obrigatório"); const items = await availabilityService.list(); const found = items.find((item) => item.id === id); if (!found) throw new Error("Disponibilidade não encontrada"); return found; }, enabled: !!id, }); } /** * Hook para buscar slots disponíveis de um médico em uma data */ export function useAvailableSlots( doctorId: string | undefined, date: string | undefined, options?: Omit, "queryKey" | "queryFn"> ) { return useQuery({ queryKey: availabilityKeys.slots(doctorId!, date!), queryFn: async () => { if (!doctorId || !date) throw new Error("Doctor ID e Data são obrigatórios"); // Buscar disponibilidade do médico const availabilities = await availabilityService.list({ doctor_id: doctorId, }); // Buscar consultas do dia const { appointmentService } = await import("../services"); const appointments = await appointmentService.list({ doctor_id: doctorId, scheduled_at: `gte.${date}T00:00:00,lt.${date}T23:59:59`, }); // Calcular slots livres (simplificado - usar lógica completa do AvailableSlotsPicker) const occupiedSlots = new Set( appointments.map((a) => a.scheduled_at.substring(11, 16)) ); const dayOfWeek = new Date(date).getDay(); const dayAvailability = availabilities.filter( (av) => av.weekday === dayOfWeek ); const freeSlots: string[] = []; dayAvailability.forEach((av) => { const start = parseInt(av.start_time.replace(":", "")); const end = parseInt(av.end_time.replace(":", "")); const slotMinutes = av.slot_minutes || 30; const increment = (slotMinutes / 60) * 100; for (let time = start; time < end; time += increment) { const hour = Math.floor(time / 100); const minute = time % 100; const timeStr = `${hour.toString().padStart(2, "0")}:${minute .toString() .padStart(2, "0")}`; if (!occupiedSlots.has(timeStr)) { freeSlots.push(timeStr); } } }); return freeSlots.sort(); }, enabled: !!doctorId && !!date, staleTime: 2 * 60 * 1000, // 2 minutos - slots mudam frequentemente ...options, }); } // ============================================================================ // MUTATION HOOKS // ============================================================================ /** * Hook para criar nova disponibilidade */ export function useCreateAvailability() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (data: CreateAvailabilityInput) => { return await availabilityService.create(data); }, onSuccess: (data) => { // Invalidar listas de disponibilidade queryClient.invalidateQueries({ queryKey: availabilityKeys.lists() }); if (data?.doctor_id) { queryClient.invalidateQueries({ queryKey: availabilityKeys.list(data.doctor_id), }); } toast.success("Disponibilidade criada com sucesso!"); }, onError: (error: Error) => { toast.error(`Erro ao criar disponibilidade: ${error.message}`); }, }); } /** * Hook para atualizar disponibilidade */ export function useUpdateAvailability() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (data: UpdateAvailabilityInput & { id: string }) => { return await availabilityService.update(data.id, data); }, onMutate: async (variables) => { // Optimistic update await queryClient.cancelQueries({ queryKey: availabilityKeys.detail(variables.id), }); const previousAvailability = queryClient.getQueryData( availabilityKeys.detail(variables.id) ); return { previousAvailability }; }, onSuccess: (data, variables) => { queryClient.invalidateQueries({ queryKey: availabilityKeys.lists() }); queryClient.invalidateQueries({ queryKey: availabilityKeys.detail(variables.id), }); if (data?.doctor_id) { queryClient.invalidateQueries({ queryKey: availabilityKeys.list(data.doctor_id), }); } toast.success("Disponibilidade atualizada com sucesso!"); }, onError: (error: Error, variables, context) => { if (context?.previousAvailability) { queryClient.setQueryData( availabilityKeys.detail(variables.id), context.previousAvailability ); } toast.error(`Erro ao atualizar: ${error.message}`); }, }); } /** * Hook para deletar disponibilidade */ export function useDeleteAvailability() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ id }: { id: string; doctorId: string }) => { return await availabilityService.delete(id); }, onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: availabilityKeys.lists() }); queryClient.invalidateQueries({ queryKey: availabilityKeys.list(variables.doctorId), }); toast.success("Disponibilidade removida com sucesso"); }, onError: (error: Error) => { toast.error(`Erro ao remover: ${error.message}`); }, }); } // ============================================================================ // UTILITY HOOKS // ============================================================================ /** * Hook para prefetch de disponibilidade (otimização) */ export function usePrefetchAvailability() { const queryClient = useQueryClient(); return (doctorId: string) => { queryClient.prefetchQuery({ queryKey: availabilityKeys.list(doctorId), queryFn: async () => { return await availabilityService.list({ doctor_id: doctorId }); }, }); }; } /** * Hook para prefetch de slots disponíveis (navegação de calendário) */ export function usePrefetchAvailableSlots() { const queryClient = useQueryClient(); return (doctorId: string, date: string) => { queryClient.prefetchQuery({ queryKey: availabilityKeys.slots(doctorId, date), queryFn: async () => { await availabilityService.list({ doctor_id: doctorId }); // Lógica simplificada - ver hook useAvailableSlots para implementação completa return []; }, }); }; }