Files
GSB-Construction/Modules/ProjectManagement/resources/js/Pages/Projects/Wizard.tsx

1379 lines
93 KiB
TypeScript

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, router, useForm, usePage } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Badge } from '@/Components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/Components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
import { Textarea } from '@/Components/ui/textarea';
import {
ChevronLeft, ChevronRight, Plus, Trash2, ClipboardList, Wrench, Users,
TrendingUp, Check, CheckCircle2, AlertTriangle, ShieldCheck, ArrowRight, Sparkles,
FileSpreadsheet, FileDown
} from 'lucide-react';
import { useState, useMemo, useEffect } from 'react';
import { PageProps } from '@/types';
import { MaterialCatalogModal, MaterialOption } from '@modules/MaterialLogistics/resources/js/Pages/Requisitions/MaterialCatalogModal';
import LaborLookupModal from '../../Components/LaborLookupModal';
import EquipmentLookupModal from '../../Components/EquipmentLookupModal';
import { ProjectForm } from '../../Components/ProjectForm';
interface Skill {
id: number;
ulid: string;
name: string;
}
interface Labor {
id: number;
ulid: string;
name: string;
category: 'skilled' | 'unskilled';
hourly_rate: string;
skills: Skill[];
}
interface EquipmentSpecification {
id: number;
ulid: string;
name: string;
}
interface Equipment {
id: number;
ulid: string;
name: string;
owner_name: string | null;
hourly_rate: string;
specifications: EquipmentSpecification[];
}
interface ProjectData {
id: number;
ulid: string;
name: string;
code: string;
status: string;
client_name?: string;
location?: string;
project_type: string;
parent_project_id?: number;
is_unprofitable: boolean;
contract_value: string;
current_wizard_step: number;
milestones: any[];
tasks: any[];
materials_estimates: any[];
personnel?: any[];
}
interface Employee {
id: number;
ulid: string;
name: string;
email: string;
user_type?: string;
roles?: { id: number; name: string }[];
employee_profile?: {
position?: string;
department?: string;
};
}
interface Props extends PageProps {
project: ProjectData;
step: number;
employees: Employee[];
projects: any[];
materials: any[];
materialGroups: any[];
labors: Labor[];
equipments: Equipment[];
budget: any;
teams?: any[];
}
export default function Wizard({ project, step: currentStep, employees, projects = [], materials = [], materialGroups = [], labors = [], equipments = [], budget, teams = [] }: Props) {
const { errors } = usePage<any>().props;
const [step, setStep] = useState(currentStep);
const [catalogModalOpen, setCatalogModalOpen] = useState(false);
const [laborModalOpen, setLaborModalOpen] = useState(false);
const [activeLaborRowIdx, setActiveLaborRowIdx] = useState<number | null>(null);
const [equipmentModalOpen, setEquipmentModalOpen] = useState(false);
const [activeEquipmentRowIdx, setActiveEquipmentRowIdx] = useState<number | null>(null);
const handleSelectLabor = (labor: Labor) => {
if (activeLaborRowIdx !== null) {
updateLaborAllocation(activeLaborRowIdx, 'labor_ulid', labor.ulid);
}
setLaborModalOpen(false);
setActiveLaborRowIdx(null);
};
const handleSelectEquipment = (equipment: Equipment) => {
if (activeEquipmentRowIdx !== null) {
updateEquipmentAllocation(activeEquipmentRowIdx, 'equipment_ulid', equipment.ulid);
}
setEquipmentModalOpen(false);
setActiveEquipmentRowIdx(null);
};
// Step 1 Local State (Project Details)
const [step1Details, setStep1Details] = useState<any>(() => ({
name: project.name || '',
project_type: project.project_type || 'standard',
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<any[]>(() => {
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<any[]>(() => {
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<any[]>(() => {
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<any[]>(() => {
const list: any[] = [];
project.tasks.forEach(t => {
if (t.task_labors && t.task_labors.length > 0) {
t.task_labors.forEach((tl: any) => {
list.push({
task_ulid: t.ulid,
task_name: t.name,
labor_ulid: tl.labor?.ulid || '',
estimated_hours: String(tl.estimated_hours)
});
});
}
});
return list;
});
// Step 5 Local States (Equipment allocations)
const [localEquipment, setLocalEquipment] = useState<any[]>(() => {
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 [approverIds, setApproverIds] = useState<string[]>([]);
const [submissionNotes, setSubmissionNotes] = useState('');
// Step 4 Team Roster / Personnel Pool Local States
const [selectedUserUlids, setSelectedUserUlids] = useState<string[]>(() => {
return project.personnel ? project.personnel.map((p: any) => p.ulid) : [];
});
const [selectedTeamUlids, setSelectedTeamUlids] = useState<string[]>(() => {
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) => {
const rateObj = labors.find(r => r.ulid === item.labor_ulid);
const rate = rateObj ? Number(rateObj.hourly_rate) : 0;
return sum + (Number(item.estimated_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 = () => {
router.post(route('projects.wizard.labor', project.ulid), {
labor: localLabor,
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), {
approver_ids: approverIds,
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)
}));
// Avoid duplicate additions
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);
};
// 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, { task_ulid: '', labor_ulid: '', estimated_hours: '0' }]);
};
const removeLaborAllocation = (idx: number) => {
setLocalLabor(localLabor.filter((_, i) => i !== idx));
};
const updateLaborAllocation = (idx: number, field: string, val: string) => {
setLocalLabor(localLabor.map((item, i) => i === idx ? { ...item, [field]: val } : item));
};
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 (
<AuthenticatedLayout
header={
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold leading-tight text-slate-800 flex items-center gap-2">
<Sparkles className="h-5 w-5 text-emerald-500" /> Project Wizard: {project.name}
</h2>
<p className="text-xs text-slate-500 mt-0.5">{project.code} &middot; Guided Project Estimation Setup</p>
</div>
<Badge className={
project.status === 'planning' ? 'bg-amber-50 text-amber-700 border-amber-100' : 'bg-emerald-50 text-emerald-700 border-emerald-100'
}>
{project.status.replace(/_/g, ' ')}
</Badge>
</div>
}
>
<Head title={`Setup Wizard: ${project.name}`} />
<div className="py-6 bg-slate-50/50 min-h-[calc(100vh-65px)]">
<div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8 space-y-6">
{/* Stepper Header */}
<div className="bg-white px-6 py-4 rounded-xl border border-slate-200/80 shadow-xs">
<div className="flex justify-between items-center relative">
{/* Horizontal Line behind icons */}
<div className="absolute left-6 right-6 top-4 -translate-y-1/2 h-[2px] bg-slate-100 z-0 hidden md:block" />
{stepsList.map((s, idx) => {
const isActive = step === s.id;
const isCompleted = step > s.id || project.current_wizard_step > s.id;
return (
<div key={s.id} className="flex flex-col items-center z-10 relative">
<div className={`h-8 w-8 rounded-full flex items-center justify-center font-bold text-xs border transition-all duration-200 ${
isActive
? 'bg-emerald-600 border-emerald-600 text-white shadow-xs'
: isCompleted
? 'bg-emerald-50 border-emerald-300 text-emerald-700'
: 'bg-white border-slate-200 text-slate-400'
}`}>
{isCompleted ? <Check className="h-4 w-4 stroke-[2.5px]" /> : s.id}
</div>
<span className={`text-[11px] mt-1.5 font-medium transition-all ${
isActive ? 'text-emerald-700 font-bold' : isCompleted ? 'text-slate-700' : 'text-slate-400'
}`}>{s.label}</span>
</div>
);
})}
</div>
</div>
{/* Step Content Container */}
<div className="bg-white rounded-2xl border border-slate-200/80 shadow-xs overflow-hidden">
{Object.keys(errors || {}).length > 0 && (
<div className="p-4 bg-rose-50 border-b border-rose-100 text-rose-800 text-xs space-y-1">
<p className="font-bold text-rose-900 flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4 text-rose-600 animate-pulse" />
Please correct the following errors before proceeding:
</p>
<ul className="list-disc pl-5 mt-1 space-y-0.5 font-medium">
{Object.entries(errors).map(([key, val]) => (
<li key={key}>{String(val)}</li>
))}
</ul>
</div>
)}
{/* Step 1: Project Details (Editable Form) */}
{step === 1 && (
<div className="p-5 md:p-6 space-y-6">
<div className="pb-4 border-b border-slate-100">
<h3 className="text-base font-bold text-slate-800">Step 1: Project Information</h3>
<p className="text-xs text-slate-500 mt-0.5">Review and update core project attributes, dates, and budget limits.</p>
</div>
<ProjectForm
data={step1Details}
setData={(field: any, value: any) => {
setStep1Details((prev: any) => ({ ...prev, [field]: value }));
}}
errors={errors || {}}
employees={employees}
projects={projects}
/>
<div className="flex justify-between pt-5 border-t border-slate-100">
<div></div>
<Button onClick={() => {
router.post(route('projects.wizard.save-details', project.ulid), step1Details, {
onSuccess: () => setStep(2)
});
}} className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
Save & Next <ChevronRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
)}
{/* Step 2: Milestone & Task Creation */}
{step === 2 && (
<div className="p-6 space-y-6">
<CardHeader className="p-0 pb-4 border-b border-slate-100 flex flex-row items-center justify-between">
<div>
<CardTitle className="text-md font-semibold text-slate-800">Step 2: Project Tasks & Milestones</CardTitle>
<CardDescription className="text-xs">Schedule the project milestones and assign tasks under them.</CardDescription>
</div>
<div className="flex items-center gap-2 bg-slate-50 p-2 rounded-lg border border-slate-150">
<input
id="prepopulate"
type="checkbox"
checked={prepopulateMilestones}
onChange={e => setPrepopulateMilestones(e.target.checked)}
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500 h-4 w-4"
/>
<Label htmlFor="prepopulate" className="text-xs font-semibold text-slate-700 cursor-pointer">Pre-populate Defaults</Label>
</div>
</CardHeader>
{/* Milestones Editor */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500">1. Milestones</h4>
<Button variant="outline" size="sm" onClick={addMilestone} className="h-8 border-slate-200 text-slate-600 hover:bg-slate-50">
<Plus className="mr-1.5 h-3.5 w-3.5" /> Add Milestone
</Button>
</div>
{localMilestones.length === 0 ? (
<p className="text-xs italic text-slate-400 py-2">No milestones defined yet. Toggle pre-populate defaults or add custom ones.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{localMilestones.map((m, idx) => (
<div key={idx} className="flex gap-2 items-center bg-slate-50/50 p-3 rounded-lg border border-slate-200/60">
<div className="flex-1 space-y-1">
<Input
placeholder="Milestone Name"
value={m.name}
onChange={e => updateMilestone(idx, 'name', e.target.value)}
className="h-8 text-xs border-slate-200"
/>
</div>
<div className="w-20">
<Input
type="number"
placeholder="Weight %"
value={m.weight_percentage}
onChange={e => updateMilestone(idx, 'weight_percentage', e.target.value)}
className="h-8 text-xs border-slate-200 text-right"
/>
</div>
<Button variant="ghost" size="icon" onClick={() => removeMilestone(idx)} className="h-8 w-8 text-rose-500 hover:text-rose-700 hover:bg-rose-50">
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
{/* Tasks Editor */}
<div className="space-y-4 pt-4 border-t border-slate-100">
<div className="flex items-center justify-between">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500">2. Scheduled Tasks</h4>
<Button variant="outline" size="sm" onClick={addTask} className="h-8 border-slate-200 text-slate-600 hover:bg-slate-50">
<Plus className="mr-1.5 h-3.5 w-3.5" /> Add Task
</Button>
</div>
{localTasks.length === 0 ? (
<p className="text-xs italic text-slate-400 py-2">No tasks defined yet. Add tasks and link them to milestones.</p>
) : (
<div className="space-y-3">
{localTasks.map((t, idx) => (
<div key={idx} className="bg-slate-50/50 p-4 rounded-xl border border-slate-200/60 space-y-3 relative">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="md:col-span-2">
<Label className="text-[10px] text-slate-500">Task Name</Label>
<Input
placeholder="Task Name"
value={t.name}
onChange={e => updateTask(idx, 'name', e.target.value)}
className="h-8 text-xs border-slate-200 mt-1"
/>
</div>
<div className="flex items-end gap-2">
<div className="min-w-0 flex-1">
<Label className="text-[10px] text-slate-500">Milestone</Label>
<Select
value={t.milestone_ulid}
onValueChange={val => updateTask(idx, 'milestone_ulid', val || '')}
items={localMilestones.map((m, mIdx) => ({ value: m.ulid || String(mIdx), label: m.name || `Milestone ${mIdx+1}` }))}
>
<SelectTrigger className="h-8 text-xs border-slate-200 mt-1">
<SelectValue placeholder="Select Milestone" />
</SelectTrigger>
<SelectContent>
{localMilestones.map((m, mIdx) => (
<SelectItem key={mIdx} value={m.ulid || String(mIdx)}>
{m.name || `Milestone ${mIdx+1}`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
type="button"
variant="ghost"
size="icon"
aria-label={`Delete task ${idx + 1}`}
title="Delete task"
onClick={() => removeTask(idx)}
className="mb-0 h-8 w-8 shrink-0 rounded-lg p-2 text-rose-500 hover:bg-rose-50 hover:text-rose-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="md:col-span-2">
<Label className="text-[10px] text-slate-500">Description</Label>
<Input
placeholder="Brief task description"
value={t.description}
onChange={e => updateTask(idx, 'description', e.target.value)}
className="h-8 text-xs border-slate-200 mt-1"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[10px] text-slate-500">Start Date</Label>
<Input
type="date"
value={t.start_date}
onChange={e => updateTask(idx, 'start_date', e.target.value)}
className="h-8 text-xs border-slate-200 mt-1"
/>
</div>
<div>
<Label className="text-[10px] text-slate-500">Target End</Label>
<Input
type="date"
value={t.end_date}
onChange={e => updateTask(idx, 'end_date', e.target.value)}
className="h-8 text-xs border-slate-200 mt-1"
/>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="flex justify-between pt-6 border-t border-slate-100">
<Button variant="outline" onClick={handleBack}>
<ChevronLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button onClick={handleSaveTasks} className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
Save & Next <ChevronRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
)}
{/* Step 3: Material Requirements Estimation */}
{step === 3 && (
<div className="p-6 space-y-6">
<CardHeader className="p-0 pb-4 border-b border-slate-100 flex flex-row items-center justify-between">
<div>
<CardTitle className="text-md font-semibold text-slate-800">Step 3: Materials Estimation</CardTitle>
<CardDescription className="text-xs">Estimate the types and quantities of materials required for this project.</CardDescription>
</div>
<Button onClick={() => setCatalogModalOpen(true)} className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
<Plus className="mr-2 h-4 w-4" /> Add Materials
</Button>
</CardHeader>
{localEstimates.length === 0 ? (
<div className="py-12 text-center border border-dashed border-slate-200 rounded-xl bg-slate-50/50">
<ClipboardList className="mx-auto h-8 w-8 text-slate-400" />
<p className="text-xs italic text-slate-500 mt-2">No material estimates added yet. Use "Add Materials" to query standard materials.</p>
</div>
) : (
<div className="border border-slate-200/80 rounded-xl overflow-hidden shadow-sm">
<Table>
<TableHeader className="bg-slate-50/60">
<TableRow>
<TableHead className="font-semibold text-slate-700">Material Name</TableHead>
<TableHead className="font-semibold text-slate-700">Unit</TableHead>
<TableHead className="font-semibold text-slate-700 w-32">Estimated Qty</TableHead>
<TableHead className="font-semibold text-slate-700 w-36">Est. Unit Cost (PHP)</TableHead>
<TableHead className="font-semibold text-slate-700 text-right w-36">Total Cost</TableHead>
<TableHead className="text-right w-16"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{localEstimates.map((item, idx) => {
const total = Number(item.estimated_qty || 0) * Number(item.unit_cost || 0);
return (
<TableRow key={idx} className="hover:bg-slate-50/20 transition-colors">
<TableCell className="font-medium text-slate-900">{item.material_name}</TableCell>
<TableCell><Badge variant="outline" className="bg-slate-50 text-slate-650 font-normal">{item.unit}</Badge></TableCell>
<TableCell>
<Input
type="number"
value={item.estimated_qty}
onChange={e => {
const copy = [...localEstimates];
copy[idx].estimated_qty = e.target.value;
setLocalEstimates(copy);
}}
className="h-8 text-xs border-slate-200"
/>
</TableCell>
<TableCell>
<Input
type="number"
value={item.unit_cost}
onChange={e => {
const copy = [...localEstimates];
copy[idx].unit_cost = e.target.value;
setLocalEstimates(copy);
}}
className="h-8 text-xs border-slate-200"
/>
</TableCell>
<TableCell className="font-mono font-medium text-slate-700 text-right">{formatCurrency(total)}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon" onClick={() => setLocalEstimates(localEstimates.filter((_, i) => i !== idx))} className="h-8 w-8 text-rose-500 hover:text-rose-700 hover:bg-rose-50">
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
<TableRow className="bg-slate-50/40 hover:bg-slate-50/40">
<TableCell colSpan={4} className="font-semibold text-slate-800">Total Materials Cost Estimate</TableCell>
<TableCell className="font-mono font-bold text-slate-900 text-right">{formatCurrency(calculatedMaterialsCost)}</TableCell>
<TableCell></TableCell>
</TableRow>
</TableBody>
</Table>
</div>
)}
<div className="flex justify-between pt-6 border-t border-slate-100">
<Button variant="outline" onClick={handleBack}>
<ChevronLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button onClick={handleSaveEstimates} className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
Save & Next <ChevronRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
)}
{/* Step 4: Manpower Allocation */}
{step === 4 && (
<div className="p-6 space-y-6">
<CardHeader className="p-0 pb-4 border-b border-slate-100 flex flex-row items-center justify-between">
<div>
<CardTitle className="text-md font-semibold text-slate-800">Step 4: Manpower Rate Allocation</CardTitle>
<CardDescription className="text-xs">Allocate standard labor trades and estimate total work hours required for scheduled tasks.</CardDescription>
</div>
<Button onClick={addLaborAllocation} className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
<Plus className="mr-2 h-4 w-4" /> Assign Labor Record
</Button>
</CardHeader>
{/* Project Team Pool Selector */}
<div className="space-y-4 bg-slate-50/50 p-4 rounded-xl border border-slate-200/60">
<div className="flex flex-col gap-1">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center gap-1.5">
<Users className="h-4 w-4 text-emerald-600" /> 1. Project Team Pool
</h4>
<p className="text-[11px] text-slate-500">Select pre-defined master team rosters and individual members to allocate to this project's workforce pool.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Master Teams list */}
<div className="space-y-2">
<Label className="text-xs font-semibold text-slate-700">Select Master Team Roster (Crews)</Label>
<div className="bg-white border border-slate-250/70 rounded-lg p-3 max-h-48 overflow-y-auto space-y-2">
{teams.length === 0 ? (
<p className="text-xs text-slate-450 italic">No master team rosters available.</p>
) : (
teams.map(t => {
const isTeamChecked = selectedTeamUlids.includes(t.ulid);
return (
<div key={t.ulid} className="flex items-start gap-2.5 p-1 hover:bg-slate-55 rounded transition-colors">
<input
type="checkbox"
id={`team-${t.ulid}`}
checked={isTeamChecked}
onChange={() => {
let newTeams = [...selectedTeamUlids];
let newUsers = [...selectedUserUlids];
const teamUserUlids = t.users ? t.users.map((u: any) => u.ulid) : [];
if (isTeamChecked) {
// Remove team and its users
newTeams = newTeams.filter(id => id !== t.ulid);
// Only remove users if they aren't part of another selected team
const otherTeamsUsers = teams
.filter((otherTeam: any) => otherTeam.ulid !== t.ulid && newTeams.includes(otherTeam.ulid))
.flatMap((otherTeam: any) => otherTeam.users ? otherTeam.users.map((u: any) => u.ulid) : []);
newUsers = newUsers.filter(ulid => !teamUserUlids.includes(ulid) || otherTeamsUsers.includes(ulid));
} else {
// Add team and its users
newTeams.push(t.ulid);
teamUserUlids.forEach((ulid: string) => {
if (!newUsers.includes(ulid)) {
newUsers.push(ulid);
}
});
}
setSelectedTeamUlids(newTeams);
setSelectedUserUlids(newUsers);
}}
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500 h-4 w-4 mt-0.5 cursor-pointer"
/>
<Label htmlFor={`team-${t.ulid}`} className="text-xs text-slate-700 cursor-pointer flex-1 select-none">
<span className="font-semibold block">{t.name}</span>
<span className="text-[10px] text-slate-500 block">{t.description || 'No description'} &middot; {t.users?.length || 0} members</span>
</Label>
</div>
);
})
)}
</div>
</div>
{/* Individual Users List */}
<div className="space-y-2">
<Label className="text-xs font-semibold text-slate-700">Add/Remove Individual Employees</Label>
<div className="bg-white border border-slate-250/70 rounded-lg p-3 max-h-48 overflow-y-auto space-y-2">
{employees.map(emp => {
const isUserChecked = selectedUserUlids.includes(emp.ulid);
return (
<label key={emp.ulid} className="flex items-center gap-2.5 p-1 hover:bg-slate-50 rounded cursor-pointer select-none transition-colors">
<input
type="checkbox"
checked={isUserChecked}
onChange={() => {
const newUsers = isUserChecked
? selectedUserUlids.filter(ulid => ulid !== emp.ulid)
: [...selectedUserUlids, emp.ulid];
setSelectedUserUlids(newUsers);
// Update team selections if checking/unchecking users affects team completeness
const newTeams = teams.filter((t: any) => t.users && t.users.length > 0 && t.users.every((u: any) => newUsers.includes(u.ulid))).map(t => t.ulid);
setSelectedTeamUlids(newTeams);
}}
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500 h-4 w-4"
/>
<span className="text-xs text-slate-700">{emp.name} <span className="text-[10px] text-slate-450">({emp.email})</span></span>
</label>
);
})}
</div>
</div>
</div>
{/* Selected Personnel summary badges */}
<div className="pt-2 border-t border-slate-200/60">
<Label className="text-[10px] uppercase font-bold text-slate-400 block mb-1.5">Project Personnel Pool ({selectedUserUlids.length} selected)</Label>
<div className="flex flex-wrap gap-1">
{selectedUserUlids.length === 0 ? (
<span className="text-xs italic text-slate-400">No personnel selected yet.</span>
) : (
selectedUserUlids.map(ulid => {
const emp = employees.find(e => e.ulid === ulid);
if (!emp) return null;
return (
<Badge key={ulid} variant="secondary" className="bg-emerald-50 text-emerald-700 hover:bg-emerald-50 border-emerald-100 font-normal py-0.5 px-2 text-[11px]">
{emp.name}
</Badge>
);
})
)}
</div>
</div>
</div>
<div className="pt-4 border-t border-slate-200/60 flex flex-col gap-1">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center gap-1">
2. Labor Trade Allocations
</h4>
<p className="text-[11px] text-slate-500">Allocate standard labor trades and estimate total work hours required for scheduled tasks.</p>
</div>
{localLabor.length === 0 ? (
<div className="py-12 text-center border border-dashed border-slate-200 rounded-xl bg-slate-50/50">
<Users className="mx-auto h-8 w-8 text-slate-400" />
<p className="text-xs italic text-slate-500 mt-2">No manpower assigned yet. Click "Assign Labor Record" to allocate labor to tasks.</p>
</div>
) : (
<div className="border border-slate-200/80 rounded-xl overflow-hidden shadow-sm">
<Table>
<TableHeader className="bg-slate-50/60">
<TableRow>
<TableHead className="font-semibold text-slate-700">Target Task</TableHead>
<TableHead className="font-semibold text-slate-700">Labor Record / Category</TableHead>
<TableHead className="font-semibold text-slate-700 w-36">Hourly Rate</TableHead>
<TableHead className="font-semibold text-slate-700 w-32">Estimated Hours</TableHead>
<TableHead className="font-semibold text-slate-700 text-right w-36">Total Labor Cost</TableHead>
<TableHead className="text-right w-16"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{localLabor.map((item, idx) => {
const rateObj = labors.find(r => r.ulid === item.labor_ulid);
const rate = rateObj ? Number(rateObj.hourly_rate) : 0;
const total = Number(item.estimated_hours || 0) * rate;
return (
<TableRow key={idx} className="hover:bg-slate-50/20 transition-colors">
<TableCell className="w-56 max-w-[220px]">
<Select
value={item.task_ulid}
onValueChange={val => updateLaborAllocation(idx, 'task_ulid', val || '')}
items={[
{ value: 'general', label: 'General Project / Unassigned' },
...project.tasks.map(t => ({ value: t.ulid, label: t.name }))
]}
>
<SelectTrigger className="h-8 text-xs border-slate-200 w-full truncate">
<SelectValue placeholder="Select Task" className="truncate" />
</SelectTrigger>
<SelectContent className="max-w-xs">
<SelectItem value="general">General Project / Unassigned</SelectItem>
{project.tasks.map(t => (
<SelectItem key={t.ulid} value={t.ulid} className="truncate">{t.name}</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Button
type="button"
variant="outline"
size="sm"
className="h-8 w-full justify-start text-xs border-slate-200 font-normal hover:bg-slate-50 text-left"
onClick={() => {
setActiveLaborRowIdx(idx);
setLaborModalOpen(true);
}}
>
{rateObj ? (
<span className="truncate">
<span className="font-semibold text-slate-800">{rateObj.name}</span>
<span className="text-slate-500 ml-1">({rateObj.category})</span>
</span>
) : (
<span className="text-slate-450">Select Labor...</span>
)}
</Button>
</TableCell>
<TableCell className="font-mono text-xs text-slate-650">
{rateObj ? `${formatCurrency(rateObj.hourly_rate)} / hr` : '—'}
</TableCell>
<TableCell>
<Input
type="number"
value={item.estimated_hours}
onChange={e => updateLaborAllocation(idx, 'estimated_hours', e.target.value)}
className="h-8 text-xs border-slate-200"
/>
</TableCell>
<TableCell className="font-mono font-medium text-slate-700 text-right">{formatCurrency(total)}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon" onClick={() => removeLaborAllocation(idx)} className="h-8 w-8 text-rose-500 hover:text-rose-700 hover:bg-rose-50">
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
<TableRow className="bg-slate-50/40 hover:bg-slate-50/40">
<TableCell colSpan={4} className="font-semibold text-slate-800">Total Manpower Cost Estimate</TableCell>
<TableCell className="font-mono font-bold text-slate-900 text-right">{formatCurrency(calculatedLaborCost)}</TableCell>
<TableCell></TableCell>
</TableRow>
</TableBody>
</Table>
</div>
)}
<div className="flex justify-between pt-6 border-t border-slate-100">
<Button variant="outline" onClick={handleBack}>
<ChevronLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button onClick={handleSaveLabor} className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
Save & Next <ChevronRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
)}
{/* Step 5: Equipment Allocation */}
{step === 5 && (
<div className="p-6 space-y-6">
<CardHeader className="p-0 pb-4 border-b border-slate-100 flex flex-row items-center justify-between">
<div>
<CardTitle className="text-md font-semibold text-slate-800">Step 5: Equipment & Machinery Allocation</CardTitle>
<CardDescription className="text-xs">Allocate standard machinery/equipment and estimate total utilization hours required.</CardDescription>
</div>
<Button onClick={addEquipmentAllocation} className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
<Plus className="mr-2 h-4 w-4" /> Assign Equipment
</Button>
</CardHeader>
{localEquipment.length === 0 ? (
<div className="py-12 text-center border border-dashed border-slate-200 rounded-xl bg-slate-50/50">
<Wrench className="mx-auto h-8 w-8 text-slate-400" />
<p className="text-xs italic text-slate-500 mt-2">No equipment assigned yet. Click "Assign Equipment" to allocate machinery to tasks.</p>
</div>
) : (
<div className="border border-slate-200/80 rounded-xl overflow-hidden shadow-sm">
<Table>
<TableHeader className="bg-slate-50/60">
<TableRow>
<TableHead className="font-semibold text-slate-700">Target Task</TableHead>
<TableHead className="font-semibold text-slate-700">Equipment / Tool</TableHead>
<TableHead className="font-semibold text-slate-700 w-36">Hourly Rate</TableHead>
<TableHead className="font-semibold text-slate-700 w-32">Estimated Hours</TableHead>
<TableHead className="font-semibold text-slate-700 text-right w-36">Total Equip. Cost</TableHead>
<TableHead className="text-right w-16"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{localEquipment.map((item, idx) => {
const rateObj = equipments.find(r => r.ulid === item.equipment_ulid);
const rate = rateObj ? Number(rateObj.hourly_rate) : 0;
const total = Number(item.estimated_hours || 0) * rate;
return (
<TableRow key={idx} className="hover:bg-slate-50/20 transition-colors">
<TableCell>
<Select
value={item.task_ulid}
onValueChange={val => updateEquipmentAllocation(idx, 'task_ulid', val || '')}
items={[
{ value: 'general', label: 'General Project / Unassigned' },
...project.tasks.map(t => ({ value: t.ulid, label: t.name }))
]}
>
<SelectTrigger className="h-8 text-xs border-slate-200">
<SelectValue placeholder="Select Task" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">General Project / Unassigned</SelectItem>
{project.tasks.map(t => (
<SelectItem key={t.ulid} value={t.ulid}>{t.name}</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Button
type="button"
variant="outline"
size="sm"
className="h-8 w-full justify-start text-xs border-slate-200 font-normal hover:bg-slate-50 text-left"
onClick={() => {
setActiveEquipmentRowIdx(idx);
setEquipmentModalOpen(true);
}}
>
{rateObj ? (
<span className="truncate">
<span className="font-semibold text-slate-800">{rateObj.name}</span>
{rateObj.owner_name && (
<span className="text-slate-500 ml-1">({rateObj.owner_name})</span>
)}
</span>
) : (
<span className="text-slate-450">Select Equipment...</span>
)}
</Button>
</TableCell>
<TableCell className="font-mono text-xs text-slate-600">
{rateObj ? `${formatCurrency(rateObj.hourly_rate)} / hr` : '—'}
</TableCell>
<TableCell>
<Input
type="number"
value={item.estimated_hours}
onChange={e => updateEquipmentAllocation(idx, 'estimated_hours', e.target.value)}
className="h-8 text-xs border-slate-200"
/>
</TableCell>
<TableCell className="font-mono font-medium text-slate-700 text-right">{formatCurrency(total)}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon" onClick={() => removeEquipmentAllocation(idx)} className="h-8 w-8 text-rose-500 hover:text-rose-700 hover:bg-rose-50">
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
<TableRow className="bg-slate-50/40 hover:bg-slate-50/40">
<TableCell colSpan={4} className="font-semibold text-slate-800">Total Equipment Cost Estimate</TableCell>
<TableCell className="font-mono font-bold text-slate-900 text-right">{formatCurrency(calculatedEquipmentCost)}</TableCell>
<TableCell></TableCell>
</TableRow>
</TableBody>
</Table>
</div>
)}
<div className="flex justify-between pt-6 border-t border-slate-100">
<Button variant="outline" onClick={handleBack}>
<ChevronLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button onClick={handleSaveEquipment} className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
Save & Next <ChevronRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
)}
{/* Step 6: Show Financial Cost Estimation Summary */}
{step === 6 && (
<div className="p-6 space-y-6">
<CardHeader className="p-0 pb-4 border-b border-slate-100 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<CardTitle className="text-md font-semibold text-slate-800">Step 6: Financial Estimation Margin Summary</CardTitle>
<CardDescription className="text-xs">Review total projected costs, contract margins, and flag checks before locking for approval.</CardDescription>
</div>
<div className="flex items-center gap-2 shrink-0">
<a href={route('projects.wizard.export.excel', project.ulid)} target="_blank" rel="noopener noreferrer">
<Button type="button" variant="outline" size="sm" className="h-8 border-slate-200 text-slate-600 hover:bg-slate-50 flex items-center gap-1.5 font-semibold">
<FileSpreadsheet className="h-4 w-4 text-emerald-600" />
Download Excel
</Button>
</a>
<a href={route('projects.wizard.export.pdf', project.ulid)} target="_blank" rel="noopener noreferrer">
<Button type="button" variant="outline" size="sm" className="h-8 border-slate-200 text-slate-650 hover:bg-slate-50 flex items-center gap-1.5 font-semibold">
<FileDown className="h-4 w-4 text-rose-600" />
Download PDF
</Button>
</a>
</div>
</CardHeader>
{/* Gross Margin Health Banner */}
{isUnprofitableEst && (
<div className="p-4 rounded-xl border border-amber-200 bg-amber-50/70 text-amber-900 flex items-start gap-3 shadow-sm">
<AlertTriangle className="h-5 w-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<h4 className="font-semibold text-sm">Special Handle: Profit Margin Risk / Overrun Warn</h4>
<p className="text-xs text-amber-700 mt-1">
This project's estimated costs exceed its contract value, resulting in a negative projected margin.
The unprofitable warning will flag this project in procurement logs and approval cycles.
</p>
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-slate-50/40">
<CardContent className="p-4">
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-450">Contract Value</span>
<p className="text-lg font-bold text-slate-900 mt-1">{formatCurrency(project.contract_value)}</p>
</CardContent>
</Card>
<Card className="bg-slate-50/40">
<CardContent className="p-4">
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-450">Total Est. Cost</span>
<p className="text-lg font-bold text-slate-900 mt-1">{formatCurrency(totalEstimatedCost)}</p>
</CardContent>
</Card>
<Card className="bg-slate-50/40">
<CardContent className="p-4">
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-450">Projected Margin</span>
<p className={`text-lg font-bold mt-1 ${grossMarginVal >= 0 ? 'text-emerald-700' : 'text-rose-600'}`}>
{formatCurrency(grossMarginVal)}
</p>
</CardContent>
</Card>
<Card className="bg-slate-50/40">
<CardContent className="p-4">
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-450">Margin %</span>
<p className={`text-lg font-bold mt-1 ${grossMarginVal >= 0 ? 'text-emerald-700' : 'text-rose-600'}`}>
{grossMarginPct.toFixed(2)}%
</p>
</CardContent>
</Card>
</div>
{/* Itemized Breakdown Card */}
<Card className="border-slate-200 shadow-none">
<CardHeader className="bg-slate-50/40 py-3 border-b border-slate-250">
<CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-650">Itemized Cost Breakdown</CardTitle>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead className="font-semibold text-slate-700">Cost Center</TableHead>
<TableHead className="font-semibold text-slate-700 text-right">Estimated Cost</TableHead>
<TableHead className="font-semibold text-slate-700 text-right">% of Contract</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium text-slate-900">Materials Cost (Step 3)</TableCell>
<TableCell className="font-mono text-right text-slate-700">{formatCurrency(calculatedMaterialsCost)}</TableCell>
<TableCell className="text-right text-slate-500">
{contractValueNum > 0 ? ((calculatedMaterialsCost / contractValueNum) * 100).toFixed(1) : 0}%
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium text-slate-900">Labor Manpower Cost (Step 4)</TableCell>
<TableCell className="font-mono text-right text-slate-700">{formatCurrency(calculatedLaborCost)}</TableCell>
<TableCell className="text-right text-slate-500">
{contractValueNum > 0 ? ((calculatedLaborCost / contractValueNum) * 100).toFixed(1) : 0}%
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium text-slate-900">Equipment Machinery Cost (Step 5)</TableCell>
<TableCell className="font-mono text-right text-slate-700">{formatCurrency(calculatedEquipmentCost)}</TableCell>
<TableCell className="text-right text-slate-500">
{contractValueNum > 0 ? ((calculatedEquipmentCost / contractValueNum) * 100).toFixed(1) : 0}%
</TableCell>
</TableRow>
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
<TableCell className="font-bold text-slate-800">Total Summed Estimation</TableCell>
<TableCell className="font-mono font-bold text-slate-900 text-right">{formatCurrency(totalEstimatedCost)}</TableCell>
<TableCell className="font-bold text-right text-slate-700">
{contractValueNum > 0 ? ((totalEstimatedCost / contractValueNum) * 100).toFixed(1) : 0}%
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
<div className="flex justify-between pt-6 border-t border-slate-100">
<Button variant="outline" onClick={handleBack}>
<ChevronLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button onClick={() => setStep(7)} className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
Next: Submit Approval <ChevronRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
)}
{/* Step 7: Submit for Approval */}
{step === 7 && (
<form onSubmit={handleSubmitApproval} className="p-6 space-y-6">
<CardHeader className="p-0 pb-4 border-b border-slate-100">
<CardTitle className="text-md font-semibold text-slate-800">Step 7: Submit Project for Approval</CardTitle>
<CardDescription className="text-xs">Once submitted, the estimation and task parameters will lock down. Approved project unlocks procurement (MR & PO).</CardDescription>
</CardHeader>
{errors && Object.keys(errors).length > 0 && (
<div className="p-4 rounded-xl border border-rose-250 bg-rose-50/75 text-rose-900 flex items-start gap-3 shadow-sm animate-pulse">
<AlertTriangle className="h-5 w-5 text-rose-600 shrink-0 mt-0.5" />
<div>
<h4 className="font-semibold text-sm">Please correct the following errors:</h4>
<ul className="list-disc pl-5 mt-1.5 space-y-1 text-xs text-rose-700">
{Object.entries(errors).map(([key, message]) => (
<li key={key}>{String(message)}</li>
))}
</ul>
</div>
</div>
)}
{project.current_wizard_step >= 7 && project.status === 'planning' ? (
<div className="py-10 text-center space-y-3">
<CheckCircle2 className="mx-auto h-12 w-12 text-emerald-500 animate-bounce" />
<h3 className="text-md font-bold text-slate-800">Setup Submitted & Pending Review</h3>
<p className="text-xs text-slate-500 max-w-md mx-auto">
The project is currently under estimation review. Once standard manager approvals are finalized, the project status will move to In Progress and unlock materials requisitions.
</p>
<div className="pt-4 flex justify-center gap-3">
<Link href={route('projects.show', project.ulid)}>
<Button className="bg-slate-800 hover:bg-slate-900 text-white">Go to Overview</Button>
</Link>
</div>
</div>
) : (
<div className="space-y-5 max-w-lg">
<div className="p-4 rounded-xl bg-slate-50 border border-slate-200/80 flex items-start gap-3">
<ShieldCheck className="h-5 w-5 text-emerald-600 shrink-0 mt-0.5" />
<div className="text-xs text-slate-600 space-y-1">
<p className="font-semibold text-slate-800 text-sm">Automated Higher-Up Approval Routing</p>
<p>Submitting will automatically route this project estimation to executive management (Admin / Super Admin) for single-approver review.</p>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="notes" className="font-semibold text-slate-700">Review Notes (Optional)</Label>
<Textarea
id="notes"
placeholder="Add setup notes, estimation summaries, or project margin context..."
value={submissionNotes}
onChange={e => setSubmissionNotes(e.target.value)}
className={`border-slate-200 focus:border-emerald-500 focus:ring-emerald-500 mt-1 ${errors?.notes ? 'border-rose-400 focus:ring-rose-500 focus:border-rose-500' : ''}`}
rows={4}
/>
{errors?.notes && (
<span className="text-xs text-rose-600 mt-1 block font-medium">
{String(errors.notes)}
</span>
)}
</div>
<div className="flex justify-between pt-6 border-t border-slate-100">
<Button type="button" variant="outline" onClick={handleBack}>
<ChevronLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button type="submit" className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium">
<ShieldCheck className="mr-2 h-4 w-4" /> Submit for Approval
</Button>
</div>
</div>
)}
</form>
)}
</div>
</div>
</div>
{/* Step 3 Material Selection Catalog Modal */}
<MaterialCatalogModal
open={catalogModalOpen}
onOpenChange={setCatalogModalOpen}
onSelectMaterials={handleCatalogSelect}
onAddKit={handleCatalogAddKit}
materials={materials}
materialGroups={materialGroups}
/>
{/* Step 4 Labor Selection Lookup Modal */}
<LaborLookupModal
open={laborModalOpen}
onOpenChange={setLaborModalOpen}
labors={labors}
onSelectLabor={handleSelectLabor}
selectedLaborUlid={activeLaborRowIdx !== null ? localLabor[activeLaborRowIdx]?.labor_ulid : undefined}
/>
{/* Step 5 Equipment Selection Lookup Modal */}
<EquipmentLookupModal
open={equipmentModalOpen}
onOpenChange={setEquipmentModalOpen}
equipments={equipments}
onSelectEquipment={handleSelectEquipment}
selectedEquipmentUlid={activeEquipmentRowIdx !== null ? localEquipment[activeEquipmentRowIdx]?.equipment_ulid : undefined}
/>
</AuthenticatedLayout>
);
}