Checklist de Subtareas
Exportar contenido
Gestiona listas de verificación dentro de tareas como subtareas completas.
Características
Sección titulada «Características»- Agregar items: Crear nuevas subtareas rápidamente
- Marcar completadas: Checkbox para cambiar estado
- Editar título: Click en el texto para editar in-place
- Eliminar items: Botón de eliminar visible al hover
- Reordenar: Arrastrar y soltar para cambiar orden (visual con GripVertical)
- Progreso visual: Barra de progreso y porcentaje
- Heredar proyecto: Las subtareas heredan automáticamente el proyecto de la tarea padre
Progreso Automático
Sección titulada «Progreso Automático»El progreso se calcula automáticamente:
Progreso = (Items Completados / Total Items) × 100Ejemplo: 3 de 5 items completados = 60% de progreso
- Abrir detalle de tarea
- Scroll hasta sección “Checklist”
- Escribir título del nuevo item
- Click en ”+” para agregar
- Marcar checkbox para completar/descompletar
- Click en texto del item para editar
- Hover y click en icono de basura para eliminar
Detalles Técnicos
Componente
Sección titulada «Componente»- Archivo:
TaskChecklistManager.tsx - Ruta:
src/features/projects/sub-modules/tasks/components/TaskChecklistManager.tsx
Implementación
Sección titulada «Implementación»Las subtareas son tareas completas en la tabla tasks con parent_task_id:
interface TaskChecklistManagerProps { parentTaskId: string; subtasks?: Task[]; onUpdate?: () => void;}Mutación Crear Item
Sección titulada «Mutación Crear Item»const createMutation = useMutation({ mutationFn: async (title: string) => { const { data: { user } } = await supabase.auth.getUser(); if (!user) throw new Error("Usuario no autenticado");
// Obtener project_id del padre para heredar const { data: parentTask } = await supabase .from('tasks') .select('project_id') .eq('id', parentTaskId) .single();
// Crear subtarea const { data, error } = await supabase .from('tasks') .insert({ user_id: user.id, parent_task_id: parentTaskId, title, status: 'pending', priority: 'medium', progress: 0, project_id: parentTask?.project_id || null, // Heredar }) .select() .single();
if (error) throw error; return data; }, onSuccess: (data) => { setNewItemTitle(""); setLocalSubtasks(prev => [...prev, data as Task]); toast({ title: "Item añadido" }); }});Mutación Toggle Completado
Sección titulada «Mutación Toggle Completado»const toggleMutation = useMutation({ mutationFn: async ({ id, currentStatus }) => { const newStatus = currentStatus === 'completed' ? 'pending' : 'completed';
const { error } = await supabase .from('tasks') .update({ status: newStatus, completed_at: newStatus === 'completed' ? new Date().toISOString() : null, progress: newStatus === 'completed' ? 100 : 0, }) .eq('id', id);
if (error) throw error; return { id, newStatus }; }, onSuccess: (data) => { // Actualizar estado local inmediatamente setLocalSubtasks(prev => prev.map(subtask => { if (subtask.id === data.id) { return { ...subtask, status: data.newStatus, completed_at: data.newStatus === 'completed' ? new Date().toISOString() : null, progress: data.newStatus === 'completed' ? 100 : 0 }; } return subtask; })); }});Mutación Actualizar Título
Sección titulada «Mutación Actualizar Título»const updateMutation = useMutation({ mutationFn: async ({ id, title }) => { const { error } = await supabase .from('tasks') .update({ title }) .eq('id', id);
if (error) throw error; return { id, title }; }, onSuccess: (data) => { setEditingId(null); setLocalSubtasks(prev => prev.map(subtask => subtask.id === data.id ? { ...subtask, title: data.title } : subtask )); }});Mutación Eliminar Item
Sección titulada «Mutación Eliminar Item»const deleteMutation = useMutation({ mutationFn: async (id: string) => { const { error } = await supabase .from('tasks') .delete() .eq('id', id);
if (error) throw error; return id; }, onSuccess: (deletedId) => { setLocalSubtasks(prev => prev.filter(subtask => subtask.id !== deletedId)); toast({ title: "Item eliminado" }); }});Cálculo de Progreso
Sección titulada «Cálculo de Progreso»const completedCount = localSubtasks.filter(s => s.status === 'completed').length;const totalCount = localSubtasks.length;const progressPercentage = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;UI del Checklist Item
Sección titulada «UI del Checklist Item»<div className="flex items-center gap-2 group p-2 rounded-lg hover:bg-accent/50"> {/* Grip para arrastrar (visual) */} <div className="opacity-0 group-hover:opacity-100"> <GripVertical className="h-4 w-4 text-muted-foreground" /> </div>
{/* Checkbox */} <Checkbox checked={subtask.status === 'completed'} onCheckedChange={() => handleToggle(subtask)} />
{/* Título editable */} {editingId === subtask.id ? ( <Input value={editingTitle} onChange={(e) => setEditingTitle(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveEdit(); if (e.key === 'Escape') handleCancelEdit(); }} autoFocus /> ) : ( <span className={cn( "flex-1 cursor-pointer", subtask.status === 'completed' && "line-through text-muted-foreground" )} onClick={() => handleStartEdit(subtask)} > {subtask.title} </span> )}
{/* Botón eliminar */} <Button size="sm" variant="ghost" className="opacity-0 group-hover:opacity-100" onClick={() => deleteMutation.mutate(subtask.id)} > <Trash2 className="h-4 w-4 text-destructive" /> </Button></div>Barra de Progreso
Sección titulada «Barra de Progreso»<div className="w-full bg-secondary rounded-full h-2"> <div className={cn( "h-2 rounded-full transition-all duration-300", progressPercentage === 100 ? "bg-primary" : "bg-primary/70" )} style={{ width: `${progressPercentage}%` }} /></div>Estado Local
Sección titulada «Estado Local»Se utiliza estado local para UI responsive inmediata:
const [localSubtasks, setLocalSubtasks] = useState<Task[]>(subtasks);
// Sync con propsuseEffect(() => { setLocalSubtasks(subtasks);}, [subtasks]);