04e0911e9f
Bug fixes: - TeamInvoices: add useAuth import/currentUser (invoice-create crash) - TeamInvoices: setChartYear -> setExportYear (year-dropdown crash) - TeamDashboard: drop redundant setState-in-effect (cascading renders) - Companies: delete dead _UnusedClientCompanies (illegal hook calls) - annotate intentional empty PDF-fallback catches Load speed: - preconnect/dns-prefetch to Supabase origin - lazy-load heic-to in Converters: page chunk 2737KB -> 9KB - split recharts into its own 'charts' vendor chunk Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
4.7 KiB
React
113 lines
4.7 KiB
React
import SortTh from './SortTh';
|
|
|
|
function fmt(val) {
|
|
return `$${Number(val || 0).toFixed(2)}`;
|
|
}
|
|
|
|
function inferItemType(item) {
|
|
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 SubcontractorInvoiceDetailView({
|
|
error,
|
|
headerActions,
|
|
leftCardTitle,
|
|
leftCardBody,
|
|
rightCardTitle,
|
|
rightCardBody,
|
|
sortKey,
|
|
sortDir,
|
|
onSort,
|
|
items,
|
|
notes,
|
|
total,
|
|
}) {
|
|
return (
|
|
<div className="invoice-detail-shell">
|
|
{error ? <div className="notification notification-info" style={{ marginBottom: 16, flexShrink: 0 }}>{error}</div> : null}
|
|
|
|
<div className="invoice-detail-stack">
|
|
<div className="invoice-detail-summary-grid">
|
|
<div className="card invoice-detail-card">
|
|
<div className="invoice-detail-section-title">{leftCardTitle}</div>
|
|
{leftCardBody}
|
|
</div>
|
|
<div className="card invoice-detail-card">
|
|
<div className="invoice-detail-card-header">
|
|
<div className="invoice-detail-section-title" style={{ marginBottom: 0 }}>{rightCardTitle}</div>
|
|
{headerActions ? <div className="invoice-detail-card-actions">{headerActions}</div> : null}
|
|
</div>
|
|
{rightCardBody}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card invoice-detail-card invoice-detail-line-items-card">
|
|
<div className="invoice-detail-section-title">Line Items</div>
|
|
{items.length === 0 ? (
|
|
<div className="card-empty-center" style={{ minHeight: 120 }}>No line items.</div>
|
|
) : (
|
|
<div className="scrollbar-thin-theme table-scroll-hidden" style={{ overflowY: 'auto' }}>
|
|
<table className="table-sticky-head invoice-detail-table" 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 className="invoice-detail-cell">{item.description}</td>
|
|
<td className="invoice-detail-cell" style={{ textAlign: 'center' }}>{item.quantity}</td>
|
|
<td className="invoice-detail-cell" style={{ textAlign: 'right' }}>{fmt(item.unit_price)}</td>
|
|
<td className="invoice-detail-cell" style={{ textAlign: 'right', fontWeight: 400 }}>
|
|
{fmt(Number(item.unit_price || 0) * Number(item.quantity || 1))}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
<div className="invoice-detail-total-row">
|
|
<div style={{ textAlign: 'right' }}>
|
|
<div className="invoice-detail-total-label">Total</div>
|
|
<div className="invoice-detail-total-value">{fmt(total)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{notes ? (
|
|
<div className="card invoice-detail-card">
|
|
<div className="invoice-detail-section-title">Notes</div>
|
|
<p className="invoice-detail-notes">{notes}</p>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|