subcontractor payments table added in owners box

This commit is contained in:
Satyam Rastogi
2026-05-20 18:45:00 +05:30
parent 11b15a2e30
commit 5dd781ca45
3 changed files with 531 additions and 0 deletions
@@ -0,0 +1,483 @@
import React, { useMemo, useState, useRef, useEffect } from 'react';
import { Wallet, Users, Search, ChevronDown, X, Check, FilterX } from 'lucide-react';
const CATEGORY_COLORS = {
'Electrical': 'text-cyan-500 dark:text-cyan-400 bg-cyan-500/10 border-cyan-500/20',
'Roofing': 'text-orange-500 dark:text-orange-400 bg-orange-500/10 border-orange-500/20',
'Painting': 'text-purple-500 dark:text-purple-400 bg-purple-500/10 border-purple-500/20',
'Plumbing': 'text-blue-500 dark:text-blue-400 bg-blue-500/10 border-blue-500/20',
'Windows & Glazing': 'text-teal-500 dark:text-teal-400 bg-teal-500/10 border-teal-500/20',
};
const PAYMENT_STATUS_COLORS = {
'Paid': 'text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 border-emerald-500/20',
'Unpaid': 'text-amber-600 dark:text-amber-400 bg-amber-500/10 border-amber-500/20',
};
function taskTotal(task) {
const expenses = (task.expenses || []).reduce((s, e) => s + (Number(e.amount) || 0), 0);
const fees = (task.fees || []).reduce((s, f) => s + (Number(f.amount) || 0), 0);
return expenses + fees;
}
const fmt = (amt) =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(Number(amt) || 0);
const fmtDate = (iso) => {
if (!iso) return '—';
try {
return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
} catch {
return iso;
}
};
// Dark-mode-aware dropdown — replaces native <select> so the option list
// doesn't render with the OS default white background in dark mode.
function FilterPopover({ id, value, onChange, options, allLabel, ariaLabel }) {
const [open, setOpen] = useState(false);
const boxRef = useRef(null);
useEffect(() => {
if (!open) return;
const onClick = (e) => {
if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false);
};
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('mousedown', onClick);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onClick);
document.removeEventListener('keydown', onKey);
};
}, [open]);
const display = value === 'all' ? allLabel : value;
return (
<div className="relative" ref={boxRef}>
<button
id={id}
type="button"
aria-haspopup="listbox"
aria-expanded={open}
aria-label={ariaLabel}
onClick={() => setOpen(o => !o)}
className="w-full flex items-center justify-between gap-2 pl-3 pr-2 py-2 text-sm rounded-xl bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white outline-none focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500/40 transition-colors"
>
<span className="truncate text-left">{display}</span>
<ChevronDown size={14} className={`shrink-0 text-zinc-400 transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<div
role="listbox"
className="absolute z-30 left-0 right-0 mt-1 rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-white/10 shadow-xl shadow-black/10 dark:shadow-black/50 overflow-hidden"
>
<div className="max-h-56 overflow-y-auto custom-scrollbar py-1">
<button
type="button"
role="option"
aria-selected={value === 'all'}
onClick={() => { onChange('all'); setOpen(false); }}
className={`w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors ${value === 'all' ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10' : 'text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
>
<span>{allLabel}</span>
{value === 'all' && <Check size={12} strokeWidth={2.5} className="shrink-0 opacity-70" />}
</button>
{options.map(opt => {
const isSelected = opt === value;
return (
<button
key={opt}
type="button"
role="option"
aria-selected={isSelected}
onClick={() => { onChange(opt); setOpen(false); }}
className={`w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors ${isSelected ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10' : 'text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
>
<span className="truncate">{opt}</span>
{isSelected && <Check size={12} strokeWidth={2.5} className="shrink-0 opacity-70" />}
</button>
);
})}
</div>
</div>
)}
</div>
);
}
const SubcontractorPaymentsPanel = ({
subcontractorTasks = [],
subcontractors = [],
projects = [],
}) => {
const subById = useMemo(() => {
const m = new Map();
for (const s of subcontractors) m.set(s.id, s);
return m;
}, [subcontractors]);
const projectById = useMemo(() => {
const m = new Map();
for (const p of projects) m.set(p.id, p);
return m;
}, [projects]);
const rows = useMemo(() => {
const entries = [];
for (const task of subcontractorTasks) {
const amount = taskTotal(task);
if (amount <= 0) continue;
const sub = subById.get(task.subcontractorId);
const project = task.projectId ? projectById.get(task.projectId) : null;
entries.push({
key: task.id,
name: task.subcontractorName,
companyName: sub?.companyName || '',
projectName: project ? (project.projectType || project.address) : '—',
projectAddress: project?.address || '',
projectId: project?.id || null,
workCategory: sub?.tradeType || '—',
amount,
requestedAt: task.paymentRequestedAt || null,
paidAt: task.paidAt || null,
paymentStatus: task.paymentStatus || 'Unpaid',
});
}
return entries;
}, [subcontractorTasks, subById, projectById]);
// ── Filter state ───────────────────────────────────────────────
const [nameQuery, setNameQuery] = useState('');
const [projectFilter, setProjectFilter] = useState('all');
const [categoryFilter, setCategoryFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
// Searchable project dropdown
const [projectOpen, setProjectOpen] = useState(false);
const [projectQuery, setProjectQuery] = useState('');
const projectBoxRef = useRef(null);
useEffect(() => {
if (!projectOpen) return;
const onClick = (e) => {
if (projectBoxRef.current && !projectBoxRef.current.contains(e.target)) {
setProjectOpen(false);
setProjectQuery('');
}
};
document.addEventListener('mousedown', onClick);
return () => document.removeEventListener('mousedown', onClick);
}, [projectOpen]);
const projectOptions = useMemo(() => {
const map = new Map();
for (const r of rows) {
const key = r.projectName || '—';
if (!map.has(key)) map.set(key, { name: key });
}
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name));
}, [rows]);
const categoryOptions = useMemo(() => {
const set = new Set(rows.map(r => r.workCategory).filter(Boolean));
return Array.from(set).sort();
}, [rows]);
const statusOptions = useMemo(() => {
const set = new Set(rows.map(r => r.paymentStatus).filter(Boolean));
return Array.from(set).sort();
}, [rows]);
const filteredProjectOptions = useMemo(() => {
const q = projectQuery.trim().toLowerCase();
if (!q) return projectOptions;
return projectOptions.filter(p => (p.name || '').toLowerCase().includes(q));
}, [projectOptions, projectQuery]);
const selectedProjectLabel = projectFilter === 'all' ? 'All Projects' : projectFilter;
const filteredRows = useMemo(() => {
const q = nameQuery.trim().toLowerCase();
return rows.filter(r => {
if (q && !(r.name || '').toLowerCase().includes(q)) return false;
if (projectFilter !== 'all' && r.projectName !== projectFilter) return false;
if (categoryFilter !== 'all' && r.workCategory !== categoryFilter) return false;
if (statusFilter !== 'all' && r.paymentStatus !== statusFilter) return false;
return true;
});
}, [rows, nameQuery, projectFilter, categoryFilter, statusFilter]);
const total = useMemo(() => filteredRows.reduce((s, r) => s + r.amount, 0), [filteredRows]);
const outstanding = useMemo(
() => filteredRows.filter(r => r.paymentStatus === 'Unpaid').reduce((s, r) => s + r.amount, 0),
[filteredRows]
);
const hasActiveFilters =
nameQuery.trim() !== '' || projectFilter !== 'all' || categoryFilter !== 'all' || statusFilter !== 'all';
const clearFilters = () => {
setNameQuery('');
setProjectFilter('all');
setCategoryFilter('all');
setStatusFilter('all');
setProjectQuery('');
};
return (
<div className="flex flex-col gap-3">
{/* Header */}
<div className="flex items-center gap-3 flex-wrap">
<Wallet className="text-amber-500 dark:text-amber-400 drop-shadow-[0_0_8px_rgba(245,158,11,0.4)]" size={20} />
<h3 className="text-sm font-mono font-bold text-zinc-900 dark:text-white tracking-widest uppercase">
Subcontractor Payments
</h3>
<span className="text-[10px] font-mono text-zinc-500 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 rounded-full px-2.5 py-0.5">
{hasActiveFilters
? `${filteredRows.length} of ${rows.length} payment${rows.length === 1 ? '' : 's'}`
: `${rows.length} payment${rows.length === 1 ? '' : 's'}`
}
</span>
</div>
{/* Filter bar */}
{rows.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2.5 p-3 rounded-xl bg-zinc-50 dark:bg-white/[0.02] border border-zinc-200 dark:border-white/5">
{/* Search by Name */}
<div className="relative">
<label htmlFor="payments-name-search" className="sr-only">Search by subcontractor name</label>
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" size={14} />
<input
id="payments-name-search"
type="text"
placeholder="Search by name..."
value={nameQuery}
onChange={(e) => setNameQuery(e.target.value)}
className="w-full bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/10 rounded-xl py-2 pl-9 pr-8 text-sm text-zinc-900 dark:text-white placeholder:text-zinc-400 dark:placeholder:text-zinc-500 outline-none focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500/40"
/>
{nameQuery && (
<button
type="button"
onClick={() => setNameQuery('')}
aria-label="Clear name search"
className="absolute right-2 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200"
>
<X size={14} />
</button>
)}
</div>
{/* Project searchable dropdown */}
<div className="relative" ref={projectBoxRef}>
<button
type="button"
onClick={() => setProjectOpen(o => !o)}
className="w-full flex items-center justify-between gap-2 pl-3 pr-2 py-2 text-sm rounded-xl bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white outline-none focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500/40 transition-colors"
>
<span className="truncate text-left">{selectedProjectLabel}</span>
<ChevronDown size={14} className={`shrink-0 text-zinc-400 transition-transform ${projectOpen ? 'rotate-180' : ''}`} />
</button>
{projectOpen && (
<div className="absolute z-30 left-0 right-0 mt-1 rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-white/10 shadow-xl shadow-black/10 dark:shadow-black/50 overflow-hidden">
<div className="relative p-2 border-b border-zinc-100 dark:border-white/5">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" size={14} />
<input
autoFocus
type="text"
placeholder="Search projects..."
value={projectQuery}
onChange={(e) => setProjectQuery(e.target.value)}
className="w-full bg-zinc-50 dark:bg-black/40 border border-zinc-200 dark:border-white/10 rounded-lg py-1.5 pl-8 pr-2 text-sm text-zinc-900 dark:text-white placeholder:text-zinc-400 dark:placeholder:text-zinc-500 outline-none"
/>
</div>
<div className="max-h-56 overflow-y-auto custom-scrollbar py-1">
<button
type="button"
onClick={() => { setProjectFilter('all'); setProjectOpen(false); setProjectQuery(''); }}
className={`w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors ${projectFilter === 'all' ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10' : 'text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
>
<span>All Projects</span>
{projectFilter === 'all' && <Check size={12} strokeWidth={2.5} className="shrink-0 opacity-70" />}
</button>
{filteredProjectOptions.length === 0 ? (
<p className="px-3 py-2 text-xs text-zinc-400 dark:text-zinc-500">No projects match "{projectQuery}"</p>
) : (
filteredProjectOptions.map(p => {
const isSelected = p.name === projectFilter;
return (
<button
key={p.name}
type="button"
onClick={() => { setProjectFilter(p.name); setProjectOpen(false); setProjectQuery(''); }}
className={`w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors ${isSelected ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10' : 'text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
>
<span className="truncate font-semibold">{p.name}</span>
{isSelected && <Check size={12} strokeWidth={2.5} className="shrink-0 opacity-70" />}
</button>
);
})
)}
</div>
</div>
)}
</div>
{/* Work Category filter */}
<FilterPopover
id="payments-category-filter"
ariaLabel="Filter by work category"
value={categoryFilter}
onChange={setCategoryFilter}
options={categoryOptions}
allLabel="All Work Categories"
/>
{/* Payment Status filter */}
<FilterPopover
id="payments-status-filter"
ariaLabel="Filter by payment status"
value={statusFilter}
onChange={setStatusFilter}
options={statusOptions}
allLabel="All Payment Statuses"
/>
{hasActiveFilters && (
<div className="sm:col-span-2 lg:col-span-4 flex justify-end">
<button
type="button"
onClick={clearFilters}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white bg-zinc-100 dark:bg-white/5 hover:bg-zinc-200 dark:hover:bg-white/10 border border-zinc-200 dark:border-white/10 transition-colors"
>
<FilterX size={12} /> Clear Filters
</button>
</div>
)}
</div>
)}
{rows.length === 0 ? (
<div className="py-10 text-center">
<Users size={28} className="mx-auto mb-3 text-zinc-400" />
<p className="text-sm text-zinc-500">No subcontractor payments yet.</p>
<p className="text-xs text-zinc-400 mt-1">Tasks with logged fees or expenses will appear here.</p>
</div>
) : (
<div className="overflow-x-auto rounded-xl border border-zinc-200 dark:border-white/5">
<table className="w-full text-left min-w-[860px]">
<thead>
<tr className="border-b border-zinc-200 dark:border-white/5 bg-zinc-50 dark:bg-white/[0.02]">
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Subcontractor</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Project</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Work Category</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest text-right">Amount to Pay</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Requested</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Paid Date</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Payment Status</th>
</tr>
</thead>
<tbody>
{filteredRows.length === 0 && (
<tr>
<td colSpan={7} className="px-4 py-10 text-center">
<Users size={22} className="mx-auto mb-2 text-zinc-400" />
<p className="text-sm text-zinc-500">No payments match the current filters.</p>
<button
type="button"
onClick={clearFilters}
className="mt-3 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider text-amber-600 dark:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 transition-colors"
>
<FilterX size={12} /> Clear Filters
</button>
</td>
</tr>
)}
{filteredRows.map((row) => (
<tr key={row.key} className="border-b border-zinc-100 dark:border-white/[0.03] hover:bg-zinc-50 dark:hover:bg-white/[0.02] transition-colors">
<td className="px-4 py-3">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-full bg-amber-500/15 flex items-center justify-center text-[10px] font-black text-amber-600 dark:text-amber-400 flex-shrink-0">
{row.name?.[0] || '?'}
</div>
<div className="min-w-0">
<div className="text-xs font-semibold text-zinc-900 dark:text-white truncate max-w-[180px]">{row.name}</div>
{row.companyName && (
<div className="text-[10px] text-zinc-500 dark:text-zinc-400 truncate max-w-[180px]">{row.companyName}</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
<div className="min-w-0">
<div className="text-xs text-zinc-700 dark:text-zinc-300 truncate max-w-[200px]" title={row.projectName}>
{row.projectName}
</div>
{row.projectAddress && (
<div className="text-[10px] text-zinc-500 dark:text-zinc-400 truncate max-w-[200px]" title={row.projectAddress}>
{row.projectAddress}
</div>
)}
</div>
</td>
<td className="px-4 py-3">
<span className={`text-[11px] font-bold rounded-md px-1.5 py-0.5 border whitespace-nowrap ${CATEGORY_COLORS[row.workCategory] || 'text-zinc-400 bg-zinc-500/10 border-zinc-500/20'}`}>
{row.workCategory}
</span>
</td>
<td className="px-4 py-3 text-right">
<span className="font-mono text-xs font-bold text-amber-600 dark:text-amber-400">
{fmt(row.amount)}
</span>
</td>
<td className="px-4 py-3">
<span className="text-[11px] font-mono text-zinc-500 dark:text-zinc-400 whitespace-nowrap">
{fmtDate(row.requestedAt)}
</span>
</td>
<td className="px-4 py-3">
<span className={`text-[11px] font-mono whitespace-nowrap ${row.paidAt ? 'text-emerald-600 dark:text-emerald-400 font-semibold' : 'text-zinc-400 dark:text-zinc-500'}`}>
{fmtDate(row.paidAt)}
</span>
</td>
<td className="px-4 py-3">
<span className={`text-[11px] font-bold rounded-md px-1.5 py-0.5 border whitespace-nowrap ${PAYMENT_STATUS_COLORS[row.paymentStatus] || 'text-zinc-400 bg-zinc-500/10 border-zinc-500/20'}`}>
{row.paymentStatus}
</span>
</td>
</tr>
))}
</tbody>
<tfoot className="border-t-2 border-zinc-300 dark:border-white/10">
<tr className="bg-zinc-50 dark:bg-white/[0.03]">
<td colSpan={3} className="px-4 py-3 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">
{hasActiveFilters ? 'Filtered Total' : 'Total Amount'}
</td>
<td className="px-4 py-3 text-right">
<span className="font-mono text-sm font-extrabold text-amber-600 dark:text-amber-400">
{fmt(total)}
</span>
</td>
<td colSpan={3} />
</tr>
<tr className="bg-zinc-50 dark:bg-white/[0.03]">
<td colSpan={3} className="px-4 py-3 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">
Outstanding (Unpaid)
</td>
<td className="px-4 py-3 text-right">
<span className="font-mono text-sm font-extrabold text-amber-600 dark:text-amber-400">
{fmt(outstanding)}
</span>
</td>
<td colSpan={3} />
</tr>
</tfoot>
</table>
</div>
)}
</div>
);
};
export default SubcontractorPaymentsPanel;
+29
View File
@@ -5152,6 +5152,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [
{ id: 'sct_001_f1', description: 'Labour — Ridge Cap Replacement', type: 'Labour / Service Fee', amount: 350, createdAt: '2026-05-16T13:20:00Z' }, { id: 'sct_001_f1', description: 'Labour — Ridge Cap Replacement', type: 'Labour / Service Fee', amount: 350, createdAt: '2026-05-16T13:20:00Z' },
{ id: 'sct_001_f2', description: 'Mobilization to site', type: 'Mobilization Fee', amount: 75, createdAt: '2026-05-16T13:22:00Z' }, { id: 'sct_001_f2', description: 'Mobilization to site', type: 'Mobilization Fee', amount: 75, createdAt: '2026-05-16T13:22:00Z' },
], ],
projectId: 'proj_004',
paymentStatus: 'Unpaid',
paymentRequestedAt: '2026-05-16T17:30:00Z',
createdAt: '2026-05-15T09:12:00Z', createdAt: '2026-05-15T09:12:00Z',
updatedAt: '2026-05-16T17:30:00Z', updatedAt: '2026-05-16T17:30:00Z',
}, },
@@ -5194,6 +5197,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [
fees: [ fees: [
{ id: 'sct_002_f1', description: 'Labour — Day 1 disconnect / prep', type: 'Labour / Service Fee', amount: 425, createdAt: '2026-05-12T17:00:00Z' }, { id: 'sct_002_f1', description: 'Labour — Day 1 disconnect / prep', type: 'Labour / Service Fee', amount: 425, createdAt: '2026-05-12T17:00:00Z' },
], ],
projectId: 'proj_003',
paymentStatus: 'Unpaid',
paymentRequestedAt: '2026-05-12T17:00:00Z',
createdAt: '2026-05-10T15:40:00Z', createdAt: '2026-05-10T15:40:00Z',
updatedAt: '2026-05-12T17:00:00Z', updatedAt: '2026-05-12T17:00:00Z',
}, },
@@ -5232,6 +5238,10 @@ const MOCK_SUBCONTRACTOR_TASKS = [
fees: [ fees: [
{ id: 'sct_003_f1', description: 'Touch-up paint labour', type: 'Labour / Service Fee', amount: 180, createdAt: '2026-05-14T17:35:00Z' }, { id: 'sct_003_f1', description: 'Touch-up paint labour', type: 'Labour / Service Fee', amount: 180, createdAt: '2026-05-14T17:35:00Z' },
], ],
projectId: 'proj_006',
paymentStatus: 'Paid',
paymentRequestedAt: '2026-05-14T17:35:00Z',
paidAt: '2026-05-18T10:15:00Z',
createdAt: '2026-05-05T11:00:00Z', createdAt: '2026-05-05T11:00:00Z',
updatedAt: '2026-05-14T17:30:00Z', updatedAt: '2026-05-14T17:30:00Z',
}, },
@@ -5267,6 +5277,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [
], ],
expenses: [], expenses: [],
fees: [], fees: [],
projectId: 'proj_003',
paymentStatus: 'Unpaid',
paymentRequestedAt: null,
createdAt: '2026-05-18T22:40:00Z', createdAt: '2026-05-18T22:40:00Z',
updatedAt: '2026-05-18T23:10:00Z', updatedAt: '2026-05-18T23:10:00Z',
}, },
@@ -5306,6 +5319,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [
fees: [ fees: [
{ id: 'sct_005_f1', description: 'Inspection — initial visit', type: 'Inspection Fee', amount: 125, createdAt: '2026-05-14T13:00:00Z' }, { id: 'sct_005_f1', description: 'Inspection — initial visit', type: 'Inspection Fee', amount: 125, createdAt: '2026-05-14T13:00:00Z' },
], ],
projectId: 'proj_012',
paymentStatus: 'Unpaid',
paymentRequestedAt: '2026-05-14T13:00:00Z',
createdAt: '2026-05-12T10:00:00Z', createdAt: '2026-05-12T10:00:00Z',
updatedAt: '2026-05-15T16:20:00Z', updatedAt: '2026-05-15T16:20:00Z',
}, },
@@ -5334,6 +5350,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [
thread: [], thread: [],
expenses: [], expenses: [],
fees: [], fees: [],
projectId: 'proj_011',
paymentStatus: 'Unpaid',
paymentRequestedAt: null,
createdAt: '2026-05-17T14:00:00Z', createdAt: '2026-05-17T14:00:00Z',
updatedAt: '2026-05-17T14:00:00Z', updatedAt: '2026-05-17T14:00:00Z',
}, },
@@ -5374,6 +5393,10 @@ const MOCK_SUBCONTRACTOR_TASKS = [
fees: [ fees: [
{ id: 'sct_007_f1', description: 'Labour — deck repair (2 days)', type: 'Labour / Service Fee', amount: 920, createdAt: '2026-05-07T16:50:00Z' }, { id: 'sct_007_f1', description: 'Labour — deck repair (2 days)', type: 'Labour / Service Fee', amount: 920, createdAt: '2026-05-07T16:50:00Z' },
], ],
projectId: 'proj_011',
paymentStatus: 'Paid',
paymentRequestedAt: '2026-05-07T16:50:00Z',
paidAt: '2026-05-12T14:00:00Z',
createdAt: '2026-05-01T09:00:00Z', createdAt: '2026-05-01T09:00:00Z',
updatedAt: '2026-05-07T17:00:00Z', updatedAt: '2026-05-07T17:00:00Z',
}, },
@@ -5426,6 +5449,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [
{ id: 'sct_008_f1', description: 'Labour — Kitchen GFCI install (2 outlets + island)', type: 'Labour / Service Fee', amount: 220, createdAt: '2026-05-19T12:35:00Z' }, { id: 'sct_008_f1', description: 'Labour — Kitchen GFCI install (2 outlets + island)', type: 'Labour / Service Fee', amount: 220, createdAt: '2026-05-19T12:35:00Z' },
{ id: 'sct_008_f2', description: 'Mobilization to site', type: 'Mobilization Fee', amount: 75, createdAt: '2026-05-19T08:00:00Z' }, { id: 'sct_008_f2', description: 'Mobilization to site', type: 'Mobilization Fee', amount: 75, createdAt: '2026-05-19T08:00:00Z' },
], ],
projectId: 'proj_011',
paymentStatus: 'Unpaid',
paymentRequestedAt: '2026-05-19T12:35:00Z',
createdAt: '2026-05-16T13:00:00Z', createdAt: '2026-05-16T13:00:00Z',
updatedAt: '2026-05-19T18:00:00Z', updatedAt: '2026-05-19T18:00:00Z',
}, },
@@ -5455,6 +5481,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [
thread: [], thread: [],
expenses: [], expenses: [],
fees: [], fees: [],
projectId: null,
paymentStatus: 'Unpaid',
paymentRequestedAt: null,
createdAt: '2026-05-09T10:00:00Z', createdAt: '2026-05-09T10:00:00Z',
updatedAt: '2026-05-13T12:00:00Z', updatedAt: '2026-05-13T12:00:00Z',
}, },
+19
View File
@@ -8,6 +8,7 @@ import FinancialDetailsModal from '../../components/owner/FinancialDetailsModal'
import ActionCenterModal from '../../components/owner/ActionCenterModal'; import ActionCenterModal from '../../components/owner/ActionCenterModal';
import TeamManagementPanel from '../../components/owner/TeamManagementPanel'; import TeamManagementPanel from '../../components/owner/TeamManagementPanel';
import CommissionDistributionPanel from '../../components/owner/CommissionDistributionPanel'; import CommissionDistributionPanel from '../../components/owner/CommissionDistributionPanel';
import SubcontractorPaymentsPanel from '../../components/owner/SubcontractorPaymentsPanel';
import { SpotlightCard } from '../../components/SpotlightCard'; import { SpotlightCard } from '../../components/SpotlightCard';
import { import {
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend
@@ -24,6 +25,7 @@ const OwnerSnapshot = () => {
orgCommissionDefaults, userCommissionOverrides, orgCommissionDefaults, userCommissionOverrides,
roleCommissionOverrides, orgMembers, roleCommissionOverrides, orgMembers,
addJobTeamMember, removeJobTeamMember, updateJobTeamMember, addJobTeamMember, removeJobTeamMember, updateJobTeamMember,
subcontractorTasks, subcontractors,
} = useMockStore(); } = useMockStore();
// Find the current owner profile // Find the current owner profile
@@ -404,6 +406,23 @@ const OwnerSnapshot = () => {
</SpotlightCard> </SpotlightCard>
</section> </section>
)} )}
{/* ── Owner's Box — Subcontractor Payments ── */}
{subcontractorTasks?.length > 0 && (
<section>
<h2 className="text-xl font-bold text-zinc-900 dark:text-white/90 mb-4 flex items-center">
<span className="w-2 h-8 bg-amber-500 rounded-full mr-3"></span>
Subcontractor Payments
</h2>
<SpotlightCard className="p-5 lg:p-6" spotlightColor="rgba(245, 158, 11, 0.06)">
<SubcontractorPaymentsPanel
subcontractorTasks={subcontractorTasks}
subcontractors={subcontractors}
projects={projects}
/>
</SpotlightCard>
</section>
)}
</div> </div>
{/* Interactive Modals */} {/* Interactive Modals */}