304 lines
8.6 KiB
TypeScript
304 lines
8.6 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
FileText,
|
|
Calendar,
|
|
User,
|
|
FolderOpen,
|
|
Tag,
|
|
Clock,
|
|
AlertTriangle,
|
|
CheckCircle2,
|
|
Info,
|
|
Hash
|
|
} from "lucide-react";
|
|
import Link from "next/link";
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import {
|
|
Task,
|
|
TASK_STATUS_CONFIG,
|
|
TASK_PRIORITY_CONFIG
|
|
} from "@/types/task";
|
|
import { Project } from "@/types/project";
|
|
|
|
interface TaskInfoProps {
|
|
task: Task;
|
|
project: Project | null;
|
|
}
|
|
|
|
// Formatear fecha completa
|
|
function formatFullDate(dateString: string | null): string {
|
|
if (!dateString) return 'No definida';
|
|
return new Date(dateString).toLocaleDateString('es-ES', {
|
|
weekday: 'long',
|
|
day: '2-digit',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
});
|
|
}
|
|
|
|
// Verificar si la tarea 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';
|
|
}
|
|
|
|
// Item de información
|
|
function InfoItem({
|
|
icon: Icon,
|
|
label,
|
|
value,
|
|
className = "",
|
|
valueClassName = ""
|
|
}: {
|
|
icon: React.ElementType;
|
|
label: string;
|
|
value: React.ReactNode;
|
|
className?: string;
|
|
valueClassName?: string;
|
|
}) {
|
|
return (
|
|
<div className={`flex items-start gap-3 ${className}`}>
|
|
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-muted/50 shrink-0">
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-0.5">{label}</p>
|
|
<div className={`text-sm font-medium ${valueClassName}`}>{value}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function TaskInfo({ task, project }: TaskInfoProps) {
|
|
const overdue = isOverdue(task);
|
|
const statusConfig = TASK_STATUS_CONFIG[task.status];
|
|
const priorityConfig = TASK_PRIORITY_CONFIG[task.priority];
|
|
|
|
return (
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
{/* Descripción */}
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<FileText className="h-5 w-5" />
|
|
Descripción
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{task.description ? (
|
|
<p className="text-sm text-muted-foreground whitespace-pre-wrap leading-relaxed">
|
|
{task.description}
|
|
</p>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground/60 italic">
|
|
Esta tarea no tiene descripción.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Información General */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<Info className="h-5 w-5" />
|
|
Información General
|
|
</CardTitle>
|
|
<CardDescription>Detalles básicos de la tarea</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<InfoItem
|
|
icon={Hash}
|
|
label="Referencia"
|
|
value={<span className="font-mono">{task.ref}</span>}
|
|
/>
|
|
|
|
<Separator />
|
|
|
|
<InfoItem
|
|
icon={Tag}
|
|
label="Estado"
|
|
value={
|
|
<Badge variant="outline" className={statusConfig.bgClass}>
|
|
{task.status === '2' && <CheckCircle2 className="h-3 w-3 mr-1" />}
|
|
{statusConfig.label}
|
|
</Badge>
|
|
}
|
|
/>
|
|
|
|
<InfoItem
|
|
icon={AlertTriangle}
|
|
label="Prioridad"
|
|
value={
|
|
task.priority !== '0' ? (
|
|
<Badge variant="outline" className={priorityConfig.bgClass}>
|
|
{priorityConfig.label}
|
|
</Badge>
|
|
) : (
|
|
<span className="text-muted-foreground">Sin prioridad asignada</span>
|
|
)
|
|
}
|
|
/>
|
|
|
|
<Separator />
|
|
|
|
<InfoItem
|
|
icon={FolderOpen}
|
|
label="Proyecto"
|
|
value={
|
|
project ? (
|
|
<Link
|
|
href={`/proyectos/${project.id}`}
|
|
className="text-blue-600 hover:text-blue-700 hover:underline transition-colors"
|
|
>
|
|
{project.name}
|
|
</Link>
|
|
) : (
|
|
<span className="text-muted-foreground">Proyecto #{task.projectId}</span>
|
|
)
|
|
}
|
|
/>
|
|
|
|
{task.parentTaskId && task.parentTaskId > 0 && (
|
|
<InfoItem
|
|
icon={FolderOpen}
|
|
label="Tarea padre"
|
|
value={
|
|
<Link
|
|
href={`/tareas/${task.parentTaskId}`}
|
|
className="text-blue-600 hover:text-blue-700 hover:underline transition-colors"
|
|
>
|
|
Ver tarea padre #{task.parentTaskId}
|
|
</Link>
|
|
}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Fechas y Tiempos */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<Calendar className="h-5 w-5" />
|
|
Fechas y Tiempos
|
|
</CardTitle>
|
|
<CardDescription>Cronograma y dedicación</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<InfoItem
|
|
icon={Calendar}
|
|
label="Fecha de creación"
|
|
value={formatFullDate(task.createdAt)}
|
|
/>
|
|
|
|
<Separator />
|
|
|
|
<InfoItem
|
|
icon={Calendar}
|
|
label="Inicio planificado"
|
|
value={formatFullDate(task.plannedStartDate)}
|
|
/>
|
|
|
|
<InfoItem
|
|
icon={Calendar}
|
|
label="Fin planificado"
|
|
value={formatFullDate(task.plannedEndDate)}
|
|
valueClassName={overdue ? 'text-red-500' : ''}
|
|
/>
|
|
|
|
{(task.startDate || task.endDate) && (
|
|
<>
|
|
<Separator />
|
|
|
|
{task.startDate && (
|
|
<InfoItem
|
|
icon={Calendar}
|
|
label="Inicio real"
|
|
value={formatFullDate(task.startDate)}
|
|
/>
|
|
)}
|
|
|
|
{task.endDate && (
|
|
<InfoItem
|
|
icon={Calendar}
|
|
label="Fin real"
|
|
value={formatFullDate(task.endDate)}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<Separator />
|
|
|
|
<InfoItem
|
|
icon={Clock}
|
|
label="Tiempo planificado"
|
|
value={task.plannedHours > 0 ? `${task.plannedHours} horas` : 'No estimado'}
|
|
/>
|
|
|
|
<InfoItem
|
|
icon={Clock}
|
|
label="Tiempo trabajado"
|
|
value={task.workedHours > 0 ? `${task.workedHours} horas` : 'Sin registrar'}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Metadatos */}
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<User className="h-5 w-5" />
|
|
Metadatos
|
|
</CardTitle>
|
|
<CardDescription>Información de seguimiento y auditoría</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<InfoItem
|
|
icon={User}
|
|
label="Creado por"
|
|
value={task.createdBy > 0 ? `Usuario #${task.createdBy}` : 'Sistema'}
|
|
/>
|
|
|
|
<InfoItem
|
|
icon={Calendar}
|
|
label="Creado"
|
|
value={formatFullDate(task.createdAt)}
|
|
/>
|
|
|
|
<InfoItem
|
|
icon={Calendar}
|
|
label="Última actualización"
|
|
value={formatFullDate(task.updatedAt)}
|
|
/>
|
|
|
|
<InfoItem
|
|
icon={Hash}
|
|
label="Orden"
|
|
value={task.order > 0 ? `Posición ${task.order}` : 'Sin ordenar'}
|
|
/>
|
|
</div>
|
|
|
|
{task.budget > 0 && (
|
|
<>
|
|
<Separator className="my-4" />
|
|
<InfoItem
|
|
icon={Tag}
|
|
label="Presupuesto asignado"
|
|
value={`${task.budget.toLocaleString('es-ES')} €`}
|
|
/>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|