import React, { useState, useEffect, useCallback, useRef } from 'react'; import FullCalendar from '@fullcalendar/react'; import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; import interactionPlugin from '@fullcalendar/interaction'; import { Calendar as CalendarIcon, Plus, X, Loader2, GripVertical, Settings as GearIcon, Edit, Trash2, CalendarRange, Clock, User, Stethoscope, AlertCircle, CheckCircle2, Bell, CalendarClock, History, Download, PanelLeftClose, PanelLeftOpen } from 'lucide-react'; import { AgendaSettingsModal } from '../components/AgendaSettingsModal.tsx'; import { AgendaDetailModal, ScoreBadge, FamiliaChips } from '../components/AgendaDetailModal.tsx'; import { WhatsChatDrawer } from './newwhats/WhatsChatDrawer.tsx'; import { AgendaPresence } from '../components/AgendaPresence.tsx'; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; import { HybridBackend } from '../services/backend.ts'; import { Realtime } from '../services/realtime.ts'; import { Dentista, Agendamento, Paciente, DentistaHorario } from '../types.ts'; import { useGTOStore } from './LancarGTO.tsx'; import { useHybridBackend } from '../hooks/useHybridBackend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; // --- Reusable Debounce Hook --- function useDebounce(value: string, delay: number) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(handler); }, [value, delay]); return debouncedValue; } // --- Advanced Appointment Modal Component --- const AppointmentModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: () => void; dentists: Dentista[] | null; initialDate?: string; initialAppointment?: Agendamento | null; initialPatient?: { id?: string; nome?: string } | null; }> = ({ isOpen, onClose, onSave, dentists, initialDate, initialAppointment, initialPatient }) => { const [selectedPatient, setSelectedPatient] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [duration, setDuration] = useState(15); const [selectedDate, setSelectedDate] = useState(initialDate ? initialDate.split('T')[0] : new Date().toISOString().split('T')[0]); const [selectedTime, setSelectedTime] = useState(initialDate ? new Date(initialDate).toTimeString().substring(0, 5) : ''); const [selectedDentistId, setSelectedDentistId] = useState(dentists?.[0]?.id); const [procedimento, setProcedimento] = useState(''); const [isSearchFocused, setIsSearchFocused] = useState(false); // Fase 4 — conflito de horário → painel de interesse const [conflito, setConflito] = useState(null); const [tempoEspera, setTempoEspera] = useState(60); const [registrando, setRegistrando] = useState(false); const debouncedSearchTerm = useDebounce(searchTerm, 400); const fetcher = useCallback(() => HybridBackend.getPacientes(debouncedSearchTerm).then(r => Array.isArray(r.data) ? r.data : []), [debouncedSearchTerm]); const { data: searchResults, isLoading: isSearching } = useHybridBackend(fetcher, [debouncedSearchTerm]); const { data: agendamentos, refresh: refreshAgendamentos } = useHybridBackend(HybridBackend.getAgendamentos); const activeWorkspace = HybridBackend.getActiveWorkspace(); const { data: allHorarios } = useHybridBackend(() => selectedDentistId && activeWorkspace ? HybridBackend.getHorariosByDentista(selectedDentistId, activeWorkspace.id) : Promise.resolve([]), [selectedDentistId, activeWorkspace?.id] ); const toast = useToast(); useEffect(() => { if (isOpen) { setConflito(null); if (initialAppointment) { setSelectedPatient({ nome: initialAppointment.pacienteNome, cpf: '', id: 'temp' } as any); const startDate = new Date(initialAppointment.start); const endDate = new Date(initialAppointment.end); setSelectedDate(startDate.toISOString().split('T')[0]); setSelectedTime(startDate.toTimeString().substring(0, 5)); setDuration((endDate.getTime() - startDate.getTime()) / 60000); setSelectedDentistId(initialAppointment.dentistaId); setProcedimento(initialAppointment.procedimento); } else { // Reagendar (a partir da lista "a reagendar"): pré-preenche o paciente. if (initialPatient?.nome) { setSelectedPatient({ nome: initialPatient.nome, cpf: '', id: initialPatient.id || 'temp' } as any); } else { setSelectedPatient(null); } setSearchTerm(''); setSelectedDate(initialDate ? initialDate.split('T')[0] : new Date().toISOString().split('T')[0]); setSelectedTime(initialDate ? new Date(initialDate).toTimeString().substring(0, 5) : ''); setProcedimento(''); setDuration(15); if (dentists && dentists.length > 0) { setSelectedDentistId(dentists[0].id); } } } }, [isOpen, initialDate, dentists, initialAppointment, initialPatient]); useEffect(() => { if (!isOpen) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, onClose]); const selectedDentist = dentists?.find(d => d.id === selectedDentistId); const timeSlots = useCallback(() => { const slots: string[] = []; if (!selectedDentistId || !selectedDate) return slots; const existingAppointments = (agendamentos ?? []).filter(ag => ag.dentistaId === selectedDentistId && new Date(ag.start).toISOString().split('T')[0] === selectedDate && ag.id !== initialAppointment?.id ).map(ag => ({ start: new Date(ag.start).getHours() * 60 + new Date(ag.start).getMinutes(), end: new Date(ag.end).getHours() * 60 + new Date(ag.end).getMinutes(), })); const dayOfWeek = new Date(selectedDate).getUTCDay(); const dayHorarios = allHorarios?.filter(h => h.dia_semana === dayOfWeek && h.ativo) || []; if (dayHorarios.length === 0) { // Default 8-18 fallback if no hours set for (let time = 8 * 60; time < 18 * 60; time += 15) { const isOccupied = existingAppointments.some(app => time >= app.start && time < app.end); if (!isOccupied) { const hour = Math.floor(time / 60), minute = time % 60; slots.push(`${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`); } } } else { for (const h of dayHorarios) { const [hStart, mStart] = h.hora_inicio.split(':').map(Number); const [hEnd, mEnd] = h.hora_fim.split(':').map(Number); for (let time = hStart * 60 + mStart; time < hEnd * 60 + mEnd; time += 15) { const isOccupied = existingAppointments.some(app => time >= app.start && time < app.end); if (!isOccupied) { const hour = Math.floor(time / 60), minute = time % 60; slots.push(`${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`); } } } } // If current time is not on the 15m grid (like 12:03), add it as a valid slot if (selectedTime && !slots.includes(selectedTime)) { slots.push(selectedTime); slots.sort(); } return slots; }, [selectedDentistId, selectedDate, agendamentos, initialAppointment, selectedTime, allHorarios])(); const handlePatientSelect = (patient: Paciente) => { setSelectedPatient(patient); setSearchTerm(''); setIsSearchFocused(false); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedPatient || !selectedDate || !selectedTime || !selectedDentistId || !procedimento) { toast.error("PREENCHA TODOS OS CAMPOS PARA AGENDAR."); return; } const startDateTime = new Date(`${selectedDate}T${selectedTime}:00`); const endDateTime = new Date(startDateTime.getTime() + duration * 60000); const agData: any = { pacienteNome: selectedPatient.nome, dentistaId: selectedDentistId, start: startDateTime.toISOString(), end: endDateTime.toISOString(), status: initialAppointment?.status || 'agendado', procedimento: procedimento.toUpperCase(), }; // Vincula o paciente real (id != 'temp') → habilita faltas, família e auto-baixa de pendência. if (selectedPatient.id && selectedPatient.id !== 'temp') agData.pacienteId = selectedPatient.id; try { if (initialAppointment?.id) { await HybridBackend.atualizarAgendamento(initialAppointment.id, agData); toast.success(`AGENDAMENTO DE ${selectedPatient.nome} ATUALIZADO!`); } else { await HybridBackend.salvarAgendamento(agData); toast.success(`AGENDAMENTO PARA ${selectedPatient.nome} SALVO!`); } refreshAgendamentos(); onSave(); } catch (err: any) { if (err?.conflito) { setConflito({ ocupado: err.ocupado, agData }); // abre painel de interesse } else { toast.error(err?.message || "ERRO AO SALVAR AGENDAMENTO."); } } }; const handleRegistrarInteresse = async () => { if (!conflito) return; setRegistrando(true); try { const currentUserId = (() => { try { return JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}').id; } catch { return undefined; } })(); await HybridBackend.registrarInteresse({ dentistaId: conflito.agData.dentistaId, clinicaInteressadaId: activeWorkspace?.id, solicitanteUsuarioId: currentUserId, start: conflito.agData.start, end: conflito.agData.end, tempoEsperaMin: tempoEspera || undefined, }); toast.success("INTERESSE REGISTRADO! VOCÊ SERÁ AVISADO SE O HORÁRIO LIBERAR."); setConflito(null); onSave(); } catch (e: any) { toast.error(e?.message || "ERRO AO REGISTRAR INTERESSE."); } finally { setRegistrando(false); } }; if (!isOpen) return null; return (
{conflito && (
setConflito(null)}>
e.stopPropagation()}>

Horário ocupado

O profissional já tem compromisso nesse horário (em qualquer clínica). Você pode registrar interesse e ser avisado se o horário liberar, ou escolher outro horário.

setTempoEspera(parseInt(e.target.value) || 0)} className="w-full px-3 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-bold outline-none focus:border-teal-400" />

0 = sem expiração (aguarda até liberar).

)}

AGENDAR CONSULTA

{!selectedPatient ? (
setSearchTerm(e.target.value.toUpperCase())} onFocus={() => setIsSearchFocused(true)} onBlur={() => setTimeout(() => setIsSearchFocused(false), 200)} placeholder="NOME, CPF OU TELEFONE..." className="w-full lg:w-1/2 bg-gray-100 p-3 lg:p-4 rounded-lg font-bold text-gray-800 uppercase focus:border-teal-500 outline-none border-2 border-transparent focus:border-2 text-sm lg:text-base" /> {isSearchFocused && debouncedSearchTerm && (
    {isSearching &&
  • BUSCANDO...
  • } {searchResults?.map(p => (
  • handlePatientSelect(p)} className="p-3 hover:bg-gray-100 cursor-pointer uppercase font-semibold text-sm"> {p.nome} {p.cpf && · CPF {p.cpf}} {p.telefone && · TEL {p.telefone}}
  • ))}
)}
) : (
{selectedPatient.nome}
{selectedPatient.id && selectedPatient.id !== 'temp' ? : null}
{selectedPatient.id && selectedPatient.id !== 'temp' ? ( setSelectedPatient({ nome: m.nome, cpf: '', id: m.id } as any)} /> ) : null}
)}
{selectedPatient && (
setSelectedDate(e.target.value)} className="w-full mt-2 border border-gray-300 bg-white text-gray-800 rounded-lg p-2.5 lg:p-3 text-sm" />
setProcedimento(e.target.value.toUpperCase())} placeholder="EX: AVALIAÇÃO, LIMPEZA..." className="w-full mt-2 border border-gray-300 bg-white text-gray-800 rounded-lg p-2.5 lg:p-3 uppercase-input placeholder-gray-400 text-sm" />
{timeSlots.map(slot => ( ))} {timeSlots.length === 0 &&

NENHUM HORÁRIO DISPONÍVEL.

}
)}
); }; export const AgendaView: React.FC<{ onNavigate?: (view: string) => void; sidebarCollapsed?: boolean; onToggleSidebar?: () => void }> = ({ onNavigate, sidebarCollapsed, onToggleSidebar }) => { const { data: agendamentos, refresh } = useHybridBackend(HybridBackend.getAgendamentos); const { data: dentists, refresh: refreshDentists } = useHybridBackend(HybridBackend.getDentistas); const { data: googleEvents, refresh: refreshGoogle } = useHybridBackend(HybridBackend.getGoogleEvents); const [events, setEvents] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [selectedDateInfo, setSelectedDateInfo] = useState(null); const [selectedAppointment, setSelectedAppointment] = useState(null); const [showDetails, setShowDetails] = useState(false); const [whatsTarget, setWhatsTarget] = useState<{ pacienteId: string; pacienteNome: string } | null>(null); const [interesses, setInteresses] = useState([]); const [showInteresses, setShowInteresses] = useState(false); const [pendencias, setPendencias] = useState([]); const [showPendencias, setShowPendencias] = useState(false); const [reagendarPatient, setReagendarPatient] = useState<{ id?: string; nome?: string } | null>(null); const [atividade, setAtividade] = useState([]); const [showAtividade, setShowAtividade] = useState(false); const toast = useToast(); // Lista "a reagendar" (faltas/cancelamentos/remarcações pendentes desta clínica) const loadPendencias = useCallback(async () => { try { setPendencias(await HybridBackend.getPendenciasReagendar()); } catch { /* ignore */ } }, []); useEffect(() => { loadPendencias(); }, [loadPendencias]); const loadAtividade = useCallback(async () => { try { setAtividade(await HybridBackend.getAtividadeAgenda()); } catch { /* ignore */ } }, []); const [isMobile, setIsMobile] = useState(typeof window !== 'undefined' && window.innerWidth < 640); useEffect(() => { const onResize = () => setIsMobile(window.innerWidth < 640); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); const [importando, setImportando] = useState(false); const handleImportarGoogle = async () => { if (!confirm('Importar os agendamentos do Google (última semana + próximos) para o sistema? Eles passam a ser editáveis aqui.')) return; setImportando(true); try { const r = await HybridBackend.importarGoogleAgenda(); if (!r.success) { toast.error(r.message || 'ERRO AO IMPORTAR.'); return; } toast.success(`IMPORTADOS: ${r.criados ?? 0} (${r.pulados ?? 0} já existiam).`); refresh(); refreshGoogle(); } catch { toast.error('ERRO AO IMPORTAR.'); } finally { setImportando(false); } }; // Fase 4 Parte 2 — o dentista vê os pedidos de interesse na própria agenda const meuUsuarioId = (() => { try { return JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}').id; } catch { return undefined; } })(); const souDentista = HybridBackend.getCurrentRole() === 'dentista'; const loadInteresses = useCallback(async () => { if (!souDentista || !meuUsuarioId) return; try { setInteresses(await HybridBackend.getInteresses({ profissionalId: meuUsuarioId })); } catch { /* ignore */ } }, [souDentista, meuUsuarioId]); useEffect(() => { loadInteresses(); }, [loadInteresses]); const interessesAguardando = interesses.filter(i => i.status === 'aguardando').length; // Atualização quase em tempo real: refaz o fetch a cada 20s e ao voltar o foco // à aba/janela, para que agendamentos criados por outra pessoa apareçam sem reload. const liveRef = useRef({ refresh, refreshGoogle, loadPendencias, loadInteresses }); liveRef.current = { refresh, refreshGoogle, loadPendencias, loadInteresses }; // Tempo real (WebSocket): ao receber aviso de mudança, refaz o fetch na hora. // Coalesce vários avisos próximos (ex.: import em lote) num único refetch. const rtDebounce = useRef | null>(null); useEffect(() => { const unsub = Realtime.subscribe((msg) => { if (msg?.type !== 'agenda') return; if (rtDebounce.current) clearTimeout(rtDebounce.current); rtDebounce.current = setTimeout(() => { liveRef.current.refresh(); liveRef.current.loadPendencias(); liveRef.current.loadInteresses(); }, 350); }); return () => { unsub(); if (rtDebounce.current) clearTimeout(rtDebounce.current); }; }, []); // Fallback (caso o WebSocket caia): refetch leve a cada 60s e ao focar a aba. const tickCount = useRef(0); useEffect(() => { const tick = (force?: boolean) => { if (document.visibilityState === 'hidden') return; liveRef.current.refresh(); liveRef.current.loadPendencias(); liveRef.current.loadInteresses(); tickCount.current += 1; if (force || tickCount.current % 2 === 0) liveRef.current.refreshGoogle(); }; const iv = setInterval(() => tick(), 60000); const onFocus = () => tick(true); window.addEventListener('focus', onFocus); document.addEventListener('visibilitychange', onFocus); return () => { clearInterval(iv); window.removeEventListener('focus', onFocus); document.removeEventListener('visibilitychange', onFocus); }; }, []); useEffect(() => { if (agendamentos && dentists) { const currentRole = HybridBackend.getCurrentRole(); const currentUserEmail = HybridBackend.getCurrentUser(); const isDentist = currentRole === 'dentista'; const loginedDentistId = dentists.find(d => d.email === currentUserEmail)?.id; let filteredAgendamentos = agendamentos; if (isDentist) { filteredAgendamentos = agendamentos.filter(ag => ag.dentistaId === loginedDentistId); } const mappedEvents = filteredAgendamentos.map(ag => { const dentist = dentists.find(d => d.id === ag.dentistaId); const st = String(ag.status || '').toLowerCase(); const faltou = st === 'falta' || st === 'faltou'; const inativo = st === 'cancelado' || st === 'remarcar'; // Prioridade: status (falta/cancelado) > cor própria do agendamento (ex.: importada do Google) > cor do dentista. const cor = faltou ? '#ef4444' : inativo ? '#cbd5e1' : ((ag as any).cor || (dentist ? dentist.corAgenda : '#94a3b8')); const nomePac = (ag as any).pacienteNome || (ag as any).pacientenome || ''; return { id: ag.id, title: `${nomePac} - ${ag.procedimento}`, start: ag.start, end: ag.end, backgroundColor: cor, borderColor: cor, extendedProps: { ...ag, pacienteNome: nomePac, isGoogle: false, dentistaNome: dentist?.nome } }; }); // Add Google events if any — com cores: usa a cor real (colorId/agenda) e, // quando não houver, escolhe uma cor NÃO usada na agenda (consistente por origem). if (googleEvents && googleEvents.length > 0) { let filteredGoogleEvents = googleEvents; if (isDentist) { filteredGoogleEvents = googleEvents.filter(ev => ev?.extendedProps?.owner_id === currentUserEmail); } const PALETTE = ['#2563eb', '#7c3aed', '#db2777', '#059669', '#d97706', '#0891b2', '#dc2626', '#4f46e5', '#0d9488', '#ca8a04', '#9333ea', '#e11d48', '#16a34a', '#f97316']; const usadas = new Set(mappedEvents.map(e => e.backgroundColor).filter(Boolean)); const corPorOrigem: Record = {}; const proximaCor = () => PALETTE.find(c => !usadas.has(c)) || PALETTE[Object.keys(corPorOrigem).length % PALETTE.length]; const coloridos = filteredGoogleEvents.map(ev => { let cor = ev.backgroundColor || null; if (cor) { usadas.add(cor); } else { const origem = ev.owner || 'google'; if (!corPorOrigem[origem]) { const c = proximaCor(); corPorOrigem[origem] = c; usadas.add(c); } cor = corPorOrigem[origem]; } return { ...ev, backgroundColor: cor, borderColor: cor }; }); mappedEvents.push(...coloridos); } setEvents(mappedEvents); } }, [agendamentos, dentists, googleEvents]); const handleDateClick = (arg: any) => { if ((HybridBackend.getActiveWorkspace() as any)?.role === 'dentista') return; setReagendarPatient(null); setSelectedAppointment(null); setSelectedDateInfo(arg); setIsModalOpen(true); }; // Selecionar (clicar/arrastar) um intervalo livre também abre o modal de novo agendamento. const handleDateSelect = (info: any) => { if ((HybridBackend.getActiveWorkspace() as any)?.role === 'dentista') return; setReagendarPatient(null); setSelectedAppointment(null); setSelectedDateInfo({ dateStr: info.startStr }); setIsModalOpen(true); }; const handleReagendarPendencia = (p: any) => { setShowPendencias(false); setSelectedAppointment(null); setReagendarPatient({ id: p.paciente_id || undefined, nome: p.paciente_nome }); setSelectedDateInfo({ dateStr: new Date().toISOString() }); setIsModalOpen(true); }; const handleDispensarPendencia = async (p: any) => { if (!confirm(`Dispensar "${p.paciente_nome}" da lista de reagendamento?`)) return; try { await HybridBackend.resolverPendencia(p.id, 'dispensado'); toast.success('PENDÊNCIA DISPENSADA.'); loadPendencias(); } catch { toast.error('ERRO AO DISPENSAR.'); } }; const handleEventClick = (info: any) => { if (info.event.extendedProps.isGoogle) { toast.info(`EVENTO DO GOOGLE CALENDAR: ${info.event.title}`); return; } const ag = info.event.extendedProps as Agendamento; setSelectedAppointment(ag); setShowDetails(true); }; const handleUpdateStatus = async (id: string, status: Agendamento['status']) => { try { await HybridBackend.atualizarAgendamento(id, { status }); toast.success(`STATUS ATUALIZADO PARA ${status.toUpperCase()}`); refresh(); setShowDetails(false); } catch { toast.error("ERRO AO ATUALIZAR STATUS."); } }; const handleDeleteAppointment = async (id: string) => { if (!confirm("DESEJA REALMENTE EXCLUIR ESTE AGENDAMENTO?")) return; try { await HybridBackend.excluirAgendamento(id); toast.success("AGENDAMENTO EXCLUÍDO!"); refresh(); setShowDetails(false); } catch { toast.error("ERRO AO EXCLUIR AGENDAMENTO."); } }; const handleSaveAppointment = () => { refresh(); refreshGoogle(); loadPendencias(); setIsModalOpen(false); setSelectedAppointment(null); setReagendarPatient(null); }; const onDragEnd = async (result: any) => { if (!result.destination || !dentists) return; const items = Array.from(dentists); const [reorderedItem] = items.splice(result.source.index, 1); items.splice(result.destination.index, 0, reorderedItem); const updatedOrders = items.map((item, index) => ({ id: item.id, ordem: index })); try { await HybridBackend.reorderDentistas(updatedOrders); toast.success("ORDEM DOS DENTISTAS ATUALIZADA!"); refreshDentists(); } catch { toast.error("ERRO AO REORDENAR DENTISTAS."); } }; const currentRole = HybridBackend.getCurrentRole(); const currentUserEmail = HybridBackend.getCurrentUser(); const gtoStore = useGTOStore(); const handleAtender = async () => { if (!selectedAppointment || !onNavigate) return; try { const result = await HybridBackend.getPacientes(selectedAppointment.pacienteNome); const list = Array.isArray(result) ? result : (result?.data ?? []); if (list.length > 0) { gtoStore.setPaciente(list[0]); } else { gtoStore.setPendingSearch(selectedAppointment.pacienteNome); } } catch { gtoStore.setPendingSearch(selectedAppointment.pacienteNome); } setShowDetails(false); onNavigate('lancar-gto'); }; const filteredDentists = dentists?.filter(d => { if (currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') return true; if (currentRole === 'dentista') return d.email === currentUserEmail; return false; }); return (
{/* Coluna de controles (desktop: lateral esquerda vertical; mobile: topo compacto) */}
{ const isGoogle = eventInfo.event.extendedProps.isGoogle; return (
{eventInfo.timeText}
{isGoogle ? (
{eventInfo.event.title}
) : ( <>
{eventInfo.event.extendedProps.pacienteNome}
{eventInfo.event.extendedProps.procedimento}
)}
); }} />
{/* Detalhes do agendamento — modal em pilha (autoria, faltas, família, histórico, ações) */} { setShowDetails(false); setSelectedAppointment(null); }} appointment={selectedAppointment} dentists={dentists} onChanged={() => { refresh(); refreshGoogle(); loadPendencias(); }} onEditar={() => { setShowDetails(false); setIsModalOpen(true); }} onAtender={handleAtender} onVerWhats={(info) => setWhatsTarget(info)} podeAtender={!!onNavigate && currentRole !== 'paciente'} dentistaRestrito={souDentista} /> {/* Drawer de WhatsApp do paciente — clone focado da área de mensagens */} setWhatsTarget(null)} pacienteId={whatsTarget?.pacienteId} pacienteNome={whatsTarget?.pacienteNome} onNavigate={onNavigate} /> {/* Lista "A REAGENDAR" — pendências de falta/cancelamento/remarcação */} {showPendencias && (
setShowPendencias(false)}>
e.stopPropagation()}>

A reagendar ({pendencias.length})

{pendencias.length === 0 ? (

NENHUMA PENDÊNCIA. TUDO EM DIA!

) : pendencias.map(p => { const ML: Record = { falta: 'FALTOU', cancelado: 'CANCELOU', remarcar: 'PEDIU REMARCAÇÃO' }; return (

{p.paciente_nome || 'PACIENTE'}

{ML[p.motivo] || p.motivo} · {new Date(p.created_at).toLocaleDateString('pt-BR')}

); })}
)} {/* Feed de ATIVIDADE da equipe (hoje) */} {showAtividade && (
setShowAtividade(false)}>
e.stopPropagation()}>

Atividade de hoje

{atividade.length === 0 ? (

NENHUMA ATIVIDADE HOJE.

) : atividade.map((a, i) => { const AC: Record = { criou: 'agendou', reagendou: 'reagendou', editou: 'editou', cancelou: 'cancelou', faltou: 'marcou falta de', remarcou: 'pediu remarcação de', restaurou: 'restaurou' }; const pac = a.pacientenome || a.detalhes?.paciente || ''; return (

{a.actor_nome || 'Alguém'} {AC[a.acao] || a.acao}{pac ? {pac} : null}

{new Date(a.at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}

); })}
)} { setIsModalOpen(false); setSelectedAppointment(null); }} onSave={handleSaveAppointment} dentists={dentists} initialDate={selectedDateInfo?.dateStr} initialAppointment={selectedAppointment} initialPatient={reagendarPatient} /> setIsSettingsOpen(false)} dentists={dentists} /> {showInteresses && (
setShowInteresses(false)}>
e.stopPropagation()}>

Pedidos de interesse

{interesses.length === 0 && (

Nenhum pedido de interesse na sua agenda.

)} {interesses.map(i => (

{new Date(i.start_time).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}

{i.status}

Interessada: {i.clinica_nome || i.clinica_interessada_id}

{i.expira_em &&

Aguarda até: {new Date(i.expira_em).toLocaleString('pt-BR')}

}
))}
)}
); };