refactor: renombra Subtareas a Tareas en todo el sistema

Actividad -> Tarea, sin nivel "sub" intermedio (no hay una entidad "Tarea"
de la que estas fueran sub-, asi que el prefijo sobraba). Tabla subtasks
-> tasks (RENAME TABLE idempotente en la migracion 005; 001_initial.sql
tambien actualizado para que una instalacion nueva cree la tabla ya con
el nombre final, sin pasar por el nombre viejo). Modulo backend, rutas,
servicio frontend, pestaña del detalle de actividad, i18n y documentacion
actualizados. Sin cambios de comportamiento, solo nombres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-10 08:55:58 -05:00
parent 29daff82ed
commit bb6bd6865c
25 changed files with 191 additions and 172 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ import departmentsRoutes from './modules/departments/routes.js';
import catalogsRoutes from './modules/catalogs/routes.js';
import projectsRoutes from './modules/projects/routes.js';
import activitiesRoutes from './modules/activities/routes.js';
import subtasksRoutes from './modules/subtasks/routes.js';
import tasksRoutes from './modules/tasks/routes.js';
import commentsRoutes from './modules/comments/routes.js';
import attachmentsRoutes from './modules/attachments/routes.js';
import notificationsRoutes from './modules/notifications/routes.js';
@@ -41,7 +41,7 @@ app.use('/api/settings', settingsRoutes);
// Mas generico (mount en /api sin sub-path propio) — SIEMPRE al final de los /api/*, para no
// interceptar con su requireAuth interno una ruta que en realidad pertenece a un router mas
// especifico registrado despues (ver ESTADO.md, deuda tecnica).
app.use('/api', subtasksRoutes);
app.use('/api', tasksRoutes);
app.use(notFoundHandler);
app.use(errorHandler);
+2 -2
View File
@@ -59,8 +59,8 @@ export async function updateProgress(id, progressPct) {
await pool.query('UPDATE activities SET progress_pct = ? WHERE id = ?', [progressPct, id]);
}
export async function countSubtasks(id) {
const [rows] = await pool.query('SELECT COUNT(*) AS c FROM subtasks WHERE activity_id = ?', [id]);
export async function countTasks(id) {
const [rows] = await pool.query('SELECT COUNT(*) AS c FROM tasks WHERE activity_id = ?', [id]);
return rows[0].c;
}
+5 -5
View File
@@ -65,15 +65,15 @@ export async function updateActivity(id, payload, currentUser) {
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.
// 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 subtaskCount = await repo.countSubtasks(id);
const taskCount = await repo.countTasks(id);
const updated = await repo.update(id, { ...payload, responsibleUserId: nextResponsible });
if (subtaskCount === 0 && payload.progressPct !== undefined) {
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;
@@ -120,8 +120,8 @@ export async function removeDependency(activityId, dependsOnActivityId, currentU
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) {
// 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);
@@ -1,23 +0,0 @@
import { ok, asyncHandler } from '../../utils/http.js';
import * as service from './service.js';
export const getSubtasks = asyncHandler(async (req, res) => {
ok(res, await service.listByActivity(Number(req.params.activityId)));
});
export const postSubtask = asyncHandler(async (req, res) => {
ok(res, await service.createSubtask(Number(req.params.activityId), req.body, req.currentUser), 201);
});
export const putSubtask = asyncHandler(async (req, res) => {
ok(res, await service.updateSubtask(Number(req.params.id), req.body, req.currentUser));
});
export const patchSubtaskDone = asyncHandler(async (req, res) => {
ok(res, await service.setSubtaskDone(Number(req.params.id), !!req.body.isDone, req.currentUser));
});
export const deleteSubtask = asyncHandler(async (req, res) => {
await service.removeSubtask(Number(req.params.id), req.currentUser);
ok(res, { deleted: true });
});
-14
View File
@@ -1,14 +0,0 @@
import { Router } from 'express';
import { requireAuth } from '../../middleware/auth.js';
import { getSubtasks, postSubtask, putSubtask, patchSubtaskDone, deleteSubtask } from './controller.js';
const router = Router();
router.use(requireAuth);
router.get('/activities/:activityId/subtasks', getSubtasks);
router.post('/activities/:activityId/subtasks', postSubtask);
router.put('/subtasks/:id', putSubtask);
router.patch('/subtasks/:id/done', patchSubtaskDone);
router.delete('/subtasks/:id', deleteSubtask);
export default router;
-59
View File
@@ -1,59 +0,0 @@
import * as repo from './repository.js';
import * as activitiesRepo from '../activities/repository.js';
import * as projectsRepo from '../projects/repository.js';
import { canWriteActivity, recalcFromSubtasks } from '../activities/service.js';
import { HttpError } from '../../utils/http.js';
import { logAudit } from '../../utils/audit.js';
async function requireActivityAccess(activityId, currentUser) {
const activity = await activitiesRepo.findById(activityId);
if (!activity) throw new HttpError(404, 'Actividad no encontrada');
const project = await projectsRepo.findById(activity.project_id);
if (!(await canWriteActivity(currentUser, project, activity))) {
throw new HttpError(403, 'No tienes permisos sobre esta actividad');
}
return activity;
}
export const listByActivity = (activityId) => repo.findByActivity(activityId);
export async function createSubtask(activityId, payload, currentUser) {
await requireActivityAccess(activityId, currentUser);
if (!payload.name) throw new HttpError(400, 'El nombre es obligatorio');
const subtask = await repo.create(activityId, payload);
await logAudit({ userId: currentUser.id, action: 'create', entityType: 'subtask', entityId: subtask.id });
const counts = await repo.countsByActivity(activityId);
await recalcFromSubtasks(activityId, counts.done, counts.total);
return subtask;
}
export async function updateSubtask(id, payload, currentUser) {
const subtask = await repo.findById(id);
if (!subtask) throw new HttpError(404, 'Subtarea no encontrada');
await requireActivityAccess(subtask.activity_id, currentUser);
if (!payload.name) throw new HttpError(400, 'El nombre es obligatorio');
const updated = await repo.update(id, payload);
await logAudit({ userId: currentUser.id, action: 'update', entityType: 'subtask', entityId: id });
return updated;
}
export async function setSubtaskDone(id, isDone, currentUser) {
const subtask = await repo.findById(id);
if (!subtask) throw new HttpError(404, 'Subtarea no encontrada');
await requireActivityAccess(subtask.activity_id, currentUser);
const updated = await repo.setDone(id, isDone);
await logAudit({ userId: currentUser.id, action: isDone ? 'complete' : 'reopen', entityType: 'subtask', entityId: id });
const counts = await repo.countsByActivity(subtask.activity_id);
await recalcFromSubtasks(subtask.activity_id, counts.done, counts.total);
return updated;
}
export async function removeSubtask(id, currentUser) {
const subtask = await repo.findById(id);
if (!subtask) throw new HttpError(404, 'Subtarea no encontrada');
await requireActivityAccess(subtask.activity_id, currentUser);
await repo.remove(id);
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'subtask', entityId: id });
const counts = await repo.countsByActivity(subtask.activity_id);
await recalcFromSubtasks(subtask.activity_id, counts.done, counts.total);
}
+23
View File
@@ -0,0 +1,23 @@
import { ok, asyncHandler } from '../../utils/http.js';
import * as service from './service.js';
export const getTasks = asyncHandler(async (req, res) => {
ok(res, await service.listByActivity(Number(req.params.activityId)));
});
export const postTask = asyncHandler(async (req, res) => {
ok(res, await service.createTask(Number(req.params.activityId), req.body, req.currentUser), 201);
});
export const putTask = asyncHandler(async (req, res) => {
ok(res, await service.updateTask(Number(req.params.id), req.body, req.currentUser));
});
export const patchTaskDone = asyncHandler(async (req, res) => {
ok(res, await service.setTaskDone(Number(req.params.id), !!req.body.isDone, req.currentUser));
});
export const deleteTask = asyncHandler(async (req, res) => {
await service.removeTask(Number(req.params.id), req.currentUser);
ok(res, { deleted: true });
});
@@ -2,47 +2,47 @@ import { pool } from '../../config/db.js';
export async function findByActivity(activityId) {
const [rows] = await pool.query(
`SELECT s.id, s.activity_id, s.name, s.responsible_user_id, u.name AS responsible_name,
s.is_done, s.due_date, s.created_at
FROM subtasks s LEFT JOIN users u ON u.id = s.responsible_user_id
WHERE s.activity_id = ? ORDER BY s.created_at`,
`SELECT t.id, t.activity_id, t.name, t.responsible_user_id, u.name AS responsible_name,
t.is_done, t.due_date, t.created_at
FROM tasks t LEFT JOIN users u ON u.id = t.responsible_user_id
WHERE t.activity_id = ? ORDER BY t.created_at`,
[activityId]
);
return rows;
}
export async function findById(id) {
const [rows] = await pool.query('SELECT * FROM subtasks WHERE id = ?', [id]);
const [rows] = await pool.query('SELECT * FROM tasks WHERE id = ?', [id]);
return rows[0] || null;
}
export async function create(activityId, { name, responsibleUserId, dueDate }) {
const [result] = await pool.query(
'INSERT INTO subtasks (activity_id, name, responsible_user_id, due_date) VALUES (?, ?, ?, ?)',
'INSERT INTO tasks (activity_id, name, responsible_user_id, due_date) VALUES (?, ?, ?, ?)',
[activityId, name, responsibleUserId || null, dueDate || null]
);
return findById(result.insertId);
}
export async function update(id, { name, responsibleUserId, dueDate }) {
await pool.query('UPDATE subtasks SET name = ?, responsible_user_id = ?, due_date = ? WHERE id = ?', [
await pool.query('UPDATE tasks SET name = ?, responsible_user_id = ?, due_date = ? WHERE id = ?', [
name, responsibleUserId || null, dueDate || null, id,
]);
return findById(id);
}
export async function setDone(id, isDone) {
await pool.query('UPDATE subtasks SET is_done = ? WHERE id = ?', [isDone ? 1 : 0, id]);
await pool.query('UPDATE tasks SET is_done = ? WHERE id = ?', [isDone ? 1 : 0, id]);
return findById(id);
}
export async function remove(id) {
await pool.query('DELETE FROM subtasks WHERE id = ?', [id]);
await pool.query('DELETE FROM tasks WHERE id = ?', [id]);
}
export async function countsByActivity(activityId) {
const [rows] = await pool.query(
'SELECT COUNT(*) AS total, SUM(is_done) AS done FROM subtasks WHERE activity_id = ?',
'SELECT COUNT(*) AS total, SUM(is_done) AS done FROM tasks WHERE activity_id = ?',
[activityId]
);
return { total: rows[0].total, done: Number(rows[0].done) || 0 };
+14
View File
@@ -0,0 +1,14 @@
import { Router } from 'express';
import { requireAuth } from '../../middleware/auth.js';
import { getTasks, postTask, putTask, patchTaskDone, deleteTask } from './controller.js';
const router = Router();
router.use(requireAuth);
router.get('/activities/:activityId/tasks', getTasks);
router.post('/activities/:activityId/tasks', postTask);
router.put('/tasks/:id', putTask);
router.patch('/tasks/:id/done', patchTaskDone);
router.delete('/tasks/:id', deleteTask);
export default router;
+59
View File
@@ -0,0 +1,59 @@
import * as repo from './repository.js';
import * as activitiesRepo from '../activities/repository.js';
import * as projectsRepo from '../projects/repository.js';
import { canWriteActivity, recalcFromTasks } from '../activities/service.js';
import { HttpError } from '../../utils/http.js';
import { logAudit } from '../../utils/audit.js';
async function requireActivityAccess(activityId, currentUser) {
const activity = await activitiesRepo.findById(activityId);
if (!activity) throw new HttpError(404, 'Actividad no encontrada');
const project = await projectsRepo.findById(activity.project_id);
if (!(await canWriteActivity(currentUser, project, activity))) {
throw new HttpError(403, 'No tienes permisos sobre esta actividad');
}
return activity;
}
export const listByActivity = (activityId) => repo.findByActivity(activityId);
export async function createTask(activityId, payload, currentUser) {
await requireActivityAccess(activityId, currentUser);
if (!payload.name) throw new HttpError(400, 'El nombre es obligatorio');
const task = await repo.create(activityId, payload);
await logAudit({ userId: currentUser.id, action: 'create', entityType: 'task', entityId: task.id });
const counts = await repo.countsByActivity(activityId);
await recalcFromTasks(activityId, counts.done, counts.total);
return task;
}
export async function updateTask(id, payload, currentUser) {
const task = await repo.findById(id);
if (!task) throw new HttpError(404, 'Tarea no encontrada');
await requireActivityAccess(task.activity_id, currentUser);
if (!payload.name) throw new HttpError(400, 'El nombre es obligatorio');
const updated = await repo.update(id, payload);
await logAudit({ userId: currentUser.id, action: 'update', entityType: 'task', entityId: id });
return updated;
}
export async function setTaskDone(id, isDone, currentUser) {
const task = await repo.findById(id);
if (!task) throw new HttpError(404, 'Tarea no encontrada');
await requireActivityAccess(task.activity_id, currentUser);
const updated = await repo.setDone(id, isDone);
await logAudit({ userId: currentUser.id, action: isDone ? 'complete' : 'reopen', entityType: 'task', entityId: id });
const counts = await repo.countsByActivity(task.activity_id);
await recalcFromTasks(task.activity_id, counts.done, counts.total);
return updated;
}
export async function removeTask(id, currentUser) {
const task = await repo.findById(id);
if (!task) throw new HttpError(404, 'Tarea no encontrada');
await requireActivityAccess(task.activity_id, currentUser);
await repo.remove(id);
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'task', entityId: id });
const counts = await repo.countsByActivity(task.activity_id);
await recalcFromTasks(task.activity_id, counts.done, counts.total);
}