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, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup';
import InvoicePopupTable from '../../components/InvoicePopupTable';
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 (
{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 '';
});
const F = POPUP_FIELD_LABEL;
return (
}
metaContent={<>
Invoice Date
{invoice.invoice_date ? new Date(invoice.invoice_date).toLocaleDateString() : '—'}
Due Date
{invoice.due_date ? new Date(invoice.due_date).toLocaleDateString() : '—'}
{invoice.paid_at && Paid On
{new Date(invoice.paid_at).toLocaleDateString()}
}
{company?.id && }
>}
footerActions={<>
{invoice.status === 'sent' && }
Download Invoice
{invoice.status === 'paid' && Download Receipt}
>}
>
);
}
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 #
Company
Issued
Due
Status
Total
{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)} />
);
}