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); }