feat: version 2.17.8 -- Panel: medidor y detalle en "Actividades pendientes"
Base = actividades no eliminadas de proyectos no cancelados; el conteo de pendientes pasa a usar el mismo criterio (antes incluia eliminadas). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,15 @@
|
|||||||
# Changelog - project.linkedcorp.net
|
# Changelog - project.linkedcorp.net
|
||||||
|
|
||||||
|
## [2.17.8] - 2026-10-21
|
||||||
|
### Changed
|
||||||
|
- Panel → "Actividades pendientes": mismo modelo que las tarjetas de proyectos. Medidor con el
|
||||||
|
porcentaje de pendientes sobre el total de actividades sin contar las de proyectos
|
||||||
|
cancelados, "N de M" debajo, y al hacer clic el bloque de abajo lista las actividades
|
||||||
|
pendientes (clic abre la actividad en su proyecto).
|
||||||
|
### Fixed
|
||||||
|
- Panel → "Actividades pendientes" contaba también actividades eliminadas y de proyectos
|
||||||
|
cancelados; ya no.
|
||||||
|
|
||||||
## [2.17.7] - 2026-10-21
|
## [2.17.7] - 2026-10-21
|
||||||
### Changed
|
### Changed
|
||||||
- Panel → "Proyectos activos": mismo modelo que "Proyectos atrasados". Medidor con el
|
- Panel → "Proyectos activos": mismo modelo que "Proyectos atrasados". Medidor con el
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "2.17.7",
|
"version": "2.17.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ function memberFilterFor(currentUser) {
|
|||||||
|
|
||||||
export const getSummary = asyncHandler(async (req, res) => {
|
export const getSummary = asyncHandler(async (req, res) => {
|
||||||
const memberUserId = memberFilterFor(req.currentUser);
|
const memberUserId = memberFilterFor(req.currentUser);
|
||||||
const [kpis, myActivities, byStatus, overdueProjects, activeProjects] = await Promise.all([
|
const [kpis, myActivities, byStatus, overdueProjects, activeProjects, pendingActivities] = await Promise.all([
|
||||||
repo.summary(memberUserId),
|
repo.summary(memberUserId),
|
||||||
repo.myPendingActivities(req.currentUser.id, memberUserId),
|
repo.myPendingActivities(req.currentUser.id, memberUserId),
|
||||||
repo.projectsByStatus(memberUserId),
|
repo.projectsByStatus(memberUserId),
|
||||||
repo.overdueProjectsList(memberUserId),
|
repo.overdueProjectsList(memberUserId),
|
||||||
repo.activeProjectsList(memberUserId),
|
repo.activeProjectsList(memberUserId),
|
||||||
|
repo.pendingActivitiesList(memberUserId),
|
||||||
]);
|
]);
|
||||||
ok(res, {
|
ok(res, {
|
||||||
kpis,
|
kpis,
|
||||||
@@ -27,6 +28,7 @@ export const getSummary = asyncHandler(async (req, res) => {
|
|||||||
projectsByStatus: byStatus,
|
projectsByStatus: byStatus,
|
||||||
overdueProjects,
|
overdueProjects,
|
||||||
activeProjects: activeProjects.map((p) => ({ ...p, dueStatus: dueStatus(p.estimated_end_date) })),
|
activeProjects: activeProjects.map((p) => ({ ...p, dueStatus: dueStatus(p.estimated_end_date) })),
|
||||||
|
pendingActivities: pendingActivities.map((a) => ({ ...a, dueStatus: dueStatus(a.due_date) })),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,18 @@ export async function summary(memberUserId) {
|
|||||||
AND p.estimated_end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY) ${memberClause}`,
|
AND p.estimated_end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY) ${memberClause}`,
|
||||||
memberParams
|
memberParams
|
||||||
);
|
);
|
||||||
|
// Pendientes y su base (medidor): sin actividades eliminadas ni de proyectos cancelados.
|
||||||
const [[pendingActivities]] = await pool.query(
|
const [[pendingActivities]] = await pool.query(
|
||||||
`SELECT COUNT(*) AS c FROM activities a JOIN activity_statuses st ON st.id = a.status_id
|
`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 projects p ON p.id = a.project_id JOIN project_statuses ps ON ps.id = p.status_id
|
||||||
WHERE st.is_closed = 0 AND p.deleted_at IS NULL ${memberClause}`,
|
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
|
memberParams
|
||||||
);
|
);
|
||||||
const [[overdueActivities]] = await pool.query(
|
const [[overdueActivities]] = await pool.query(
|
||||||
@@ -61,12 +69,30 @@ export async function summary(memberUserId) {
|
|||||||
overdueProjects: overdueProjects.c,
|
overdueProjects: overdueProjects.c,
|
||||||
dueSoonProjects: dueSoonProjects.c,
|
dueSoonProjects: dueSoonProjects.c,
|
||||||
pendingActivities: pendingActivities.c,
|
pendingActivities: pendingActivities.c,
|
||||||
|
nonCancelledActivities: nonCancelledActivities.c,
|
||||||
overdueActivities: overdueActivities.c,
|
overdueActivities: overdueActivities.c,
|
||||||
todayActivities: todayActivities.c,
|
todayActivities: todayActivities.c,
|
||||||
averageProgress: avgProgress.avg_pct === null ? 0 : Math.round(Number(avgProgress.avg_pct) * 100) / 100,
|
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) {
|
||||||
|
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 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%' ${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().
|
// Mismo criterio que el KPI activeProjects de summary().
|
||||||
export async function activeProjectsList(memberUserId) {
|
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 memberClause = memberUserId ? 'AND EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)' : '';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-frontend",
|
"name": "project-linkedcorp-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.17.7",
|
"version": "2.17.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export const messages = {
|
|||||||
dashboard_my_activities: 'Mis actividades pendientes',
|
dashboard_my_activities: 'Mis actividades pendientes',
|
||||||
dashboard_my_overdue_projects: 'Mis proyectos atrasados',
|
dashboard_my_overdue_projects: 'Mis proyectos atrasados',
|
||||||
dashboard_my_active_projects: 'Mis proyectos activos',
|
dashboard_my_active_projects: 'Mis proyectos activos',
|
||||||
|
dashboard_pending_activities_list: 'Actividades pendientes',
|
||||||
common_of: 'de',
|
common_of: 'de',
|
||||||
dashboard_back_to_my_activities: 'Ver mis actividades',
|
dashboard_back_to_my_activities: 'Ver mis actividades',
|
||||||
dashboard_projects_by_status: 'Proyectos por estado',
|
dashboard_projects_by_status: 'Proyectos por estado',
|
||||||
@@ -646,6 +647,7 @@ export const messages = {
|
|||||||
dashboard_my_activities: 'My pending activities',
|
dashboard_my_activities: 'My pending activities',
|
||||||
dashboard_my_overdue_projects: 'My overdue projects',
|
dashboard_my_overdue_projects: 'My overdue projects',
|
||||||
dashboard_my_active_projects: 'My active projects',
|
dashboard_my_active_projects: 'My active projects',
|
||||||
|
dashboard_pending_activities_list: 'Pending activities',
|
||||||
common_of: 'of',
|
common_of: 'of',
|
||||||
dashboard_back_to_my_activities: 'Show my activities',
|
dashboard_back_to_my_activities: 'Show my activities',
|
||||||
dashboard_projects_by_status: 'Projects by status',
|
dashboard_projects_by_status: 'Projects by status',
|
||||||
|
|||||||
@@ -40,6 +40,33 @@ function ProjectsDetail({ title, projects, getDueStatus, onBack }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Igual que ProjectsDetail pero para actividades (clic abre el proyecto en la actividad).
|
||||||
|
function ActivitiesDetail({ title, activities, onBack }) {
|
||||||
|
const { t } = useUi();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div className="panel-card">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<button type="button" className="link-button" onClick={onBack}>{t('dashboard_back_to_my_activities')}</button>
|
||||||
|
</div>
|
||||||
|
{activities.length === 0 && <div className="text-muted">{t('common_empty')}</div>}
|
||||||
|
{activities.map((a) => (
|
||||||
|
<div key={a.id} onClick={() => navigate(`/proyectos/${a.project_id}?tab=activities&activity=${a.id}`)} style={ROW_STYLE}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: '.82rem', fontWeight: 600 }}>{a.code} — {a.name}</div>
|
||||||
|
<div className="text-muted" style={{ fontSize: '.72rem' }}>
|
||||||
|
{a.project_code} — {a.project_name} · {a.responsible_name || '—'} · {a.due_date ? String(a.due_date).slice(0, 10) : '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 120 }}><ProgressBar value={Number(a.progress_pct)} /></div>
|
||||||
|
<DueBadge status={a.dueStatus} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { t } = useUi();
|
const { t } = useUi();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -49,7 +76,9 @@ export default function DashboardPage() {
|
|||||||
const toggleDetail = (key) => setDetail((d) => (d === key ? null : key));
|
const toggleDetail = (key) => setDetail((d) => (d === key ? null : key));
|
||||||
|
|
||||||
if (!data) return <div className="text-muted">{t('common_loading')}</div>;
|
if (!data) return <div className="text-muted">{t('common_loading')}</div>;
|
||||||
const { kpis, myActivities, projectsByStatus, overdueProjects, activeProjects } = data;
|
const { kpis, myActivities, projectsByStatus, overdueProjects, activeProjects, pendingActivities } = data;
|
||||||
|
const activitiesTotal = Number(kpis.nonCancelledActivities) || 0;
|
||||||
|
const pending = ratioOf(kpis.pendingActivities, activitiesTotal);
|
||||||
const activeTotal = Number(kpis.activeProjects) || 0;
|
const activeTotal = Number(kpis.activeProjects) || 0;
|
||||||
const nonCancelledTotal = Number(kpis.nonCancelledProjects) || 0;
|
const nonCancelledTotal = Number(kpis.nonCancelledProjects) || 0;
|
||||||
const active = ratioOf(kpis.activeProjects, nonCancelledTotal);
|
const active = ratioOf(kpis.activeProjects, nonCancelledTotal);
|
||||||
@@ -75,7 +104,13 @@ export default function DashboardPage() {
|
|||||||
selected={detail === 'overdue'}
|
selected={detail === 'overdue'}
|
||||||
/>
|
/>
|
||||||
<KpiCard label={t('dashboard_due_soon_projects')} value={kpis.dueSoonProjects} />
|
<KpiCard label={t('dashboard_due_soon_projects')} value={kpis.dueSoonProjects} />
|
||||||
<KpiCard label={t('dashboard_pending_activities')} value={kpis.pendingActivities} />
|
<KpiCard
|
||||||
|
label={t('dashboard_pending_activities')}
|
||||||
|
value={`${pending.pct}%`}
|
||||||
|
gauge={{ ratio: pending.ratio, color: 'warning', caption: `${kpis.pendingActivities} ${t('common_of')} ${activitiesTotal}` }}
|
||||||
|
onClick={() => toggleDetail('pending')}
|
||||||
|
selected={detail === 'pending'}
|
||||||
|
/>
|
||||||
<KpiCard label={t('dashboard_overdue_activities')} value={kpis.overdueActivities} />
|
<KpiCard label={t('dashboard_overdue_activities')} value={kpis.overdueActivities} />
|
||||||
<KpiCard label={t('dashboard_today_activities')} value={kpis.todayActivities} />
|
<KpiCard label={t('dashboard_today_activities')} value={kpis.todayActivities} />
|
||||||
<KpiCard label={t('dashboard_average_progress')} value={`${kpis.averageProgress}%`} />
|
<KpiCard label={t('dashboard_average_progress')} value={`${kpis.averageProgress}%`} />
|
||||||
@@ -87,6 +122,9 @@ export default function DashboardPage() {
|
|||||||
{detail === 'active' && (
|
{detail === 'active' && (
|
||||||
<ProjectsDetail title={t('dashboard_my_active_projects')} projects={activeProjects} getDueStatus={(p) => p.dueStatus} onBack={() => setDetail(null)} />
|
<ProjectsDetail title={t('dashboard_my_active_projects')} projects={activeProjects} getDueStatus={(p) => p.dueStatus} onBack={() => setDetail(null)} />
|
||||||
)}
|
)}
|
||||||
|
{detail === 'pending' && (
|
||||||
|
<ActivitiesDetail title={t('dashboard_pending_activities_list')} activities={pendingActivities} onBack={() => setDetail(null)} />
|
||||||
|
)}
|
||||||
{!detail && (
|
{!detail && (
|
||||||
<div className="panel-card">
|
<div className="panel-card">
|
||||||
<h3>{t('dashboard_my_activities')}</h3>
|
<h3>{t('dashboard_my_activities')}</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user