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 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-06-08 14:01:59 -04:00
parent 6e4e99c33c
commit 0945b548c1
6 changed files with 191 additions and 151 deletions
+34 -2
View File
@@ -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()}
>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, paddingBottom: 18, marginBottom: 18, borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, paddingBottom: 18, borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
<div>
<div style={{ fontSize: 18, fontWeight: 500, color: 'var(--text-primary)', letterSpacing: 0.2, lineHeight: 1.1 }}>{title}</div>
{subtitle && <div style={{ marginTop: 6, fontSize: 13, color: 'var(--text-secondary)' }}>{subtitle}</div>}
@@ -25,16 +39,34 @@ export default function InvoiceDetailPopup({
{headerRight && <div style={{ flexShrink: 0 }}>{headerRight}</div>}
</div>
{/* Flat meta strip */}
{metaContent && (
<div style={{ padding: '18px 0', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 }}>
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${metaCols}, minmax(0, 1fr))`, gap: '14px 18px', flex: 1 }}>
{metaContent}
</div>
{metaActions && (
<div style={{ flexShrink: 0, display: 'flex', gap: 8, alignItems: 'flex-start' }}>
{metaActions}
</div>
)}
</div>
</div>
)}
{/* Body */}
{loading ? (
<div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<PageLoader />
</div>
) : (
<div className="scrollbar-thin-theme table-scroll-hidden" style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
<div className="scrollbar-thin-theme table-scroll-hidden" style={{ flex: 1, minHeight: 0, overflowY: 'auto', paddingTop: 18 }}>
{children}
</div>
)}
{/* Footer */}
{footerActions && (
<div className="modal-action-row" style={{ paddingTop: 18, borderTop: '1px solid var(--border)', flexShrink: 0 }}>
{footerActions}
+71
View File
@@ -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 ? (
<div className="card-empty-center" style={{ minHeight: 120 }}>No line items.</div>
) : (
<table className="table-sticky-head" style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
<colgroup>
<col style={{ width: '12%' }} />
<col style={{ width: '46%' }} />
<col style={{ width: '14%' }} />
<col style={{ width: '14%' }} />
<col style={{ width: '14%' }} />
</colgroup>
<thead>
<tr>
<SortTh col="type" sortKey={sortKey} sortDir={sortDir} onSort={onSort}>Type</SortTh>
<SortTh col="description" sortKey={sortKey} sortDir={sortDir} onSort={onSort}>Description</SortTh>
<SortTh col="quantity" sortKey={sortKey} sortDir={sortDir} onSort={onSort} style={{ textAlign: 'center' }}>Qty</SortTh>
<SortTh col="unit_price" sortKey={sortKey} sortDir={sortDir} onSort={onSort} style={{ textAlign: 'right' }}>Unit Price</SortTh>
<SortTh col="line_total" sortKey={sortKey} sortDir={sortDir} onSort={onSort} style={{ textAlign: 'right' }}>Total</SortTh>
</tr>
</thead>
<tbody>
{items.map(item => {
const itemType = inferItemType(item);
return (
<tr key={item.id}>
<td style={{ padding: '5px 0' }}>
<span className={`badge ${itemType.badgeClass}`}>{itemType.label}</span>
</td>
<td style={{ padding: '5px 0', fontSize: 13 }}>{item.description}</td>
<td style={{ padding: '5px 0', fontSize: 13, textAlign: 'center' }}>{item.quantity}</td>
<td style={{ padding: '5px 0', fontSize: 13, textAlign: 'right' }}>{fmt(item.unit_price)}</td>
<td style={{ padding: '5px 0', fontSize: 13, textAlign: 'right', fontWeight: 400 }}>
{fmt(Number(item.unit_price || 0) * Number(item.quantity || 1))}
</td>
</tr>
);
})}
</tbody>
</table>
)}
{notes && (
<div style={{ marginTop: 18, padding: '12px 14px', background: 'var(--card-bg-2)', borderRadius: 6, border: '1px solid var(--border)' }}>
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 4 }}>Notes</div>
<div style={{ fontSize: 13, color: 'var(--text-secondary)', whiteSpace: 'pre-wrap' }}>{notes}</div>
</div>
)}
</>
);
}