diff --git a/layout.md b/layout.md
index 9f0afbf..ac60e7b 100644
--- a/layout.md
+++ b/layout.md
@@ -60,6 +60,14 @@ This is the single source of truth for dashboard/profile visual structure and UI
- Theme toggle to avatar: `14px` (`avatar-wrap margin-left`)
- Avatar button: `49x49`, circle, `2px` inner ring + `2px` accent outline
+## 6.5) Section Control Bars (Tabs + Actions)
+- For page-level card controls (ex: Tasks/Projects, Finances tabs):
+ - container uses: `display: flex`, `align-items: center`, `gap: 4`, `margin-bottom: 10`, `flex-shrink: 0`
+ - tabs stay on the left in source order
+ - action buttons group sits on the right using: `margin-left: auto`, `display: flex`, `align-items: center`, `gap: 8`
+ - do not use hardcoded spacer blocks (`width` filler divs) to force alignment
+ - icon-only filter/action buttons share the same row and align vertically with add buttons
+
## 7) Dashboard Grids (Team)
- Stat row: `grid-template-columns: 1fr 1fr 1fr 1.5fr`, `gap: 24`, `margin-bottom: 0`
- Row 2: `grid-template-columns: 1fr 1fr 280px`, `gap: 24`, `margin-top: 24`
@@ -147,7 +155,7 @@ This is the single source of truth for dashboard/profile visual structure and UI
- company line under title is always visible; fallback display is `—` when no company is assigned
- right-side meta labels (`Member Since`, `Role`) use widget-title sizing: `11px`, `500`, uppercase, `letter-spacing: 0.8px`
- right-side meta values use body sizing: `13px`
- - self-only edit button: `position: absolute`, `top: 18px`, `right: 21px` (aligns to card padding), `border-radius: 8px` (matches card), `height: 30px`, `font-size: 12px`, `padding: 0 12px`
+ - self-only edit button: `position: absolute`, `top: 18px`, `right: 21px` (aligns to card padding), `border-radius: 8px` (matches card), `height: 22px`, `font-size: 12px`, `padding: 0 12px`
- default outline button hover/focus fills with accent gold and uses dark text
- Right calendar card shows only tasks/events assigned to the viewed profile user
- Profile page left column includes a `Tasks In Progress` card above `Recent Activity`
@@ -196,7 +204,7 @@ This is the single source of truth for dashboard/profile visual structure and UI
- current file path uses the same `11px` uppercase label treatment as the title row family
- toolbar does not show `+ Folder` or `Upload` buttons
- upload happens by dragging and dropping files or folders into the right card
- - delete action uses the same geometry as the profile edit button (`top: 18px`, `right: 21px`, `8px` radius, `30px` height, `12px` text, `0 12px` padding) but uses danger styling with red hover/fill
+ - delete action uses the same geometry as the profile edit button (`top: 18px`, `right: 21px`, `8px` radius, `22px` height, `12px` text, `0 12px` padding) but uses danger styling with red hover/fill
- Right card table:
- uses dashboard table styling (`table-layout: fixed`, transparent header row, `10px` uppercase headers, `13px/12px` body text)
- card padding matches dashboard widget shell (`18px 21px`)
@@ -216,7 +224,15 @@ This is the single source of truth for dashboard/profile visual structure and UI
- Dashboard/profile widgets: `8px` radius
- Sidebar: `8px` radius
- Standard app buttons use the `Edit Profile` geometry:
- - height: `30px`
+ - geometry is tokenized globally in CSS vars and must be reused (no per-page hardcoded button geometry):
+ - `--btn-height`
+ - `--btn-padding-x`
+ - `--btn-radius`
+ - `--btn-font-size`
+ - `--btn-font-weight`
+ - `--btn-letter-spacing`
+ - `--btn-line-height`
+ - height: `22px`
- horizontal padding: `0 12px`
- border-radius: `8px`
- font-size: `11px`
@@ -269,6 +285,32 @@ This is the single source of truth for dashboard/profile visual structure and UI
- field surfaces and borders use global theme tokens (`var(--card-bg-2)` + `var(--border)`) for dark/light parity
- modal form action buttons use the standard outline geometry/style (`btn btn-outline`) unless a page explicitly defines a different role
+## 14.7) Status Tags
+- Shared status tags use `StatusBadge` (`.badge.badge-status`).
+- Base badge geometry:
+ - `display: inline-flex`
+ - `align-items: center`
+ - `gap: 4px`
+ - `padding: 3px 10px`
+ - `border-radius: 4px`
+ - `font-size: 11px`
+ - `font-weight: 400`
+ - `letter-spacing: 0.3px`
+ - `white-space: nowrap`
+- Status badge geometry override (`.badge-status`):
+ - `min-width: 78px`
+ - `height: 20px`
+ - `justify-content: center`
+ - `padding: 0 8px`
+ - `font-size: 10px`
+ - `line-height: 1`
+- Status colors are variant classes (`.badge-not_started`, `.badge-in_progress`, etc.) and are theme-aware.
+- Exception: compact urgent tag (`.badge-needs_revision`) uses tighter geometry:
+ - `min-width: 28px`
+ - `padding: 3px 4px`
+ - `border-radius: 4px`
+ - fixed red treatment in both dark/light themes.
+
## 15) Motion
- Motion vars:
- fast `160ms`
diff --git a/src/components/FileBrowser.jsx b/src/components/FileBrowser.jsx
index bb461fd..1067e62 100644
--- a/src/components/FileBrowser.jsx
+++ b/src/components/FileBrowser.jsx
@@ -349,26 +349,26 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) {
{uploadProgress !== null && Uploading {uploadProgress}% }
{selected.size > 0 && (
- Delete ({selected.size})
+ Delete ({selected.size})
)}
{!readOnly && (showFolderInput ? (
) : (
- setShowFolderInput(true)}>+ Folder
+ setShowFolderInput(true)}>+ Folder
))}
{!readOnly && (
<>
uploadFolder(e.target.files)} {...{ webkitdirectory: '' }} />
uploadFiles(e.target.files)} />
- folderInputRef.current?.click()}>↑ Folder
- fileInputRef.current?.click()}>↑ Files
+ folderInputRef.current?.click()}>↑ Folder
+ fileInputRef.current?.click()}>↑ Files
>
)}
- loadFiles(currentPath)}>⟳
+ loadFiles(currentPath)}>⟳
@@ -439,8 +439,8 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) {
{isRenaming ? (
) : (
@@ -459,12 +459,12 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) {
{!isRenaming && !readOnly && (
{!isDir && (
-
downloadFile(entry)}>↓
+
downloadFile(entry)}>↓
)}
-
{ setRenamingEntry(entry); setRenameValue(entry.name); }}>
+ { setRenamingEntry(entry); setRenameValue(entry.name); }}>
- deleteEntry(entry)}>✕
+ deleteEntry(entry)}>✕
)}
diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx
index 4ae40d5..d4ad85c 100644
--- a/src/components/Layout.jsx
+++ b/src/components/Layout.jsx
@@ -147,13 +147,16 @@ export default function Layout({ children }) {
const isFileSharingRoute = location.pathname === '/file-sharing';
const isTaskDetailRoute = location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/requests/');
const isRequestsRoute = location.pathname === '/tasks' || location.pathname === '/requests' || location.pathname === '/team/tasks' || isTaskDetailRoute;
- const headerTitle = isProfileRoute ? 'Profile' : isFileSharingRoute ? 'File Sharing' : isRequestsRoute && !isTaskDetailRoute ? 'Tasks & Projects' : !isTaskDetailRoute ? `Good ${timeOfDay}${firstName ? `, ${firstName}` : ''}` : null;
+ const isFinancesRoute = location.pathname === '/invoices' || location.pathname.startsWith('/invoices/') || location.pathname === '/sub-invoices' || location.pathname.startsWith('/sub-invoices/');
+ const headerTitle = isProfileRoute ? 'Profile' : isFileSharingRoute ? 'File Sharing' : isFinancesRoute ? 'Finances' : isRequestsRoute && !isTaskDetailRoute ? 'Tasks & Projects' : !isTaskDetailRoute ? `Good ${timeOfDay}${firstName ? `, ${firstName}` : ''}` : null;
const headerSubtitle = isProfileRoute
? 'Account details and security settings.'
: isFileSharingRoute
? 'Browse, share and manage files.'
- : isRequestsRoute && !isTaskDetailRoute
- ? 'Browse and manage all tasks and projects.'
+ : isFinancesRoute
+ ? 'Invoices, expenses and subcontractor POs.'
+ : isRequestsRoute && !isTaskDetailRoute
+ ? 'Browse and manage all tasks and projects.'
: isTaskDetailRoute ? null : "Here's what's happening today.";
useEffect(() => {
diff --git a/src/index.css b/src/index.css
index 7c8d2b0..2561e4f 100644
--- a/src/index.css
+++ b/src/index.css
@@ -11,7 +11,14 @@
--motion-fast: 160ms;
--motion-base: 220ms;
--motion-ease: cubic-bezier(0.22, 1, 0.36, 1);
- --h-control: 30px;
+ --btn-height: 22px;
+ --btn-padding-x: 12px;
+ --btn-radius: 8px;
+ --btn-font-size: 11px;
+ --btn-font-weight: 500;
+ --btn-letter-spacing: 0.8px;
+ --btn-line-height: 1;
+ --h-control: var(--btn-height);
--sidebar-bg: #0d0d0d;
--sidebar-text: #888888;
--sidebar-active-text: #ffffff;
@@ -997,10 +1004,10 @@ input.site-header-search[type="text"]:focus { border-color: var(--accent); }
/* Buttons */
.btn {
display: inline-flex; align-items: center; gap: 6px;
- height: var(--h-control); padding: 0 12px; border-radius: 8px; font-size: 11px;
- font-weight: 500; cursor: pointer; border: 1px solid transparent;
+ height: var(--btn-height); padding: 0 var(--btn-padding-x); border-radius: var(--btn-radius); font-size: var(--btn-font-size);
+ font-weight: var(--btn-font-weight); cursor: pointer; border: 1px solid transparent;
transition: all 0.15s; text-decoration: none; white-space: nowrap;
- font-family: inherit; line-height: 1; letter-spacing: 0.8px; text-transform: uppercase;
+ font-family: inherit; line-height: var(--btn-line-height); letter-spacing: var(--btn-letter-spacing); text-transform: uppercase;
}
.btn:disabled {
opacity: 0.65;
@@ -1024,7 +1031,7 @@ input.site-header-search[type="text"]:focus { border-color: var(--accent); }
@keyframes btn-spin {
to { transform: rotate(360deg); }
}
-.btn-sm { padding: 0 12px; font-size: 11px; line-height: 1; letter-spacing: 0.8px; text-transform: uppercase; }
+.btn-sm { padding: 0 var(--btn-padding-x); font-size: var(--btn-font-size); line-height: var(--btn-line-height); letter-spacing: var(--btn-letter-spacing); text-transform: uppercase; }
.btn-primary { background: var(--accent); color: #111111; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-outline { background: transparent; color: var(--text-primary); border: 1px solid var(--border); }
diff --git a/src/pages/ProjectDetail.jsx b/src/pages/ProjectDetail.jsx
index 4f2e089..756ec73 100644
--- a/src/pages/ProjectDetail.jsx
+++ b/src/pages/ProjectDetail.jsx
@@ -373,7 +373,7 @@ export default function ProjectDetailPage() {
);
})}
{activeTab === 'members' && isTeam && (
-
{ const currentIds = new Set(members.filter(m => m.profile?.role === 'external').map(m => m.profile_id)); setSelectedExts(currentIds); setAddingMember(true); }}>Manage
+
{ const currentIds = new Set(members.filter(m => m.profile?.role === 'external').map(m => m.profile_id)); setSelectedExts(currentIds); setAddingMember(true); }}>Manage
)}
);
@@ -484,7 +484,7 @@ export default function ProjectDetailPage() {
{m.profile?.avatar_url
?
- : {initials}
+ : {initials}
}
{m.profile?.name || '—'}
@@ -669,8 +669,8 @@ export default function ProjectDetailPage() {
{p.avatar_url
?
- :
-
{initials}
+ :
+ {initials}
}
diff --git a/src/pages/TaskDetail.jsx b/src/pages/TaskDetail.jsx
index 7c0c4f5..aa2ecca 100644
--- a/src/pages/TaskDetail.jsx
+++ b/src/pages/TaskDetail.jsx
@@ -192,7 +192,7 @@ function SubmissionFiles({ files, downloading, onDownloadAll }) {
className="btn btn-outline"
disabled={Boolean(downloading)}
onClick={() => onDownloadAll(files)}
- style={{ marginTop: 10, fontSize: 10, padding: '2px 10px', letterSpacing: 0.6, textTransform: 'uppercase' }}
+ style={{ marginTop: 10 }}
>
{downloading === 'all' ? 'Downloading…' : 'Download All'}
@@ -416,7 +416,7 @@ export default function TaskDetail() {
{task.title}
{isTeamOrSub && status === 'not_started' && (
- Start Task
+ Start Task
)}
{isTeamOrSub && status === 'in_progress' && task.assigned_to === currentUser?.id && (
<>
@@ -438,13 +438,13 @@ export default function TaskDetail() {
>
)}
{isClient && ['client_approved', 'invoiced', 'paid'].includes(status) && (
- setRevisionModal(true)}>Request Revision
+ setRevisionModal(true)}>Request Revision
)}
{submission?.service_type && (
- {submission.service_type}
+ {submission.service_type}
)}
@@ -529,7 +529,7 @@ export default function TaskDetail() {
{isClient && ['not_started', 'in_progress'].includes(status) && (
- setAmendModal(true)}>Amend Request
+ setAmendModal(true)}>Amend Request
)}
@@ -596,7 +596,7 @@ export default function TaskDetail() {
{isClient && i === 0 && ['not_started', 'in_progress'].includes(status) && (
- setAmendModal(true)}>Amend
+ setAmendModal(true)}>Amend
)}
@@ -632,7 +632,7 @@ export default function TaskDetail() {
onKeyDown={e => { if (e.key === 'Enter') handlePostComment(); }}
style={{ flex: 1, fontSize: 13, color: 'var(--text-primary)', background: 'var(--card-bg-2)', border: '1px solid var(--border)', borderRadius: 6, padding: '6px 10px', fontFamily: 'inherit', height: 30 }}
/>
- Post
+ Post
{comments.length === 0
? No comments yet.
@@ -649,7 +649,7 @@ export default function TaskDetail() {
{dateStr} · {timeStr}
{isOwn && (
- handleDeleteComment(c.id)} style={{ fontSize: 11, padding: '0 10px', height: 26 }}>Delete
+ handleDeleteComment(c.id)}>Delete
)}
{c.body}
@@ -659,12 +659,19 @@ export default function TaskDetail() {
}
)}
- {activeTab === 'Folder' && company?.name && project?.name && (
-
- )}
+ {activeTab === 'Folder' && company?.name && project?.name && (() => {
+ const fbInitial = role === 'external'
+ ? `/Projects/${safeName(project.name)}/${safeName(task.title)}`
+ : role === 'client'
+ ? `/${safeName(company.name)}/Projects/${safeName(project.name)}/${safeName(task.title)}`
+ : `/Clients/${safeName(company.name)}/Projects/${safeName(project.name)}/${safeName(task.title)}`;
+ const fbRoot = role === 'external'
+ ? `/Projects/${safeName(project.name)}`
+ : role === 'client'
+ ? `/${safeName(company.name)}/Projects/${safeName(project.name)}`
+ : `/Clients/${safeName(company.name)}/Projects/${safeName(project.name)}`;
+ return ;
+ })()}
{activeTab === 'Folder' && (!company?.name || !project?.name) && (
No folder available — task needs a project and company.
)}
@@ -729,7 +736,7 @@ export default function TaskDetail() {
Attach Files
setRevisionFiles(prev => [...prev, ...Array.from(e.target.files)])} />
-
revisionFileRef.current?.click()}>+ Add Files
+
revisionFileRef.current?.click()}>+ Add Files
{revisionFiles.length > 0 && (
{revisionFiles.map((f, i) => (
@@ -742,8 +749,8 @@ export default function TaskDetail() {
)}
- { setRevisionModal(false); setRevisionForm({ description: '', deadline: '' }); setRevisionFiles([]); }}>Cancel
-
+ { setRevisionModal(false); setRevisionForm({ description: '', deadline: '' }); setRevisionFiles([]); }}>Cancel
+
{revisionSaving ? 'Submitting…' : 'Submit Revision'}
@@ -781,7 +788,7 @@ export default function TaskDetail() {
{amendFiles.map((f, i) => (
{f.name}
- setAmendFiles(prev => prev.filter((_, idx) => idx !== i))} style={{ fontSize: 10, padding: '1px 8px', height: 22 }}>Remove
+ setAmendFiles(prev => prev.filter((_, idx) => idx !== i))}>Remove
))}
diff --git a/src/pages/team/TeamDashboard.jsx b/src/pages/team/TeamDashboard.jsx
index 6f9632e..f87022d 100644
--- a/src/pages/team/TeamDashboard.jsx
+++ b/src/pages/team/TeamDashboard.jsx
@@ -76,10 +76,9 @@ function ActionIcon({ actionKey, size = 27 }) {
}
function Avatar({ name, size = 27 }) {
- const tone = iconTone(name);
return (
-
-
+
+
@@ -88,10 +87,9 @@ function Avatar({ name, size = 27 }) {
}
function InitialPortrait({ name }) {
- const tone = iconTone(name);
return (
-
-
{(name || '?')[0].toUpperCase()}
+
+ {(name || '?')[0].toUpperCase()}
);
}
@@ -592,18 +590,17 @@ function TeamPerformanceCard({ tasks, profiles }) {
{people.slice(0, 5).map((p, i) => {
const pct = totalDone > 0 ? Math.round((p.done / totalDone) * 100) : 0;
- const tone = iconTone(p.name);
return (
0 ? 10 : 0 }}>
- {(() => { const prof = p.id ? profileMap.get(p.id) : null; const tone = iconTone(p.name); return prof?.avatar_url ?
:
; })()}
+ {(() => { const prof = p.id ? profileMap.get(p.id) : null; return prof?.avatar_url ?
:
; })()}
{p.id ? navigate(`/profile/${p.id}`)}>{p.name} : {p.name} }
{p.newCount} new · {p.revCount} revision
-
diff --git a/src/pages/team/TeamInvoices.jsx b/src/pages/team/TeamInvoices.jsx
index 065c0dc..9bd30df 100644
--- a/src/pages/team/TeamInvoices.jsx
+++ b/src/pages/team/TeamInvoices.jsx
@@ -30,7 +30,7 @@ const poStatusLabel = {
cancelled: 'Cancelled',
};
const RECEIPT_BUCKET = 'expense-receipts';
-const FIELD_LABEL_STYLE = { fontSize: 11, fontWeight: 400, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5, display: 'block', marginBottom: 4 };
+const FIELD_LABEL_STYLE = { fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.8, display: 'block', marginBottom: 4 };
const FIELD_INPUT_STYLE = { minHeight: 42, margin: 0 };
const blankExpense = () => ({
@@ -71,10 +71,14 @@ export default function Invoices() {
const [invoices, setInvoices] = useState(() => cached?.invoices || []);
const [loading, setLoading] = useState(() => !cached);
const [activeTab, setActiveTab] = useState(() => location.state?.tab || 'invoices');
+ const [financeTab, setFinanceTab] = useState('overview');
const [filter, setFilter] = useState('all');
const { sortKey: invSortKey, sortDir: invSortDir, toggle: invToggle, sort: invSort } = useSortable('invoice_date');
const { sortKey: expSortKey, sortDir: expSortDir, toggle: expToggle, sort: expSort } = useSortable('date');
const { sortKey: subSortKey, sortDir: subSortDir, toggle: subToggle, sort: subSort } = useSortable('submitted_at');
+ const { sortKey: ovExpKey, sortDir: ovExpDir, toggle: ovExpToggle } = useSortable('date');
+ const { sortKey: ovSubKey, sortDir: ovSubDir, toggle: ovSubToggle } = useSortable('date');
+ const { sortKey: ovInvKey, sortDir: ovInvDir, toggle: ovInvToggle } = useSortable('invoice_date');
const [filterCompany, setFilterCompany] = useState('');
const [exportYear, setExportYear] = useState(new Date().getFullYear());
const [exporting, setExporting] = useState(false);
@@ -498,46 +502,294 @@ export default function Invoices() {
return (
-
-
-
-
Finances
-
{invoices.length} invoice{invoices.length !== 1 ? 's' : ''} · {expenses.length} expense{expenses.length !== 1 ? 's' : ''} · {subcontractorPOs.length} subcontractor PO{subcontractorPOs.length !== 1 ? 's' : ''}
-
-
- {paidYears.length > 0 && (
-
setExportYear(Number(e.target.value))} style={{ width: 120 }}>
- {paidYears.map(y => {y} )}
-
+
+ {/* Top row: 60% chart + 40% stat cards */}
+ {(() => {
+ const yr = chartData.reduce((s, m) => s + m.Revenue, 0);
+ const yp = chartData.reduce((s, m) => s + m.Profit, 0);
+ const ye = chartData.reduce((s, m) => s + m.Expenses, 0);
+ const yo = chartData.reduce((s, m) => s + m.Outstanding, 0);
+ const fmt = v => v >= 1000 ? `$${(v/1000).toFixed(1)}k` : `$${v.toFixed(0)}`;
+ const CARD = { background: 'var(--card-bg)', border: '1px solid var(--border)', borderRadius: 8, padding: '18px 21px', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)' };
+ const StatCard = ({ label, value, sub, iconBg, iconColor, iconSvg }) => (
+
+
+
{label}
+
+ {sub &&
{sub}
}
+
+
+
+ );
+ return (
+
+
+
+ Revenue Overview
+ setChartYear(Number(e.target.value))} style={{ width: 90, borderRadius: 8, fontSize: 11, fontWeight: 500 }}>
+ {(paidYears.length > 0 ? paidYears : [new Date().getFullYear()]).map(y => {y} )}
+
+
+
+
+
+
+
+
+
+
+
+
+ `$${v >= 1000 ? (v/1000).toFixed(0)+'k' : v}`} width={45} />
+ } />
+
+
+
+
+
+
+
+
+
+
>} />
+ >} />
+ >} />
+ >} />
+
+
+ );
+ })()}
+
+
+
+ {[
+ { id: 'overview', label: 'Overview' },
+ { id: 'expenses', label: 'Expenses' },
+ { id: 'subcontractors', label: 'Subcontractors' },
+ { id: 'invoices', label: 'Invoices' },
+ { id: 'legacy', label: 'Legacy' },
+ ].map(t => (
+
setFinanceTab(t.id)}
+ style={{
+ fontFamily: "Fourge, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
+ fontSize: 13, fontWeight: 500, letterSpacing: 0.2,
+ padding: '2px 0 5px', margin: '0 8px',
+ borderRadius: 0, border: 'none',
+ borderBottom: financeTab === t.id ? '2px solid var(--accent)' : '2px solid transparent',
+ cursor: 'pointer', background: 'transparent',
+ color: financeTab === t.id ? 'var(--text-primary)' : 'var(--text-secondary)',
+ transition: 'all 160ms',
+ }}
+ >{t.label}
+ ))}
+ {financeTab === 'expenses' && (
+
+ { setEditingExpenseId(''); setNewExpense(blankExpense()); setExpensesError(''); setShowExpenseForm(true); }}>+ Expense
+
)}
-
- {exporting ? 'Exporting…' : 'Export for CPA'}
-
+
-
-
-
-
-
-
-
-
-
-
-
- `$${v >= 1000 ? (v/1000).toFixed(0)+'k' : v}`} width={45} />
- } />
-
-
-
-
-
-
-
-
+ {financeTab === 'overview' && (() => {
+ const CARD = { background: 'var(--card-bg)', border: '1px solid var(--border)', borderRadius: 8, padding: '18px 21px', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)' };
+ const TH = { fontSize: 10, fontWeight: 500, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.6, padding: '0 0 12px 0', border: 'none', background: 'transparent', textAlign: 'left' };
+ const TD = { fontSize: 13, color: 'var(--text-primary)', padding: '5px 0', border: 'none', background: 'transparent', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
+ const sortRows = (arr, key, dir, getter) => [...arr].sort((a, b) => {
+ const av = getter(a, key), bv = getter(b, key);
+ if (av < bv) return dir === 'asc' ? -1 : 1;
+ if (av > bv) return dir === 'asc' ? 1 : -1;
+ return 0;
+ });
+ const recentExp = sortRows(expenses, ovExpKey, ovExpDir, (e, k) => {
+ if (k === 'description') return (e.description || '').toLowerCase();
+ if (k === 'category') return (e.category || '').toLowerCase();
+ if (k === 'amount') return Number(e.amount) || 0;
+ return e.date || '';
+ }).slice(0, 5);
+ const recentSub = sortRows(subcontractorPOs, ovSubKey, ovSubDir, (po, k) => {
+ if (k === 'name') return (po.profile?.name || '').toLowerCase();
+ if (k === 'project') return (po.project?.name || '').toLowerCase();
+ if (k === 'amount') return Number(po.amount) || 0;
+ return po.paid_at || po.date || '';
+ }).slice(0, 5);
+ const recentInv = sortRows(invoices, ovInvKey, ovInvDir, (inv, k) => {
+ if (k === 'company') return (inv.company?.name || '').toLowerCase();
+ if (k === 'total') return Number(inv.total) || 0;
+ return inv.invoice_date || '';
+ }).slice(0, 5);
+ const fmtDate = d => d ? new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '—';
+ const fmtAmt = v => `$${Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+ return (
+
+ {/* Recent Expenses */}
+
+
Recent Expenses
+ {recentExp.length === 0 ?
No expenses
: (
+
+
+
+
+ Description
+ Category
+ Amount
+
+ {recentExp.map(e => (
+
+ {e.description || '—'}
+ {e.category || '—'}
+ {fmtAmt(e.amount)}
+
+ ))}
+
+
+ )}
+
+ {/* Recent Subcontractor Payments */}
+
+
Recent Subcontractor Payments
+ {recentSub.length === 0 ?
No payments
: (
+
+
+
+
+ Name
+ Project
+ Amount
+
+ {recentSub.map(po => (
+
+ {po.profile?.name || '—'}
+ {po.project?.name || '—'}
+ {fmtAmt(po.amount)}
+
+ ))}
+
+
+ )}
+
+ {/* Recent Invoices */}
+
+
Recent Invoices
+ {recentInv.length === 0 ?
No invoices
: (
+
+
+
+
+ Company
+ Date
+ Total
+
+ {recentInv.map(inv => (
+
+ {inv.company?.name || '—'}
+ {fmtDate(inv.invoice_date)}
+ {fmtAmt(inv.total)}
+
+ ))}
+
+
+ )}
+
+
+ );
+ })()}
+ {financeTab === 'expenses' && (() => {
+ const CARD = { background: 'var(--card-bg)', border: '1px solid var(--border)', borderRadius: 8, padding: '18px 21px', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)' };
+ const TH = { fontSize: 10, fontWeight: 500, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.6, padding: '0 0 12px 0', border: 'none', background: 'transparent', textAlign: 'left' };
+ const TD = { fontSize: 13, color: 'var(--text-primary)', padding: '5px 0', border: 'none', background: 'transparent', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
+ const fmtDate = d => d ? new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—';
+ const fmtAmt = v => `$${Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+ const sortedExpenses = expSort(expenses, (exp, key) => {
+ if (key === 'amount') return Number(exp.amount) || 0;
+ if (key === 'date') return exp.date || '';
+ return exp[key] || '';
+ });
+ const categoryTotals = CATEGORIES
+ .map(cat => ({ cat, total: expenses.filter(e => e.category === cat).reduce((s, e) => s + Number(e.amount || 0), 0) }))
+ .filter(x => x.total > 0)
+ .sort((a, b) => b.total - a.total);
+ const grandTotal = expenses.reduce((s, e) => s + Number(e.amount || 0), 0);
+ return (
+
+
+ {/* Left: expense list */}
+
+
Expenses
+ {expenses.length === 0 ?
No expenses
: (
+
+
+
+
+
+
+
+
+
+
+ Date
+ Description
+ Category
+ Notes
+ Amount
+
+ {sortedExpenses.map(exp => (
+ startEditExpense(exp)}>
+ {fmtDate(exp.date)}
+ {exp.description || '—'}
+ {exp.category || '—'}
+ {exp.notes || '—'}
+ {fmtAmt(exp.amount)}
+
+ ))}
+
+
+ )}
+
+ {/* Right: category breakdown */}
+
+
By Category
+ {categoryTotals.length === 0 ?
No expenses
: (
+
+
+ {categoryTotals.map(({ cat, total }) => {
+ const pct = grandTotal > 0 ? (total / grandTotal) * 100 : 0;
+ return (
+
+
+ {cat}
+ {fmtAmt(total)}
+
+
+
+ );
+ })}
+
+ Total
+ {fmtAmt(grandTotal)}
+
+
+
+ )}
+
+
+
+ );
+ })()}
+
+ {financeTab === 'legacy' &&
{[
{ id: 'invoices', label: 'INVOICES' },
@@ -886,7 +1138,9 @@ export default function Invoices() {
)}
+
}{/* end legacy wrapper */}
+
{/* end flex column wrapper */}
);
}