diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9d5fe2..d3f0801 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog - project.linkedcorp.net
+## [2.11.0] - 2026-10-14
+### Added
+- Listado de Actividades (dentro de un proyecto): columnas nuevas con el número de
+ dependencias, comentarios y documentos de cada actividad (siempre visible, 0 si no hay).
+- Listado de Proyectos: columnas nuevas con el número de miembros, actividades completas al
+ 100% sobre el total (ej. "1/4"), comentarios y documentos de cada proyecto (siempre visible,
+ 0 si no hay).
+
## [2.10.7] - 2026-10-12
### Changed
- Tarjetas de Notas: la fecha (abajo a la izquierda) queda al mismo nivel que los íconos de
diff --git a/VERSION b/VERSION
index b630701..46b81d8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.10.7
+2.11.0
diff --git a/backend/package.json b/backend/package.json
index 25957df..50cd47f 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "project-linkedcorp-backend",
- "version": "2.10.7",
+ "version": "2.11.0",
"private": true,
"type": "module",
"main": "src/server.js",
diff --git a/backend/src/modules/activities/repository.js b/backend/src/modules/activities/repository.js
index 762d418..899af0b 100644
--- a/backend/src/modules/activities/repository.js
+++ b/backend/src/modules/activities/repository.js
@@ -6,27 +6,41 @@ const LIST_SELECT = `
a.status_id, st.name AS status_name, st.color AS status_color, st.is_closed AS status_is_closed,
a.start_date, a.due_date, a.actual_end_date, a.progress_pct, a.is_milestone,
a.estimated_hours, a.sort_order, a.created_by, a.created_at, a.updated_at,
- a.deleted_at, a.deleted_by
+ a.deleted_at, a.deleted_by,
+ (SELECT COUNT(*) FROM activity_dependencies d WHERE d.activity_id = a.id) AS dependencies_count,
+ (SELECT COUNT(*) FROM comments c WHERE c.entity_type = 'activity' AND c.entity_id = a.id) AS comments_count,
+ (SELECT COUNT(*) FROM attachments att WHERE att.entity_type = 'activity' AND att.entity_id = a.id) AS documents_count
FROM activities a
JOIN priorities pr ON pr.id = a.priority_id
JOIN activity_statuses st ON st.id = a.status_id
LEFT JOIN users u ON u.id = a.responsible_user_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,
+ dependencies_count: Number(row.dependencies_count),
+ comments_count: Number(row.comments_count),
+ documents_count: Number(row.documents_count),
+ };
+}
+
export async function findByProject(projectId) {
const [rows] = await pool.query(`${LIST_SELECT} WHERE a.project_id = ? AND a.deleted_at IS NULL ORDER BY a.sort_order, a.id`, [projectId]);
- return rows;
+ return rows.map(castCounts);
}
export async function findTrashByProject(projectId) {
const [rows] = await pool.query(`${LIST_SELECT} WHERE a.project_id = ? AND a.deleted_at IS NOT NULL ORDER BY a.id DESC`, [projectId]);
- return rows;
+ return rows.map(castCounts);
}
export async function findById(id, { includeDeleted = false } = {}) {
const where = includeDeleted ? 'a.id = ?' : 'a.id = ? AND a.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 create(projectId, data) {
diff --git a/backend/src/modules/projects/repository.js b/backend/src/modules/projects/repository.js
index dda9156..872b2a7 100644
--- a/backend/src/modules/projects/repository.js
+++ b/backend/src/modules/projects/repository.js
@@ -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) {
diff --git a/frontend/package.json b/frontend/package.json
index dc07eda..ba6d4af 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "project-linkedcorp-frontend",
"private": true,
- "version": "2.10.7",
+ "version": "2.11.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/CountBadge.jsx b/frontend/src/components/CountBadge.jsx
new file mode 100644
index 0000000..c264c18
--- /dev/null
+++ b/frontend/src/components/CountBadge.jsx
@@ -0,0 +1,11 @@
+// Contador chico con icono para columnas de listado (dependencias/comentarios/documentos/
+// miembros). Siempre muestra el numero, incluido 0 -- a diferencia de SharedWithIcon/Tabs que
+// ocultan el badge en 0, aca el pedido es que el contador siempre sea visible.
+export function CountBadge({ icon: Icon, count, title }) {
+ return (
+
+