From 2e7075a5deea1a74c072e5ef9ab5f38923248a42 Mon Sep 17 00:00:00 2001 From: Mayur Shinde Date: Fri, 5 Jun 2026 20:37:01 +0530 Subject: [PATCH] Project Progression: added popup when project move from contract signed to work in progress --- src/components/owner/CreateItemModal.jsx | 5 +- src/data/mockStore.jsx | 96 ++++++++++++++++++++++++ src/pages/owner/OwnerProjectDetail.jsx | 73 ++++++++++++++---- 3 files changed, 159 insertions(+), 15 deletions(-) diff --git a/src/components/owner/CreateItemModal.jsx b/src/components/owner/CreateItemModal.jsx index 9f39182..e2f47bc 100644 --- a/src/components/owner/CreateItemModal.jsx +++ b/src/components/owner/CreateItemModal.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { X } from 'lucide-react'; -const CreateItemModal = ({ isOpen, onClose, title, icon: Icon, iconColor = "text-[#00f0ff]", iconBg = "bg-blue-500/10", fields, onSubmit, submitLabel = "Submit", submitColor = "bg-blue-500/20 text-[#00f0ff] border-blue-500/30" }) => { +const CreateItemModal = ({ isOpen, onClose, title, icon: Icon, iconColor = "text-[#00f0ff]", iconBg = "bg-blue-500/10", description, fields = [], onSubmit, submitLabel = "Submit", submitColor = "bg-blue-500/20 text-[#00f0ff] border-blue-500/30" }) => { const [formData, setFormData] = useState({}); useEffect(() => { @@ -44,6 +44,9 @@ const CreateItemModal = ({ isOpen, onClose, title, icon: Icon, iconColor = "text {/* Content */}
+ {description && ( +

{description}

+ )} {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 && (