203 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|