80981e9063
Hook reutilizable useDragReorder (drag & drop nativo HTML5, sin libreria nueva) usado en dos lugares: ListView (opt-in via onReorder, deshabilitado mientras haya un ordenamiento por columna activo) para el listado de actividades de un proyecto, y el listado de tareas de una actividad. Orden persistido en sort_order (activities y tasks, migracion 012, posiciones siempre 1..N). Fix real encontrado durante las pruebas: sincronizar el estado interno del hook por la REFERENCIA del arreglo de entrada entraba en loop infinito de renders cuando el llamador pasa "algo || []" (un arreglo nuevo en cada render mientras los datos cargan). Corregido para sincronizar por el contenido (ids), no por referencia. Probado en dev con curl (persistencia del orden) y Playwright (arrastrar filas en el navegador, orden visible y guardado correctamente, sin errores de consola tras el fix). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
3.2 KiB
JavaScript
68 lines
3.2 KiB
JavaScript
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;
|
|
}
|
|
|
|
// Nuevo orden manual (arrastrar con el mouse): mismo permiso que crear/editar tareas de la actividad.
|
|
export async function reorderTasks(activityId, ids, currentUser) {
|
|
await requireActivityAccess(activityId, currentUser);
|
|
if (!Array.isArray(ids) || ids.length === 0) throw new HttpError(400, 'Falta el orden de tareas');
|
|
await repo.reorder(activityId, ids);
|
|
return repo.findByActivity(activityId);
|
|
}
|
|
|
|
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);
|
|
}
|