53570bd4f8
Menu de tres puntos en el listado de Actividades -> elegir proyecto destino -> confirmar
("Se movera la actividad... (junto con sus tareas)... Aceptar/Cancelar) antes de ejecutar.
Tareas, comentarios y adjuntos se mueven solos (referencian la actividad, no el proyecto). Las
dependencias existentes se eliminan al mover -- solo tienen sentido dentro del mismo proyecto
(addDependency ya lo exigia). Nuevo PUT /api/activities/:id/move, exige permiso de escritura en
ambos proyectos (origen y destino).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
191 lines
9.2 KiB
JavaScript
191 lines
9.2 KiB
JavaScript
import * as repo from './repository.js';
|
|
import * as projectsRepo from '../projects/repository.js';
|
|
import * as commentsRepo from '../comments/repository.js';
|
|
import * as attachmentsService from '../attachments/service.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 tareas.
|
|
const nextResponsible = currentUser.roleCode === ROLES.COLLABORATOR
|
|
? activity.responsible_user_id
|
|
: (payload.responsibleUserId ?? activity.responsible_user_id);
|
|
|
|
const taskCount = await repo.countTasks(id);
|
|
const updated = 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. Comentarios/adjuntos si se limpian solos, no llevan informacion de progreso.
|
|
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 commentsRepo.removeByEntity('activity', id);
|
|
await attachmentsService.removeAllForEntity('activity', id);
|
|
await repo.remove(id);
|
|
await logAudit({ userId: currentUser.id, action: 'delete', 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;
|
|
}
|
|
|
|
// 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);
|
|
}
|