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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<InvoiceDetailPopup
|
||||
title={invoice.invoice_number}
|
||||
subtitle={company?.name || invoice.bill_to}
|
||||
subtitle={invoice.bill_to || company?.name}
|
||||
headerRight={<StatusBadge status={statusColor[invoice.status] || 'not_started'} label={`${invoiceStatusLabel(invoice.status)}${isOverdue ? ' · Overdue' : ''}`} />}
|
||||
onClose={onClose}
|
||||
blockClose={Boolean(downloading)}
|
||||
metaContent={<>
|
||||
<div><div style={F}>Invoice Date</div><div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{invoice.invoice_date ? new Date(invoice.invoice_date).toLocaleDateString() : '—'}</div></div>
|
||||
<div><div style={F}>Due Date</div><div style={{ fontSize: 13, color: isOverdue ? 'var(--danger)' : 'var(--text-primary)' }}>{invoice.due_date ? new Date(invoice.due_date).toLocaleDateString() : '—'}</div></div>
|
||||
<div><div style={F}>Status</div><div style={{ fontSize: 13, color: 'var(--text-primary)' }}><StatusBadge status={statusColor[invoice.status] || 'not_started'} label={invoiceStatusLabel(invoice.status)} /></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>
|
||||
{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>}
|
||||
{company?.id && <div><div style={F}>Bill To</div><div style={{ fontSize: 13 }}><Link to={`/company/${company.id}`} className="dashboard-inline-link" onClick={onClose}>{company.name}</Link></div></div>}
|
||||
</>}
|
||||
footerActions={<>
|
||||
{invoice.status === 'sent' && <button className="btn btn-outline" onClick={openPay}>Pay Invoice</button>}
|
||||
<LoadingButton className="btn btn-outline" loading={downloading === 'invoice'} disabled={Boolean(downloading)} loadingText="Generating…" onClick={handleDownloadInvoice}>Download Invoice</LoadingButton>
|
||||
@@ -111,34 +120,7 @@ function ClientInvoiceModal({ invoice, onClose }) {
|
||||
<button className="btn btn-outline" onClick={onClose}>Close</button>
|
||||
</>}
|
||||
>
|
||||
<SubcontractorInvoiceDetailView
|
||||
leftCardTitle="Bill To"
|
||||
leftCardBody={<>
|
||||
<div style={{ fontSize: 15, fontWeight: 400 }}>{invoice.bill_to || company?.name || '—'}</div>
|
||||
{company?.id && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Link to={`/company/${company.id}`} className="btn btn-outline btn-sm" onClick={onClose}>View Company</Link>
|
||||
</div>
|
||||
)}
|
||||
</>}
|
||||
rightCardTitle="Invoice Details"
|
||||
rightCardBody={
|
||||
<div className="invoice-detail-meta-grid">
|
||||
<div className="invoice-detail-meta-item"><label>Invoice Date</label><p>{invoice.invoice_date ? new Date(invoice.invoice_date).toLocaleDateString() : '—'}</p></div>
|
||||
<div className="invoice-detail-meta-item"><label>Due Date</label><p style={{ color: isOverdue ? 'var(--danger)' : 'inherit' }}>{invoice.due_date ? 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>Status</label><p><StatusBadge status={statusColor[invoice.status] || 'not_started'} label={invoiceStatusLabel(invoice.status)} /></p></div>
|
||||
<div className="invoice-detail-meta-item"><label>Total</label><p style={{ fontSize: 18, fontWeight: 400, color: 'var(--accent)' }}>${total.toFixed(2)}</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>
|
||||
}
|
||||
sortKey={sortKey}
|
||||
sortDir={sortDir}
|
||||
onSort={toggle}
|
||||
items={tableItems}
|
||||
notes={invoice.notes}
|
||||
total={total}
|
||||
/>
|
||||
<InvoicePopupTable sortKey={sortKey} sortDir={sortDir} onSort={toggle} items={tableItems} notes={invoice.notes} />
|
||||
</InvoiceDetailPopup>
|
||||
);
|
||||
}
|
||||
|
||||
+13
-30
@@ -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 (
|
||||
<InvoiceDetailPopup
|
||||
title={inv?.invoice_number || 'Subcontractor Invoice'}
|
||||
@@ -470,6 +471,12 @@ export default function MyInvoices() {
|
||||
onClose={() => { if (!busy) { setViewingInvoice(null); setDetailError(''); } }}
|
||||
blockClose={busy}
|
||||
loading={detailLoading && !inv}
|
||||
metaContent={inv ? <>
|
||||
<div><div style={F}>Created</div><div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{inv.created_at ? new Date(inv.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}</div></div>
|
||||
<div><div style={F}>Submitted</div><div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{inv.submitted_at ? new Date(inv.submitted_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>
|
||||
</> : null}
|
||||
footerActions={inv ? (
|
||||
<>
|
||||
<LoadingButton className="btn btn-outline" loading={downloadingInvoice} loadingText="Generating…" disabled={busy} onClick={handleDownloadInvoice}>Download Invoice</LoadingButton>
|
||||
@@ -480,34 +487,10 @@ export default function MyInvoices() {
|
||||
) : null}
|
||||
>
|
||||
{detailError && <div className="notification notification-info" style={{ marginBottom: 16 }}>{detailError}</div>}
|
||||
{inv ? (
|
||||
<SubcontractorInvoiceDetailView
|
||||
leftCardTitle="Submitted By"
|
||||
leftCardBody={<>
|
||||
<div style={{ fontSize: 15, fontWeight: 400 }}>{currentUser?.name || 'Subcontractor'}</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--text-muted)', marginTop: 4 }}>{currentUser?.email || '—'}</div>
|
||||
</>}
|
||||
rightCardTitle="Invoice Details"
|
||||
rightCardBody={
|
||||
<div className="invoice-detail-meta-grid">
|
||||
<div className="invoice-detail-meta-item"><label>Invoice #</label><p>{inv.invoice_number || '—'}</p></div>
|
||||
<div className="invoice-detail-meta-item"><label>Status</label><p>{invoiceStatus}</p></div>
|
||||
<div className="invoice-detail-meta-item"><label>Created</label><p>{inv.created_at ? new Date(inv.created_at).toLocaleDateString() : '—'}</p></div>
|
||||
<div className="invoice-detail-meta-item"><label>Submitted</label><p>{inv.submitted_at ? new Date(inv.submitted_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={detailSortKey}
|
||||
sortDir={detailSortDir}
|
||||
onSort={toggleDetailSort}
|
||||
items={tableItems}
|
||||
notes={inv.notes}
|
||||
total={total}
|
||||
/>
|
||||
) : !detailError ? (
|
||||
<div className="card-empty-center">Invoice not found.</div>
|
||||
) : null}
|
||||
{inv
|
||||
? <InvoicePopupTable sortKey={detailSortKey} sortDir={detailSortDir} onSort={toggleDetailSort} items={tableItems} notes={inv.notes} />
|
||||
: !detailError ? <div className="card-empty-center">Invoice not found.</div> : null
|
||||
}
|
||||
</InvoiceDetailPopup>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user