255 lines
6.8 KiB
TypeScript
255 lines
6.8 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { ColumnDef } from "@tanstack/react-table";
|
||
|
|
import {
|
||
|
|
ArrowUpDown,
|
||
|
|
MoreHorizontal,
|
||
|
|
Eye,
|
||
|
|
Pencil,
|
||
|
|
Trash2,
|
||
|
|
Clock,
|
||
|
|
AlertTriangle,
|
||
|
|
CheckCircle2
|
||
|
|
} from "lucide-react";
|
||
|
|
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
||
|
|
import { Progress } from "@/components/ui/progress";
|
||
|
|
import {
|
||
|
|
DropdownMenu,
|
||
|
|
DropdownMenuContent,
|
||
|
|
DropdownMenuItem,
|
||
|
|
DropdownMenuLabel,
|
||
|
|
DropdownMenuSeparator,
|
||
|
|
DropdownMenuTrigger,
|
||
|
|
} from "@/components/ui/dropdown-menu";
|
||
|
|
import {
|
||
|
|
Task,
|
||
|
|
TaskStatus,
|
||
|
|
TaskPriority,
|
||
|
|
TASK_STATUS_CONFIG,
|
||
|
|
TASK_PRIORITY_CONFIG
|
||
|
|
} from "@/types/task";
|
||
|
|
|
||
|
|
// Badge de estado
|
||
|
|
function TaskStatusBadge({ status }: { status: TaskStatus }) {
|
||
|
|
const config = TASK_STATUS_CONFIG[status];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Badge variant="outline" className={config.bgClass}>
|
||
|
|
{status === '2' && <CheckCircle2 className="h-3 w-3 mr-1" />}
|
||
|
|
{config.label}
|
||
|
|
</Badge>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Badge de prioridad
|
||
|
|
function TaskPriorityBadge({ priority }: { priority: TaskPriority }) {
|
||
|
|
const config = TASK_PRIORITY_CONFIG[priority];
|
||
|
|
|
||
|
|
if (priority === '0') {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Badge variant="outline" className={config.bgClass}>
|
||
|
|
{priority === '3' && <AlertTriangle className="h-3 w-3 mr-1" />}
|
||
|
|
{config.label}
|
||
|
|
</Badge>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Barra de progreso compacta
|
||
|
|
function TaskProgress({ progress }: { progress: number }) {
|
||
|
|
const getColorClass = (value: number) => {
|
||
|
|
if (value >= 100) return '[&>div]:bg-green-500';
|
||
|
|
if (value >= 75) return '[&>div]:bg-blue-500';
|
||
|
|
if (value >= 50) return '[&>div]:bg-yellow-500';
|
||
|
|
return '[&>div]:bg-gray-400';
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Progress value={progress} className={`w-16 h-1.5 ${getColorClass(progress)}`} />
|
||
|
|
<span className="text-xs text-muted-foreground w-8">{progress}%</span>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Formatear fecha
|
||
|
|
function formatDate(dateString: string | null): string {
|
||
|
|
if (!dateString) return '-';
|
||
|
|
return new Date(dateString).toLocaleDateString('es-ES', {
|
||
|
|
day: '2-digit',
|
||
|
|
month: 'short',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Formatear horas
|
||
|
|
function formatHours(hours: number): string {
|
||
|
|
if (hours === 0) return '-';
|
||
|
|
return `${hours}h`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const taskColumns: ColumnDef<Task>[] = [
|
||
|
|
// Checkbox
|
||
|
|
{
|
||
|
|
id: "select",
|
||
|
|
header: ({ table }) => (
|
||
|
|
<Checkbox
|
||
|
|
checked={
|
||
|
|
table.getIsAllPageRowsSelected() ||
|
||
|
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||
|
|
}
|
||
|
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||
|
|
aria-label="Seleccionar todo"
|
||
|
|
/>
|
||
|
|
),
|
||
|
|
cell: ({ row }) => (
|
||
|
|
<Checkbox
|
||
|
|
checked={row.getIsSelected()}
|
||
|
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||
|
|
aria-label="Seleccionar fila"
|
||
|
|
/>
|
||
|
|
),
|
||
|
|
enableSorting: false,
|
||
|
|
enableHiding: false,
|
||
|
|
},
|
||
|
|
// Título
|
||
|
|
{
|
||
|
|
accessorKey: "title",
|
||
|
|
header: ({ column }) => (
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
className="-ml-3 h-8"
|
||
|
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||
|
|
>
|
||
|
|
Tarea
|
||
|
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
),
|
||
|
|
cell: ({ row }) => (
|
||
|
|
<div className="max-w-[300px]">
|
||
|
|
<p className="font-medium truncate">{row.getValue("title")}</p>
|
||
|
|
{row.original.ref && (
|
||
|
|
<p className="text-xs text-muted-foreground font-mono">{row.original.ref}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
// Estado
|
||
|
|
{
|
||
|
|
accessorKey: "status",
|
||
|
|
header: "Estado",
|
||
|
|
cell: ({ row }) => <TaskStatusBadge status={row.getValue("status")} />,
|
||
|
|
filterFn: (row, id, value) => value.includes(row.getValue(id)),
|
||
|
|
},
|
||
|
|
// Prioridad
|
||
|
|
{
|
||
|
|
accessorKey: "priority",
|
||
|
|
header: "Prioridad",
|
||
|
|
cell: ({ row }) => <TaskPriorityBadge priority={row.getValue("priority")} />,
|
||
|
|
filterFn: (row, id, value) => value.includes(row.getValue(id)),
|
||
|
|
},
|
||
|
|
// Progreso
|
||
|
|
{
|
||
|
|
accessorKey: "progress",
|
||
|
|
header: ({ column }) => (
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
className="-ml-3 h-8"
|
||
|
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||
|
|
>
|
||
|
|
Progreso
|
||
|
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
),
|
||
|
|
cell: ({ row }) => <TaskProgress progress={row.getValue("progress")} />,
|
||
|
|
},
|
||
|
|
// Horas
|
||
|
|
{
|
||
|
|
id: "hours",
|
||
|
|
header: () => (
|
||
|
|
<div className="flex items-center gap-1">
|
||
|
|
<Clock className="h-3.5 w-3.5" />
|
||
|
|
Horas
|
||
|
|
</div>
|
||
|
|
),
|
||
|
|
cell: ({ row }) => (
|
||
|
|
<div className="text-sm">
|
||
|
|
<span className="font-medium">{formatHours(row.original.workedHours)}</span>
|
||
|
|
{row.original.plannedHours > 0 && (
|
||
|
|
<span className="text-muted-foreground"> / {formatHours(row.original.plannedHours)}</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
// Fecha límite
|
||
|
|
{
|
||
|
|
accessorKey: "endDate",
|
||
|
|
header: ({ column }) => (
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
className="-ml-3 h-8"
|
||
|
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||
|
|
>
|
||
|
|
Fecha límite
|
||
|
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
),
|
||
|
|
cell: ({ row }) => {
|
||
|
|
const endDate = row.original.endDate || row.original.plannedEndDate;
|
||
|
|
const isOverdue = endDate && new Date(endDate) < new Date() && row.original.status !== '2';
|
||
|
|
|
||
|
|
return (
|
||
|
|
<span className={`text-sm ${isOverdue ? 'text-red-500 font-medium' : 'text-muted-foreground'}`}>
|
||
|
|
{formatDate(endDate)}
|
||
|
|
{isOverdue && <AlertTriangle className="h-3 w-3 ml-1 inline" />}
|
||
|
|
</span>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
// Acciones
|
||
|
|
{
|
||
|
|
id: "actions",
|
||
|
|
enableHiding: false,
|
||
|
|
cell: ({ row }) => {
|
||
|
|
return (
|
||
|
|
<DropdownMenu>
|
||
|
|
<DropdownMenuTrigger asChild>
|
||
|
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||
|
|
<span className="sr-only">Abrir menú</span>
|
||
|
|
<MoreHorizontal className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent align="end">
|
||
|
|
<DropdownMenuLabel>Acciones</DropdownMenuLabel>
|
||
|
|
<DropdownMenuSeparator />
|
||
|
|
<DropdownMenuItem>
|
||
|
|
<Eye className="mr-2 h-4 w-4" />
|
||
|
|
Ver detalles
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem>
|
||
|
|
<Pencil className="mr-2 h-4 w-4" />
|
||
|
|
Editar
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem>
|
||
|
|
<Clock className="mr-2 h-4 w-4" />
|
||
|
|
Registrar tiempo
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuSeparator />
|
||
|
|
<DropdownMenuItem className="text-destructive focus:text-destructive">
|
||
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
||
|
|
Eliminar
|
||
|
|
</DropdownMenuItem>
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|