chore: estructura inicial del MVP

Backend (Express + MariaDB) y frontend (React + Vite, identidad Fluent/Windows) con el
nucleo funcional: seguridad, organizacion, proyectos, actividades, subtareas, dependencias,
dashboard, calendario, gantt, comentarios, adjuntos, notificaciones in-app y catalogos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-03 08:37:58 -05:00
commit 1d22adc42e
135 changed files with 10913 additions and 0 deletions
+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, 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);
}