165 lines
9.3 KiB
TypeScript
165 lines
9.3 KiB
TypeScript
import React from 'react';
|
|
import { Card, CardContent } from '@/Components/ui/card';
|
|
import { MapPin, Users } from 'lucide-react';
|
|
import { ProjectData } from '../../../types/project-show';
|
|
|
|
interface OverviewTabProps {
|
|
project: ProjectData;
|
|
taskStats: { total: number; pending: number; in_progress: number; completed: number; closed?: number };
|
|
formatCurrency: (v: string | number) => string;
|
|
}
|
|
|
|
export default function OverviewTab({ project, taskStats, formatCurrency }: OverviewTabProps) {
|
|
return (
|
|
<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 || 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>
|
|
);
|
|
}
|