import * as repo from './repository.js'; import * as projectsRepo from '../projects/repository.js'; import * as commentsRepo from '../comments/repository.js'; import * as attachmentsRepo from '../attachments/repository.js'; import * as tasksRepo from '../tasks/repository.js'; import { recalcProjectProgress, canReadProject, canWriteProject } from '../projects/service.js'; import { HttpError } from '../../utils/http.js'; import { requireEditVersion, requireUpdated } from '../../utils/editVersion.js'; import { logAudit } from '../../utils/audit.js'; import { ROLES } from '../../middleware/auth.js'; import { createNotification } from '../notifications/service.js'; async function getProjectOr404(projectId) { const project = await projectsRepo.findById(projectId); if (!project) throw new HttpError(404, 'Proyecto no encontrado'); return project; } // Se delega entero a canWriteProject: con el modelo de compartir por proyecto (Lectura/Escritura, // ver projects/service.js), quien tiene Escritura puede crear/editar/eliminar cualquier // actividad o tarea del proyecto -- ya no importa si es el "responsable" asignado o no (esa // regla vieja, previa a tener niveles de permiso reales por proyecto, quedo obsoleta). export async function canWriteActivity(user, project) { return canWriteProject(user, project); } export async function listByProject(projectId, currentUser) { const project = await getProjectOr404(projectId); if (!(await canReadProject(currentUser, project))) throw new HttpError(404, 'Proyecto no encontrado'); return repo.findByProject(projectId); } export async function listTrash(projectId, currentUser) { const project = await getProjectOr404(projectId); if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto'); return repo.findTrashByProject(projectId); } export async function getActivity(id, currentUser) { const activity = await repo.findById(id); if (!activity) throw new HttpError(404, 'Actividad no encontrada'); const project = await getProjectOr404(activity.project_id); if (!(await canReadProject(currentUser, project))) throw new HttpError(404, 'Actividad no encontrada'); const dependencies = await repo.listDependencies(id); return { ...activity, dependencies }; } export async function createActivity(projectId, payload, currentUser) { const project = await getProjectOr404(projectId); if (!(await canWriteProject(currentUser, project))) { throw new HttpError(403, 'No tienes permisos para crear actividades en este proyecto'); } if (!payload.name || !payload.priorityId || !payload.statusId) { throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios'); } const activity = await repo.create(projectId, { ...payload, createdBy: currentUser.id }); await logAudit({ userId: currentUser.id, action: 'create', entityType: 'activity', entityId: activity.id }); if (activity.responsible_user_id && activity.responsible_user_id !== currentUser.id) { await createNotification({ userId: activity.responsible_user_id, type: 'activity_assigned', message: `Te asignaron la actividad "${activity.name}"`, entityType: 'activity', entityId: activity.id, }); } await recalcProjectProgress(projectId); return activity; } export async function updateActivity(id, payload, currentUser) { const activity = await repo.findById(id); if (!activity) throw new HttpError(404, 'Actividad no encontrada'); const project = await getProjectOr404(activity.project_id); if (!(await canWriteActivity(currentUser, project, activity))) { throw new HttpError(403, 'No tienes permisos sobre esta actividad'); } if (!payload.name || !payload.priorityId || !payload.statusId) { throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios'); } // Colaborador no puede reasignar la actividad a otra persona ni cambiar avance manual si hay tareas. const nextResponsible = currentUser.roleCode === ROLES.COLLABORATOR ? activity.responsible_user_id : (payload.responsibleUserId ?? activity.responsible_user_id); requireEditVersion(payload.editVersion); const taskCount = await repo.countTasks(id); const updated = requireUpdated(await repo.update(id, { ...payload, responsibleUserId: nextResponsible })); if (taskCount === 0 && payload.progressPct !== undefined) { const pct = Math.max(0, Math.min(100, Number(payload.progressPct))); await repo.updateProgress(id, pct); updated.progress_pct = pct; } await logAudit({ userId: currentUser.id, action: 'update', entityType: 'activity', entityId: id }); if (nextResponsible && nextResponsible !== activity.responsible_user_id && nextResponsible !== currentUser.id) { await createNotification({ userId: nextResponsible, type: 'activity_assigned', message: `Te asignaron la actividad "${updated.name}"`, entityType: 'activity', entityId: id, }); } await recalcProjectProgress(activity.project_id); return repo.findById(id); } // Bloquea el borrado si hay tareas pendientes (a proposito: evita perder de vista trabajo real // hecho/por hacer solo porque alguien borro la actividad de un clic). El usuario debe vaciar el // checklist primero. Es eliminacion logica (recuperable, ver restoreActivity) -- comentarios y // adjuntos NO se tocan, quedan intactos por si se restaura despues. export async function removeActivity(id, currentUser) { const activity = await repo.findById(id); if (!activity) throw new HttpError(404, 'Actividad no encontrada'); const project = await getProjectOr404(activity.project_id); if (!(await canWriteActivity(currentUser, project, activity))) { throw new HttpError(403, 'No tienes permisos sobre esta actividad'); } const taskCount = await repo.countTasks(id); if (taskCount > 0) { throw new HttpError(400, `Esta actividad tiene ${taskCount} tarea(s). Elimina primero las tareas antes de borrar la actividad.`); } await repo.softDelete(id, currentUser.id); await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'activity', entityId: id }); await recalcProjectProgress(activity.project_id); } export async function restoreActivity(id, currentUser) { const activity = await repo.findById(id, { includeDeleted: true }); if (!activity || !activity.deleted_at) throw new HttpError(404, 'Actividad no encontrada en la papelera'); const project = await getProjectOr404(activity.project_id); if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto'); await repo.restore(id); await logAudit({ userId: currentUser.id, action: 'restore', entityType: 'activity', entityId: id }); await recalcProjectProgress(activity.project_id); } // Mueve la actividad (y sus tareas/comentarios/adjuntos, que van solos por referenciar activity_id // en vez de project_id) a otro proyecto. Exige permiso de escritura en AMBOS proyectos. export async function moveActivity(id, targetProjectId, currentUser) { const activity = await repo.findById(id); if (!activity) throw new HttpError(404, 'Actividad no encontrada'); const sourceProject = await getProjectOr404(activity.project_id); if (!(await canWriteActivity(currentUser, sourceProject, activity))) { throw new HttpError(403, 'No tienes permisos sobre esta actividad'); } if (Number(targetProjectId) === activity.project_id) { throw new HttpError(400, 'La actividad ya pertenece a este proyecto'); } const targetProject = await getProjectOr404(targetProjectId); if (!(await canWriteProject(currentUser, targetProject))) { throw new HttpError(403, 'No tienes permisos sobre el proyecto de destino'); } const moved = await repo.moveToProject(id, targetProjectId); await logAudit({ userId: currentUser.id, action: 'move', entityType: 'activity', entityId: id, detail: { fromProjectId: activity.project_id, toProjectId: Number(targetProjectId) }, }); await recalcProjectProgress(activity.project_id); await recalcProjectProgress(targetProjectId); return moved; } // Convierte una actividad en un proyecto nuevo. Una tarea no puede vivir directo dentro de un // proyecto (siempre necesita una actividad contenedora), asi que esto crea DOS cosas: el proyecto // nuevo, y una actividad nueva adentro que recibe las tareas/comentarios/adjuntos de la original // (la original se borra al final, ya vacia de tareas). Exige el mismo permiso que crear un // proyecto (no solo escribir la actividad) — convertir equivale a crear un proyecto de la nada. export async function convertToProject(activityId, { projectName, taskActivityName }, currentUser) { const activity = await repo.findById(activityId); if (!activity) throw new HttpError(404, 'Actividad no encontrada'); const sourceProject = await getProjectOr404(activity.project_id); if (!(await canWriteActivity(currentUser, sourceProject, activity))) { throw new HttpError(403, 'No tienes permisos sobre esta actividad'); } if (![ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER, ROLES.PROJECT_LEAD].includes(currentUser.roleCode)) { throw new HttpError(403, 'No tienes permisos para crear proyectos'); } const name = (projectName || '').trim(); const taskActName = (taskActivityName || '').trim(); if (!name) throw new HttpError(400, 'El nombre del proyecto es obligatorio'); if (!taskActName) throw new HttpError(400, 'El nombre de la actividad es obligatorio'); const defaultStatusId = await projectsRepo.getDefaultStatusId(); if (!defaultStatusId) throw new HttpError(500, 'No hay catalogo de estados de proyecto configurado'); const newProject = await projectsRepo.create({ name, priorityId: activity.priority_id, statusId: defaultStatusId, ownerUserId: currentUser.id, createdBy: currentUser.id, }); const newActivity = await repo.create(newProject.id, { name: taskActName, priorityId: activity.priority_id, statusId: activity.status_id, createdBy: currentUser.id, }); await tasksRepo.moveAll(activityId, newActivity.id); await commentsRepo.reassignEntity('activity', activityId, newActivity.id); await attachmentsRepo.reassignEntity('activity', activityId, newActivity.id); const counts = await tasksRepo.countsByActivity(newActivity.id); await recalcFromTasks(newActivity.id, counts.done, counts.total); await repo.remove(activityId); // ya sin tareas: el bloqueo de removeActivity no aplica aqui. await logAudit({ userId: currentUser.id, action: 'convert_to_project', entityType: 'activity', entityId: activityId, detail: { newProjectId: newProject.id, newActivityId: newActivity.id }, }); await recalcProjectProgress(activity.project_id); await recalcProjectProgress(newProject.id); return { project: newProject, activity: newActivity }; } // Nuevo orden manual (arrastrar con el mouse): mismo permiso que crear actividades en el proyecto. export async function reorderActivities(projectId, ids, currentUser) { const project = await getProjectOr404(projectId); if (!(await canWriteProject(currentUser, project))) { throw new HttpError(403, 'No tienes permisos sobre este proyecto'); } if (!Array.isArray(ids) || ids.length === 0) throw new HttpError(400, 'Falta el orden de actividades'); await repo.reorder(projectId, ids); return repo.findByProject(projectId); } export async function addDependency(activityId, dependsOnActivityId, currentUser) { if (activityId === dependsOnActivityId) throw new HttpError(400, 'Una actividad no puede depender de si misma'); const activity = await repo.findById(activityId); if (!activity) throw new HttpError(404, 'Actividad no encontrada'); const dependsOn = await repo.findById(dependsOnActivityId); if (!dependsOn || dependsOn.project_id !== activity.project_id) { throw new HttpError(400, 'La dependencia debe ser una actividad del mismo proyecto'); } const project = await getProjectOr404(activity.project_id); if (!(await canWriteActivity(currentUser, project, activity))) throw new HttpError(403, 'No tienes permisos sobre esta actividad'); const deps = await repo.addDependency(activityId, dependsOnActivityId); await logAudit({ userId: currentUser.id, action: 'add_dependency', entityType: 'activity', entityId: activityId, detail: { dependsOnActivityId } }); return deps; } export async function removeDependency(activityId, dependsOnActivityId, currentUser) { const activity = await repo.findById(activityId); if (!activity) throw new HttpError(404, 'Actividad no encontrada'); const project = await getProjectOr404(activity.project_id); if (!(await canWriteActivity(currentUser, project, activity))) throw new HttpError(403, 'No tienes permisos sobre esta actividad'); const deps = await repo.removeDependency(activityId, dependsOnActivityId); await logAudit({ userId: currentUser.id, action: 'remove_dependency', entityType: 'activity', entityId: activityId, detail: { dependsOnActivityId } }); return deps; } // Recalcula el avance de una actividad a partir de sus tareas (si tiene) y propaga al proyecto. export async function recalcFromTasks(activityId, doneCount, totalCount) { if (totalCount === 0) return; const pct = Math.round((doneCount / totalCount) * 10000) / 100; await repo.updateProgress(activityId, pct); const activity = await repo.findById(activityId); await recalcProjectProgress(activity.project_id); }