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'; // --- 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 [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(), descricao: '', valor: '', dataVencimento: new Date().toISOString().split('T')[0], status: 'Pendente' as 'Pago' | 'Pendente' | 'Atrasado', formaPagamento: 'Pix' as 'Boleto' | 'Pix' | 'Cartão' | 'Dinheiro', }); 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 (!selectedPatient || !form.descricao || !form.valor) { toast.error('PREENCHA TODOS OS CAMPOS OBRIGATÓRIOS.'); return; } try { await HybridBackend.saveFinanceiro({ ...form, pacienteNome: selectedPatient.nome, valor: parseFloat(form.valor), }); toast.success('LANÇAMENTO SALVO COM SUCESSO!'); onSaved(); onClose(); } catch { toast.error('ERRO AO SALVAR LANÇAMENTO.'); } }; return (
{/* HEADER */}

NOVO LANÇAMENTO

{/* BODY */}
{/* PACIENTE SEARCH */}
{!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-blue-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 */}
{/* FOOTER */}
); }; // ----------- MAIN VIEW ----------- import { PageHeader } from '../components/PageHeader.tsx'; 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 toast = useToast(); 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 handleConfirmarPagamento = async (item: FinanceiroItem) => { if (!window.confirm(`CONFIRMAR PAGAMENTO DE R$ ${Number(item.valor).toFixed(2)} DE ${item.pacienteNome}?`)) return; try { await HybridBackend.updateFinanceiro({ ...item, status: 'Pago' }); toast.success(`PAGAMENTO DE ${item.pacienteNome} CONFIRMADO!`); fetchData(); } catch { toast.error('ERRO AO CONFIRMAR PAGAMENTO.'); } }; const handleExcluir = async (item: FinanceiroItem) => { if (!window.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} />}
); };