import * as repo from './repository.js'; import * as projectsRepo from '../projects/repository.js'; import { recalcProjectProgress, canWriteProject } from '../projects/service.js'; import { HttpError } from '../../utils/http.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; } // admin/it_manager siempre; project_lead si tiene acceso al proyecto; collaborator solo si es // el responsable de la actividad (no puede crear actividades nuevas ni reasignarlas a otros). export async function canWriteActivity(user, project, activity) { if ([ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER].includes(user.roleCode)) return true; if (user.roleCode === ROLES.PROJECT_LEAD) return canWriteProject(user, project); if (user.roleCode === ROLES.COLLABORATOR && activity) return activity.responsible_user_id === user.id; return false; } export const listByProject = (projectId) => repo.findByProject(projectId); export async function getActivity(id) { const activity = await repo.findById(id); if (!activity) 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 subtareas. const nextResponsible = currentUser.roleCode === ROLES.COLLABORATOR ? activity.responsible_user_id : (payload.responsibleUserId ?? activity.responsible_user_id); const subtaskCount = await repo.countSubtasks(id); const updated = await repo.update(id, { ...payload, responsibleUserId: nextResponsible }); if (subtaskCount === 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); } 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 subtareas (si tiene) y propaga al proyecto. export async function recalcFromSubtasks(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); }