From 6e4e99c33c9ac397a4be6818c59d398d042418b9 Mon Sep 17 00:00:00 2001 From: Krao Hasanee Date: Mon, 8 Jun 2026 13:46:08 -0400 Subject: [PATCH] feat: unify all invoice popups to shared InvoiceDetailPopup shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All invoice popup modals (team invoice, sub-invoice team, client invoice, external sub-invoice) now share InvoiceDetailPopup + SubcontractorInvoiceDetailView. Same overlay, header, scroll body, footer action row — just different data per role. - New InvoiceDetailPopup component (overlay shell, header, scrollable body, footer) - SubcontractorInvoiceDetailView: respects item._inferredType if pre-set - ClientInvoiceModal: uses shared components, adds sortable headers - TeamInvoiceDetailPanel embedded: uses shared components, keeps edit-dates/email/company - TeamInvoices sub-invoice popup: uses shared components, cleans descriptions - ExternalMyInvoices popup: uses shared components, fixes detailSort vars (was unused) Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.local.json | 6 +- src/components/InvoiceDetailPopup.jsx | 46 +++++ .../SubcontractorInvoiceDetailView.jsx | 2 +- src/pages/client/ClientMyInvoices.jsx | 175 ++++++---------- src/pages/external/ExternalMyInvoices.jsx | 186 +++++++----------- src/pages/team/TeamInvoiceDetail.jsx | 146 ++++++++++---- src/pages/team/TeamInvoices.jsx | 165 ++++++---------- 7 files changed, 347 insertions(+), 379 deletions(-) create mode 100644 src/components/InvoiceDetailPopup.jsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ef4cfcd..bfaee62 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,11 @@ "Bash(sed -n '147p' src/pages/Profile.jsx)", "Bash(awk '{print $5, $9}')", "Bash(git add *)", - "Bash(git commit -q -m ' *)" + "Bash(git commit -q -m ' *)", + "Bash(git push *)", + "Bash(vercel --prod --yes)", + "Bash(sed -n '1,15p' src/pages/external/ExternalMyInvoices.jsx)", + "Bash(sed -n '1,15p' src/pages/client/ClientMyInvoices.jsx)" ] } } diff --git a/src/components/InvoiceDetailPopup.jsx b/src/components/InvoiceDetailPopup.jsx new file mode 100644 index 0000000..79a873d --- /dev/null +++ b/src/components/InvoiceDetailPopup.jsx @@ -0,0 +1,46 @@ +import { popupOverlayStyle, popupSurfaceStyle } from '../lib/popupStyles'; +import PageLoader from './PageLoader'; + +export default function InvoiceDetailPopup({ + title, + subtitle, + headerRight, + onClose, + blockClose = false, + footerActions, + loading = false, + children, +}) { + return ( +
{ if (!blockClose) onClose?.(); }}> +
e.stopPropagation()} + > +
+
+
{title}
+ {subtitle &&
{subtitle}
} +
+ {headerRight &&
{headerRight}
} +
+ + {loading ? ( +
+ +
+ ) : ( +
+ {children} +
+ )} + + {footerActions && ( +
+ {footerActions} +
+ )} +
+
+ ); +} diff --git a/src/components/SubcontractorInvoiceDetailView.jsx b/src/components/SubcontractorInvoiceDetailView.jsx index 8849644..0ba9a70 100644 --- a/src/components/SubcontractorInvoiceDetailView.jsx +++ b/src/components/SubcontractorInvoiceDetailView.jsx @@ -73,7 +73,7 @@ export default function SubcontractorInvoiceDetailView({ {items.map((item) => { - const itemType = inferItemType(item); + const itemType = item._inferredType || inferItemType(item); return ( diff --git a/src/pages/client/ClientMyInvoices.jsx b/src/pages/client/ClientMyInvoices.jsx index 7f6f6a5..e37425f 100644 --- a/src/pages/client/ClientMyInvoices.jsx +++ b/src/pages/client/ClientMyInvoices.jsx @@ -10,7 +10,8 @@ import { supabase } from '../../lib/supabase'; import { generateInvoicePDF, generateReceiptPDF } from '../../lib/invoice'; import { useAuth } from '../../context/AuthContext'; import { useSortable } from '../../hooks/useSortable'; -import { popupOverlayStyle, popupSurfaceStyle } from '../../lib/popupStyles'; +import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; +import SubcontractorInvoiceDetailView from '../../components/SubcontractorInvoiceDetailView'; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; const invoiceStatusLabel = (status) => { @@ -48,11 +49,13 @@ function ClientFinanceTooltip({ active, payload, label, year }) { function ClientInvoiceModal({ invoice, onClose }) { const navigate = useNavigate(); const [downloading, setDownloading] = useState(''); + const { sortKey, sortDir, toggle, sort } = useSortable('description'); if (!invoice) return null; const items = invoice.items || []; const company = invoice.company || null; const isOverdue = invoice.status !== 'paid' && invoice.due_date && new Date(invoice.due_date) < new Date(); + const total = Number(invoice.total || 0); const handleDownloadInvoice = async () => { if (downloading) return; @@ -79,128 +82,64 @@ function ClientInvoiceModal({ invoice, onClose }) { navigate(`/pay/${encodeURIComponent(invoice.invoice_number)}`); }; - const sortedItems = [...items].sort((a, b) => { - const aOrder = Number(a.sort_order ?? 0); - const bOrder = Number(b.sort_order ?? 0); - return aOrder - bOrder; + const baseItems = [...items].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)); + const tableItems = sort(baseItems.map(item => ({ + ...item, + _inferredType: item.submission_id + ? { label: 'Revision', badgeClass: 'badge-client_revision' } + : { label: 'New', badgeClass: 'badge-initial' }, + })), (item, key) => { + if (key === 'type') return item._inferredType?.label || ''; + if (key === 'description') return item.description || ''; + if (key === 'quantity') return Number(item.quantity || 0); + if (key === 'unit_price') return Number(item.unit_price || 0); + if (key === 'line_total') return Number(item.quantity || 0) * Number(item.unit_price || 0); + return ''; }); return ( -
-
e.stopPropagation()}> -
-
-
{invoice.invoice_number}
-
- {company?.id ? ( - - {company.name} - - ) : ( - company?.name || invoice.bill_to || 'Invoice' - )} + } + onClose={onClose} + blockClose={Boolean(downloading)} + footerActions={<> + {invoice.status === 'sent' && } + Download Invoice + {invoice.status === 'paid' && Download Receipt} + + } + > + +
{invoice.bill_to || company?.name || '—'}
+ {company?.id && ( +
+ View Company
+ )} + } + rightCardTitle="Invoice Details" + rightCardBody={ +
+

{invoice.invoice_date ? new Date(invoice.invoice_date).toLocaleDateString() : '—'}

+

{invoice.due_date ? new Date(invoice.due_date).toLocaleDateString() : '—'}

+

Net 30

+

+

${total.toFixed(2)}

+ {invoice.paid_at &&

{new Date(invoice.paid_at).toLocaleDateString()}

}
-
- - {invoice.status === 'sent' && ( - - )} - - Download Invoice - - {invoice.status === 'paid' && ( - - Download Receipt - - )} - -
-
- -
-
-
Bill To
-
{invoice.bill_to || company?.name || '—'}
-
- {company?.id ? ( - - {company.name} - - ) : ( - company?.name || 'Fourge Branding Client Invoice' - )} -
-
-
-
Invoice Details
-
-

{invoice.invoice_date ? new Date(invoice.invoice_date).toLocaleDateString() : '—'}

-

{invoice.due_date ? new Date(invoice.due_date).toLocaleDateString() : '—'}

-

Net 30

-

-

${Number(invoice.total || 0).toFixed(2)}

- {invoice.paid_at &&

{new Date(invoice.paid_at).toLocaleDateString()}

} -
-
-
- -
-
Line Items
-
- - - - - - - - - - - - - - - - - - - {sortedItems.map(item => ( - - - - - - - - ))} - -
TypeDescriptionQtyUnit PriceTotal
- - {item.submission_id ? 'Revision' : 'New'} - - {item.description}{item.quantity}${Number(item.unit_price || 0).toFixed(2)}${(Number(item.quantity || 0) * Number(item.unit_price || 0)).toFixed(2)}
-
-
-
-
Total
-
${Number(invoice.total || 0).toFixed(2)}
-
-
-
- - {invoice.notes ? ( -
-
Notes
-

{invoice.notes}

-
- ) : null} -
-
+ } + sortKey={sortKey} + sortDir={sortDir} + onSort={toggle} + items={tableItems} + notes={invoice.notes} + total={total} + /> + ); } diff --git a/src/pages/external/ExternalMyInvoices.jsx b/src/pages/external/ExternalMyInvoices.jsx index 2d4940c..cc088b8 100644 --- a/src/pages/external/ExternalMyInvoices.jsx +++ b/src/pages/external/ExternalMyInvoices.jsx @@ -8,8 +8,9 @@ import { useAuth } from '../../context/AuthContext'; import { readPageCache, writePageCache } from '../../lib/pageCache'; import { useSortable } from '../../hooks/useSortable'; import { isCompletedVersionEligible } from '../../lib/invoiceVersionRules'; -import { popupOverlayStyle, popupSurfaceStyle } from '../../lib/popupStyles'; import SubcontractorInvoiceForm from '../../components/SubcontractorInvoiceForm'; +import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; +import SubcontractorInvoiceDetailView from '../../components/SubcontractorInvoiceDetailView'; import { generateSubcontractorPOPDF } from '../../lib/invoice'; const STATUS_BADGE = { draft: 'not_started', submitted: 'in_progress', paid: 'client_approved' }; @@ -443,126 +444,73 @@ export default function MyInvoices() {
)} - {(viewingInvoice || detailLoading || detailError) && ( -
{ if (!submittingInvoice && !downloadingInvoice && !downloadingReceipt) { setViewingInvoice(null); setDetailError(''); } }}> -
e.stopPropagation()} + {(viewingInvoice || detailLoading || detailError) && (() => { + const inv = viewingInvoice; + const total = inv ? invoiceTotal(inv.items) : 0; + const baseItems = inv ? [...(inv.items || [])].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) : []; + const invoiceStatus = inv?.status ? inv.status.charAt(0).toUpperCase() + inv.status.slice(1) : '—'; + const busy = submittingInvoice || downloadingInvoice || downloadingReceipt; + const tableItems = sortDetailItems(baseItems.map(item => ({ + ...item, + _inferredType: { label: itemWorkLabel(item), badgeClass: itemWorkLabel(item) === 'New' ? 'badge-initial' : 'badge-client_revision' }, + description: cleanItemDescription(item), + })), (item, key) => { + if (key === 'type') return item._inferredType?.label || ''; + if (key === 'description') return item.description || ''; + if (key === 'quantity') return Number(item.quantity || 0); + if (key === 'unit_price') return Number(item.unit_price || 0); + if (key === 'line_total') return Number(item.unit_price || 0) * Number(item.quantity || 1); + return ''; + }); + return ( + : null} + onClose={() => { if (!busy) { setViewingInvoice(null); setDetailError(''); } }} + blockClose={busy} + loading={detailLoading && !inv} + footerActions={inv ? ( + <> + Download Invoice + {inv.status === 'draft' && Submit to Team} + {inv.status === 'paid' && Download Receipt} + + + ) : null} > - {detailLoading && !viewingInvoice ? ( -
Loading invoice…
- ) : viewingInvoice ? (() => { - const total = invoiceTotal(viewingInvoice.items); - const sortedItems = [...(viewingInvoice.items || [])].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)); - const invoiceStatus = viewingInvoice.status ? viewingInvoice.status.charAt(0).toUpperCase() + viewingInvoice.status.slice(1) : '—'; - return ( - <> - {detailError ?
{detailError}
: null} - -
-
-
{viewingInvoice.invoice_number || 'Subcontractor Invoice'}
-
- {currentUser?.name || 'Subcontractor'}{currentUser?.email ? ` · ${currentUser.email}` : ''} -
-
- + {detailError &&
{detailError}
} + {inv ? ( + +
{currentUser?.name || 'Subcontractor'}
+
{currentUser?.email || '—'}
+ } + rightCardTitle="Invoice Details" + rightCardBody={ +
+

{inv.invoice_number || '—'}

+

{invoiceStatus}

+

{inv.created_at ? new Date(inv.created_at).toLocaleDateString() : '—'}

+

{inv.submitted_at ? new Date(inv.submitted_at).toLocaleDateString() : '—'}

+

{baseItems.length}

+

${total.toFixed(2)}

- -
-
-
Created
-
{viewingInvoice.created_at ? new Date(viewingInvoice.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}
-
-
-
Submitted
-
{viewingInvoice.submitted_at ? new Date(viewingInvoice.submitted_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}
-
-
-
Line Items
-
{sortedItems.length}
-
-
-
Total
-
${total.toFixed(2)}
-
-
- -
- {sortedItems.length === 0 ? ( -
No line items
- ) : ( - - - - - - - - - - - - - - - - - - - - - - - {sortedItems.map(item => ( - - - - - - - - - - ))} - -
WorkR#DescriptionTypeQtyUnit PriceAmount
{itemWorkLabel(item)}{itemVersionLabel(item)}{cleanItemDescription(item)}{detailTaskTypeMap[item.task_id] || 'Other'}{item.quantity || 1}${Number(item.unit_price || 0).toFixed(2)}${(Number(item.unit_price || 0) * Number(item.quantity || 1)).toFixed(2)}
- )} - - {viewingInvoice.notes && ( -
-
Notes
-
{viewingInvoice.notes}
-
- )} -
- -
- - Download Invoice - - {viewingInvoice.status === 'draft' && ( - - Submit to Team - - )} - {viewingInvoice.status === 'paid' && ( - - Download Receipt - - )} - -
- - ); - })() : ( -
{detailError || 'Invoice not found.'}
- )} -
-
- )} + } + sortKey={detailSortKey} + sortDir={detailSortDir} + onSort={toggleDetailSort} + items={tableItems} + notes={inv.notes} + total={total} + /> + ) : !detailError ? ( +
Invoice not found.
+ ) : null} + + ); + })()} ); } diff --git a/src/pages/team/TeamInvoiceDetail.jsx b/src/pages/team/TeamInvoiceDetail.jsx index 21b9635..98bbe9b 100644 --- a/src/pages/team/TeamInvoiceDetail.jsx +++ b/src/pages/team/TeamInvoiceDetail.jsx @@ -10,7 +10,8 @@ import { generateInvoicePDF, generateReceiptPDF } from '../../lib/invoice'; import { blobToEmailAttachment, sendEmail } from '../../lib/email'; import { withTimeout } from '../../lib/withTimeout'; import { useSortable } from '../../hooks/useSortable'; -import { popupOverlayStyle, popupSurfaceStyle } from '../../lib/popupStyles'; +import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; +import SubcontractorInvoiceDetailView from '../../components/SubcontractorInvoiceDetailView'; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; @@ -349,36 +350,120 @@ export function TeamInvoiceDetailPanel({ setEmailRecipient(defaultEmail); }; - if (loading) { - if (embedded) { - return ( -
- -
- ); - } - return ; - } - if (!invoice) { - if (embedded) { - return ( -
- Invoice not found. -
- ); - } - return

Invoice not found.

; - } - const sortedItems = sort(items, (item, key) => { + if (loading && !embedded) return ; + if (!invoice && !embedded) return

Invoice not found.

; + + const sortedItems = invoice ? sort(items, (item, key) => { if (key === 'type') return item.submission_id ? 'Revision' : 'New'; if (key === 'description') return item.description || ''; if (key === 'quantity') return Number(item.quantity || 0); if (key === 'unit_price') return Number(item.unit_price || 0); if (key === 'line_total') return Number(item.quantity || 0) * Number(item.unit_price || 0); return ''; - }); + }) : []; + + const isOverdue = invoice && invoice.status !== 'paid' && new Date(invoice.due_date) < new Date(); + + if (embedded) { + const tableItems = sortedItems.map(item => ({ + ...item, + _inferredType: item.submission_id + ? { label: 'Revision', badgeClass: 'badge-client_revision' } + : { label: 'New', badgeClass: 'badge-initial' }, + })); + return ( + + ) : null} + onClose={onClose} + blockClose={saving || Boolean(generating)} + loading={loading} + footerActions={invoice && !loading ? ( + <> + {invoice.status === 'draft' && Finalize & Send} + {invoice.status === 'sent' && Resend Invoice} + {invoice.status === 'sent' && } + {invoice.status === 'paid' && } + Download Invoice + {invoice.status === 'paid' && <> + Download Receipt + Send Receipt + } + {invoice.status !== 'paid' && } + + + ) : null} + > + {invoice ? ( + +
{invoice.bill_to || company?.name}
+
+ View Company +
+ } + headerActions={!editingDates + ? + :
+ + +
+ } + rightCardTitle="Invoice Details" + rightCardBody={ +
+
+ + {editingDates + ? setDateForm(f => ({ ...f, invoice_date: e.target.value }))} /> + :

{new Date(invoice.invoice_date).toLocaleDateString()}

} +
+
+ + {editingDates + ? setDateForm(f => ({ ...f, due_date: e.target.value }))} /> + :

{new Date(invoice.due_date).toLocaleDateString()}

} +
+

Net 30

+
+ + +
+
+ + setEmailRecipient(e.target.value)} onBlur={handleEmailBlur} placeholder="client@example.com" disabled={saving} /> +
+

${Number(invoice.total).toFixed(2)}

+ {invoice.paid_at &&

{new Date(invoice.paid_at).toLocaleDateString()}

} + {invoice.status === 'paid' && invoice.stripe_fee != null && <> +

−${Number(invoice.stripe_fee).toFixed(2)}

+

${(Number(invoice.total) - Number(invoice.stripe_fee)).toFixed(2)}

+ } +
+ } + sortKey={sortKey} + sortDir={sortDir} + onSort={toggle} + items={tableItems} + notes={invoice.notes} + total={Number(invoice.total)} + /> + ) : ( +
Invoice not found.
+ )} +
+ ); + } - const isOverdue = invoice.status !== 'paid' && new Date(invoice.due_date) < new Date(); const detailContent = ( <> {!embedded && } @@ -571,18 +656,7 @@ export function TeamInvoiceDetailPanel({ ); - if (embedded) { - return ( -
{ if (!saving && !generating) onClose?.(); }}> -
e.stopPropagation()} - > - {detailContent} -
-
- ); - } + return {detailContent}; } diff --git a/src/pages/team/TeamInvoices.jsx b/src/pages/team/TeamInvoices.jsx index 05e3036..a2b6150 100644 --- a/src/pages/team/TeamInvoices.jsx +++ b/src/pages/team/TeamInvoices.jsx @@ -18,6 +18,8 @@ import FileAttachment from '../../components/FileAttachment'; import { isReviewShadowDescription } from '../../lib/taskVersions'; import { getRevisionChargeQuantity, isCompletedVersionEligible, isInitialVersionEligible } from '../../lib/invoiceVersionRules'; import { TeamInvoiceDetailPanel } from './TeamInvoiceDetail'; +import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; +import SubcontractorInvoiceDetailView from '../../components/SubcontractorInvoiceDetailView'; const CATEGORIES = ['Software', 'Contractor', 'Advertising', 'Office', 'Travel', 'Meals', 'Equipment', 'Other']; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; @@ -241,6 +243,7 @@ export default function Invoices() { const { sortKey: ovExpKey, sortDir: ovExpDir, toggle: ovExpToggle } = useSortable('date'); const { sortKey: ovSubKey, sortDir: ovSubDir, toggle: ovSubToggle } = useSortable('date'); const { sortKey: ovInvKey, sortDir: ovInvDir, toggle: ovInvToggle } = useSortable('invoice_date'); + const { sortKey: subPopupKey, sortDir: subPopupDir, toggle: subPopupToggle, sort: subPopupSort } = useSortable('description'); const [filterCompany, setFilterCompany] = useState(''); const initMonth = () => { const n = new Date(); return { month: n.getMonth(), year: n.getFullYear() }; }; const [ovMonth1, setOvMonth1] = useState(initMonth); @@ -1712,114 +1715,68 @@ export default function Invoices() { {viewingSubInvoice && (() => { const invoice = viewingSubInvoice; - const sortedItems = [...(invoice.items || [])].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)); - const total = sortedItems.reduce((s, item) => s + Number(item.unit_price || 0) * Number(item.quantity || 1), 0); + const baseItems = [...(invoice.items || [])].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)); + const total = baseItems.reduce((s, item) => s + Number(item.unit_price || 0) * Number(item.quantity || 1), 0); const invoiceStatus = invoice.status ? invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1) : '—'; const subInvoiceStatusColor = { draft: 'not_started', submitted: 'in_progress', paid: 'client_approved' }; + const tableItems = subPopupSort(baseItems.map(item => ({ + ...item, + _inferredType: { label: subInvoiceItemWorkLabel(item), badgeClass: subInvoiceItemWorkLabel(item) === 'New' ? 'badge-initial' : 'badge-client_revision' }, + description: cleanSubInvoiceItemDescription(item), + })), (item, key) => { + if (key === 'type') return item._inferredType?.label || ''; + if (key === 'description') return item.description || ''; + if (key === 'quantity') return Number(item.quantity || 0); + if (key === 'unit_price') return Number(item.unit_price || 0); + if (key === 'line_total') return Number(item.unit_price || 0) * Number(item.quantity || 1); + return ''; + }); return ( -
{ if (!markingPaid) setViewingSubInvoice(null); }}> -
e.stopPropagation()}> -
-
-
{invoice.invoice_number || 'Subcontractor Invoice'}
-
- {invoice.profile?.name || 'External'}{invoice.profile?.email ? ` · ${invoice.profile.email}` : ''} -
+ } + onClose={() => { if (!markingPaid) setViewingSubInvoice(null); }} + blockClose={Boolean(markingPaid)} + footerActions={<> + + {invoice.status === 'submitted' && ( + { + await handleMarkSubInvoicePaid(invoice); + setViewingSubInvoice(current => current?.id === invoice.id ? { ...current, status: 'paid', paid_at: new Date().toISOString() } : current); + }} loading={markingPaid === invoice.id} loadingText="Processing…">Mark as Paid + )} + {invoice.status === 'paid' && ( + + )} + + } + > + +
{invoice.profile?.name || 'External'}
+
{invoice.profile?.email || '—'}
+ } + rightCardTitle="Invoice Details" + rightCardBody={ +
+

{invoice.invoice_number || '—'}

+

{invoiceStatus}

+

{invoice.submitted_at ? new Date(invoice.submitted_at).toLocaleDateString() : '—'}

+ {invoice.paid_at &&

{new Date(invoice.paid_at).toLocaleDateString()}

} +

{baseItems.length}

+

${total.toFixed(2)}

- -
- -
-
-
Submitted
-
{invoice.submitted_at ? new Date(invoice.submitted_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}
-
-
-
Paid
-
{invoice.paid_at ? new Date(invoice.paid_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}
-
-
-
Line Items
-
{sortedItems.length}
-
-
-
Total
-
${total.toFixed(2)}
-
-
- -
- {sortedItems.length === 0 ? ( -
No line items
- ) : ( - - - - - - - - - - - - - - - - - - - - - - - {sortedItems.map(item => ( - - - - - - - - - - ))} - -
WorkR#DescriptionTypeQtyUnit PriceAmount
{subInvoiceItemWorkLabel(item)}{subInvoiceItemVersionLabel(item)}{cleanSubInvoiceItemDescription(item)}{viewingSubInvoiceTaskTypeMap[item.task_id] || 'Other'}{item.quantity || 1}${Number(item.unit_price || 0).toFixed(2)}${(Number(item.unit_price || 0) * Number(item.quantity || 1)).toFixed(2)}
- )} - - {invoice.notes && ( -
-
Notes
-
{invoice.notes}
-
- )} -
- -
- - {invoice.status === 'submitted' && ( - { - await handleMarkSubInvoicePaid(invoice); - setViewingSubInvoice(current => current?.id === invoice.id ? { ...current, status: 'paid', paid_at: new Date().toISOString() } : current); - }} loading={markingPaid === invoice.id} loadingText="Processing…"> - Mark as Paid - - )} - {invoice.status === 'paid' && ( - - )} - -
-
-
+ } + sortKey={subPopupKey} + sortDir={subPopupDir} + onSort={subPopupToggle} + items={tableItems} + notes={invoice.notes} + total={total} + /> + ); })()}