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
+20
View File
@@ -0,0 +1,20 @@
import { ok, asyncHandler } from '../../utils/http.js';
import * as service from './service.js';
export const getUsers = asyncHandler(async (req, res) => ok(res, await service.listUsers()));
export const getRoles = asyncHandler(async (req, res) => ok(res, await service.listRoles()));
export const postUser = asyncHandler(async (req, res) => {
const user = await service.createUser(req.body, req.currentUser.id);
ok(res, user, 201);
});
export const putUser = asyncHandler(async (req, res) => {
const user = await service.updateUser(Number(req.params.id), req.body, req.currentUser.id);
ok(res, user);
});
export const patchUserActive = asyncHandler(async (req, res) => {
const user = await service.setUserActive(Number(req.params.id), !!req.body.isActive, req.currentUser.id);
ok(res, user);
});
+54
View File
@@ -0,0 +1,54 @@
import { pool } from '../../config/db.js';
export async function findAll() {
const [rows] = await pool.query(
`SELECT u.id, u.name, u.email, u.role_id, r.code AS role_code, r.name AS role_name,
u.department_id, d.name AS department_name, u.position, u.is_active, u.created_at
FROM users u
JOIN roles r ON r.id = u.role_id
LEFT JOIN departments d ON d.id = u.department_id
ORDER BY u.name`
);
return rows;
}
export async function findById(id) {
const [rows] = await pool.query(
`SELECT u.id, u.name, u.email, u.role_id, r.code AS role_code, u.department_id, u.position, u.is_active
FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = ?`,
[id]
);
return rows[0] || null;
}
export async function findByEmail(email) {
const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
return rows[0] || null;
}
export async function create({ name, email, passwordHash, roleId, departmentId, position }) {
const [result] = await pool.query(
`INSERT INTO users (name, email, password_hash, role_id, department_id, position, is_active)
VALUES (?, ?, ?, ?, ?, ?, 1)`,
[name, email, passwordHash, roleId, departmentId || null, position || null]
);
return findById(result.insertId);
}
export async function update(id, { name, roleId, departmentId, position }) {
await pool.query(
`UPDATE users SET name = ?, role_id = ?, department_id = ?, position = ? WHERE id = ?`,
[name, roleId, departmentId || null, position || null, id]
);
return findById(id);
}
export async function setActive(id, isActive) {
await pool.query('UPDATE users SET is_active = ? WHERE id = ?', [isActive ? 1 : 0, id]);
return findById(id);
}
export async function listRoles() {
const [rows] = await pool.query('SELECT id, code, name FROM roles ORDER BY id');
return rows;
}
+14
View File
@@ -0,0 +1,14 @@
import { Router } from 'express';
import { requireAuth, requireRole, CAN_MANAGE_USERS } from '../../middleware/auth.js';
import { getUsers, getRoles, postUser, putUser, patchUserActive } from './controller.js';
const router = Router();
router.use(requireAuth);
router.get('/', getUsers);
router.get('/roles', getRoles);
router.post('/', requireRole(...CAN_MANAGE_USERS), postUser);
router.put('/:id', requireRole(...CAN_MANAGE_USERS), putUser);
router.patch('/:id/active', requireRole(...CAN_MANAGE_USERS), patchUserActive);
export default router;
+41
View File
@@ -0,0 +1,41 @@
import bcrypt from 'bcryptjs';
import * as repo from './repository.js';
import { HttpError } from '../../utils/http.js';
import { logAudit } from '../../utils/audit.js';
export const listUsers = () => repo.findAll();
export const listRoles = () => repo.listRoles();
export async function createUser(payload, actorId) {
if (!payload.name || !payload.email || !payload.password || !payload.roleId) {
throw new HttpError(400, 'Nombre, email, clave y rol son obligatorios');
}
const existing = await repo.findByEmail(payload.email);
if (existing) throw new HttpError(400, 'Ya existe un usuario con ese email');
const passwordHash = await bcrypt.hash(payload.password, 12);
const user = await repo.create({ ...payload, passwordHash });
await logAudit({ userId: actorId, action: 'create', entityType: 'user', entityId: user.id });
return user;
}
export async function updateUser(id, payload, actorId) {
const existing = await repo.findById(id);
if (!existing) throw new HttpError(404, 'Usuario no encontrado');
const user = await repo.update(id, payload);
await logAudit({ userId: actorId, action: 'update', entityType: 'user', entityId: id });
return user;
}
export async function setUserActive(id, isActive, actorId) {
const existing = await repo.findById(id);
if (!existing) throw new HttpError(404, 'Usuario no encontrado');
const user = await repo.setActive(id, isActive);
await logAudit({
userId: actorId,
action: isActive ? 'activate' : 'deactivate',
entityType: 'user',
entityId: id,
});
return user;
}