+ )}
{fields.map((field, i) => (
diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx
index fb7027f..57c5c69 100644
--- a/src/data/mockStore.jsx
+++ b/src/data/mockStore.jsx
@@ -2158,6 +2158,43 @@ const MOCK_PROJECTS = [
{ name: 'City of Plano', category: 'Permits & Inspection', allocated: 2500, committed: 2000, actual: 2000 },
{ name: 'Junk King DFW', category: 'Cleanup & Haul-Away', allocated: 4000, committed: 1500, actual: 0 }
],
+ // Cost breakdown / expenses for the table (requested amounts never exceed actual)
+ expenses: [
+ { id: 'exp_001_1', type: 'material', name: 'ABC Supply Co.', category: 'Materials - Shingles', allocated: 15000, actual: 7000, paid: false, paymentRequests: [
+ { id: 'req_001_1_1', requestDate: '2026-01-26', requestedAmount: 5000, amountPaid: 5000, paymentDate: '2026-01-28' },
+ { id: 'req_001_1_2', requestDate: '2026-02-10', requestedAmount: 2000, amountPaid: 1000, paymentDate: '2026-02-12' }
+ ] },
+ { id: 'exp_001_2', type: 'material', name: 'ABC Supply Co.', category: 'Materials - Underlayment', allocated: 3500, actual: 2500, paid: true, paymentRequests: [
+ { id: 'req_001_2_1', requestDate: '2026-01-27', requestedAmount: 2500, amountPaid: 2500, paymentDate: '2026-01-30' }
+ ] },
+ { id: 'exp_001_3', type: 'labor', name: 'Texas Roofing Crew A', category: 'Labor - Tear Off', allocated: 8000, actual: 5000, paid: true, paymentRequests: [
+ { id: 'req_001_3_1', requestDate: '2026-01-29', requestedAmount: 3000, amountPaid: 3000, paymentDate: '2026-02-01' },
+ { id: 'req_001_3_2', requestDate: '2026-02-03', requestedAmount: 2000, amountPaid: 2000, paymentDate: '2026-02-05' }
+ ] },
+ { id: 'exp_001_4', type: 'labor', name: 'Texas Roofing Crew A', category: 'Labor - Install', allocated: 12000, actual: 0, paid: false, paymentRequests: [] },
+ { id: 'exp_001_5', type: 'other', name: 'City of Plano', category: 'Permits & Inspection', allocated: 2500, actual: 2000, paid: true, paymentRequests: [
+ { id: 'req_001_5_1', requestDate: '2026-01-18', requestedAmount: 2000, amountPaid: 2000, paymentDate: '2026-01-20' }
+ ] },
+ { id: 'exp_001_6', type: 'other', name: 'Junk King DFW', category: 'Cleanup & Haul-Away', allocated: 4000, actual: 0, paid: false, paymentRequests: [] },
+ { id: 'exp_001_7', type: 'team', name: 'Jesus Gonzales', category: 'Sales Rep', costType: 'Commission', allocated: 4200, actual: 4200, paid: true, paymentRequests: [
+ { id: 'req_001_7_1', requestDate: '2026-02-06', requestedAmount: 4200, amountPaid: 4200, paymentDate: '2026-02-08' }
+ ] },
+ { id: 'exp_001_8', type: 'team', name: 'Cody Tatum', category: 'Canvasser', costType: 'Hourly Wage', allocated: 1500, actual: 1500, paid: false, paymentRequests: [
+ { id: 'req_001_8_1', requestDate: '2026-02-09', requestedAmount: 1500, amountPaid: 750, paymentDate: '2026-02-11' }
+ ] },
+ { id: 'exp_001_9', type: 'subcontractor', name: 'Carlos Mendoza', category: 'Electrical', allocated: 1800, actual: 1800, paid: true, paymentRequests: [
+ { id: 'req_001_9_1', requestDate: '2026-02-04', requestedAmount: 1800, amountPaid: 1800, paymentDate: '2026-02-06' }
+ ] },
+ { id: 'exp_001_10', type: 'subcontractor', name: 'Maya Patel', category: 'Roofing', allocated: 2400, actual: 2400, paid: false, paymentRequests: [
+ { id: 'req_001_10_1', requestDate: '2026-02-12', requestedAmount: 1200, amountPaid: 1200, paymentDate: '2026-02-14' },
+ { id: 'req_001_10_2', requestDate: '2026-02-20', requestedAmount: 1200, amountPaid: 0, paymentDate: null }
+ ] }
+ ],
+ paymentsReceived: [
+ { id: 'rcv_001_1', from: 'State Farm Insurance', method: 'ACH', amount: 18500, date: '2026-01-28', refNumber: 'ACH-2026-4411', memo: 'Initial claim payout — roof replacement' },
+ { id: 'rcv_001_2', from: 'Justin Johnson', method: 'Check', amount: 2500, date: '2026-02-05', refNumber: 'CHK-8832', memo: 'Homeowner deductible payment' },
+ { id: 'rcv_001_3', from: 'State Farm Insurance', method: 'ACH', amount: 6200, date: '2026-02-18', refNumber: 'ACH-2026-4499', memo: 'Supplement approval — additional scope' }
+ ],
changeOrders: [],
rfis: [],
riskLog: [
@@ -2214,6 +2251,65 @@ const MOCK_PROJECTS = [
],
},
+ // TEST PROJECT: Contract Signed with NO team members — used to verify the
+ // "Add Team Members First" guard when advancing to "Work In Progress".
+ {
+ id: 'PRJ-2026-TEST',
+ ownerId: 'own_001',
+ propertyId: 'P-2602',
+ address: 'TEST — 100 Demo Street, Plano, TX 75023',
+ contractorId: 'con_001',
+ subcontractorIds: [],
+ projectType: 'Roof Replacement',
+ status: 'active',
+ phase: 'Pre-Construction',
+ budget: 32000,
+ approvedBudget: 32000,
+ committedCost: 12000,
+ actualCost: 0,
+ spent: 0,
+ variancePercent: 0,
+ margin: 0.30,
+ commissionType: 'percent_gross',
+ commissionRate: 10,
+ commission: 3200,
+ startDate: '2026-06-01',
+ endDate: '2026-07-15',
+ completionPercentage: 40,
+ healthScore: 78,
+ openRFIs: 0,
+ changeOrderCount: 0,
+ pendingInvoiceCount: 0,
+ budgetBreakdown: [],
+ // Cost breakdown / expenses for the table (Contract Signed — allocated only, nothing spent yet)
+ expenses: [],
+ // No funds received yet at Contract Signed
+ paymentsReceived: [],
+ changeOrders: [],
+ rfis: [],
+ riskLog: [],
+ issueLog: [],
+ paymentSchedule: [
+ { id: 'ps_test_1', milestone: 'Mobilization (20%)', amount: 6400, dueDate: '2026-06-05', status: 'pending' }
+ ],
+ activityTimeline: [
+ { date: '2026-06-01', action: 'Contract Signed', user: 'Justin Johnson', details: 'Contract executed; awaiting team assignment.' }
+ ],
+ milestones: [],
+ invoices: [],
+ documents: [
+ { name: 'Scope of Work - Roof Replacement', url: '#', type: 'contract' }
+ ],
+ teamMembers: [],
+ lifecycleStage: 'Contract Signed',
+ stageHistory: [
+ { stage: 'Lead', date: '2026-05-15', by: 'Justin Johnson', note: 'Storm damage lead identified — TEST project.' },
+ { stage: 'Damage Verified', date: '2026-05-20', by: 'Justin Johnson', note: 'Adjuster confirmed hail damage; claim approved.' },
+ { stage: 'Scope Approved', date: '2026-05-25', by: 'Justin Johnson', note: 'Full roof replacement scope signed off.' },
+ { stage: 'Contract Signed', date: '2026-06-01', by: 'Justin Johnson', note: 'Contract executed; mobilization deposit pending.' },
+ ],
+ },
+
// Project 2: Completed, zero variance (perfectly on budget)
{
id: 'PRJ-2026-002',
diff --git a/src/pages/owner/OwnerProjectDetail.jsx b/src/pages/owner/OwnerProjectDetail.jsx
index b82ffa9..4276cb5 100644
--- a/src/pages/owner/OwnerProjectDetail.jsx
+++ b/src/pages/owner/OwnerProjectDetail.jsx
@@ -115,7 +115,12 @@ function seedDocsMock(projectId) {
return all.slice(0, 4 + seed);
}
-function seedPaymentsMock(projectId) {
+function seedPaymentsMock(project) {
+ // Prefer per-project payments-received data when defined on the project (even if empty)
+ if (Array.isArray(project?.paymentsReceived)) return project.paymentsReceived;
+
+ // Fallback (legacy) for any project not yet migrated — generic pool sliced by id
+ const projectId = project?.id || '';
const all = [
{ id: 'pay_001', from: 'State Farm Insurance', method: 'ACH', amount: 18500, date: '2026-01-28', refNumber: 'ACH-2026-4411', memo: 'Initial claim payout — roof replacement' },
{ id: 'pay_002', from: 'Justin Johnson', method: 'Check', amount: 2500, date: '2026-02-05', refNumber: 'CHK-8832', memo: 'Homeowner deductible payment' },
@@ -124,6 +129,7 @@ function seedPaymentsMock(projectId) {
{ id: 'pay_005', from: 'State Farm Insurance', method: 'Check', amount: 4100, date: '2026-03-12', refNumber: 'CHK-7761', memo: 'Final supplement payout' },
{ id: 'pay_006', from: 'Justin Johnson', method: 'Cash', amount: 500, date: '2026-03-20', refNumber: '', memo: 'Gutter guard add-on' },
];
+ if (!projectId) return all.slice(0, 3);
const seed = projectId.charCodeAt(projectId.length - 1) % 3;
return all.slice(0, 3 + seed);
}
@@ -162,6 +168,25 @@ const inferExpenseType = (category = '') => {
};
function seedExpenseRowsMock(project) {
+ // Prefer per-project expenses data when defined on the project (even if empty)
+ if (Array.isArray(project?.expenses)) {
+ return project.expenses.map((e, i) => {
+ const allocated = Number(e.allocated) || 0;
+ const actual = Number(e.actual) || 0;
+ return {
+ id: e.id || `exp_${i}`,
+ type: e.type || inferExpenseType(e.category),
+ name: e.name || '—',
+ category: e.category || '',
+ costType: e.costType || '',
+ allocated,
+ actual,
+ paid: e.paid ?? (actual > 0 && actual >= allocated * 0.9),
+ paymentRequests: e.paymentRequests || [],
+ };
+ });
+ }
+
const baseRows = (project?.budgetBreakdown || []).map((b, i) => {
const id = `exp_base_${i}`;
const type = inferExpenseType(b.category);
@@ -236,7 +261,8 @@ const OwnerProjectDetail = () => {
const [createModalConfig, setCreateModalConfig] = useState(null);
// ── Payments state ─────────────────────────────────────────────────────────
- const [payments, setPayments] = useState(() => seedPaymentsMock(projectId));
+ // Seeded from the resolved project (see effect below) so each project shows its own payments
+ const [payments, setPayments] = useState([]);
const [paymentSort, setPaymentSort] = useState({ field: 'date', dir: 'desc' });
const [paymentPage, setPaymentPage] = useState(0);
@@ -416,6 +442,7 @@ const OwnerProjectDetail = () => {
useEffect(() => {
if (project && !expensesSeeded) {
setExpenseRows(seedExpenseRowsMock(project));
+ setPayments(seedPaymentsMock(project));
setExpensesSeeded(true);
}
}, [project, expensesSeeded]);
@@ -767,18 +794,36 @@ const OwnerProjectDetail = () => {
{!isAtFinalStage && (