Project Progression: added popup when project move from contract signed to work in progress

This commit is contained in:
Mayur Shinde
2026-06-05 20:37:01 +05:30
parent e40a1a828d
commit 2e7075a5de
3 changed files with 159 additions and 15 deletions
+4 -1
View File
@@ -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 */}
<form onSubmit={handleSubmit} className="flex-1 flex flex-col min-h-0">
<div className="flex-1 overflow-y-auto px-5 sm:px-6 py-5 space-y-5">
{description && (
<p className="text-sm text-zinc-600 dark:text-zinc-300 leading-relaxed">{description}</p>
)}
{fields.map((field, i) => (
<div key={i} className="space-y-1.5">
<label htmlFor={field.name} className="text-xs font-bold uppercase tracking-wider text-zinc-600 dark:text-zinc-400">{field.label}</label>
+96
View File
@@ -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',
+59 -14
View File
@@ -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 = () => {
<div className="flex flex-wrap items-center gap-2 mb-5">
{!isAtFinalStage && (
<button
onClick={() => setCreateModalConfig({
title: `Advance to "${nextStageName}"`,
icon: ChevronRight,
iconColor: 'text-sky-500 dark:text-[#00f0ff]',
iconBg: 'bg-sky-500/10',
submitLabel: `Advance Stage`,
submitColor: 'bg-sky-500/20 text-sky-600 dark:text-[#00f0ff] border-sky-500/30',
fields: [
{ name: 'note', label: 'Note (optional)', type: 'textarea', required: false, placeholder: 'Add a note about this stage transition…' },
],
onSubmit: (d) => advanceLifecycleStage(project.id, nextStageName, d.note || '', user?.name || user?.id || 'Owner'),
})}
onClick={() => {
// Gate: a project can't move to "Work In Progress" without any team members assigned
const hasTeamMembers = (project.teamMembers || []).length > 0;
if (nextStageName === 'Work In Progress' && !hasTeamMembers) {
setCreateModalConfig({
title: 'Add Team Members First',
icon: Users,
iconColor: 'text-amber-500 dark:text-amber-400',
iconBg: 'bg-amber-500/10',
description: 'This project has no team members assigned yet. Add at least one team member in the Team tab before moving the project to "Work In Progress".',
submitLabel: 'Go to Team Tab',
submitColor: 'bg-purple-500/20 text-purple-600 dark:text-purple-400 border-purple-500/30',
fields: [],
onSubmit: () => setActiveTab('team'),
});
return;
}
setCreateModalConfig({
title: `Advance to "${nextStageName}"`,
icon: ChevronRight,
iconColor: 'text-sky-500 dark:text-[#00f0ff]',
iconBg: 'bg-sky-500/10',
submitLabel: `Advance Stage`,
submitColor: 'bg-sky-500/20 text-sky-600 dark:text-[#00f0ff] border-sky-500/30',
fields: [
{ name: 'note', label: 'Note (optional)', type: 'textarea', required: false, placeholder: 'Add a note about this stage transition…' },
],
onSubmit: (d) => advanceLifecycleStage(project.id, nextStageName, d.note || '', user?.name || user?.id || 'Owner'),
});
}}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-sky-500/15 hover:bg-sky-500/25 text-sky-600 dark:text-[#00f0ff] border border-sky-500/30 dark:border-[#00f0ff]/30 text-xs font-bold transition shadow-sm"
>
<ChevronRight size={14} />