feat: version 2.19.0 -- Panel: selector de tarjetas visibles
Desplegable con casillas junto al titulo para mostrar/ocultar cada tarjeta, persistido por usuario en localStorage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useUi } from '../../context/ui-context.jsx';
|
||||
import { useAuth } from '../../context/auth-context.jsx';
|
||||
import { useAsync } from '../../hooks/useAsync.js';
|
||||
import { getSummary } from '../../services/dashboard.js';
|
||||
import { KpiCard } from '../../components/KpiCard.jsx';
|
||||
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
||||
import { CheckListDropdown } from '../../components/CheckListDropdown.jsx';
|
||||
import { ProgressBar, DueBadge } from '../../components/StatusBadge.jsx';
|
||||
|
||||
const ROW_STYLE = { display: 'flex', alignItems: 'center', gap: 12, padding: '8px 0', borderBottom: '1px solid rgb(var(--line) / .5)', cursor: 'pointer' };
|
||||
@@ -19,6 +21,27 @@ function ratioOf(part, total) {
|
||||
const BAR_COL = { width: 120, flex: 'none' };
|
||||
const BADGE_COL = { width: 96, flex: 'none', display: 'flex', justifyContent: 'flex-end' };
|
||||
|
||||
// Orden fijo de las tarjetas; el selector solo decide cuales se ven.
|
||||
const CARD_KEYS = ['active', 'overdue', 'dueSoon', 'pending', 'overdueActivities', 'today', 'average'];
|
||||
|
||||
// Tarjetas visibles, persistidas por usuario en este navegador (sobrevive a recargar/cerrar
|
||||
// sesion). Clave por usuario para que dos personas en el mismo equipo no se pisen.
|
||||
function useVisibleCards(userId) {
|
||||
const key = `plc_dashboard_cards_${userId || 'anon'}`;
|
||||
const [visible, setVisible] = useState(() => {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(key));
|
||||
if (Array.isArray(saved)) return saved.filter((k) => CARD_KEYS.includes(k));
|
||||
} catch { /* sin storage o JSON invalido: todas visibles */ }
|
||||
return CARD_KEYS;
|
||||
});
|
||||
function update(next) {
|
||||
setVisible(next);
|
||||
try { localStorage.setItem(key, JSON.stringify(next)); } catch { /* solo en memoria */ }
|
||||
}
|
||||
return [visible, update];
|
||||
}
|
||||
|
||||
const dateOnly = (d) => (d ? String(d).slice(0, 10) : '—');
|
||||
|
||||
function ProjectRows({ projects, onOpen }) {
|
||||
@@ -58,9 +81,11 @@ function ActivityRows({ activities, onOpen }) {
|
||||
export default function DashboardPage() {
|
||||
const { t } = useUi();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { data } = useAsync(getSummary, []);
|
||||
// Tarjeta cuyo detalle se muestra en el popup (null = cerrado).
|
||||
const [detail, setDetail] = useState(null);
|
||||
const [visibleCards, setVisibleCards] = useVisibleCards(user?.id);
|
||||
|
||||
if (!data) return <div className="text-muted">{t('common_loading')}</div>;
|
||||
const { kpis, projectsByStatus } = data;
|
||||
@@ -91,40 +116,48 @@ export default function DashboardPage() {
|
||||
const cardProps = (key) => ({ onClick: () => setDetail(key), selected: detail === key });
|
||||
const openFromPopup = (path) => { setDetail(null); navigate(path); };
|
||||
|
||||
const CARDS = {
|
||||
active: {
|
||||
label: t('dashboard_active_projects'), value: `${active.pct}%`,
|
||||
gauge: { ratio: active.ratio, color: 'accent', caption: of(kpis.activeProjects, nonCancelledTotal) },
|
||||
},
|
||||
overdue: {
|
||||
label: t('dashboard_overdue_projects'), value: `${overdue.pct}%`,
|
||||
gauge: { ratio: overdue.ratio, color: 'danger', caption: of(kpis.overdueProjects, activeTotal) },
|
||||
},
|
||||
dueSoon: { label: t('dashboard_due_soon_projects'), value: kpis.dueSoonProjects },
|
||||
pending: {
|
||||
label: t('dashboard_pending_activities'), value: `${pending.pct}%`,
|
||||
gauge: { ratio: pending.ratio, color: 'warning', caption: of(kpis.pendingActivities, activitiesTotal) },
|
||||
},
|
||||
overdueActivities: {
|
||||
label: t('dashboard_overdue_activities'), value: `${overdueAct.pct}%`,
|
||||
gauge: { ratio: overdueAct.ratio, color: 'danger', caption: of(kpis.overdueActivities, activitiesStrictTotal) },
|
||||
},
|
||||
today: { label: t('dashboard_today_activities'), value: kpis.todayActivities },
|
||||
average: {
|
||||
label: t('dashboard_average_progress'), value: `${kpis.averageProgress}%`,
|
||||
gauge: { ratio: Number(kpis.averageProgress) / 100, color: 'success' },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="page-title mt-0" style={{ marginBottom: 16 }}>{t('dashboard_title')}</h2>
|
||||
|
||||
<div className="kpi-grid">
|
||||
<KpiCard
|
||||
label={t('dashboard_active_projects')} value={`${active.pct}%`}
|
||||
gauge={{ ratio: active.ratio, color: 'accent', caption: of(kpis.activeProjects, nonCancelledTotal) }}
|
||||
{...cardProps('active')}
|
||||
/>
|
||||
<KpiCard
|
||||
label={t('dashboard_overdue_projects')} value={`${overdue.pct}%`}
|
||||
gauge={{ ratio: overdue.ratio, color: 'danger', caption: of(kpis.overdueProjects, activeTotal) }}
|
||||
{...cardProps('overdue')}
|
||||
/>
|
||||
<KpiCard label={t('dashboard_due_soon_projects')} value={kpis.dueSoonProjects} {...cardProps('dueSoon')} />
|
||||
<KpiCard
|
||||
label={t('dashboard_pending_activities')} value={`${pending.pct}%`}
|
||||
gauge={{ ratio: pending.ratio, color: 'warning', caption: of(kpis.pendingActivities, activitiesTotal) }}
|
||||
{...cardProps('pending')}
|
||||
/>
|
||||
<KpiCard
|
||||
label={t('dashboard_overdue_activities')} value={`${overdueAct.pct}%`}
|
||||
gauge={{ ratio: overdueAct.ratio, color: 'danger', caption: of(kpis.overdueActivities, activitiesStrictTotal) }}
|
||||
{...cardProps('overdueActivities')}
|
||||
/>
|
||||
<KpiCard label={t('dashboard_today_activities')} value={kpis.todayActivities} {...cardProps('today')} />
|
||||
<KpiCard
|
||||
label={t('dashboard_average_progress')} value={`${kpis.averageProgress}%`}
|
||||
gauge={{ ratio: Number(kpis.averageProgress) / 100, color: 'success' }}
|
||||
{...cardProps('average')}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
<h2 className="page-title mt-0" style={{ margin: 0 }}>{t('dashboard_title')}</h2>
|
||||
<CheckListDropdown
|
||||
label={`${t('dashboard_cards')} (${visibleCards.length}/${CARD_KEYS.length})`}
|
||||
options={CARD_KEYS.map((k) => ({ value: k, label: CARDS[k].label }))}
|
||||
values={visibleCards}
|
||||
onChange={setVisibleCards}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="kpi-grid">
|
||||
{CARD_KEYS.filter((k) => visibleCards.includes(k)).map((k) => <KpiCard key={k} {...CARDS[k]} {...cardProps(k)} />)}
|
||||
</div>
|
||||
{visibleCards.length === 0 && <div className="text-muted" style={{ marginBottom: 20 }}>{t('dashboard_no_cards')}</div>}
|
||||
|
||||
<div className="panel-card">
|
||||
<h3>{t('dashboard_projects_by_status')}</h3>
|
||||
{projectsByStatus.map((s) => (
|
||||
|
||||
Reference in New Issue
Block a user