import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Search, Plus, DollarSign, X, Printer, CreditCard, Rocket, Loader2, FolderOpen, CalendarDays, RadioTower, Pencil, Wallet, PlusCircle, Banknote, History, FileText, Calendar, Copy, CheckCircle2, Users, UserCheck, ChevronLeft, ChevronRight, Save, Stethoscope, ClipboardList, BarChart3, MapPin } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { Paciente, Dentista, Agendamento, Plano } from '../types.ts'; import { useHybridBackend } from '../hooks/useHybridBackend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; import { buscarCep } from '../services/viacep.ts'; const RELACOES_COMUNS = ['MÃE', 'PAI', 'FILHO', 'FILHA', 'IRMÃO', 'IRMÃ', 'CÔNJUGE', 'AVÔ', 'AVÓ', 'NETO', 'NETA', 'TIO', 'TIA', 'SOBRINHO', 'SOBRINHA', 'PRIMO', 'PRIMA', 'PADRASTO', 'MADRASTA', 'ENTEADO', 'ENTEADA']; function toInputDate(str?: string): string { if (!str) return ''; if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return str; const m = str.match(/^(\d{2})\/(\d{2})\/(\d{4})$/); return m ? `${m[3]}-${m[2]}-${m[1]}` : ''; } // Debounce hook to avoid excessive API calls on search 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; } // Autocomplete genérico com debounce function AutocompleteField({ label, placeholder, value, onSelect, onClear, fetchFn, renderOption, extraAction }: { label: string; placeholder: string; value?: { id: string; nome: string } | null; onSelect: (item: T) => void; onClear: () => void; fetchFn: (q: string) => Promise; renderOption?: (item: T) => React.ReactNode; extraAction?: React.ReactNode; }) { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const debounced = useDebounce(query, 300); const ref = useRef(null); useEffect(() => { if (!debounced.trim() || value) { setResults([]); setOpen(false); return; } setLoading(true); fetchFn(debounced).then(r => { const arr = Array.isArray(r) ? r : []; setResults(arr); setOpen(arr.length > 0); }).finally(() => setLoading(false)); }, [debounced]); useEffect(() => { const close = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', close); return () => document.removeEventListener('mousedown', close); }, []); if (value) return (
{value.nome}
); return (
setQuery(e.target.value)} placeholder={placeholder} className="w-full pl-7 pr-3 py-2 border border-gray-200 rounded-lg text-xs focus:ring-2 focus:ring-teal-100 focus:border-teal-300 outline-none" /> {loading && }
{extraAction}
{open && results.length > 0 && (
{results.map(item => ( ))}
)}
); } // Modal de familiar vinculado const FamiliaModal: React.FC<{ paciente: Paciente; onClose: () => void }> = ({ paciente, onClose }) => { useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', h); return () => document.removeEventListener('keydown', h); }, [onClose]); return (

Vínculo Familiar

{paciente.nome}

{paciente.grupoFamiliarId ? (

{paciente.grupoFamiliarNome}

{paciente.grupoFamiliarRelacao || 'Familiar'}

) : (

Nenhum familiar vinculado.

)}
); }; const AgendamentoModal: React.FC<{ patient: Paciente, onClose: () => void }> = ({ patient, onClose }) => { const { data: dentistas } = useHybridBackend(HybridBackend.getDentistas); const { data: agendamentos, refresh: refreshAgendamentos } = useHybridBackend(HybridBackend.getAgendamentos); const toast = useToast(); const [duration, setDuration] = useState(30); const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); const [selectedTime, setSelectedTime] = useState(''); const [selectedDentistId, setSelectedDentistId] = useState(undefined); const [procedimento, setProcedimento] = useState(''); useEffect(() => { if (dentistas && !selectedDentistId) { setSelectedDentistId(dentistas[0]?.id); } }, [dentistas, selectedDentistId]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [onClose]); const selectedDentist = dentistas?.find(d => d.id === selectedDentistId); const generateTimeSlots = useCallback(() => { const slots: { time: string, available: boolean }[] = []; if (!selectedDentistId || !selectedDate) return slots; const startTime = 8 * 60, endTime = 18 * 60, lunchStart = 12 * 60, lunchEnd = 14 * 60; const existingAppointments = (agendamentos ?? []).filter(ag => ag.dentistaId === selectedDentistId && new Date(ag.start).toISOString().split('T')[0] === selectedDate ).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(), })); for (let time = startTime; time < endTime; time += duration) { const slotStart = time, slotEnd = time + duration; if (slotEnd > endTime) continue; const isInLunch = (slotStart >= lunchStart && slotStart < lunchEnd) || (slotEnd > lunchStart && slotEnd <= lunchEnd) || (slotStart < lunchStart && slotEnd > lunchEnd); if (isInLunch) continue; const isOccupied = existingAppointments.some(app => (slotStart >= app.start && slotStart < app.end) || (slotEnd > app.start && slotEnd <= app.end) || (slotStart < app.start && slotEnd > app.end)); if (!isOccupied) { const hour = Math.floor(slotStart / 60), minute = slotStart % 60; slots.push({ time: `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`, available: true }); } } return slots; }, [selectedDentistId, selectedDate, agendamentos, duration]); const timeSlots = generateTimeSlots(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!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); try { await HybridBackend.salvarAgendamento({ pacienteNome: patient.nome, dentistaId: selectedDentistId, start: startDateTime.toISOString(), end: endDateTime.toISOString(), status: 'Pendente', procedimento: procedimento.toUpperCase(), }); toast.success(`AGENDAMENTO PARA ${patient.nome} SALVO!`); refreshAgendamentos(); onClose(); } catch { toast.error("ERRO AO SALVAR AGENDAMENTO."); } }; return (

AGENDAR CONSULTA

{patient.nome}
setSelectedDate(e.target.value)} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" />
{timeSlots.map(slot => ( ))} {timeSlots.length === 0 &&

NENHUM HORÁRIO DISPONÍVEL PARA ESTA DATA/DURAÇÃO.

}
setProcedimento(e.target.value.toUpperCase())} placeholder="EX: AVALIAÇÃO..." className="w-full mt-1 border border-gray-300 rounded-lg p-2.5 uppercase-input" />
) }; // ----------- NOVO PACIENTE MODAL ----------- const NovoPacienteModal: React.FC<{ onClose: () => void; onSaved: () => void }> = ({ onClose, onSaved }) => { const toast = useToast(); const [form, setForm] = useState({ nome: '', cpf: '', telefone: '', dataNascimento: '', convenio: '', email: '' }); const [familiar, setFamiliar] = useState<{ id: string; nome: string } | null>(null); const [familiarRelacao, setFamiliarRelacao] = useState(''); const [indicadoPor, setIndicadoPor] = useState<{ id: string; nome: string } | null>(null); const { data: planos } = useHybridBackend(HybridBackend.getPlanos); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [onClose]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!form.nome || !form.cpf) { toast.error('PREENCHA NOME E CPF.'); return; } try { const id = Date.now().toString(); await HybridBackend.savePaciente({ ...form, id, grupoFamiliarId: familiar?.id, grupoFamiliarRelacao: familiarRelacao || undefined, indicadoPorId: indicadoPor?.id, } as any); toast.success('PACIENTE CADASTRADO!'); onSaved(); onClose(); } catch { toast.error('ERRO AO CADASTRAR PACIENTE.'); } }; const input = "w-full border border-gray-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-teal-100 focus:border-teal-300 outline-none"; return (

Novo Paciente

setForm(f => ({ ...f, nome: e.target.value.toUpperCase() }))} className={input} placeholder="NOME COMPLETO" />
setForm(f => ({ ...f, cpf: e.target.value }))} className={input} placeholder="000.000.000-00" />
setForm(f => ({ ...f, telefone: e.target.value }))} className={input} placeholder="(67) 99999-9999" />
setForm(f => ({ ...f, dataNascimento: e.target.value }))} className={input} />
setForm(f => ({ ...f, email: e.target.value }))} className={input} placeholder="email@exemplo.com" />
{/* Familiar */} label="Familiar (vínculo)" placeholder="Buscar paciente familiar..." value={familiar} onSelect={p => { setFamiliar({ id: p.id, nome: p.nome }); }} onClear={() => { setFamiliar(null); setFamiliarRelacao(''); }} fetchFn={q => HybridBackend.getPacientes(q).then(r => r.data)} renderOption={p => ( {p.nome} {p.cpf} )} /> {familiar && (
setFamiliarRelacao(e.target.value.toUpperCase())} placeholder="EX: MÃE, PAI, IRMÃO..." className={input} /> {RELACOES_COMUNS.map(r =>
)} {/* Indicado por */} label="Indicado por" placeholder="Buscar paciente que indicou..." value={indicadoPor} onSelect={p => setIndicadoPor({ id: p.id, nome: p.nome })} onClear={() => setIndicadoPor(null)} fetchFn={q => HybridBackend.getPacientes(q).then(r => r.data)} renderOption={p => ( {p.nome} {p.cpf} )} />
); }; // ----------- HISTORICO MODAL ----------- const HistoricoModal: React.FC<{ patient: Paciente; onClose: () => void }> = ({ patient, onClose }) => { const { data: agendamentos, isLoading } = useHybridBackend(HybridBackend.getAgendamentos); const meus = agendamentos?.filter(a => a.pacienteNome === patient.nome) || []; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [onClose]); return (

HISTÓRICO — {patient.nome}

{isLoading &&

CARREGANDO...

} {!isLoading && meus.length === 0 &&

NENHUM AGENDAMENTO ENCONTRADO.

} {meus.map(a => (

{a.procedimento}

{new Date(a.start).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })}

{a.status}
))}
); }; // ----------- EDITAR PACIENTE MODAL (4 páginas) ----------- const ESTADOS_BR = ['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO']; const EDIT_STEPS = ['Dados', 'Família', 'Endereço', 'Histórico']; const EditarPacienteModal: React.FC<{ patient: Paciente; onClose: () => void; onSaved: () => void }> = ({ patient, onClose, onSaved }) => { const toast = useToast(); const { data: planos } = useHybridBackend(HybridBackend.getPlanos); const { data: agendamentos } = useHybridBackend(HybridBackend.getAgendamentos); const [page, setPage] = useState(0); const [form, setForm] = useState({ ...patient, dataNascimento: toInputDate(patient.dataNascimento) }); const [familiar, setFamiliar] = useState<{ id: string; nome: string } | null>( patient.grupoFamiliarId ? { id: patient.grupoFamiliarId, nome: patient.grupoFamiliarNome || '' } : null ); const [familiarRelacao, setFamiliarRelacao] = useState(patient.grupoFamiliarRelacao || ''); const [indicadoPor, setIndicadoPor] = useState<{ id: string; nome: string } | null>( patient.indicadoPorId ? { id: patient.indicadoPorId, nome: patient.indicadoPorNome || '' } : null ); const meusAgendamentos = (agendamentos ?? []).filter(a => a.pacienteNome === patient.nome); useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', h); return () => document.removeEventListener('keydown', h); }, [onClose]); const handleSubmit = async () => { try { const { grupoFamiliarNome: _gn, indicadoPorNome: _in, ...formData } = form; await HybridBackend.updatePaciente({ ...formData, grupoFamiliarId: familiar?.id, grupoFamiliarRelacao: familiarRelacao || undefined, indicadoPorId: indicadoPor?.id, } as any); toast.success('PACIENTE ATUALIZADO!'); onSaved(); onClose(); } catch { toast.error('ERRO AO SALVAR.'); } }; const inp = "w-full border border-gray-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-teal-100 focus:border-teal-300 outline-none"; return (
{/* Header */}

Editar Paciente

{patient.nome}

{/* Step indicator */}
{EDIT_STEPS.map((s, i) => ( {i < EDIT_STEPS.length - 1 && (
)} ))}
{/* Page content */}
{/* Página 1: Dados básicos */} {page === 0 && (<>
setForm(f => ({ ...f, nome: e.target.value.toUpperCase() }))} className={inp} />
setForm(f => ({ ...f, cpf: e.target.value }))} className={inp} />
setForm(f => ({ ...f, telefone: e.target.value }))} className={inp} />
setForm(f => ({ ...f, dataNascimento: e.target.value }))} className={inp} />
setForm(f => ({ ...f, email: e.target.value }))} className={inp} />
)} {/* Página 2: Família e indicação */} {page === 1 && (
label="Familiar (vínculo)" placeholder="Buscar paciente familiar..." value={familiar} onSelect={p => { setFamiliar({ id: p.id, nome: p.nome }); }} onClear={() => { setFamiliar(null); setFamiliarRelacao(''); }} fetchFn={q => HybridBackend.getPacientes(q).then(r => r.data)} renderOption={p => ( {p.nome} {p.cpf} )} /> {familiar && (
setFamiliarRelacao(e.target.value.toUpperCase())} placeholder="EX: MÃE, PAI, IRMÃO..." className={inp} /> {RELACOES_COMUNS.map(r =>
)} label="Indicado por" placeholder="Buscar paciente que indicou..." value={indicadoPor} onSelect={p => setIndicadoPor({ id: p.id, nome: p.nome })} onClear={() => setIndicadoPor(null)} fetchFn={q => HybridBackend.getPacientes(q).then(r => r.data)} renderOption={p => ( {p.nome} {p.cpf} )} />
)} {/* Página 3: Endereço */} {page === 2 && (
{ const v = e.target.value; setForm(f => ({ ...f, cep: v })); if (v.replace(/\D/g, '').length === 8) { const r = await buscarCep(v); if (r) { setForm(f => ({ ...f, logradouro: r.logradouro, bairro: r.bairro, cidade: r.cidade, estado: r.estado })); toast.success('ENDEREÇO PREENCHIDO PELO CEP!'); } else toast.error('CEP NÃO ENCONTRADO.'); } }} className={inp} placeholder="00000-000" maxLength={9} />
setForm(f => ({ ...f, logradouro: e.target.value.toUpperCase() }))} className={inp} placeholder="RUA, AV, TRAVESSA..." />
setForm(f => ({ ...f, numero: e.target.value }))} className={inp} placeholder="123" />
setForm(f => ({ ...f, complemento: e.target.value.toUpperCase() }))} className={inp} placeholder="APTO, BLOCO..." />
setForm(f => ({ ...f, bairro: e.target.value.toUpperCase() }))} className={inp} placeholder="BAIRRO" />
setForm(f => ({ ...f, cidade: e.target.value.toUpperCase() }))} className={inp} placeholder="CIDADE" />
)} {/* Página 4: Histórico e observações */} {page === 3 && (