Simplify client invoice list: remove summary stats, strip card to essentials
Shows invoice number, issue/due date, overdue flag, item count, status, download PDF. Removes outstanding/paid/overdue summary grid and total amount display. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Layout from '../../components/Layout';
|
import Layout from '../../components/Layout';
|
||||||
|
import LoadingButton from '../../components/LoadingButton';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { generateInvoicePDF } from '../../lib/invoice';
|
import { generateInvoicePDF } from '../../lib/invoice';
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_a
|
|||||||
export default function MyInvoices() {
|
export default function MyInvoices() {
|
||||||
const [invoices, setInvoices] = useState([]);
|
const [invoices, setInvoices] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [generatingInvoiceId, setGeneratingInvoiceId] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -22,12 +24,15 @@ export default function MyInvoices() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDownload = async (invoice) => {
|
const handleDownload = async (invoice) => {
|
||||||
await generateInvoicePDF(invoice, invoice.company, invoice.items || []);
|
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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
@@ -37,17 +42,6 @@ export default function MyInvoices() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stats-grid" style={{ marginBottom: 24 }}>
|
|
||||||
<div className="stat-card">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p style={{ color: 'var(--text-muted)' }}>Loading...</p>
|
<p style={{ color: 'var(--text-muted)' }}>Loading...</p>
|
||||||
) : invoices.length === 0 ? (
|
) : invoices.length === 0 ? (
|
||||||
@@ -56,35 +50,28 @@ export default function MyInvoices() {
|
|||||||
<p>Your invoices will appear here once they are sent.</p>
|
<p>Your invoices will appear here once they are sent.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
{invoices.map(inv => {
|
{invoices.map(inv => {
|
||||||
const isOverdue = inv.status !== 'paid' && new Date(inv.due_date) < new Date();
|
const isOverdue = inv.status !== 'paid' && new Date(inv.due_date) < new Date();
|
||||||
return (
|
return (
|
||||||
<div key={inv.id} className="request-card">
|
<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 className="request-card-header">
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div>
|
<div style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)', marginBottom: 4 }}>{inv.invoice_number}</div>
|
||||||
<div className="request-card-title">{inv.invoice_number}</div>
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||||
<div className="request-card-meta">
|
Issued {new Date(inv.invoice_date).toLocaleDateString()} · Due{' '}
|
||||||
Issued {new Date(inv.invoice_date).toLocaleDateString()} · Due{' '}
|
<span style={{ color: isOverdue ? 'var(--danger)' : 'inherit' }}>
|
||||||
<span style={{ color: isOverdue ? 'var(--danger)' : 'inherit' }}>
|
{new Date(inv.due_date).toLocaleDateString()}
|
||||||
{new Date(inv.due_date).toLocaleDateString()}
|
</span>
|
||||||
</span>
|
{isOverdue && ' · Overdue'}
|
||||||
{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 }}>
|
|
||||||
<span className={`badge badge-${statusColor[inv.status]}`} style={{ textTransform: 'capitalize' }}>{inv.status}</span>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--accent)' }}>${Number(inv.total).toFixed(2)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{inv.items && inv.items.length > 0 && (
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: 10 }}>
|
<span className={`badge badge-${statusColor[inv.status]}`} style={{ textTransform: 'capitalize' }}>{inv.status}</span>
|
||||||
{inv.items.map(i => i.description).join(' · ')}
|
<LoadingButton className="btn btn-outline btn-sm" loading={generatingInvoiceId === inv.id} disabled={Boolean(generatingInvoiceId)} loadingText="Generating..." onClick={() => handleDownload(inv)}>
|
||||||
</div>
|
Download PDF
|
||||||
)}
|
</LoadingButton>
|
||||||
<button className="btn btn-outline btn-sm" onClick={() => handleDownload(inv)}>
|
</div>
|
||||||
Download PDF
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user