From d420deaa0b34247bb19e62a15d503a543435d5f8 Mon Sep 17 00:00:00 2001 From: Mayur Shinde Date: Thu, 4 Jun 2026 12:36:43 +0530 Subject: [PATCH 1/7] Leads page: change door knock to Canvassers and Storm Chase to Storm Chase/Self-Gen --- src/components/leads/LeadJobSection.jsx | 4 +- src/data/mockStore.jsx | 74 ++++++++++++------------- src/pages/CreateLeadPage.jsx | 6 +- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/components/leads/LeadJobSection.jsx b/src/components/leads/LeadJobSection.jsx index 7d6db1a..a320f27 100644 --- a/src/components/leads/LeadJobSection.jsx +++ b/src/components/leads/LeadJobSection.jsx @@ -62,9 +62,9 @@ export default function LeadJobSection({ formData, updateField, isQuickCapture } accent="blue" /> - {/* --- Door Knock → Canvasser selector --- */} + {/* --- Canvassers → Canvasser selector --- */} - {formData.leadSource === 'Door Knock' && ( + {formData.leadSource === 'Canvassers' && ( { const next = { ...prev, [field]: value }; if (field === 'leadSource') { - if (value !== 'Door Knock') { next.canvasserId = ''; next.canvasserName = ''; } + if (value !== 'Canvassers') { next.canvasserId = ''; next.canvasserName = ''; } if (value !== 'Referral') { next.referralNote = ''; } } return next; @@ -215,7 +215,7 @@ export default function CreateLeadPage() { if (!formData.address) missingFields.push('Street Address'); if (!formData.city) missingFields.push('City'); if (!formData.leadSource) missingFields.push('Lead Source'); - if (formData.leadSource === 'Door Knock' && !formData.canvasserId) missingFields.push('Canvasser'); + if (formData.leadSource === 'Canvassers' && !formData.canvasserId) missingFields.push('Canvasser'); if (missingFields.length > 0) { setValidationError(`Required: ${missingFields.join(', ')}`); From 96381d4e0a7b24ec2adc631fbb506388f5fbe68f Mon Sep 17 00:00:00 2001 From: Mayur Shinde Date: Thu, 4 Jun 2026 14:53:35 +0530 Subject: [PATCH 2/7] Owners box: subcontractor table restrutcured name wise and display projects of each subcon with payment statuses --- .../owner/SubcontractorPaymentsPanel.jsx | 777 +++++++++++------- src/data/mockStore.jsx | 401 ++++++++- 2 files changed, 881 insertions(+), 297 deletions(-) diff --git a/src/components/owner/SubcontractorPaymentsPanel.jsx b/src/components/owner/SubcontractorPaymentsPanel.jsx index 546b71c..4e4dde8 100644 --- a/src/components/owner/SubcontractorPaymentsPanel.jsx +++ b/src/components/owner/SubcontractorPaymentsPanel.jsx @@ -1,6 +1,9 @@ import React, { useMemo, useState, useRef, useEffect } from 'react'; -import { Wallet, Users, Search, ChevronDown, X, Check, FilterX } from 'lucide-react'; +import { Wallet, Users, Search, ChevronDown, ChevronRight, X, Check, FilterX } from 'lucide-react'; +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── 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', @@ -9,47 +12,67 @@ const CATEGORY_COLORS = { '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', +// Contractor-level status: Paid | Partially Paid | Unpaid +const CONTRACTOR_STATUS = { + Paid: 'text-emerald-700 dark:text-emerald-400 bg-emerald-500/10 border-emerald-500/25', + 'Partially Paid':'text-amber-700 dark:text-amber-400 bg-amber-500/10 border-amber-500/25', + Unpaid: 'text-red-700 dark:text-red-400 bg-red-500/10 border-red-500/25', +}; + +// Request-level status colours +const REQUEST_STATUS = { + Paid: 'text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 border-emerald-500/20', + 'Partially Paid':'text-amber-600 dark:text-amber-400 bg-amber-500/10 border-amber-500/20', + Unpaid: 'text-red-600 dark:text-red-400 bg-red-500/10 border-red-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); + const fees = (task.fees || []).reduce((s, f) => s + (Number(f.amount) || 0), 0); return expenses + fees; } +function taskPaid(task) { + return Number(task.amountPaid || 0); +} + 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; - } + 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 */} + {/* Category filter */} - {/* Payment Status filter */} + {/* Contractor-status filter */} {hasActiveFilters && ( -
-
@@ -358,122 +512,155 @@ const SubcontractorPaymentsPanel = ({ )} - {rows.length === 0 ? ( + {/* Empty state */} + {contractorGroups.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.

+
+ + {/* ── Parent table header ── */} +
+ {['Contractor Name', 'Payment Requested', 'Amount Left to be Paid', 'Status', ''].map((h, i) => ( +
= 1 && i <= 2 ? 'text-right' : ''}`}> + {h} +
+ ))} +
+ + {filteredContractors.length === 0 ? ( +
+ +

No contractors match the current filters.

+ +
+ ) : ( +
+ {filteredContractors.map((c) => { + const remaining = Math.max(0, c.totalRequested - c.totalPaid); + const cStatus = contractorStatus(c.totalRequested, c.totalPaid); + const isExpanded = expanded.has(c.subId); + + return ( +
+ {/* ── Parent row — desktop grid ── */} -
-
-
- {row.name?.[0] || '?'} -
-
-
{row.name}
- {row.companyName && ( -
{row.companyName}
- )} -
-
-
-
-
- {row.projectName} -
- {row.projectAddress && ( -
- {row.projectAddress} + {/* Desktop */} +
+ {/* Contractor name */} +
+
+ {c.subName?.[0] || '?'} +
+
+
{c.subName}
+ {c.companyName &&
{c.companyName}
} +
+ {c.projects.length} project{c.projects.length !== 1 ? 's' : ''} · {allRequests.filter(r => r.subId === c.subId).length} request{allRequests.filter(r => r.subId === c.subId).length !== 1 ? 's' : ''} +
+
- )} -
-
- - {row.workCategory} - - - - {fmt(row.amount)} - - - - {fmtDate(row.requestedAt)} - - - - {fmtDate(row.paidAt)} - - - - {row.paymentStatus} - -
- {hasActiveFilters ? 'Filtered Total' : 'Total Amount'} - - - {fmt(total)} + {/* Payment Requested */} +
+ {fmt(c.totalRequested)} +
+ {/* Amount Left */} +
+ 0 ? 'text-red-500 dark:text-red-400' : 'text-zinc-400 dark:text-zinc-500'}`}> + {remaining > 0 ? fmt(remaining) : '—'} + +
+ {/* Status */} +
+ +
+ {/* Chevron */} +
+ {isExpanded + ? + : + } +
+ + + {/* Mobile card */} +
+
+
+
+ {c.subName?.[0] || '?'} +
+
+
{c.subName}
+ {c.companyName &&
{c.companyName}
} +
+
+
+ + {isExpanded + ? + : + } +
+
+
+
Requested: {fmt(c.totalRequested)}
+
Outstanding: 0 ? 'text-red-500 dark:text-red-400' : 'text-zinc-400'}`}>{remaining > 0 ? fmt(remaining) : '—'}
+
+
+ + + {/* ── Expanded detail ── */} + {isExpanded && ( +
+ +
+ )} + + ); + })} + + )} + + {/* Footer totals */} + {filteredContractors.length > 0 && ( +
+ {/* Desktop totals row */} +
+
+ {hasActiveFilters ? 'Filtered Total' : 'Grand Total'} +
+
+ {fmt(grandTotalRequested)} +
+
+ 0 ? 'text-red-500 dark:text-red-400' : 'text-zinc-400'}`}> + {grandTotalRemaining > 0 ? fmt(grandTotalRemaining) : '—'} -
-
- Outstanding (Unpaid) - - - {fmt(outstanding)} - - -
+
+
+
+ {/* Mobile totals */} +
+ {hasActiveFilters ? 'Filtered Total' : 'Grand Total'} +
+ {fmt(grandTotalRequested)} requested + {grandTotalRemaining > 0 && {fmt(grandTotalRemaining)} outstanding} +
+
+ + )} )} diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 50584c7..b03932a 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -7388,8 +7388,10 @@ const MOCK_SUBCONTRACTOR_TASKS = [ }, ], projectId: 'PRJ-2026-004', - paymentStatus: 'Unpaid', + paymentStatus: 'Partially Paid', paymentRequestedAt: '2026-05-16T17:30:00Z', + amountPaid: 350, + paidAt: '2026-05-20T11:00:00Z', createdAt: '2026-05-15T09:12:00Z', updatedAt: '2026-05-16T17:30:00Z', }, @@ -7484,6 +7486,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ projectId: 'PRJ-2026-003', paymentStatus: 'Unpaid', paymentRequestedAt: '2026-05-12T17:00:00Z', + amountPaid: 0, createdAt: '2026-05-10T15:40:00Z', updatedAt: '2026-05-12T17:00:00Z', }, @@ -7561,6 +7564,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ projectId: 'PRJ-2026-006', paymentStatus: 'Paid', paymentRequestedAt: '2026-05-14T17:35:00Z', + amountPaid: 232, paidAt: '2026-05-18T10:15:00Z', createdAt: '2026-05-05T11:00:00Z', updatedAt: '2026-05-14T17:30:00Z', @@ -7603,6 +7607,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ projectId: 'PRJ-2026-001', paymentStatus: 'Unpaid', paymentRequestedAt: null, + amountPaid: 0, createdAt: '2026-05-18T22:40:00Z', updatedAt: '2026-05-18T23:10:00Z', }, @@ -7672,8 +7677,10 @@ const MOCK_SUBCONTRACTOR_TASKS = [ }, ], projectId: 'PRJ-2026-007', - paymentStatus: 'Unpaid', + paymentStatus: 'Partially Paid', paymentRequestedAt: '2026-05-14T13:00:00Z', + amountPaid: 80, + paidAt: '2026-05-18T09:00:00Z', createdAt: '2026-05-12T10:00:00Z', updatedAt: '2026-05-15T16:20:00Z', }, @@ -7733,6 +7740,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ projectId: 'PRJ-2026-009', paymentStatus: 'Unpaid', paymentRequestedAt: null, + amountPaid: 0, createdAt: '2026-05-17T14:00:00Z', updatedAt: '2026-05-26T11:00:00Z', }, @@ -7812,6 +7820,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ projectId: 'PRJ-2026-001', paymentStatus: 'Paid', paymentRequestedAt: '2026-05-07T16:50:00Z', + amountPaid: 1089, paidAt: '2026-05-12T14:00:00Z', createdAt: '2026-05-01T09:00:00Z', updatedAt: '2026-05-07T17:00:00Z', @@ -7908,6 +7917,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ projectId: 'PRJ-2026-004', paymentStatus: 'Unpaid', paymentRequestedAt: '2026-05-19T12:35:00Z', + amountPaid: 0, createdAt: '2026-05-16T13:00:00Z', updatedAt: '2026-05-19T18:00:00Z', }, @@ -7996,6 +8006,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ projectId: 'PRJ-2026-006', paymentStatus: 'Unpaid', paymentRequestedAt: null, + amountPaid: 0, createdAt: '2026-05-20T09:00:00Z', updatedAt: '2026-05-27T11:05:00Z', }, @@ -8049,9 +8060,395 @@ const MOCK_SUBCONTRACTOR_TASKS = [ projectId: null, paymentStatus: 'Unpaid', paymentRequestedAt: null, + amountPaid: 0, createdAt: '2026-05-09T10:00:00Z', updatedAt: '2026-05-20T15:30:00Z', }, + + // ------------------------------------------------------------------- + // sct_011 — Maya Patel (sub_002) — full roof replacement PRJ-2026-001, PAID + // ------------------------------------------------------------------- + { + id: 'sct_011', + companyId: 'lynkeduppro', + companyName: 'LynkedUp Pro Roofing', + subcontractorId: 'sub_002', + subcontractorName: 'Maya Patel', + assignedBy: 'own_001', + assignedByName: 'Justin Johnson', + title: 'Full roof replacement — PRJ-001', + location: '2604 Dunwick Dr, Plano, TX 75023', + description: 'Insurance-approved full tear-off and replacement. 28 sq IKO Cambridge Charcoal. Includes ice & water shield at eaves.', + dueDate: '2026-04-20', + priority: 'high', + category: 'Roofing', + assignedDate: '2026-04-10T09:00:00Z', + status: 'Completed', + photos: [], + statusHistory: [ + { id: 'sct_011_h1', status: 'Assigned', actorId: 'own_001', actorName: 'Justin Johnson', comment: 'Task assigned.', at: '2026-04-10T09:00:00Z' }, + { id: 'sct_011_h2', status: 'Completed', actorId: 'sub_002', actorName: 'Maya Patel', comment: 'Roof complete.', at: '2026-04-18T15:00:00Z' }, + ], + thread: [], + expenses: [ + { id: 'sct_011_e1', description: 'IKO Cambridge Charcoal — 28 sq', category: 'Materials', amount: 2940, notes: '', receiptUrl: '', createdAt: '2026-04-11T08:00:00Z' }, + { id: 'sct_011_e2', description: 'Ice & water shield — 4 rolls', category: 'Materials', amount: 320, notes: '', receiptUrl: '', createdAt: '2026-04-11T08:10:00Z' }, + { id: 'sct_011_e3', description: 'Dumpster — 3 days', category: 'Disposal / Dumpster', amount: 420, notes: '', receiptUrl: '', createdAt: '2026-04-18T16:00:00Z' }, + { id: 'sct_011_e4', description: 'Fuel & crew transport', category: 'Travel / Fuel', amount: 140, notes: '', receiptUrl: '', createdAt: '2026-04-18T17:00:00Z' }, + ], + fees: [ + { id: 'sct_011_f1', description: 'Labour — full replacement (3-day crew)', type: 'Labour / Service Fee', amount: 4200, createdAt: '2026-04-18T15:30:00Z' }, + { id: 'sct_011_f2', description: 'Mobilization', type: 'Mobilization Fee', amount: 200, createdAt: '2026-04-10T09:00:00Z' }, + ], + activities: [], + projectId: 'PRJ-2026-001', + paymentStatus: 'Paid', + paymentRequestedAt: '2026-04-18T16:00:00Z', + amountPaid: 8220, + paidAt: '2026-04-25T10:00:00Z', + createdAt: '2026-04-10T09:00:00Z', + updatedAt: '2026-04-18T17:00:00Z', + }, + + // ------------------------------------------------------------------- + // sct_012 — Maya Patel (sub_002) — shingle repair PRJ-2026-003, Partially Paid + // ------------------------------------------------------------------- + { + id: 'sct_012', + companyId: 'lynkeduppro', + companyName: 'LynkedUp Pro Roofing', + subcontractorId: 'sub_002', + subcontractorName: 'Maya Patel', + assignedBy: 'a1', + assignedByName: 'Wade Hollis', + title: 'Hail damage shingle repair — PRJ-003', + location: '6613 Phoenix Pl, Plano, TX 75023', + description: 'Replace 6 damaged shingle sections on west slope identified during adjuster inspection.', + dueDate: '2026-05-10', + priority: 'medium', + category: 'Roofing', + assignedDate: '2026-05-02T09:00:00Z', + status: 'Completed', + photos: [], + statusHistory: [ + { id: 'sct_012_h1', status: 'Assigned', actorId: 'a1', actorName: 'Wade Hollis', comment: 'Assigned.', at: '2026-05-02T09:00:00Z' }, + { id: 'sct_012_h2', status: 'Completed', actorId: 'sub_002', actorName: 'Maya Patel', comment: 'Complete.', at: '2026-05-09T14:00:00Z' }, + ], + thread: [], + expenses: [ + { id: 'sct_012_e1', description: 'Materials — premium shingles, underlayment & sealant', category: 'Materials', amount: 1200, notes: '', receiptUrl: '', createdAt: '2026-05-03T07:30:00Z' }, + ], + fees: [ + { id: 'sct_012_f1', description: 'Labour — shingle repair and roof patch', type: 'Labour / Service Fee', amount: 3800, createdAt: '2026-05-09T14:10:00Z' }, + ], + activities: [], + projectId: 'PRJ-2026-003', + paymentStatus: 'Partially Paid', + paymentRequestedAt: '2026-05-09T15:00:00Z', + amountPaid: 4000, + paidAt: '2026-05-15T09:30:00Z', + createdAt: '2026-05-02T09:00:00Z', + updatedAt: '2026-05-09T15:00:00Z', + }, + + // ------------------------------------------------------------------- + // sct_013 — Carlos Mendoza (sub_001) — electrical rough-in PRJ-2026-001, PAID + // ------------------------------------------------------------------- + { + id: 'sct_013', + companyId: 'lynkeduppro', + companyName: 'LynkedUp Pro Roofing', + subcontractorId: 'sub_001', + subcontractorName: 'Carlos Mendoza', + assignedBy: 'own_001', + assignedByName: 'Justin Johnson', + title: 'Electrical rough-in — PRJ-001 addition', + location: '2604 Dunwick Dr, Plano, TX 75023', + description: 'Rough-in wiring for garage addition: 2 circuits, 4 outlets, overhead LED fixture prewire.', + dueDate: '2026-04-15', + priority: 'medium', + category: 'Electrical', + assignedDate: '2026-04-08T10:00:00Z', + status: 'Completed', + photos: [], + statusHistory: [ + { id: 'sct_013_h1', status: 'Assigned', actorId: 'own_001', actorName: 'Justin Johnson', comment: 'Assigned.', at: '2026-04-08T10:00:00Z' }, + { id: 'sct_013_h2', status: 'Completed', actorId: 'sub_001', actorName: 'Carlos Mendoza', comment: 'Complete.', at: '2026-04-14T16:00:00Z' }, + ], + thread: [], + expenses: [ + { id: 'sct_013_e1', description: '14/2 NM cable — 100 ft', category: 'Materials', amount: 68, notes: '', receiptUrl: '', createdAt: '2026-04-09T07:00:00Z' }, + { id: 'sct_013_e2', description: 'Outlet boxes + covers', category: 'Materials', amount: 32, notes: '', receiptUrl: '', createdAt: '2026-04-09T07:05:00Z' }, + { id: 'sct_013_e3', description: 'Conduit + fittings', category: 'Materials', amount: 41, notes: '', receiptUrl: '', createdAt: '2026-04-09T07:10:00Z' }, + ], + fees: [ + { id: 'sct_013_f1', description: 'Labour — rough-in (1.5 days)', type: 'Labour / Service Fee', amount: 640, createdAt: '2026-04-14T16:10:00Z' }, + { id: 'sct_013_f2', description: 'Mobilization', type: 'Mobilization Fee', amount: 75, createdAt: '2026-04-08T10:00:00Z' }, + ], + activities: [], + projectId: 'PRJ-2026-001', + paymentStatus: 'Paid', + paymentRequestedAt: '2026-04-14T17:00:00Z', + amountPaid: 856, + paidAt: '2026-04-20T10:00:00Z', + createdAt: '2026-04-08T10:00:00Z', + updatedAt: '2026-04-14T17:00:00Z', + }, + + // ------------------------------------------------------------------- + // sct_014 — Jennifer Castillo (sub_004) — plumbing PRJ-2026-004, Paid + // ------------------------------------------------------------------- + { + id: 'sct_014', + companyId: 'lynkeduppro', + companyName: 'LynkedUp Pro Roofing', + subcontractorId: 'sub_004', + subcontractorName: 'Jennifer Castillo', + assignedBy: 'own_001', + assignedByName: 'Justin Johnson', + title: 'Re-pipe master bath supply lines', + location: '2612 Dunwick Dr, Plano, TX 75023', + description: 'Replace old galvanized supply lines in master bath with 1/2" PEX. Two shut-offs, one shower valve, one sink valve.', + dueDate: '2026-04-30', + priority: 'medium', + category: 'Plumbing', + assignedDate: '2026-04-22T08:00:00Z', + status: 'Completed', + photos: [], + statusHistory: [ + { id: 'sct_014_h1', status: 'Assigned', actorId: 'own_001', actorName: 'Justin Johnson', comment: 'Assigned.', at: '2026-04-22T08:00:00Z' }, + { id: 'sct_014_h2', status: 'Completed', actorId: 'sub_004', actorName: 'Jennifer Castillo', comment: 'Complete.', at: '2026-04-28T15:00:00Z' }, + ], + thread: [], + expenses: [ + { id: 'sct_014_e1', description: '1/2" PEX — 50 ft', category: 'Materials', amount: 62, notes: '', receiptUrl: '', createdAt: '2026-04-23T07:30:00Z' }, + { id: 'sct_014_e2', description: 'PEX fittings + crimp rings', category: 'Materials', amount: 44, notes: '', receiptUrl: '', createdAt: '2026-04-23T07:35:00Z' }, + { id: 'sct_014_e3', description: 'Shower valve (Moen 2520)', category: 'Materials', amount: 128, notes: '', receiptUrl: '', createdAt: '2026-04-23T07:40:00Z' }, + ], + fees: [ + { id: 'sct_014_f1', description: 'Labour — master bath re-pipe', type: 'Labour / Service Fee', amount: 740, createdAt: '2026-04-28T15:10:00Z' }, + ], + activities: [], + projectId: 'PRJ-2026-004', + paymentStatus: 'Paid', + paymentRequestedAt: '2026-04-28T16:00:00Z', + amountPaid: 974, + paidAt: '2026-05-05T10:00:00Z', + createdAt: '2026-04-22T08:00:00Z', + updatedAt: '2026-04-28T16:00:00Z', + }, + + // ------------------------------------------------------------------- + // sct_015 — Jennifer Castillo (sub_004) — PRJ-2026-007, Partially Paid + // ------------------------------------------------------------------- + { + id: 'sct_015', + companyId: 'lynkeduppro', + companyName: 'LynkedUp Pro Roofing', + subcontractorId: 'sub_004', + subcontractorName: 'Jennifer Castillo', + assignedBy: 'a1', + assignedByName: 'Wade Hollis', + title: 'Hot water heater replacement — PRJ-007', + location: '6909 Custer Rd, Plano, TX 75023', + description: 'Replace 40-gal gas water heater (unit failed). Install Bradford White 40-gal NG, reconnect supply & drain.', + dueDate: '2026-05-20', + priority: 'high', + category: 'Plumbing', + assignedDate: '2026-05-14T08:00:00Z', + status: 'Completed', + photos: [], + statusHistory: [ + { id: 'sct_015_h1', status: 'Assigned', actorId: 'a1', actorName: 'Wade Hollis', comment: 'Assigned.', at: '2026-05-14T08:00:00Z' }, + { id: 'sct_015_h2', status: 'Completed', actorId: 'sub_004', actorName: 'Jennifer Castillo', comment: 'Complete.', at: '2026-05-16T13:00:00Z' }, + ], + thread: [], + expenses: [ + { id: 'sct_015_e1', description: 'Bradford White 40-gal NG heater', category: 'Materials', amount: 680, notes: '', receiptUrl: '', createdAt: '2026-05-14T10:00:00Z' }, + { id: 'sct_015_e2', description: 'Gas flex connector + fittings', category: 'Materials', amount: 48, notes: '', receiptUrl: '', createdAt: '2026-05-14T10:05:00Z' }, + { id: 'sct_015_e3', description: 'Expansion tank + pressure relief', category: 'Materials', amount: 95, notes: '', receiptUrl: '', createdAt: '2026-05-14T10:10:00Z' }, + { id: 'sct_015_e4', description: 'Haul-away — old unit', category: 'Disposal / Dumpster', amount: 65, notes: '', receiptUrl: '', createdAt: '2026-05-16T14:00:00Z' }, + ], + fees: [ + { id: 'sct_015_f1', description: 'Labour — WHR install', type: 'Labour / Service Fee', amount: 420, createdAt: '2026-05-16T13:10:00Z' }, + { id: 'sct_015_f2', description: 'Mobilization', type: 'Mobilization Fee', amount: 75, createdAt: '2026-05-14T08:00:00Z' }, + ], + activities: [], + projectId: 'PRJ-2026-007', + paymentStatus: 'Partially Paid', + paymentRequestedAt: '2026-05-16T14:00:00Z', + amountPaid: 700, + paidAt: '2026-05-22T10:00:00Z', + createdAt: '2026-05-14T08:00:00Z', + updatedAt: '2026-05-16T14:00:00Z', + }, + + // ------------------------------------------------------------------- + // sct_016 — Dwayne Boudreaux (sub_003) — interior repaint PRJ-2026-004, Paid + // ------------------------------------------------------------------- + { + id: 'sct_016', + companyId: 'lynkeduppro', + companyName: 'LynkedUp Pro Roofing', + subcontractorId: 'sub_003', + subcontractorName: 'Dwayne Boudreaux', + assignedBy: 'own_001', + assignedByName: 'Justin Johnson', + title: 'Interior repaint — living & dining', + location: '2612 Dunwick Dr, Plano, TX 75023', + description: 'Repaint living room and dining area after drywall repair. 2 coats SW Accessible Beige. Mask trim.', + dueDate: '2026-05-05', + priority: 'low', + category: 'Painting', + assignedDate: '2026-04-28T11:00:00Z', + status: 'Completed', + photos: [], + statusHistory: [ + { id: 'sct_016_h1', status: 'Assigned', actorId: 'own_001', actorName: 'Justin Johnson', comment: 'Assigned.', at: '2026-04-28T11:00:00Z' }, + { id: 'sct_016_h2', status: 'Completed', actorId: 'sub_003', actorName: 'Dwayne Boudreaux', comment: 'Complete.', at: '2026-05-03T15:00:00Z' }, + ], + thread: [], + expenses: [ + { id: 'sct_016_e1', description: 'SW Accessible Beige — 2 gal', category: 'Materials', amount: 104, notes: '', receiptUrl: '', createdAt: '2026-04-29T08:00:00Z' }, + { id: 'sct_016_e2', description: 'Tape, drop cloths, roller kit', category: 'Materials', amount: 38, notes: '', receiptUrl: '', createdAt: '2026-04-29T08:05:00Z' }, + ], + fees: [ + { id: 'sct_016_f1', description: 'Labour — 2-day interior repaint', type: 'Labour / Service Fee', amount: 760, createdAt: '2026-05-03T15:10:00Z' }, + ], + activities: [], + projectId: 'PRJ-2026-004', + paymentStatus: 'Paid', + paymentRequestedAt: '2026-05-03T16:00:00Z', + amountPaid: 902, + paidAt: '2026-05-10T09:00:00Z', + createdAt: '2026-04-28T11:00:00Z', + updatedAt: '2026-05-03T16:00:00Z', + }, + + // ------------------------------------------------------------------- + // sct_017 — Dwayne Boudreaux (sub_003) — PRJ-2026-007 ceiling repaint, Unpaid + // ------------------------------------------------------------------- + { + id: 'sct_017', + companyId: 'lynkeduppro', + companyName: 'LynkedUp Pro Roofing', + subcontractorId: 'sub_003', + subcontractorName: 'Dwayne Boudreaux', + assignedBy: 'a1', + assignedByName: 'Wade Hollis', + title: 'Ceiling repaint after water damage — PRJ-007', + location: '6909 Custer Rd, Plano, TX 75023', + description: 'Repaint ceilings in master bedroom and en-suite after remediation. Stain block primer + 2 coats flat white.', + dueDate: '2026-05-28', + priority: 'medium', + category: 'Painting', + assignedDate: '2026-05-18T10:00:00Z', + status: 'Completed', + photos: [], + statusHistory: [ + { id: 'sct_017_h1', status: 'Assigned', actorId: 'a1', actorName: 'Wade Hollis', comment: 'Assigned.', at: '2026-05-18T10:00:00Z' }, + { id: 'sct_017_h2', status: 'Completed', actorId: 'sub_003', actorName: 'Dwayne Boudreaux', comment: 'Complete.', at: '2026-05-25T14:00:00Z' }, + ], + thread: [], + expenses: [ + { id: 'sct_017_e1', description: 'Zinsser stain-block primer — 1 gal', category: 'Materials', amount: 52, notes: '', receiptUrl: '', createdAt: '2026-05-19T08:00:00Z' }, + { id: 'sct_017_e2', description: 'Ceiling flat white — 1 gal', category: 'Materials', amount: 42, notes: '', receiptUrl: '', createdAt: '2026-05-19T08:05:00Z' }, + ], + fees: [ + { id: 'sct_017_f1', description: 'Labour — ceiling repaint (1.5 days)', type: 'Labour / Service Fee', amount: 580, createdAt: '2026-05-25T14:10:00Z' }, + ], + activities: [], + projectId: 'PRJ-2026-007', + paymentStatus: 'Unpaid', + paymentRequestedAt: '2026-05-25T15:00:00Z', + amountPaid: 0, + createdAt: '2026-05-18T10:00:00Z', + updatedAt: '2026-05-25T15:00:00Z', + }, + + // ------------------------------------------------------------------- + // sct_018 — Greg Alston (sub_005) — window replacement PRJ-2026-003, Partially Paid + // ------------------------------------------------------------------- + { + id: 'sct_018', + companyId: 'lynkeduppro', + companyName: 'LynkedUp Pro Roofing', + subcontractorId: 'sub_005', + subcontractorName: 'Greg Alston', + assignedBy: 'own_001', + assignedByName: 'Justin Johnson', + title: 'Replace 3 hail-damaged windows — PRJ-003', + location: '6613 Phoenix Pl, Plano, TX 75023', + description: 'Insurance scope: replace 3 double-hung windows (front elevation). Source Andersen 400 series double-pane. Include disposal of broken units.', + dueDate: '2026-05-15', + priority: 'high', + category: 'Windows & Glazing', + assignedDate: '2026-05-03T09:00:00Z', + status: 'Completed', + photos: [], + statusHistory: [ + { id: 'sct_018_h1', status: 'Assigned', actorId: 'own_001', actorName: 'Justin Johnson', comment: 'Assigned.', at: '2026-05-03T09:00:00Z' }, + { id: 'sct_018_h2', status: 'Completed', actorId: 'sub_005', actorName: 'Greg Alston', comment: 'Complete.', at: '2026-05-13T15:00:00Z' }, + ], + thread: [], + expenses: [ + { id: 'sct_018_e1', description: 'Andersen 400 DH windows (qty 3)', category: 'Materials', amount: 1470, notes: '', receiptUrl: '', createdAt: '2026-05-04T08:00:00Z' }, + { id: 'sct_018_e2', description: 'Weatherstrip + sealant', category: 'Materials', amount: 55, notes: '', receiptUrl: '', createdAt: '2026-05-04T08:05:00Z' }, + { id: 'sct_018_e3', description: 'Disposal — broken units', category: 'Disposal / Dumpster', amount: 90, notes: '', receiptUrl: '', createdAt: '2026-05-13T16:00:00Z' }, + ], + fees: [ + { id: 'sct_018_f1', description: 'Labour — window replacement (2 days)', type: 'Labour / Service Fee', amount: 960, createdAt: '2026-05-13T15:10:00Z' }, + { id: 'sct_018_f2', description: 'Mobilization', type: 'Mobilization Fee', amount: 100, createdAt: '2026-05-03T09:00:00Z' }, + ], + activities: [], + projectId: 'PRJ-2026-003', + paymentStatus: 'Partially Paid', + paymentRequestedAt: '2026-05-13T16:00:00Z', + amountPaid: 1500, + paidAt: '2026-05-20T10:00:00Z', + createdAt: '2026-05-03T09:00:00Z', + updatedAt: '2026-05-13T16:00:00Z', + }, + + // ------------------------------------------------------------------- + // sct_019 — Greg Alston (sub_005) — window sealing PRJ-2026-009, Unpaid + // ------------------------------------------------------------------- + { + id: 'sct_019', + companyId: 'lynkeduppro', + companyName: 'LynkedUp Pro Roofing', + subcontractorId: 'sub_005', + subcontractorName: 'Greg Alston', + assignedBy: 'a1', + assignedByName: 'Wade Hollis', + title: 'Re-seal & caulk all windows — PRJ-009', + location: '7224 Independence Pkwy, Plano, TX 75025', + description: 'Full perimeter re-seal on 8 windows. Remove old silicone, clean frames, apply new NP1 polyurethane sealant.', + dueDate: '2026-06-05', + priority: 'low', + category: 'Windows & Glazing', + assignedDate: '2026-05-22T09:00:00Z', + status: 'In Progress', + photos: [], + statusHistory: [ + { id: 'sct_019_h1', status: 'Assigned', actorId: 'a1', actorName: 'Wade Hollis', comment: 'Assigned.', at: '2026-05-22T09:00:00Z' }, + { id: 'sct_019_h2', status: 'In Progress', actorId: 'sub_005', actorName: 'Greg Alston', comment: 'In progress.', at: '2026-05-28T08:30:00Z' }, + ], + thread: [], + expenses: [ + { id: 'sct_019_e1', description: 'NP1 polyurethane sealant (6 tubes)', category: 'Materials', amount: 78, notes: '', receiptUrl: '', createdAt: '2026-05-23T07:00:00Z' }, + ], + fees: [ + { id: 'sct_019_f1', description: 'Labour — window sealing (1 day)', type: 'Labour / Service Fee', amount: 380, createdAt: '2026-05-28T12:00:00Z' }, + ], + activities: [], + projectId: 'PRJ-2026-009', + paymentStatus: 'Unpaid', + paymentRequestedAt: '2026-05-28T12:00:00Z', + amountPaid: 0, + createdAt: '2026-05-22T09:00:00Z', + updatedAt: '2026-05-28T12:00:00Z', + }, ]; const MOCK_NOTIFICATIONS = [ From 2f1703870a8969415ef009c6c7a69e721d148259 Mon Sep 17 00:00:00 2001 From: Mayur Shinde Date: Thu, 4 Jun 2026 15:37:16 +0530 Subject: [PATCH 3/7] Owners box:navigate user to project details page when click on project from subcontractor table list --- .../owner/SubcontractorPaymentsPanel.jsx | 14 +- src/data/mockStore.jsx | 341 ------------------ src/pages/owner/OwnerProjectDetail.jsx | 17 +- src/pages/owner/OwnerSnapshot.jsx | 2 +- 4 files changed, 27 insertions(+), 347 deletions(-) diff --git a/src/components/owner/SubcontractorPaymentsPanel.jsx b/src/components/owner/SubcontractorPaymentsPanel.jsx index 4e4dde8..c33fa7c 100644 --- a/src/components/owner/SubcontractorPaymentsPanel.jsx +++ b/src/components/owner/SubcontractorPaymentsPanel.jsx @@ -1,4 +1,5 @@ import React, { useMemo, useState, useRef, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Wallet, Users, Search, ChevronDown, ChevronRight, X, Check, FilterX } from 'lucide-react'; // ───────────────────────────────────────────────────────────────────────────── @@ -134,17 +135,25 @@ function StatusBadge({ status, colorMap }) { // Expanded detail view (project → requests hierarchy) // ───────────────────────────────────────────────────────────────────────────── function ContractorDetail({ projects }) { + const navigate = useNavigate(); + return (
{projects.map((proj, pi) => (
{/* Project header */} -
+
proj.projectId && navigate(`/owner/projects/${proj.projectId}?tab=budget`)} + className={`flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-2.5 bg-zinc-50 dark:bg-white/[0.03] border-b border-zinc-200 dark:border-white/[0.06] ${proj.projectId ? 'cursor-pointer hover:bg-zinc-100 dark:hover:bg-white/[0.06] transition-colors' : ''}`} + > Project - {proj.projectName} + {proj.projectName} {proj.projectAddress && ( {proj.projectAddress} )} + {proj.projectId && ( + + )}
{/* Requests — table on md+, cards on mobile */} @@ -298,6 +307,7 @@ const SubcontractorPaymentsPanel = ({ if (requested <= 0) continue; const sub = subById.get(task.subcontractorId); const project = task.projectId ? projectById.get(task.projectId) : null; + if (!project) continue; entries.push({ taskId: task.id, subId: task.subcontractorId, diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index b03932a..fd0a296 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -7612,139 +7612,6 @@ const MOCK_SUBCONTRACTOR_TASKS = [ updatedAt: '2026-05-18T23:10:00Z', }, - // ------------------------------------------------------------------- - // sct_005 — Maya (sub_002) — Attic ventilation inspection, On Hold - // ------------------------------------------------------------------- - { - id: 'sct_005', - companyId: 'lynkeduppro', - companyName: 'LynkedUp Pro Roofing', - subcontractorId: 'sub_002', - subcontractorName: 'Maya Patel', - assignedBy: 'a1', - assignedByName: 'Wade Hollis', - title: 'Inspect attic ventilation', - location: '6909 Custer Rd, Plano, TX 75023', - description: 'Customer reports unusually high attic temps. Check ridge vent, soffit airflow, and intake/exhaust balance. Recommend remediation.', - dueDate: '2026-05-30', - priority: 'medium', - category: 'Inspection', - assignedDate: '2026-05-12T10:00:00Z', - status: 'On Hold', - photos: [ - { id: 'sct_005_p1', name: 'Attic_View.jpg', url: '/assets/images/properties/Roof_Inspection_Angle.jpg', annotation: 'Access from south gable' }, - ], - statusHistory: [ - { id: 'sct_005_h1', status: 'Assigned', actorId: 'a1', actorName: 'Wade Hollis', comment: 'Task created.', at: '2026-05-12T10:00:00Z' }, - { id: 'sct_005_h2', status: 'In Progress', actorId: 'sub_002', actorName: 'Maya Patel', comment: 'Initial pass done; baffles look blocked.', at: '2026-05-14T11:00:00Z' }, - { id: 'sct_005_h3', status: 'On Hold', actorId: 'sub_002', actorName: 'Maya Patel', comment: 'Waiting on homeowner to clear stored boxes from access path.', at: '2026-05-15T16:00:00Z' }, - ], - thread: [ - { id: 'sct_005_t1', channel: 'group', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Access is blocked — homeowner needs to move some storage before I can complete the inspection.', at: '2026-05-15T15:55:00Z', mine: true }, - { id: 'sct_005_t2', channel: 'direct', senderId: 'a1', senderName: 'Wade Hollis', senderRole: 'Admin', body: 'I will reach out to the homeowner and reschedule.', at: '2026-05-15T16:20:00Z' }, - ], - expenses: [ - { id: 'sct_005_e1', description: 'Infrared thermometer (rental)', category: 'Equipment Rental', amount: 35.0, notes: '', receiptUrl: '', createdAt: '2026-05-14T10:30:00Z' }, - ], - fees: [ - { id: 'sct_005_f1', description: 'Inspection — initial visit', type: 'Inspection Fee', amount: 125, createdAt: '2026-05-14T13:00:00Z' }, - ], - activities: [ - { - id: 'sct_005_a1', type: 'status_change', status: 'Assigned', - note: 'Task created. Customer reports unusually high attic temps.', - photos: [], - actorId: 'a1', actorName: 'Wade Hollis', actorRole: 'Admin', - at: '2026-05-12T10:00:00Z', - }, - { - id: 'sct_005_a2', type: 'status_change', status: 'In Progress', - note: 'Initial inspection pass — soffit baffles appear blocked. Need attic access for confirmation.', - photos: [ - { id: 'sct_005_a2_p1', url: '/assets/images/properties/Roof_Inspection_Angle.jpg', name: 'south-gable.jpg' }, - ], - actorId: 'sub_002', actorName: 'Maya Patel', actorRole: 'Subcontractor', - at: '2026-05-14T11:00:00Z', - }, - { - id: 'sct_005_a3', type: 'status_change', status: 'On Hold', - note: 'Work stopped — homeowner needs to clear stored boxes blocking attic access before I can finish.', - photos: [ - { id: 'sct_005_a3_p1', url: '/assets/images/properties/Beige_Two_Story_House.jpg', name: 'access-blocked.jpg' }, - ], - actorId: 'sub_002', actorName: 'Maya Patel', actorRole: 'Subcontractor', - at: '2026-05-15T16:00:00Z', - }, - ], - projectId: 'PRJ-2026-007', - paymentStatus: 'Partially Paid', - paymentRequestedAt: '2026-05-14T13:00:00Z', - amountPaid: 80, - paidAt: '2026-05-18T09:00:00Z', - createdAt: '2026-05-12T10:00:00Z', - updatedAt: '2026-05-15T16:20:00Z', - }, - - // ------------------------------------------------------------------- - // sct_006 — Maya (sub_002) — Gutter replacement, Pre-Work Inspection - // ------------------------------------------------------------------- - { - id: 'sct_006', - companyId: 'lynkeduppro', - companyName: 'LynkedUp Pro Roofing', - subcontractorId: 'sub_002', - subcontractorName: 'Maya Patel', - assignedBy: 'own_001', - assignedByName: 'Justin Johnson', - title: 'Replace gutter on south elevation', - location: '7224 Independence Pkwy, Plano, TX 75025', - description: '30 ft section of K-style gutter — pulling away from fascia after storm. Replace with new 6" oversized gutter, match existing bronze color.', - dueDate: '2026-06-02', - priority: 'low', - category: 'Gutters', - assignedDate: '2026-05-17T14:00:00Z', - status: 'Pre-Work Inspection', - photos: [ - { id: 'sct_006_p1', name: 'Gutter_Damage.jpg', url: '/assets/images/properties/Broken_Roof_Surface.jpg', annotation: 'Gutter pulling away from fascia — south elevation' }, - ], - statusHistory: [ - { id: 'sct_006_h1', status: 'Assigned', actorId: 'own_001', actorName: 'Justin Johnson', comment: 'Task created and assigned.', at: '2026-05-17T14:00:00Z' }, - { id: 'sct_006_h2', status: 'Pre-Work Inspection', actorId: 'sub_002', actorName: 'Maya Patel', comment: 'Site walk done — materials staged, access confirmed. Measured 32 ft run, bronze 6" gutter ordered.', at: '2026-05-26T10:00:00Z' }, - ], - thread: [ - { id: 'sct_006_t1', channel: 'group', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'On site for pre-work walkthrough. Gutter is pulling at four hangers — 32 ft run needs full replacement. Ordering materials now.', at: '2026-05-26T10:05:00Z', mine: true }, - { id: 'sct_006_t2', channel: 'group', senderId: 'own_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Perfect. Confirm when materials arrive and I will coordinate access with the homeowner.', at: '2026-05-26T10:30:00Z' }, - ], - expenses: [ - { id: 'sct_006_e1', description: '6" K-style gutter bronze (32 ft)', category: 'Materials', amount: 118.0, notes: 'Special order — delivery expected May 28.', receiptUrl: '', createdAt: '2026-05-26T11:00:00Z' }, - ], - fees: [], - activities: [ - { - id: 'sct_006_a1', type: 'status_change', status: 'Assigned', - note: 'Task created and assigned. 30 ft gutter section pulling away from fascia after storm damage.', - photos: [], - actorId: 'own_001', actorName: 'Justin Johnson', actorRole: 'Owner', - at: '2026-05-17T14:00:00Z', - }, - { - id: 'sct_006_a2', type: 'status_change', status: 'Pre-Work Inspection', - note: 'Site walk done — measured 32 ft run, confirmed bronze colour match, four failed hangers identified. Materials ordered, work scheduled for May 29.', - photos: [ - { id: 'sct_006_a2_p1', url: '/assets/images/properties/Broken_Roof_Surface.jpg', name: 'gutter-inspection.jpg' }, - ], - actorId: 'sub_002', actorName: 'Maya Patel', actorRole: 'Subcontractor', - at: '2026-05-26T10:00:00Z', - }, - ], - projectId: 'PRJ-2026-009', - paymentStatus: 'Unpaid', - paymentRequestedAt: null, - amountPaid: 0, - createdAt: '2026-05-17T14:00:00Z', - updatedAt: '2026-05-26T11:00:00Z', - }, - // ------------------------------------------------------------------- // sct_007 — Maya (sub_002) — completed earlier this month // ------------------------------------------------------------------- @@ -8011,60 +7878,6 @@ const MOCK_SUBCONTRACTOR_TASKS = [ updatedAt: '2026-05-27T11:05:00Z', }, - // ------------------------------------------------------------------- - // sct_009 — Jennifer Castillo (sub_004) — Cancelled plumbing - // ------------------------------------------------------------------- - { - id: 'sct_009', - companyId: 'lynkeduppro', - companyName: 'LynkedUp Pro Roofing', - subcontractorId: 'sub_004', - subcontractorName: 'Jennifer Castillo', - assignedBy: 'own_001', - assignedByName: 'Justin Johnson', - title: 'Rough-in plumbing for ADU bathroom', - location: '6112 Roundrock Dr, Plano, TX 75025', - description: 'New ADU build — bathroom rough-in. Coordinate with framing inspection.', - dueDate: '2026-06-15', - priority: 'medium', - category: 'Plumbing', - assignedDate: '2026-05-09T10:00:00Z', - status: 'Cancelled', - photos: [], - statusHistory: [ - { id: 'sct_009_h1', status: 'Assigned', actorId: 'own_001', actorName: 'Justin Johnson', comment: 'Task created and assigned.', at: '2026-05-09T10:00:00Z' }, - { id: 'sct_009_h2', status: 'Cancelled', actorId: 'own_001', actorName: 'Justin Johnson', comment: 'Homeowner cancelled the ADU project — entire scope suspended. Task voided, no further work required.', at: '2026-05-20T15:00:00Z' }, - ], - thread: [ - { id: 'sct_009_t1', channel: 'group', senderId: 'own_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Jennifer — unfortunately the homeowner has decided to cancel the ADU build entirely. No further work needed on this task. Apologies for the inconvenience.', at: '2026-05-20T15:02:00Z' }, - { id: 'sct_009_t2', channel: 'group', senderId: 'sub_004', senderName: 'Jennifer Castillo', senderRole: 'Subcontractor', body: 'Understood — no work had been started so no impact on our end. Thanks for the heads-up.', at: '2026-05-20T15:30:00Z', mine: true }, - ], - expenses: [], - fees: [], - activities: [ - { - id: 'sct_009_a1', type: 'status_change', status: 'Assigned', - note: 'Task created. New ADU build — bathroom rough-in to coordinate with framing inspection.', - photos: [], - actorId: 'own_001', actorName: 'Justin Johnson', actorRole: 'Owner', - at: '2026-05-09T10:00:00Z', - }, - { - id: 'sct_009_a2', type: 'status_change', status: 'Cancelled', - note: 'Task cancelled — homeowner suspended the ADU project. No work was started.', - photos: [], - actorId: 'own_001', actorName: 'Justin Johnson', actorRole: 'Owner', - at: '2026-05-20T15:00:00Z', - }, - ], - projectId: null, - paymentStatus: 'Unpaid', - paymentRequestedAt: null, - amountPaid: 0, - createdAt: '2026-05-09T10:00:00Z', - updatedAt: '2026-05-20T15:30:00Z', - }, - // ------------------------------------------------------------------- // sct_011 — Maya Patel (sub_002) — full roof replacement PRJ-2026-001, PAID // ------------------------------------------------------------------- @@ -8238,51 +8051,6 @@ const MOCK_SUBCONTRACTOR_TASKS = [ updatedAt: '2026-04-28T16:00:00Z', }, - // ------------------------------------------------------------------- - // sct_015 — Jennifer Castillo (sub_004) — PRJ-2026-007, Partially Paid - // ------------------------------------------------------------------- - { - id: 'sct_015', - companyId: 'lynkeduppro', - companyName: 'LynkedUp Pro Roofing', - subcontractorId: 'sub_004', - subcontractorName: 'Jennifer Castillo', - assignedBy: 'a1', - assignedByName: 'Wade Hollis', - title: 'Hot water heater replacement — PRJ-007', - location: '6909 Custer Rd, Plano, TX 75023', - description: 'Replace 40-gal gas water heater (unit failed). Install Bradford White 40-gal NG, reconnect supply & drain.', - dueDate: '2026-05-20', - priority: 'high', - category: 'Plumbing', - assignedDate: '2026-05-14T08:00:00Z', - status: 'Completed', - photos: [], - statusHistory: [ - { id: 'sct_015_h1', status: 'Assigned', actorId: 'a1', actorName: 'Wade Hollis', comment: 'Assigned.', at: '2026-05-14T08:00:00Z' }, - { id: 'sct_015_h2', status: 'Completed', actorId: 'sub_004', actorName: 'Jennifer Castillo', comment: 'Complete.', at: '2026-05-16T13:00:00Z' }, - ], - thread: [], - expenses: [ - { id: 'sct_015_e1', description: 'Bradford White 40-gal NG heater', category: 'Materials', amount: 680, notes: '', receiptUrl: '', createdAt: '2026-05-14T10:00:00Z' }, - { id: 'sct_015_e2', description: 'Gas flex connector + fittings', category: 'Materials', amount: 48, notes: '', receiptUrl: '', createdAt: '2026-05-14T10:05:00Z' }, - { id: 'sct_015_e3', description: 'Expansion tank + pressure relief', category: 'Materials', amount: 95, notes: '', receiptUrl: '', createdAt: '2026-05-14T10:10:00Z' }, - { id: 'sct_015_e4', description: 'Haul-away — old unit', category: 'Disposal / Dumpster', amount: 65, notes: '', receiptUrl: '', createdAt: '2026-05-16T14:00:00Z' }, - ], - fees: [ - { id: 'sct_015_f1', description: 'Labour — WHR install', type: 'Labour / Service Fee', amount: 420, createdAt: '2026-05-16T13:10:00Z' }, - { id: 'sct_015_f2', description: 'Mobilization', type: 'Mobilization Fee', amount: 75, createdAt: '2026-05-14T08:00:00Z' }, - ], - activities: [], - projectId: 'PRJ-2026-007', - paymentStatus: 'Partially Paid', - paymentRequestedAt: '2026-05-16T14:00:00Z', - amountPaid: 700, - paidAt: '2026-05-22T10:00:00Z', - createdAt: '2026-05-14T08:00:00Z', - updatedAt: '2026-05-16T14:00:00Z', - }, - // ------------------------------------------------------------------- // sct_016 — Dwayne Boudreaux (sub_003) — interior repaint PRJ-2026-004, Paid // ------------------------------------------------------------------- @@ -8325,47 +8093,6 @@ const MOCK_SUBCONTRACTOR_TASKS = [ updatedAt: '2026-05-03T16:00:00Z', }, - // ------------------------------------------------------------------- - // sct_017 — Dwayne Boudreaux (sub_003) — PRJ-2026-007 ceiling repaint, Unpaid - // ------------------------------------------------------------------- - { - id: 'sct_017', - companyId: 'lynkeduppro', - companyName: 'LynkedUp Pro Roofing', - subcontractorId: 'sub_003', - subcontractorName: 'Dwayne Boudreaux', - assignedBy: 'a1', - assignedByName: 'Wade Hollis', - title: 'Ceiling repaint after water damage — PRJ-007', - location: '6909 Custer Rd, Plano, TX 75023', - description: 'Repaint ceilings in master bedroom and en-suite after remediation. Stain block primer + 2 coats flat white.', - dueDate: '2026-05-28', - priority: 'medium', - category: 'Painting', - assignedDate: '2026-05-18T10:00:00Z', - status: 'Completed', - photos: [], - statusHistory: [ - { id: 'sct_017_h1', status: 'Assigned', actorId: 'a1', actorName: 'Wade Hollis', comment: 'Assigned.', at: '2026-05-18T10:00:00Z' }, - { id: 'sct_017_h2', status: 'Completed', actorId: 'sub_003', actorName: 'Dwayne Boudreaux', comment: 'Complete.', at: '2026-05-25T14:00:00Z' }, - ], - thread: [], - expenses: [ - { id: 'sct_017_e1', description: 'Zinsser stain-block primer — 1 gal', category: 'Materials', amount: 52, notes: '', receiptUrl: '', createdAt: '2026-05-19T08:00:00Z' }, - { id: 'sct_017_e2', description: 'Ceiling flat white — 1 gal', category: 'Materials', amount: 42, notes: '', receiptUrl: '', createdAt: '2026-05-19T08:05:00Z' }, - ], - fees: [ - { id: 'sct_017_f1', description: 'Labour — ceiling repaint (1.5 days)', type: 'Labour / Service Fee', amount: 580, createdAt: '2026-05-25T14:10:00Z' }, - ], - activities: [], - projectId: 'PRJ-2026-007', - paymentStatus: 'Unpaid', - paymentRequestedAt: '2026-05-25T15:00:00Z', - amountPaid: 0, - createdAt: '2026-05-18T10:00:00Z', - updatedAt: '2026-05-25T15:00:00Z', - }, - // ------------------------------------------------------------------- // sct_018 — Greg Alston (sub_005) — window replacement PRJ-2026-003, Partially Paid // ------------------------------------------------------------------- @@ -8409,46 +8136,6 @@ const MOCK_SUBCONTRACTOR_TASKS = [ createdAt: '2026-05-03T09:00:00Z', updatedAt: '2026-05-13T16:00:00Z', }, - - // ------------------------------------------------------------------- - // sct_019 — Greg Alston (sub_005) — window sealing PRJ-2026-009, Unpaid - // ------------------------------------------------------------------- - { - id: 'sct_019', - companyId: 'lynkeduppro', - companyName: 'LynkedUp Pro Roofing', - subcontractorId: 'sub_005', - subcontractorName: 'Greg Alston', - assignedBy: 'a1', - assignedByName: 'Wade Hollis', - title: 'Re-seal & caulk all windows — PRJ-009', - location: '7224 Independence Pkwy, Plano, TX 75025', - description: 'Full perimeter re-seal on 8 windows. Remove old silicone, clean frames, apply new NP1 polyurethane sealant.', - dueDate: '2026-06-05', - priority: 'low', - category: 'Windows & Glazing', - assignedDate: '2026-05-22T09:00:00Z', - status: 'In Progress', - photos: [], - statusHistory: [ - { id: 'sct_019_h1', status: 'Assigned', actorId: 'a1', actorName: 'Wade Hollis', comment: 'Assigned.', at: '2026-05-22T09:00:00Z' }, - { id: 'sct_019_h2', status: 'In Progress', actorId: 'sub_005', actorName: 'Greg Alston', comment: 'In progress.', at: '2026-05-28T08:30:00Z' }, - ], - thread: [], - expenses: [ - { id: 'sct_019_e1', description: 'NP1 polyurethane sealant (6 tubes)', category: 'Materials', amount: 78, notes: '', receiptUrl: '', createdAt: '2026-05-23T07:00:00Z' }, - ], - fees: [ - { id: 'sct_019_f1', description: 'Labour — window sealing (1 day)', type: 'Labour / Service Fee', amount: 380, createdAt: '2026-05-28T12:00:00Z' }, - ], - activities: [], - projectId: 'PRJ-2026-009', - paymentStatus: 'Unpaid', - paymentRequestedAt: '2026-05-28T12:00:00Z', - amountPaid: 0, - createdAt: '2026-05-22T09:00:00Z', - updatedAt: '2026-05-28T12:00:00Z', - }, ]; const MOCK_NOTIFICATIONS = [ @@ -8470,20 +8157,6 @@ const MOCK_NOTIFICATIONS = [ taskId: 'sct_004', fromCompanyId: 'lynkeduppro', fromCompanyName: 'LynkedUp Pro Roofing', isRead: false, createdAt: '2026-05-18T22:40:00Z', }, - { - id: 'notif_003', recipientUserId: 'sub_002', recipientRole: 'SUBCONTRACTOR', - type: 'task_assigned', - message: 'You have been assigned a task by LynkedUp Pro Roofing — "Replace gutter on south elevation"', - taskId: 'sct_006', fromCompanyId: 'lynkeduppro', fromCompanyName: 'LynkedUp Pro Roofing', - isRead: false, createdAt: '2026-05-17T14:00:00Z', - }, - { - id: 'notif_004', recipientUserId: 'sub_002', recipientRole: 'SUBCONTRACTOR', - type: 'task_assigned', - message: 'You have been assigned a task by LynkedUp Pro Roofing — "Inspect attic ventilation"', - taskId: 'sct_005', fromCompanyId: 'lynkeduppro', fromCompanyName: 'LynkedUp Pro Roofing', - isRead: true, createdAt: '2026-05-12T10:00:00Z', - }, // -- Message Received -------------------------- { id: 'notif_005', recipientUserId: 'sub_002', recipientRole: 'SUBCONTRACTOR', @@ -8499,13 +8172,6 @@ const MOCK_NOTIFICATIONS = [ taskId: 'sct_004', fromCompanyId: 'lynkeduppro', fromCompanyName: 'LynkedUp Pro Roofing', isRead: false, createdAt: '2026-05-18T22:42:00Z', }, - { - id: 'notif_007', recipientUserId: 'sub_002', recipientRole: 'SUBCONTRACTOR', - type: 'message_received', - message: 'Wade Hollis added an internal note on sct_005 — "I will reach out to the homeowner…"', - taskId: 'sct_005', fromCompanyId: 'lynkeduppro', fromCompanyName: 'LynkedUp Pro Roofing', - isRead: true, createdAt: '2026-05-15T16:20:00Z', - }, // -- Task Updated ------------------------------ { id: 'notif_008', recipientUserId: 'sub_002', recipientRole: 'SUBCONTRACTOR', @@ -8536,13 +8202,6 @@ const MOCK_NOTIFICATIONS = [ taskId: 'sct_001', fromCompanyId: 'lynkeduppro', fromCompanyName: 'LynkedUp Pro Roofing', isRead: true, createdAt: '2026-05-16T08:30:00Z', }, - { - id: 'notif_012', recipientUserId: 'sub_002', recipientRole: 'SUBCONTRACTOR', - type: 'status_changed', - message: 'Status for sct_005 changed to On Hold by Maya Patel.', - taskId: 'sct_005', fromCompanyId: 'lynkeduppro', fromCompanyName: 'LynkedUp Pro Roofing', - isRead: true, createdAt: '2026-05-15T16:00:00Z', - }, { id: 'notif_013', recipientUserId: 'sub_002', recipientRole: 'SUBCONTRACTOR', type: 'status_changed', diff --git a/src/pages/owner/OwnerProjectDetail.jsx b/src/pages/owner/OwnerProjectDetail.jsx index bc7446a..20c1cf5 100644 --- a/src/pages/owner/OwnerProjectDetail.jsx +++ b/src/pages/owner/OwnerProjectDetail.jsx @@ -1,5 +1,5 @@ import React, { useState, useMemo, useRef, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; import { useMockStore } from '../../data/mockStore'; import { LIFECYCLE_STAGES, REWORK_STAGE, nextStage, prevStage, stageToPct } from '../../data/lifecycle'; @@ -199,7 +199,15 @@ const OwnerProjectDetail = () => { } = useMockStore(); const navigate = useNavigate(); const { can } = usePermissions(); - const [activeTab, setActiveTab] = useState('overview'); + const [searchParams, setSearchParams] = useSearchParams(); + const [activeTab, setActiveTab] = useState(() => searchParams.get('tab') || 'overview'); + + useEffect(() => { + const tab = searchParams.get('tab'); + if (tab && tab !== activeTab) { + setActiveTab(tab); + } + }, [searchParams, activeTab]); const [selectedCO, setSelectedCO] = useState(null); const [selectedInvoice, setSelectedInvoice] = useState(null); const [selectedVersion, setSelectedVersion] = useState(null); @@ -554,7 +562,10 @@ const OwnerProjectDetail = () => { return ( +
+ + {/* Form */} +
+
+ + {/* Team Member Name */} +
+ + setForm(f => ({ ...f, name: e.target.value }))} + /> +
+ + {/* Role / Category */} +
+ + setForm(f => ({ ...f, role: e.target.value }))} + /> +
+ + {/* Cost Type */} +
+ + setForm(f => ({ ...f, customCostType: e.target.value }))} + autoFocus + /> + + )} + +
+ + {/* Amount */} +
+ +
+ $ + setForm(f => ({ ...f, amount: e.target.value }))} + /> +
+
+ + {/* Live preview */} + {(form.name || resolvedCostType || form.amount) && ( +
+
+ +
+
+

+ {form.name || '—'} · {resolvedCostType || '—'} +

+

+ {form.amount ? `$${parseFloat(form.amount).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '—'} +

+
+
+ )} +
+ + {/* Footer */} +
+ + +
+
+ +
+ )} + , + document.body + ); +}; + +export default AddTeamCostModal; diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 288efa1..fb7027f 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -10379,22 +10379,21 @@ export const MOCK_STORM_ATTRIBUTION_LEADS = [ { id: 'SAL-035', firstName: 'Dylan', lastName: 'Nelson', address: '12080 Coit Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Canvassers', status: 'New', contractValue: null, createdAt: '2025-09-22T10:45:00Z', stormSource: { id: 'SE-007', areaName: 'Frisco / N Plano', date: '2025-09-18', maxHailSize: 1.1, severity: 'moderate' } }, ]; -export const generateMockPaymentRequests = (rowId, actual, paid) => { - if (actual <= 0) return []; - +export const generateMockPaymentRequests = (rowId, actual, paid, allocated = 0) => { // Extract a numeric seed from the rowId (e.g. 'exp_base_0' -> 0, 'exp_team_1' -> 1) const seedNum = parseInt(rowId.replace(/[^0-9]/g, '')) || 0; - const seed = seedNum % 2; + const seed = seedNum % 3; - if (paid) { + // ── Case: actual > 0 and fully paid ────────────────────────────────────── + if (actual > 0 && paid) { if (actual <= 1000) { return [ { id: `req_${rowId}_1`, - requestDate: '2026-05-02', + requestDate: '2026-04-28', requestedAmount: actual, amountPaid: actual, - paymentDate: '2026-05-05', + paymentDate: '2026-05-02', } ]; } else { @@ -10403,81 +10402,114 @@ export const generateMockPaymentRequests = (rowId, actual, paid) => { return [ { id: `req_${rowId}_1`, - requestDate: '2026-04-20', + requestDate: '2026-04-15', requestedAmount: part1, amountPaid: part1, - paymentDate: '2026-04-22', + paymentDate: '2026-04-18', }, { id: `req_${rowId}_2`, - requestDate: '2026-05-05', + requestDate: '2026-05-01', requestedAmount: part2, amountPaid: part2, - paymentDate: '2026-05-07', + paymentDate: '2026-05-04', } ]; } - } else { + } + + // ── Case: actual > 0, not fully paid ───────────────────────────────────── + if (actual > 0 && !paid) { if (actual > 2000) { const part1 = Math.round(actual * 0.5); const part2 = actual - part1; - - if (seed === 1) { - // Return one paid request and one partially paid request + + if (seed === 0) { + // One paid + one partially paid const part2Paid = Math.round(part2 * 0.4); return [ { id: `req_${rowId}_1`, - requestDate: '2026-05-10', + requestDate: '2026-05-05', requestedAmount: part1, amountPaid: part1, - paymentDate: '2026-05-12', + paymentDate: '2026-05-08', }, { id: `req_${rowId}_2`, - requestDate: '2026-05-20', + requestDate: '2026-05-18', requestedAmount: part2, amountPaid: part2Paid, paymentDate: '2026-05-22', } ]; - } else { - // Return one paid request and one unpaid request + } else if (seed === 1) { + // One paid + one fully unpaid return [ { id: `req_${rowId}_1`, - requestDate: '2026-05-10', + requestDate: '2026-05-08', requestedAmount: part1, amountPaid: part1, - paymentDate: '2026-05-12', + paymentDate: '2026-05-10', }, { id: `req_${rowId}_2`, - requestDate: '2026-05-20', + requestDate: '2026-05-22', requestedAmount: part2, amountPaid: 0, paymentDate: null, } ]; + } else { + // Three splits: paid, partial, unpaid + const p1 = Math.round(actual * 0.4); + const p2 = Math.round(actual * 0.35); + const p3 = actual - p1 - p2; + const p2Paid = Math.round(p2 * 0.6); + return [ + { + id: `req_${rowId}_1`, + requestDate: '2026-04-20', + requestedAmount: p1, + amountPaid: p1, + paymentDate: '2026-04-23', + }, + { + id: `req_${rowId}_2`, + requestDate: '2026-05-05', + requestedAmount: p2, + amountPaid: p2Paid, + paymentDate: '2026-05-09', + }, + { + id: `req_${rowId}_3`, + requestDate: '2026-05-20', + requestedAmount: p3, + amountPaid: 0, + paymentDate: null, + } + ]; } } else { if (seed === 1) { - // Return a single partially paid request + // Single partially paid request + const partPaid = Math.round(actual * 0.5); return [ { id: `req_${rowId}_1`, - requestDate: '2026-05-15', + requestDate: '2026-05-12', requestedAmount: actual, - amountPaid: Math.round(actual * 0.5), - paymentDate: '2026-05-18', + amountPaid: partPaid, + paymentDate: '2026-05-15', } ]; } else { - // Return a single unpaid request + // Single unpaid request return [ { id: `req_${rowId}_1`, - requestDate: '2026-05-15', + requestDate: '2026-05-14', requestedAmount: actual, amountPaid: 0, paymentDate: null, @@ -10486,4 +10518,55 @@ export const generateMockPaymentRequests = (rowId, actual, paid) => { } } } + + // ── Case: actual === 0 but allocated > 0 — pending / not-started ───────── + // Show a pending payment request for the allocated amount (requested but not yet started) + if (actual <= 0 && allocated > 0) { + if (seed === 0) { + // Fully pending (submitted, no payment yet) + return [ + { + id: `req_${rowId}_1`, + requestDate: '2026-05-25', + requestedAmount: allocated, + amountPaid: 0, + paymentDate: null, + } + ]; + } else if (seed === 1) { + // Two-part pending — initial deposit requested, balance pending + const deposit = Math.round(allocated * 0.3); + const balance = allocated - deposit; + return [ + { + id: `req_${rowId}_1`, + requestDate: '2026-05-20', + requestedAmount: deposit, + amountPaid: 0, + paymentDate: null, + }, + { + id: `req_${rowId}_2`, + requestDate: '2026-05-28', + requestedAmount: balance, + amountPaid: 0, + paymentDate: null, + } + ]; + } else { + // Single pending with partial deposit paid upfront + const deposit = Math.round(allocated * 0.25); + return [ + { + id: `req_${rowId}_1`, + requestDate: '2026-05-22', + requestedAmount: allocated, + amountPaid: deposit, + paymentDate: '2026-05-24', + } + ]; + } + } + + return []; }; diff --git a/src/pages/owner/OwnerProjectDetail.jsx b/src/pages/owner/OwnerProjectDetail.jsx index 467e697..fe165bc 100644 --- a/src/pages/owner/OwnerProjectDetail.jsx +++ b/src/pages/owner/OwnerProjectDetail.jsx @@ -20,6 +20,7 @@ import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal'; import EstimateDetailModal from '../../components/estimates/EstimateDetailModal'; import CreateItemModal from '../../components/owner/CreateItemModal'; import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal'; +import AddTeamCostModal from '../../components/owner/AddTeamCostModal'; import TeamManagementPanel from '../../components/owner/TeamManagementPanel'; import { usePermissions } from '../../hooks/usePermissions'; import { resolveCommission, findAssignedUserRoleKey } from '../../utils/commissionResolver'; @@ -133,7 +134,7 @@ const PAYMENTS_PER_PAGE = 5; const EXPENSE_TYPE_CONFIG = { material: { label: 'Material', icon: Wrench, color: 'text-sky-600 dark:text-[#00f0ff]', bg: 'bg-sky-50 dark:bg-blue-500/10', border: 'border-sky-200 dark:border-blue-500/20' }, labor: { label: 'Labor', icon: HardHat, color: 'text-emerald-600 dark:text-emerald-400', bg: 'bg-emerald-50 dark:bg-emerald-500/10', border: 'border-emerald-200 dark:border-emerald-500/20' }, - team: { label: 'Team Commission', icon: UserCheck, color: 'text-purple-600 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/20' }, + team: { label: 'Team Cost', icon: UserCheck, color: 'text-purple-600 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/20' }, subcontractor: { label: 'Subcontractor', icon: HardHat, color: 'text-amber-600 dark:text-[#fda913]', bg: 'bg-amber-50 dark:bg-[#fda913]/10', border: 'border-amber-200 dark:border-[#fda913]/20' }, other: { label: 'Other', icon: DollarSign, color: 'text-zinc-600 dark:text-zinc-400', bg: 'bg-zinc-100 dark:bg-white/5', border: 'border-zinc-200 dark:border-white/10' }, }; @@ -160,7 +161,7 @@ function seedExpenseRowsMock(project) { const paid = Number(b.actual) > 0 && Number(b.actual) >= Number(b.allocated) * 0.9; return { id, type, name, category, allocated, actual, paid, - paymentRequests: generateMockPaymentRequests(id, actual, paid) + paymentRequests: generateMockPaymentRequests(id, actual, paid, allocated) }; }); const seed = (project?.id || '').charCodeAt((project?.id || 'x').length - 1) % 3; @@ -170,7 +171,7 @@ function seedExpenseRowsMock(project) { { id: 'exp_team_3', type: 'team', name: 'Sarah Calloway', category: 'Estimator', allocated: 1200, actual: 0, paid: false }, ].slice(0, 2 + seed).map(row => ({ ...row, - paymentRequests: generateMockPaymentRequests(row.id, row.actual, row.paid) + paymentRequests: generateMockPaymentRequests(row.id, row.actual, row.paid, row.allocated) })); const subRows = [ { id: 'exp_sub_1', type: 'subcontractor', name: 'PlumbPro Services', category: 'Plumbing', allocated: 1800, actual: 1800, paid: true }, @@ -179,7 +180,7 @@ function seedExpenseRowsMock(project) { { id: 'exp_sub_4', type: 'subcontractor', name: 'Solid Civil Works', category: 'Civil work', allocated: 3200, actual: 0, paid: false }, ].slice(0, 3 + seed).map(row => ({ ...row, - paymentRequests: generateMockPaymentRequests(row.id, row.actual, row.paid) + paymentRequests: generateMockPaymentRequests(row.id, row.actual, row.paid, row.allocated) })); return [...baseRows, ...teamRows, ...subRows]; } @@ -232,20 +233,15 @@ const OwnerProjectDetail = () => { const [expenseRows, setExpenseRows] = useState([]); const [expensesSeeded, setExpensesSeeded] = useState(false); - // ── Row Expansion state for Cost Breakdown ───────────────────────────────── - const [expandedRowIds, setExpandedRowIds] = useState(new Set()); + // ── Row Expansion state for Cost Breakdown (accordion — one open at a time) ── + const [expandedRowId, setExpandedRowId] = useState(null); const toggleRowExpanded = (rowId) => { - setExpandedRowIds(prev => { - const next = new Set(prev); - if (next.has(rowId)) { - next.delete(rowId); - } else { - next.add(rowId); - } - return next; - }); + setExpandedRowId(prev => (prev === rowId ? null : rowId)); }; + // ── Add Team Cost modal ─────────────────────────────────────────────────── + const [addTeamCostOpen, setAddTeamCostOpen] = useState(false); + // Helper functions for payment requests & status calculations const getRequestRemaining = (req) => (Number(req.requestedAmount) || 0) - (Number(req.amountPaid) || 0); const getRequestStatus = (req) => { @@ -256,6 +252,10 @@ const OwnerProjectDetail = () => { return 'Unpaid'; }; + const getRowTotalRequested = (row) => { + const reqs = row.paymentRequests || []; + return reqs.reduce((sum, r) => sum + (Number(r.requestedAmount) || 0), 0); + }; const getRowTotalPaid = (row) => { const reqs = row.paymentRequests || []; return reqs.reduce((sum, r) => sum + (Number(r.amountPaid) || 0), 0); @@ -263,14 +263,20 @@ const OwnerProjectDetail = () => { const getRowRemaining = (row) => { const actual = Number(row.actual) || 0; const totalPaid = getRowTotalPaid(row); - return Math.max(0, actual - totalPaid); + if (actual > 0) { + return Math.max(0, actual - totalPaid); + } + // For zero-actual rows, use total requested - total paid from payment requests + const totalRequested = getRowTotalRequested(row); + return Math.max(0, totalRequested - totalPaid); }; const getRowStatus = (row) => { const actual = Number(row.actual) || 0; const totalPaid = getRowTotalPaid(row); const remaining = getRowRemaining(row); - - if (actual === 0) { + const totalRequested = getRowTotalRequested(row); + + if (actual === 0 && totalRequested === 0) { return 'Unpaid'; } if (remaining <= 0) { @@ -279,6 +285,10 @@ const OwnerProjectDetail = () => { if (totalPaid > 0) { return 'Partially Paid'; } + // Has payment requests but nothing paid + if (totalRequested > 0) { + return 'Unpaid'; + } return 'Unpaid'; }; @@ -1092,16 +1102,7 @@ const OwnerProjectDetail = () => {