feat: version 2.17.9 -- Panel: medidor y detalle en "Actividades vencidas"
Base = actividades no eliminadas, no canceladas y de proyectos no cancelados; el conteo de vencidas usa el mismo criterio (antes incluia eliminadas). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog - project.linkedcorp.net
|
||||
|
||||
## [2.17.9] - 2026-10-21
|
||||
### Changed
|
||||
- Panel → "Actividades vencidas": mismo modelo. Medidor con el porcentaje de vencidas sobre el
|
||||
total de actividades sin contar las canceladas (ni las de proyectos cancelados), "N de M"
|
||||
debajo, y al hacer clic el bloque de abajo lista las actividades vencidas.
|
||||
### Fixed
|
||||
- El conteo de vencidas incluía actividades eliminadas y de proyectos cancelados; ya no.
|
||||
|
||||
## [2.17.8] - 2026-10-21
|
||||
### Changed
|
||||
- Panel → "Actividades pendientes": mismo modelo que las tarjetas de proyectos. Medidor con el
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "project-linkedcorp-backend",
|
||||
"version": "2.17.8",
|
||||
"version": "2.17.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/server.js",
|
||||
|
||||
@@ -14,13 +14,14 @@ function memberFilterFor(currentUser) {
|
||||
|
||||
export const getSummary = asyncHandler(async (req, res) => {
|
||||
const memberUserId = memberFilterFor(req.currentUser);
|
||||
const [kpis, myActivities, byStatus, overdueProjects, activeProjects, pendingActivities] = await Promise.all([
|
||||
const [kpis, myActivities, byStatus, overdueProjects, activeProjects, pendingActivities, overdueActivities] = await Promise.all([
|
||||
repo.summary(memberUserId),
|
||||
repo.myPendingActivities(req.currentUser.id, memberUserId),
|
||||
repo.projectsByStatus(memberUserId),
|
||||
repo.overdueProjectsList(memberUserId),
|
||||
repo.activeProjectsList(memberUserId),
|
||||
repo.pendingActivitiesList(memberUserId),
|
||||
repo.pendingActivitiesList(memberUserId, true),
|
||||
]);
|
||||
ok(res, {
|
||||
kpis,
|
||||
@@ -29,6 +30,7 @@ export const getSummary = asyncHandler(async (req, res) => {
|
||||
overdueProjects,
|
||||
activeProjects: activeProjects.map((p) => ({ ...p, dueStatus: dueStatus(p.estimated_end_date) })),
|
||||
pendingActivities: pendingActivities.map((a) => ({ ...a, dueStatus: dueStatus(a.due_date) })),
|
||||
overdueActivities: overdueActivities.map((a) => ({ ...a, dueStatus: 'overdue' })),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -45,10 +45,19 @@ export async function summary(memberUserId) {
|
||||
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
|
||||
WHERE st.is_closed = 0 AND p.deleted_at IS NULL AND a.due_date IS NOT NULL AND a.due_date < CURDATE() ${memberClause}`,
|
||||
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(
|
||||
@@ -70,15 +79,17 @@ export async function summary(memberUserId) {
|
||||
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 el KPI pendingActivities de summary().
|
||||
export async function pendingActivitiesList(memberUserId) {
|
||||
// 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
|
||||
@@ -86,7 +97,7 @@ export async function pendingActivitiesList(memberUserId) {
|
||||
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%' ${memberClause}
|
||||
AND ps.name NOT LIKE 'cancel%' ${overdueClause} ${memberClause}
|
||||
ORDER BY (a.due_date IS NULL), a.due_date, p.code, a.code`,
|
||||
memberUserId ? [memberUserId] : []
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "project-linkedcorp-frontend",
|
||||
"private": true,
|
||||
"version": "2.17.8",
|
||||
"version": "2.17.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -158,6 +158,7 @@ export const messages = {
|
||||
dashboard_my_overdue_projects: 'Mis proyectos atrasados',
|
||||
dashboard_my_active_projects: 'Mis proyectos activos',
|
||||
dashboard_pending_activities_list: 'Actividades pendientes',
|
||||
dashboard_overdue_activities_list: 'Actividades vencidas',
|
||||
common_of: 'de',
|
||||
dashboard_back_to_my_activities: 'Ver mis actividades',
|
||||
dashboard_projects_by_status: 'Proyectos por estado',
|
||||
@@ -648,6 +649,7 @@ export const messages = {
|
||||
dashboard_my_overdue_projects: 'My overdue projects',
|
||||
dashboard_my_active_projects: 'My active projects',
|
||||
dashboard_pending_activities_list: 'Pending activities',
|
||||
dashboard_overdue_activities_list: 'Overdue activities',
|
||||
common_of: 'of',
|
||||
dashboard_back_to_my_activities: 'Show my activities',
|
||||
dashboard_projects_by_status: 'Projects by status',
|
||||
|
||||
@@ -76,7 +76,9 @@ export default function DashboardPage() {
|
||||
const toggleDetail = (key) => setDetail((d) => (d === key ? null : key));
|
||||
|
||||
if (!data) return <div className="text-muted">{t('common_loading')}</div>;
|
||||
const { kpis, myActivities, projectsByStatus, overdueProjects, activeProjects, pendingActivities } = data;
|
||||
const { kpis, myActivities, projectsByStatus, overdueProjects, activeProjects, pendingActivities, overdueActivities } = data;
|
||||
const activitiesStrictTotal = Number(kpis.nonCancelledActivitiesStrict) || 0;
|
||||
const overdueAct = ratioOf(kpis.overdueActivities, activitiesStrictTotal);
|
||||
const activitiesTotal = Number(kpis.nonCancelledActivities) || 0;
|
||||
const pending = ratioOf(kpis.pendingActivities, activitiesTotal);
|
||||
const activeTotal = Number(kpis.activeProjects) || 0;
|
||||
@@ -111,7 +113,13 @@ export default function DashboardPage() {
|
||||
onClick={() => toggleDetail('pending')}
|
||||
selected={detail === 'pending'}
|
||||
/>
|
||||
<KpiCard label={t('dashboard_overdue_activities')} value={kpis.overdueActivities} />
|
||||
<KpiCard
|
||||
label={t('dashboard_overdue_activities')}
|
||||
value={`${overdueAct.pct}%`}
|
||||
gauge={{ ratio: overdueAct.ratio, color: 'danger', caption: `${kpis.overdueActivities} ${t('common_of')} ${activitiesStrictTotal}` }}
|
||||
onClick={() => toggleDetail('overdueActivities')}
|
||||
selected={detail === 'overdueActivities'}
|
||||
/>
|
||||
<KpiCard label={t('dashboard_today_activities')} value={kpis.todayActivities} />
|
||||
<KpiCard label={t('dashboard_average_progress')} value={`${kpis.averageProgress}%`} />
|
||||
</div>
|
||||
@@ -125,6 +133,9 @@ export default function DashboardPage() {
|
||||
{detail === 'pending' && (
|
||||
<ActivitiesDetail title={t('dashboard_pending_activities_list')} activities={pendingActivities} onBack={() => setDetail(null)} />
|
||||
)}
|
||||
{detail === 'overdueActivities' && (
|
||||
<ActivitiesDetail title={t('dashboard_overdue_activities_list')} activities={overdueActivities} onBack={() => setDetail(null)} />
|
||||
)}
|
||||
{!detail && (
|
||||
<div className="panel-card">
|
||||
<h3>{t('dashboard_my_activities')}</h3>
|
||||
|
||||
Reference in New Issue
Block a user