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:
@@ -347,6 +347,8 @@ export default function SubcontractorInvoiceForm({ embedded = false, onCancel, o
|
|||||||
</div>
|
</div>
|
||||||
{loadingTasks ? (
|
{loadingTasks ? (
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>Loading…</div>
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>Loading…</div>
|
||||||
|
) : error ? (
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--danger)' }}>{error}</div>
|
||||||
) : completedTasks.length === 0 ? (
|
) : completedTasks.length === 0 ? (
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>No completed tasks available to invoice.</div>
|
<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>
|
</div>
|
||||||
{loadingTasks ? (
|
{loadingTasks ? (
|
||||||
<p style={{ fontSize: 13, color: 'var(--text-muted)' }}>Loading...</p>
|
<p style={{ fontSize: 13, color: 'var(--text-muted)' }}>Loading...</p>
|
||||||
|
) : error ? (
|
||||||
|
<p style={{ fontSize: 13, color: 'var(--danger)' }}>{error}</p>
|
||||||
) : completedTasks.length === 0 ? (
|
) : completedTasks.length === 0 ? (
|
||||||
<p style={{ fontSize: 13, color: 'var(--text-muted)' }}>No completed tasks available to invoice.</p>
|
<p style={{ fontSize: 13, color: 'var(--text-muted)' }}>No completed tasks available to invoice.</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -34,9 +34,16 @@ function subBillingStatusFromInvoices(items = []) {
|
|||||||
function billingLabel(status) {
|
function billingLabel(status) {
|
||||||
if (status === 'paid') return 'Paid';
|
if (status === 'paid') return 'Paid';
|
||||||
if (status === 'invoiced') return 'Invoiced';
|
if (status === 'invoiced') return 'Invoiced';
|
||||||
|
if (status === 'not_applicable') return 'N/A';
|
||||||
return 'Not Invoiced';
|
return 'Not Invoiced';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function asArray(value) {
|
||||||
|
if (Array.isArray(value)) return value;
|
||||||
|
if (value == null) return [];
|
||||||
|
return typeof value === 'object' ? [value] : [];
|
||||||
|
}
|
||||||
|
|
||||||
function versionTypeLabel(versionNumber) {
|
function versionTypeLabel(versionNumber) {
|
||||||
return Number(versionNumber || 0) === 0 ? 'New' : 'Revision';
|
return Number(versionNumber || 0) === 0 ? 'New' : 'Revision';
|
||||||
}
|
}
|
||||||
@@ -85,6 +92,7 @@ export default function TeamReports() {
|
|||||||
{ data: submissionRows, error: submissionsError },
|
{ data: submissionRows, error: submissionsError },
|
||||||
{ data: invoiceItemRows, error: invoiceItemsError },
|
{ data: invoiceItemRows, error: invoiceItemsError },
|
||||||
{ data: subInvoiceRows, error: subInvoicesError },
|
{ data: subInvoiceRows, error: subInvoicesError },
|
||||||
|
{ data: profileRows, error: profilesError },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
supabase
|
supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
@@ -92,7 +100,7 @@ export default function TeamReports() {
|
|||||||
.order('title'),
|
.order('title'),
|
||||||
supabase
|
supabase
|
||||||
.from('submissions')
|
.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'])
|
.in('type', ['initial', 'revision'])
|
||||||
.order('submitted_at', { ascending: true }),
|
.order('submitted_at', { ascending: true }),
|
||||||
supabase
|
supabase
|
||||||
@@ -101,15 +109,28 @@ export default function TeamReports() {
|
|||||||
supabase
|
supabase
|
||||||
.from('subcontractor_invoices')
|
.from('subcontractor_invoices')
|
||||||
.select('status, items:subcontractor_invoice_items(task_id, version_number, description)')
|
.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 (tasksError) throw tasksError;
|
||||||
if (submissionsError) throw submissionsError;
|
if (submissionsError) throw submissionsError;
|
||||||
if (invoiceItemsError) throw invoiceItemsError;
|
if (invoiceItemsError) throw invoiceItemsError;
|
||||||
if (subInvoicesError) throw subInvoicesError;
|
if (subInvoicesError) throw subInvoicesError;
|
||||||
|
if (profilesError) throw profilesError;
|
||||||
if (cancelled) return;
|
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 taskMap = new Map((taskRows || []).map((task) => [task.id, task]));
|
||||||
const companiesMap = new Map();
|
const companiesMap = new Map();
|
||||||
const projectsMap = 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)) {
|
if (!entry.first_submitted_at || (submission.submitted_at && submission.submitted_at < entry.first_submitted_at)) {
|
||||||
entry.first_submitted_at = submission.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();
|
const invoiceItemGroups = new Map();
|
||||||
@@ -190,7 +217,12 @@ export default function TeamReports() {
|
|||||||
const key = `${task.id}:${Number(versionEntry.version_number || 0)}`;
|
const key = `${task.id}:${Number(versionEntry.version_number || 0)}`;
|
||||||
const invoiceItems = invoiceItemGroups.get(key) || [];
|
const invoiceItems = invoiceItemGroups.get(key) || [];
|
||||||
const billingStatus = billingStatusFromInvoices(invoiceItems);
|
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({
|
rows.push({
|
||||||
key,
|
key,
|
||||||
company_id: company?.id || '',
|
company_id: company?.id || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user