94 lines
3.7 KiB
React
94 lines
3.7 KiB
React
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import Layout from '../../components/Layout';
|
|
import StatusBadge from '../../components/StatusBadge';
|
|
import { supabase } from '../../lib/supabase';
|
|
import { useAuth } from '../../context/AuthContext';
|
|
import { readPageCache, writePageCache } from '../../lib/pageCache';
|
|
|
|
const STATUS_BADGE = { draft: 'not_started', submitted: 'in_progress', paid: 'client_approved' };
|
|
const STATUS_LABEL = { draft: 'Draft', submitted: 'Submitted', paid: 'Paid' };
|
|
|
|
function fmt(val) {
|
|
return `$${Number(val || 0).toFixed(2)}`;
|
|
}
|
|
|
|
function invoiceTotal(items) {
|
|
return (items || []).reduce((s, i) => s + Number(i.unit_price || 0) * Number(i.quantity || 1), 0);
|
|
}
|
|
|
|
export default function MyInvoices() {
|
|
const { currentUser } = useAuth();
|
|
const navigate = useNavigate();
|
|
const cacheKey = `ext-invoices:${currentUser?.id}`;
|
|
const cached = readPageCache(cacheKey, 3 * 60_000);
|
|
const [invoices, setInvoices] = useState(() => cached || []);
|
|
const [loading, setLoading] = useState(() => !cached);
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (!currentUser?.id) { setLoading(false); return; }
|
|
supabase
|
|
.from('subcontractor_invoices')
|
|
.select('*, items:subcontractor_invoice_items(*)')
|
|
.order('created_at', { ascending: false })
|
|
.then(({ data, error: err }) => {
|
|
if (err) setError(err.message);
|
|
else { setInvoices(data || []); writePageCache(cacheKey, data || []); }
|
|
setLoading(false);
|
|
});
|
|
}, [currentUser?.id]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="page-header">
|
|
<div>
|
|
<div className="page-title">Invoices</div>
|
|
<div className="page-subtitle">Submit invoices to Fourge Branding for your completed work.</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 4 }}>Payment terms: NET 30 from the date Fourge receives payment from the client.</div>
|
|
</div>
|
|
<button className="btn btn-primary" onClick={() => navigate('/my-invoices-sub/new')} disabled={loading}>
|
|
+ New Invoice
|
|
</button>
|
|
</div>
|
|
|
|
{error && <div className="notification notification-info" style={{ marginBottom: 16 }}>{error}</div>}
|
|
|
|
{loading ? (
|
|
<div className="empty-state">Loading invoices...</div>
|
|
) : invoices.length === 0 ? (
|
|
<div className="empty-state">
|
|
<h3>No invoices yet</h3>
|
|
<p>Create your first invoice to get paid for your completed work.</p>
|
|
</div>
|
|
) : (
|
|
<div className="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Invoice #</th>
|
|
<th>Submitted</th>
|
|
<th>Status</th>
|
|
<th style={{ textAlign: 'right' }}>Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{invoices.map(inv => {
|
|
const total = invoiceTotal(inv.items);
|
|
return (
|
|
<tr key={inv.id} style={{ cursor: 'pointer' }} onClick={() => navigate(`/my-invoices-sub/${inv.id}`)}>
|
|
<td style={{ fontWeight: 700 }}>{inv.invoice_number}</td>
|
|
<td>{inv.submitted_at ? new Date(inv.submitted_at).toLocaleDateString() : <span style={{ color: 'var(--text-muted)' }}>—</span>}</td>
|
|
<td><StatusBadge status={STATUS_BADGE[inv.status]} label={STATUS_LABEL[inv.status]} /></td>
|
|
<td style={{ textAlign: 'right', fontWeight: 700 }}>{fmt(total)}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</Layout>
|
|
);
|
|
}
|