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
+73
View File
@@ -0,0 +1,73 @@
import * as repo from './repository.js';
import { HttpError } from '../../utils/http.js';
import { logAudit } from '../../utils/audit.js';
import { ROLES } from '../../middleware/auth.js';
// Alcance de escritura: admin/it_manager siempre; project_lead solo si es dueño o miembro.
export async function canWriteProject(user, project) {
if (user.roleCode === ROLES.ADMIN || user.roleCode === ROLES.IT_MANAGER) return true;
if (user.roleCode !== ROLES.PROJECT_LEAD) return false;
if (project.owner_user_id === user.id) return true;
return repo.isMember(project.id, user.id);
}
export const listProjects = (filters) => repo.findAll(filters);
export async function getProject(id) {
const project = await repo.findById(id);
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
const members = await repo.listMembers(id);
return { ...project, members };
}
export async function createProject(payload, currentUser) {
if (!payload.name || !payload.priorityId || !payload.statusId) {
throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios');
}
if (![ROLES.ADMIN, ROLES.IT_MANAGER, ROLES.PROJECT_LEAD].includes(currentUser.roleCode)) {
throw new HttpError(403, 'No tienes permisos para crear proyectos');
}
const project = await repo.create({
...payload,
ownerUserId: payload.ownerUserId || currentUser.id,
createdBy: currentUser.id,
});
await logAudit({ userId: currentUser.id, action: 'create', entityType: 'project', entityId: project.id });
return project;
}
export async function updateProject(id, payload, currentUser) {
const project = await repo.findById(id);
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
if (!payload.name || !payload.priorityId || !payload.statusId) {
throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios');
}
const updated = await repo.update(id, { ...payload, ownerUserId: payload.ownerUserId || project.owner_user_id });
await logAudit({ userId: currentUser.id, action: 'update', entityType: 'project', entityId: id });
return updated;
}
// Recalcula el avance del proyecto como promedio del avance de sus actividades.
export async function recalcProjectProgress(projectId) {
const avg = await repo.averageActivityProgress(projectId);
await repo.updateProgress(projectId, Math.round(avg * 100) / 100);
}
export async function addMember(projectId, userId, roleInProject, currentUser) {
const project = await repo.findById(projectId);
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
const members = await repo.addMember(projectId, userId, roleInProject);
await logAudit({ userId: currentUser.id, action: 'add_member', entityType: 'project', entityId: projectId, detail: { userId } });
return members;
}
export async function removeMember(projectId, userId, currentUser) {
const project = await repo.findById(projectId);
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
const members = await repo.removeMember(projectId, userId);
await logAudit({ userId: currentUser.id, action: 'remove_member', entityType: 'project', entityId: projectId, detail: { userId } });
return members;
}