Files
fourge-portal/src/pages/client/MyInvoices.jsx
T
Krao Hasanee ed30bb3a18 Restore summary stats and price to client invoice page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 12:12:19 -04:00

103 lines
4.7 KiB
React

import { useState, useEffect } from 'react';
import Layout from '../../components/Layout';
import LoadingButton from '../../components/LoadingButton';
import { supabase } from '../../lib/supabase';
import { generateInvoicePDF } from '../../lib/invoice';
const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' };
export default function MyInvoices() {
const [invoices, setInvoices] = useState([]);
const [loading, setLoading] = useState(true);
const [generatingInvoiceId, setGeneratingInvoiceId] = useState('');
useEffect(() => {
async function load() {
const { data } = await supabase
.from('invoices')
.select('*, company:companies(name), items:invoice_items(*)')
.order('created_at', { ascending: false });
setInvoices((data || []).filter(inv => inv.status !== 'draft'));
setLoading(false);
}
load();
}, []);
const handleDownload = async (invoice) => {
if (generatingInvoiceId) return;
setGeneratingInvoiceId(invoice.id);
try {
await generateInvoicePDF(invoice, invoice.company, invoice.items || []);
} finally {
setGeneratingInvoiceId('');
}
};
const outstanding = invoices.filter(i => i.status === 'sent').reduce((s, i) => s + Number(i.total), 0);
const paid = invoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total), 0);
const overdueCount = invoices.filter(inv => inv.status !== 'paid' && new Date(inv.due_date) < new Date()).length;
return (
<Layout>
<div className="page-header">
<div>
<div className="page-title">Invoices</div>
<div className="page-subtitle">{invoices.length} invoice{invoices.length !== 1 ? 's' : ''}</div>
</div>
</div>
<div className="stats-grid" style={{ marginBottom: 24 }}>
<div className="stat-card stat-card-highlight">
<div className="stat-value" style={{ fontSize: 22 }}>${outstanding.toFixed(2)}</div>
<div className="stat-label">Outstanding</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ fontSize: 22 }}>${paid.toFixed(2)}</div>
<div className="stat-label">Paid</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ fontSize: 22, color: overdueCount > 0 ? 'var(--danger)' : undefined }}>{overdueCount}</div>
<div className="stat-label">Overdue</div>
</div>
</div>
{loading ? (
<p style={{ color: 'var(--text-muted)' }}>Loading...</p>
) : invoices.length === 0 ? (
<div className="empty-state">
<h3>No invoices yet</h3>
<p>Your invoices will appear here once they are sent.</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{invoices.map(inv => {
const isOverdue = inv.status !== 'paid' && new Date(inv.due_date) < new Date();
return (
<div key={inv.id} className="interactive-surface" style={{ border: '1px solid var(--border)', borderRadius: 8, background: 'var(--card-bg)', padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)', marginBottom: 4 }}>{inv.invoice_number}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
Issued {new Date(inv.invoice_date).toLocaleDateString()} · Due{' '}
<span style={{ color: isOverdue ? 'var(--danger)' : 'inherit' }}>
{new Date(inv.due_date).toLocaleDateString()}
</span>
{isOverdue && ' · Overdue'}
{inv.items?.length > 0 && ` · ${inv.items.length} item${inv.items.length !== 1 ? 's' : ''}`}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
<span className={`badge badge-${statusColor[inv.status]}`} style={{ textTransform: 'capitalize' }}>{inv.status}</span>
<div style={{ fontSize: 16, fontWeight: 700, color: 'var(--accent)' }}>${Number(inv.total).toFixed(2)}</div>
<LoadingButton className="btn btn-outline btn-sm" loading={generatingInvoiceId === inv.id} disabled={Boolean(generatingInvoiceId)} loadingText="Generating..." onClick={() => handleDownload(inv)}>
Download PDF
</LoadingButton>
</div>
</div>
);
})}
</div>
)}
</Layout>
);
}