import { useState, useEffect, useMemo, useRef } from 'react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; import { DashboardBanner } from '../../lib/dashboardBanner'; import { useLocation, useNavigate } from 'react-router-dom'; import Layout from '../../components/Layout'; import SortTh from '../../components/SortTh'; import StatusBadge from '../../components/StatusBadge'; import { useSortable } from '../../hooks/useSortable'; import { supabase } from '../../lib/supabase'; import { useAuth } from '../../context/AuthContext'; import { readPageCache, writePageCache } from '../../lib/pageCache'; import { exportCPAPackage, generateSubcontractorPOPDF, generateInvoicePDF } from '../../lib/invoice'; import { withTimeout } from '../../lib/withTimeout'; import LoadingButton from '../../components/LoadingButton'; import { blobToEmailAttachment, sendEmail } from '../../lib/email'; import { popupOverlayStyle, popupSurfaceStyle } from '../../lib/popupStyles'; import FileAttachment from '../../components/FileAttachment'; import { isReviewShadowDescription } from '../../lib/taskVersions'; import { getRevisionChargeQuantity, isCompletedVersionEligible, isInitialVersionEligible } from '../../lib/invoiceVersionRules'; import { TeamInvoiceDetailPanel } from './TeamInvoiceDetail'; import InvoiceDetailPopup, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup'; import InvoicePopupTable from '../../components/InvoicePopupTable'; const CATEGORIES = ['Software', 'Contractor', 'Advertising', 'Office', 'Travel', 'Meals', 'Equipment', 'Other']; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; const poStatusColor = { draft: 'not_started', sent: 'in_progress', approved: 'client_approved', ready_to_pay: 'in_progress', paid: 'client_approved', cancelled: 'needs_revision', }; const poStatusLabel = { draft: 'Draft', sent: 'Sent', approved: 'Approved', ready_to_pay: 'Ready to Pay', paid: 'Paid', cancelled: 'Cancelled', }; const invoiceStatusBadgeLabel = (status) => { if (status === 'sent') return 'Invoiced'; return status ? status.charAt(0).toUpperCase() + status.slice(1) : '—'; }; const RECEIPT_BUCKET = 'expense-receipts'; const FIELD_LABEL_STYLE = { fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.8, display: 'block', marginBottom: 4 }; const FIELD_INPUT_STYLE = { minHeight: 42, margin: 0 }; const FINANCE_MODAL_INPUT_STYLE = { ...FIELD_INPUT_STYLE, width: '100%', padding: '0 10px', fontSize: 13, color: 'var(--text-primary)', background: 'var(--card-bg-2)', border: '1px solid var(--border)', borderRadius: 6, fontFamily: 'inherit', boxSizing: 'border-box', }; const FINANCE_MODAL_TEXTAREA_STYLE = { ...FINANCE_MODAL_INPUT_STYLE, minHeight: 72, padding: '10px', resize: 'vertical', }; const FINANCE_MODAL_LINE_ITEM_INPUT_STYLE = { ...FINANCE_MODAL_INPUT_STYLE, minHeight: 32, padding: '0 8px', }; const FINANCE_MODAL_NUMBER_INPUT_STYLE = { ...FINANCE_MODAL_LINE_ITEM_INPUT_STYLE, fontVariantNumeric: 'tabular-nums', }; const FINANCE_MODAL_AMOUNT_FIELD_STYLE = { display: 'flex', alignItems: 'center', gap: 8, minHeight: 42, width: '100%', padding: '0 12px', background: 'var(--card-bg-2)', border: '1px solid var(--border)', borderRadius: 6, boxSizing: 'border-box', }; const FINANCE_MODAL_AMOUNT_PREFIX_STYLE = { flexShrink: 0, fontSize: 16, fontWeight: 500, lineHeight: 1, color: 'var(--text-secondary)', }; const FINANCE_MODAL_AMOUNT_INPUT_STYLE = { flex: 1, minWidth: 0, margin: 0, padding: 0, border: 'none', outline: 'none', background: 'transparent', boxShadow: 'none', fontFamily: 'inherit', fontSize: 22, fontWeight: 500, lineHeight: 1.1, textAlign: 'right', color: 'var(--text-primary)', fontVariantNumeric: 'tabular-nums', appearance: 'textfield', }; const FINANCE_MODAL_TOTAL_VALUE_STYLE = { fontSize: 24, fontWeight: 500, lineHeight: 1.1, color: 'var(--accent)', fontVariantNumeric: 'tabular-nums', }; function parseSubInvoiceItemVersionNumber(item) { if (Number.isFinite(Number(item?.version_number))) return Number(item.version_number); const match = String(item?.description || '').match(/\bR(\d{2})\b/i); return match ? Number(match[1]) : 0; } function subInvoiceItemVersionLabel(item) { return `R${String(parseSubInvoiceItemVersionNumber(item)).padStart(2, '0')}`; } function subInvoiceItemWorkLabel(item) { return parseSubInvoiceItemVersionNumber(item) > 0 ? 'Revision' : 'New'; } function cleanSubInvoiceItemDescription(item) { return String(item?.description || '—').replace(/\s*[–-]\s*R\d{2}\b/i, '').trim() || '—'; } function asArray(value) { if (Array.isArray(value)) return value; if (value == null) return []; return typeof value === 'object' ? [value] : []; } async function fetchSubInvoiceTaskTypeMap(taskIds) { const uniqueTaskIds = [...new Set((taskIds || []).filter(Boolean))]; if (uniqueTaskIds.length === 0) return {}; const { data, error } = await supabase .from('tasks') .select('id, title, submissions(service_type, type)') .in('id', uniqueTaskIds); if (error) throw error; const next = {}; for (const task of (data || [])) { const submissions = asArray(task.submissions); const initial = submissions.find((submission) => submission?.type === 'initial' && submission?.service_type); const fallback = submissions.find((submission) => submission?.service_type); next[task.id] = initial?.service_type || fallback?.service_type || task.title || 'Other'; } return next; } function CurrencyInput({ value, onChange, placeholder = '0.00', required = false, min = '0', step = '0.01' }) { return (
| {fmtDate(exp.date)} | {exp.category || '—'} | {exp.notes || '—'} | {fmtAmt(exp.amount)} |
| {inv.profile?.name || '—'} | {fmtDate(inv.submitted_at)} | {fmtDate(inv.paid_at)} |
|
{fmtAmt(total)} |
| {fmtDate(inv.invoice_date)} | {fmtDate(inv.due_date)} |
|
0 ? '#ef4444' : 'var(--text-muted)' }}> {Number(inv.stripe_fee) > 0 ? `-${fmtAmt(inv.stripe_fee)}` : '$0.00'} | {fmtAmt(inv.total)} |
Loading...
) : filtered.length === 0 ? (| {inv.invoice_number} | {inv.bill_to || inv.company?.name} | {new Date(inv.invoice_date).toLocaleDateString()} | {new Date(inv.due_date).toLocaleDateString()} |
|
${Number(inv.total).toFixed(2)} |
Loading...
) : expensesError ? ({expensesError}
) : filteredExpenses.length === 0 ? (| Action | |||||
|---|---|---|---|---|---|
| {new Date(exp.date + 'T12:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} | {exp.description} | {exp.category} |
{exp.notes || exp.receipt_path ? (
{exp.notes || '—'}
{exp.receipt_path && (
)}
) : '—'}
|
${Number(exp.amount).toFixed(2)} |
Loading...
) : subInvoicesError ? ({subInvoicesError}
) : subInvoices.length === 0 ? (| Actions | |||||
|---|---|---|---|---|---|
| {inv.invoice_number} |
{inv.profile?.name || 'External'}
{inv.profile?.email || '—'}
|
{inv.submitted_at ? new Date(inv.submitted_at).toLocaleDateString() : '—'} |
|
${total.toFixed(2)} |