fix: version 2.11.1 -- mueve los contadores de 2.11.0 de columnas de grilla a badges de pestaña
La 2.11.0 agrego los contadores de dependencias/comentarios/documentos/miembros como columnas nuevas en las tablas de Proyectos y Actividades -- no era el pedido original, desbordaba la grilla. Se revierten esas columnas (y el componente CountBadge que solo ellas usaban) y los contadores van donde correspondia desde el principio: como badge junto al nombre de cada pestaña, al mismo estilo que ya usaba "Tareas n/n" (componente Tabs + .tab-badge): - Proyecto: pestañas Miembros, Actividades (completas/total), Comentarios, Documentos. - Actividad: pestaña Dependencias (ya existia Tareas/Comentarios/Documentos, solo faltaba esta). De paso: Tabs.jsx ocultaba el badge cuando count era 0 (!!tab.count) -- se cambia a mostrar siempre que la pestaña mande la prop (count !== undefined), para que 0 se vea como "0" y no desaparezca. CommentsPanel/AttachmentsPanel ganan un onChanged opcional para que el contador de Comentarios/Documentos del PROYECTO se actualice al tiro sin tener que recargar la pagina (el de la actividad ya se refrescaba solo al cambiar de pestaña). El backend de 2.11.0 (subconsultas dependencies_count/comments_count/documents_count/ members_count/activities_total/activities_complete en los LIST_SELECT) se mantiene igual, solo cambia donde el frontend los usa. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog - project.linkedcorp.net
|
# Changelog - project.linkedcorp.net
|
||||||
|
|
||||||
|
## [2.11.1] - 2026-10-14
|
||||||
|
### Fixed
|
||||||
|
- La 2.11.0 puso los contadores de dependencias/comentarios/documentos/miembros como columnas
|
||||||
|
nuevas en las grillas de Proyectos y Actividades — no era lo pedido, desbordaba la tabla.
|
||||||
|
Se quitan esas columnas y los contadores van donde correspondía: como badge junto al nombre
|
||||||
|
de cada pestaña (Miembros, Actividades, Comentarios, Documentos del proyecto; Dependencias,
|
||||||
|
Comentarios, Documentos de la actividad), al mismo estilo que ya usaba "Tareas n/n". De paso
|
||||||
|
se corrige que el badge de una pestaña se ocultaba en 0 en vez de mostrarlo.
|
||||||
|
|
||||||
## [2.11.0] - 2026-10-14
|
## [2.11.0] - 2026-10-14
|
||||||
### Added
|
### Added
|
||||||
- Listado de Actividades (dentro de un proyecto): columnas nuevas con el número de
|
- Listado de Actividades (dentro de un proyecto): columnas nuevas con el número de
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "2.11.0",
|
"version": "2.11.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-frontend",
|
"name": "project-linkedcorp-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.11.0",
|
"version": "2.11.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import hljsDark from 'highlight.js/styles/vs2015.css?inline';
|
|||||||
|
|
||||||
const VIEWABLE_MIME = new Set(['text/plain', 'text/markdown']);
|
const VIEWABLE_MIME = new Set(['text/plain', 'text/markdown']);
|
||||||
|
|
||||||
export function AttachmentsPanel({ entityType, entityId }) {
|
export function AttachmentsPanel({ entityType, entityId, onChanged }) {
|
||||||
const { t, openDialog, theme } = useUi();
|
const { t, openDialog, theme } = useUi();
|
||||||
const runAction = useAction();
|
const runAction = useAction();
|
||||||
const { data: files, reload } = useAsync(() => listAttachments(entityType, entityId), [entityType, entityId]);
|
const { data: files, reload } = useAsync(() => listAttachments(entityType, entityId), [entityType, entityId]);
|
||||||
@@ -41,6 +41,7 @@ export function AttachmentsPanel({ entityType, entityId }) {
|
|||||||
await runAction(async () => {
|
await runAction(async () => {
|
||||||
await uploadAttachment(entityType, entityId, file);
|
await uploadAttachment(entityType, entityId, file);
|
||||||
await reload();
|
await reload();
|
||||||
|
onChanged?.(); // refresca el contador de la pestaña "Documentos" en el padre
|
||||||
});
|
});
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ export function AttachmentsPanel({ entityType, entityId }) {
|
|||||||
message: t('common_confirm_delete_msg'),
|
message: t('common_confirm_delete_msg'),
|
||||||
actions: [
|
actions: [
|
||||||
{ label: t('common_cancel'), variant: 'standard' },
|
{ label: t('common_cancel'), variant: 'standard' },
|
||||||
{ label: t('common_delete'), variant: 'danger', onClick: () => runAction(async () => { await deleteAttachment(file.id); await reload(); }) },
|
{ label: t('common_delete'), variant: 'danger', onClick: () => runAction(async () => { await deleteAttachment(file.id); await reload(); onChanged?.(); }) },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useAsync, useAction } from '../hooks/useAsync.js';
|
|||||||
import { listComments, createComment } from '../services/comments.js';
|
import { listComments, createComment } from '../services/comments.js';
|
||||||
import { Button } from './Button.jsx';
|
import { Button } from './Button.jsx';
|
||||||
|
|
||||||
export function CommentsPanel({ entityType, entityId }) {
|
export function CommentsPanel({ entityType, entityId, onChanged }) {
|
||||||
const { t } = useUi();
|
const { t } = useUi();
|
||||||
const runAction = useAction();
|
const runAction = useAction();
|
||||||
const { data: comments, reload } = useAsync(() => listComments(entityType, entityId), [entityType, entityId]);
|
const { data: comments, reload } = useAsync(() => listComments(entityType, entityId), [entityType, entityId]);
|
||||||
@@ -17,6 +17,7 @@ export function CommentsPanel({ entityType, entityId }) {
|
|||||||
await createComment(entityType, entityId, message.trim());
|
await createComment(entityType, entityId, message.trim());
|
||||||
setMessage('');
|
setMessage('');
|
||||||
await reload();
|
await reload();
|
||||||
|
onChanged?.(); // refresca el contador de la pestaña "Comentarios" en el padre
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
<span className="count-badge" title={title}>
|
|
||||||
<Icon size={14} />
|
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,9 @@ export function Tabs({ tabs, active, onChange }) {
|
|||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button key={tab.key} className={`tab-item ${active === tab.key ? 'active' : ''}`} onClick={() => onChange(tab.key)}>
|
<button key={tab.key} className={`tab-item ${active === tab.key ? 'active' : ''}`} onClick={() => onChange(tab.key)}>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
{!!tab.count && <span className="tab-badge">{tab.count}</span>}
|
{/* count=0 SI se muestra (badge "0") -- solo se omite si la pestaña ni siquiera manda
|
||||||
|
la prop (undefined), para pestañas que no tienen numero que mostrar (Info/Gantt). */}
|
||||||
|
{tab.count !== undefined && <span className="tab-badge">{tab.count}</span>}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
|||||||
tabs={[
|
tabs={[
|
||||||
{ key: 'detail', label: t('tab_info') },
|
{ key: 'detail', label: t('tab_info') },
|
||||||
{ key: 'tasks', label: t('tasks_title'), count: tasksTabCount },
|
{ key: 'tasks', label: t('tasks_title'), count: tasksTabCount },
|
||||||
{ key: 'dependencies', label: t('activity_dependencies') },
|
{ key: 'dependencies', label: t('activity_dependencies'), count: (activity.dependencies || []).length },
|
||||||
{ key: 'comments', label: t('tab_comments'), count: (comments || []).length },
|
{ key: 'comments', label: t('tab_comments'), count: (comments || []).length },
|
||||||
{ key: 'documents', label: t('tab_documents'), count: (attachments || []).length },
|
{ key: 'documents', label: t('tab_documents'), count: (attachments || []).length },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Plus, Save, Trash2, ArrowRightLeft, FolderUp, Archive, RotateCcw, Link2, MessageSquare, Paperclip } from 'lucide-react';
|
import { Plus, Save, Trash2, ArrowRightLeft, FolderUp, Archive, RotateCcw } from 'lucide-react';
|
||||||
import { useUi } from '../../context/ui-context.jsx';
|
import { useUi } from '../../context/ui-context.jsx';
|
||||||
import { useAuth } from '../../context/auth-context.jsx';
|
import { useAuth } from '../../context/auth-context.jsx';
|
||||||
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
||||||
@@ -19,7 +19,6 @@ import { ComboBox } from '../../components/ComboBox.jsx';
|
|||||||
import { StatusBadge, ProgressBar, DueBadge } from '../../components/StatusBadge.jsx';
|
import { StatusBadge, ProgressBar, DueBadge } from '../../components/StatusBadge.jsx';
|
||||||
import { dueStatus } from '../../utils/dueStatus.js';
|
import { dueStatus } from '../../utils/dueStatus.js';
|
||||||
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
||||||
import { CountBadge } from '../../components/CountBadge.jsx';
|
|
||||||
import ActivityDetailPanel from './ActivityDetailPanel.jsx';
|
import ActivityDetailPanel from './ActivityDetailPanel.jsx';
|
||||||
|
|
||||||
const EMPTY_FORM = { name: '', description: '', responsibleUserId: '', priorityId: '', statusId: '', startDate: '', dueDate: '', isMilestone: false };
|
const EMPTY_FORM = { name: '', description: '', responsibleUserId: '', priorityId: '', statusId: '', startDate: '', dueDate: '', isMilestone: false };
|
||||||
@@ -213,9 +212,6 @@ export default function ProjectActivitiesTab({ project, projectId, openActivityI
|
|||||||
</div>
|
</div>
|
||||||
) },
|
) },
|
||||||
{ key: 'progress_pct', label: t('activity_progress'), render: (r) => <div style={{ width: 100 }}><ProgressBar value={Number(r.progress_pct)} /></div> },
|
{ key: 'progress_pct', label: t('activity_progress'), render: (r) => <div style={{ width: 100 }}><ProgressBar value={Number(r.progress_pct)} /></div> },
|
||||||
{ key: 'dependencies_count', label: t('activity_dependencies'), render: (r) => <CountBadge icon={Link2} count={r.dependencies_count} title={t('activity_dependencies')} /> },
|
|
||||||
{ key: 'comments_count', label: t('tab_comments'), render: (r) => <CountBadge icon={MessageSquare} count={r.comments_count} title={t('tab_comments')} /> },
|
|
||||||
{ key: 'documents_count', label: t('tab_documents'), render: (r) => <CountBadge icon={Paperclip} count={r.documents_count} title={t('tab_documents')} /> },
|
|
||||||
{ key: 'updated_at', label: t('activity_updated_at'), render: (r) => <span style={{ whiteSpace: 'nowrap' }}>{String(r.updated_at).slice(0, 16)}</span> },
|
{ key: 'updated_at', label: t('activity_updated_at'), render: (r) => <span style={{ whiteSpace: 'nowrap' }}>{String(r.updated_at).slice(0, 16)}</span> },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ export default function ProyectoDetallePage() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
tabs={[
|
tabs={[
|
||||||
{ key: 'info', label: t('tab_info') },
|
{ key: 'info', label: t('tab_info') },
|
||||||
{ key: 'members', label: t('tab_members') },
|
{ key: 'members', label: t('tab_members'), count: project.members_count },
|
||||||
{ key: 'activities', label: t('tab_activities') },
|
{ key: 'activities', label: t('tab_activities'), count: `${project.activities_complete}/${project.activities_total}` },
|
||||||
{ key: 'gantt', label: t('tab_gantt') },
|
{ key: 'gantt', label: t('tab_gantt') },
|
||||||
{ key: 'calendar', label: t('tab_calendar') },
|
{ key: 'calendar', label: t('tab_calendar') },
|
||||||
{ key: 'comments', label: t('tab_comments') },
|
{ key: 'comments', label: t('tab_comments'), count: project.comments_count },
|
||||||
{ key: 'documents', label: t('tab_documents') },
|
{ key: 'documents', label: t('tab_documents'), count: project.documents_count },
|
||||||
]}
|
]}
|
||||||
active={tab}
|
active={tab}
|
||||||
onChange={setTab}
|
onChange={setTab}
|
||||||
@@ -62,8 +62,8 @@ export default function ProyectoDetallePage() {
|
|||||||
)}
|
)}
|
||||||
{tab === 'gantt' && <ProjectGanttTab projectId={projectId} />}
|
{tab === 'gantt' && <ProjectGanttTab projectId={projectId} />}
|
||||||
{tab === 'calendar' && <ProjectCalendarTab projectId={projectId} />}
|
{tab === 'calendar' && <ProjectCalendarTab projectId={projectId} />}
|
||||||
{tab === 'comments' && <CommentsPanel entityType="project" entityId={projectId} />}
|
{tab === 'comments' && <CommentsPanel entityType="project" entityId={projectId} onChanged={reload} />}
|
||||||
{tab === 'documents' && <AttachmentsPanel entityType="project" entityId={projectId} />}
|
{tab === 'documents' && <AttachmentsPanel entityType="project" entityId={projectId} onChanged={reload} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Plus, Save, Trash2, RotateCcw, ArrowLeft, Users, ListChecks, MessageSquare, Paperclip } from 'lucide-react';
|
import { Plus, Save, Trash2, RotateCcw, ArrowLeft } from 'lucide-react';
|
||||||
import { useUi } from '../../context/ui-context.jsx';
|
import { useUi } from '../../context/ui-context.jsx';
|
||||||
import { useAuth } from '../../context/auth-context.jsx';
|
import { useAuth } from '../../context/auth-context.jsx';
|
||||||
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
||||||
@@ -20,7 +20,6 @@ import { StatusBadge, ProgressBar } from '../../components/StatusBadge.jsx';
|
|||||||
import { Badge } from '../../components/Badge.jsx';
|
import { Badge } from '../../components/Badge.jsx';
|
||||||
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
||||||
import { SharedWithIcon } from '../../components/SharedWithIcon.jsx';
|
import { SharedWithIcon } from '../../components/SharedWithIcon.jsx';
|
||||||
import { CountBadge } from '../../components/CountBadge.jsx';
|
|
||||||
|
|
||||||
const CAN_DELETE_ROLES = ['superadmin', 'admin', 'it_manager', 'project_lead'];
|
const CAN_DELETE_ROLES = ['superadmin', 'admin', 'it_manager', 'project_lead'];
|
||||||
|
|
||||||
@@ -170,13 +169,7 @@ export default function ProyectosPage() {
|
|||||||
{ key: 'status_name', label: t('common_status'), render: (r) => <StatusBadge name={r.status_name} color={r.status_color} /> },
|
{ key: 'status_name', label: t('common_status'), render: (r) => <StatusBadge name={r.status_name} color={r.status_color} /> },
|
||||||
{ key: 'priority_name', label: t('common_priority'), sortValue: (r) => r.priority_level, render: (r) => <StatusBadge name={r.priority_name} color={r.priority_color} /> },
|
{ key: 'priority_name', label: t('common_priority'), sortValue: (r) => r.priority_level, render: (r) => <StatusBadge name={r.priority_name} color={r.priority_color} /> },
|
||||||
{ key: 'owner_name', label: t('project_owner') },
|
{ key: 'owner_name', label: t('project_owner') },
|
||||||
{ key: 'members_count', label: t('project_members'), render: (r) => <CountBadge icon={Users} count={r.members_count} title={t('project_members')} /> },
|
|
||||||
{ key: 'progress_pct', label: t('project_progress'), render: (r) => <div style={{ width: 90 }}><ProgressBar value={Number(r.progress_pct)} /></div> },
|
{ key: 'progress_pct', label: t('project_progress'), render: (r) => <div style={{ width: 90 }}><ProgressBar value={Number(r.progress_pct)} /></div> },
|
||||||
{ key: 'activities_progress', label: t('tab_activities'), sortValue: (r) => (r.activities_total ? r.activities_complete / r.activities_total : 0), render: (r) => (
|
|
||||||
<span className="count-badge" title={t('tab_activities')}><ListChecks size={14} />{r.activities_complete}/{r.activities_total}</span>
|
|
||||||
) },
|
|
||||||
{ key: 'comments_count', label: t('tab_comments'), render: (r) => <CountBadge icon={MessageSquare} count={r.comments_count} title={t('tab_comments')} /> },
|
|
||||||
{ key: 'documents_count', label: t('tab_documents'), render: (r) => <CountBadge icon={Paperclip} count={r.documents_count} title={t('tab_documents')} /> },
|
|
||||||
{ key: 'shared', label: '', sortable: false, render: (r) => <SharedWithIcon projectId={r.id} count={r.shared_count} /> },
|
{ key: 'shared', label: '', sortable: false, render: (r) => <SharedWithIcon projectId={r.id} count={r.shared_count} /> },
|
||||||
{ key: 'risk_level', label: t('project_risk'), render: (r) => <Badge color={RISK_COLOR[r.risk_level]}>{t(`risk_${r.risk_level}`)}</Badge> },
|
{ key: 'risk_level', label: t('project_risk'), render: (r) => <Badge color={RISK_COLOR[r.risk_level]}>{t(`risk_${r.risk_level}`)}</Badge> },
|
||||||
{ key: 'updated_at', label: t('project_updated_at'), render: (r) => String(r.updated_at).slice(0, 16) },
|
{ key: 'updated_at', label: t('project_updated_at'), render: (r) => String(r.updated_at).slice(0, 16) },
|
||||||
|
|||||||
@@ -229,8 +229,6 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
|
|||||||
background: rgb(var(--danger) / .18); color: rgb(var(--danger));
|
background: rgb(var(--danger) / .18); color: rgb(var(--danger));
|
||||||
font-size: .68rem; font-weight: 700; line-height: 1;
|
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 --- */
|
/* --- Cards / KPIs --- */
|
||||||
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 20px; }
|
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 20px; }
|
||||||
.kpi-card { background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); padding: 16px; }
|
.kpi-card { background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); padding: 16px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user