import React, { useState, useEffect, useCallback } from 'react'; import { Filter, Plus, TrendingUp, Clock, TrendingDown, CheckCircle, Download, Trash2, X, DollarSign, Loader2 } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { FinanceiroItem, Paciente } from '../types.ts'; import { useToast } from '../contexts/ToastContext.tsx'; import { useConfirm } from '../contexts/ConfirmContext.tsx'; // --- Reusable Debounce Hook --- 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; } // ----------- NOVO LANÇAMENTO MODAL ----------- const NovoLancamentoModal: React.FC<{ onClose: () => void; onSaved: () => void; }> = ({ onClose, onSaved }) => { const toast = useToast(); const confirm = useConfirm(); const [selectedPatient, setSelectedPatient] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [isSearchFocused, setIsSearchFocused] = useState(false); const debouncedSearchTerm = useDebounce(searchTerm, 400); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); useEffect(() => { if (!debouncedSearchTerm) { setSearchResults([]); return; } setIsSearching(true); HybridBackend.getPacientes(debouncedSearchTerm).then(results => { setSearchResults(results.data || []); setIsSearching(false); }); }, [debouncedSearchTerm]); const handlePatientSelect = (patient: Paciente) => { setSelectedPatient(patient); setSearchTerm(''); setIsSearchFocused(false); }; const [form, setForm] = useState({ id: crypto.randomUUID(), tipo: 'RECEITA' as 'RECEITA' | 'DESPESA', descricao: '', valor: '', dataVencimento: new Date().toISOString().split('T')[0], status: 'Pendente' as 'Pago' | 'Pendente' | 'Atrasado', formaPagamento: 'Pix' as 'Boleto' | 'Pix' | 'Cartão' | 'Dinheiro', fornecedor: '', centroCusto: '', recorrente: false, }); const isDespesa = form.tipo === 'DESPESA'; 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.descricao || !form.valor || (!isDespesa && !selectedPatient)) { toast.error('PREENCHA TODOS OS CAMPOS OBRIGATÓRIOS.'); return; } try { const { centroCusto, ...rest } = form; await HybridBackend.saveFinanceiro({ ...rest, origem: 'manual', valor: parseFloat(form.valor), ...(selectedPatient ? { paciente_id: selectedPatient.id, pacienteNome: selectedPatient.nome } : {}), ...(isDespesa ? { centro_custo: centroCusto, fornecedor: form.fornecedor, recorrente: form.recorrente } : { fornecedor: undefined, centro_custo: undefined, recorrente: undefined }), }); toast.success('LANÇAMENTO SALVO COM SUCESSO!'); onSaved(); onClose(); } catch { toast.error('ERRO AO SALVAR LANÇAMENTO.'); } }; return (
{/* HEADER */}

NOVO LANÇAMENTO

{/* BODY */}
{/* TIPO: RECEITA / DESPESA */}
{/* PACIENTE SEARCH (apenas receita) */} {!isDespesa && (
{!selectedPatient ? (
setSearchTerm(e.target.value.toUpperCase())} onFocus={() => setIsSearchFocused(true)} onBlur={() => setTimeout(() => setIsSearchFocused(false), 200)} placeholder="NOME, CPF OU TELEFONE..." className="w-full lg:w-1/2 bg-gray-100 p-3 lg:p-4 rounded-lg font-bold text-gray-800 uppercase focus:border-teal-500 outline-none border-2 border-transparent focus:border-2 text-sm lg:text-base" /> {isSearchFocused && debouncedSearchTerm && (
    {isSearching &&
  • BUSCANDO...
  • } {searchResults.map(p => (
  • handlePatientSelect(p)} className="p-3 hover:bg-gray-100 cursor-pointer uppercase font-semibold text-sm"> {p.nome} {p.cpf && · CPF {p.cpf}} {p.telefone && · TEL {p.telefone}}
  • ))} {!isSearching && searchResults.length === 0 &&
  • NENHUM PACIENTE ENCONTRADO.
  • }
)}
) : (
{selectedPatient.nome}
)}
)}
{/* DESCRIÇÃO */}
setForm(f => ({ ...f, descricao: e.target.value.toUpperCase() }))} className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 uppercase-input text-sm" placeholder="EX: CONSULTA AVALIAÇÃO, LIMPEZA..." />
{/* VALOR */}
setForm(f => ({ ...f, valor: e.target.value }))} className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 text-sm" placeholder="0,00" />
{/* VENCIMENTO */}
setForm(f => ({ ...f, dataVencimento: e.target.value }))} className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 text-sm" />
{/* STATUS */}
{/* FORMA DE PAGAMENTO */}
{/* CAMPOS DE DESPESA (H2.3) */} {isDespesa && ( <>
setForm(f => ({ ...f, fornecedor: e.target.value.toUpperCase() }))} className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 uppercase-input text-sm" placeholder="EX: DENTAL CREMER, ALUGUEL..." />
setForm(f => ({ ...f, centroCusto: e.target.value.toUpperCase() }))} className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 uppercase-input text-sm" placeholder="EX: MATERIAIS, ALUGUEL..." /> {['ALUGUEL', 'MATERIAIS', 'SALÁRIOS', 'MARKETING', 'EQUIPAMENTOS', 'IMPOSTOS', 'LABORATÓRIO', 'MANUTENÇÃO', 'UTILIDADES', 'OUTROS'].map(c =>
)}
{/* FOOTER */}
); }; // ----------- MAIN VIEW ----------- import { PageHeader } from '../components/PageHeader.tsx'; // ----------- BAIXA DE PAGAMENTO MODAL (H2.1) ----------- const FORMAS = ['Pix', 'Cartão', 'Boleto', 'Dinheiro'] as const; const BaixaPagamentoModal: React.FC<{ item: FinanceiroItem; onClose: () => void; onConfirm: (dados: { pago_em: string; formaPagamento: string }) => void; }> = ({ item, onClose, onConfirm }) => { const [pagoEm, setPagoEm] = useState(new Date().toISOString().split('T')[0]); const [forma, setForma] = useState(item.formaPagamento || 'Pix'); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [onClose]); return (
e.stopPropagation()}>

Baixa de pagamento

{item.pacienteNome || '—'}

{item.descricao}

R$ {Number(item.valor).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

setPagoEm(e.target.value)} className="w-full bg-gray-50 border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400 focus:bg-white transition-colors" />
); }; export const FinanceiroView: React.FC = () => { const [transacoes, setTransacoes] = useState([]); const [loading, setLoading] = useState(true); const [filtroStatus, setFiltroStatus] = useState<'TODOS' | 'Pago' | 'Pendente' | 'Atrasado'>('TODOS'); const [showFiltro, setShowFiltro] = useState(false); const [isNovoOpen, setIsNovoOpen] = useState(false); const [baixaItem, setBaixaItem] = useState(null); const toast = useToast(); const confirm = useConfirm(); const fetchData = useCallback(() => { setLoading(true); HybridBackend.getFinanceiro().then((data) => { setTransacoes(data); setLoading(false); }); }, []); useEffect(() => { fetchData(); }, [fetchData]); const transacoesFiltradas = filtroStatus === 'TODOS' ? transacoes : transacoes.filter(t => t.status === filtroStatus); const totalRecebido = transacoes.filter(t => t.status === 'Pago').reduce((acc, t) => acc + Number(t.valor), 0); const totalPendente = transacoes.filter(t => t.status === 'Pendente').reduce((acc, t) => acc + Number(t.valor), 0); const totalAtrasado = transacoes.filter(t => t.status === 'Atrasado').reduce((acc, t) => acc + Number(t.valor), 0); const handleBaixaConfirm = async (dados: { pago_em: string; formaPagamento: string }) => { const item = baixaItem; if (!item) return; // useConfirm extra para valores altos if (Number(item.valor) >= 1000 && !await confirm(`CONFIRMAR BAIXA DE R$ ${Number(item.valor).toLocaleString('pt-BR', { minimumFractionDigits: 2 })} DE ${item.pacienteNome}?`)) return; try { await HybridBackend.updateFinanceiro({ ...item, status: 'Pago', pago_em: dados.pago_em, formaPagamento: dados.formaPagamento as FinanceiroItem['formaPagamento'] }); toast.success(`PAGAMENTO DE ${item.pacienteNome} CONFIRMADO!`); setBaixaItem(null); fetchData(); } catch { toast.error('ERRO AO CONFIRMAR PAGAMENTO.'); } }; const handleExcluir = async (item: FinanceiroItem) => { if (!await confirm(`TEM CERTEZA QUE DESEJA EXCLUIR O LANÇAMENTO DE ${item.pacienteNome}? ESTA AÇÃO NÃO PODE SER DESFEITA.`)) return; try { await HybridBackend.deleteFinanceiro(item.id); toast.success('LANÇAMENTO EXCLUÍDO!'); fetchData(); } catch { toast.error('ERRO AO EXCLUIR LANÇAMENTO.'); } }; const handleDownload = (item: FinanceiroItem) => { toast.success(`RECIBO DE ${item.pacienteNome} GERADO COM SUCESSO!`); }; const filtroLabel = filtroStatus === 'TODOS' ? 'FILTRAR' : filtroStatus.toUpperCase(); return (
{/* FILTRAR */}
{showFiltro && (
{(['TODOS', 'Pago', 'Pendente', 'Atrasado'] as const).map(status => ( ))}
)}
{/* NOVO LANÇAMENTO */}
{/* DASHBOARD CARDS */}

RECEBIDO (MÊS)

R$ {totalRecebido.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

A RECEBER / PENDENTE

R$ {totalPendente.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

EM ATRASO

R$ {totalAtrasado.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

{/* LISTA DE TRANSAÇÕES */}

LANÇAMENTOS {filtroStatus !== 'TODOS' && ({filtroStatus.toUpperCase()})}

{transacoesFiltradas.length} REGISTRO(S)
{loading ? ( ) : transacoesFiltradas.length === 0 ? ( ) : transacoesFiltradas.map(t => ( ))}
PACIENTE DESCRIÇÃO VENCIMENTO VALOR STATUS AÇÕES
CARREGANDO...
NENHUM LANÇAMENTO ENCONTRADO {filtroStatus !== 'TODOS' && `COM STATUS "${filtroStatus.toUpperCase()}"`}.
{t.pacienteNome} {t.descricao} {t.formaPagamento?.toUpperCase()} {new Date(t.dataVencimento).toLocaleDateString('pt-BR')} R$ {Number(t.valor).toLocaleString('pt-BR', { minimumFractionDigits: 2 })} {t.status}
{t.status !== 'Pago' && ( )}
{/* CLOSE FILTRO DROPDOWN when clicking outside */} {showFiltro &&
setShowFiltro(false)} />} {/* NOVO LANÇAMENTO MODAL */} {isNovoOpen && setIsNovoOpen(false)} onSaved={fetchData} />} {/* BAIXA DE PAGAMENTO MODAL (H2.1) */} {baixaItem && setBaixaItem(null)} onConfirm={handleBaixaConfirm} />}
); };