Files
project.linkedcorp.net/backend/src/modules/projects/repository.js
T
Milton Gonzalez fda83e9dff fix: version 2.5.0 -- compartir proyectos con permisos reales (Lectura/Escritura)
Bug de seguridad real corregido: cualquier usuario autenticado veia
TODOS los proyectos (listado, detalle, actividades, tareas,
comentarios, adjuntos, KPIs del panel), sin importar si era miembro.
Ahora nadie ve un proyecto ajeno sin que su dueno se lo comparta --
los roles de administracion (superadmin/admin/it_manager) mantienen
visibilidad total para soporte.

Nuevo modelo de permisos por proyecto, simple y no granular:
- Solo el dueno original puede compartir un proyecto (agregar/quitar
  miembros, cambiar su nivel) -- ni un admin del proyecto ni un rol
  global pueden hacerlo por el.
- Dos niveles: Lectura (solo ver) y Escritura (crear/editar/eliminar
  actividades y tareas -- nunca el proyecto en si: no lo borra, no
  edita sus datos, no crea proyectos nuevos).

Actividades y tareas pasan a borrado logico (antes se borraban
fisico sin poder recuperar) -- nueva papelera por proyecto con
restaurar.

Incluye tambien el filtro "Usuario" en Proyectos (probado en dev en
un paso anterior), que va de la mano con este cambio de visibilidad.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:19:41 -05:00

165 lines
7.2 KiB
JavaScript

import { pool } from '../../config/db.js';
const LIST_SELECT = `
SELECT p.id, p.code, p.name, p.description, p.objective, p.project_type_id, pt.name AS project_type_name,
p.priority_id, pr.name AS priority_name, pr.color AS priority_color, pr.level AS priority_level,
p.status_id, ps.name AS status_name, ps.color AS status_color, ps.is_closed AS status_is_closed,
p.owner_user_id, u.name AS owner_name,
p.requesting_department_id, rd.name AS requesting_department_name,
p.responsible_department_id, sd.name AS responsible_department_name,
p.start_date, p.estimated_end_date, p.actual_end_date, p.progress_pct,
p.risk_level, p.notes, p.created_by, p.created_at, p.updated_at, p.deleted_at, p.deleted_by
FROM projects p
JOIN priorities pr ON pr.id = p.priority_id
JOIN project_statuses ps ON ps.id = p.status_id
JOIN users u ON u.id = p.owner_user_id
LEFT JOIN project_types pt ON pt.id = p.project_type_id
LEFT JOIN departments rd ON rd.id = p.requesting_department_id
LEFT JOIN departments sd ON sd.id = p.responsible_department_id
`;
export async function findAll({ statusId, priorityId, ownerUserId, memberUserId, trash } = {}) {
const clauses = [trash ? 'p.deleted_at IS NOT NULL' : 'p.deleted_at IS NULL'];
const params = [];
if (statusId) { clauses.push('p.status_id = ?'); params.push(statusId); }
if (priorityId) { clauses.push('p.priority_id = ?'); params.push(priorityId); }
if (ownerUserId) { clauses.push('p.owner_user_id = ?'); params.push(ownerUserId); }
// Restringe a proyectos donde el usuario es miembro (compartidos con el, o el propio dueño ya
// que quien crea un proyecto queda como miembro 'owner' automaticamente -- ver create()).
if (memberUserId) {
clauses.push('EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)');
params.push(memberUserId);
}
const where = `WHERE ${clauses.join(' AND ')}`;
const [rows] = await pool.query(`${LIST_SELECT} ${where} ORDER BY p.created_at DESC`, params);
return rows;
}
export async function findById(id, { includeDeleted = false } = {}) {
const where = includeDeleted ? 'p.id = ?' : 'p.id = ? AND p.deleted_at IS NULL';
const [rows] = await pool.query(`${LIST_SELECT} WHERE ${where}`, [id]);
return rows[0] || null;
}
export async function softDelete(id, userId) {
await pool.query('UPDATE projects SET deleted_at = NOW(), deleted_by = ? WHERE id = ? AND deleted_at IS NULL', [userId, id]);
}
export async function restore(id) {
await pool.query('UPDATE projects SET deleted_at = NULL, deleted_by = NULL WHERE id = ?', [id]);
}
export async function hardDelete(id) {
await pool.query('DELETE FROM projects WHERE id = ?', [id]);
}
export async function create(data) {
const [result] = await pool.query(
`INSERT INTO projects
(code, name, description, objective, project_type_id, priority_id, status_id, owner_user_id,
requesting_department_id, responsible_department_id, start_date, estimated_end_date,
risk_level, notes, created_by)
VALUES ('PENDING', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
data.name, data.description || null, data.objective || null, data.projectTypeId || null,
data.priorityId, data.statusId, data.ownerUserId,
data.requestingDepartmentId || null, data.responsibleDepartmentId || null,
data.startDate || null, data.estimatedEndDate || null,
data.riskLevel || 'low', data.notes || null, data.createdBy,
]
);
const id = result.insertId;
const code = `PRY-${String(id).padStart(4, '0')}`;
await pool.query('UPDATE projects SET code = ? WHERE id = ?', [code, id]);
// El creador queda como miembro por defecto.
await pool.query('INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (?, ?, ?)', [
id, data.ownerUserId, 'owner',
]);
return findById(id);
}
// Usado al crear un proyecto sin elegir estado explicito (ej. al convertir una actividad en
// proyecto): el primer estado no-cerrado por orden manual, tipicamente "Planificado".
export async function getDefaultStatusId() {
const [rows] = await pool.query('SELECT id FROM project_statuses WHERE is_closed = 0 ORDER BY sort_order LIMIT 1');
return rows[0]?.id || null;
}
export async function update(id, data) {
await pool.query(
`UPDATE projects SET
name = ?, description = ?, objective = ?, project_type_id = ?, priority_id = ?, status_id = ?,
owner_user_id = ?, requesting_department_id = ?, responsible_department_id = ?,
start_date = ?, estimated_end_date = ?, actual_end_date = ?, risk_level = ?, notes = ?
WHERE id = ?`,
[
data.name, data.description || null, data.objective || null, data.projectTypeId || null,
data.priorityId, data.statusId, data.ownerUserId,
data.requestingDepartmentId || null, data.responsibleDepartmentId || null,
data.startDate || null, data.estimatedEndDate || null, data.actualEndDate || null,
data.riskLevel || 'low', data.notes || null, id,
]
);
return findById(id);
}
export async function updateProgress(id, progressPct) {
await pool.query('UPDATE projects SET progress_pct = ? WHERE id = ?', [progressPct, id]);
}
export async function listMembers(projectId) {
const [rows] = await pool.query(
`SELECT pm.user_id, u.name, u.email, pm.role_in_project, pm.added_at
FROM project_members pm JOIN users u ON u.id = pm.user_id
WHERE pm.project_id = ? ORDER BY u.name`,
[projectId]
);
return rows;
}
export async function isMember(projectId, userId) {
const [rows] = await pool.query('SELECT 1 FROM project_members WHERE project_id = ? AND user_id = ?', [
projectId, userId,
]);
return rows.length > 0;
}
// null = no es miembro; 'member' = miembro sin rol especifico; 'admin'/'owner' = con ese rol.
export async function getMembership(projectId, userId) {
const [rows] = await pool.query('SELECT role_in_project FROM project_members WHERE project_id = ? AND user_id = ?', [
projectId, userId,
]);
if (!rows.length) return null;
return rows[0].role_in_project || 'member';
}
export async function addMember(projectId, userId, roleInProject) {
await pool.query(
'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (?, ?, ?)',
[projectId, userId, roleInProject || null]
);
return listMembers(projectId);
}
// Nunca toca una fila con role_in_project = 'owner' (protege al dueño de perder ese rol por aqui).
export async function updateMemberRole(projectId, userId, roleInProject) {
await pool.query(
`UPDATE project_members SET role_in_project = ?
WHERE project_id = ? AND user_id = ? AND (role_in_project IS NULL OR role_in_project <> 'owner')`,
[roleInProject, projectId, userId]
);
return listMembers(projectId);
}
export async function removeMember(projectId, userId) {
await pool.query('DELETE FROM project_members WHERE project_id = ? AND user_id = ?', [projectId, userId]);
return listMembers(projectId);
}
export async function averageActivityProgress(projectId) {
const [rows] = await pool.query('SELECT AVG(progress_pct) AS avg_pct FROM activities WHERE project_id = ?', [
projectId,
]);
return rows[0].avg_pct === null ? 0 : Number(rows[0].avg_pct);
}