import React, { useState, useMemo } from 'react'; import { X, Search, Plus, Minus, Trash2, DollarSign, Check, ChevronDown, Loader2, Tag, Percent, Banknote } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { Paciente, Procedimento, Especialidade, Plano } from '../types.ts'; import { useHybridBackend } from '../hooks/useHybridBackend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; // ─── Tipos internos ──────────────────────────────────────────────────────────── interface LancItem { uid: string; proc: Procedimento; quantidade: number; dente: string; face: string; arco: string; observacao: string; valorUnit: number; } const FACES = ['V', 'L', 'M', 'D', 'O']; const ARCOS = ['Superior', 'Inferior', 'Ambas']; const FORMAS_PAG = ['Pix', 'Cartão', 'Dinheiro', 'Boleto'] as const; const STATUS_LIST = ['Pendente', 'Pago', 'Atrasado'] as const; const fmt = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); function addMonths(date: string, n: number): string { const d = new Date(date + 'T12:00:00'); d.setMonth(d.getMonth() + n); return d.toISOString().slice(0, 10); } // ─── Chip de procedimento selecionado ───────────────────────────────────────── const ItemCard: React.FC<{ item: LancItem; onChange: (patch: Partial) => void; onRemove: () => void; }> = ({ item, onChange, onRemove }) => { const [open, setOpen] = useState(item.proc.tipo_regiao !== 'GERAL'); const inp = "border border-gray-200 rounded-lg px-2 py-1.5 text-xs focus:ring-2 focus:ring-teal-100 outline-none w-full"; return (

{item.proc.nome}

{item.proc.especialidadeNome && ( {item.proc.especialidadeNome} )} {fmt(item.valorUnit * item.quantidade)} {item.quantidade > 1 && {item.quantidade}× {fmt(item.valorUnit)}}
{item.quantidade}
{open && (
{item.proc.tipo_regiao === 'DENTE' && (
onChange({ dente: e.target.value })} className={inp} placeholder="ex: 16, 21-22" />
)} {item.proc.tipo_regiao === 'ARCO' && (
)} {item.proc.exige_face && (
{FACES.map(f => ( ))}
)}
onChange({ observacao: e.target.value })} className={inp} placeholder="observação opcional" />
)}
); }; // ─── Modal principal ────────────────────────────────────────────────────────── export const LancarProcedimentoModal: React.FC<{ patient: Paciente; onClose: () => void; onSaved: () => void; }> = ({ patient, onClose, onSaved }) => { const toast = useToast(); const { data: procsRaw } = useHybridBackend(HybridBackend.getProcedimentos); const { data: espsRaw } = useHybridBackend(HybridBackend.getEspecialidades); const { data: planosRaw } = useHybridBackend(HybridBackend.getPlanos); const procs = procsRaw ?? []; const esps = espsRaw ?? []; const planos = planosRaw ?? []; const [busca, setBusca] = useState(''); const [espFiltro, setEspFiltro] = useState(''); const [items, setItems] = useState([]); const [saving, setSaving] = useState(false); // Pagamento const [descTipo, setDescTipo] = useState<'R$' | '%'>('R$'); const [descValor, setDescValor] = useState(''); const [entrada, setEntrada] = useState(''); const [parcelas, setParcelas] = useState(1); const [primVenc, setPrimVenc] = useState(new Date().toISOString().slice(0, 10)); const [formaPag, setFormaPag] = useState('Pix'); const [statusPag, setStatusPag] = useState('Pendente'); // Plano vigente do paciente const planoVigente = useMemo(() => { if (!patient.convenio) return null; return planos.find(p => p.nome === patient.convenio) ?? null; }, [patient.convenio, planos]); const getValorProc = (proc: Procedimento): number => { if (planoVigente) { const vp = proc.valoresPlanos?.find(v => v.planoId === planoVigente.id); if (vp) return vp.valor; } return proc.valorParticular ?? 0; }; const addProc = (proc: Procedimento) => { if (items.some(i => i.proc.id === proc.id)) { setItems(prev => prev.map(i => i.proc.id === proc.id ? { ...i, quantidade: i.quantidade + 1 } : i)); return; } setItems(prev => [...prev, { uid: `${proc.id}_${Date.now()}`, proc, quantidade: 1, dente: '', face: '', arco: '', observacao: '', valorUnit: getValorProc(proc), }]); }; const updateItem = (uid: string, patch: Partial) => setItems(prev => prev.map(i => i.uid === uid ? { ...i, ...patch } : i)); const removeItem = (uid: string) => setItems(prev => prev.filter(i => i.uid !== uid)); const subtotal = items.reduce((s, i) => s + i.valorUnit * i.quantidade, 0); const descNum = parseFloat(descValor) || 0; const descReais = descTipo === '%' ? subtotal * descNum / 100 : descNum; const entradaNum = parseFloat(entrada) || 0; const restante = Math.max(0, subtotal - descReais - entradaNum); const valorParcela = parcelas > 0 ? restante / parcelas : 0; const procsFiltrados = useMemo(() => { return procs.filter(p => { if (espFiltro && p.especialidadeId !== espFiltro) return false; if (busca && !p.nome.toLowerCase().includes(busca.toLowerCase())) return false; return true; }); }, [procs, espFiltro, busca]); const procsPorEsp = useMemo(() => { const grupos: Record = {}; procsFiltrados.forEach(p => { const key = p.especialidadeId || '__geral'; if (!grupos[key]) { grupos[key] = { esp: esps.find(e => e.id === p.especialidadeId) ?? null, procs: [] }; } grupos[key].procs.push(p); }); return Object.values(grupos).sort((a, b) => (a.esp?.ordem ?? 99) - (b.esp?.ordem ?? 99)); }, [procsFiltrados, esps]); const handleSave = async () => { if (items.length === 0) { toast.error('SELECIONE AO MENOS UM PROCEDIMENTO'); return; } setSaving(true); try { const entriesToCreate: any[] = []; // FKs de origem (Épico 1): sempre liga ao paciente; procedimento_id só quando há 1 item (não ambíguo) const fkBase = { paciente_id: patient.id, procedimento_id: items.length === 1 ? items[0].proc.id : null, origem: 'procedimento', }; const baseDesc = items.map(i => { const region = [i.dente, i.arco, i.face].filter(Boolean).join('/'); return i.proc.nome + (region ? ` (${region})` : '') + (i.quantidade > 1 ? ` x${i.quantidade}` : ''); }).join(', '); if (entradaNum > 0) { entriesToCreate.push({ id: crypto.randomUUID(), ...fkBase, pacienteNome: patient.nome, descricao: `ENTRADA — ${baseDesc}`, valor: entradaNum, dataVencimento: primVenc, status: 'Pago', formaPagamento: formaPag, }); } for (let i = 0; i < parcelas; i++) { const label = parcelas > 1 ? ` — ${i + 1}/${parcelas}` : ''; entriesToCreate.push({ id: crypto.randomUUID(), ...fkBase, pacienteNome: patient.nome, descricao: baseDesc + label, valor: parseFloat(valorParcela.toFixed(2)), dataVencimento: entradaNum > 0 ? addMonths(primVenc, i + 1) : addMonths(primVenc, i), status: i === 0 && parcelas === 1 ? statusPag : (i === 0 ? statusPag : 'Pendente'), formaPagamento: formaPag, }); } for (const entry of entriesToCreate) { await HybridBackend.saveFinanceiro(entry); } toast.success(`${entriesToCreate.length} LANÇAMENTO(S) GERADO(S) COM SUCESSO!`); onSaved(); onClose(); } catch { toast.error('ERRO AO SALVAR LANÇAMENTOS'); } finally { setSaving(false); } }; const inp = "border border-gray-200 rounded-lg px-2.5 py-2 text-sm focus:ring-2 focus:ring-teal-100 outline-none w-full bg-white"; const jaAdicionado = (id: string) => items.some(i => i.proc.id === id); return (
{/* Header */}

Lançar Procedimento

{patient.nome} {planoVigente && {planoVigente.nome}}

{/* Body */}
{/* ── Catálogo (esquerda) ── */}
{/* Busca + filtro especialidade */}
setBusca(e.target.value)} placeholder="Buscar procedimento..." className="w-full pl-8 pr-3 py-2 border border-gray-200 rounded-lg text-xs focus:ring-2 focus:ring-teal-100 outline-none bg-white" />
{esps.map(e => ( ))}
{/* Lista procedimentos */}
{!procsRaw &&
} {procsPorEsp.map((grupo, gi) => (
{grupo.esp && (

{grupo.esp.nome}

)}
{grupo.procs.map(p => { const val = getValorProc(p); const adicionado = jaAdicionado(p.id); return ( ); })}
))} {procsRaw && procsFiltrados.length === 0 && (

Nenhum procedimento encontrado

)}
{/* ── Selecionados + pagamento (direita) ── */}
{/* Items selecionados */}
{items.length === 0 ? (

Nenhum procedimento selecionado

Clique nos procedimentos ao lado para adicionar

) : ( items.map(item => ( updateItem(item.uid, patch)} onRemove={() => removeItem(item.uid)} /> )) )}
{/* ── Painel financeiro ── */}
{/* Subtotal */}
Subtotal {fmt(subtotal)}
{/* Desconto + entrada */}
setDescValor(e.target.value)} placeholder="0" className={inp + " text-sm"} />
{descReais > 0 &&

− {fmt(descReais)}

}
setEntrada(e.target.value)} placeholder="0" className={inp} /> {entradaNum > 0 &&

− {fmt(entradaNum)} (marca como Pago)

}
{/* Restante + parcelas */}
Restante a parcelar {fmt(restante)}
setPrimVenc(e.target.value)} className={inp + " text-xs"} />
{parcelas > 1 && (
{parcelas}x de {fmt(valorParcela)}
)}

Total geral

{fmt(entradaNum + restante)}

{/* Ações */}
); };