// lib/tasksService.ts import { dolibarrFetch } from "./dolibarrClient"; import { DolibarrTask, Task, mapDolibarrTask, CreateTaskData, UpdateTaskData } from "@/types/task"; // ==================== GESTIÓN DE DEPENDENCIAS ==================== // Las dependencias entre tareas se almacenan en localStorage ya que // la API REST de Dolibarr no expone la tabla llx_projet_task_dependency. // Formato: { [taskId]: number[] } donde el array contiene IDs de tareas predecesoras. const DEPENDENCIES_STORAGE_KEY = "task_dependencies"; /** * Obtener todas las dependencias almacenadas */ function getAllDependencies(): Record { if (typeof window === "undefined") return {}; try { const stored = localStorage.getItem(DEPENDENCIES_STORAGE_KEY); return stored ? JSON.parse(stored) : {}; } catch { return {}; } } /** * Guardar todas las dependencias */ function saveAllDependencies(deps: Record): void { if (typeof window === "undefined") return; localStorage.setItem(DEPENDENCIES_STORAGE_KEY, JSON.stringify(deps)); } /** * Obtener dependencias (predecesoras) de una tarea específica */ export function getTaskDependencies(taskId: number): number[] { const all = getAllDependencies(); return all[String(taskId)] || []; } /** * Establecer dependencias (predecesoras) de una tarea */ export function setTaskDependencies(taskId: number, dependencies: number[]): void { const all = getAllDependencies(); if (dependencies.length === 0) { delete all[String(taskId)]; } else { all[String(taskId)] = dependencies; } saveAllDependencies(all); } /** * Eliminar todas las dependencias de una tarea (como predecesora y como dependiente) */ export function removeTaskDependencies(taskId: number): void { const all = getAllDependencies(); // Eliminar la entrada de esta tarea delete all[String(taskId)]; // Eliminar esta tarea de las dependencias de otras tareas for (const key of Object.keys(all)) { all[key] = all[key].filter(id => id !== taskId); if (all[key].length === 0) delete all[key]; } saveAllDependencies(all); } /** * Obtener las dependencias de todas las tareas de un proyecto * Devuelve un Map de taskId -> array de IDs de predecesoras */ export function getProjectTaskDependencies(taskIds: number[]): Map { const all = getAllDependencies(); const result = new Map(); for (const taskId of taskIds) { const deps = all[String(taskId)]; if (deps && deps.length > 0) { // Solo incluir dependencias que pertenecen al mismo proyecto result.set(taskId, deps.filter(depId => taskIds.includes(depId))); } } return result; } /** * Enriquecer tareas con sus dependencias */ function enrichTasksWithDependencies(tasks: Task[]): Task[] { const taskIds = tasks.map(t => t.id); const depsMap = getProjectTaskDependencies(taskIds); return tasks.map(task => ({ ...task, dependencies: depsMap.get(task.id) || [], })); } /** * Obtener todas las tareas de un proyecto específico * * Nota: El endpoint /projects/{id}/tasks de Dolibarr no devuelve tareas correctamente, * por lo que obtenemos todas las tareas y filtramos por fk_project en el cliente. */ export async function getTasksByProjectId(projectId: number): Promise { try { // Obtener todas las tareas (Dolibarr no filtra bien por proyecto) const dolibarrTasks: DolibarrTask[] = await dolibarrFetch('tasks'); // Si no hay tareas, devolver array vacío if (!dolibarrTasks || !Array.isArray(dolibarrTasks)) { return []; } // Filtrar tareas que pertenecen a este proyecto const projectTasks = dolibarrTasks.filter( task => String(task.fk_project) === String(projectId) ); // Mapear las tareas al formato de la UI y enriquecer con dependencias const mappedTasks = projectTasks.map(mapDolibarrTask); return enrichTasksWithDependencies(mappedTasks); } catch (error) { console.error('Error fetching tasks for project:', projectId, error); // Si el error es 404 (no hay tareas), devolver array vacío if (error instanceof Error && error.message.includes('404')) { return []; } throw error; } } /** * Obtener todas las tareas (sin filtro de proyecto) * Endpoint: /tasks */ export async function getAllTasks(): Promise { try { const dolibarrTasks: DolibarrTask[] = await dolibarrFetch('tasks'); if (!dolibarrTasks || !Array.isArray(dolibarrTasks)) { return []; } const mappedTasks = dolibarrTasks.map(mapDolibarrTask); return enrichTasksWithDependencies(mappedTasks); } catch (error) { console.error('Error fetching all tasks:', error); throw error; } } /** * Obtener una tarea específica por ID * Endpoint: /tasks/{id} */ export async function getTaskById(taskId: number): Promise { try { const dolibarrTask: DolibarrTask = await dolibarrFetch(`tasks/${taskId}`); const task = mapDolibarrTask(dolibarrTask); task.dependencies = getTaskDependencies(taskId); return task; } catch (error) { console.error('Error fetching task:', taskId, error); return null; } } /** * Obtener datos crudos de Dolibarr para una tarea */ export async function getDolibarrTaskById(taskId: number): Promise { try { return await dolibarrFetch(`tasks/${taskId}`); } catch (error) { console.error('Error fetching Dolibarr task:', taskId, error); return null; } } /** * Calcular estadísticas de las tareas de un proyecto */ export function calculateTaskStats(tasks: Task[]) { const total = tasks.length; const completed = tasks.filter(t => t.status === '2').length; const inProgress = tasks.filter(t => t.status === '1').length; const draft = tasks.filter(t => t.status === '0').length; const totalPlannedHours = tasks.reduce((sum, t) => sum + t.plannedHours, 0); const totalWorkedHours = tasks.reduce((sum, t) => sum + t.workedHours, 0); const avgProgress = total > 0 ? Math.round(tasks.reduce((sum, t) => sum + t.progress, 0) / total) : 0; const highPriority = tasks.filter(t => t.priority === '3').length; const overdue = tasks.filter(t => { if (!t.endDate && !t.plannedEndDate) return false; const endDate = t.endDate || t.plannedEndDate; return endDate && new Date(endDate) < new Date() && t.status !== '2'; }).length; return { total, completed, inProgress, draft, totalPlannedHours, totalWorkedHours, avgProgress, highPriority, overdue, completionRate: total > 0 ? Math.round((completed / total) * 100) : 0, }; } // ==================== FUNCIONES DE ESCRITURA (CREATE/UPDATE/DELETE) ==================== /** * Crear una nueva tarea * Endpoint: POST /tasks * @returns La tarea creada con su ID */ export async function createTask(data: CreateTaskData): Promise { try { // Limpiar datos undefined/null para la API const cleanData = Object.fromEntries( Object.entries(data).filter(([, v]) => v !== undefined && v !== null && v !== '') ); const response = await dolibarrFetch('tasks', { method: 'POST', body: JSON.stringify(cleanData), }); // Dolibarr devuelve el ID de la tarea creada const taskId = typeof response === 'object' ? response.id || response : response; // Obtener la tarea completa const createdTask = await getTaskById(Number(taskId)); if (!createdTask) { throw new Error('No se pudo obtener la tarea creada'); } return createdTask; } catch (error) { console.error('Error creating task:', error); throw error; } } /** * Actualizar una tarea existente * Endpoint: PUT /tasks/{id} * @returns La tarea actualizada */ export async function updateTask(taskId: number, data: UpdateTaskData): Promise { try { // Limpiar datos undefined/null para la API const cleanData = Object.fromEntries( Object.entries(data).filter(([, v]) => v !== undefined && v !== null) ); await dolibarrFetch(`tasks/${taskId}`, { method: 'PUT', body: JSON.stringify(cleanData), }); // Obtener la tarea actualizada const updatedTask = await getTaskById(taskId); if (!updatedTask) { throw new Error('No se pudo obtener la tarea actualizada'); } return updatedTask; } catch (error) { console.error('Error updating task:', taskId, error); throw error; } } /** * Eliminar una tarea * Endpoint: DELETE /tasks/{id} */ export async function deleteTask(taskId: number): Promise { try { await dolibarrFetch(`tasks/${taskId}`, { method: 'DELETE', }); // Limpiar dependencias asociadas a esta tarea removeTaskDependencies(taskId); } catch (error) { console.error('Error deleting task:', taskId, error); throw error; } } /** * Validar (cambiar estado a 1) una tarea * Endpoint: POST /tasks/{id}/validate */ export async function validateTask(taskId: number): Promise { try { await dolibarrFetch(`tasks/${taskId}/validate`, { method: 'POST', body: JSON.stringify({ notrigger: 0 }), }); const task = await getTaskById(taskId); if (!task) { throw new Error('No se pudo obtener la tarea validada'); } return task; } catch (error) { console.error('Error validating task:', taskId, error); throw error; } }