Files

923 lines
67 KiB
TypeScript

import ProjectLayout from '../../Layouts/ProjectLayout';
import EvmSCurveChart from '../../Components/EvmSCurveChart';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/Components/ui/card';
import { MapPin, Users, TrendingUp, DollarSign, Calendar, AlertTriangle, ArrowRight, Activity, ShieldCheck, FileText, ClipboardList, Hammer, Truck, Layers, Milestone as MilestoneIcon, Tags, FileSpreadsheet, FileDown } from 'lucide-react';
import { PageProps } from '@/types';
import { Badge } from '@/Components/ui/badge';
import { Link } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/Components/ui/tabs';
interface ChildProject {
id: number;
ulid: string;
name: string;
code: string;
status: string;
contract_value: string;
total_capitalization: string;
start_date?: string;
target_end_date?: string;
}
interface ProjectData {
id: number; ulid: string; name: string; code: string; description?: string;
status: string; location?: string; client_name?: string;
contract_value: string; total_capitalization: string; completion_percentage: string;
capitalization_percentage: number; is_over_budget: boolean;
contract_duration?: number;
start_date?: string; target_end_date?: string; actual_end_date?: string;
created_at: string;
is_unprofitable: boolean;
project_type: string;
classifications?: string[];
extension_projects?: ChildProject[];
rollup_contract_value?: string | number;
rollup_capitalization?: string | number;
parent_project?: { id: number; ulid: string; name: string; code: string } | null;
milestones?: {
id: number;
ulid: string;
name: string;
weight_percentage: string | number;
}[];
materials_estimates?: {
id: number;
ulid: string;
estimated_qty: string | number;
unit_cost: string | number;
material: {
id: number;
ulid: string;
name: string;
sku?: string;
unit: string;
category?: string;
};
}[];
tasks: {
id: number;
ulid: string;
name: string;
description?: string;
status: string;
estimated_hours: string | number;
task_labors?: {
id: number;
estimated_hours: string | number;
labor: {
id: number;
name: string;
category: string;
hourly_rate: string | number;
};
}[];
task_equipments?: {
id: number;
estimated_hours: string | number;
equipment: {
id: number;
name: string;
owner_name?: string;
hourly_rate: string | number;
};
}[];
}[];
}
interface StatusOption { value: string; label: string }
interface Props extends PageProps {
project: ProjectData;
taskStats: { total: number; pending: number; in_progress: number; completed: number; closed?: number };
allowedTransitions: StatusOption[];
estimationTotals?: {
materials: number;
labor: number;
equipment: number;
sum: number;
margin: number;
margin_pct: number;
};
evmData?: any;
tab?: string;
}
const CLASSIFICATION_LABEL_MAP: Record<string, string> = {
'road highway, pavement, railways, airport horizontal structures and bridges': 'Road, Highway, Pavement, Railways, Airport Horizontal Structures and Bridges',
'road, highway, pavement, railways, airport horizontal structures and bridges': 'Road, Highway, Pavement, Railways, Airport Horizontal Structures and Bridges',
'irrigation and flood control': 'Irrigation and Flood Control',
'dam, reservoir, and tunneling': 'Dam, Reservoir, and Tunneling',
'dam, reservoir and tunneling': 'Dam, Reservoir, and Tunneling',
'water supply': 'Water Supply',
'port, harbor and offshore engineering': 'Port, Harbor and Offshore Engineering',
'building and industrial plant': 'Building and Industrial Plant',
'sewerage treatment/disposal plant': 'Sewerage Treatment / Disposal Plant',
'sewerage treatment / disposal plant': 'Sewerage Treatment / Disposal Plant',
'water treatment plant and system': 'Water Treatment Plant and System',
'park, playground and recreational work': 'Park, Playground and Recreational Work',
'electrical work': 'Electrical Work',
};
const formatClassificationTag = (tag: string): string => {
if (!tag) return '';
const trimmed = tag.trim();
const lower = trimmed.toLowerCase();
return CLASSIFICATION_LABEL_MAP[lower] || CLASSIFICATION_LABEL_MAP[trimmed] || trimmed;
};
const formatCurrency = (v: string | number) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v));
function StatCard({ icon: Icon, label, value, sub }: { icon: React.ComponentType<{ className?: string }>; label: string; value: string; sub?: string }) {
return (
<Card>
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gray-100">
<Icon className="h-5 w-5 text-gray-600" />
</div>
<div>
<p className="text-xs text-gray-500">{label}</p>
<p className="text-lg font-semibold">{value}</p>
{sub && <p className="text-xs text-gray-400">{sub}</p>}
</div>
</div>
</CardContent>
</Card>
);
}
export default function Overview({ project, taskStats, allowedTransitions, estimationTotals, tab, evmData }: Props) {
const [activeTab, setActiveTab] = useState(tab || 'overview');
return (
<ProjectLayout project={project} allowedTransitions={allowedTransitions} currentTab="overview">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<TabsList className="bg-slate-100/80 border border-slate-200/50 p-1 rounded-xl">
<TabsTrigger
value="overview"
className="rounded-lg px-4 py-2 text-sm font-medium transition-all data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2"
>
<ClipboardList className="h-4 w-4" /> Overview Dashboard
</TabsTrigger>
<TabsTrigger
value="progress"
className="rounded-lg px-4 py-2 text-sm font-medium transition-all data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2"
>
<TrendingUp className="h-4 w-4" /> Progress Monitoring (EVM)
</TabsTrigger>
<TabsTrigger
value="estimation"
className="rounded-lg px-4 py-2 text-sm font-medium transition-all data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2"
>
<FileText className="h-4 w-4" /> Estimation Review
</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2">
<a href={route('projects.wizard.export.excel', project.ulid)} target="_blank" rel="noopener noreferrer">
<Button type="button" variant="outline" size="sm" className="h-9 border-slate-200 text-slate-700 hover:bg-slate-50 flex items-center gap-1.5 font-semibold shadow-2xs cursor-pointer">
<FileSpreadsheet className="h-4 w-4 text-emerald-600" />
Export 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-9 border-slate-200 text-slate-700 hover:bg-slate-50 flex items-center gap-1.5 font-semibold shadow-2xs cursor-pointer">
<FileDown className="h-4 w-4 text-rose-600" />
Export PDF
</Button>
</a>
</div>
</div>
<TabsContent value="overview" className="mt-0 space-y-6 focus-visible:outline-none">
{/* Unprofitable Warning Banner */}
{project.is_unprofitable && (
<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 Handling: Unprofitable / Loss-Making Project Flagged</h4>
<p className="text-xs text-amber-700 mt-1">
This project has been explicitly flagged as unprofitable. Budget overruns, procurement approvals, and requisition validation will be visually highlighted for special auditing.
</p>
</div>
</div>
)}
{/* Parent Project Linked Alert */}
{project.project_type === 'extension' && project.parent_project && (
<div className="p-4 rounded-xl border border-slate-200 bg-slate-50 text-slate-800 flex items-center justify-between shadow-sm">
<div className="flex items-center gap-3">
<ShieldCheck className="h-5 w-5 text-emerald-600 shrink-0" />
<div>
<h4 className="font-semibold text-sm">Extension Project / Variation Order</h4>
<p className="text-xs text-slate-500 mt-0.5">
This project is linked to parent project: <strong className="text-slate-700">{project.parent_project.code}</strong>. Tasks and capitalization values roll up recursively.
</p>
</div>
</div>
<Link href={route('projects.show', project.parent_project.ulid)}>
<Button variant="outline" size="sm" className="gap-1.5">
View Parent <ArrowRight className="h-3.5 w-3.5" />
</Button>
</Link>
</div>
)}
{/* Stat Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard icon={DollarSign} label="Contract Value" value={formatCurrency(project.contract_value)} />
<Card>
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${
project.is_over_budget ? 'bg-red-100' : project.capitalization_percentage >= 80 ? 'bg-amber-100' : 'bg-gray-100'
}`}>
<TrendingUp className={`h-5 w-5 ${
project.is_over_budget ? 'text-red-600' : project.capitalization_percentage >= 80 ? 'text-amber-600' : 'text-gray-600'
}`} />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-gray-500">Capitalization</p>
<div className="flex items-baseline gap-2">
<p className="text-lg font-semibold">{project.capitalization_percentage.toFixed(1)}%</p>
<p className="text-xs text-gray-400 truncate">{formatCurrency(project.total_capitalization)}</p>
</div>
<div className="mt-1.5 h-1.5 rounded-full bg-gray-200 overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
project.is_over_budget ? 'bg-red-500' : project.capitalization_percentage >= 80 ? 'bg-amber-500' : 'bg-emerald-500'
}`}
style={{ width: `${Math.min(project.capitalization_percentage, 100)}%` }}
/>
</div>
</div>
</div>
</CardContent>
</Card>
<StatCard icon={Calendar} label="Timeline" value={project.start_date ? new Date(project.start_date).toLocaleDateString() : 'Not set'} sub={project.target_end_date ? `${new Date(project.target_end_date).toLocaleDateString()}` : undefined} />
<StatCard icon={Users} label="Progress" value={`${Number(project.completion_percentage).toFixed(0)}%`} sub={`${taskStats.completed} / ${taskStats.total} tasks done`} />
</div>
<Card>
<CardContent className="pt-6 space-y-4">
{project.description && <div><p className="text-xs text-gray-500 mb-1">Description</p><p className="text-sm">{project.description}</p></div>}
<div className="grid grid-cols-2 gap-4">
{project.location && <div className="flex items-center gap-2 text-sm"><MapPin className="h-4 w-4 text-gray-400" />{project.location}</div>}
{project.client_name && <div className="flex items-center gap-2 text-sm"><Users className="h-4 w-4 text-gray-400" />Client: {project.client_name}</div>}
</div>
{project.classifications && project.classifications.length > 0 && (
<div>
<p className="text-xs text-gray-500 mb-1.5 flex items-center gap-1.5 font-medium">
<Tags className="h-3.5 w-3.5 text-emerald-600" /> Project Classifications
</p>
<div className="flex flex-wrap gap-1.5">
{project.classifications.map((tag: string, idx: number) => (
<Badge
key={idx}
variant="outline"
className="bg-emerald-50/90 text-emerald-900 border-emerald-200 text-xs py-0.5 px-2.5 font-medium shadow-2xs"
>
{formatClassificationTag(tag)}
</Badge>
))}
</div>
</div>
)}
{/* Capitalization Section */}
<div className="mt-4 space-y-3">
<div>
<div className="flex items-baseline justify-between mb-2">
<p className="text-sm font-medium text-gray-700">Cost Capitalization</p>
<p className="text-sm">
<span className={`font-semibold ${
project.is_over_budget ? 'text-red-600' : project.capitalization_percentage >= 80 ? 'text-amber-600' : 'text-gray-800'
}`}>{project.capitalization_percentage.toFixed(1)}%</span>
<span className="text-gray-400"> of contract value</span>
</p>
</div>
<div className="h-3 rounded-full bg-gray-200 overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
project.is_over_budget ? 'bg-red-500' : project.capitalization_percentage >= 80 ? 'bg-amber-500' : 'bg-emerald-500'
}`}
style={{ width: `${Math.min(project.capitalization_percentage, 100)}%` }}
/>
</div>
<div className="flex items-center justify-between mt-1">
<p className="text-xs text-gray-400">
{formatCurrency(project.total_capitalization)} / {formatCurrency(project.contract_value)}
</p>
{project.is_over_budget && (
<p className="text-xs font-medium text-red-500"> Over budget</p>
)}
</div>
</div>
</div>
{/* Task Progress Section */}
<div className="mt-4 space-y-4">
<div>
<div className="flex items-baseline justify-between mb-2">
<p className="text-sm font-medium text-gray-700">Progress</p>
<p className="text-sm text-gray-500">
<span className="font-semibold text-gray-800">{(taskStats.completed || 0) + (taskStats.closed || 0)}</span>
<span className="text-gray-400"> / {taskStats.total} tasks completed</span>
</p>
</div>
<div className="h-3 rounded-full bg-gray-200 overflow-hidden">
<div
className="h-full bg-emerald-500 rounded-full transition-all duration-500"
style={{ width: taskStats.total > 0 ? `${(((taskStats.completed || 0) + (taskStats.closed || 0)) / taskStats.total) * 100}%` : '0%' }}
/>
</div>
<p className="text-xs text-gray-400 mt-1">
{taskStats.total > 0 ? `${((((taskStats.completed || 0) + (taskStats.closed || 0)) / taskStats.total) * 100).toFixed(0)}%` : '0%'} complete
</p>
</div>
{taskStats.total > 0 && (
<div>
<p className="text-xs font-medium text-gray-500 mb-1.5">Task Distribution</p>
<div className="flex h-2 rounded-full overflow-hidden bg-gray-200">
{(taskStats.closed || 0) > 0 && (
<div
className="bg-orange-500 transition-all duration-500"
style={{ width: `${((taskStats.closed || 0) / taskStats.total) * 100}%` }}
/>
)}
{taskStats.completed > 0 && (
<div
className="bg-emerald-500 transition-all duration-500"
style={{ width: `${(taskStats.completed / taskStats.total) * 100}%` }}
/>
)}
{taskStats.in_progress > 0 && (
<div
className="bg-blue-500 transition-all duration-500"
style={{ width: `${(taskStats.in_progress / taskStats.total) * 100}%` }}
/>
)}
{taskStats.pending > 0 && (
<div
className="bg-gray-300 transition-all duration-500"
style={{ width: `${(taskStats.pending / taskStats.total) * 100}%` }}
/>
)}
</div>
</div>
)}
{taskStats.total > 0 ? (
<div className="space-y-2">
{[
{ label: 'Closed', count: taskStats.closed || 0, color: 'bg-orange-500', textColor: 'text-orange-600' },
{ label: 'Completed', count: taskStats.completed, color: 'bg-emerald-500', textColor: 'text-emerald-600' },
{ label: 'In Progress', count: taskStats.in_progress, color: 'bg-blue-500', textColor: 'text-blue-600' },
{ label: 'Pending', count: taskStats.pending, color: 'bg-gray-300', textColor: 'text-gray-500' },
].map((item) => {
const pct = taskStats.total > 0 ? (item.count / taskStats.total) * 100 : 0;
return (
<div key={item.label} className="flex items-center gap-3">
<div className={`h-2.5 w-2.5 rounded-full ${item.color} shrink-0`} />
<span className="text-xs text-gray-600 w-20">{item.label}</span>
<div className="flex-1 h-1.5 rounded-full bg-gray-100 overflow-hidden">
<div
className={`h-full ${item.color} rounded-full transition-all duration-500`}
style={{ width: `${pct}%` }}
/>
</div>
<span className={`text-xs font-medium tabular-nums w-20 text-right ${item.textColor}`}>
{item.count} task{item.count !== 1 ? 's' : ''} ({pct.toFixed(0)}%)
</span>
</div>
);
})}
</div>
) : (
<p className="text-sm text-gray-400 italic">No tasks yet. Add tasks to track progress.</p>
)}
</div>
</CardContent>
</Card>
{/* Project Extensions & Financial Rollups */}
{project.project_type !== 'extension' && (
<Card className="border-slate-200/80 shadow-sm overflow-hidden">
<CardHeader className="bg-slate-50/50 border-b border-slate-100 flex flex-row items-center justify-between py-4">
<div>
<CardTitle className="text-md font-medium text-slate-800 flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-emerald-600" /> Project Extensions & Variation Orders
</CardTitle>
<CardDescription className="text-xs text-slate-500">
Financial and task progress rollup aggregates for parent project and child variations.
</CardDescription>
</div>
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-100 hover:bg-emerald-50 font-mono">
{project.extension_projects?.length || 0} Extension(s)
</Badge>
</CardHeader>
<CardContent className="p-6 space-y-6">
{/* Rollup Financials Summary */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-slate-50/60 p-4 rounded-xl border border-slate-100">
<span className="text-xs text-slate-400 font-medium">Combined Contract Value</span>
<h4 className="text-lg font-bold text-slate-900 mt-1">{formatCurrency(project.rollup_contract_value || project.contract_value)}</h4>
<span className="text-[10px] text-slate-400 mt-1 block">Includes parent & child contracts</span>
</div>
<div className="bg-slate-50/60 p-4 rounded-xl border border-slate-100">
<span className="text-xs text-slate-400 font-medium">Combined Capitalization</span>
<h4 className="text-lg font-bold text-slate-900 mt-1">{formatCurrency(project.rollup_capitalization || project.total_capitalization)}</h4>
<span className="text-[10px] text-slate-400 mt-1 block">Combined actual spent costs</span>
</div>
<div className="bg-slate-50/60 p-4 rounded-xl border border-slate-100">
<span className="text-xs text-slate-400 font-medium">Combined Capitalization %</span>
<h4 className="text-lg font-bold text-slate-900 mt-1">
{((Number(project.rollup_capitalization || project.total_capitalization) /
Math.max(Number(project.rollup_contract_value || project.contract_value), 1)) * 100).toFixed(1)}%
</h4>
<span className="text-[10px] text-slate-400 mt-1 block">Combined budget utilization rate</span>
</div>
</div>
{/* Combined Capitalization Progress Bar */}
<div className="space-y-2">
<div className="flex items-center justify-between text-xs font-semibold text-slate-700">
<span>Combined Cost Capitalization Progress</span>
<span>
{formatCurrency(project.rollup_capitalization || project.total_capitalization)} / {formatCurrency(project.rollup_contract_value || project.contract_value)}
</span>
</div>
<div className="h-2 rounded-full bg-slate-100 overflow-hidden border border-slate-200/50">
<div
className={`h-full rounded-full transition-all duration-500 ${
Number(project.rollup_capitalization || project.total_capitalization) > Number(project.rollup_contract_value || project.contract_value)
? 'bg-rose-500'
: 'bg-emerald-500'
}`}
style={{
width: `${Math.min(
(Number(project.rollup_capitalization || project.total_capitalization) /
Math.max(Number(project.rollup_contract_value || project.contract_value), 1)) * 100,
100
)}%`
}}
/>
</div>
</div>
{/* Extensions List Table */}
{project.extension_projects && project.extension_projects.length > 0 ? (
<div className="border border-slate-150 rounded-lg overflow-hidden">
<table className="w-full text-left border-collapse text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-150 font-medium text-slate-650">
<th className="py-2.5 px-4">Code & Project Name</th>
<th className="py-2.5 px-4">Status</th>
<th className="py-2.5 px-4">Contract Value</th>
<th className="py-2.5 px-4">Capitalization</th>
<th className="py-2.5 px-4">Timeline</th>
<th className="py-2.5 px-4 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-150">
{project.extension_projects.map((ext) => (
<tr key={ext.id} className="hover:bg-slate-50/30 transition-colors">
<td className="py-3 px-4 font-semibold text-slate-800">
<span className="font-mono text-slate-500 text-xs block">{ext.code}</span>
{ext.name}
</td>
<td className="py-3 px-4">
<Badge className="bg-slate-100 text-slate-700 hover:bg-slate-100 capitalize">
{ext.status.replace(/_/g, ' ')}
</Badge>
</td>
<td className="py-3 px-4 text-slate-700">{formatCurrency(ext.contract_value)}</td>
<td className="py-3 px-4 text-slate-700">{formatCurrency(ext.total_capitalization)}</td>
<td className="py-3 px-4 text-xs text-slate-500">
{ext.start_date ? new Date(ext.start_date).toLocaleDateString() : 'N/A'} - {ext.target_end_date ? new Date(ext.target_end_date).toLocaleDateString() : 'N/A'}
</td>
<td className="py-3 px-4 text-right">
<Link href={route('projects.show', ext.ulid)}>
<Button variant="ghost" size="sm" className="h-8 text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50">
View <ArrowRight className="ml-1.5 h-3.5 w-3.5" />
</Button>
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="py-8 text-center text-slate-400 border border-dashed border-slate-200 rounded-xl bg-slate-50/30">
<p className="text-xs italic">No extension projects or variation orders registered for this project.</p>
<p className="text-[10px] text-slate-500 mt-1">Extensions can be created via the Create Project page and selecting this as the Parent Project.</p>
</div>
)}
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="estimation" className="mt-0 space-y-6 focus-visible:outline-none">
{/* Summary Metrics */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card className="bg-slate-50/50 border-slate-200/80 shadow-sm">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-blue-100 text-blue-600">
<Layers className="h-5 w-5" />
</div>
<div>
<p className="text-xs text-gray-500 font-medium">Materials Cost</p>
<p className="text-lg font-bold text-slate-900 mt-1">
{formatCurrency(estimationTotals?.materials || 0)}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-50/50 border-slate-200/80 shadow-sm">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-orange-100 text-orange-600">
<Hammer className="h-5 w-5" />
</div>
<div>
<p className="text-xs text-gray-500 font-medium">Labor Cost</p>
<p className="text-lg font-bold text-slate-900 mt-1">
{formatCurrency(estimationTotals?.labor || 0)}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-50/50 border-slate-200/80 shadow-sm">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-indigo-100 text-indigo-600">
<Truck className="h-5 w-5" />
</div>
<div>
<p className="text-xs text-gray-500 font-medium">Equipment Cost</p>
<p className="text-lg font-bold text-slate-900 mt-1">
{formatCurrency(estimationTotals?.equipment || 0)}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-50/50 border-slate-200/80 shadow-sm">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-slate-600">
<DollarSign className="h-5 w-5" />
</div>
<div>
<p className="text-xs text-gray-500 font-medium">Total Estimate</p>
<p className="text-lg font-bold text-slate-900 mt-1">
{formatCurrency(estimationTotals?.sum || 0)}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Profitability Margin Bar */}
{estimationTotals && (
<Card className={cn(
"border shadow-sm",
estimationTotals.margin >= 0 ? "border-emerald-200 bg-emerald-50/30" : "border-rose-200 bg-rose-50/30"
)}>
<CardContent className="p-5 flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-start gap-3">
<AlertTriangle className={cn(
"h-5 w-5 shrink-0 mt-0.5",
estimationTotals.margin >= 0 ? "text-emerald-600" : "text-rose-600"
)} />
<div>
<h4 className={cn(
"font-semibold text-sm",
estimationTotals.margin >= 0 ? "text-emerald-900" : "text-rose-900"
)}>
{estimationTotals.margin >= 0 ? "Estimated Project Margins Clear" : "Unprofitable Estimation Warning"}
</h4>
<p className={cn(
"text-xs mt-1",
estimationTotals.margin >= 0 ? "text-emerald-700" : "text-rose-700"
)}>
This estimation consumes {((estimationTotals.sum / Math.max(Number(project.contract_value), 1)) * 100).toFixed(1)}% of the registered contract value ({formatCurrency(project.contract_value)}).
</p>
</div>
</div>
<div className="text-right shrink-0">
<span className={cn(
"text-lg font-bold block",
estimationTotals.margin >= 0 ? "text-emerald-700" : "text-rose-700"
)}>
{estimationTotals.margin >= 0 ? "+" : ""}{formatCurrency(estimationTotals.margin)}
</span>
<span className={cn(
"text-xs font-semibold px-2.5 py-0.5 rounded-full inline-block mt-1",
estimationTotals.margin >= 0 ? "bg-emerald-100 text-emerald-800" : "bg-rose-100 text-rose-800"
)}>
Margin: {estimationTotals.margin_pct.toFixed(1)}%
</span>
</div>
</CardContent>
</Card>
)}
{/* Materials Estimates */}
<Card className="border-slate-200/80 shadow-sm overflow-hidden">
<CardHeader className="bg-slate-50/50 border-b border-slate-100">
<CardTitle className="text-sm font-semibold text-slate-800 flex items-center gap-2">
<Layers className="h-4 w-4 text-slate-500" /> Materials Estimates
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-150 font-medium text-slate-650">
<th className="py-2.5 px-4 w-12">#</th>
<th className="py-2.5 px-4">Material Name</th>
<th className="py-2.5 px-4">Category</th>
<th className="py-2.5 px-4 text-center">Unit</th>
<th className="py-2.5 px-4 text-right">Est. Qty</th>
<th className="py-2.5 px-4 text-right">Unit Cost</th>
<th className="py-2.5 px-4 text-right">Total Cost</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-150">
{project.materials_estimates && project.materials_estimates.length > 0 ? (
project.materials_estimates.map((est, idx) => (
<tr key={est.id} className="hover:bg-slate-50/20 transition-colors">
<td className="py-3 px-4 font-mono text-xs text-slate-400">{idx + 1}</td>
<td className="py-3 px-4 font-medium text-slate-800">
{est.material?.name || 'Unknown Material'}
{est.material?.sku && <span className="font-mono text-[10px] text-slate-400 ml-2">({est.material.sku})</span>}
</td>
<td className="py-3 px-4 text-slate-500 capitalize">{est.material?.category || '—'}</td>
<td className="py-3 px-4 text-center text-slate-500">{est.material?.unit}</td>
<td className="py-3 px-4 text-right font-medium tabular-nums">{parseFloat(est.estimated_qty as string).toLocaleString()}</td>
<td className="py-3 px-4 text-right text-slate-600 tabular-nums">{formatCurrency(est.unit_cost)}</td>
<td className="py-3 px-4 text-right font-semibold text-slate-800 tabular-nums">
{formatCurrency(parseFloat(est.estimated_qty as string) * parseFloat(est.unit_cost as string))}
</td>
</tr>
))
) : (
<tr>
<td colSpan={7} className="py-8 text-center text-slate-400 italic">No materials estimates configured for this project.</td>
</tr>
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* Manpower & Equipment Breakdown */}
<Card className="border-slate-200/80 shadow-sm">
<CardHeader className="border-b border-slate-100">
<CardTitle className="text-sm font-semibold text-slate-800 flex items-center gap-2">
<ClipboardList className="h-4 w-4 text-slate-500" /> Tasks & Resource Allocations
</CardTitle>
</CardHeader>
<CardContent className="p-6 space-y-6">
{project.tasks && project.tasks.length > 0 ? (
project.tasks.map((task) => {
const taskLaborSubtotal = task.task_labors?.reduce((sum, tl) => sum + (parseFloat(tl.estimated_hours as string) * parseFloat(tl.labor?.hourly_rate as string || '0')), 0) || 0;
const taskEquipmentSubtotal = task.task_equipments?.reduce((sum, te) => sum + (parseFloat(te.estimated_hours as string) * parseFloat(te.equipment?.hourly_rate as string || '0')), 0) || 0;
const totalTaskCost = taskLaborSubtotal + taskEquipmentSubtotal;
return (
<div key={task.id} className="border border-slate-200 rounded-xl overflow-hidden shadow-sm">
<div className="bg-slate-50/50 px-4 py-3 border-b border-slate-200 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div>
<h4 className="font-semibold text-sm text-slate-805">{task.name}</h4>
{task.description && <p className="text-xs text-slate-500 mt-0.5">{task.description}</p>}
</div>
<div className="text-right shrink-0">
<span className="text-xs text-slate-400 font-medium mr-2">Estimated Task Cost:</span>
<span className="font-bold text-sm text-slate-900">{formatCurrency(totalTaskCost)}</span>
</div>
</div>
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Manpower Allocations */}
<div>
<h5 className="text-xs font-semibold text-slate-700 flex items-center gap-1.5 mb-2 border-b border-slate-100 pb-1.5">
<Hammer className="h-3.5 w-3.5 text-orange-500" /> Manpower Allocation
</h5>
{task.task_labors && task.task_labors.length > 0 ? (
<div className="space-y-2.5">
{(() => {
const bundles: { [key: string]: any[] } = {};
const singleTrades: any[] = [];
task.task_labors.forEach((tl: any) => {
if (tl.allocation_type === 'bundle' && tl.bundle_name) {
if (!bundles[tl.bundle_name]) {
bundles[tl.bundle_name] = [];
}
bundles[tl.bundle_name].push(tl);
} else {
singleTrades.push(tl);
}
});
return (
<>
{/* Render Job Crew Bundles */}
{Object.entries(bundles).map(([bundleName, items]) => {
const first = items[0];
const bundleSubtotal = items.reduce((sum, tl) =>
sum + (parseFloat(tl.estimated_hours as string) * parseFloat(tl.labor?.hourly_rate as string || '0')), 0
);
const bundleUnit = first?.bundle_unit || 'm²';
const bundleQty = first?.bundle_quantity ? Number(first.bundle_quantity) : null;
const bundleRate = first?.bundle_unit_rate ? Number(first.bundle_unit_rate) : null;
return (
<div key={bundleName} className="p-2.5 bg-indigo-50/40 border border-indigo-100 rounded-lg space-y-2">
<div className="flex items-center justify-between gap-2 border-b border-indigo-100/70 pb-1.5">
<div className="flex items-center gap-1.5">
<Layers className="h-3.5 w-3.5 text-indigo-600 shrink-0" />
<div>
<span className="font-bold text-xs text-slate-900 block leading-tight">{bundleName}</span>
<div className="flex items-center gap-1.5 mt-0.5">
<Badge className="bg-indigo-100 text-indigo-800 border-indigo-200 text-[9px] px-1 py-0 font-semibold uppercase">
Crew Bundle
</Badge>
{bundleQty !== null && (
<span className="text-[10px] text-slate-500 font-medium">
{bundleQty} {bundleUnit} {bundleRate ? `@ ${formatCurrency(bundleRate)}/${bundleUnit}` : ''}
</span>
)}
</div>
</div>
</div>
<div className="text-right shrink-0">
<span className="font-bold text-xs text-indigo-950 font-mono block">
{formatCurrency(bundleSubtotal)}
</span>
</div>
</div>
{/* Itemized Trades Breakdown within bundle */}
<div className="pl-2 space-y-1 border-l-2 border-indigo-200">
{items.map((tl: any) => (
<div key={tl.id} className="flex justify-between items-center text-[11px] text-slate-600">
<span>
{tl.labor?.name} <span className="text-[10px] text-slate-400">({tl.labor?.category} {formatCurrency(tl.labor?.hourly_rate)}/hr)</span>
</span>
<span className="font-mono font-medium text-slate-700">
{tl.estimated_hours} hrs ({formatCurrency(parseFloat(tl.estimated_hours as string) * parseFloat(tl.labor?.hourly_rate as string || '0'))})
</span>
</div>
))}
</div>
</div>
);
})}
{/* Render Single Trades */}
{singleTrades.map((tl: any) => (
<div key={tl.id} className="flex justify-between items-center text-xs p-2 bg-slate-50/30 border border-slate-100 rounded-lg">
<div>
<div className="flex items-center gap-1.5">
<span className="font-medium text-slate-800">{tl.labor?.name}</span>
<Badge variant="outline" className="text-[9px] px-1 py-0 text-emerald-700 bg-emerald-50 border-emerald-100 font-normal uppercase">
Trade
</Badge>
</div>
<span className="text-[10px] text-slate-400 capitalize">{tl.labor?.category} {formatCurrency(tl.labor?.hourly_rate)}/hr</span>
</div>
<div className="text-right">
<span className="font-semibold text-slate-700 block">{tl.estimated_hours} hrs</span>
<span className="text-[10px] font-bold text-slate-550">{formatCurrency(parseFloat(tl.estimated_hours as string) * parseFloat(tl.labor?.hourly_rate as string || '0'))}</span>
</div>
</div>
))}
<div className="flex justify-between items-center text-xs font-semibold border-t border-slate-100 pt-2 px-1 text-slate-700">
<span>Labor Subtotal</span>
<span>{formatCurrency(taskLaborSubtotal)}</span>
</div>
</>
);
})()}
</div>
) : (
<p className="text-xs text-slate-400 italic py-2">No labor allocated to this task.</p>
)}
</div>
{/* Equipment Allocations */}
<div>
<h5 className="text-xs font-semibold text-slate-700 flex items-center gap-1.5 mb-2 border-b border-slate-100 pb-1.5">
<Truck className="h-3.5 w-3.5 text-indigo-500" /> Equipment Allocation
</h5>
{task.task_equipments && task.task_equipments.length > 0 ? (
<div className="space-y-2">
{task.task_equipments.map((te) => (
<div key={te.id} className="flex justify-between items-center text-xs p-2 bg-slate-50/30 border border-slate-100 rounded-lg">
<div>
<span className="font-medium text-slate-800 block">{te.equipment?.name}</span>
<span className="text-[10px] text-slate-450">{te.equipment?.owner_name || 'Unspecified'} {formatCurrency(te.equipment?.hourly_rate)}/hr</span>
</div>
<div className="text-right">
<span className="font-semibold text-slate-700 block">{te.estimated_hours} hrs</span>
<span className="text-[10px] font-bold text-slate-550">{formatCurrency(parseFloat(te.estimated_hours as string) * parseFloat(te.equipment?.hourly_rate as string || '0'))}</span>
</div>
</div>
))}
<div className="flex justify-between items-center text-xs font-semibold border-t border-slate-100 pt-2 px-1 text-slate-700">
<span>Equipment Subtotal</span>
<span>{formatCurrency(taskEquipmentSubtotal)}</span>
</div>
</div>
) : (
<p className="text-xs text-slate-400 italic py-2">No equipment allocated to this task.</p>
)}
</div>
</div>
</div>
);
})
) : (
<p className="text-sm text-slate-400 italic text-center py-4">No tasks found for this project.</p>
)}
</CardContent>
</Card>
{/* Milestone schedule weights */}
<Card className="border-slate-200/80 shadow-sm overflow-hidden">
<CardHeader className="bg-slate-50/50 border-b border-slate-100">
<CardTitle className="text-sm font-semibold text-slate-800 flex items-center gap-2">
<MilestoneIcon className="h-4 w-4 text-slate-500" /> Milestone Schedule Weights
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-150 font-medium text-slate-650">
<th className="py-2.5 px-4 w-12">#</th>
<th className="py-2.5 px-4">Milestone Phase</th>
<th className="py-2.5 px-4 text-right">Planned Weight</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-150">
{project.milestones && project.milestones.length > 0 ? (
project.milestones.map((m, idx) => (
<tr key={m.id} className="hover:bg-slate-50/20 transition-colors">
<td className="py-3 px-4 font-mono text-xs text-slate-400">{idx + 1}</td>
<td className="py-3 px-4 font-semibold text-slate-750">{m.name}</td>
<td className="py-3 px-4 text-right font-medium text-slate-800 tabular-nums">
{parseFloat(m.weight_percentage as string).toFixed(1)}%
</td>
</tr>
))
) : (
<tr>
<td colSpan={3} className="py-8 text-center text-slate-400 italic">No milestones defined.</td>
</tr>
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="progress" className="mt-0 space-y-6 focus-visible:outline-none">
{evmData ? (
<EvmSCurveChart evmData={evmData} />
) : (
<Card className="border-dashed border-2 bg-gray-50/50">
<CardHeader className="text-center pb-4">
<CardTitle className="text-xl">Progress Monitoring Baseline</CardTitle>
<CardDescription>
No EVM progress data is currently available for this project.
</CardDescription>
</CardHeader>
</Card>
)}
</TabsContent>
</Tabs>
</ProjectLayout>
);
}