trello_fake/components/project-detail/task-columns.tsx

284 lines
8.1 KiB
TypeScript
Raw Normal View History

"use client";
import { ColumnDef } from "@tanstack/react-table";
import {
ArrowUpDown,
MoreHorizontal,
Eye,
Pencil,
Trash2,
Clock,
AlertTriangle,
CheckCircle2
} from "lucide-react";
import Link from "next/link";
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`;
}
// Interfaz para callbacks de acciones
export interface TaskColumnCallbacks {
onEdit?: (task: Task) => void;
onDelete?: (task: Task) => void;
}
// Función para crear columnas con callbacks opcionales
export function createTaskColumns(callbacks?: TaskColumnCallbacks): ColumnDef<Task>[] {
return [
// 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]">
<Link
href={`/tareas/${row.original.id}`}
className="font-medium truncate hover:text-blue-600 hover:underline transition-colors block"
>
{row.getValue("title")}
</Link>
{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 }) => {
const task = row.original;
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 asChild>
<Link href={`/tareas/${task.id}`} className="cursor-pointer">
<Eye className="mr-2 h-4 w-4" />
Ver detalles
</Link>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => callbacks?.onEdit?.(task)}
disabled={!callbacks?.onEdit}
>
<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"
onClick={() => callbacks?.onDelete?.(task)}
disabled={!callbacks?.onDelete}
>
<Trash2 className="mr-2 h-4 w-4" />
Eliminar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
}
// Columnas por defecto sin callbacks (para compatibilidad)
export const taskColumns: ColumnDef<Task>[] = createTaskColumns();