diff --git a/src/components/SubcontractorInvoiceForm.jsx b/src/components/SubcontractorInvoiceForm.jsx index f0d59f6..8752350 100644 --- a/src/components/SubcontractorInvoiceForm.jsx +++ b/src/components/SubcontractorInvoiceForm.jsx @@ -5,10 +5,12 @@ import { supabase } from '../lib/supabase'; import { useAuth } from '../context/AuthContext'; import { sendEmail } from '../lib/email'; import { isCompletedVersionEligible } from '../lib/invoiceVersionRules'; +import { isReviewShadowDescription } from '../lib/taskVersions'; import { useActionLock } from '../hooks/useActionLock'; const INVOICE_TODAY = new Date().toISOString().split('T')[0]; const REVISION_RATE = 30; +const ELIGIBLE_TASK_STATUSES = new Set(['not_started', 'in_progress', 'on_hold', 'client_review', 'client_approved', 'invoiced', 'paid']); 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 = { @@ -113,20 +115,20 @@ export default function SubcontractorInvoiceForm({ embedded = false, onCancel, o setLoadingTasks(true); setError(''); try { - const [{ data: tasks, error: tasksError }, { data: existingInvoices, error: invoicesError }, { data: nextNum, error: nextNumError }] = await Promise.all([ + const [{ data: deliveryRows, error: deliveriesError }, { data: existingInvoices, error: invoicesError }, { data: nextNum, error: nextNumError }] = await Promise.all([ + // Scoped by who actually delivered each version, not by current task assignment — + // a task can be reassigned mid-flight and the original sub still needs to bill + // for the version(s) they personally delivered. supabase - .from('tasks') - .select('id, title, status, current_version, project:projects(name)') - .in('status', ['not_started', 'in_progress', 'on_hold', 'client_review', 'client_approved', 'invoiced', 'paid']) - .eq('assigned_to', currentUser.id) - .order('title'), + .from('deliveries') + .select('version_number, sent_by, submission:submissions!inner(task_id, description, task:tasks!inner(id, title, status, current_version, project:projects(name)))'), supabase .from('subcontractor_invoices') .select('id, status, items:subcontractor_invoice_items(task_id, version_number, description)') .in('status', ['submitted', 'paid']), supabase.rpc('get_next_sub_invoice_number'), ]); - if (tasksError) throw tasksError; + if (deliveriesError) throw deliveriesError; if (invoicesError) throw invoicesError; if (nextNumError) throw nextNumError; @@ -141,46 +143,31 @@ export default function SubcontractorInvoiceForm({ embedded = false, onCancel, o }).filter(Boolean)) ); - const taskIds = (tasks || []).map((task) => task.id).filter(Boolean); - const { data: submissionRows, error: submissionsError } = taskIds.length > 0 - ? await supabase - .from('submissions') - .select('task_id, deliveries(version_number, sent_by, sent_at)') - .in('task_id', taskIds) - : { data: [], error: null }; - if (submissionsError) throw submissionsError; - - const deliveriesByTask = new Map(); - for (const row of (submissionRows || [])) { - const taskId = row.task_id; - if (!taskId) continue; - const bucket = deliveriesByTask.get(taskId) || new Map(); - for (const delivery of asArray(row.deliveries)) { - if ((delivery.sent_by || '').trim().toLowerCase() !== (currentUser.name || '').trim().toLowerCase()) continue; - const version = Number(delivery.version_number || 0); - if (!bucket.has(version)) bucket.set(version, delivery); - } - deliveriesByTask.set(taskId, bucket); + const myName = (currentUser.name || '').trim().toLowerCase(); + const unitsByKey = new Map(); + for (const delivery of (deliveryRows || [])) { + if ((delivery.sent_by || '').trim().toLowerCase() !== myName) continue; + const submission = delivery.submission; + const task = submission?.task; + if (!task || !ELIGIBLE_TASK_STATUSES.has(task.status)) continue; + if (isReviewShadowDescription(submission.description)) continue; + const version = Number(delivery.version_number || 0); + if (!isCompletedVersionEligible(task, version)) continue; + const key = `${task.id}:${version}`; + if (invoicedUnitKeys.has(key) || unitsByKey.has(key)) continue; + unitsByKey.set(key, { + key, + task_id: task.id, + version_number: version, + is_revision: version > 0, + title: task.title, + project_name: task.project?.name || '', + description: `${task.project?.name ? `${task.project.name} • ` : ''}${task.title} – ${versionLabel(version)}`, + rate: subcontractorVersionRate(version, rate), + }); } - const units = (tasks || []).flatMap((task) => { - const versions = [...(deliveriesByTask.get(task.id)?.keys() || [])].sort((a, b) => a - b); - return versions - .filter((version) => isCompletedVersionEligible(task, version)) - .map((version) => { - const key = `${task.id}:${version}`; - return { - key, - task_id: task.id, - version_number: version, - is_revision: version > 0, - title: task.title, - project_name: task.project?.name || '', - description: `${task.project?.name ? `${task.project.name} • ` : ''}${task.title} – ${versionLabel(version)}`, - rate: subcontractorVersionRate(version, rate), - }; - }); - }).filter((unit) => !invoicedUnitKeys.has(unit.key)); + const units = [...unitsByKey.values()].sort((a, b) => a.title.localeCompare(b.title) || a.version_number - b.version_number); setCompletedTasks(units); } catch (err) {