import { pool } from '../../config/db.js'; // memberUserId: se pasa solo para roles sin visibilidad total (ver dashboard/controller.js), // restringe cada KPI a proyectos donde el usuario es miembro -- mismo criterio que // projects/repository.js#findAll, para no mostrar conteos de proyectos que ni siquiera puede ver. export async function summary(memberUserId) { const memberClause = memberUserId ? 'AND EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)' : ''; const memberParams = memberUserId ? [memberUserId] : []; const [[activeProjects]] = await pool.query( `SELECT COUNT(*) AS c FROM projects p JOIN project_statuses ps ON ps.id = p.status_id WHERE ps.is_closed = 0 AND p.deleted_at IS NULL ${memberClause}`, memberParams ); // Base del medidor de "Proyectos activos": todos menos los cancelados (los completados si // cuentan). El catalogo no tiene bandera de "cancelado", se identifica por nombre. const [[nonCancelledProjects]] = await pool.query( `SELECT COUNT(*) AS c FROM projects p JOIN project_statuses ps ON ps.id = p.status_id WHERE p.deleted_at IS NULL AND ps.name NOT LIKE 'cancel%' ${memberClause}`, memberParams ); const [[overdueProjects]] = await pool.query( `SELECT COUNT(*) AS c FROM projects p JOIN project_statuses ps ON ps.id = p.status_id WHERE ps.is_closed = 0 AND p.deleted_at IS NULL AND p.estimated_end_date IS NOT NULL AND p.estimated_end_date < CURDATE() ${memberClause}`, memberParams ); const [[dueSoonProjects]] = await pool.query( `SELECT COUNT(*) AS c FROM projects p JOIN project_statuses ps ON ps.id = p.status_id WHERE ps.is_closed = 0 AND p.deleted_at IS NULL AND p.estimated_end_date IS NOT NULL AND p.estimated_end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY) ${memberClause}`, memberParams ); // Pendientes y su base (medidor): sin actividades eliminadas ni de proyectos cancelados. const [[pendingActivities]] = await pool.query( `SELECT COUNT(*) AS c FROM activities a JOIN activity_statuses st ON st.id = a.status_id JOIN projects p ON p.id = a.project_id JOIN project_statuses ps ON ps.id = p.status_id WHERE st.is_closed = 0 AND a.deleted_at IS NULL AND p.deleted_at IS NULL AND ps.name NOT LIKE 'cancel%' ${memberClause}`, memberParams ); const [[nonCancelledActivities]] = await pool.query( `SELECT COUNT(*) AS c FROM activities a JOIN projects p ON p.id = a.project_id JOIN project_statuses ps ON ps.id = p.status_id WHERE a.deleted_at IS NULL AND p.deleted_at IS NULL AND ps.name NOT LIKE 'cancel%' ${memberClause}`, memberParams ); // Vencidas y su base (medidor): sin eliminadas, sin actividades canceladas ni de proyectos cancelados. const [[overdueActivities]] = await pool.query( `SELECT COUNT(*) AS c FROM activities a JOIN activity_statuses st ON st.id = a.status_id JOIN projects p ON p.id = a.project_id JOIN project_statuses ps ON ps.id = p.status_id WHERE st.is_closed = 0 AND a.deleted_at IS NULL AND p.deleted_at IS NULL AND ps.name NOT LIKE 'cancel%' AND a.due_date IS NOT NULL AND a.due_date < CURDATE() ${memberClause}`, memberParams ); const [[nonCancelledActivitiesStrict]] = await pool.query( `SELECT COUNT(*) AS c FROM activities a JOIN activity_statuses st ON st.id = a.status_id JOIN projects p ON p.id = a.project_id JOIN project_statuses ps ON ps.id = p.status_id WHERE a.deleted_at IS NULL AND p.deleted_at IS NULL AND ps.name NOT LIKE 'cancel%' AND st.name NOT LIKE 'cancel%' ${memberClause}`, memberParams ); const [[todayActivities]] = await pool.query( `SELECT COUNT(*) AS c FROM activities a JOIN activity_statuses st ON st.id = a.status_id JOIN projects p ON p.id = a.project_id WHERE st.is_closed = 0 AND p.deleted_at IS NULL AND a.due_date = CURDATE() ${memberClause}`, memberParams ); const [[avgProgress]] = await pool.query( `SELECT AVG(progress_pct) AS avg_pct FROM projects p JOIN project_statuses ps ON ps.id = p.status_id WHERE ps.is_closed = 0 AND p.deleted_at IS NULL ${memberClause}`, memberParams ); return { activeProjects: activeProjects.c, nonCancelledProjects: nonCancelledProjects.c, overdueProjects: overdueProjects.c, dueSoonProjects: dueSoonProjects.c, pendingActivities: pendingActivities.c, nonCancelledActivities: nonCancelledActivities.c, nonCancelledActivitiesStrict: nonCancelledActivitiesStrict.c, overdueActivities: overdueActivities.c, todayActivities: todayActivities.c, averageProgress: avgProgress.avg_pct === null ? 0 : Math.round(Number(avgProgress.avg_pct) * 100) / 100, }; } // Mismo criterio que los KPI pendingActivities (overdueOnly=false) / overdueActivities (true). export async function pendingActivitiesList(memberUserId, overdueOnly = false) { const memberClause = memberUserId ? 'AND EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)' : ''; const overdueClause = overdueOnly ? 'AND a.due_date IS NOT NULL AND a.due_date < CURDATE()' : ''; const [rows] = await pool.query( `SELECT a.id, a.code, a.name, a.due_date, a.progress_pct, p.id AS project_id, p.code AS project_code, p.name AS project_name, u.name AS responsible_name FROM activities a JOIN activity_statuses st ON st.id = a.status_id JOIN projects p ON p.id = a.project_id JOIN project_statuses ps ON ps.id = p.status_id LEFT JOIN users u ON u.id = a.responsible_user_id WHERE st.is_closed = 0 AND a.deleted_at IS NULL AND p.deleted_at IS NULL AND ps.name NOT LIKE 'cancel%' ${overdueClause} ${memberClause} ORDER BY (a.due_date IS NULL), a.due_date, p.code, a.code`, memberUserId ? [memberUserId] : [] ); return rows; } // Mismo criterio que el KPI activeProjects de summary(). export async function activeProjectsList(memberUserId) { const memberClause = memberUserId ? 'AND EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)' : ''; const [rows] = await pool.query( `SELECT p.id, p.code, p.name, p.estimated_end_date, p.progress_pct, u.name AS owner_name, ps.name AS status_name FROM projects p JOIN project_statuses ps ON ps.id = p.status_id LEFT JOIN users u ON u.id = p.owner_user_id WHERE ps.is_closed = 0 AND p.deleted_at IS NULL ${memberClause} ORDER BY (p.estimated_end_date IS NULL), p.estimated_end_date, p.code`, memberUserId ? [memberUserId] : [] ); return rows; } // Mismo criterio que el KPI overdueProjects de summary() -- la lista y el numero nunca difieren. export async function overdueProjectsList(memberUserId) { const memberClause = memberUserId ? 'AND EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)' : ''; const [rows] = await pool.query( `SELECT p.id, p.code, p.name, p.estimated_end_date, p.progress_pct, u.name AS owner_name FROM projects p JOIN project_statuses ps ON ps.id = p.status_id LEFT JOIN users u ON u.id = p.owner_user_id WHERE ps.is_closed = 0 AND p.deleted_at IS NULL AND p.estimated_end_date IS NOT NULL AND p.estimated_end_date < CURDATE() ${memberClause} ORDER BY p.estimated_end_date`, memberUserId ? [memberUserId] : [] ); return rows; } export async function myPendingActivities(userId, memberUserId) { const memberClause = memberUserId ? 'AND EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)' : ''; const params = memberUserId ? [userId, memberUserId] : [userId]; const [rows] = await pool.query( `SELECT a.id, a.code, a.name, a.due_date, a.progress_pct, p.id AS project_id, p.name AS project_name FROM activities a JOIN activity_statuses st ON st.id = a.status_id JOIN projects p ON p.id = a.project_id WHERE st.is_closed = 0 AND p.deleted_at IS NULL AND a.responsible_user_id = ? ${memberClause} ORDER BY (a.due_date IS NULL), a.due_date LIMIT 20`, params ); return rows; } export async function calendarActivities(monthStart, monthEnd, memberUserId) { const memberClause = memberUserId ? 'AND EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)' : ''; const params = memberUserId ? [monthStart, monthEnd, memberUserId] : [monthStart, monthEnd]; const [rows] = await pool.query( `SELECT a.id, a.code, a.name, a.due_date, a.is_milestone, a.progress_pct, st.is_closed AS status_is_closed, p.id AS project_id, p.name AS project_name FROM activities a JOIN activity_statuses st ON st.id = a.status_id JOIN projects p ON p.id = a.project_id WHERE a.due_date BETWEEN ? AND ? AND p.deleted_at IS NULL ${memberClause} ORDER BY a.due_date`, params ); return rows; } export async function projectsByStatus(memberUserId) { const memberClause = memberUserId ? 'AND EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)' : ''; const params = memberUserId ? [memberUserId] : []; const [rows] = await pool.query( `SELECT ps.name, ps.color, COUNT(p.id) AS total FROM project_statuses ps LEFT JOIN projects p ON p.status_id = ps.id AND p.deleted_at IS NULL ${memberClause} GROUP BY ps.id ORDER BY ps.sort_order`, params ); return rows; }