trello_fake/lib/tasksService.ts

327 lines
9.3 KiB
TypeScript
Raw Normal View History

// lib/tasksService.ts
import { dolibarrFetch } from "./dolibarrClient";
import {
DolibarrTask,
Task,
mapDolibarrTask,
CreateTaskData,
UpdateTaskData
} from "@/types/task";
2026-02-06 19:06:25 +00:00
// ==================== 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<string, number[]> {
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<string, number[]>): 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<number, number[]> {
const all = getAllDependencies();
const result = new Map<number, number[]>();
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<Task[]> {
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)
);
2026-02-06 19:06:25 +00:00
// 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<Task[]> {
try {
const dolibarrTasks: DolibarrTask[] = await dolibarrFetch('tasks');
if (!dolibarrTasks || !Array.isArray(dolibarrTasks)) {
return [];
}
2026-02-06 19:06:25 +00:00
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<Task | null> {
try {
const dolibarrTask: DolibarrTask = await dolibarrFetch(`tasks/${taskId}`);
2026-02-06 19:06:25 +00:00
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<DolibarrTask | null> {
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<Task> {
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<Task> {
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<void> {
try {
await dolibarrFetch(`tasks/${taskId}`, {
method: 'DELETE',
});
2026-02-06 19:06:25 +00:00
// 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<Task> {
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;
}
}