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

203 lines
13 KiB
TypeScript

import ProjectLayout from '../../Layouts/ProjectLayout';
import { Card, CardContent } from '@/Components/ui/card';
import { MapPin, Users, TrendingUp, DollarSign, Calendar } from 'lucide-react';
import { PageProps } from '@/types';
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;
}
interface StatusOption { value: string; label: string }
interface Props extends PageProps {
project: ProjectData;
taskStats: { total: number; pending: number; in_progress: number; completed: number };
allowedTransitions: StatusOption[];
}
const formatCurrency = (v: string) => 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 }: Props) {
return (
<ProjectLayout project={project} allowedTransitions={allowedTransitions} currentTab="overview">
{/* Stat Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-6">
<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>
{/* 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}</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 / taskStats.total) * 100}%` : '0%' }}
/>
</div>
<p className="text-xs text-gray-400 mt-1">
{taskStats.total > 0 ? `${((taskStats.completed / 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.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: '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>
</ProjectLayout>
);
}