trello_fake/components/task-detail/task-stats.tsx

192 lines
6.3 KiB
TypeScript
Raw Normal View History

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