agendapro/admin/src/components/Payments.tsx

126 lines
6.7 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Search, Download, Eye, CheckCircle, AlertCircle, XCircle, DollarSign, Filter } from 'lucide-react';
interface Payment {
id: string; tenant: string; plan: string; amount: number; status: string; method: string; date: string; invoiceId: string;
}
const statusMap: Record<string, { label: string; cls: string; icon: typeof CheckCircle }> = {
paid: { label: 'Pago', cls: 'badge-success', icon: CheckCircle },
pending: { label: 'Pendente', cls: 'badge-warning', icon: AlertCircle },
failed: { label: 'Falhou', cls: 'badge-danger', icon: XCircle },
refunded: { label: 'Estornado', cls: 'badge-info', icon: DollarSign },
};
export default function Payments() {
const [payments, setPayments] = useState<Payment[]>([]);
const [search, setSearch] = useState('');
const [filterStatus, setFilterStatus] = useState('all');
const [filterMonth, setFilterMonth] = useState('all');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/payments')
.then(res => res.json())
.then(data => {
setPayments(Array.isArray(data) ? data : []);
})
.catch(err => console.error(err))
.finally(() => setLoading(false));
}, []);
const filtered = payments.filter(p => {
const matchSearch = p.tenant.toLowerCase().includes(search.toLowerCase()) || p.invoiceId.toLowerCase().includes(search.toLowerCase());
const matchStatus = filterStatus === 'all' || p.status === filterStatus;
const matchMonth = filterMonth === 'all' || p.date.startsWith(filterMonth);
return matchSearch && matchStatus && matchMonth;
});
const totalPaid = filtered.filter(p => p.status === 'paid').reduce((s, p) => s + p.amount, 0);
const totalPending = filtered.filter(p => p.status === 'pending').reduce((s, p) => s + p.amount, 0);
const totalFailed = filtered.filter(p => p.status === 'failed').reduce((s, p) => s + p.amount, 0);
// Helper to get unique months for the filter
const months = Array.from(new Set(payments.map(p => p.date.substring(0, 7)))).sort().reverse();
const handleExportCSV = () => {
if (filtered.length === 0) return;
const headers = ['Fatura', 'Estabelecimento', 'Plano', 'Valor', 'Status', 'Metodo', 'Data'];
const rows = filtered.map(p => [
p.invoiceId,
p.tenant,
p.plan,
p.amount.toFixed(2),
statusMap[p.status]?.label || p.status,
p.method,
new Date(p.date + 'T12:00:00').toLocaleDateString('pt-BR')
]);
const csvContent = [headers.join(','), ...rows.map(e => e.map(val => `"${val}"`).join(','))].join('\n');
const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `pagamentos_${new Date().toISOString().slice(0,10)}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
if (loading) return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando pagamentos...</div>;
return (
<div className="slide-up">
<div className="stats-grid" style={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
<div className="stat-card"><div className="stat-icon green"><CheckCircle size={24} /></div><div><div className="stat-value" style={{ fontSize: 22 }}>R$ {totalPaid.toFixed(2)}</div><div className="stat-label">Total Recebido</div></div></div>
<div className="stat-card"><div className="stat-icon amber"><AlertCircle size={24} /></div><div><div className="stat-value" style={{ fontSize: 22 }}>R$ {totalPending.toFixed(2)}</div><div className="stat-label">Pendente</div></div></div>
<div className="stat-card"><div className="stat-icon red"><XCircle size={24} /></div><div><div className="stat-value" style={{ fontSize: 22 }}>R$ {totalFailed.toFixed(2)}</div><div className="stat-label">Falhas</div></div></div>
</div>
<div className="flex-between mb-6">
<div className="flex-gap" style={{ gap: 12 }}>
<div style={{ position: 'relative' }}>
<Search size={18} style={{ position: 'absolute', left: 14, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
<input className="form-input" placeholder="Buscar por tenant ou fatura..." style={{ width: 300, paddingLeft: 42 }} value={search} onChange={e => setSearch(e.target.value)} />
</div>
<select className="form-select" style={{ width: 140 }} value={filterStatus} onChange={e => setFilterStatus(e.target.value)}>
<option value="all">Todos</option><option value="paid">Pagos</option><option value="pending">Pendentes</option><option value="failed">Falhas</option><option value="refunded">Estornados</option>
</select>
<select className="form-select" style={{ width: 140 }} value={filterMonth} onChange={e => setFilterMonth(e.target.value)}>
<option value="all">Todo período</option>
{months.map(m => (
<option key={m} value={m}>{m}</option>
))}
</select>
</div>
<button className="btn btn-secondary" onClick={handleExportCSV}><Download size={16} /> Exportar CSV</button>
</div>
<div className="card" style={{ padding: 0 }}>
<div className="table-container">
<table>
<thead><tr><th>Fatura</th><th>Estabelecimento</th><th>Plano</th><th>Valor</th><th>Status</th><th>Método</th><th>Data</th></tr></thead>
<tbody>
{filtered.map(p => {
const st = statusMap[p.status] || statusMap.pending;
return (
<tr key={p.id}>
<td><span style={{ fontFamily: 'monospace', fontSize: 13, color: 'var(--text-muted)' }}>{p.invoiceId}</span></td>
<td style={{ fontWeight: 600 }}>{p.tenant}</td>
<td><span className="badge badge-amber">{p.plan}</span></td>
<td style={{ fontWeight: 700 }}>R$ {p.amount.toFixed(2)}</td>
<td><span className={`badge ${st.cls}`}>{st.label}</span></td>
<td style={{ fontSize: 13, color: 'var(--text-secondary)' }}>{p.method}</td>
<td style={{ fontSize: 13, color: 'var(--text-secondary)' }}>{new Date(p.date + 'T12:00:00').toLocaleDateString('pt-BR')}</td>
</tr>
);
})}
{filtered.length === 0 && <tr><td colSpan={7} style={{ textAlign: 'center', padding: 40, color: 'var(--text-muted)' }}>Nenhum pagamento encontrado</td></tr>}
</tbody>
</table>
</div>
</div>
</div>
);
}