Files

1063 lines
70 KiB
TypeScript

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, useForm, router } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Badge } from '@/Components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/Components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/Components/ui/select';
import {
ArrowLeft, Save, Plus, Trash2, ClipboardList, Users, Wrench, AlertTriangle, ChevronRight, Check,
TrendingUp, FileSpreadsheet, FileDown
} from 'lucide-react';
import { PageProps } from '@/types';
import { FormEvent, useState, useMemo } from 'react';
import { ProjectForm, ProjectFormData } from '../../Components/ProjectForm';
import { MaterialCatalogModal, MaterialOption } from '@modules/MaterialLogistics/resources/js/Pages/Requisitions/MaterialCatalogModal';
import LaborLookupModal from '../../Components/LaborLookupModal';
import EquipmentLookupModal from '../../Components/EquipmentLookupModal';
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 Employee { id: number; ulid: string; name: string; email: string; }
interface ParentProject { id: number; ulid: string; name: string; code: string }
interface ProjectData {
id: number; ulid: string; name: string; code: string; status: string;
description?: string; client_name?: string; location?: string;
contract_value: string; contract_duration?: number;
start_date?: string; target_end_date?: string;
personnel?: { id: number; ulid: string; name: string; pivot?: { role: string } }[];
project_type: string;
classifications?: string[];
parent_project?: { id: number; ulid: string } | null;
is_unprofitable: boolean;
current_wizard_step: number;
milestones: any[];
tasks: any[];
materials_estimates?: any[];
materialsEstimates?: any[];
}
interface Props extends PageProps {
project: ProjectData;
employees: Employee[];
projects: ParentProject[];
classifications?: any[];
materials: any[];
materialGroups: any[];
labors: Labor[];
equipments: Equipment[];
}
export default function Edit({ project, employees, projects, classifications = [], labors, equipments, materials = [], materialGroups = [] }: Props) {
const isLocked = project.current_wizard_step >= 8;
const [activeTab, setActiveTab] = useState<'details' | 'tasks' | 'materials' | 'manpower' | 'equipment' | 'estimation'>('details');
const pm = project.personnel?.find(p => p.pivot?.role === 'pm');
// States for Modals
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);
// Form Details State
const { data, setData, put, processing, errors } = useForm<ProjectFormData>({
name: project.name,
client_name: project.client_name || '',
location: project.location || '',
start_date: project.start_date || '',
target_end_date: project.target_end_date || '',
contract_duration: project.contract_duration != null ? String(project.contract_duration) : '',
description: project.description || '',
pm_id: pm?.ulid || '',
contract_value: project.contract_value || '',
project_type: project.project_type || 'standard',
classifications: project.classifications || [],
parent_project_id: project.parent_project?.ulid || '',
is_unprofitable: !!project.is_unprofitable,
});
// Step 2 Local States (Tasks & Milestones)
const [localMilestones, setLocalMilestones] = useState<any[]>(() => {
return (project.milestones || []).map(m => ({
ulid: m.ulid,
name: m.name,
weight_percentage: String(m.weight_percentage)
}));
});
const [localTasks, setLocalTasks] = useState<any[]>(() => {
return (project.tasks || []).map(t => ({
ulid: t.ulid,
name: t.name,
description: t.description || '',
milestone_ulid: t.milestone?.ulid || (project.milestones?.find(m => m.id === t.milestone_id)?.ulid) || '',
start_date: t.start_date || '',
end_date: t.end_date || ''
}));
});
// Step 3 Local States (Material Estimates)
const [localEstimates, setLocalEstimates] = useState<any[]>(() => {
const ests = project.materials_estimates || project.materialsEstimates || [];
return ests.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 => {
const laborsList = t.task_labors || t.taskLabors || [];
laborsList.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 => {
const eqsList = t.task_equipments || t.taskEquipments || [];
eqsList.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;
});
// Currency Formatter
const formatCurrency = (v: string | number) => {
return new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v));
};
// Save Handlers
const handleSaveDetails = (e: FormEvent) => {
e.preventDefault();
if (isLocked) return;
put(route('projects.update', project.ulid));
};
const handleSaveTasks = () => {
if (isLocked) return;
router.post(route('projects.wizard.tasks', project.ulid), {
milestones: localMilestones,
tasks: localTasks
});
};
const handleSaveEstimates = () => {
if (isLocked) return;
router.post(route('projects.wizard.estimates', project.ulid), {
estimates: localEstimates
});
};
const handleSaveLabor = () => {
if (isLocked) return;
router.post(route('projects.wizard.labor', project.ulid), {
labor: localLabor
});
};
const handleSaveEquipment = () => {
if (isLocked) return;
router.post(route('projects.wizard.equipment', project.ulid), {
equipment: localEquipment
});
};
// Selection Handlers
const handleSelectLabor = (labor: Labor) => {
if (activeLaborRowIdx !== null) {
setLocalLabor(localLabor.map((item, i) => i === activeLaborRowIdx ? { ...item, labor_ulid: labor.ulid } : item));
}
setLaborModalOpen(false);
setActiveLaborRowIdx(null);
};
const handleAddLaborBundle = (bundle: any, crewMultiplier: number) => {
const newAllocations: any[] = [];
bundle.items.forEach((item: any) => {
const matched = labors.find(l =>
l.name.toLowerCase().includes(item.labor_name.toLowerCase()) ||
item.labor_name.toLowerCase().includes(l.name.toLowerCase())
);
if (matched) {
newAllocations.push({
task_ulid: '',
labor_ulid: matched.ulid,
estimated_hours: String(item.default_hours * crewMultiplier)
});
} else if (labors.length > 0) {
newAllocations.push({
task_ulid: '',
labor_ulid: labors[0].ulid,
estimated_hours: String(item.default_hours * crewMultiplier)
});
}
});
if (newAllocations.length > 0) {
setLocalLabor(prev => [...prev, ...newAllocations]);
}
setLaborModalOpen(false);
setActiveLaborRowIdx(null);
};
const handleSelectEquipment = (equipment: Equipment) => {
if (activeEquipmentRowIdx !== null) {
setLocalEquipment(localEquipment.map((item, i) => i === activeEquipmentRowIdx ? { ...item, equipment_ulid: equipment.ulid } : item));
}
setEquipmentModalOpen(false);
setActiveEquipmentRowIdx(null);
};
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);
};
// Tasks Modifiers
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));
};
// Resources Modifiers
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));
};
// Calculate Costs for Summary
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) || 0;
const grossMarginVal = contractValueNum - totalEstimatedCost;
const grossMarginPct = contractValueNum > 0 ? (grossMarginVal / contractValueNum) * 100 : 0;
const isUnprofitableEst = grossMarginVal < 0 || project.is_unprofitable;
return (
<AuthenticatedLayout
header={
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href={route('projects.show', project.ulid)}>
<Button variant="ghost" size="icon-sm"><ArrowLeft className="h-4 w-4" /></Button>
</Link>
<div>
<h2 className="text-xl font-bold leading-tight text-slate-800">
Edit Project: {project.name}
</h2>
<p className="text-xs text-slate-500 mt-0.5">{project.code} &middot; Setup & Estimations Review</p>
</div>
</div>
<Badge className={
isLocked ? 'bg-emerald-50 text-emerald-700 border-emerald-100' : 'bg-amber-50 text-amber-700 border-amber-100'
}>
{isLocked ? 'Setup Approved & Locked' : 'Setup Draft / Editable'}
</Badge>
</div>
}
>
<Head title={`Edit ${project.name}`} />
<div className="py-6 bg-slate-50/50 min-h-[calc(100vh-65px)]">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 space-y-6">
{isLocked && (
<Card className="border-amber-200 bg-amber-50/40 text-amber-900 shadow-none">
<CardContent className="p-4 flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<h4 className="font-semibold text-sm">Project Setup Locked</h4>
<p className="text-xs text-amber-750 mt-1">
This project's setup and estimation has been approved. All parameters, milestones, tasks, and resource estimates are read-only and locked against any edits.
</p>
</div>
</CardContent>
</Card>
)}
{/* Tab Navigation */}
<div className="flex border-b border-slate-200 overflow-x-auto bg-white p-1 rounded-t-xl border">
{[
{ id: 'details', label: 'Details', icon: ClipboardList },
{ id: 'tasks', label: 'Tasks & Milestones', icon: ChevronRight },
{ id: 'materials', label: 'Materials Estimates', icon: ClipboardList },
{ id: 'manpower', label: 'Manpower Allocation', icon: Users },
{ id: 'equipment', label: 'Equipment Allocation', icon: Wrench },
{ id: 'estimation', label: 'Estimation Summary', icon: TrendingUp },
].map((t) => {
const Icon = t.icon;
return (
<button
key={t.id}
type="button"
onClick={() => setActiveTab(t.id as any)}
className={`flex items-center gap-2 px-4 py-2.5 font-medium text-xs rounded-lg transition-all whitespace-nowrap ${
activeTab === t.id
? 'bg-slate-100 text-slate-900 shadow-xs'
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'
}`}
>
<Icon className="h-4 w-4 shrink-0" />
{t.label}
</button>
);
})}
</div>
{/* Content Body */}
<div className="bg-white rounded-b-xl border border-t-0 p-6 shadow-xs">
{/* Tab 1: Details */}
{activeTab === 'details' && (
<form onSubmit={handleSaveDetails} className="space-y-6">
<ProjectForm
data={data}
setData={setData}
errors={errors}
employees={employees}
projects={projects}
classifications={classifications}
isLocked={isLocked}
/>
{!isLocked && (
<div className="flex justify-end gap-3 pt-4 border-t">
<Link href={route('projects.show', project.ulid)}>
<Button variant="outline" type="button">Cancel</Button>
</Link>
<Button type="submit" disabled={processing} className="bg-indigo-600 hover:bg-indigo-700 text-white">
<Save className="mr-2 h-4 w-4" /> Save Project Info
</Button>
</div>
)}
</form>
)}
{/* Tab 2: Tasks & Milestones */}
{activeTab === 'tasks' && (
<div className="space-y-6">
<div className="flex justify-between items-center pb-3 border-b">
<div>
<h3 className="text-sm font-semibold text-slate-800">Project Tasks & Milestones</h3>
<p className="text-xs text-slate-500 mt-0.5">Define schedule milestones and task timelines.</p>
</div>
{!isLocked && (
<Button onClick={addMilestone} variant="outline" size="sm" className="h-8">
<Plus className="mr-1.5 h-3.5 w-3.5" /> Add Milestone
</Button>
)}
</div>
{/* Milestones Editor */}
<div className="space-y-3">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500">1. Milestones</h4>
{localMilestones.length === 0 ? (
<p className="text-xs italic text-slate-400">No milestones defined yet.</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">
<Input
placeholder="Milestone Name"
value={m.name}
onChange={e => updateMilestone(idx, 'name', e.target.value)}
className="h-8 text-xs bg-white"
disabled={isLocked}
/>
<Input
type="number"
placeholder="Weight %"
value={m.weight_percentage}
onChange={e => updateMilestone(idx, 'weight_percentage', e.target.value)}
className="h-8 text-xs w-20 text-right bg-white"
disabled={isLocked}
/>
{!isLocked && (
<Button variant="ghost" size="icon" onClick={() => removeMilestone(idx)} className="h-8 w-8 text-rose-500 hover:text-rose-700 hover:bg-rose-50 shrink-0">
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
))}
</div>
)}
</div>
{/* Tasks Editor */}
<div className="space-y-3 pt-6 border-t">
<div className="flex justify-between items-center">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500">2. Tasks list</h4>
{!isLocked && (
<Button onClick={addTask} variant="outline" size="sm" className="h-8">
<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">No tasks defined yet.</p>
) : (
<div className="space-y-3">
{localTasks.map((t, idx) => (
<div key={idx} className="bg-slate-50/50 p-4 rounded-xl border space-y-3 relative">
{!isLocked && (
<div className="absolute right-3 top-3">
<Button variant="ghost" size="icon" onClick={() => removeTask(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 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 bg-white mt-1"
disabled={isLocked}
/>
</div>
<div>
<Label className="text-[10px] text-slate-500">Milestone</Label>
<Select
value={t.milestone_ulid}
onValueChange={val => updateTask(idx, 'milestone_ulid', val || '')}
disabled={isLocked}
>
<SelectTrigger className="h-8 text-xs bg-white 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>
</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="Description"
value={t.description}
onChange={e => updateTask(idx, 'description', e.target.value)}
className="h-8 text-xs bg-white mt-1"
disabled={isLocked}
/>
</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 bg-white mt-1"
disabled={isLocked}
/>
</div>
<div>
<Label className="text-[10px] text-slate-500">End Date</Label>
<Input
type="date"
value={t.end_date}
onChange={e => updateTask(idx, 'end_date', e.target.value)}
className="h-8 text-xs bg-white mt-1"
disabled={isLocked}
/>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
{!isLocked && (
<div className="flex justify-end gap-3 pt-4 border-t">
<Button onClick={handleSaveTasks} className="bg-indigo-600 hover:bg-indigo-700 text-white">
<Save className="mr-2 h-4 w-4" /> Save Schedule & Tasks
</Button>
</div>
)}
</div>
)}
{/* Tab 3: Materials Estimates */}
{activeTab === 'materials' && (
<div className="space-y-6">
<div className="flex justify-between items-center pb-3 border-b">
<div>
<h3 className="text-sm font-semibold text-slate-800">Materials Estimation</h3>
<p className="text-xs text-slate-500 mt-0.5">Budget the estimated materials for project construction.</p>
</div>
{!isLocked && (
<Button onClick={() => setCatalogModalOpen(true)} className="bg-indigo-600 hover:bg-indigo-700 text-white font-medium h-8 text-xs">
<Plus className="mr-1.5 h-3.5 w-3.5" /> Add Materials
</Button>
)}
</div>
{localEstimates.length === 0 ? (
<div className="py-12 text-center border border-dashed 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.</p>
</div>
) : (
<div className="border rounded-xl overflow-hidden shadow-xs">
<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>
{!isLocked && <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"
disabled={isLocked}
/>
</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"
disabled={isLocked}
/>
</TableCell>
<TableCell className="font-mono font-medium text-slate-700 text-right">{formatCurrency(total)}</TableCell>
{!isLocked && (
<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-850">Total Materials Estimate</TableCell>
<TableCell className="font-mono font-bold text-slate-900 text-right">{formatCurrency(calculatedMaterialsCost)}</TableCell>
{!isLocked && <TableCell></TableCell>}
</TableRow>
</TableBody>
</Table>
</div>
)}
{!isLocked && (
<div className="flex justify-end gap-3 pt-4 border-t">
<Button onClick={handleSaveEstimates} className="bg-indigo-600 hover:bg-indigo-700 text-white">
<Save className="mr-2 h-4 w-4" /> Save Material Estimates
</Button>
</div>
)}
</div>
)}
{/* Tab 4: Manpower Allocation */}
{activeTab === 'manpower' && (
<div className="space-y-6">
<div className="flex justify-between items-center pb-3 border-b">
<div>
<h3 className="text-sm font-semibold text-slate-800">Manpower Allocations</h3>
<p className="text-xs text-slate-500 mt-0.5">Assign specialized labor resources and hours to project tasks.</p>
</div>
{!isLocked && (
<Button onClick={addLaborAllocation} className="bg-indigo-600 hover:bg-indigo-700 text-white font-medium h-8 text-xs">
<Plus className="mr-1.5 h-3.5 w-3.5" /> Assign Labor
</Button>
)}
</div>
{localLabor.length === 0 ? (
<div className="py-12 text-center border border-dashed 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.</p>
</div>
) : (
<div className="border rounded-xl overflow-hidden shadow-xs">
<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 trade / role</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>
{!isLocked && <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>
<Select
value={item.task_ulid}
onValueChange={val => updateLaborAllocation(idx, 'task_ulid', val || '')}
disabled={isLocked}
>
<SelectTrigger className="h-8 text-xs bg-white">
<SelectValue placeholder="Select Task" />
</SelectTrigger>
<SelectContent>
{localTasks.map((t, tIdx) => (
<SelectItem key={tIdx} value={t.ulid || String(tIdx)}>
{t.name || `Task ${tIdx+1}`}
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Input
value={rateObj ? rateObj.name : 'Choose Labor trade...'}
className="h-8 text-xs bg-slate-50 font-medium"
disabled
/>
{!isLocked && (
<Button type="button" variant="outline" size="sm" onClick={() => { setActiveLaborRowIdx(idx); setLaborModalOpen(true); }} className="h-8 text-xs">
Query
</Button>
)}
</div>
</TableCell>
<TableCell className="font-mono text-slate-600">{formatCurrency(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"
disabled={isLocked}
/>
</TableCell>
<TableCell className="font-mono font-medium text-slate-705 text-right">{formatCurrency(total)}</TableCell>
{!isLocked && (
<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-850">Total Labor Estimate</TableCell>
<TableCell className="font-mono font-bold text-slate-900 text-right">{formatCurrency(calculatedLaborCost)}</TableCell>
{!isLocked && <TableCell></TableCell>}
</TableRow>
</TableBody>
</Table>
</div>
)}
{!isLocked && (
<div className="flex justify-end gap-3 pt-4 border-t">
<Button onClick={handleSaveLabor} className="bg-indigo-600 hover:bg-indigo-700 text-white">
<Save className="mr-2 h-4 w-4" /> Save Manpower Allocations
</Button>
</div>
)}
</div>
)}
{/* Tab 5: Equipment Allocation */}
{activeTab === 'equipment' && (
<div className="space-y-6">
<div className="flex justify-between items-center pb-3 border-b">
<div>
<h3 className="text-sm font-semibold text-slate-800">Equipment & Machinery Allocations</h3>
<p className="text-xs text-slate-500 mt-0.5">Assign specialized machinery and hours to project tasks.</p>
</div>
{!isLocked && (
<Button onClick={addEquipmentAllocation} className="bg-indigo-600 hover:bg-indigo-700 text-white font-medium h-8 text-xs">
<Plus className="mr-1.5 h-3.5 w-3.5" /> Assign Equipment
</Button>
)}
</div>
{localEquipment.length === 0 ? (
<div className="py-12 text-center border border-dashed 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.</p>
</div>
) : (
<div className="border rounded-xl overflow-hidden shadow-xs">
<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 / Machine</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 Machinery Cost</TableHead>
{!isLocked && <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 || '')}
disabled={isLocked}
>
<SelectTrigger className="h-8 text-xs bg-white">
<SelectValue placeholder="Select Task" />
</SelectTrigger>
<SelectContent>
{localTasks.map((t, tIdx) => (
<SelectItem key={tIdx} value={t.ulid || String(tIdx)}>
{t.name || `Task ${tIdx+1}`}
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Input
value={rateObj ? rateObj.name : 'Choose Equipment...'}
className="h-8 text-xs bg-slate-50 font-medium"
disabled
/>
{!isLocked && (
<Button type="button" variant="outline" size="sm" onClick={() => { setActiveEquipmentRowIdx(idx); setEquipmentModalOpen(true); }} className="h-8 text-xs">
Query
</Button>
)}
</div>
</TableCell>
<TableCell className="font-mono text-slate-600">{formatCurrency(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"
disabled={isLocked}
/>
</TableCell>
<TableCell className="font-mono font-medium text-slate-705 text-right">{formatCurrency(total)}</TableCell>
{!isLocked && (
<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-850">Total Machinery Estimate</TableCell>
<TableCell className="font-mono font-bold text-slate-900 text-right">{formatCurrency(calculatedEquipmentCost)}</TableCell>
{!isLocked && <TableCell></TableCell>}
</TableRow>
</TableBody>
</Table>
</div>
)}
{!isLocked && (
<div className="flex justify-end gap-3 pt-4 border-t">
<Button onClick={handleSaveEquipment} className="bg-indigo-600 hover:bg-indigo-700 text-white">
<Save className="mr-2 h-4 w-4" /> Save Equipment Allocations
</Button>
</div>
)}
</div>
)}
{/* Tab 6: Estimation Summary */}
{activeTab === 'estimation' && (
<div className="space-y-6">
<div className="flex justify-between items-center pb-3 border-b">
<div>
<h3 className="text-sm font-semibold text-slate-800">Financial Estimation Margin Summary</h3>
<p className="text-xs text-slate-500 mt-0.5">Review total projected costs, contract margins, and flag checks.</p>
</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-650 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>
</div>
{/* 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 (Tab 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 (Tab 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 (Tab 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>
)}
</div>
</div>
</div>
{/* Modals for Lookups (Active only when not locked) */}
{!isLocked && (
<>
<MaterialCatalogModal
open={catalogModalOpen}
onOpenChange={setCatalogModalOpen}
materials={materials}
materialGroups={materialGroups}
onSelectMaterials={handleCatalogSelect}
onAddKit={handleCatalogAddKit}
/>
<LaborLookupModal
open={laborModalOpen}
onOpenChange={setLaborModalOpen}
labors={labors}
onSelectLabor={handleSelectLabor}
onAddLaborBundle={handleAddLaborBundle}
/>
<EquipmentLookupModal
open={equipmentModalOpen}
onOpenChange={setEquipmentModalOpen}
equipments={equipments}
onSelectEquipment={handleSelectEquipment}
/>
</>
)}
</AuthenticatedLayout>
);
}