From 0945b548c18a117e2c8697e133f7ae4aab9a00cf Mon Sep 17 00:00:00 2001 From: Krao Hasanee Date: Mon, 8 Jun 2026 14:01:59 -0400 Subject: [PATCH] fix: remove cards from invoice popups, match original flat layout Replace SubcontractorInvoiceDetailView (card-based) with flat layout matching the original sub-invoice popup: meta strip + raw table + notes. - InvoiceDetailPopup: add metaContent/metaActions/metaCols props + export POPUP_FIELD_LABEL - New InvoicePopupTable: flat sortable 5-col table, no card wrapper - All 4 popups (client, team invoice, team sub-invoice, external): use flat meta strip (4-col grid, borderBottom) and InvoicePopupTable - Sub-invoice + external: Submitted | Paid/Created | Items | Total - Client invoice: Date | Due | Status | Total - Team invoice: Date | Due | Company | Email | Total (+ stripe if paid) with Edit Dates button in metaActions slot Co-Authored-By: Claude Sonnet 4.6 --- src/components/InvoiceDetailPopup.jsx | 36 ++++++- src/components/InvoicePopupTable.jsx | 71 ++++++++++++++ src/pages/client/ClientMyInvoices.jsx | 44 +++------ src/pages/external/ExternalMyInvoices.jsx | 43 +++------ src/pages/team/TeamInvoiceDetail.jsx | 112 ++++++++++------------ src/pages/team/TeamInvoices.jsx | 36 ++----- 6 files changed, 191 insertions(+), 151 deletions(-) create mode 100644 src/components/InvoicePopupTable.jsx diff --git a/src/components/InvoiceDetailPopup.jsx b/src/components/InvoiceDetailPopup.jsx index 79a873d..5506569 100644 --- a/src/components/InvoiceDetailPopup.jsx +++ b/src/components/InvoiceDetailPopup.jsx @@ -1,12 +1,25 @@ import { popupOverlayStyle, popupSurfaceStyle } from '../lib/popupStyles'; import PageLoader from './PageLoader'; +export const POPUP_FIELD_LABEL = { + fontSize: 11, + fontWeight: 500, + color: 'var(--text-secondary)', + textTransform: 'uppercase', + letterSpacing: 0.8, + display: 'block', + marginBottom: 4, +}; + export default function InvoiceDetailPopup({ title, subtitle, headerRight, onClose, blockClose = false, + metaContent, // JSX fields rendered in flat meta grid (no cards) + metaActions, // optional JSX rendered right-aligned beside meta grid (e.g. Edit Dates) + metaCols = 4, // number of grid columns for meta strip footerActions, loading = false, children, @@ -17,7 +30,8 @@ export default function InvoiceDetailPopup({ style={{ ...popupSurfaceStyle, background: 'var(--card-bg)', width: '80vw', height: '80vh', display: 'flex', flexDirection: 'column', gap: 0 }} onClick={e => e.stopPropagation()} > -
+ {/* Header */} +
{title}
{subtitle &&
{subtitle}
} @@ -25,16 +39,34 @@ export default function InvoiceDetailPopup({ {headerRight &&
{headerRight}
}
+ {/* Flat meta strip */} + {metaContent && ( +
+
+
+ {metaContent} +
+ {metaActions && ( +
+ {metaActions} +
+ )} +
+
+ )} + + {/* Body */} {loading ? (
) : ( -
+
{children}
)} + {/* Footer */} {footerActions && (
{footerActions} diff --git a/src/components/InvoicePopupTable.jsx b/src/components/InvoicePopupTable.jsx new file mode 100644 index 0000000..621e93d --- /dev/null +++ b/src/components/InvoicePopupTable.jsx @@ -0,0 +1,71 @@ +import SortTh from './SortTh'; + +function fmt(val) { + return `$${Number(val || 0).toFixed(2)}`; +} + +function inferItemType(item) { + if (item._inferredType) return item._inferredType; + const match = String(item?.description || '').match(/\bR(\d{2})\b/i); + if (match) { + return Number(match[1]) <= 0 + ? { label: 'New', badgeClass: 'badge-initial' } + : { label: 'Revision', badgeClass: 'badge-client_revision' }; + } + if (item?.task_id) return { label: 'Task', badgeClass: 'badge-in_progress' }; + return { label: 'Other', badgeClass: 'badge-initial' }; +} + +export default function InvoicePopupTable({ sortKey, sortDir, onSort, items, notes }) { + return ( + <> + {items.length === 0 ? ( +
No line items.
+ ) : ( + + + + + + + + + + + Type + Description + Qty + Unit Price + Total + + + + {items.map(item => { + const itemType = inferItemType(item); + return ( + + + + + + + + ); + })} + +
+ {itemType.label} + {item.description}{item.quantity}{fmt(item.unit_price)} + {fmt(Number(item.unit_price || 0) * Number(item.quantity || 1))} +
+ )} + + {notes && ( +
+
Notes
+
{notes}
+
+ )} + + ); +} diff --git a/src/pages/client/ClientMyInvoices.jsx b/src/pages/client/ClientMyInvoices.jsx index e37425f..54487be 100644 --- a/src/pages/client/ClientMyInvoices.jsx +++ b/src/pages/client/ClientMyInvoices.jsx @@ -10,8 +10,8 @@ import { supabase } from '../../lib/supabase'; import { generateInvoicePDF, generateReceiptPDF } from '../../lib/invoice'; import { useAuth } from '../../context/AuthContext'; import { useSortable } from '../../hooks/useSortable'; -import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; -import SubcontractorInvoiceDetailView from '../../components/SubcontractorInvoiceDetailView'; +import InvoiceDetailPopup, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup'; +import InvoicePopupTable from '../../components/InvoicePopupTable'; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; const invoiceStatusLabel = (status) => { @@ -97,13 +97,22 @@ function ClientInvoiceModal({ invoice, onClose }) { return ''; }); + const F = POPUP_FIELD_LABEL; return ( } onClose={onClose} blockClose={Boolean(downloading)} + metaContent={<> +
Invoice Date
{invoice.invoice_date ? new Date(invoice.invoice_date).toLocaleDateString() : '—'}
+
Due Date
{invoice.due_date ? new Date(invoice.due_date).toLocaleDateString() : '—'}
+
Status
+
Total
${total.toFixed(2)}
+ {invoice.paid_at &&
Paid On
{new Date(invoice.paid_at).toLocaleDateString()}
} + {company?.id &&
Bill To
{company.name}
} + } footerActions={<> {invoice.status === 'sent' && } Download Invoice @@ -111,34 +120,7 @@ function ClientInvoiceModal({ invoice, onClose }) { } > - -
{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()}

} -
- } - 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 cc088b8..e90c2f5 100644 --- a/src/pages/external/ExternalMyInvoices.jsx +++ b/src/pages/external/ExternalMyInvoices.jsx @@ -9,8 +9,8 @@ import { readPageCache, writePageCache } from '../../lib/pageCache'; import { useSortable } from '../../hooks/useSortable'; import { isCompletedVersionEligible } from '../../lib/invoiceVersionRules'; import SubcontractorInvoiceForm from '../../components/SubcontractorInvoiceForm'; -import InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; -import SubcontractorInvoiceDetailView from '../../components/SubcontractorInvoiceDetailView'; +import InvoiceDetailPopup, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup'; +import InvoicePopupTable from '../../components/InvoicePopupTable'; import { generateSubcontractorPOPDF } from '../../lib/invoice'; const STATUS_BADGE = { draft: 'not_started', submitted: 'in_progress', paid: 'client_approved' }; @@ -462,6 +462,7 @@ export default function MyInvoices() { if (key === 'line_total') return Number(item.unit_price || 0) * Number(item.quantity || 1); return ''; }); + const F = POPUP_FIELD_LABEL; return ( { if (!busy) { setViewingInvoice(null); setDetailError(''); } }} blockClose={busy} loading={detailLoading && !inv} + metaContent={inv ? <> +
Created
{inv.created_at ? new Date(inv.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}
+
Submitted
{inv.submitted_at ? new Date(inv.submitted_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}
+
Line Items
{baseItems.length}
+
Total
${total.toFixed(2)}
+ : null} footerActions={inv ? ( <> Download Invoice @@ -480,34 +487,10 @@ export default function MyInvoices() { ) : null} > {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)}

-
- } - sortKey={detailSortKey} - sortDir={detailSortDir} - onSort={toggleDetailSort} - items={tableItems} - notes={inv.notes} - total={total} - /> - ) : !detailError ? ( -
Invoice not found.
- ) : null} + {inv + ? + : !detailError ?
Invoice not found.
: null + }
); })()} diff --git a/src/pages/team/TeamInvoiceDetail.jsx b/src/pages/team/TeamInvoiceDetail.jsx index 98bbe9b..f049098 100644 --- a/src/pages/team/TeamInvoiceDetail.jsx +++ b/src/pages/team/TeamInvoiceDetail.jsx @@ -10,8 +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 InvoiceDetailPopup from '../../components/InvoiceDetailPopup'; -import SubcontractorInvoiceDetailView from '../../components/SubcontractorInvoiceDetailView'; +import InvoiceDetailPopup, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup'; +import InvoicePopupTable from '../../components/InvoicePopupTable'; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; @@ -365,6 +365,7 @@ export function TeamInvoiceDetailPanel({ const isOverdue = invoice && invoice.status !== 'paid' && new Date(invoice.due_date) < new Date(); if (embedded) { + const F = POPUP_FIELD_LABEL; const tableItems = sortedItems.map(item => ({ ...item, _inferredType: item.submission_id @@ -384,6 +385,49 @@ export function TeamInvoiceDetailPanel({ onClose={onClose} blockClose={saving || Boolean(generating)} loading={loading} + metaContent={invoice ? <> +
+
Invoice Date
+ {editingDates + ? setDateForm(f => ({ ...f, invoice_date: e.target.value }))} /> + :
{new Date(invoice.invoice_date).toLocaleDateString()}
} +
+
+
Due Date
+ {editingDates + ? setDateForm(f => ({ ...f, due_date: e.target.value }))} /> + :
{new Date(invoice.due_date).toLocaleDateString()}
} +
+
+
Company
+ +
+
+
Email To
+ setEmailRecipient(e.target.value)} onBlur={handleEmailBlur} placeholder="client@example.com" disabled={saving} /> +
+
+
Total
+
${Number(invoice.total).toFixed(2)}
+
+ {invoice.paid_at &&
Paid On
{new Date(invoice.paid_at).toLocaleDateString()}
} + {invoice.status === 'paid' && invoice.stripe_fee != null && <> +
Stripe Fee
−${Number(invoice.stripe_fee).toFixed(2)}
+
Net Received
${(Number(invoice.total) - Number(invoice.stripe_fee)).toFixed(2)}
+ } + : null} + metaActions={invoice && <> + {!editingDates + ? + : <> + + + + } + } + metaCols={4} footerActions={invoice && !loading ? ( <> {invoice.status === 'draft' && Finalize & Send} @@ -400,66 +444,10 @@ export function TeamInvoiceDetailPanel({ ) : 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.
- )} + {invoice + ? + :
Invoice not found.
+ } ); } diff --git a/src/pages/team/TeamInvoices.jsx b/src/pages/team/TeamInvoices.jsx index a2b6150..aeb79a3 100644 --- a/src/pages/team/TeamInvoices.jsx +++ b/src/pages/team/TeamInvoices.jsx @@ -18,8 +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'; +import InvoiceDetailPopup, { POPUP_FIELD_LABEL } from '../../components/InvoiceDetailPopup'; +import InvoicePopupTable from '../../components/InvoicePopupTable'; const CATEGORIES = ['Software', 'Contractor', 'Advertising', 'Office', 'Travel', 'Meals', 'Equipment', 'Other']; const statusColor = { draft: 'not_started', sent: 'in_progress', paid: 'client_approved' }; @@ -1731,6 +1731,7 @@ export default function Invoices() { if (key === 'line_total') return Number(item.unit_price || 0) * Number(item.quantity || 1); return ''; }); + const F = POPUP_FIELD_LABEL; return ( } onClose={() => { if (!markingPaid) setViewingSubInvoice(null); }} blockClose={Boolean(markingPaid)} + metaContent={<> +
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
{baseItems.length}
+
Total
${total.toFixed(2)}
+ } footerActions={<> {invoice.status === 'submitted' && ( @@ -1752,30 +1759,7 @@ export default function Invoices() { } > - -
{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)}

-
- } - sortKey={subPopupKey} - sortDir={subPopupDir} - onSort={subPopupToggle} - items={tableItems} - notes={invoice.notes} - total={total} - /> +
); })()}