Files
scoreodonto.com/frontend/views/PatientsView.tsx
VPS 4 Builder 3e64514b08 feat(financeiro+salas): motor financeiro V1/V2, glosas, GTO→financeiro, contratos de convênio e redesenho de Salas
Financeiro V1 (Épicos 1–4):
- FKs de origem no lançamento (paciente/profissional/procedimento/agendamento/contrato/realizado), soft delete e cron "Atrasado"
- prontuário de procedimentos realizados (fotos antes/depois) + regra sem-comissão (garantia/reabertura do mesmo profissional)
- normalizeFinanceiro: corrige CHECK capitalizado vs enforceUppercase (lançamentos não salvavam pela UI)
- baixa de pagamento (pago_em), despesas (fornecedor/centro de custo/recorrente), competência por data de conclusão
- relatório financeiro (período, paciente, profissional, forma, convênio, glosa)
- orçamento Assinado -> contas a receber; contrato vigente -> cobrança recorrente; renovação automática de contrato

Financeiro V2 (Comissão/Repasse):
- regras de % (padrão/procedimento/override por profissional) + custo de laboratório
- base selecionável (recebido/produção), fechamento persistido, pagar (gera despesa) + comprovante + estorno

Glosas de convênio: por item de GTO, recurso/protocolo/prazo, recuperar/estornar, impacto no relatório

GTO -> Financeiro: finalizar gera RECEITA (convênio a receber); LancarGTO passa a persistir; convênios reais (planos)

Contratos: modelo "Atendimento a Convênios / Repasse (Dentista Credenciado)" + variáveis de convênio

Salas (redesenho): perfil "Dono de Sala"; locação sempre ativa; menus MINHAS/ALUGAR SALAS;
sala como workspace isolado (seletor agrupado Clínicas x Salas, tenantGuard por dono); financeiro de
locação (reserva confirmada -> recebível); Painel da Sala (KPIs, agenda visual, recebíveis, relatório por sala)

Docs: BACKLOG-FINANCEIRO-V1, AUDITORIA-FUNCIONAL-SCOREODONTO, CHECKLIST-DEPLOY-PRODUCAO

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 00:28:07 +02:00

1331 lines
90 KiB
TypeScript

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<T extends { id: string; nome: string }>({
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<T[]>;
renderOption?: (item: T) => React.ReactNode;
extraAction?: React.ReactNode;
}) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<T[]>([]);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const debounced = useDebounce(query, 300);
const ref = useRef<HTMLDivElement>(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 (
<div className="flex items-center justify-between bg-teal-50 border border-teal-100 rounded-lg px-3 py-2">
<span className="text-xs font-bold text-teal-700 uppercase truncate">{value.nome}</span>
<button onClick={onClear} className="ml-2 text-teal-300 hover:text-teal-600 flex-shrink-0"><X size={13} /></button>
</div>
);
return (
<div ref={ref} className="relative">
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">{label}</label>
<div className="flex gap-1">
<div className="relative flex-1">
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" />
<input
value={query} onChange={e => 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 && <Loader2 size={12} className="absolute right-2.5 top-1/2 -translate-y-1/2 animate-spin text-gray-400" />}
</div>
{extraAction}
</div>
{open && results.length > 0 && (
<div className="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-xl shadow-xl overflow-hidden">
{results.map(item => (
<button key={item.id} onClick={() => { onSelect(item); setQuery(''); setOpen(false); }}
className="w-full text-left px-3 py-2 hover:bg-teal-50 text-xs font-medium text-gray-700 flex items-center gap-2 border-b border-gray-50 last:border-0">
{renderOption ? renderOption(item) : item.nome}
</button>
))}
</div>
)}
</div>
);
}
// 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 (
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-2xl flex flex-col animate-in fade-in zoom-in-50 duration-200">
<div className="p-5 border-b border-gray-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-emerald-100 rounded-lg flex items-center justify-center">
<Users size={16} className="text-emerald-600" />
</div>
<div>
<p className="text-xs font-black text-gray-800 uppercase">Vínculo Familiar</p>
<p className="text-[10px] text-gray-400 font-medium uppercase">{paciente.nome}</p>
</div>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
</div>
<div className="p-4">
{paciente.grupoFamiliarId ? (
<div className="flex items-center gap-3 p-3 bg-emerald-50 rounded-xl border border-emerald-100">
<div className="w-9 h-9 rounded-full bg-emerald-200 flex items-center justify-center flex-shrink-0">
<UserCheck size={15} className="text-emerald-700" />
</div>
<div>
<p className="text-xs font-bold text-gray-800 uppercase">{paciente.grupoFamiliarNome}</p>
<p className="text-[10px] text-emerald-500 font-bold uppercase">{paciente.grupoFamiliarRelacao || 'Familiar'}</p>
</div>
</div>
) : (
<p className="text-center text-xs text-gray-400 py-4">Nenhum familiar vinculado.</p>
)}
</div>
<div className="p-4 border-t border-gray-100 flex justify-end">
<button onClick={onClose} className="px-5 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 text-xs font-bold rounded-lg uppercase">Fechar</button>
</div>
</div>
</div>
);
};
const AgendamentoModal: React.FC<{ patient: Paciente, onClose: () => void }> = ({ patient, onClose }) => {
const { data: dentistas } = useHybridBackend<Dentista[]>(HybridBackend.getDentistas);
const { data: agendamentos, refresh: refreshAgendamentos } = useHybridBackend<Agendamento[]>(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<string | undefined>(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 (
<div className="fixed inset-0 bg-black bg-opacity-40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<form onSubmit={handleSubmit} className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-xl flex flex-col animate-in fade-in zoom-in-50 duration-200">
<div className="p-5 border-b border-gray-200">
<div className="flex justify-between items-center">
<h2 className="text-lg font-bold text-gray-800 flex items-center gap-2">
<Calendar size={20} className="text-teal-500" /> AGENDAR CONSULTA
</h2>
<button type="button" onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={20} /></button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-4">
<div>
<label className="text-xs font-bold text-gray-500 uppercase">PACIENTE</label>
<div className="w-full bg-gray-100 p-3 rounded-lg mt-1 font-bold text-gray-800 uppercase">{patient.nome}</div>
</div>
<div>
<label className="text-xs font-bold text-gray-500 uppercase">DENTISTA RESPONSÁVEL</label>
<div className="flex items-center gap-2 mt-1">
<select value={selectedDentistId} onChange={(e) => setSelectedDentistId(e.target.value)} className="w-full border border-gray-300 rounded-lg p-2.5 bg-white uppercase-select">
{dentistas?.map(d => <option key={d.id} value={d.id}>{d.nome}</option>)}
</select>
<div className="w-7 h-7 rounded-full border-2 border-white shadow flex-shrink-0" style={{ backgroundColor: selectedDentist?.corAgenda || '#ccc' }}></div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-gray-500 uppercase">DATA</label>
<input type="date" value={selectedDate} onChange={e => setSelectedDate(e.target.value)} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5" />
</div>
<div>
<label className="text-xs font-bold text-gray-500 uppercase">TEMPO</label>
<select value={duration} onChange={e => setDuration(Number(e.target.value))} className="w-full mt-1 border border-gray-300 rounded-lg p-2.5 bg-white uppercase-select">
<option value={15}>15 MIN</option>
<option value={30}>30 MIN</option>
<option value={45}>45 MIN</option>
<option value={60}>60 MIN</option>
</select>
</div>
</div>
<div>
<label className="text-xs font-bold text-gray-500 uppercase">HORÁRIOS</label>
<div className="grid grid-cols-6 gap-2 mt-2">
{timeSlots.map(slot => (
<button type="button" key={slot.time} onClick={() => setSelectedTime(slot.time)}
className={`py-2 rounded-lg text-sm font-bold border transition-colors ${selectedTime === slot.time ? 'bg-teal-600 text-white border-teal-600' : 'bg-white border-gray-300 hover:border-teal-500'}`}>
{slot.time}
</button>
))}
{timeSlots.length === 0 && <p className="col-span-6 text-center text-xs text-gray-500 p-4 bg-gray-50 rounded-lg">NENHUM HORÁRIO DISPONÍVEL PARA ESTA DATA/DURAÇÃO.</p>}
</div>
</div>
<div>
<label className="text-xs font-bold text-gray-500 uppercase">PROCEDIMENTO</label>
<input type="text" value={procedimento} onChange={(e) => 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" />
</div>
</div>
<div className="p-4 bg-gray-50 border-t border-gray-200 flex justify-end gap-3">
<button type="button" onClick={onClose} className="px-6 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold rounded-lg transition-colors text-sm uppercase">CANCELAR</button>
<button type="submit" className="px-6 py-2 bg-teal-500 hover:bg-teal-600 text-white font-bold rounded-lg shadow-sm transition-colors text-sm uppercase">CONFIRMAR</button>
</div>
</form>
</div>
)
};
// ----------- 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<Plano[]>(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 (
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-2xl flex flex-col animate-in fade-in zoom-in-50 duration-200">
<div className="p-5 border-b border-gray-100 flex justify-between items-center">
<h2 className="text-sm font-black text-gray-800 uppercase flex items-center gap-2">
<Plus size={16} className="text-green-500" /> Novo Paciente
</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
</div>
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-5 space-y-4">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Nome Completo *</label>
<input required value={form.nome} onChange={e => setForm(f => ({ ...f, nome: e.target.value.toUpperCase() }))} className={input} placeholder="NOME COMPLETO" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">CPF *</label>
<input required value={form.cpf} onChange={e => setForm(f => ({ ...f, cpf: e.target.value }))} className={input} placeholder="000.000.000-00" />
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Telefone</label>
<input value={form.telefone} onChange={e => setForm(f => ({ ...f, telefone: e.target.value }))} className={input} placeholder="(67) 99999-9999" />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Nascimento</label>
<input type="date" value={form.dataNascimento} onChange={e => setForm(f => ({ ...f, dataNascimento: e.target.value }))} className={input} />
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Convênio</label>
<select value={form.convenio} onChange={e => setForm(f => ({ ...f, convenio: e.target.value }))} className={input}>
<option value=""> Particular </option>
{planos?.map(p => <option key={p.id} value={p.nome}>{p.nome}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">E-mail</label>
<input type="email" value={form.email} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} className={input} placeholder="email@exemplo.com" />
</div>
<div className="border-t border-gray-100 pt-3 space-y-3">
{/* Familiar */}
<AutocompleteField<Paciente>
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 => (
<span className="flex items-center gap-2 w-full">
<Users size={12} className="text-emerald-400 flex-shrink-0" />
<span className="font-bold flex-1">{p.nome}</span>
<span className="text-[10px] text-gray-400">{p.cpf}</span>
</span>
)}
/>
{familiar && (
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Parentesco</label>
<input
list="relacoes-list-novo"
value={familiarRelacao}
onChange={e => setFamiliarRelacao(e.target.value.toUpperCase())}
placeholder="EX: MÃE, PAI, IRMÃO..."
className={input}
/>
<datalist id="relacoes-list-novo">
{RELACOES_COMUNS.map(r => <option key={r} value={r} />)}
</datalist>
</div>
)}
{/* Indicado por */}
<AutocompleteField<Paciente>
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 => (
<span className="flex items-center gap-2 w-full">
<UserCheck size={12} className="text-green-400 flex-shrink-0" />
<span className="font-bold flex-1">{p.nome}</span>
<span className="text-[10px] text-gray-400">{p.cpf}</span>
</span>
)}
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<button type="button" onClick={onClose} className="px-5 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-lg text-xs uppercase">Cancelar</button>
<button type="submit" className="px-5 py-2 bg-green-600 hover:bg-green-700 text-white font-bold rounded-lg text-xs uppercase flex items-center gap-1.5">
<Plus size={14} /> Cadastrar
</button>
</div>
</form>
</div>
</div>
);
};
// ----------- HISTORICO MODAL -----------
const HistoricoModal: React.FC<{ patient: Paciente; onClose: () => void }> = ({ patient, onClose }) => {
const { data: agendamentos, isLoading } = useHybridBackend<Agendamento[]>(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 (
<div className="fixed inset-0 bg-black bg-opacity-40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-xl flex flex-col animate-in fade-in zoom-in-50 duration-200">
<div className="p-5 border-b border-gray-200 flex justify-between items-center">
<h2 className="text-base font-bold text-gray-800 uppercase">HISTÓRICO {patient.nome}</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={20} /></button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoading && <p className="text-center text-gray-400 text-sm">CARREGANDO...</p>}
{!isLoading && meus.length === 0 && <p className="text-center text-gray-400 text-sm py-6">NENHUM AGENDAMENTO ENCONTRADO.</p>}
{meus.map(a => (
<div key={a.id} className="flex items-center justify-between border border-gray-100 rounded-lg p-3 bg-gray-50">
<div>
<p className="font-bold text-gray-800 text-sm uppercase">{a.procedimento}</p>
<p className="text-xs text-gray-500">{new Date(a.start).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })}</p>
</div>
<span className={`text-xs font-bold px-2 py-1 rounded-full uppercase ${a.status === 'Confirmado' ? 'bg-green-100 text-green-700' : a.status === 'Pendente' ? 'bg-amber-100 text-amber-700' : 'bg-gray-100 text-gray-600'}`}>
{a.status}
</span>
</div>
))}
</div>
<div className="p-4 border-t border-gray-200 flex justify-end">
<button onClick={onClose} className="px-6 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-lg text-sm uppercase">FECHAR</button>
</div>
</div>
</div>
);
};
// ----------- 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<Plano[]>(HybridBackend.getPlanos);
const { data: agendamentos } = useHybridBackend<Agendamento[]>(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 (
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-2xl flex flex-col animate-in fade-in zoom-in-50 duration-200">
{/* Header */}
<div className="p-5 border-b border-gray-100 flex justify-between items-center flex-shrink-0">
<div className="flex items-center gap-2">
<Pencil size={15} className="text-teal-500" />
<div>
<h2 className="text-sm font-black text-gray-800 uppercase">Editar Paciente</h2>
<p className="text-[10px] text-gray-400 font-medium uppercase">{patient.nome}</p>
</div>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
</div>
{/* Step indicator */}
<div className="px-6 pt-4 pb-0 flex-shrink-0">
<div className="flex items-center">
{EDIT_STEPS.map((s, i) => (
<React.Fragment key={i}>
<button onClick={() => setPage(i)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] font-black uppercase transition-all whitespace-nowrap ${
page === i ? 'bg-teal-600 text-white' : i < page ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-400'
}`}>
<span className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-black">{i < page ? '✓' : i + 1}</span>
{s}
</button>
{i < EDIT_STEPS.length - 1 && (
<div className={`flex-1 h-0.5 mx-1 ${i < page ? 'bg-green-300' : 'bg-gray-200'}`} />
)}
</React.Fragment>
))}
</div>
</div>
{/* Page content */}
<div className="flex-1 overflow-y-auto px-6 py-5 space-y-4">
{/* Página 1: Dados básicos */}
{page === 0 && (<>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Nome Completo</label>
<input required value={form.nome} onChange={e => setForm(f => ({ ...f, nome: e.target.value.toUpperCase() }))} className={inp} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">CPF</label>
<input value={form.cpf} onChange={e => setForm(f => ({ ...f, cpf: e.target.value }))} className={inp} />
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Telefone</label>
<input value={form.telefone} onChange={e => setForm(f => ({ ...f, telefone: e.target.value }))} className={inp} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Nascimento</label>
<input type="date" value={form.dataNascimento} onChange={e => setForm(f => ({ ...f, dataNascimento: e.target.value }))} className={inp} />
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Convênio</label>
<select value={form.convenio || ''} onChange={e => setForm(f => ({ ...f, convenio: e.target.value }))} className={inp}>
<option value=""> Particular </option>
{planos?.map(p => <option key={p.id} value={p.nome}>{p.nome}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">E-mail</label>
<input type="email" value={form.email || ''} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} className={inp} />
</div>
</>)}
{/* Página 2: Família e indicação */}
{page === 1 && (
<div className="space-y-4">
<AutocompleteField<Paciente>
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 => (
<span className="flex items-center gap-2 w-full">
<Users size={12} className="text-emerald-400 flex-shrink-0" />
<span className="font-bold flex-1">{p.nome}</span>
<span className="text-[10px] text-gray-400">{p.cpf}</span>
</span>
)}
/>
{familiar && (
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Parentesco</label>
<input
list="relacoes-list-editar"
value={familiarRelacao}
onChange={e => setFamiliarRelacao(e.target.value.toUpperCase())}
placeholder="EX: MÃE, PAI, IRMÃO..."
className={inp}
/>
<datalist id="relacoes-list-editar">
{RELACOES_COMUNS.map(r => <option key={r} value={r} />)}
</datalist>
</div>
)}
<AutocompleteField<Paciente>
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 => (
<span className="flex items-center gap-2 w-full">
<UserCheck size={12} className="text-green-400 flex-shrink-0" />
<span className="font-bold flex-1">{p.nome}</span>
<span className="text-[10px] text-gray-400">{p.cpf}</span>
</span>
)}
/>
</div>
)}
{/* Página 3: Endereço */}
{page === 2 && (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">CEP</label>
<input value={form.cep || ''} onChange={async e => {
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} />
</div>
<div className="col-span-2">
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Logradouro</label>
<input value={form.logradouro || ''} onChange={e => setForm(f => ({ ...f, logradouro: e.target.value.toUpperCase() }))} className={inp} placeholder="RUA, AV, TRAVESSA..." />
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Número</label>
<input value={form.numero || ''} onChange={e => setForm(f => ({ ...f, numero: e.target.value }))} className={inp} placeholder="123" />
</div>
<div className="col-span-2">
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Complemento</label>
<input value={form.complemento || ''} onChange={e => setForm(f => ({ ...f, complemento: e.target.value.toUpperCase() }))} className={inp} placeholder="APTO, BLOCO..." />
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="col-span-2">
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Bairro</label>
<input value={form.bairro || ''} onChange={e => setForm(f => ({ ...f, bairro: e.target.value.toUpperCase() }))} className={inp} placeholder="BAIRRO" />
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">UF</label>
<select value={form.estado || ''} onChange={e => setForm(f => ({ ...f, estado: e.target.value }))} className={inp}>
<option value=""></option>
{ESTADOS_BR.map(uf => <option key={uf}>{uf}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Cidade</label>
<input value={form.cidade || ''} onChange={e => setForm(f => ({ ...f, cidade: e.target.value.toUpperCase() }))} className={inp} placeholder="CIDADE" />
</div>
</div>
)}
{/* Página 4: Histórico e observações */}
{page === 3 && (
<div className="space-y-4">
<div>
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-wider mb-1">Observações Clínicas</label>
<textarea
rows={4}
value={form.observacoes || ''}
onChange={e => setForm(f => ({ ...f, observacoes: e.target.value }))}
className={inp + " resize-none"}
placeholder="ALERGIAS, HISTÓRICO MÉDICO, OBSERVAÇÕES..."
/>
</div>
<div>
<p className="text-[10px] font-black text-gray-500 uppercase tracking-wider mb-2">
Histórico de Atendimentos <span className="text-teal-600">({meusAgendamentos.length})</span>
</p>
{meusAgendamentos.length === 0 && (
<p className="text-xs text-gray-400 text-center py-4 border-2 border-dashed border-gray-100 rounded-xl">Nenhum atendimento registrado.</p>
)}
<div className="space-y-1.5 max-h-52 overflow-y-auto">
{meusAgendamentos.map(a => (
<div key={a.id} className="flex items-center justify-between border border-gray-100 rounded-lg p-2.5 bg-gray-50/70">
<div>
<p className="font-bold text-gray-800 text-xs uppercase">{a.procedimento}</p>
<p className="text-[10px] text-gray-400">{new Date(a.start).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' })}</p>
</div>
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${
a.status === 'Confirmado' ? 'bg-green-100 text-green-700' :
a.status === 'Pendente' ? 'bg-amber-100 text-amber-700' :
a.status === 'Faltou' ? 'bg-red-100 text-red-600' :
'bg-gray-100 text-gray-600'
}`}>{a.status}</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
{/* Footer navigation */}
<div className="p-4 border-t border-gray-100 flex justify-between items-center flex-shrink-0">
<button
type="button"
onClick={() => page > 0 ? setPage(p => p - 1) : onClose()}
className="px-5 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-lg text-xs uppercase flex items-center gap-1.5"
>
<ChevronLeft size={14} /> {page === 0 ? 'Cancelar' : 'Voltar'}
</button>
{page < EDIT_STEPS.length - 1 ? (
<button
type="button"
onClick={() => setPage(p => p + 1)}
className="px-5 py-2 bg-teal-600 hover:bg-teal-700 text-white font-bold rounded-lg text-xs uppercase flex items-center gap-1.5"
>
Próximo <ChevronRight size={14} />
</button>
) : (
<button
type="button"
onClick={handleSubmit}
className="px-5 py-2 bg-green-600 hover:bg-green-700 text-white font-bold rounded-lg text-xs uppercase flex items-center gap-1.5"
>
<Save size={14} /> Salvar Tudo
</button>
)}
</div>
</div>
</div>
);
};
import { PageHeader } from '../components/PageHeader.tsx';
import { ReceitaModal } from './ReceitaModal.tsx';
import { PedidoExameModal } from './PedidoExameModal.tsx';
import { useGTOStore } from './LancarGTO.tsx';
import { LancarProcedimentoModal } from './LancarProcedimentoModal.tsx';
import { OrcamentoModal } from './OrcamentoModal.tsx';
import { ProcedimentosProntuario } from './ProcedimentosProntuario.tsx';
// ----------- TRATAMENTOS MODAL -----------
const TratamentosModal: React.FC<{ patient: Paciente; onClose: () => void }> = ({ patient, onClose }) => {
const { data: agendamentos, isLoading } = useHybridBackend<Agendamento[]>(HybridBackend.getAgendamentos);
const meus = (agendamentos ?? []).filter(a => a.pacienteNome === patient.nome);
const concluidos = meus.filter(a => a.status === 'Concluido');
const faltou = meus.filter(a => a.status === 'Faltou');
const procedureCounts = concluidos.reduce<Record<string, number>>((acc, a) => {
acc[a.procedimento] = (acc[a.procedimento] || 0) + 1;
return acc;
}, {});
useEffect(() => {
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', h);
return () => document.removeEventListener('keydown', h);
}, [onClose]);
return (
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-2xl flex flex-col animate-in fade-in zoom-in-50 duration-200">
<div className="p-5 border-b border-gray-100 flex items-center justify-between flex-shrink-0">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-teal-100 rounded-lg flex items-center justify-center"><Stethoscope size={16} className="text-teal-600" /></div>
<div>
<p className="text-xs font-black text-gray-800 uppercase">Tratamentos</p>
<p className="text-[10px] text-gray-400 font-medium uppercase">{patient.nome}</p>
</div>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-3 p-5 flex-shrink-0">
<div className="bg-teal-50 rounded-xl p-3 text-center">
<p className="text-2xl font-black text-teal-600">{meus.length}</p>
<p className="text-[10px] font-bold text-teal-400 uppercase">Total Consultas</p>
</div>
<div className="bg-green-50 rounded-xl p-3 text-center">
<p className="text-2xl font-black text-green-600">{concluidos.length}</p>
<p className="text-[10px] font-bold text-green-400 uppercase">Concluídas</p>
</div>
<div className="bg-red-50 rounded-xl p-3 text-center">
<p className="text-2xl font-black text-red-600">{faltou.length}</p>
<p className="text-[10px] font-bold text-red-400 uppercase">Faltas</p>
</div>
</div>
<div className="flex-1 overflow-y-auto px-5 pb-5 space-y-5">
{/* Procedimentos realizados */}
{Object.keys(procedureCounts).length > 0 && (
<div>
<p className="text-[10px] font-black text-gray-500 uppercase tracking-wider mb-2">Procedimentos Realizados</p>
<div className="space-y-1.5">
{Object.entries(procedureCounts).sort(([,a],[,b]) => b - a).map(([proc, count]) => (
<div key={proc} className="flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2">
<p className="text-xs font-bold text-gray-700 uppercase">{proc}</p>
<span className="text-[10px] font-black bg-teal-100 text-teal-700 px-2 py-0.5 rounded-full">{count}x</span>
</div>
))}
</div>
</div>
)}
{/* Histórico cronológico */}
<div>
<p className="text-[10px] font-black text-gray-500 uppercase tracking-wider mb-2">Histórico Cronológico ({meus.length})</p>
{isLoading && <p className="text-xs text-gray-400 text-center py-4">Carregando...</p>}
{!isLoading && meus.length === 0 && <p className="text-xs text-gray-400 text-center py-4 border-2 border-dashed border-gray-100 rounded-xl">Nenhum atendimento registrado.</p>}
<div className="space-y-1.5">
{[...meus].sort((a, b) => new Date(b.start).getTime() - new Date(a.start).getTime()).map(a => (
<div key={a.id} className="flex items-center justify-between border border-gray-100 rounded-lg p-2.5">
<div className="min-w-0 flex-1">
<p className="font-bold text-gray-800 text-xs uppercase truncate">{a.procedimento}</p>
<p className="text-[10px] text-gray-400">{new Date(a.start).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' })}</p>
</div>
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase flex-shrink-0 ml-2 ${
a.status === 'Concluido' ? 'bg-green-100 text-green-700' :
a.status === 'Confirmado' ? 'bg-blue-100 text-blue-700' :
a.status === 'Pendente' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-600'
}`}>{a.status}</span>
</div>
))}
</div>
</div>
</div>
<div className="p-4 border-t border-gray-100 flex justify-end flex-shrink-0">
<button onClick={onClose} className="px-5 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 text-xs font-bold rounded-lg uppercase">Fechar</button>
</div>
</div>
</div>
);
};
// ----------- RELATORIO PACIENTE MODAL -----------
const RelatorioPacienteModal: React.FC<{ patient: Paciente; onClose: () => void }> = ({ patient, onClose }) => {
const { data: agendamentos } = useHybridBackend<Agendamento[]>(HybridBackend.getAgendamentos);
const meus = (agendamentos ?? []).filter(a => a.pacienteNome === patient.nome);
const concluidos = meus.filter(a => a.status === 'Concluido').length;
const faltou = meus.filter(a => a.status === 'Faltou').length;
const hoje = new Date().toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' });
useEffect(() => {
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', h);
return () => document.removeEventListener('keydown', h);
}, [onClose]);
return (
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-2xl flex flex-col animate-in fade-in zoom-in-50 duration-200">
<div className="p-4 border-b border-gray-100 flex items-center justify-between flex-shrink-0 print:hidden">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-violet-100 rounded-lg flex items-center justify-center"><BarChart3 size={16} className="text-violet-600" /></div>
<div>
<p className="text-xs font-black text-gray-800 uppercase">Relatório do Paciente</p>
<p className="text-[10px] text-gray-400 font-medium uppercase">{patient.nome}</p>
</div>
</div>
<div className="flex items-center gap-2">
<button onClick={() => window.print()} className="flex items-center gap-1.5 px-3 py-2 bg-gray-900 text-white rounded-lg text-xs font-bold hover:bg-black">
<Printer size={13} /> Imprimir
</button>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button>
</div>
</div>
<div className="flex-1 overflow-y-auto bg-gray-50 p-6 print:p-0 print:bg-white">
<div className="max-w-2xl mx-auto bg-white rounded-2xl shadow-sm border border-gray-200 p-8 print:shadow-none print:rounded-none print:border-0 print:max-w-full space-y-6">
{/* Cabeçalho */}
<div className="border-b-2 border-gray-800 pb-4">
<h1 className="text-lg font-black text-gray-900 uppercase">Relatório do Paciente</h1>
<p className="text-xs text-gray-500 mt-1">Gerado em {hoje}</p>
</div>
{/* Dados pessoais */}
<div>
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest mb-3 border-b border-gray-100 pb-1">Dados Pessoais</h2>
<div className="grid grid-cols-2 gap-3 text-sm">
<div><span className="text-gray-400 text-xs font-bold uppercase block">Nome</span><p className="font-bold text-gray-800 uppercase">{patient.nome}</p></div>
<div><span className="text-gray-400 text-xs font-bold uppercase block">CPF</span><p className="font-bold text-gray-800">{patient.cpf || '—'}</p></div>
<div><span className="text-gray-400 text-xs font-bold uppercase block">Telefone</span><p className="font-bold text-gray-800">{patient.telefone || '—'}</p></div>
<div><span className="text-gray-400 text-xs font-bold uppercase block">E-mail</span><p className="font-bold text-gray-800">{patient.email || '—'}</p></div>
<div><span className="text-gray-400 text-xs font-bold uppercase block">Nascimento</span><p className="font-bold text-gray-800">{patient.dataNascimento || '—'}</p></div>
<div><span className="text-gray-400 text-xs font-bold uppercase block">Convênio</span><p className="font-bold text-gray-800">{patient.convenio || 'Particular'}</p></div>
</div>
</div>
{/* Endereço */}
{(patient.logradouro || patient.cidade) && (
<div>
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest mb-3 border-b border-gray-100 pb-1 flex items-center gap-1"><MapPin size={11} /> Endereço</h2>
<p className="text-sm text-gray-700">
{[patient.logradouro, patient.numero, patient.complemento].filter(Boolean).join(', ')}
{patient.bairro && <span className="block">{patient.bairro}{patient.cidade && `${patient.cidade}/${patient.estado}`}</span>}
{patient.cep && <span className="block text-gray-400 text-xs">CEP {patient.cep}</span>}
</p>
</div>
)}
{/* Vínculos */}
{(patient.grupoFamiliarNome || patient.indicadoPorNome) && (
<div>
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest mb-3 border-b border-gray-100 pb-1">Vínculos</h2>
<div className="grid grid-cols-2 gap-3 text-sm">
{patient.grupoFamiliarNome && <div><span className="text-gray-400 text-xs font-bold uppercase block">Familiar</span><p className="font-bold text-gray-800 uppercase">{patient.grupoFamiliarNome} {patient.grupoFamiliarRelacao && `(${patient.grupoFamiliarRelacao})`}</p></div>}
{patient.indicadoPorNome && <div><span className="text-gray-400 text-xs font-bold uppercase block">Indicado por</span><p className="font-bold text-gray-800 uppercase">{patient.indicadoPorNome}</p></div>}
</div>
</div>
)}
{/* Resumo de atendimentos */}
<div>
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest mb-3 border-b border-gray-100 pb-1">Resumo de Atendimentos</h2>
<div className="grid grid-cols-3 gap-3 mb-4">
<div className="bg-teal-50 rounded-xl p-3 text-center"><p className="text-xl font-black text-teal-600">{meus.length}</p><p className="text-[10px] font-bold text-teal-400 uppercase">Total</p></div>
<div className="bg-green-50 rounded-xl p-3 text-center"><p className="text-xl font-black text-green-600">{concluidos}</p><p className="text-[10px] font-bold text-green-400 uppercase">Concluídos</p></div>
<div className="bg-red-50 rounded-xl p-3 text-center"><p className="text-xl font-black text-red-600">{faltou}</p><p className="text-[10px] font-bold text-red-400 uppercase">Faltas</p></div>
</div>
<div className="space-y-1.5 max-h-48 overflow-y-auto">
{[...meus].sort((a, b) => new Date(b.start).getTime() - new Date(a.start).getTime()).slice(0, 20).map(a => (
<div key={a.id} className="flex items-center justify-between border border-gray-100 rounded-lg px-3 py-2">
<div><p className="text-xs font-bold text-gray-800 uppercase">{a.procedimento}</p><p className="text-[10px] text-gray-400">{new Date(a.start).toLocaleDateString('pt-BR')}</p></div>
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${a.status === 'Concluido' ? 'bg-green-100 text-green-700' : a.status === 'Faltou' ? 'bg-red-100 text-red-600' : 'bg-amber-100 text-amber-700'}`}>{a.status}</span>
</div>
))}
</div>
</div>
{/* Observações */}
{patient.observacoes && (
<div>
<h2 className="text-xs font-black text-gray-500 uppercase tracking-widest mb-3 border-b border-gray-100 pb-1">Observações Clínicas</h2>
<p className="text-sm text-gray-700 italic">{patient.observacoes}</p>
</div>
)}
<div className="pt-6 border-t-2 border-gray-800 flex justify-between items-end">
<div><p className="text-xs text-gray-500">Assinatura / Responsável</p><div className="h-8" /></div>
<p className="text-xs text-gray-400">{hoje}</p>
</div>
</div>
</div>
</div>
</div>
);
};
const CopyField: React.FC<{ label: string; value?: string | null }> = ({ label, value }) => {
const [copied, setCopied] = useState(false);
const copy = () => {
if (!value) return;
navigator.clipboard.writeText(value).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
};
return (
<div className="flex items-center justify-between bg-gray-50/70 border border-gray-200/50 rounded-lg px-2 py-1.5 gap-1 min-w-0">
<div className="min-w-0 flex-1">
<p className="text-gray-400 text-[9px] font-bold uppercase leading-none mb-0.5">{label}</p>
<p className="text-gray-800 font-semibold text-xs truncate leading-tight">{value || '—'}</p>
</div>
{value && (
<button onClick={copy} className="flex-shrink-0 text-gray-300 hover:text-teal-500 transition-colors ml-1" title={`Copiar ${label}`}>
{copied ? <CheckCircle2 size={13} className="text-green-500" /> : <Copy size={13} />}
</button>
)}
</div>
);
};
type SortOpt = 'recentes' | 'az' | 'za' | 'convenio';
const SORT_OPTS: { key: SortOpt; label: string }[] = [
{ key: 'recentes', label: 'Recentes' },
{ key: 'az', label: 'A→Z' },
{ key: 'za', label: 'Z→A' },
{ key: 'convenio', label: 'Convênio' },
];
export const PatientsView: React.FC = () => {
const [busca, setBusca] = useState('');
const [sort, setSort] = useState<SortOpt>('recentes');
const debouncedBusca = useDebounce(busca, 500);
const fetcher = useCallback(() => HybridBackend.getPacientes(debouncedBusca, sort), [debouncedBusca, sort]);
const { data: result, isLoading, error, refresh } = useHybridBackend<{ data: Paciente[]; total: number }>(fetcher, [debouncedBusca, sort]);
const pacientes = result?.data ?? [];
const totalPacientes = result?.total ?? 0;
const toast = useToast();
const [modal, setModal] = useState<{ type: string | null; patient: Paciente | null }>({ type: null, patient: null });
const [familiaModal, setFamiliaModal] = useState<Paciente | null>(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<HTMLInputElement | HTMLSelectElement>) => {
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 handleAsaasSubmit = (e: React.FormEvent) => {
e.preventDefault();
toast.success(`COBRANÇA GERADA NO ASAAS PARA ${modal.patient?.nome}!`);
closeModal();
};
const displayedPacientes = pacientes;
return (
<div className="h-full flex flex-col relative">
<PageHeader title="PACIENTES">
<button onClick={() => setIsNovoPacienteOpen(true)} className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors uppercase font-bold text-sm shadow-sm">
<Plus size={18} />
NOVO PACIENTE
</button>
</PageHeader>
{/* Search + sort bar */}
<div className="flex items-center gap-2 mb-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" size={16} />
<input
type="text"
placeholder="Nome ou CPF..."
className="w-full pl-9 pr-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-100 focus:border-teal-300 outline-none bg-white"
value={busca}
onChange={(e) => setBusca(e.target.value.toUpperCase())}
/>
</div>
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-1">
{SORT_OPTS.map(o => (
<button
key={o.key}
onClick={() => setSort(o.key)}
className={`px-2.5 py-1 rounded-md text-[11px] font-bold transition-all whitespace-nowrap ${sort === o.key ? 'bg-white text-gray-800 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
>
{o.label}
</button>
))}
</div>
</div>
<div className="grid grid-cols-3 gap-3 overflow-y-auto pb-6">
{isLoading && <div className="col-span-3 text-center p-10 text-gray-500 uppercase font-bold flex justify-center items-center gap-2"><Loader2 className="animate-spin" /> CARREGANDO DADOS DO POSTGRESQL...</div>}
{error && <div className="col-span-3 text-center p-10 text-red-600 uppercase font-bold">ERRO AO CARREGAR: {error.message}</div>}
{displayedPacientes && displayedPacientes.map(p => (
<div key={p.id} className="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-all duration-200 flex flex-col">
<div className="px-3 pt-3 pb-2 flex-grow flex flex-col gap-2">
<div className="flex items-start justify-between gap-1.5">
<h3 className="font-bold text-teal-700 text-[11px] leading-snug uppercase break-words flex-1">{p.nome}</h3>
{p.convenio && (
<span className="bg-green-100 text-green-700 text-[9px] font-black px-1.5 py-0.5 rounded-full uppercase shrink-0">{p.convenio}</span>
)}
</div>
{/* chips família / indicação */}
{(p.grupoFamiliarNome || p.indicadoPorNome) && (
<div className="flex flex-wrap gap-1">
{p.grupoFamiliarNome && (
<button onClick={() => setFamiliaModal(p)}
className="flex items-center gap-1 bg-emerald-50 hover:bg-emerald-100 text-emerald-600 px-1.5 py-0.5 rounded-full text-[9px] font-bold uppercase transition-colors">
<Users size={9} />{p.grupoFamiliarRelacao ? `${p.grupoFamiliarRelacao}: ${p.grupoFamiliarNome}` : p.grupoFamiliarNome}
</button>
)}
{p.indicadoPorNome && (
<span className="flex items-center gap-1 bg-emerald-50 text-emerald-600 px-1.5 py-0.5 rounded-full text-[9px] font-bold uppercase">
<UserCheck size={9} />{p.indicadoPorNome}
</span>
)}
</div>
)}
<div className="grid grid-cols-2 gap-1">
<CopyField label="CPF" value={p.cpf} />
<CopyField label="Telefone" value={p.telefone} />
<CopyField label="Nascimento" value={p.dataNascimento} />
<CopyField label="Convênio" value={p.convenio} />
</div>
</div>
<div className="flex border-t border-gray-100">
<button onClick={() => setModal({ type: 'clinical', patient: p })} className="w-1/2 flex items-center justify-center gap-1.5 py-2 text-teal-600 hover:bg-teal-50 rounded-bl-xl text-[10px] font-bold transition-colors uppercase">
<FolderOpen size={12} /> Clínico
</button>
<div className="border-l border-gray-100" />
<button onClick={() => setModal({ type: 'financial', patient: p })} className="w-1/2 flex items-center justify-center gap-1.5 py-2 text-green-700 hover:bg-green-50 rounded-br-xl text-[10px] font-bold transition-colors uppercase">
<DollarSign size={12} /> Financeiro
</button>
</div>
</div>
))}
</div>
{/* FAMILIA MODAL */}
{familiaModal && (
<FamiliaModal paciente={familiaModal} onClose={() => setFamiliaModal(null)} />
)}
{/* NOVO PACIENTE MODAL */}
{isNovoPacienteOpen && (
<NovoPacienteModal onClose={() => setIsNovoPacienteOpen(false)} onSaved={refresh} />
)}
{/* EDITAR PACIENTE MODAL */}
{modal.type === 'editar' && modal.patient && (
<EditarPacienteModal patient={modal.patient} onClose={closeModal} onSaved={refresh} />
)}
{/* HISTORICO MODAL */}
{modal.type === 'historico' && modal.patient && (
<HistoricoModal patient={modal.patient} onClose={closeModal} />
)}
{/* MODAL MENU CLÍNICO */}
{modal.type === 'clinical' && modal.patient && (
<div className="fixed inset-0 bg-black bg-opacity-40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-2xl flex flex-col p-6 animate-in fade-in zoom-in-50 duration-200">
<div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-3 text-gray-700 font-bold text-xl"><FolderOpen size={28} className="text-gray-400" />Menu Clínico</div>
<button onClick={closeModal} className="text-gray-400 hover:text-gray-600"><X size={20} /></button>
</div>
<p className="text-gray-500 text-sm mb-6 font-bold uppercase">{modal.patient.nome}</p>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 flex-1 content-start">
<button onClick={() => setModal({ type: 'agendamento', patient: modal.patient })} className="flex flex-col items-center justify-center p-5 bg-white border border-gray-200 rounded-xl hover:border-teal-400 hover:bg-teal-50/50 transition-all group shadow-sm"><CalendarDays size={32} className="text-teal-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm text-center">AGENDAR</span></button>
<button onClick={() => setModal({ type: 'historico', patient: modal.patient })} className="flex flex-col items-center justify-center p-5 bg-white border border-gray-200 rounded-xl hover:border-teal-400 hover:bg-teal-50/50 transition-all group shadow-sm"><Calendar size={32} className="text-teal-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm text-center">AGENDAMENTOS</span></button>
<button onClick={() => setModal({ type: 'tratamentos', patient: modal.patient })} className="flex flex-col items-center justify-center p-5 bg-white border border-gray-200 rounded-xl hover:border-teal-400 hover:bg-teal-50/50 transition-all group shadow-sm"><Stethoscope size={32} className="text-teal-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm text-center">TRATAMENTOS</span></button>
<button onClick={() => setModal({ type: 'procedimentos', patient: modal.patient })} className="flex flex-col items-center justify-center p-5 bg-white border border-gray-200 rounded-xl hover:border-teal-400 hover:bg-teal-50/50 transition-all group shadow-sm"><ClipboardList size={32} className="text-teal-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm text-center">PROCEDIMENTOS</span></button>
<button onClick={() => {
useGTOStore.getState().setPaciente(modal.patient);
closeModal();
window.location.hash = '#/lancargto';
}} className="flex flex-col items-center justify-center p-5 bg-white border border-gray-200 rounded-xl hover:border-yellow-400 hover:bg-yellow-50/50 transition-all group shadow-sm"><ClipboardList size={32} className="text-yellow-600 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm text-center">GTOs</span></button>
<button onClick={() => setModal({ type: 'doc-receita', patient: modal.patient })} className="flex flex-col items-center justify-center p-5 bg-white border border-gray-200 rounded-xl hover:border-red-400 hover:bg-red-50/50 transition-all group shadow-sm"><FileText size={32} className="text-red-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm text-center">RECEITA</span></button>
<button onClick={() => setModal({ type: 'doc-raiox', patient: modal.patient })} className="flex flex-col items-center justify-center p-5 bg-white border border-gray-200 rounded-xl hover:border-purple-400 hover:bg-purple-50/50 transition-all group shadow-sm"><RadioTower size={32} className="text-purple-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm text-center">RAIO-X</span></button>
<button onClick={() => setModal({ type: 'relatorio-paciente', patient: modal.patient })} className="flex flex-col items-center justify-center p-5 bg-white border border-gray-200 rounded-xl hover:border-violet-400 hover:bg-violet-50/50 transition-all group shadow-sm"><BarChart3 size={32} className="text-violet-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm text-center">RELATÓRIOS</span></button>
<button onClick={() => setModal({ type: 'editar', patient: modal.patient })} className="flex flex-col items-center justify-center p-5 bg-white border border-gray-200 rounded-xl hover:border-teal-400 hover:bg-teal-50/50 transition-all group shadow-sm"><Pencil size={32} className="text-teal-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm text-center">EDITAR DADOS</span></button>
</div>
</div>
</div>
)}
{/* MODAL DE AGENDAMENTO */}
{modal.type === 'agendamento' && modal.patient && (
<AgendamentoModal patient={modal.patient} onClose={closeModal} />
)}
{/* RECEITA MODAL */}
{modal.type === 'doc-receita' && modal.patient && (
<ReceitaModal paciente={modal.patient} onClose={closeModal} />
)}
{/* PEDIDO DE EXAME MODAL */}
{modal.type === 'doc-raiox' && modal.patient && (
<PedidoExameModal paciente={modal.patient} onClose={closeModal} />
)}
{/* TRATAMENTOS MODAL */}
{modal.type === 'procedimentos' && modal.patient && (
<ProcedimentosProntuario patient={modal.patient} onClose={closeModal} />
)}
{modal.type === 'tratamentos' && modal.patient && (
<TratamentosModal patient={modal.patient} onClose={closeModal} />
)}
{/* RELATÓRIO PACIENTE MODAL */}
{modal.type === 'relatorio-paciente' && modal.patient && (
<RelatorioPacienteModal patient={modal.patient} onClose={closeModal} />
)}
{/* LANÇAR PROCEDIMENTO MODAL */}
{modal.type === 'lancar' && modal.patient && (
<LancarProcedimentoModal patient={modal.patient} onClose={closeModal} onSaved={refresh} />
)}
{/* CONTRATO MODAL */}
{modal.type === 'orcamento' && modal.patient && (
<OrcamentoModal paciente={modal.patient} onClose={closeModal} onSaved={refresh} />
)}
{/* MODAL MENU FINANCEIRO */}
{modal.type === 'financial' && modal.patient && (
<div className="fixed inset-0 bg-black bg-opacity-40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-2xl flex flex-col p-6 animate-in fade-in zoom-in-50 duration-200">
<div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-3 text-gray-700 font-bold text-xl"><Wallet size={28} className="text-gray-400" />Menu Financeiro</div>
<button onClick={closeModal} className="text-gray-400 hover:text-gray-600"><X size={20} /></button>
</div>
<p className="text-gray-500 text-sm mb-8 font-bold uppercase">{modal.patient.nome}</p>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<button onClick={() => setModal({ type: 'lancar', patient: modal.patient })} className="flex flex-col items-center justify-center p-6 bg-white border border-gray-200 rounded-xl hover:border-green-400 hover:bg-green-50/50 transition-all group shadow-sm"><PlusCircle size={32} className="text-green-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm">LANÇAR</span></button>
<button onClick={() => setModal({ type: 'asaas', patient: modal.patient })} className="flex flex-col items-center justify-center p-6 bg-white border border-gray-200 rounded-xl hover:border-teal-400 hover:bg-teal-50/50 transition-all group shadow-sm"><Banknote size={32} className="text-teal-600 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm">ASAAS</span></button>
<button onClick={() => setModal({ type: 'historico', patient: modal.patient })} className="flex flex-col items-center justify-center p-6 bg-white border border-gray-200 rounded-xl hover:border-gray-400 hover:bg-gray-50/50 transition-all group shadow-sm"><History size={32} className="text-gray-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm">HISTÓRICO</span></button>
<button onClick={() => setModal({ type: 'orcamento', patient: modal.patient })} className="flex flex-col items-center justify-center p-6 bg-white border border-gray-200 rounded-xl hover:border-emerald-400 hover:bg-emerald-50/50 transition-all group shadow-sm"><FileText size={32} className="text-emerald-500 mb-2 group-hover:scale-110 transition-transform" /><span className="font-bold text-gray-700 text-sm">ORÇAMENTOS</span></button>
</div>
</div>
</div>
)}
{/* ASAAS MODAL */}
{modal.type === 'asaas' && modal.patient && (
<div className="fixed inset-0 bg-black bg-opacity-40 z-50 flex items-center justify-center backdrop-blur-sm p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-xl flex flex-col animate-in fade-in zoom-in-50 duration-200">
<form onSubmit={handleAsaasSubmit}>
<div className="p-6 border-b border-gray-200">
<div className="flex justify-between items-start">
<div>
<h2 className="text-lg font-bold text-gray-800 flex items-center gap-2">
<span className="w-6 h-4 bg-yellow-400 rounded-sm flex items-center justify-center text-yellow-800">
<CreditCard size={14} />
</span>
PARCELAMENTO INTELIGENTE (ASAAS)
</h2>
<p className="text-sm text-gray-500 mt-2">
Cliente: <span className="font-bold uppercase">{modal.patient.nome}</span>
</p>
</div>
<button type="button" onClick={closeModal} className="text-gray-400 hover:text-gray-600"><X size={20} /></button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-4">
<div>
<label className="block text-xs font-bold text-gray-500 mb-1 uppercase">TRATAMENTO</label>
<input type="text" name="tratamento" value={asaasForm.tratamento} onChange={handleAsaasFormChange} placeholder="EX: ORTODONTIA COMPLETA" className="w-full border border-gray-300 rounded-md p-2 text-sm uppercase-input" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-gray-500 mb-1 uppercase">VALOR TOTAL (R$)</label>
<input type="number" step="0.01" name="valorTotal" value={asaasForm.valorTotal} onChange={handleAsaasFormChange} placeholder="0,00" className="w-full border border-gray-300 rounded-md p-2 text-sm" />
</div>
<div>
<label className="block text-xs font-bold text-gray-500 mb-1 uppercase">DESCONTO (R$)</label>
<input type="number" step="0.01" name="desconto" value={asaasForm.desconto} onChange={handleAsaasFormChange} placeholder="0,00" className="w-full border border-gray-300 rounded-md p-2 text-sm" />
</div>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 mb-1 uppercase">ENTRADA (R$)</label>
<input type="number" step="0.01" name="entrada" value={asaasForm.entrada} onChange={handleAsaasFormChange} placeholder="0,00" className="w-full border border-gray-300 rounded-md p-2 text-sm" />
</div>
<div className="bg-teal-50/70 p-3 rounded-md flex justify-between items-center border border-teal-100/50">
<span className="font-semibold text-teal-700 text-sm">SALDO A PARCELAR:</span>
<span className="font-bold text-teal-700 text-lg">{saldoAParcelar.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}</span>
</div>
<div className="grid grid-cols-2 gap-4 pt-2">
<div>
<label className="block text-xs font-bold text-gray-500 mb-1 uppercase">PARCELAS (ATÉ 48X)</label>
<select name="parcelas" value={asaasForm.parcelas} onChange={handleAsaasFormChange} className="w-full border border-gray-300 rounded-md p-2 text-sm bg-white">
{Array.from({ length: 48 }, (_, i) => i + 1).map(num => (
<option key={num} value={num}>{num}x</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 mb-1 uppercase">VENCIMENTO 1ª PARCELA</label>
<input type="date" name="vencimento" value={asaasForm.vencimento} onChange={handleAsaasFormChange} className="w-full border border-gray-300 rounded-md p-2 text-sm" />
</div>
</div>
<div className="text-center text-teal-600 font-bold text-lg py-2">
{asaasForm.parcelas}x DE {valorParcela.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
</div>
</div>
<div className="p-6 bg-gray-50 border-t border-gray-200 flex justify-end gap-3">
<button type="button" onClick={closeModal} className="px-6 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold rounded-lg transition-colors text-sm uppercase">CANCELAR</button>
<button type="submit" className="px-6 py-2 bg-teal-600 hover:bg-teal-700 text-white font-bold rounded-lg shadow-sm flex items-center justify-center gap-2 transition-colors text-sm uppercase">
<Rocket size={16} />
GERAR CARNÊ/PIX
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};