fix: sub billing only applies to work an external sub delivered
Report was flagging every unbilled delivered version as "Not Invoiced" for sub billing, even when a team member delivered it — team work is billed to the client directly and is never sub-invoiced. Now traces each version's actual deliverer and shows N/A when it's a team member, regardless of who delivered earlier versions on the same task. Also: SubcontractorInvoiceForm no longer masks fetch errors as "No completed tasks available to invoice" — the real error now shows in the Completed Tasks panel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,9 +34,16 @@ function subBillingStatusFromInvoices(items = []) {
|
||||
function billingLabel(status) {
|
||||
if (status === 'paid') return 'Paid';
|
||||
if (status === 'invoiced') return 'Invoiced';
|
||||
if (status === 'not_applicable') return 'N/A';
|
||||
return 'Not Invoiced';
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value == null) return [];
|
||||
return typeof value === 'object' ? [value] : [];
|
||||
}
|
||||
|
||||
function versionTypeLabel(versionNumber) {
|
||||
return Number(versionNumber || 0) === 0 ? 'New' : 'Revision';
|
||||
}
|
||||
@@ -85,6 +92,7 @@ export default function TeamReports() {
|
||||
{ data: submissionRows, error: submissionsError },
|
||||
{ data: invoiceItemRows, error: invoiceItemsError },
|
||||
{ data: subInvoiceRows, error: subInvoicesError },
|
||||
{ data: profileRows, error: profilesError },
|
||||
] = await Promise.all([
|
||||
supabase
|
||||
.from('tasks')
|
||||
@@ -92,7 +100,7 @@ export default function TeamReports() {
|
||||
.order('title'),
|
||||
supabase
|
||||
.from('submissions')
|
||||
.select('id, task_id, type, version_number, submitted_at, invoiced, description')
|
||||
.select('id, task_id, type, version_number, submitted_at, invoiced, description, delivery:deliveries(sent_by)')
|
||||
.in('type', ['initial', 'revision'])
|
||||
.order('submitted_at', { ascending: true }),
|
||||
supabase
|
||||
@@ -101,15 +109,28 @@ export default function TeamReports() {
|
||||
supabase
|
||||
.from('subcontractor_invoices')
|
||||
.select('status, items:subcontractor_invoice_items(task_id, version_number, description)')
|
||||
.in('status', ['submitted', 'approved', 'paid'])
|
||||
.in('status', ['submitted', 'approved', 'paid']),
|
||||
supabase
|
||||
.from('profiles')
|
||||
.select('name, role'),
|
||||
]);
|
||||
|
||||
if (tasksError) throw tasksError;
|
||||
if (submissionsError) throw submissionsError;
|
||||
if (invoiceItemsError) throw invoiceItemsError;
|
||||
if (subInvoicesError) throw subInvoicesError;
|
||||
if (profilesError) throw profilesError;
|
||||
if (cancelled) return;
|
||||
|
||||
// Sub billing only applies to work an external subcontractor delivered.
|
||||
// A team member's own delivery is billed to the client directly and is
|
||||
// never sub-invoiced, no matter who started the task's earlier versions.
|
||||
const roleByName = new Map(
|
||||
(profileRows || [])
|
||||
.filter((profile) => profile.name)
|
||||
.map((profile) => [profile.name.trim().toLowerCase(), profile.role])
|
||||
);
|
||||
|
||||
const taskMap = new Map((taskRows || []).map((task) => [task.id, task]));
|
||||
const companiesMap = new Map();
|
||||
const projectsMap = new Map();
|
||||
@@ -137,6 +158,12 @@ export default function TeamReports() {
|
||||
if (!entry.first_submitted_at || (submission.submitted_at && submission.submitted_at < entry.first_submitted_at)) {
|
||||
entry.first_submitted_at = submission.submitted_at;
|
||||
}
|
||||
// Duplicate submissions for the same task+version can exist (double-submits);
|
||||
// take the deliverer from whichever one actually has a delivery logged.
|
||||
if (!entry.delivered_by) {
|
||||
const deliverer = asArray(submission.delivery)[0]?.sent_by;
|
||||
if (deliverer) entry.delivered_by = deliverer;
|
||||
}
|
||||
}
|
||||
|
||||
const invoiceItemGroups = new Map();
|
||||
@@ -190,7 +217,12 @@ 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) || []);
|
||||
const delivererRole = versionEntry.delivered_by
|
||||
? roleByName.get(versionEntry.delivered_by.trim().toLowerCase()) || null
|
||||
: null;
|
||||
const subBillingStatus = delivererRole === 'external'
|
||||
? subBillingStatusFromInvoices(subBillingGroups.get(key) || [])
|
||||
: 'not_applicable';
|
||||
rows.push({
|
||||
key,
|
||||
company_id: company?.id || '',
|
||||
|
||||
Reference in New Issue
Block a user