import { useState, useEffect, useMemo, useRef } from 'react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { Link } from 'react-router-dom'; import { useNavigate } from 'react-router-dom'; import Layout from '../../components/Layout'; import LoadingButton from '../../components/LoadingButton'; import SortTh from '../../components/SortTh'; import StatusBadge from '../../components/StatusBadge'; import { supabase } from '../../lib/supabase'; import { generateInvoicePDF, generateReceiptPDF } from '../../lib/invoice'; import { useAuth } from '../../context/AuthContext'; import { useSortable } from '../../hooks/useSortable'; import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; import SubcontractorInvoiceDetailView from '../../components/SubcontractorInvoiceDetailView'; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; const invoiceStatusLabel = (status) => { if (status === 'sent') return 'Invoiced'; return status ? status.charAt(0).toUpperCase() + status.slice(1) : '—'; }; function ClientFinanceStatCard({ label, value, sub, iconBg, iconColor, iconPath }) { return (
{label}
{value}
{sub ?
{sub}
: null}
); } function ClientFinanceTooltip({ active, payload, label, year }) { if (!active || !payload?.length) return null; return (
{label} {year}
{payload.map(p => (
{p.name}: ${Number(p.value).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
))}
); } function ClientInvoiceModal({ invoice, onClose }) { const navigate = useNavigate(); const [downloading, setDownloading] = useState(''); const { sortKey, sortDir, toggle, sort } = useSortable('description'); if (!invoice) return null; const items = invoice.items || []; const company = invoice.company || null; const isOverdue = invoice.status !== 'paid' && invoice.due_date && new Date(invoice.due_date) < new Date(); const total = Number(invoice.total || 0); const handleDownloadInvoice = async () => { if (downloading) return; setDownloading('invoice'); try { await generateInvoicePDF(invoice, company, items); } finally { setDownloading(''); } }; const handleDownloadReceipt = async () => { if (downloading) return; setDownloading('receipt'); try { await generateReceiptPDF(invoice, company, items); } finally { setDownloading(''); } }; const openPay = () => { onClose?.(); navigate(`/pay/${encodeURIComponent(invoice.invoice_number)}`); }; const baseItems = [...items].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)); const tableItems = sort(baseItems.map(item => ({ ...item, _inferredType: item.submission_id ? { label: 'Revision', badgeClass: 'badge-client_revision' } : { label: 'New', badgeClass: 'badge-initial' }, })), (item, key) => { if (key === 'type') return item._inferredType?.label || ''; if (key === 'description') return item.description || ''; if (key === 'quantity') return Number(item.quantity || 0); if (key === 'unit_price') return Number(item.unit_price || 0); if (key === 'line_total') return Number(item.quantity || 0) * Number(item.unit_price || 0); return ''; }); return ( } onClose={onClose} blockClose={Boolean(downloading)} footerActions={<> {invoice.status === 'sent' && } Download Invoice {invoice.status === 'paid' && Download Receipt} } >
{invoice.bill_to || company?.name || '—'}
{company?.id && (
View Company
)} } rightCardTitle="Invoice Details" rightCardBody={

{invoice.invoice_date ? new Date(invoice.invoice_date).toLocaleDateString() : '—'}

{invoice.due_date ? new Date(invoice.due_date).toLocaleDateString() : '—'}

Net 30

${total.toFixed(2)}

{invoice.paid_at &&

{new Date(invoice.paid_at).toLocaleDateString()}

}
} sortKey={sortKey} sortDir={sortDir} onSort={toggle} items={tableItems} notes={invoice.notes} total={total} />
); } export default function MyInvoices() { const { currentUser } = useAuth(); const companyIds = (currentUser?.companies?.length ? currentUser.companies : (currentUser?.company ? [currentUser.company] : [])) .map(company => company?.id) .filter(Boolean); const [invoices, setInvoices] = useState([]); const [loading, setLoading] = useState(true); const [activeInvoice, setActiveInvoice] = useState(null); const [yearFilter, setYearFilter] = useState(String(new Date().getFullYear())); const [companyFilter, setCompanyFilter] = useState('all'); const [filterMenuOpen, setFilterMenuOpen] = useState(false); const filterMenuRef = useRef(null); const { sortKey, sortDir, toggle, sort } = useSortable('invoice_date', 'desc'); useEffect(() => { async function load() { if (companyIds.length === 0) { setInvoices([]); setLoading(false); return; } const { data } = await supabase .from('invoices') .select('*, company:companies(id, name), items:invoice_items(*)') .in('company_id', companyIds) .order('created_at', { ascending: false }); setInvoices((data || []).filter(inv => inv.status !== 'draft')); setLoading(false); } load(); }, [currentUser?.id]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!filterMenuOpen) return; function onDocClick(e) { if (filterMenuRef.current && !filterMenuRef.current.contains(e.target)) setFilterMenuOpen(false); } document.addEventListener('mousedown', onDocClick); return () => document.removeEventListener('mousedown', onDocClick); }, [filterMenuOpen]); const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const invoiceYears = [...new Set(invoices.map(i => i.invoice_date?.slice(0, 4)).filter(Boolean))].sort((a, b) => b.localeCompare(a)); const availableCompanyOptions = (currentUser?.companies?.length ? currentUser.companies : (currentUser?.company ? [currentUser.company] : [])) .filter(company => company?.id && invoices.some(inv => inv.company_id === company.id)) .sort((a, b) => (a.name || '').localeCompare(b.name || '')); const chartYear = Number(yearFilter) || new Date().getFullYear(); const filteredInvoices = invoices.filter(inv => { if (yearFilter !== 'all' && inv.invoice_date?.slice(0, 4) !== yearFilter) return false; if (companyFilter !== 'all' && inv.company_id !== companyFilter) return false; return true; }); const chartData = useMemo(() => MONTHS.map((month, mi) => { const paidAmt = filteredInvoices.filter(i => i.status === 'paid' && new Date(i.invoice_date).getFullYear() === chartYear && new Date(i.invoice_date).getMonth() === mi).reduce((s, i) => s + Number(i.total || 0), 0); const outAmt = filteredInvoices.filter(i => i.status === 'sent' && new Date(i.invoice_date).getFullYear() === chartYear && new Date(i.invoice_date).getMonth() === mi).reduce((s, i) => s + Number(i.total || 0), 0); return { month, Paid: +paidAmt.toFixed(2), Outstanding: +outAmt.toFixed(2) }; }), [filteredInvoices, chartYear]); const outstanding = filteredInvoices.filter(i => i.status === 'sent').reduce((s, i) => s + Number(i.total || 0), 0); const paid = filteredInvoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total || 0), 0); const overdueCount = filteredInvoices.filter(inv => inv.status !== 'paid' && inv.due_date && new Date(inv.due_date) < new Date()).length; const paidCount = filteredInvoices.filter(i => i.status === 'paid').length; const sortedInvoices = sort(filteredInvoices, (inv, key) => { if (key === 'invoice_date' || key === 'due_date') return new Date(inv[key] || 0).getTime(); if (key === 'total') return Number(inv.total || 0); if (key === 'company') return inv.company?.name || ''; return inv[key] || ''; }); return (
Invoice Overview {chartYear}
`$${v >= 1000 ? (v / 1000).toFixed(0) + 'k' : v}`} width={45} /> } />
'} /> '} /> '} /> '} />
{filterMenuOpen && (
Year
{invoiceYears.map(year => ( ))} {availableCompanyOptions.length > 1 && ( <>
Company
{availableCompanyOptions.map(company => ( ))} )}
)}
{filteredInvoices.length === 0 ? (
No invoices
) : (
Invoice #CompanyIssuedDueStatusTotal {sortedInvoices.map(inv => { const isOverdue = inv.status !== 'paid' && inv.due_date && new Date(inv.due_date) < new Date(); return ( ); })}
{inv.company?.id ? ( {inv.company?.name || inv.bill_to || '—'} ) : ( inv.company?.name || inv.bill_to || '—' )} {inv.invoice_date ? new Date(inv.invoice_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'} {inv.due_date ? new Date(inv.due_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'} ${Number(inv.total || 0).toFixed(2)}
)}
setActiveInvoice(null)} /> ); }