Files
Ajjj b98951f65f
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
feat: decompose project god components, add database performance indexes, and isolate contractor admin retention
2026-08-14 16:20:15 +08:00

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>
);
}