diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 07025d9..3e86984 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -25,7 +25,10 @@ "Bash(sed -n '88,100p' src/pages/Tasks.jsx)", "Bash(sed -n '570,600p' src/pages/Tasks.jsx)", "Bash(git commit -q -m 'fix: hide counter on All tab in projects table *)", - "Bash(git commit -q -m 'fix: guard task status updates on invoice lifecycle transitions *)" + "Bash(git commit -q -m 'fix: guard task status updates on invoice lifecycle transitions *)", + "Bash(vercel ls *)", + "Bash(vercel --prod)", + "Bash(git commit -q -m 'feat: add Sub Billing column to billing report \\(sub invoiced/paid per version\\) *)" ] } } diff --git a/src/pages/team/TeamReports.jsx b/src/pages/team/TeamReports.jsx index 9e5fb8d..035ba71 100644 --- a/src/pages/team/TeamReports.jsx +++ b/src/pages/team/TeamReports.jsx @@ -8,6 +8,7 @@ import LoadingButton from '../../components/LoadingButton'; import { useSortable } from '../../hooks/useSortable'; import { supabase } from '../../lib/supabase'; import { isReviewShadowDescription } from '../../lib/taskVersions'; +import { parseVersionFromItemDescription } from '../../lib/invoiceVersionRules'; function fmtVersion(versionNumber) { return `R${String(Number(versionNumber || 0)).padStart(2, '0')}`; @@ -19,6 +20,13 @@ function billingStatusFromInvoices(items = []) { return 'invoiced'; } +// Sub billing: did the subcontractor invoice us for this version, and did we pay them? +function subBillingStatusFromInvoices(items = []) { + if (!items.length) return 'not_started'; + if (items.some((item) => item.invoice_status === 'paid')) return 'paid'; + return 'invoiced'; +} + function billingLabel(status) { if (status === 'paid') return 'Paid'; if (status === 'invoiced') return 'Invoiced'; @@ -72,6 +80,7 @@ export default function TeamReports() { { data: taskRows, error: tasksError }, { data: submissionRows, error: submissionsError }, { data: invoiceItemRows, error: invoiceItemsError }, + { data: subInvoiceRows, error: subInvoicesError }, ] = await Promise.all([ supabase .from('tasks') @@ -84,12 +93,17 @@ export default function TeamReports() { .order('submitted_at', { ascending: true }), supabase .from('invoice_items') - .select('id, task_id, submission_id, invoice:invoices(id, status), submission:submissions(id, task_id, version_number)') + .select('id, task_id, submission_id, invoice:invoices(id, status), submission:submissions(id, task_id, version_number)'), + supabase + .from('subcontractor_invoices') + .select('status, items:subcontractor_invoice_items(task_id, description)') + .in('status', ['submitted', 'approved', 'paid']) ]); if (tasksError) throw tasksError; if (submissionsError) throw submissionsError; if (invoiceItemsError) throw invoiceItemsError; + if (subInvoicesError) throw subInvoicesError; if (cancelled) return; const taskMap = new Map((taskRows || []).map((task) => [task.id, task])); @@ -131,6 +145,18 @@ export default function TeamReports() { invoiceItemGroups.get(key).push(item); } + const subBillingGroups = new Map(); + for (const subInvoice of (subInvoiceRows || [])) { + const status = subInvoice.status; + for (const item of (subInvoice.items || [])) { + if (!item.task_id) continue; + const version = parseVersionFromItemDescription(item.description); + const key = `${item.task_id}:${version}`; + if (!subBillingGroups.has(key)) subBillingGroups.set(key, []); + subBillingGroups.get(key).push({ invoice_status: status }); + } + } + const rows = []; for (const task of (taskRows || [])) { const company = task.project?.company; @@ -159,6 +185,7 @@ export default function TeamReports() { const key = `${task.id}:${Number(versionEntry.version_number || 0)}`; const invoiceItems = invoiceItemGroups.get(key) || []; const billingStatus = billingStatusFromInvoices(invoiceItems); + const subBillingStatus = subBillingStatusFromInvoices(subBillingGroups.get(key) || []); rows.push({ key, company_id: company?.id || '', @@ -173,6 +200,8 @@ export default function TeamReports() { version_state: versionStateFor(task, versionEntry.version_number), billing_status: billingStatus, billing_label: billingLabel(billingStatus), + sub_billing_status: subBillingStatus, + sub_billing_label: billingLabel(subBillingStatus), first_submitted_at: versionEntry.first_submitted_at, }); } @@ -215,6 +244,7 @@ export default function TeamReports() { if (key === 'version_type') return row.version_type || ''; if (key === 'version_state') return row.version_state || ''; if (key === 'billing_label') return row.billing_label || ''; + if (key === 'sub_billing_label') return row.sub_billing_label || ''; return ''; }), [visibleRows, sort]); @@ -243,7 +273,7 @@ export default function TeamReports() { autoTable(doc, { startY: 78, - head: [['Company', 'Project', 'Task', 'Version', 'Type', 'Version Status', 'Billing']], + head: [['Company', 'Project', 'Task', 'Version', 'Type', 'Version Status', 'Client Billing', 'Sub Billing']], body: sortedRows.map((row) => [ row.company_name, row.project_name, @@ -252,6 +282,7 @@ export default function TeamReports() { row.version_type, row.version_state === 'client_review' ? 'In Review' : row.version_state === 'client_approved' ? 'Approved' : row.version_state.replace(/_/g, ' '), row.billing_label, + row.sub_billing_label, ]), styles: { fontSize: 9, @@ -337,13 +368,14 @@ export default function TeamReports() { ) : ( - - - - - - - + + + + + + + + @@ -353,7 +385,8 @@ export default function TeamReports() { R#TypeVersion Status - Billing + Client Billing + Sub Billing @@ -366,6 +399,7 @@ export default function TeamReports() { + ))}