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:
Krao Hasanee
2026-07-15 20:15:04 -04:00
parent 2c1ff61b29
commit 1f52b6e96c
2 changed files with 39 additions and 3 deletions
@@ -347,6 +347,8 @@ export default function SubcontractorInvoiceForm({ embedded = false, onCancel, o
</div>
{loadingTasks ? (
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>Loading</div>
) : error ? (
<div style={{ fontSize: 12, color: 'var(--danger)' }}>{error}</div>
) : completedTasks.length === 0 ? (
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>No completed tasks available to invoice.</div>
) : (
@@ -442,6 +444,8 @@ export default function SubcontractorInvoiceForm({ embedded = false, onCancel, o
</div>
{loadingTasks ? (
<p style={{ fontSize: 13, color: 'var(--text-muted)' }}>Loading...</p>
) : error ? (
<p style={{ fontSize: 13, color: 'var(--danger)' }}>{error}</p>
) : completedTasks.length === 0 ? (
<p style={{ fontSize: 13, color: 'var(--text-muted)' }}>No completed tasks available to invoice.</p>
) : (
+35 -3
View File
@@ -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 || '',