diff --git a/src/components/owner/SubcontractorPaymentsPanel.jsx b/src/components/owner/SubcontractorPaymentsPanel.jsx new file mode 100644 index 0000000..546b71c --- /dev/null +++ b/src/components/owner/SubcontractorPaymentsPanel.jsx @@ -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 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 && ( + + )} + + + {/* Project searchable dropdown */} +
+ + {projectOpen && ( +
+
+ + 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" + /> +
+
+ + {filteredProjectOptions.length === 0 ? ( +

No projects match "{projectQuery}"

+ ) : ( + filteredProjectOptions.map(p => { + const isSelected = p.name === projectFilter; + return ( + + ); + }) + )} +
+
+ )} +
+ + {/* Work Category filter */} + + + {/* Payment Status filter */} + + + {hasActiveFilters && ( +
+ +
+ )} + + )} + + {rows.length === 0 ? ( +
+ +

No subcontractor payments yet.

+

Tasks with logged fees or expenses will appear here.

+
+ ) : ( +
+ + + + + + + + + + + + + + {filteredRows.length === 0 && ( + + + + )} + {filteredRows.map((row) => ( + + + + + + + + + + ))} + + + + + + + + + + + +
SubcontractorProjectWork CategoryAmount to PayRequestedPaid DatePayment Status
+ +

No payments match the current filters.

+ +
+
+
+ {row.name?.[0] || '?'} +
+
+
{row.name}
+ {row.companyName && ( +
{row.companyName}
+ )} +
+
+
+
+
+ {row.projectName} +
+ {row.projectAddress && ( +
+ {row.projectAddress} +
+ )} +
+
+ + {row.workCategory} + + + + {fmt(row.amount)} + + + + {fmtDate(row.requestedAt)} + + + + {fmtDate(row.paidAt)} + + + + {row.paymentStatus} + +
+ {hasActiveFilters ? 'Filtered Total' : 'Total Amount'} + + + {fmt(total)} + + +
+ Outstanding (Unpaid) + + + {fmt(outstanding)} + + +
+
+ )} + + ); +}; + +export default SubcontractorPaymentsPanel; diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 0f9a7e8..cf438be 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -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_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', updatedAt: '2026-05-16T17:30:00Z', }, @@ -5194,6 +5197,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [ fees: [ { 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', updatedAt: '2026-05-12T17:00:00Z', }, @@ -5232,6 +5238,10 @@ const MOCK_SUBCONTRACTOR_TASKS = [ fees: [ { 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', updatedAt: '2026-05-14T17:30:00Z', }, @@ -5267,6 +5277,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [ ], expenses: [], fees: [], + projectId: 'proj_003', + paymentStatus: 'Unpaid', + paymentRequestedAt: null, createdAt: '2026-05-18T22:40:00Z', updatedAt: '2026-05-18T23:10:00Z', }, @@ -5306,6 +5319,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [ fees: [ { 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', updatedAt: '2026-05-15T16:20:00Z', }, @@ -5334,6 +5350,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [ thread: [], expenses: [], fees: [], + projectId: 'proj_011', + paymentStatus: 'Unpaid', + paymentRequestedAt: null, createdAt: '2026-05-17T14:00:00Z', updatedAt: '2026-05-17T14:00:00Z', }, @@ -5374,6 +5393,10 @@ const MOCK_SUBCONTRACTOR_TASKS = [ fees: [ { 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', 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_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', updatedAt: '2026-05-19T18:00:00Z', }, @@ -5455,6 +5481,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [ thread: [], expenses: [], fees: [], + projectId: null, + paymentStatus: 'Unpaid', + paymentRequestedAt: null, createdAt: '2026-05-09T10:00:00Z', updatedAt: '2026-05-13T12:00:00Z', }, diff --git a/src/pages/owner/OwnerSnapshot.jsx b/src/pages/owner/OwnerSnapshot.jsx index c9b1ac5..48e9e4b 100644 --- a/src/pages/owner/OwnerSnapshot.jsx +++ b/src/pages/owner/OwnerSnapshot.jsx @@ -8,6 +8,7 @@ import FinancialDetailsModal from '../../components/owner/FinancialDetailsModal' import ActionCenterModal from '../../components/owner/ActionCenterModal'; import TeamManagementPanel from '../../components/owner/TeamManagementPanel'; import CommissionDistributionPanel from '../../components/owner/CommissionDistributionPanel'; +import SubcontractorPaymentsPanel from '../../components/owner/SubcontractorPaymentsPanel'; import { SpotlightCard } from '../../components/SpotlightCard'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend @@ -24,6 +25,7 @@ const OwnerSnapshot = () => { orgCommissionDefaults, userCommissionOverrides, roleCommissionOverrides, orgMembers, addJobTeamMember, removeJobTeamMember, updateJobTeamMember, + subcontractorTasks, subcontractors, } = useMockStore(); // Find the current owner profile @@ -404,6 +406,23 @@ const OwnerSnapshot = () => { )} + + {/* ── Owner's Box — Subcontractor Payments ── */} + {subcontractorTasks?.length > 0 && ( +
+

+ + Subcontractor Payments +

+ + + +
+ )} {/* Interactive Modals */}