import React, { useEffect, useState } from 'react'; import { StackModal, StackPage, useStackModal } from './StackModal.tsx'; import { HybridBackend } from '../services/backend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; import { AvaliacaoEditor } from './AvaliacaoEditor.tsx'; import { User, Stethoscope, Clock, CalendarRange, AlertCircle, CheckCircle2, History, RotateCcw, CalendarClock, Ban, Users, Pencil, ClipboardCheck, ShieldAlert, Search, UserPlus, Link2, Trash2, ClipboardList } from 'lucide-react'; interface Props { isOpen: boolean; onClose: () => void; appointment: any | null; dentists: any[] | null; onChanged: () => void; // recarrega a agenda onEditar: () => void; // abre o modal de edição/reagendar (mover de horário) onAtender?: () => void; podeAtender?: boolean; dentistaRestrito?: boolean; // dentista linkado por clínica: modal simples, sem dados sensíveis/agendamento } // Cache simples na sessão do modal (evita refetch ao voltar/avançar). const _cache: Record = {}; const low = (s: any) => String(s || '').toLowerCase(); const STATUS_LABEL: Record = { importado: 'A CONFIRMAR · GOOGLE', agendado: 'AGENDADO', confirmado: 'CONFIRMADO', reagendado: 'REAGENDADO', remarcar: 'A REAGENDAR', cancelado: 'CANCELADO', falta: 'FALTOU', atendido: 'ATENDIDO', pendente: 'PENDENTE', concluido: 'CONCLUÍDO', faltou: 'FALTOU', }; const STATUS_COLOR: Record = { importado: 'bg-violet-50 text-violet-700', cancelado: 'bg-red-50 text-red-600', falta: 'bg-red-50 text-red-600', faltou: 'bg-red-50 text-red-600', remarcar: 'bg-amber-50 text-amber-600', confirmado: 'bg-emerald-50 text-emerald-600', atendido: 'bg-emerald-50 text-emerald-600', concluido: 'bg-emerald-50 text-emerald-600', }; export const FamiliaChips: React.FC<{ pacienteId: string; onPick?: (m: any) => void }> = ({ pacienteId, onPick }) => { const [membros, setMembros] = useState(_cache[pacienteId]?.familia?.membros ?? null); useEffect(() => { if (_cache[pacienteId]?.familia) { setMembros(_cache[pacienteId].familia!.membros); return; } let alive = true; HybridBackend.getFamiliaPaciente(pacienteId).then(f => { _cache[pacienteId] = { ..._cache[pacienteId], familia: f }; if (alive) setMembros(f.membros); }).catch(() => alive && setMembros([])); return () => { alive = false; }; }, [pacienteId]); if (!membros || membros.length <= 1) return null; return (
{membros.map(m => { const inner = (<>{m.nome}{m.grupofamiliarrelacao ? · {m.grupofamiliarrelacao} : null}); const cls = 'shrink-0 inline-flex items-center gap-1 bg-emerald-50 text-emerald-700 rounded-full px-2.5 py-1 text-[10px] font-bold uppercase whitespace-nowrap'; return onPick ? : {inner}; })}
); }; export const FaltasBadge: React.FC<{ pacienteId: string }> = ({ pacienteId }) => { const [n, setN] = useState(_cache[pacienteId]?.faltas ?? null); useEffect(() => { if (typeof _cache[pacienteId]?.faltas === 'number') { setN(_cache[pacienteId].faltas!); return; } let alive = true; HybridBackend.getFaltasPaciente(pacienteId).then(v => { _cache[pacienteId] = { ..._cache[pacienteId], faltas: v }; if (alive) setN(v); }).catch(() => alive && setN(0)); return () => { alive = false; }; }, [pacienteId]); if (!n) return null; return ( {n} falta{n > 1 ? 's' : ''} ); }; // Selo de SCORE do paciente: nível (ATENÇÃO/RISCO) + contadores (faltas · remarcações). // Não aparece quando o paciente está limpo (0 faltas e 0 remarcações). export const ScoreBadge: React.FC<{ pacienteId: string }> = ({ pacienteId }) => { const [sc, setSc] = useState<{ faltas: number; remarcacoes: number; nivel: string } | null>(_cache[pacienteId]?.score ?? null); useEffect(() => { if (_cache[pacienteId]?.score) { setSc(_cache[pacienteId].score!); return; } let alive = true; HybridBackend.getScorePaciente(pacienteId).then(s => { _cache[pacienteId] = { ..._cache[pacienteId], score: s }; if (alive) setSc(s); }).catch(() => { }); return () => { alive = false; }; }, [pacienteId]); if (!sc || (sc.faltas === 0 && sc.remarcacoes === 0)) return null; const cor = sc.nivel === 'RISCO' ? 'bg-red-50 text-red-700' : 'bg-amber-50 text-amber-700'; const label = sc.nivel === 'RISCO' ? 'RISCO' : 'ATENÇÃO'; const parts: string[] = []; if (sc.faltas) parts.push(`${sc.faltas} falta${sc.faltas > 1 ? 's' : ''}`); if (sc.remarcacoes) parts.push(`${sc.remarcacoes} remarc.`); return ( {label} · {parts.join(' · ')} ); }; const HistoryPage: React.FC<{ appointmentId: string }> = ({ appointmentId }) => { const [hist, setHist] = useState(null); useEffect(() => { let alive = true; HybridBackend.getHistoricoAgendamento(appointmentId).then(h => alive && setHist(h)).catch(() => alive && setHist([])); return () => { alive = false; }; }, [appointmentId]); if (hist === null) return

CARREGANDO...

; if (!hist.length) return

SEM HISTÓRICO.

; const ACAO: Record = { criou: 'criou', reagendou: 'reagendou', editou: 'editou', cancelou: 'cancelou', faltou: 'marcou falta', remarcou: 'pediu remarcação', restaurou: 'restaurou' }; return (
{hist.map((h, i) => (
{i < hist.length - 1 &&
}

{h.actor_nome || 'Alguém'} {ACAO[h.acao] || h.acao}

{new Date(h.at).toLocaleString('pt-BR')}

{h.detalhes?.de && h.detalhes?.para && (

{new Date(h.detalhes.de.start).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })} {' → '} {new Date(h.detalhes.para.start).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}

)}
))}
); }; // Página de CONFIRMAÇÃO de um importado: busca/desambigua paciente, exige CPF+telefone, vincula. const ConfirmImportadoPage: React.FC<{ ag: any; onConfirmed: () => void }> = ({ ag, onConfirmed }) => { const { close } = useStackModal(); const toast = useToast(); const [q, setQ] = useState(''); const [results, setResults] = useState([]); const [sel, setSel] = useState(null); const [novo, setNovo] = useState(false); const [nome, setNome] = useState(ag.pacienteNome || ag.pacientenome || ''); const [cpf, setCpf] = useState(''); const [tel, setTel] = useState(''); const [proc, setProc] = useState(ag.procedimento || ''); const [saving, setSaving] = useState(false); useEffect(() => { let alive = true; const t = setTimeout(() => { HybridBackend.getPacientes(q, 'recentes').then(r => alive && setResults((r.data || []).slice(0, 8))).catch(() => { }); }, 300); return () => { alive = false; clearTimeout(t); }; }, [q]); const pick = (pac: any) => { setSel(pac); setNovo(false); setCpf(pac.cpf || ''); setTel(pac.telefone || ''); }; const iniciarNovo = () => { setNovo(true); setSel(null); setCpf(''); setTel(''); }; const confirmar = async () => { if (!cpf.trim() || !tel.trim()) { toast.error('CPF E TELEFONE SÃO OBRIGATÓRIOS.'); return; } setSaving(true); try { let pacienteId = sel?.id; if (novo) { if (!nome.trim()) { toast.error('INFORME O NOME.'); setSaving(false); return; } pacienteId = `p_${Date.now()}`; await HybridBackend.savePaciente({ id: pacienteId, nome, cpf, telefone: tel } as any); } if (!pacienteId) { toast.error('SELECIONE UM PACIENTE OU CRIE UM NOVO.'); setSaving(false); return; } const res = await HybridBackend.confirmarAgendamento(ag.id, { pacienteId, cpf, telefone: tel, procedimento: proc || undefined }); if (!res.success) { toast.error(res.message || 'ERRO AO CONFIRMAR.'); return; } toast.success('AGENDAMENTO CONFIRMADO!'); onConfirmed(); close(); } catch { toast.error('ERRO AO CONFIRMAR.'); } finally { setSaving(false); } }; const faltaDados = !cpf.trim() || !tel.trim(); return (

Vincule o paciente do cadastro. Se houver homônimos, use CPF/telefone/nascimento para identificar. CPF e telefone são obrigatórios.

{!novo && ( <>
setQ(e.target.value)} placeholder="Buscar por nome, CPF ou telefone..." className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:border-teal-400" />
{results.map(pac => ( ))} {q && results.length === 0 &&

Nenhum paciente encontrado.

}
)} {novo && (
setNome(e.target.value.toUpperCase())} placeholder="NOME COMPLETO" className="w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm font-bold focus:outline-none focus:border-teal-400" />
)} {(sel || novo) && (
{sel &&

{sel.nome}

}
setCpf(e.target.value)} placeholder="000.000.000-00" className={`w-full px-2.5 py-2 border rounded-lg text-xs font-bold focus:outline-none ${!cpf.trim() ? 'border-amber-300 bg-amber-50' : 'border-gray-200 focus:border-teal-400'}`} />
setTel(e.target.value)} placeholder="(00) 00000-0000" className={`w-full px-2.5 py-2 border rounded-lg text-xs font-bold focus:outline-none ${!tel.trim() ? 'border-amber-300 bg-amber-50' : 'border-gray-200 focus:border-teal-400'}`} />
setProc(e.target.value.toUpperCase())} placeholder="EX: AVALIAÇÃO" className="w-full px-2.5 py-2 border border-gray-200 rounded-lg text-xs font-bold focus:outline-none focus:border-teal-400" />
{faltaDados &&

Preencha CPF e telefone para confirmar.

}
)}
); }; // Modal SIMPLES do dentista linkado por clínica: sem dados sensíveis, sem agendamento. // Só: Atender · Reencaminhar (+motivo) · Solicitar avaliação (+motivo). const DentistaRestritoView: React.FC<{ ag: any; nome: string; dentNome: string; dentCor?: string; onAtender?: () => void; onChanged: () => void }> = ({ ag, nome, dentNome, dentCor, onAtender, onChanged }) => { const { close } = useStackModal(); const toast = useToast(); const [modo, setModo] = useState<'' | 'reencaminhar' | 'avaliacao'>(''); const [motivo, setMotivo] = useState(''); const [saving, setSaving] = useState(false); const [avalLinked, setAvalLinked] = useState(null); const [showEditor, setShowEditor] = useState(false); useEffect(() => { let alive = true; if (ag?.id) HybridBackend.getAvaliacaoDoAgendamento(ag.id).then(a => { if (alive) setAvalLinked(a); }); return () => { alive = false; }; }, [ag?.id]); const enviar = async (fn: () => Promise, ok: string) => { setSaving(true); try { const r = await fn(); if (r && r.success === false) { toast.error(r.message || 'ERRO NA OPERAÇÃO.'); return; } toast.success(ok); onChanged(); close(); } catch { toast.error('ERRO NA OPERAÇÃO.'); } finally { setSaving(false); } }; return (

{nome}

{ag.procedimento && ( {ag.procedimento} )}

{ag.start ? new Date(ag.start).toLocaleDateString('pt-BR', { weekday: 'short', day: 'numeric', month: 'short' }) : '—'}

{ag.start ? new Date(ag.start).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '—'}

{dentNome}

{modo === '' ? (
{avalLinked && avalLinked.status !== 'cancelada' && ( )}
) : (