import React, { useState, useEffect, useCallback } from 'react'; import { Search, Plus, DollarSign, X, Printer, CreditCard, Rocket, Loader2, FolderOpen, CalendarDays, RadioTower, Pencil, Wallet, PlusCircle, Banknote, History, FileText, Calendar } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { Paciente, Dentista, Agendamento } from '../types.ts'; import { useHybridBackend } from '../hooks/useHybridBackend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; // 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; } 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: '' }); 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 { await HybridBackend.savePaciente({ ...form, id: Math.random().toString() } as any); toast.success('PACIENTE CADASTRADO!'); onSaved(); onClose(); } catch { toast.error('ERRO AO CADASTRAR PACIENTE.'); } }; return (

NOVO PACIENTE

setForm(f => ({ ...f, nome: e.target.value.toUpperCase() }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5 font-semibold" placeholder="NOME COMPLETO" />
setForm(f => ({ ...f, cpf: e.target.value }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" placeholder="000.000.000-00" />
setForm(f => ({ ...f, telefone: e.target.value }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" placeholder="(XX) XXXXX-XXXX" />
setForm(f => ({ ...f, dataNascimento: e.target.value }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" />
setForm(f => ({ ...f, convenio: e.target.value.toUpperCase() }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" placeholder="EX: CASSEMS" />
setForm(f => ({ ...f, email: e.target.value }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" placeholder="EMAIL@EXEMPLO.COM" />
); }; // ----------- 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 ----------- const EditarPacienteModal: React.FC<{ patient: Paciente; onClose: () => void; onSaved: () => void }> = ({ patient, onClose, onSaved }) => { const toast = useToast(); const [form, setForm] = useState({ ...patient }); 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(); try { await HybridBackend.updatePaciente(form); toast.success('PACIENTE ATUALIZADO!'); onSaved(); onClose(); } catch { toast.error('ERRO AO SALVAR.'); } }; return (

EDITAR PACIENTE

setForm(f => ({ ...f, nome: e.target.value.toUpperCase() }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5 font-semibold" />
setForm(f => ({ ...f, cpf: e.target.value }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" />
setForm(f => ({ ...f, telefone: e.target.value }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" />
setForm(f => ({ ...f, dataNascimento: e.target.value }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" />
setForm(f => ({ ...f, convenio: e.target.value.toUpperCase() }))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" />
); }; import { PageHeader } from '../components/PageHeader.tsx'; export const PatientsView: React.FC = () => { const [busca, setBusca] = useState(''); const debouncedBusca = useDebounce(busca, 500); const fetcher = useCallback(() => HybridBackend.getPacientes(debouncedBusca), [debouncedBusca]); const { data: pacientes, isLoading, error, refresh } = useHybridBackend(fetcher, [debouncedBusca]); const toast = useToast(); const [modal, setModal] = useState<{ type: string | null; patient: Paciente | null }>({ type: null, patient: null }); const [isNovoPacienteOpen, setIsNovoPacienteOpen] = useState(false); const closeModal = useCallback(() => setModal({ type: null, patient: null }), []); // Effect to handle Esc key to close modals useEffect(() => { const isModalActive = modal.type !== null && modal.type !== 'agendamento' && modal.type !== 'historico' && modal.type !== 'editar'; if (!isModalActive) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') closeModal(); }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [modal.type, closeModal]); // Asaas form state const [asaasForm, setAsaasForm] = useState({ tratamento: '', valorTotal: '', desconto: '', entrada: '', parcelas: 1, vencimento: new Date().toISOString().split('T')[0] }); useEffect(() => { if (modal.type === 'asaas') { setAsaasForm({ tratamento: '', valorTotal: '', desconto: '', entrada: '', parcelas: 1, vencimento: new Date().toISOString().split('T')[0] }); } }, [modal.type, modal.patient]); const handleAsaasFormChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setAsaasForm(prev => ({ ...prev, [name]: value })); }; const valorTotalNum = parseFloat(asaasForm.valorTotal) || 0; const descontoNum = parseFloat(asaasForm.desconto) || 0; const entradaNum = parseFloat(asaasForm.entrada) || 0; const saldoAParcelar = valorTotalNum - descontoNum - entradaNum; const valorParcela = saldoAParcelar > 0 && asaasForm.parcelas > 0 ? saldoAParcelar / Number(asaasForm.parcelas) : 0; const handleDocGeneration = () => { toast.success(`DOCUMENTO GERADO E SALVO NO DRIVE PARA ${modal.patient?.nome}!`); closeModal(); }; const handleAsaasSubmit = (e: React.FormEvent) => { e.preventDefault(); toast.success(`COBRANÇA GERADA NO ASAAS PARA ${modal.patient?.nome}!`); closeModal(); }; const displayedPacientes = busca ? pacientes : pacientes?.slice(-3).reverse(); return (
setBusca(e.target.value.toUpperCase())} />
{!busca && (

ÚLTIMOS CADASTRADOS ({pacientes?.length ?? 0})

)}
{isLoading &&
CARREGANDO DADOS DO MYSQL...
} {error &&
ERRO AO CARREGAR: {error.message}
} {displayedPacientes && displayedPacientes.map(p => (

{p.nome}

{p.convenio && ( {p.convenio} )}

{p.cpf}

{p.telefone}

{p.dataNascimento}

{p.convenio}

))}
{/* NOVO PACIENTE MODAL */} {isNovoPacienteOpen && ( setIsNovoPacienteOpen(false)} onSaved={refresh} /> )} {/* EDITAR PACIENTE MODAL */} {modal.type === 'editar' && modal.patient && ( )} {/* HISTORICO MODAL */} {modal.type === 'historico' && modal.patient && ( )} {/* MODAL MENU CLÍNICO */} {modal.type === 'clinical' && modal.patient && (
Menu Clínico

{modal.patient.nome}

)} {/* MODAL DE AGENDAMENTO */} {modal.type === 'agendamento' && modal.patient && ( )} {/* MODAL DOCUMENTOS */} {(modal.type === 'doc-receita' || modal.type === 'doc-raiox') && (

GERAR {modal.type === 'doc-receita' ? 'RECEITA' : 'PEDIDO DE RAIO-X'}

O SISTEMA IRÁ GERAR UM DOCUMENTO NO GOOGLE DOCS USANDO O MODELO PADRÃO.

)} {/* MODAL MENU FINANCEIRO */} {modal.type === 'financial' && modal.patient && (
Menu Financeiro

{modal.patient.nome}

)} {/* ASAAS MODAL */} {modal.type === 'asaas' && modal.patient && (

PARCELAMENTO INTELIGENTE (ASAAS)

Cliente: {modal.patient.nome}

SALDO A PARCELAR: {saldoAParcelar.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
{asaasForm.parcelas}x DE {valorParcela.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
)}
); };