feat: version 2.18.0 -- Panel: detalle de cada tarjeta en popup

Las 7 tarjetas abren su lista en un DetailPopup (proyectos o actividades segun
la tarjeta); el bloque "Mis actividades" queda fijo. Nuevas listas de proximos
a vencer y para hoy; "Para hoy" deja de contar eliminadas/cancelados.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-21 08:14:39 -05:00
parent 081c90332b
commit 3884239b7e
8 changed files with 146 additions and 128 deletions
+15 -7
View File
@@ -14,23 +14,31 @@ function memberFilterFor(currentUser) {
export const getSummary = asyncHandler(async (req, res) => {
const memberUserId = memberFilterFor(req.currentUser);
const [kpis, myActivities, byStatus, overdueProjects, activeProjects, pendingActivities, overdueActivities] = await Promise.all([
const [kpis, myActivities, byStatus, overdueProjects, activeProjects, dueSoonProjects,
pendingActivities, overdueActivities, todayActivities] = 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),
repo.dueSoonProjectsList(memberUserId),
repo.pendingActivitiesList(memberUserId, 'all'),
repo.pendingActivitiesList(memberUserId, 'overdue'),
repo.pendingActivitiesList(memberUserId, 'today'),
]);
const withProjectDue = (p) => ({ ...p, dueStatus: dueStatus(p.estimated_end_date) });
const withActivityDue = (a) => ({ ...a, dueStatus: dueStatus(a.due_date) });
ok(res, {
kpis,
myActivities: myActivities.map((a) => ({ ...a, dueStatus: dueStatus(a.due_date) })),
myActivities: myActivities.map(withActivityDue),
projectsByStatus: byStatus,
overdueProjects,
activeProjects: activeProjects.map((p) => ({ ...p, dueStatus: dueStatus(p.estimated_end_date) })),
pendingActivities: pendingActivities.map((a) => ({ ...a, dueStatus: dueStatus(a.due_date) })),
// Detalle de cada tarjeta del Panel (popup al hacer clic).
overdueProjects: overdueProjects.map((p) => ({ ...p, dueStatus: 'overdue' })),
activeProjects: activeProjects.map(withProjectDue),
dueSoonProjects: dueSoonProjects.map(withProjectDue),
pendingActivities: pendingActivities.map(withActivityDue),
overdueActivities: overdueActivities.map((a) => ({ ...a, dueStatus: 'overdue' })),
todayActivities: todayActivities.map(withActivityDue),
});
});
+27 -6
View File
@@ -62,8 +62,9 @@ export async function summary(memberUserId) {
);
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}`,
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 = CURDATE() ${memberClause}`,
memberParams
);
const [[avgProgress]] = await pool.query(
@@ -86,10 +87,16 @@ export async function summary(memberUserId) {
};
}
// Mismo criterio que los KPI pendingActivities (overdueOnly=false) / overdueActivities (true).
export async function pendingActivitiesList(memberUserId, overdueOnly = false) {
// Mismo criterio que los KPI pendingActivities ('all') / overdueActivities ('overdue') /
// todayActivities ('today') -- la lista y el numero de cada tarjeta nunca difieren.
const ACTIVITY_DUE_CLAUSES = {
all: '',
overdue: 'AND a.due_date IS NOT NULL AND a.due_date < CURDATE()',
today: 'AND a.due_date = CURDATE()',
};
export async function pendingActivitiesList(memberUserId, which = 'all') {
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 overdueClause = ACTIVITY_DUE_CLAUSES[which];
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
@@ -104,6 +111,20 @@ export async function pendingActivitiesList(memberUserId, overdueOnly = false) {
return rows;
}
// Mismo criterio que el KPI dueSoonProjects de summary().
export async function dueSoonProjectsList(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 AND p.estimated_end_date IS NOT NULL
AND p.estimated_end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY) ${memberClause}
ORDER BY p.estimated_end_date, p.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 = ?)' : '';
@@ -121,7 +142,7 @@ export async function activeProjectsList(memberUserId) {
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
`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
AND p.estimated_end_date IS NOT NULL AND p.estimated_end_date < CURDATE() ${memberClause}