import React, { useState, useEffect } from "react"; import { Clock, Plus, Trash2, Save, Copy, Calendar as CalendarIcon, X } from "lucide-react"; import toast from "react-hot-toast"; import { format } from "date-fns"; import { ptBR } from "date-fns/locale"; import { availabilityService, doctorService } from "../services/index"; import type { DoctorException, DoctorAvailability, } from "../services/availability/types"; import { useAuth } from "../hooks/useAuth"; interface TimeSlot { id: string; dbId?: string; // ID do banco de dados (se já existir) inicio: string; fim: string; ativo: boolean; slotMinutes?: number; appointmentType?: "presencial" | "telemedicina"; } interface DaySchedule { day: string; dayOfWeek: number; enabled: boolean; slots: TimeSlot[]; } const daysOfWeek = [ { key: 0, label: "Domingo", dbKey: "domingo" }, { key: 1, label: "Segunda-feira", dbKey: "segunda" }, { key: 2, label: "Terça-feira", dbKey: "terca" }, { key: 3, label: "Quarta-feira", dbKey: "quarta" }, { key: 4, label: "Quinta-feira", dbKey: "quinta" }, { key: 5, label: "Sexta-feira", dbKey: "sexta" }, { key: 6, label: "Sábado", dbKey: "sabado" }, ]; const DisponibilidadeMedico: React.FC = () => { const { user } = useAuth(); const [doctorId, setDoctorId] = useState(null); const [schedule, setSchedule] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [activeTab, setActiveTab] = useState<"weekly" | "blocked">("weekly"); // States for adding/editing slots const [showAddSlotDialog, setShowAddSlotDialog] = useState(false); const [selectedDay, setSelectedDay] = useState(null); const [newSlot, setNewSlot] = useState({ inicio: "09:00", fim: "10:00", slotMinutes: 30, appointmentType: "presencial" as "presencial" | "telemedicina" }); // States for blocked dates const [selectedDate, setSelectedDate] = useState( new Date() ); const [blockedDates, setBlockedDates] = useState([]); const [exceptions, setExceptions] = useState([]); // States for exceptions form const [showExceptionDialog, setShowExceptionDialog] = useState(false); const [exceptionForm, setExceptionForm] = useState({ date: format(new Date(), "yyyy-MM-dd"), kind: "bloqueio" as "bloqueio" | "disponibilidade_extra", start_time: "09:00", end_time: "18:00", wholeDayBlock: true, reason: "", }); // Load doctor ID from doctors table useEffect(() => { const loadDoctorId = async () => { if (!user?.id) return; try { const doctors = await doctorService.list({ user_id: user.id }); if (doctors.length > 0) { setDoctorId(doctors[0].id); } } catch (error) { console.error("Erro ao buscar ID do médico:", error); } }; loadDoctorId(); }, [user?.id]); const loadAvailability = React.useCallback(async () => { if (!doctorId) return; try { setLoading(true); const availabilities = await availabilityService.list({ doctor_id: doctorId, }); if (availabilities && availabilities.length > 0) { const newSchedule: Record = {}; // Inicializar todos os dias daysOfWeek.forEach(({ key, label }) => { newSchedule[key] = { day: label, dayOfWeek: key, enabled: false, slots: [], }; }); // Agrupar disponibilidades por dia da semana availabilities.forEach((avail: DoctorAvailability) => { // avail.weekday agora é um número (0-6) const dayKey = avail.weekday; if (!newSchedule[dayKey]) return; if (!newSchedule[dayKey].enabled) { newSchedule[dayKey].enabled = true; } newSchedule[dayKey].slots.push({ id: `${dayKey}-${avail.id || Math.random().toString(36).slice(2)}`, dbId: avail.id, // Armazenar ID do banco inicio: avail.start_time?.slice(0, 5) || "09:00", fim: avail.end_time?.slice(0, 5) || "17:00", ativo: avail.active ?? true, }); }); setSchedule(newSchedule); } else { // Initialize empty schedule const newSchedule: Record = {}; daysOfWeek.forEach(({ key, label }) => { newSchedule[key] = { day: label, dayOfWeek: key, enabled: false, slots: [], }; }); setSchedule(newSchedule); } } catch (error) { console.error("Erro ao carregar disponibilidade:", error); toast.error("Erro ao carregar disponibilidade"); } finally { setLoading(false); } }, [doctorId]); const loadExceptions = React.useCallback(async () => { if (!doctorId) return; try { const exceptions = await availabilityService.listExceptions({ doctor_id: doctorId, }); setExceptions(exceptions); const blocked = exceptions .filter((exc: DoctorException) => exc.kind === "bloqueio" && exc.date) .map((exc: DoctorException) => new Date(exc.date!)); setBlockedDates(blocked); } catch (error) { console.error("Erro ao carregar exceções:", error); } }, [doctorId]); useEffect(() => { if (doctorId) { loadAvailability(); loadExceptions(); } }, [doctorId, loadAvailability, loadExceptions]); const toggleDay = (dayKey: number) => { setSchedule((prev) => ({ ...prev, [dayKey]: { ...prev[dayKey], enabled: !prev[dayKey].enabled, }, })); }; const addTimeSlot = () => { if (selectedDay !== null) { const newSlotId = `${selectedDay}-${Date.now()}`; setSchedule((prev) => ({ ...prev, [selectedDay]: { ...prev[selectedDay], slots: [ ...prev[selectedDay].slots, { id: newSlotId, inicio: newSlot.inicio, fim: newSlot.fim, ativo: true, }, ], }, })); setShowAddSlotDialog(false); setNewSlot({ inicio: "09:00", fim: "10:00", slotMinutes: 30, appointmentType: "presencial" }); setSelectedDay(null); } }; const removeTimeSlot = async (dayKey: number, slotId: string) => { const slot = schedule[dayKey]?.slots.find((s) => s.id === slotId); // Se o slot tem um ID do banco, deletar imediatamente if (slot?.dbId) { try { await availabilityService.delete(slot.dbId); toast.success("Horário removido com sucesso"); } catch (error) { console.error("Erro ao remover horário:", error); toast.error("Erro ao remover horário"); return; } } // Atualizar o estado local setSchedule((prev) => ({ ...prev, [dayKey]: { ...prev[dayKey], slots: prev[dayKey].slots.filter((slot) => slot.id !== slotId), }, })); }; const toggleSlotAvailability = (dayKey: number, slotId: string) => { setSchedule((prev) => ({ ...prev, [dayKey]: { ...prev[dayKey], slots: prev[dayKey].slots.map((slot) => slot.id === slotId ? { ...slot, ativo: !slot.ativo } : slot ), }, })); }; const copySchedule = (fromDay: number) => { const sourceSchedule = schedule[fromDay]; if (!sourceSchedule.enabled || sourceSchedule.slots.length === 0) { toast.error("Dia não tem horários configurados"); return; } const updatedSchedule = { ...schedule }; Object.keys(updatedSchedule).forEach((key) => { const dayKey = Number(key); if (dayKey !== fromDay && updatedSchedule[dayKey].enabled) { updatedSchedule[dayKey].slots = sourceSchedule.slots.map((slot) => ({ ...slot, id: `${dayKey}-${slot.id}`, })); } }); setSchedule(updatedSchedule); toast.success("Horários copiados com sucesso!"); }; const handleSaveSchedule = async () => { try { setSaving(true); if (!doctorId) { toast.error("Médico não autenticado"); return; } const requests: Array> = []; const timeToMinutes = (t: string) => { const [hStr, mStr] = t.split(":"); const h = Number(hStr || "0"); const m = Number(mStr || "0"); return h * 60 + m; }; // Para cada dia, processar slots daysOfWeek.forEach(({ key }) => { const daySchedule = schedule[key]; if (!daySchedule || !daySchedule.enabled) { // Se o dia foi desabilitado, deletar todos os slots existentes daySchedule?.slots.forEach((slot) => { if (slot.dbId) { requests.push(availabilityService.delete(slot.dbId)); } }); return; } // Processar cada slot do dia daySchedule.slots.forEach((slot) => { const inicio = slot.inicio ? slot.inicio.length === 5 ? `${slot.inicio}:00` : slot.inicio : "00:00:00"; const fim = slot.fim ? slot.fim.length === 5 ? `${slot.fim}:00` : slot.fim : "00:00:00"; const minutes = Math.max( 1, timeToMinutes(fim.slice(0, 5)) - timeToMinutes(inicio.slice(0, 5)) ); const payload = { weekday: key, // Agora usa número (0-6) ao invés de string start_time: inicio.slice(0, 5), // HH:MM ao invés de HH:MM:SS end_time: fim.slice(0, 5), // HH:MM ao invés de HH:MM:SS slot_minutes: minutes, appointment_type: "presencial" as const, active: !!slot.ativo, }; if (slot.dbId) { // Atualizar slot existente requests.push(availabilityService.update(slot.dbId, payload as any)); } else { // Criar novo slot requests.push( availabilityService.create({ doctor_id: doctorId, ...payload, } as any) ); } }); }); if (requests.length === 0) { toast.error("Nenhuma alteração para salvar"); return; } const results = await Promise.allSettled(requests); const errors: string[] = []; let successCount = 0; results.forEach((r, idx) => { if (r.status === "fulfilled") { const val = r.value as { success?: boolean; error?: string; message?: string; }; if (val && val.success) successCount++; else errors.push(`Item ${idx}: ${val?.error || val?.message || "Erro"}`); } else { errors.push(`Item ${idx}: ${r.reason?.message || String(r.reason)}`); } }); if (errors.length > 0) { console.error("Erros ao salvar disponibilidades:", errors); toast.error( `Algumas disponibilidades não foram salvas (${errors.length})` ); } if (successCount > 0) { toast.success(`${successCount} alteração(ões) salvas com sucesso!`); await loadAvailability(); } } catch (error) { console.error("Erro ao salvar disponibilidade:", error); const errorMessage = error instanceof Error ? error.message : "Erro ao salvar disponibilidade"; toast.error(errorMessage); } finally { setSaving(false); } }; const toggleBlockedDate = async () => { if (!selectedDate) return; const dateString = format(selectedDate, "yyyy-MM-dd"); const dateExists = blockedDates.some( (d) => format(d, "yyyy-MM-dd") === dateString ); try { if (dateExists) { // Remove block const exception = exceptions.find( (exc) => exc.date && format(new Date(exc.date), "yyyy-MM-dd") === dateString ); if (exception && exception.id) { await availabilityService.deleteException(exception.id); setBlockedDates( blockedDates.filter((d) => format(d, "yyyy-MM-dd") !== dateString) ); toast.success("Data desbloqueada"); } } else { // Add block await availabilityService.createException({ doctor_id: doctorId!, date: dateString, kind: "bloqueio", reason: "Data bloqueada pelo médico", created_by: user?.id || doctorId!, }); setBlockedDates([...blockedDates, selectedDate]); toast.success("Data bloqueada"); } loadExceptions(); } catch (error) { console.error("Erro ao alternar bloqueio de data:", error); toast.error("Erro ao bloquear/desbloquear data"); } }; if (loading) { return (
); } return (
{/* Header */}

Gerenciar Disponibilidade

Configure seus horários de atendimento

{/* Tabs */}
{/* Tab Content - Weekly Schedule */} {activeTab === "weekly" && (

Horários por Dia da Semana

Defina seus horários de atendimento para cada dia da semana

{daysOfWeek.map(({ key, label }) => (
{label} {schedule[key]?.enabled && ( {schedule[key]?.slots.length || 0} horário(s) )}
{schedule[key]?.enabled && (
)}
{schedule[key]?.enabled && (
{schedule[key]?.slots.length === 0 ? (

Nenhum horário configurado

) : ( schedule[key]?.slots.map((slot) => (
{slot.inicio} - {slot.fim} {!slot.ativo && ( Bloqueado )}
)) )}
)}
))}
)} {/* Tab Content - Blocked Dates */} {activeTab === "blocked" && (

Selecionar Datas

Clique em uma data no calendário e depois no botão para bloquear/desbloquear

setSelectedDate(new Date(e.target.value))} className="form-input" />

Datas Bloqueadas

{blockedDates.length} data(s) bloqueada(s)

{blockedDates.length === 0 ? (

Nenhuma data bloqueada

) : (
{blockedDates.map((date, index) => (
{format(date, "EEEE, dd 'de' MMMM 'de' yyyy", { locale: ptBR, })}
))}
)}
)} {/* Add Time Slot Dialog */} {showAddSlotDialog && (

Adicionar Horário

Defina o período de atendimento para{" "} {selectedDay !== null ? schedule[selectedDay]?.day : ""}

setNewSlot({ ...newSlot, inicio: e.target.value }) } className="form-input" />
setNewSlot({ ...newSlot, fim: e.target.value }) } className="form-input" />
)}
); }; export default DisponibilidadeMedico;