feat: contador en las pestañas de Tareas/Comentarios/Documentos de una actividad
Circulo con el numero al lado del nombre de la pestaña: Tareas muestra las pendientes (sin marcar), Comentarios y Documentos el total. Soporte agregado al componente Tabs reutilizable (tab.count), no solo al detalle de actividad. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog - project.linkedcorp.net
|
# Changelog - project.linkedcorp.net
|
||||||
|
|
||||||
|
## [1.16.0] - 2026-09-16
|
||||||
|
### Added
|
||||||
|
- Contador en un circulo junto al nombre de la pestaña, dentro del detalle de una actividad:
|
||||||
|
"Tareas" muestra las tareas pendientes (sin marcar como hechas), "Comentarios" y "Documentos"
|
||||||
|
muestran el total. Se oculta cuando el contador es 0. Soporte agregado al componente `Tabs`
|
||||||
|
reutilizable (`tab.count`), disponible para cualquier otra pantalla que use pestañas.
|
||||||
|
|
||||||
## [1.15.2] - 2026-09-16
|
## [1.15.2] - 2026-09-16
|
||||||
### Fixed
|
### Fixed
|
||||||
- El checkbox de "hecho" de una tarea (y el renombrado en linea) dejaba de reflejarse tras
|
- El checkbox de "hecho" de una tarea (y el renombrado en linea) dejaba de reflejarse tras
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "1.15.2",
|
"version": "1.16.0",
|
||||||
"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": "1.15.2",
|
"version": "1.16.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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>}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { listTasks, createTask, updateTask, setTaskDone, deleteTask, reorderTask
|
|||||||
import { promoteTaskToRecurring } from '../../services/recurringTasks.js';
|
import { promoteTaskToRecurring } from '../../services/recurringTasks.js';
|
||||||
import { listCatalog } from '../../services/catalogs.js';
|
import { listCatalog } from '../../services/catalogs.js';
|
||||||
import { listUsers } from '../../services/users.js';
|
import { listUsers } from '../../services/users.js';
|
||||||
|
import { listComments } from '../../services/comments.js';
|
||||||
|
import { listAttachments } from '../../services/attachments.js';
|
||||||
import { Button } from '../../components/Button.jsx';
|
import { Button } from '../../components/Button.jsx';
|
||||||
import { Field, FieldRow } from '../../components/Field.jsx';
|
import { Field, FieldRow } from '../../components/Field.jsx';
|
||||||
import { ComboBox } from '../../components/ComboBox.jsx';
|
import { ComboBox } from '../../components/ComboBox.jsx';
|
||||||
@@ -41,6 +43,10 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [tab, setTab] = useState('detail');
|
const [tab, setTab] = useState('detail');
|
||||||
|
// Recargadas al cambiar de pestaña (listas livianas) para que los contadores de las pestañas
|
||||||
|
// se mantengan al dia sin necesidad de que CommentsPanel/AttachmentsPanel avisen al padre.
|
||||||
|
const { data: comments } = useAsync(() => listComments('activity', activity.id), [activity.id, tab]);
|
||||||
|
const { data: attachments } = useAsync(() => listAttachments('activity', activity.id), [activity.id, tab]);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [form, setForm] = useState(() => toForm(activity));
|
const [form, setForm] = useState(() => toForm(activity));
|
||||||
const [newTask, setNewTask] = useState('');
|
const [newTask, setNewTask] = useState('');
|
||||||
@@ -53,6 +59,7 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
|||||||
useEffect(() => { setForm(toForm(activity)); setEditing(false); }, [activity]);
|
useEffect(() => { setForm(toForm(activity)); setEditing(false); }, [activity]);
|
||||||
|
|
||||||
const hasTasks = tasks && tasks.length > 0;
|
const hasTasks = tasks && tasks.length > 0;
|
||||||
|
const pendingTasksCount = (tasks || []).filter((task) => !task.is_done).length;
|
||||||
const priorityOptions = (priorities || []).map((p) => ({ value: p.id, label: p.name }));
|
const priorityOptions = (priorities || []).map((p) => ({ value: p.id, label: p.name }));
|
||||||
const statusOptions = (statuses || []).map((s) => ({ value: s.id, label: s.name }));
|
const statusOptions = (statuses || []).map((s) => ({ value: s.id, label: s.name }));
|
||||||
const userOptions = (users || []).map((u) => ({ value: u.id, label: u.name }));
|
const userOptions = (users || []).map((u) => ({ value: u.id, label: u.name }));
|
||||||
@@ -141,10 +148,10 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
|||||||
<Tabs
|
<Tabs
|
||||||
tabs={[
|
tabs={[
|
||||||
{ key: 'detail', label: t('tab_info') },
|
{ key: 'detail', label: t('tab_info') },
|
||||||
{ key: 'tasks', label: t('tasks_title') },
|
{ key: 'tasks', label: t('tasks_title'), count: pendingTasksCount },
|
||||||
{ key: 'dependencies', label: t('activity_dependencies') },
|
{ key: 'dependencies', label: t('activity_dependencies') },
|
||||||
{ key: 'comments', label: t('tab_comments') },
|
{ key: 'comments', label: t('tab_comments'), count: (comments || []).length },
|
||||||
{ key: 'documents', label: t('tab_documents') },
|
{ key: 'documents', label: t('tab_documents'), count: (attachments || []).length },
|
||||||
]}
|
]}
|
||||||
active={tab}
|
active={tab}
|
||||||
onChange={setTab}
|
onChange={setTab}
|
||||||
|
|||||||
@@ -174,11 +174,18 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
|
|||||||
/* --- Pestañas reales --- */
|
/* --- Pestañas reales --- */
|
||||||
.tabs { display: flex; gap: 4px; border-bottom: 1px solid rgb(var(--line)); margin-bottom: 16px; overflow-x: auto; }
|
.tabs { display: flex; gap: 4px; border-bottom: 1px solid rgb(var(--line)); margin-bottom: 16px; overflow-x: auto; }
|
||||||
.tab-item {
|
.tab-item {
|
||||||
|
display: inline-flex; align-items: center; gap: 7px;
|
||||||
padding: 10px 14px; font-size: .82rem; font-weight: 600; color: rgb(var(--muted));
|
padding: 10px 14px; font-size: .82rem; font-weight: 600; color: rgb(var(--muted));
|
||||||
background: transparent; border: none; border-bottom: 2px solid transparent; cursor: pointer; white-space: nowrap;
|
background: transparent; border: none; border-bottom: 2px solid transparent; cursor: pointer; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.tab-item:hover { color: rgb(var(--body)); }
|
.tab-item:hover { color: rgb(var(--body)); }
|
||||||
.tab-item.active { color: rgb(var(--accent)); border-bottom-color: rgb(var(--accent)); }
|
.tab-item.active { color: rgb(var(--accent)); border-bottom-color: rgb(var(--accent)); }
|
||||||
|
.tab-badge {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
|
||||||
|
background: rgb(var(--accent) / .18); color: rgb(var(--accent));
|
||||||
|
font-size: .68rem; font-weight: 700; line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- 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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user