159 lines
5.5 KiB
TypeScript
159 lines
5.5 KiB
TypeScript
import {
|
|
TrendingUp,
|
|
DollarSign,
|
|
Calendar,
|
|
Clock,
|
|
CheckCircle2,
|
|
ListTodo,
|
|
AlertTriangle
|
|
} from "lucide-react";
|
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { Project } from "@/types/project";
|
|
|
|
interface ProjectStatsProps {
|
|
project: Project;
|
|
taskStats: {
|
|
total: number;
|
|
completed: number;
|
|
inProgress: number;
|
|
highPriority: number;
|
|
overdue: number;
|
|
completionRate: number;
|
|
totalPlannedHours: number;
|
|
totalWorkedHours: number;
|
|
};
|
|
}
|
|
|
|
// Formatear moneda
|
|
function formatCurrency(amount: number): string {
|
|
return new Intl.NumberFormat("es-ES", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(amount);
|
|
}
|
|
|
|
// Formatear fecha
|
|
function formatDate(dateString: string): string {
|
|
return new Date(dateString).toLocaleDateString("es-ES", {
|
|
day: "numeric",
|
|
month: "short",
|
|
});
|
|
}
|
|
|
|
// Calcular días restantes
|
|
function getDaysRemaining(endDate: string): number {
|
|
const end = new Date(endDate);
|
|
const today = new Date();
|
|
const diff = end.getTime() - today.getTime();
|
|
return Math.ceil(diff / (1000 * 60 * 60 * 24));
|
|
}
|
|
|
|
export function ProjectStats({ project, taskStats }: ProjectStatsProps) {
|
|
const daysRemaining = getDaysRemaining(project.endDate);
|
|
const isOverdue = daysRemaining < 0;
|
|
const budgetUsedPercent = project.budget > 0 ? (project.spent / project.budget) * 100 : 0;
|
|
|
|
return (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{/* Progreso */}
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900/30">
|
|
<TrendingUp className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-muted-foreground">Progreso</p>
|
|
<p className="text-2xl font-bold">{project.progress}%</p>
|
|
</div>
|
|
</div>
|
|
<Progress value={project.progress} className="mt-3 h-1.5" />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Presupuesto */}
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-900/30">
|
|
<DollarSign className="h-5 w-5 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-muted-foreground">Presupuesto</p>
|
|
<p className="text-2xl font-bold">{formatCurrency(project.budget)}</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 flex items-center justify-between text-xs">
|
|
<span className="text-muted-foreground">
|
|
Usado: {formatCurrency(project.spent)}
|
|
</span>
|
|
<span className={budgetUsedPercent > 90 ? "text-red-500" : "text-muted-foreground"}>
|
|
{budgetUsedPercent.toFixed(0)}%
|
|
</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Fecha límite */}
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`p-2 rounded-lg ${isOverdue ? "bg-red-100 dark:bg-red-900/30" : "bg-purple-100 dark:bg-purple-900/30"}`}>
|
|
<Calendar className={`h-5 w-5 ${isOverdue ? "text-red-600 dark:text-red-400" : "text-purple-600 dark:text-purple-400"}`} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-muted-foreground">Fecha límite</p>
|
|
<p className="text-2xl font-bold">{formatDate(project.endDate)}</p>
|
|
</div>
|
|
</div>
|
|
<p className={`mt-3 text-xs ${isOverdue ? "text-red-500" : "text-muted-foreground"}`}>
|
|
{isOverdue
|
|
? `${Math.abs(daysRemaining)} días de retraso`
|
|
: `${daysRemaining} días restantes`
|
|
}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Tareas */}
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded-lg bg-orange-100 dark:bg-orange-900/30">
|
|
<ListTodo className="h-5 w-5 text-orange-600 dark:text-orange-400" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-muted-foreground">Tareas</p>
|
|
<p className="text-2xl font-bold">{taskStats.completed}/{taskStats.total}</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 flex items-center gap-3 text-xs">
|
|
{taskStats.highPriority > 0 && (
|
|
<span className="flex items-center gap-1 text-red-500">
|
|
<AlertTriangle className="h-3 w-3" />
|
|
{taskStats.highPriority} alta prioridad
|
|
</span>
|
|
)}
|
|
{taskStats.overdue > 0 && (
|
|
<span className="flex items-center gap-1 text-yellow-600 dark:text-yellow-500">
|
|
<Clock className="h-3 w-3" />
|
|
{taskStats.overdue} vencidas
|
|
</span>
|
|
)}
|
|
{taskStats.highPriority === 0 && taskStats.overdue === 0 && (
|
|
<span className="flex items-center gap-1 text-green-600 dark:text-green-500">
|
|
<CheckCircle2 className="h-3 w-3" />
|
|
Todo en orden
|
|
</span>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|