import * as repo from './repository.js'; import { HttpError } from '../../utils/http.js'; import { logAudit } from '../../utils/audit.js'; import { ROLES } from '../../middleware/auth.js'; // Alcance de escritura: admin/superadmin/it_manager siempre; un proyecto puede tener uno o mas // "administradores del proyecto" (role_in_project = 'owner'/'admin' en project_members) con acceso // total sin importar su rol global — un collaborator puede ser admin de UN proyecto puntual; // ademas, por compatibilidad, cualquier project_lead que sea miembro (con o sin ese tag) tambien // puede escribir, igual que antes de introducir el rol de administrador por proyecto. export async function canWriteProject(user, project) { if ([ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER].includes(user.roleCode)) return true; const membership = await repo.getMembership(project.id, user.id); if (membership === 'owner' || membership === 'admin') return true; if (user.roleCode === ROLES.PROJECT_LEAD && membership !== null) return true; return false; } // Solo el superadmin puede ver la papelera (proyectos eliminados logicamente). export async function listProjects(filters, currentUser) { if (filters.trash && currentUser?.roleCode !== ROLES.SUPERADMIN) { throw new HttpError(403, 'Solo el superadministrador puede ver la papelera'); } return repo.findAll(filters); } export async function getProject(id) { const project = await repo.findById(id); if (!project) throw new HttpError(404, 'Proyecto no encontrado'); const members = await repo.listMembers(id); return { ...project, members }; } export async function createProject(payload, currentUser) { if (!payload.name || !payload.priorityId || !payload.statusId) { throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios'); } 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 project = await repo.create({ ...payload, ownerUserId: payload.ownerUserId || currentUser.id, createdBy: currentUser.id, }); await logAudit({ userId: currentUser.id, action: 'create', entityType: 'project', entityId: project.id }); return project; } export async function updateProject(id, payload, currentUser) { const project = await repo.findById(id); if (!project) throw new HttpError(404, 'Proyecto no encontrado'); if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto'); if (!payload.name || !payload.priorityId || !payload.statusId) { throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios'); } const updated = await repo.update(id, { ...payload, ownerUserId: payload.ownerUserId || project.owner_user_id }); await logAudit({ userId: currentUser.id, action: 'update', entityType: 'project', entityId: id }); return updated; } // Eliminacion logica: cualquiera que pueda editar el proyecto puede "eliminarlo" (deja de verse // en el listado, pero sigue en la BD). Solo el superadmin puede restaurarlo o borrarlo definitivo. export async function deleteProject(id, currentUser) { const project = await repo.findById(id); if (!project) throw new HttpError(404, 'Proyecto no encontrado'); if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto'); await repo.softDelete(id, currentUser.id); await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'project', entityId: id }); } export async function restoreProject(id, currentUser) { if (currentUser.roleCode !== ROLES.SUPERADMIN) throw new HttpError(403, 'Solo el superadministrador puede restaurar proyectos'); const project = await repo.findById(id, { includeDeleted: true }); if (!project || !project.deleted_at) throw new HttpError(404, 'Proyecto no encontrado en la papelera'); await repo.restore(id); await logAudit({ userId: currentUser.id, action: 'restore', entityType: 'project', entityId: id }); } // Borrado fisico e irreversible (arrastra actividades/comentarios/adjuntos por ON DELETE CASCADE). // Solo el superadmin, y solo sobre un proyecto que ya paso por eliminacion logica primero. export async function permanentlyDeleteProject(id, currentUser) { if (currentUser.roleCode !== ROLES.SUPERADMIN) throw new HttpError(403, 'Solo el superadministrador puede eliminar proyectos definitivamente'); const project = await repo.findById(id, { includeDeleted: true }); if (!project) throw new HttpError(404, 'Proyecto no encontrado'); if (!project.deleted_at) throw new HttpError(400, 'El proyecto debe eliminarse primero antes de poder borrarlo definitivamente'); await repo.hardDelete(id); await logAudit({ userId: currentUser.id, action: 'permanent_delete', entityType: 'project', entityId: id }); } // Recalcula el avance del proyecto como promedio del avance de sus actividades. export async function recalcProjectProgress(projectId) { const avg = await repo.averageActivityProgress(projectId); await repo.updateProgress(projectId, Math.round(avg * 100) / 100); } export async function addMember(projectId, userId, roleInProject, currentUser) { const project = await repo.findById(projectId); if (!project) throw new HttpError(404, 'Proyecto no encontrado'); if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto'); const members = await repo.addMember(projectId, userId, roleInProject); await logAudit({ userId: currentUser.id, action: 'add_member', entityType: 'project', entityId: projectId, detail: { userId } }); return members; } export async function setMemberAdmin(projectId, userId, isAdmin, currentUser) { const project = await repo.findById(projectId); if (!project) throw new HttpError(404, 'Proyecto no encontrado'); if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto'); const members = await repo.updateMemberRole(projectId, userId, isAdmin ? 'admin' : null); await logAudit({ userId: currentUser.id, action: isAdmin ? 'grant_project_admin' : 'revoke_project_admin', entityType: 'project', entityId: projectId, detail: { userId }, }); return members; } export async function removeMember(projectId, userId, currentUser) { const project = await repo.findById(projectId); if (!project) throw new HttpError(404, 'Proyecto no encontrado'); if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto'); const members = await repo.removeMember(projectId, userId); await logAudit({ userId: currentUser.id, action: 'remove_member', entityType: 'project', entityId: projectId, detail: { userId } }); return members; }