trello_fake/components/project-detail/tasks-section.tsx

313 lines
11 KiB
TypeScript

"use client";
import { useState, useMemo } from "react";
import {
Search,
X,
Plus,
ListTodo,
CheckCircle2,
Clock,
AlertTriangle,
LayoutGrid,
List
} from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { DataTable } from "@/components/projects/data-table";
import { taskColumns } from "./task-columns";
import { Task, TASK_STATUS_CONFIG, TASK_PRIORITY_CONFIG } from "@/types/task";
interface TasksSectionProps {
tasks: Task[];
isLoading?: boolean;
}
// Tarjeta de tarea para vista grid
function TaskCard({ task }: { task: Task }) {
const statusConfig = TASK_STATUS_CONFIG[task.status];
const priorityConfig = TASK_PRIORITY_CONFIG[task.priority];
const isOverdue = (() => {
const endDate = task.endDate || task.plannedEndDate;
return endDate && new Date(endDate) < new Date() && task.status !== '2';
})();
return (
<Card className={`hover:shadow-md transition-shadow ${isOverdue ? 'border-red-200 dark:border-red-900' : ''}`}>
<CardContent className="p-4">
<div className="space-y-3">
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{task.title}</p>
{task.ref && (
<p className="text-xs text-muted-foreground font-mono">{task.ref}</p>
)}
</div>
{task.priority !== '0' && (
<Badge variant="outline" className={priorityConfig.bgClass}>
{task.priority === '3' && <AlertTriangle className="h-3 w-3 mr-1" />}
{priorityConfig.label}
</Badge>
)}
</div>
{/* Progreso */}
<div className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">Progreso</span>
<span className="font-medium">{task.progress}%</span>
</div>
<div className="w-full h-1.5 bg-muted rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
task.progress >= 100 ? 'bg-green-500' :
task.progress >= 50 ? 'bg-blue-500' : 'bg-gray-400'
}`}
style={{ width: `${task.progress}%` }}
/>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-between pt-2">
<Badge variant="outline" className={statusConfig.bgClass}>
{task.status === '2' && <CheckCircle2 className="h-3 w-3 mr-1" />}
{statusConfig.label}
</Badge>
{(task.endDate || task.plannedEndDate) && (
<span className={`text-xs flex items-center gap-1 ${isOverdue ? 'text-red-500' : 'text-muted-foreground'}`}>
<Clock className="h-3 w-3" />
{new Date(task.endDate || task.plannedEndDate!).toLocaleDateString('es-ES', {
day: '2-digit',
month: 'short',
})}
{isOverdue && <AlertTriangle className="h-3 w-3" />}
</span>
)}
</div>
</div>
</CardContent>
</Card>
);
}
// Estado vacío
function EmptyTasks() {
return (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="p-3 rounded-full bg-muted mb-4">
<ListTodo className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-1">Sin tareas</h3>
<p className="text-sm text-muted-foreground mb-4 max-w-sm">
Este proyecto aún no tiene tareas asignadas. Crea la primera tarea para comenzar.
</p>
<Button>
<Plus className="h-4 w-4 mr-2" />
Crear tarea
</Button>
</div>
);
}
export function TasksSection({ tasks, isLoading }: TasksSectionProps) {
const [searchValue, setSearchValue] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [priorityFilter, setPriorityFilter] = useState("all");
const [viewMode, setViewMode] = useState<"table" | "grid">("table");
// Filtrar tareas
const filteredTasks = useMemo(() => {
let result = [...tasks];
// Búsqueda
if (searchValue) {
const search = searchValue.toLowerCase();
result = result.filter(
(task) =>
task.title.toLowerCase().includes(search) ||
task.ref?.toLowerCase().includes(search) ||
task.description?.toLowerCase().includes(search)
);
}
// Filtro de estado
if (statusFilter !== "all") {
result = result.filter((task) => task.status === statusFilter);
}
// Filtro de prioridad
if (priorityFilter !== "all") {
result = result.filter((task) => task.priority === priorityFilter);
}
return result;
}, [tasks, searchValue, statusFilter, priorityFilter]);
// Estadísticas rápidas
const stats = useMemo(() => ({
total: tasks.length,
completed: tasks.filter(t => t.status === '2').length,
inProgress: tasks.filter(t => t.status === '1').length,
highPriority: tasks.filter(t => t.priority === '3').length,
}), [tasks]);
// Limpiar filtros
const handleClearFilters = () => {
setSearchValue("");
setStatusFilter("all");
setPriorityFilter("all");
};
const isFiltered = searchValue !== "" || statusFilter !== "all" || priorityFilter !== "all";
if (tasks.length === 0 && !isLoading) {
return <EmptyTasks />;
}
return (
<div className="space-y-4">
{/* Stats rápidos */}
<div className="flex flex-wrap items-center gap-4 text-sm">
<div className="flex items-center gap-2">
<ListTodo className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Total:</span>
<span className="font-medium">{stats.total}</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span className="text-muted-foreground">Completadas:</span>
<span className="font-medium">{stats.completed}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-blue-500" />
<span className="text-muted-foreground">En progreso:</span>
<span className="font-medium">{stats.inProgress}</span>
</div>
{stats.highPriority > 0 && (
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-red-500" />
<span className="text-muted-foreground">Alta prioridad:</span>
<span className="font-medium text-red-500">{stats.highPriority}</span>
</div>
)}
</div>
{/* Toolbar */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-1 items-center gap-2">
{/* Búsqueda */}
<div className="relative w-full sm:w-[250px]">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar tareas..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
className="pl-8 h-9"
/>
</div>
{/* Filtro estado */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[130px] h-9">
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="0">Borrador</SelectItem>
<SelectItem value="1">Validada</SelectItem>
<SelectItem value="2">Completada</SelectItem>
</SelectContent>
</Select>
{/* Filtro prioridad */}
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger className="w-[130px] h-9">
<SelectValue placeholder="Prioridad" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas</SelectItem>
<SelectItem value="3">Alta</SelectItem>
<SelectItem value="2">Media</SelectItem>
<SelectItem value="1">Baja</SelectItem>
<SelectItem value="0">Sin prioridad</SelectItem>
</SelectContent>
</Select>
{/* Limpiar */}
{isFiltered && (
<Button variant="ghost" size="sm" onClick={handleClearFilters} className="h-9 px-2">
Limpiar
<X className="ml-1 h-4 w-4" />
</Button>
)}
</div>
<div className="flex items-center gap-2">
{/* Toggle vista */}
<div className="flex items-center border rounded-md">
<Button
variant={viewMode === "table" ? "secondary" : "ghost"}
size="sm"
className="h-8 px-2 rounded-r-none"
onClick={() => setViewMode("table")}
>
<List className="h-4 w-4" />
</Button>
<Button
variant={viewMode === "grid" ? "secondary" : "ghost"}
size="sm"
className="h-8 px-2 rounded-l-none"
onClick={() => setViewMode("grid")}
>
<LayoutGrid className="h-4 w-4" />
</Button>
</div>
{/* Nueva tarea */}
<Button size="sm" className="h-9">
<Plus className="h-4 w-4 mr-2" />
Nueva tarea
</Button>
</div>
</div>
{/* Contenido */}
{viewMode === "table" ? (
<DataTable columns={taskColumns} data={filteredTasks} />
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredTasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</div>
)}
{/* Mensaje sin resultados */}
{filteredTasks.length === 0 && tasks.length > 0 && (
<div className="text-center py-8">
<p className="text-muted-foreground">
No se encontraron tareas con los filtros aplicados.
</p>
<Button variant="link" onClick={handleClearFilters}>
Limpiar filtros
</Button>
</div>
)}
</div>
);
}