192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import {
|
||
|
|
Target,
|
||
|
|
Clock,
|
||
|
|
Calendar,
|
||
|
|
TrendingUp,
|
||
|
|
FileText,
|
||
|
|
AlertTriangle,
|
||
|
|
CheckCircle2
|
||
|
|
} from "lucide-react";
|
||
|
|
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import { Progress } from "@/components/ui/progress";
|
||
|
|
import { Task } from "@/types/task";
|
||
|
|
|
||
|
|
interface TaskStatsProps {
|
||
|
|
task: Task;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Formatear fecha completa
|
||
|
|
function formatDate(dateString: string | null): string {
|
||
|
|
if (!dateString) return 'No definida';
|
||
|
|
return new Date(dateString).toLocaleDateString('es-ES', {
|
||
|
|
day: '2-digit',
|
||
|
|
month: 'long',
|
||
|
|
year: 'numeric',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Obtener clase de color según progreso
|
||
|
|
function getProgressColorClass(value: number): string {
|
||
|
|
if (value >= 100) return '[&>div]:bg-green-500';
|
||
|
|
if (value >= 75) return '[&>div]:bg-blue-500';
|
||
|
|
if (value >= 50) return '[&>div]:bg-yellow-500';
|
||
|
|
if (value >= 25) return '[&>div]:bg-orange-500';
|
||
|
|
return '[&>div]:bg-gray-400';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calcular si está vencida
|
||
|
|
function isOverdue(task: Task): boolean {
|
||
|
|
const endDate = task.endDate || task.plannedEndDate;
|
||
|
|
if (!endDate) return false;
|
||
|
|
return new Date(endDate) < new Date() && task.status !== '2';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calcular días restantes
|
||
|
|
function getDaysRemaining(task: Task): { days: number; label: string } | null {
|
||
|
|
const endDate = task.endDate || task.plannedEndDate;
|
||
|
|
if (!endDate) return null;
|
||
|
|
|
||
|
|
const end = new Date(endDate);
|
||
|
|
const today = new Date();
|
||
|
|
today.setHours(0, 0, 0, 0);
|
||
|
|
end.setHours(0, 0, 0, 0);
|
||
|
|
|
||
|
|
const diffTime = end.getTime() - today.getTime();
|
||
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||
|
|
|
||
|
|
if (diffDays < 0) {
|
||
|
|
return { days: Math.abs(diffDays), label: `${Math.abs(diffDays)} días de retraso` };
|
||
|
|
} else if (diffDays === 0) {
|
||
|
|
return { days: 0, label: 'Vence hoy' };
|
||
|
|
} else if (diffDays === 1) {
|
||
|
|
return { days: 1, label: 'Vence mañana' };
|
||
|
|
} else {
|
||
|
|
return { days: diffDays, label: `${diffDays} días restantes` };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function TaskStats({ task }: TaskStatsProps) {
|
||
|
|
const overdue = isOverdue(task);
|
||
|
|
const daysInfo = getDaysRemaining(task);
|
||
|
|
const efficiency = task.plannedHours > 0
|
||
|
|
? Math.round((task.workedHours / task.plannedHours) * 100)
|
||
|
|
: 0;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||
|
|
{/* Progreso */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">Progreso</CardTitle>
|
||
|
|
<Target className="h-4 w-4 text-muted-foreground" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">{task.progress}%</div>
|
||
|
|
<Progress
|
||
|
|
value={task.progress}
|
||
|
|
className={`mt-2 h-2 ${getProgressColorClass(task.progress)}`}
|
||
|
|
/>
|
||
|
|
<p className="text-xs text-muted-foreground mt-2">
|
||
|
|
{task.progress >= 100 ? (
|
||
|
|
<span className="flex items-center gap-1 text-green-600">
|
||
|
|
<CheckCircle2 className="h-3 w-3" />
|
||
|
|
Tarea completada
|
||
|
|
</span>
|
||
|
|
) : task.progress > 0 ? (
|
||
|
|
'En progreso'
|
||
|
|
) : (
|
||
|
|
'Sin iniciar'
|
||
|
|
)}
|
||
|
|
</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Tiempo */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">Tiempo</CardTitle>
|
||
|
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">
|
||
|
|
{task.workedHours > 0 ? `${task.workedHours}h` : '-'}
|
||
|
|
</div>
|
||
|
|
{task.plannedHours > 0 && (
|
||
|
|
<Progress
|
||
|
|
value={Math.min(efficiency, 100)}
|
||
|
|
className={`mt-2 h-2 ${efficiency > 100 ? '[&>div]:bg-red-500' : '[&>div]:bg-blue-500'}`}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
<p className="text-xs text-muted-foreground mt-2">
|
||
|
|
{task.plannedHours > 0 ? (
|
||
|
|
<>
|
||
|
|
de {task.plannedHours}h planificadas
|
||
|
|
{efficiency > 100 && (
|
||
|
|
<span className="text-red-500 ml-1">({efficiency}%)</span>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
'Sin estimación'
|
||
|
|
)}
|
||
|
|
</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Fecha límite */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">Fecha límite</CardTitle>
|
||
|
|
<Calendar className={`h-4 w-4 ${overdue ? 'text-red-500' : 'text-muted-foreground'}`} />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className={`text-2xl font-bold ${overdue ? 'text-red-500' : ''}`}>
|
||
|
|
{formatDate(task.endDate || task.plannedEndDate).split(' de ')[0] || '-'}
|
||
|
|
</div>
|
||
|
|
<p className={`text-xs mt-2 ${overdue ? 'text-red-500' : 'text-muted-foreground'}`}>
|
||
|
|
{daysInfo ? (
|
||
|
|
<span className="flex items-center gap-1">
|
||
|
|
{overdue && <AlertTriangle className="h-3 w-3" />}
|
||
|
|
{daysInfo.label}
|
||
|
|
</span>
|
||
|
|
) : (
|
||
|
|
'Sin fecha límite'
|
||
|
|
)}
|
||
|
|
</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Eficiencia / Estado */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">Rendimiento</CardTitle>
|
||
|
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">
|
||
|
|
{task.plannedHours > 0 && task.workedHours > 0 ? (
|
||
|
|
`${Math.round((task.progress / efficiency) * 100)}%`
|
||
|
|
) : task.progress > 0 ? (
|
||
|
|
'Activa'
|
||
|
|
) : (
|
||
|
|
'-'
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<p className="text-xs text-muted-foreground mt-2">
|
||
|
|
{task.plannedHours > 0 && task.workedHours > 0 ? (
|
||
|
|
'Progreso vs tiempo invertido'
|
||
|
|
) : (
|
||
|
|
<span className="flex items-center gap-1">
|
||
|
|
<FileText className="h-3 w-3" />
|
||
|
|
{task.description ? 'Con descripción' : 'Sin descripción'}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|