import React, { useState, useMemo, useEffect } from 'react'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, router, usePage } from '@inertiajs/react'; import { Badge } from '@/Components/ui/badge'; import { Check, AlertTriangle, Sparkles } from 'lucide-react'; import { MaterialCatalogModal, MaterialOption } from '@modules/MaterialLogistics/resources/js/Pages/Requisitions/MaterialCatalogModal'; import LaborLookupModal from '../../Components/LaborLookupModal'; import EquipmentLookupModal from '../../Components/EquipmentLookupModal'; import { ProjectWizardProps, Labor, Equipment } from '../../types/project-wizard'; import Step1Details from './Wizard/Step1Details'; import Step2Tasks from './Wizard/Step2Tasks'; import Step3Materials from './Wizard/Step3Materials'; import Step4Manpower from './Wizard/Step4Manpower'; import Step5Equipment from './Wizard/Step5Equipment'; import Step6Estimation from './Wizard/Step6Estimation'; import Step7Submit from './Wizard/Step7Submit'; export default function Wizard({ project, step: currentStep, employees, projects = [], classifications = [], materials = [], materialGroups = [], labors = [], equipments = [], teams = [] }: ProjectWizardProps) { const { errors } = usePage().props; const [step, setStep] = useState(currentStep); const [catalogModalOpen, setCatalogModalOpen] = useState(false); const [laborModalOpen, setLaborModalOpen] = useState(false); const [activeLaborRowIdx, setActiveLaborRowIdx] = useState(null); const [equipmentModalOpen, setEquipmentModalOpen] = useState(false); const [activeEquipmentRowIdx, setActiveEquipmentRowIdx] = useState(null); // Step 1 Local State (Project Details) const [step1Details, setStep1Details] = useState(() => ({ name: project.name || '', project_type: project.project_type || 'standard', classifications: project.classifications || [], is_unprofitable: project.is_unprofitable || false, location: project.location || '', client_name: project.client_name || '', start_date: (project as any).start_date || '', target_end_date: (project as any).target_end_date || '', contract_duration: (project as any).contract_duration || '', description: (project as any).description || '', pm_id: (project as any).pm_id ? String((project as any).pm_id) : '', contract_value: project.contract_value || '', parent_project_id: project.parent_project_id ? String(project.parent_project_id) : '', })); // Step 2 Local States (Tasks & Milestones) const [localMilestones, setLocalMilestones] = useState(() => { return project.milestones.length > 0 ? project.milestones.map(m => ({ ulid: m.ulid, name: m.name, weight_percentage: String(m.weight_percentage) })) : []; }); const [localTasks, setLocalTasks] = useState(() => { return project.tasks.length > 0 ? project.tasks.map(t => ({ ulid: t.ulid, name: t.name, description: t.description || '', milestone_ulid: t.milestone?.ulid || '', start_date: t.start_date || '', end_date: t.end_date || '' })) : []; }); const [prepopulateMilestones, setPrepopulateMilestones] = useState(false); // Step 3 Local States (Material Estimates) const [localEstimates, setLocalEstimates] = useState(() => { return project.materials_estimates.length > 0 ? project.materials_estimates.map(e => ({ material_ulid: e.material.ulid, material_name: e.material.name, unit: e.material.unit, estimated_qty: String(e.estimated_qty), unit_cost: String(e.unit_cost) })) : []; }); // Step 4 Local States (Manpower/Labor allocations) const [localLabor, setLocalLabor] = useState(() => { const list: any[] = []; project.tasks.forEach(t => { if (t.task_labors && t.task_labors.length > 0) { const bundleGroups: { [key: string]: any[] } = {}; t.task_labors.forEach((tl: any) => { if (tl.allocation_type === 'bundle' && tl.bundle_name) { const bKey = `${t.ulid}_${tl.bundle_name}`; if (!bundleGroups[bKey]) { bundleGroups[bKey] = []; } bundleGroups[bKey].push(tl); } else { list.push({ type: 'trade', task_ulid: t.ulid, task_name: t.name, labor_ulid: tl.labor?.ulid || '', name: tl.labor?.name || 'Labor Trade', category: tl.labor?.category || 'skilled', unit: 'hrs', unit_rate: Number(tl.labor?.hourly_rate || 0), quantity: String(tl.estimated_hours), estimated_hours: String(tl.estimated_hours) }); } }); Object.values(bundleGroups).forEach(group => { const first = group[0]; list.push({ type: 'bundle', task_ulid: t.ulid, task_name: t.name, name: first.bundle_name, unit: first.bundle_unit || 'm²', quantity: String(first.bundle_quantity || 10), unit_rate: Number(first.bundle_unit_rate || 0), bundle_items: group.map((tl: any) => ({ labor_name: tl.labor?.name || 'Trade', category: tl.labor?.category || 'skilled', rate_per_sqm: Number(tl.bundle_unit_rate || 0) / group.length, estimated_hourly_rate: Number(tl.labor?.hourly_rate || 0) })) }); }); } }); return list; }); // Step 5 Local States (Equipment allocations) const [localEquipment, setLocalEquipment] = useState(() => { const list: any[] = []; project.tasks.forEach(t => { if (t.task_equipments && t.task_equipments.length > 0) { t.task_equipments.forEach((te: any) => { list.push({ task_ulid: t.ulid, task_name: t.name, equipment_ulid: te.equipment?.ulid || '', estimated_hours: String(te.estimated_hours) }); }); } }); return list; }); // Step 7 Local States (Approval Submission) const [submissionNotes, setSubmissionNotes] = useState(''); // Step 4 Team Roster / Personnel Pool Local States const [selectedUserUlids, setSelectedUserUlids] = useState(() => { return project.personnel ? project.personnel.map((p: any) => p.ulid) : []; }); const [selectedTeamUlids, setSelectedTeamUlids] = useState(() => { const activePersonnelUlids = project.personnel ? project.personnel.map((p: any) => p.ulid) : []; return teams.filter(t => t.users && t.users.length > 0 && t.users.every((u: any) => activePersonnelUlids.includes(u.ulid))).map(t => t.ulid); }); // Prepopulate default milestones locally when toggle checked useEffect(() => { if (prepopulateMilestones && localMilestones.length === 0) { setLocalMilestones([ { name: 'Mobilization', weight_percentage: '5' }, { name: 'Earthworks & Foundation', weight_percentage: '15' }, { name: 'Structural Works', weight_percentage: '25' }, { name: 'Roofing & Waterproofing', weight_percentage: '15' }, { name: 'Architectural Finishing', weight_percentage: '20' }, { name: 'MEP Rough-In', weight_percentage: '10' }, { name: 'Final Inspection & Punch List', weight_percentage: '5' }, { name: 'Turnover', weight_percentage: '5' } ]); } }, [prepopulateMilestones]); // Financial Summaries const formatCurrency = (v: string | number) => { return new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v)); }; const calculatedLaborCost = useMemo(() => { return localLabor.reduce((sum, item) => { if (item.type === 'bundle') { const rate = Number(item.unit_rate || 0); const area = Number(item.quantity || 0); return sum + (area * rate); } else { const rateObj = labors.find(r => r.ulid === item.labor_ulid); const rate = rateObj ? Number(rateObj.hourly_rate) : Number(item.unit_rate || 0); const hours = Number(item.quantity || item.estimated_hours || 0); return sum + (hours * rate); } }, 0); }, [localLabor, labors]); const calculatedEquipmentCost = useMemo(() => { return localEquipment.reduce((sum, item) => { const rateObj = equipments.find(r => r.ulid === item.equipment_ulid); const rate = rateObj ? Number(rateObj.hourly_rate) : 0; return sum + (Number(item.estimated_hours) * rate); }, 0); }, [localEquipment, equipments]); const calculatedMaterialsCost = useMemo(() => { return localEstimates.reduce((sum, item) => { return sum + (Number(item.estimated_qty) * Number(item.unit_cost)); }, 0); }, [localEstimates]); const totalEstimatedCost = calculatedMaterialsCost + calculatedLaborCost + calculatedEquipmentCost; const contractValueNum = Number(project.contract_value); const grossMarginVal = contractValueNum - totalEstimatedCost; const grossMarginPct = contractValueNum > 0 ? (grossMarginVal / contractValueNum) * 100 : 0; const isUnprofitableEst = grossMarginVal < 0 || project.is_unprofitable; // Stepper Titles const stepsList = [ { id: 1, label: 'Details' }, { id: 2, label: 'Tasks' }, { id: 3, label: 'Materials' }, { id: 4, label: 'Manpower' }, { id: 5, label: 'Equipment' }, { id: 6, label: 'Estimation' }, { id: 7, label: 'Submit' } ]; // Navigation and Saves const handleBack = () => { if (step > 1) { router.get(route('projects.wizard', [project.ulid, { step: step - 1 }])); } }; const handleSaveTasks = () => { router.post(route('projects.wizard.tasks', project.ulid), { prepopulate_milestones: prepopulateMilestones, milestones: localMilestones, tasks: localTasks }, { onSuccess: () => setStep(3) }); }; const handleSaveEstimates = () => { router.post(route('projects.wizard.estimates', project.ulid), { estimates: localEstimates }, { onSuccess: () => setStep(4) }); }; const handleSaveLabor = () => { const flattenedLabor: any[] = []; localLabor.forEach(item => { const targetTaskUlid = (item.task_ulid && item.task_ulid !== 'general') ? item.task_ulid : (project.tasks[0]?.ulid || ''); if (!targetTaskUlid) return; if (item.type === 'bundle') { const area = Number(item.quantity || 0); const bundleItems = item.bundle_items || []; const bundleRate = Number(item.unit_rate || 0); bundleItems.forEach((bItem: any) => { const matched = labors.find(l => l.name.toLowerCase().includes(bItem.labor_name.toLowerCase()) || bItem.labor_name.toLowerCase().includes(l.name.toLowerCase()) ) || (labors.find(l => l.category === bItem.category) || labors[0]); if (matched) { const itemRatePerSqm = Number(bItem.rate_per_sqm || bItem.estimated_hourly_rate || 50); const itemCost = itemRatePerSqm * area; const hourlyRate = Number(matched.hourly_rate) || 100; const hours = itemCost / hourlyRate; flattenedLabor.push({ task_ulid: targetTaskUlid, labor_ulid: matched.ulid, allocation_type: 'bundle', bundle_name: item.name, bundle_unit: 'm²', bundle_quantity: String(area), bundle_unit_rate: String(bundleRate), estimated_hours: (Math.round(hours * 100) / 100).toFixed(2) }); } }); } else { if (item.labor_ulid) { const rateObj = labors.find(r => r.ulid === item.labor_ulid); flattenedLabor.push({ task_ulid: targetTaskUlid, labor_ulid: item.labor_ulid, allocation_type: 'trade', bundle_name: null, bundle_unit: 'hrs', bundle_quantity: String(item.quantity || item.estimated_hours || 0), bundle_unit_rate: String(rateObj ? rateObj.hourly_rate : (item.unit_rate || 0)), estimated_hours: String(item.quantity || item.estimated_hours || 0) }); } } }); router.post(route('projects.wizard.labor', project.ulid), { labor: flattenedLabor, team_ulids: selectedTeamUlids, user_ulids: selectedUserUlids, }, { onSuccess: () => setStep(5) }); }; const handleSaveEquipment = () => { router.post(route('projects.wizard.equipment', project.ulid), { equipment: localEquipment }, { onSuccess: () => setStep(6) }); }; const handleSubmitApproval = (e: React.FormEvent) => { e.preventDefault(); router.post(route('projects.wizard.submit', project.ulid), { notes: submissionNotes }); }; const handleCatalogSelect = (selectedMaterials: MaterialOption[]) => { const mapped = selectedMaterials.map(m => ({ material_ulid: m.ulid, material_name: m.name, unit: m.unit, estimated_qty: '1', unit_cost: String(m.unit_cost || 0) })); const filtered = mapped.filter(m => !localEstimates.some(e => e.material_ulid === m.material_ulid)); setLocalEstimates([...localEstimates, ...filtered]); setCatalogModalOpen(false); }; const handleCatalogAddKit = (group: any, quantityMultiplier: number) => { if (!group.materials) return; const mapped = group.materials.map((m: any) => ({ material_ulid: m.ulid, material_name: m.name, unit: m.unit, estimated_qty: String((m.pivot?.quantity || 1) * quantityMultiplier), unit_cost: String(m.unit_cost || 0) })); const updatedEstimates = [...localEstimates]; mapped.forEach((newItem: any) => { const existingIdx = updatedEstimates.findIndex(e => e.material_ulid === newItem.material_ulid); if (existingIdx > -1) { const existingQty = Number(updatedEstimates[existingIdx].estimated_qty) || 0; updatedEstimates[existingIdx].estimated_qty = String(existingQty + Number(newItem.estimated_qty)); } else { updatedEstimates.push(newItem); } }); setLocalEstimates(updatedEstimates); setCatalogModalOpen(false); }; const handleSelectLabor = (labor: Labor) => { if (activeLaborRowIdx !== null) { setLocalLabor(prev => prev.map((item, i) => i === activeLaborRowIdx ? { ...item, type: 'trade', labor_ulid: labor.ulid, name: labor.name, category: labor.category, unit: 'hrs', unit_rate: Number(labor.hourly_rate), quantity: item.quantity || item.estimated_hours || '40', estimated_hours: item.quantity || item.estimated_hours || '40' } : item)); } else { setLocalLabor(prev => [ ...prev, { type: 'trade', task_ulid: 'general', labor_ulid: labor.ulid, name: labor.name, category: labor.category, unit: 'hrs', unit_rate: Number(labor.hourly_rate), quantity: '40', estimated_hours: '40' } ]); } setLaborModalOpen(false); setActiveLaborRowIdx(null); }; const handleAddLaborBundle = (bundle: any, crewMultiplier: number) => { const ratePerSqm = bundle.items.reduce((sum: number, item: any) => sum + (item.rate_per_sqm || item.estimated_hourly_rate || 0), 0); const sqmArea = crewMultiplier || 10; const newBundleData = { type: 'bundle', bundle_id: bundle.id, name: bundle.name, job_type: bundle.job_type, category: bundle.category, unit: 'm²', unit_rate: ratePerSqm, quantity: String(sqmArea), bundle_items: bundle.items }; if (activeLaborRowIdx !== null) { setLocalLabor(prev => prev.map((item, i) => i === activeLaborRowIdx ? { ...item, ...newBundleData, task_ulid: item.task_ulid || 'general' } : item)); } else { setLocalLabor(prev => [ ...prev, { ...newBundleData, task_ulid: 'general' } ]); } setLaborModalOpen(false); setActiveLaborRowIdx(null); }; const handleSelectEquipment = (equipment: Equipment) => { if (activeEquipmentRowIdx !== null) { updateEquipmentAllocation(activeEquipmentRowIdx, 'equipment_ulid', equipment.ulid); } setEquipmentModalOpen(false); setActiveEquipmentRowIdx(null); }; // Task & Milestone Helpers const addMilestone = () => { setLocalMilestones([...localMilestones, { name: '', weight_percentage: '0' }]); }; const removeMilestone = (idx: number) => { setLocalMilestones(localMilestones.filter((_, i) => i !== idx)); }; const updateMilestone = (idx: number, field: string, val: string) => { setLocalMilestones(localMilestones.map((m, i) => i === idx ? { ...m, [field]: val } : m)); }; const addTask = () => { setLocalTasks([...localTasks, { name: '', description: '', milestone_ulid: '', start_date: '', end_date: '' }]); }; const removeTask = (idx: number) => { setLocalTasks(localTasks.filter((_, i) => i !== idx)); }; const updateTask = (idx: number, field: string, val: string) => { setLocalTasks(localTasks.map((t, i) => i === idx ? { ...t, [field]: val } : t)); }; // Allocations Helpers const addLaborAllocation = () => { setLocalLabor([...localLabor, { type: 'trade', task_ulid: 'general', labor_ulid: '', name: '', unit: 'hrs', unit_rate: 0, quantity: '40', estimated_hours: '40' }]); }; const removeLaborAllocation = (idx: number) => { setLocalLabor(localLabor.filter((_, i) => i !== idx)); }; const updateLaborAllocation = (idx: number, field: string, val: string) => { setLocalLabor(localLabor.map((item, i) => { if (i !== idx) return item; const updated = { ...item, [field]: val }; if (field === 'quantity') { updated.estimated_hours = val; } return updated; })); }; const addEquipmentAllocation = () => { setLocalEquipment([...localEquipment, { task_ulid: '', equipment_ulid: '', estimated_hours: '0' }]); }; const removeEquipmentAllocation = (idx: number) => { setLocalEquipment(localEquipment.filter((_, i) => i !== idx)); }; const updateEquipmentAllocation = (idx: number, field: string, val: string) => { setLocalEquipment(localEquipment.map((item, i) => i === idx ? { ...item, [field]: val } : item)); }; return (

Project Wizard: {project.name}

{project.code} · Guided Project Estimation Setup

{project.status.replace(/_/g, ' ')} } >
{/* Stepper Header */}
{stepsList.map((s) => { const isActive = step === s.id; const isCompleted = step > s.id || project.current_wizard_step > s.id; return (
{isCompleted ? : s.id}
{s.label}
); })}
{/* Step Content Container */}
{Object.keys(errors || {}).length > 0 && (

Please correct the following errors before proceeding:

    {Object.entries(errors).map(([key, val]) => (
  • {String(val)}
  • ))}
)} {step === 1 && ( setStep(2)} /> )} {step === 2 && ( )} {step === 3 && ( setCatalogModalOpen(true)} handleBack={handleBack} handleSaveEstimates={handleSaveEstimates} /> )} {step === 4 && ( { setActiveLaborRowIdx(idx !== undefined ? idx : null); setLaborModalOpen(true); }} handleBack={handleBack} handleSaveLabor={handleSaveLabor} /> )} {step === 5 && ( { setActiveEquipmentRowIdx(idx); setEquipmentModalOpen(true); }} handleBack={handleBack} handleSaveEquipment={handleSaveEquipment} /> )} {step === 6 && ( setStep(7)} /> )} {step === 7 && ( )}
{/* Modals */} ); }