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
+50 -62
View File
@@ -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 ? <>
<div>
<div style={F}>Invoice Date</div>
{editingDates
? <input type="date" className="input" style={{ margin: 0 }} value={dateForm.invoice_date} onChange={e => setDateForm(f => ({ ...f, invoice_date: e.target.value }))} />
: <div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{new Date(invoice.invoice_date).toLocaleDateString()}</div>}
</div>
<div>
<div style={F}>Due Date</div>
{editingDates
? <input type="date" className="input" style={{ margin: 0 }} value={dateForm.due_date} onChange={e => setDateForm(f => ({ ...f, due_date: e.target.value }))} />
: <div style={{ fontSize: 13, color: isOverdue ? 'var(--danger)' : 'var(--text-primary)' }}>{new Date(invoice.due_date).toLocaleDateString()}</div>}
</div>
<div>
<div style={F}>Company</div>
<select className="input" style={{ margin: 0 }} value={invoice.company_id || ''} onChange={e => handleCompanyChange(e.target.value)} disabled={saving}>
{companies.map(co => <option key={co.id} value={co.id}>{co.name}</option>)}
</select>
</div>
<div>
<div style={F}>Email To</div>
<input type="email" className="input" style={{ margin: 0 }} value={emailRecipient} onChange={e => setEmailRecipient(e.target.value)} onBlur={handleEmailBlur} placeholder="client@example.com" disabled={saving} />
</div>
<div>
<div style={F}>Total</div>
<div style={{ fontSize: 18, fontWeight: 500, color: 'var(--accent)', lineHeight: 1.1 }}>${Number(invoice.total).toFixed(2)}</div>
</div>
{invoice.paid_at && <div><div style={F}>Paid On</div><div style={{ fontSize: 13, color: 'var(--success, #16a34a)' }}>{new Date(invoice.paid_at).toLocaleDateString()}</div></div>}
{invoice.status === 'paid' && invoice.stripe_fee != null && <>
<div><div style={F}>Stripe Fee</div><div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>${Number(invoice.stripe_fee).toFixed(2)}</div></div>
<div><div style={F}>Net Received</div><div style={{ fontSize: 13, color: 'var(--text-primary)' }}>${(Number(invoice.total) - Number(invoice.stripe_fee)).toFixed(2)}</div></div>
</>}
</> : null}
metaActions={invoice && <>
{!editingDates
? <button className="btn btn-outline btn-sm" onClick={handleEditDates} disabled={saving}>Edit Dates</button>
: <>
<button className="btn btn-primary btn-sm" onClick={handleSaveDates} disabled={saving}>Save</button>
<button className="btn btn-outline btn-sm" onClick={() => setEditingDates(false)} disabled={saving}>Cancel</button>
</>
}
</>}
metaCols={4}
footerActions={invoice && !loading ? (
<>
{invoice.status === 'draft' && <LoadingButton className="btn btn-primary" onClick={handleFinalizeSend} loading={saving} loadingText="Finalizing & Sending...">Finalize & Send</LoadingButton>}
@@ -400,66 +444,10 @@ export function TeamInvoiceDetailPanel({
</>
) : null}
>
{invoice ? (
<SubcontractorInvoiceDetailView
leftCardTitle="Bill To"
leftCardBody={<>
<div style={{ fontSize: 15, fontWeight: 400 }}>{invoice.bill_to || company?.name}</div>
<div style={{ marginTop: 12 }}>
<Link to={`/company/${company?.id}`} className="btn btn-outline btn-sm">View Company</Link>
</div>
</>}
headerActions={!editingDates
? <button className="btn btn-outline btn-sm" onClick={handleEditDates}>Edit Dates</button>
: <div style={{ display: 'flex', gap: 8 }}>
<button className="btn btn-primary btn-sm" onClick={handleSaveDates} disabled={saving}>Save</button>
<button className="btn btn-outline btn-sm" onClick={() => setEditingDates(false)} disabled={saving}>Cancel</button>
</div>
}
rightCardTitle="Invoice Details"
rightCardBody={
<div className="invoice-detail-meta-grid">
<div className="invoice-detail-meta-item">
<label>Invoice Date</label>
{editingDates
? <input type="date" className="input" style={{ margin: 0 }} value={dateForm.invoice_date} onChange={e => setDateForm(f => ({ ...f, invoice_date: e.target.value }))} />
: <p>{new Date(invoice.invoice_date).toLocaleDateString()}</p>}
</div>
<div className="invoice-detail-meta-item">
<label>Due Date</label>
{editingDates
? <input type="date" className="input" style={{ margin: 0 }} value={dateForm.due_date} onChange={e => setDateForm(f => ({ ...f, due_date: e.target.value }))} />
: <p style={{ color: isOverdue ? 'var(--danger)' : 'inherit' }}>{new Date(invoice.due_date).toLocaleDateString()}</p>}
</div>
<div className="invoice-detail-meta-item"><label>Terms</label><p>Net 30</p></div>
<div className="invoice-detail-meta-item">
<label>Company</label>
<select className="input" style={{ margin: 0 }} value={invoice.company_id || ''} onChange={e => handleCompanyChange(e.target.value)} disabled={saving}>
{companies.map(co => <option key={co.id} value={co.id}>{co.name}</option>)}
</select>
</div>
<div className="invoice-detail-meta-item">
<label>Email To</label>
<input type="email" className="input" style={{ margin: 0 }} value={emailRecipient} onChange={e => setEmailRecipient(e.target.value)} onBlur={handleEmailBlur} placeholder="client@example.com" disabled={saving} />
</div>
<div className="invoice-detail-meta-item"><label>Total</label><p style={{ fontSize: 18, fontWeight: 400, color: 'var(--accent)' }}>${Number(invoice.total).toFixed(2)}</p></div>
{invoice.paid_at && <div className="invoice-detail-meta-item"><label>Paid On</label><p style={{ color: 'var(--success, #16a34a)', fontWeight: 400 }}>{new Date(invoice.paid_at).toLocaleDateString()}</p></div>}
{invoice.status === 'paid' && invoice.stripe_fee != null && <>
<div className="invoice-detail-meta-item"><label>Stripe Fee</label><p style={{ color: 'var(--text-secondary)' }}>${Number(invoice.stripe_fee).toFixed(2)}</p></div>
<div className="invoice-detail-meta-item"><label>Net Received</label><p style={{ fontWeight: 400 }}>${(Number(invoice.total) - Number(invoice.stripe_fee)).toFixed(2)}</p></div>
</>}
</div>
}
sortKey={sortKey}
sortDir={sortDir}
onSort={toggle}
items={tableItems}
notes={invoice.notes}
total={Number(invoice.total)}
/>
) : (
<div style={{ padding: 24, color: 'var(--text-muted)' }}>Invoice not found.</div>
)}
{invoice
? <InvoicePopupTable sortKey={sortKey} sortDir={sortDir} onSort={toggle} items={tableItems} notes={invoice.notes} />
: <div style={{ padding: 24, color: 'var(--text-muted)' }}>Invoice not found.</div>
}
</InvoiceDetailPopup>
);
}
+10 -26
View File
@@ -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 (
<InvoiceDetailPopup
title={invoice.invoice_number || 'Subcontractor Invoice'}
@@ -1738,6 +1739,12 @@ export default function Invoices() {
headerRight={<StatusBadge status={subInvoiceStatusColor[invoice.status] || 'not_started'} label={invoiceStatus} />}
onClose={() => { if (!markingPaid) setViewingSubInvoice(null); }}
blockClose={Boolean(markingPaid)}
metaContent={<>
<div><div style={F}>Submitted</div><div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{invoice.submitted_at ? new Date(invoice.submitted_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}</div></div>
<div><div style={F}>Paid</div><div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{invoice.paid_at ? new Date(invoice.paid_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}</div></div>
<div><div style={F}>Line Items</div><div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{baseItems.length}</div></div>
<div><div style={F}>Total</div><div style={{ fontSize: 18, fontWeight: 500, color: 'var(--accent)', lineHeight: 1.1 }}>${total.toFixed(2)}</div></div>
</>}
footerActions={<>
<button type="button" className="btn btn-outline" onClick={() => handleDownloadSubInvoice(invoice)}>Download Invoice</button>
{invoice.status === 'submitted' && (
@@ -1752,30 +1759,7 @@ export default function Invoices() {
<button type="button" className="btn btn-outline" onClick={() => setViewingSubInvoice(null)}>Close</button>
</>}
>
<SubcontractorInvoiceDetailView
leftCardTitle="Submitted By"
leftCardBody={<>
<div style={{ fontSize: 15, fontWeight: 400 }}>{invoice.profile?.name || 'External'}</div>
<div style={{ fontSize: 13, color: 'var(--text-muted)', marginTop: 4 }}>{invoice.profile?.email || '—'}</div>
</>}
rightCardTitle="Invoice Details"
rightCardBody={
<div className="invoice-detail-meta-grid">
<div className="invoice-detail-meta-item"><label>Invoice #</label><p>{invoice.invoice_number || '—'}</p></div>
<div className="invoice-detail-meta-item"><label>Status</label><p>{invoiceStatus}</p></div>
<div className="invoice-detail-meta-item"><label>Submitted</label><p>{invoice.submitted_at ? new Date(invoice.submitted_at).toLocaleDateString() : '—'}</p></div>
{invoice.paid_at && <div className="invoice-detail-meta-item"><label>Paid On</label><p style={{ color: 'var(--success, #16a34a)' }}>{new Date(invoice.paid_at).toLocaleDateString()}</p></div>}
<div className="invoice-detail-meta-item"><label>Line Items</label><p>{baseItems.length}</p></div>
<div className="invoice-detail-meta-item"><label>Total</label><p style={{ fontSize: 18, color: 'var(--accent)' }}>${total.toFixed(2)}</p></div>
</div>
}
sortKey={subPopupKey}
sortDir={subPopupDir}
onSort={subPopupToggle}
items={tableItems}
notes={invoice.notes}
total={total}
/>
<InvoicePopupTable sortKey={subPopupKey} sortDir={subPopupDir} onSort={subPopupToggle} items={tableItems} notes={invoice.notes} />
</InvoiceDetailPopup>
);
})()}