import { useEffect, useState } from 'react'; import { useAuth } from '../context/AuthContext'; import { ExternalLink, Filter, CreditCard, Printer, X } from 'lucide-react'; import { useSearchParams } from 'react-router-dom'; import type { Payment, Boleto } from '../types'; type FilterType = 'all' | 'pending' | 'paid' | 'overdue'; export default function Financeiro() { const { token } = useAuth(); const [searchParams] = useSearchParams(); const [payments, setPayments] = useState([]); const [boletos, setBoletos] = useState([]); const [filter, setFilter] = useState((searchParams.get('filter') as FilterType) || 'all'); const [loading, setLoading] = useState(true); const [receiptPayment, setReceiptPayment] = useState(null); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); useEffect(() => { const urlFilter = searchParams.get('filter') as FilterType; if (urlFilter && ['all', 'pending', 'paid', 'overdue'].includes(urlFilter)) { setFilter(urlFilter); } }, [searchParams]); useEffect(() => { const fetchData = async () => { try { const headers = { Authorization: `Bearer ${token}` }; const [payRes, bolRes] = await Promise.all([ fetch('/api/portal/financeiro', { headers }), fetch('/api/portal/boletos', { headers }), ]); const payData = await payRes.json(); const bolData = await bolRes.json(); const fetchedPayments = payData.payments || []; const fetchedBoletos = bolData.boletos || []; // We use only the detailed payments list (JSON source) as the primary data // to show labels like "Parcela 1/3". The boletos (Supabase source) are // kept only for the PDF link lookup in getBoletoLink. setPayments(fetchedPayments); setBoletos(fetchedBoletos); } catch (err) { console.error(err); } finally { setLoading(false); } }; if (token) fetchData(); }, [token]); const normalizeStatus = (payment: Payment) => { const s = payment.status?.toLowerCase(); if (['paid', 'received', 'confirmed', 'pago'].includes(s)) return 'paid'; if (['cancelled', 'cancelado'].includes(s)) return 'cancelled'; // Check if explicitly overdue in database if (['overdue', 'atrasado', 'atrasada', 'vencido'].includes(s)) return 'overdue'; return 'pending'; }; const isPaid = (p: Payment) => normalizeStatus(p) === 'paid'; const isPending = (p: Payment) => ['pending', 'overdue'].includes(normalizeStatus(p)); const filtered = payments.filter(p => { if (filter === 'all') return true; return normalizeStatus(p) === filter; }); const sorted = [...filtered].sort((a, b) => { const dateA = new Date(a.dueDate).getTime(); const dateB = new Date(b.dueDate).getTime(); return sortOrder === 'asc' ? dateA - dateB : dateB - dateA; }); const formatCurrency = (val: number) => val.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); const formatDate = (d: string) => { const date = new Date(d + 'T00:00:00'); return date.toLocaleDateString('pt-BR'); }; const getStatusBadge = (p: Payment) => { const norm = normalizeStatus(p); const map: Record = { paid: { className: 'badge badge-success', label: 'Pago' }, pending: { className: 'badge badge-warning', label: 'Pendente' }, overdue: { className: 'badge badge-danger', label: 'Atrasado' }, cancelled: { className: 'badge badge-info', label: 'Cancelado' }, }; const s = map[norm] || { className: 'badge badge-info', label: norm }; return {s.label}; }; const getReceiptLink = (payment: Payment): string | null => { if ((payment as any).transactionReceiptUrl) return (payment as any).transactionReceiptUrl; if ((payment as any).transaction_receipt_url) return (payment as any).transaction_receipt_url; const asaasId = payment.asaasPaymentId || (payment as any).asaas_payment_id; let boleto = null; if (asaasId) { boleto = boletos.find(b => (b as any).asaas_payment_id === asaasId); } if (!boleto) { boleto = boletos.find(b => (b as any).vencimento === payment.dueDate && Math.abs(Number((b as any).valor) - (payment.amount - (payment.discount || 0))) < 1 ); } if (!boleto) return null; return (boleto as any).link_recibo || (boleto as any).transaction_receipt_url || null; }; const handleOpenReceipt = (payment: Payment) => { const receiptUrl = getReceiptLink(payment); if (receiptUrl) { window.open(receiptUrl, '_blank', 'noopener,noreferrer'); } else { setReceiptPayment(payment); } }; const getBoletoLink = (payment: Payment) => { if (payment.asaasPaymentUrl) return payment.asaasPaymentUrl; if ((payment as any).link_boleto) return (payment as any).link_boleto; const asaasId = payment.asaasPaymentId || (payment as any).asaas_payment_id; let boleto = null; if (asaasId) { boleto = boletos.find(b => (b as any).asaas_payment_id === asaasId); } if (!boleto) { boleto = boletos.find(b => (b as any).vencimento === payment.dueDate && Math.abs(Number((b as any).valor) - (payment.amount - (payment.discount || 0))) < 1 ); } return (boleto as any)?.link_boleto || null; }; const getEffectiveValue = (payment: Payment) => { const baseAmount = payment.amount || 0; const discount = payment.discount || 0; const netAmount = baseAmount - discount; const status = normalizeStatus(payment); // Try to find matching boleto from Supabase sync const asaasId = payment.asaasPaymentId || (payment as any).asaas_payment_id; let boleto = null; if (asaasId) { boleto = boletos.find(b => (b as any).asaas_payment_id === asaasId); } if (!boleto) { // Fallback: Match by due date and base amount (allowing for interest/fines) boleto = boletos.find(b => { const bVenc = (b as any).vencimento; const bVal = Number((b as any).valor); // Exact date match if (bVenc === payment.dueDate) { // If value is exactly base or exactly net if (Math.abs(bVal - baseAmount) < 1 || Math.abs(bVal - netAmount) < 1) return true; // If it's overdue, the boleto value will be HIGHER than baseAmount if (status === 'overdue' && bVal > netAmount) return true; } return false; }); } // If we have a boleto and it is overdue or paid, use current Asaas value if ((status === 'overdue' || status === 'paid') && (boleto as any).valor) { return Number((boleto as any).valor); } } // Default: use the discounted base value (net amount) return netAmount; }; const totalPending = payments .filter(p => isPending(p)) .reduce((s, p) => s + getEffectiveValue(p), 0); const totalPaid = payments .filter(p => isPaid(p)) .reduce((s, p) => s + getEffectiveValue(p), 0); const filters: { key: FilterType; label: string }[] = [ { key: 'all', label: 'Todos' }, { key: 'pending', label: 'Pendentes' }, { key: 'paid', label: 'Pagos' }, { key: 'overdue', label: 'Atrasados' }, ]; if (loading) { return (
); } return (

Financeiro

Acompanhe seus pagamentos e boletos

{/* Summary Cards */}

TOTAL EM ABERTO

0 ? 'var(--color-warning)' : 'var(--color-success)' }}> {formatCurrency(totalPending)}

TOTAL PAGO

{formatCurrency(totalPaid)}

TOTAL DE PARCELAS

{payments.length}

{/* Filters */}
{filters.map(f => ( ))}
ORDEM:
{/* Table */}
{sorted.length === 0 ? (

Nenhum pagamento encontrado

) : (
{sorted.map((payment, idx) => { const link = getBoletoLink(payment); return ( ); })}
Descrição Vencimento Valor Desconto {filter === 'paid' ? 'Valor Pago' : filter === 'all' ? 'Valor / A Pagar' : 'A Pagar'} Status Ação

{payment.description || `Parcela ${payment.installmentNumber || '—'}`}

{payment.totalInstallments && (

{payment.installmentNumber}/{payment.totalInstallments}

)}
{formatDate(payment.dueDate)} {formatCurrency(payment.amount)} {payment.discount ? `- ${formatCurrency(payment.discount)}` : '—'}
{formatCurrency(getEffectiveValue(payment))} {normalizeStatus(payment) === 'paid' && ( • Pago )}
{getStatusBadge(payment)} {isPaid(payment) ? ( ) : isPending(payment) && link ? ( Ver Boleto ) : ( )}
)}
{receiptPayment && (
setReceiptPayment(null)}>
e.stopPropagation()}>

Recibo de Pagamento

Referência: {receiptPayment.description || `Parcela ${receiptPayment.installmentNumber || '—'}`}
Valor Pago: {formatCurrency(receiptPayment.amount - (receiptPayment.discount || 0))}
Data de Vencimento: {formatDate(receiptPayment.dueDate)}
Status: Quitado
{receiptPayment.asaasPaymentId && (
Cód. Transação: {receiptPayment.asaasPaymentId}
)}
)}
); }