diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b5ce7..45d9ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog - project.linkedcorp.net +## [2.17.7] - 2026-10-21 +### Changed +- Panel → "Proyectos activos": mismo modelo que "Proyectos atrasados". Medidor con el + porcentaje de activos sobre el total de proyectos sin contar los cancelados (los completados + sí cuentan), "N de M" debajo, y al hacer clic el bloque de abajo muestra "Mis proyectos + activos". + ## [2.17.6] - 2026-10-21 ### Changed - Panel → "Proyectos atrasados": el centro del medidor muestra el porcentaje (atrasados sobre diff --git a/VERSION b/VERSION index 728789a..17bbe1e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.17.6 +2.17.7 diff --git a/backend/package.json b/backend/package.json index 4e8becc..fee5a38 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "project-linkedcorp-backend", - "version": "2.17.6", + "version": "2.17.7", "private": true, "type": "module", "main": "src/server.js", diff --git a/backend/src/modules/dashboard/controller.js b/backend/src/modules/dashboard/controller.js index 063008e..9f270c2 100644 --- a/backend/src/modules/dashboard/controller.js +++ b/backend/src/modules/dashboard/controller.js @@ -14,17 +14,19 @@ function memberFilterFor(currentUser) { export const getSummary = asyncHandler(async (req, res) => { const memberUserId = memberFilterFor(req.currentUser); - const [kpis, myActivities, byStatus, overdueProjects] = await Promise.all([ + const [kpis, myActivities, byStatus, overdueProjects, activeProjects] = await Promise.all([ repo.summary(memberUserId), repo.myPendingActivities(req.currentUser.id, memberUserId), repo.projectsByStatus(memberUserId), repo.overdueProjectsList(memberUserId), + repo.activeProjectsList(memberUserId), ]); ok(res, { kpis, myActivities: myActivities.map((a) => ({ ...a, dueStatus: dueStatus(a.due_date) })), projectsByStatus: byStatus, overdueProjects, + activeProjects: activeProjects.map((p) => ({ ...p, dueStatus: dueStatus(p.estimated_end_date) })), }); }); diff --git a/backend/src/modules/dashboard/repository.js b/backend/src/modules/dashboard/repository.js index f0b49ad..e1e5197 100644 --- a/backend/src/modules/dashboard/repository.js +++ b/backend/src/modules/dashboard/repository.js @@ -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 = ?)' : ''; diff --git a/frontend/package.json b/frontend/package.json index 9482ea0..edf536b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "project-linkedcorp-frontend", "private": true, - "version": "2.17.6", + "version": "2.17.7", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/i18n/messages.js b/frontend/src/i18n/messages.js index d8e2752..d6d1e81 100644 --- a/frontend/src/i18n/messages.js +++ b/frontend/src/i18n/messages.js @@ -156,6 +156,7 @@ export const messages = { dashboard_average_progress: 'Avance promedio', dashboard_my_activities: 'Mis actividades pendientes', dashboard_my_overdue_projects: 'Mis proyectos atrasados', + dashboard_my_active_projects: 'Mis proyectos activos', common_of: 'de', dashboard_back_to_my_activities: 'Ver mis actividades', dashboard_projects_by_status: 'Proyectos por estado', @@ -644,6 +645,7 @@ export const messages = { dashboard_average_progress: 'Average progress', dashboard_my_activities: 'My pending activities', dashboard_my_overdue_projects: 'My overdue projects', + dashboard_my_active_projects: 'My active projects', common_of: 'of', dashboard_back_to_my_activities: 'Show my activities', dashboard_projects_by_status: 'Projects by status', diff --git a/frontend/src/pages/Dashboard/DashboardPage.jsx b/frontend/src/pages/Dashboard/DashboardPage.jsx index 792d353..513447b 100644 --- a/frontend/src/pages/Dashboard/DashboardPage.jsx +++ b/frontend/src/pages/Dashboard/DashboardPage.jsx @@ -6,30 +6,71 @@ import { getSummary } from '../../services/dashboard.js'; import { KpiCard } from '../../components/KpiCard.jsx'; import { ProgressBar, DueBadge } from '../../components/StatusBadge.jsx'; +const ROW_STYLE = { display: 'flex', alignItems: 'center', gap: 12, padding: '8px 0', borderBottom: '1px solid rgb(var(--line) / .5)', cursor: 'pointer' }; + +function ratioOf(part, total) { + const ratio = total ? Number(part) / total : 0; + return { ratio, pct: Math.round(ratio * 100) }; +} + +// Lista de proyectos que reemplaza "Mis actividades" al hacer clic en una tarjeta con detalle. +function ProjectsDetail({ title, projects, getDueStatus, onBack }) { + const { t } = useUi(); + const navigate = useNavigate(); + return ( +