feat: version 2.17.7 -- Panel: medidor y detalle en "Proyectos activos"

Porcentaje de activos sobre el total sin cancelados, "N de M" y lista de
proyectos activos al hacer clic. La lista de detalle pasa a un componente
compartido con "Proyectos atrasados".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-21 07:52:49 -05:00
parent d15a58cb29
commit 10952beea5
8 changed files with 99 additions and 38 deletions
@@ -12,6 +12,13 @@ export async function summary(memberUserId) {
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
@@ -50,6 +57,7 @@ export async function summary(memberUserId) {
return {
activeProjects: activeProjects.c,
nonCancelledProjects: nonCancelledProjects.c,
overdueProjects: overdueProjects.c,
dueSoonProjects: dueSoonProjects.c,
pendingActivities: pendingActivities.c,
@@ -59,6 +67,19 @@ export async function summary(memberUserId) {
};
}
// 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 = ?)' : '';