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 ( + + + {count} + + ); +} diff --git a/frontend/src/pages/Proyectos/ProjectActivitiesTab.jsx b/frontend/src/pages/Proyectos/ProjectActivitiesTab.jsx index 14fafec..e89a0d4 100644 --- a/frontend/src/pages/Proyectos/ProjectActivitiesTab.jsx +++ b/frontend/src/pages/Proyectos/ProjectActivitiesTab.jsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Plus, Save, Trash2, ArrowRightLeft, FolderUp, Archive, RotateCcw } from 'lucide-react'; +import { Plus, Save, Trash2, ArrowRightLeft, FolderUp, Archive, RotateCcw, Link2, MessageSquare, Paperclip } from 'lucide-react'; import { useUi } from '../../context/ui-context.jsx'; import { useAuth } from '../../context/auth-context.jsx'; import { useAsync, useAction } from '../../hooks/useAsync.js'; @@ -19,6 +19,7 @@ import { ComboBox } from '../../components/ComboBox.jsx'; import { StatusBadge, ProgressBar, DueBadge } from '../../components/StatusBadge.jsx'; import { dueStatus } from '../../utils/dueStatus.js'; import { DetailPopup } from '../../components/DetailPopup.jsx'; +import { CountBadge } from '../../components/CountBadge.jsx'; import ActivityDetailPanel from './ActivityDetailPanel.jsx'; const EMPTY_FORM = { name: '', description: '', responsibleUserId: '', priorityId: '', statusId: '', startDate: '', dueDate: '', isMilestone: false }; @@ -212,6 +213,9 @@ export default function ProjectActivitiesTab({ project, projectId, openActivityI ) }, { key: 'progress_pct', label: t('activity_progress'), render: (r) =>
}, + { key: 'dependencies_count', label: t('activity_dependencies'), render: (r) => }, + { key: 'comments_count', label: t('tab_comments'), render: (r) => }, + { key: 'documents_count', label: t('tab_documents'), render: (r) => }, { key: 'updated_at', label: t('activity_updated_at'), render: (r) => {String(r.updated_at).slice(0, 16)} }, ]; diff --git a/frontend/src/pages/Proyectos/ProyectosPage.jsx b/frontend/src/pages/Proyectos/ProyectosPage.jsx index a0a92b1..256f773 100644 --- a/frontend/src/pages/Proyectos/ProyectosPage.jsx +++ b/frontend/src/pages/Proyectos/ProyectosPage.jsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Plus, Save, Trash2, RotateCcw, ArrowLeft } from 'lucide-react'; +import { Plus, Save, Trash2, RotateCcw, ArrowLeft, Users, ListChecks, MessageSquare, Paperclip } from 'lucide-react'; import { useUi } from '../../context/ui-context.jsx'; import { useAuth } from '../../context/auth-context.jsx'; import { useAsync, useAction } from '../../hooks/useAsync.js'; @@ -20,6 +20,7 @@ import { StatusBadge, ProgressBar } from '../../components/StatusBadge.jsx'; import { Badge } from '../../components/Badge.jsx'; import { DetailPopup } from '../../components/DetailPopup.jsx'; import { SharedWithIcon } from '../../components/SharedWithIcon.jsx'; +import { CountBadge } from '../../components/CountBadge.jsx'; const CAN_DELETE_ROLES = ['superadmin', 'admin', 'it_manager', 'project_lead']; @@ -169,7 +170,13 @@ export default function ProyectosPage() { { key: 'status_name', label: t('common_status'), render: (r) => }, { key: 'priority_name', label: t('common_priority'), sortValue: (r) => r.priority_level, render: (r) => }, { key: 'owner_name', label: t('project_owner') }, + { key: 'members_count', label: t('project_members'), render: (r) => }, { key: 'progress_pct', label: t('project_progress'), render: (r) =>
}, + { key: 'activities_progress', label: t('tab_activities'), sortValue: (r) => (r.activities_total ? r.activities_complete / r.activities_total : 0), render: (r) => ( + {r.activities_complete}/{r.activities_total} + ) }, + { key: 'comments_count', label: t('tab_comments'), render: (r) => }, + { key: 'documents_count', label: t('tab_documents'), render: (r) => }, { key: 'shared', label: '', sortable: false, render: (r) => }, { key: 'risk_level', label: t('project_risk'), render: (r) => {t(`risk_${r.risk_level}`)} }, { key: 'updated_at', label: t('project_updated_at'), render: (r) => String(r.updated_at).slice(0, 16) }, diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index 9b24718..b5e2118 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -229,6 +229,7 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; } background: rgb(var(--danger) / .18); color: rgb(var(--danger)); font-size: .68rem; font-weight: 700; line-height: 1; } +.count-badge { display: inline-flex; align-items: center; gap: 4px; color: rgb(var(--muted)); font-size: .8rem; white-space: nowrap; } /* --- Cards / KPIs --- */ .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 20px; }