feat: mover una actividad (con sus tareas) a otro proyecto

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>
This commit is contained in:
Milton Gonzalez
2026-09-16 14:47:57 -05:00
parent 397fed5eb1
commit 53570bd4f8
12 changed files with 122 additions and 6 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "project-linkedcorp-backend",
"version": "1.16.4",
"version": "1.17.0",
"private": true,
"type": "module",
"main": "src/server.js",
@@ -26,6 +26,10 @@ export const deleteActivity = asyncHandler(async (req, res) => {
ok(res, { deleted: true });
});
export const putMoveActivity = asyncHandler(async (req, res) => {
ok(res, await service.moveActivity(Number(req.params.id), Number(req.body.projectId), req.currentUser));
});
export const postDependency = asyncHandler(async (req, res) => {
const deps = await service.addDependency(Number(req.params.id), Number(req.body.dependsOnActivityId), req.currentUser);
ok(res, deps, 201);
@@ -64,6 +64,17 @@ export async function remove(id) {
await pool.query('DELETE FROM activities WHERE id = ?', [id]);
}
// Las tareas (tasks.activity_id) y los comentarios/adjuntos (entity_id de la actividad) se
// "mueven" solos, sin tocar nada — no referencian project_id. sort_order vuelve a 0 ("nunca
// reordenado a mano") para que aparezca al final del proyecto destino sin pisar su orden manual.
// Las dependencias solo tienen sentido dentro del mismo proyecto (addDependency ya lo exige), asi
// que las existentes (en cualquiera de los dos sentidos) quedan invalidas al cambiar de proyecto.
export async function moveToProject(id, projectId) {
await pool.query('UPDATE activities SET project_id = ?, sort_order = 0 WHERE id = ?', [projectId, id]);
await pool.query('DELETE FROM activity_dependencies WHERE activity_id = ? OR depends_on_activity_id = ?', [id, id]);
return findById(id);
}
// Posiciones SIEMPRE 1..N (nunca 0 — 0 queda reservado para "nunca reordenado a mano" en la
// migracion). Solo reordena las filas de projectId que vengan en ids, ignora el resto.
export async function reorder(projectId, ids) {
+2 -1
View File
@@ -1,6 +1,6 @@
import { Router } from 'express';
import { requireAuth } from '../../middleware/auth.js';
import { getActivity, putActivity, deleteActivity, postDependency, deleteDependency } from './controller.js';
import { getActivity, putActivity, deleteActivity, putMoveActivity, postDependency, deleteDependency } from './controller.js';
const router = Router();
@@ -8,6 +8,7 @@ router.use(requireAuth);
router.get('/:id', getActivity);
router.put('/:id', putActivity);
router.delete('/:id', deleteActivity);
router.put('/:id/move', putMoveActivity);
router.post('/:id/dependencies', postDependency);
router.delete('/:id/dependencies/:dependsOnId', deleteDependency);
+26
View File
@@ -118,6 +118,32 @@ export async function removeActivity(id, currentUser) {
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);