Ir al contenido

Gestionar Dependencias

Establece relaciones entre tareas para definir orden de ejecución.

TipoDescripciónUso Común
Fin a Inicio (Finish-to-Start)La tarea B solo puede iniciar cuando termine ASecuencia estándar de tareas
Inicio a Inicio (Start-to-Start)Las tareas A y B deben iniciar juntasTareas paralelas coordinadas
Fin a Fin (Finish-to-Finish)Las tareas A y B deben finalizar juntasTareas con entregables relacionados
Inicio a Fin (Start-to-Finish)La tarea B solo puede terminar cuando inicie ATransiciones de fase

Agregar Dependencia:

  1. Seleccionar tarea de la cual depende
  2. Elegir tipo de dependencia
  3. Click en ”+” para agregar

Validaciones:

  • Prevención de ciclos: No permite dependencias circulares
  • Mismo proyecto: Solo tareas del mismo proyecto
  • Excluir subtareas: No puede depender de sus propias subtareas
  • Sin duplicados: No permite agregar misma dependencia dos veces

Indicadores Visuales:

  • Estado de la tarea dependiente (completada/pendiente)
  • Color según estado
  • Alerta si hay dependencias incompletas

Una tarea con dependencias incompletas:

  • Muestra advertencia en color rojo
  • Puede marcarse como completada (no bloqueante, solo informativo)
  • Indica claramente qué tareas faltan
Detalles Técnicos
  • Archivo: TaskDependencyManager.tsx
  • Ruta: src/features/projects/sub-modules/tasks/components/TaskDependencyManager.tsx
type DependencyType = 'finish_to_start' | 'start_to_start' | 'finish_to_finish' | 'start_to_finish';
interface TaskDependency {
id: string;
task_id: string;
depends_on_task_id: string;
dependency_type: DependencyType;
created_at: string;
updated_at: string;
// Relación
depends_on_task?: Task;
}
const { data: availableTasks } = useQuery({
queryKey: ["available-tasks", projectId, taskId],
queryFn: async () => {
if (!projectId) return [];
const { data, error } = await supabase
.from("tasks")
.select("id, title, status, parent_task_id")
.eq("project_id", projectId)
.neq("id", taskId) // Excluir tarea actual
.neq("parent_task_id", taskId) // Excluir subtareas
.order("title");
return data;
}
});
const checkCircularDependency = async (targetTaskId: string): Promise<boolean> => {
const visited = new Set<string>();
const checkPath = async (currentTaskId: string): Promise<boolean> => {
// Si llegamos a la tarea original, hay ciclo
if (currentTaskId === taskId) return true;
if (visited.has(currentTaskId)) return false;
visited.add(currentTaskId);
// Obtener dependencias de la tarea actual
const { data } = await supabase
.from("task_dependencies")
.select("depends_on_task_id")
.eq("task_id", currentTaskId);
if (!data) return false;
// Revisar recursivamente cada dependencia
for (const dep of data) {
if (await checkPath(dep.depends_on_task_id)) return true;
}
return false;
};
return await checkPath(targetTaskId);
};
const addDependency = useMutation({
mutationFn: async () => {
if (!selectedTaskId) throw new Error("Selecciona una tarea");
// Validar ciclos
const isCircular = await checkCircularDependency(selectedTaskId);
if (isCircular) {
throw new Error("Esta dependencia crearía un ciclo circular");
}
// Insertar dependencia
const { error } = await supabase
.from("task_dependencies")
.insert({
task_id: taskId,
depends_on_task_id: selectedTaskId,
dependency_type: dependencyType,
});
if (error) throw error;
},
onSuccess: () => {
toast({ title: "Dependencia agregada" });
refetchDependencies();
}
});
const { data: dependencies } = useQuery({
queryKey: ["task-dependencies", taskId],
queryFn: async () => {
const { data, error } = await supabase
.from("task_dependencies")
.select(`
*,
depends_on_task:tasks!task_dependencies_depends_on_task_id_fkey(
id,
title,
status
)
`)
.eq("task_id", taskId);
if (error) throw error;
return data as TaskDependency[];
},
});
const getDependencyTypeLabel = (type: DependencyType) => {
const labels: Record<DependencyType, string> = {
finish_to_start: "Fin a Inicio",
start_to_start: "Inicio a Inicio",
finish_to_finish: "Fin a Fin",
start_to_finish: "Inicio a Fin",
};
return labels[type];
};
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
pending: "bg-yellow-500",
in_progress: "bg-blue-500",
completed: "bg-green-500",
cancelled: "bg-gray-500",
on_hold: "bg-orange-500",
};
return colors[status] || "bg-gray-500";
};
CREATE TABLE task_dependencies (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
depends_on_task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
dependency_type VARCHAR(20) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT no_self_dependency CHECK (task_id != depends_on_task_id),
UNIQUE(task_id, depends_on_task_id)
);
{dependencies.some(d => d.depends_on_task?.status !== 'completed') && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Esta tarea tiene dependencias incompletas. No podrá marcarse como
completada hasta que todas las tareas dependientes estén finalizadas.
</AlertDescription>
</Alert>
)}