feat: version 2.11.0 -- contadores de dependencias/comentarios/documentos/miembros en listados

Actividades (dentro de un proyecto): nuevas columnas con la cantidad de dependencias,
comentarios y documentos de cada fila -- subconsultas correlacionadas agregadas al
LIST_SELECT existente (mismo patron que share_count de notas/proyectos), siempre numericas
(0 si no hay, nunca oculto).

Proyectos: nuevas columnas con la cantidad de miembros, actividades completas al 100% sobre
el total (ej. "1/4", mismo patron pendientes/total que ya usa la pestaña de tareas de una
actividad), comentarios y documentos.

Nuevo componente CountBadge (icono + numero) reutilizado en ambos listados -- a diferencia de
SharedWithIcon/Tabs que ocultan el badge en 0, este siempre lo muestra.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-14 11:40:07 -05:00
parent 481ba008da
commit 4520abe96f
10 changed files with 76 additions and 12 deletions
+22 -3
View File
@@ -9,7 +9,12 @@ const LIST_SELECT = `
p.responsible_department_id, sd.name AS responsible_department_name,
p.start_date, p.estimated_end_date, p.actual_end_date, p.progress_pct,
p.risk_level, p.notes, p.created_by, p.created_at, p.updated_at, p.deleted_at, p.deleted_by,
(SELECT COUNT(*) FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id <> p.owner_user_id) AS shared_count
(SELECT COUNT(*) FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id <> p.owner_user_id) AS shared_count,
(SELECT COUNT(*) FROM project_members pm2 WHERE pm2.project_id = p.id) AS members_count,
(SELECT COUNT(*) FROM activities act WHERE act.project_id = p.id AND act.deleted_at IS NULL) AS activities_total,
(SELECT COUNT(*) FROM activities act2 WHERE act2.project_id = p.id AND act2.deleted_at IS NULL AND act2.progress_pct >= 100) AS activities_complete,
(SELECT COUNT(*) FROM comments c WHERE c.entity_type = 'project' AND c.entity_id = p.id) AS comments_count,
(SELECT COUNT(*) FROM attachments att WHERE att.entity_type = 'project' AND att.entity_id = p.id) AS documents_count
FROM projects p
JOIN priorities pr ON pr.id = p.priority_id
JOIN project_statuses ps ON ps.id = p.status_id
@@ -19,6 +24,20 @@ const LIST_SELECT = `
LEFT JOIN departments sd ON sd.id = p.responsible_department_id
`;
// Los COUNT(*) de subconsultas correlacionadas vuelven de mysql2 como string/BigInt-like --
// se castean a Number aqui para que el frontend los reciba como numero real (orden, comparar).
function castCounts(row) {
return {
...row,
shared_count: Number(row.shared_count),
members_count: Number(row.members_count),
activities_total: Number(row.activities_total),
activities_complete: Number(row.activities_complete),
comments_count: Number(row.comments_count),
documents_count: Number(row.documents_count),
};
}
export async function findAll({ statusId, priorityId, ownerUserId, memberUserId, trash } = {}) {
const clauses = [trash ? 'p.deleted_at IS NOT NULL' : 'p.deleted_at IS NULL'];
const params = [];
@@ -33,13 +52,13 @@ export async function findAll({ statusId, priorityId, ownerUserId, memberUserId,
}
const where = `WHERE ${clauses.join(' AND ')}`;
const [rows] = await pool.query(`${LIST_SELECT} ${where} ORDER BY p.created_at DESC`, params);
return rows;
return rows.map(castCounts);
}
export async function findById(id, { includeDeleted = false } = {}) {
const where = includeDeleted ? 'p.id = ?' : 'p.id = ? AND p.deleted_at IS NULL';
const [rows] = await pool.query(`${LIST_SELECT} WHERE ${where}`, [id]);
return rows[0] || null;
return rows[0] ? castCounts(rows[0]) : null;
}
export async function softDelete(id, userId) {