feat: version 2.17.5 -- Panel: medidor y detalle en "Proyectos atrasados"
La tarjeta muestra un medidor semicircular (atrasados/activos) con el valor al centro; al hacer clic, el bloque inferior lista los proyectos atrasados en vez de mis actividades. Por ahora solo esta tarjeta. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "project-linkedcorp-frontend",
|
||||
"private": true,
|
||||
"version": "2.17.4",
|
||||
"version": "2.17.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
export function KpiCard({ label, value }) {
|
||||
// Medidor semicircular (tipo odometro): arco de fondo + arco relleno segun ratio (0..1), con el
|
||||
// valor del KPI al centro. Sin libreria -- un SVG con stroke-dasharray sobre el semicirculo.
|
||||
function Gauge({ ratio, color, value }) {
|
||||
const r = 40;
|
||||
const length = Math.PI * r;
|
||||
const clamped = Math.max(0, Math.min(1, ratio || 0));
|
||||
const arc = `M ${50 - r} 50 A ${r} ${r} 0 0 1 ${50 + r} 50`;
|
||||
return (
|
||||
<div className="kpi-card">
|
||||
<div className="kpi-value">{value}</div>
|
||||
<div className="kpi-gauge">
|
||||
<svg viewBox="0 0 100 56" aria-hidden="true">
|
||||
<path d={arc} fill="none" stroke="rgb(var(--line))" strokeWidth="9" strokeLinecap="round" />
|
||||
<path
|
||||
d={arc} fill="none" stroke={`rgb(var(--${color}))`} strokeWidth="9" strokeLinecap="round"
|
||||
strokeDasharray={`${length * clamped} ${length}`}
|
||||
style={{ transition: 'stroke-dasharray .6s ease' }}
|
||||
/>
|
||||
</svg>
|
||||
<div className="kpi-gauge-value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function KpiCard({ label, value, gauge, onClick, selected }) {
|
||||
const clickable = !!onClick;
|
||||
return (
|
||||
<div
|
||||
className={`kpi-card ${clickable ? 'kpi-card-clickable' : ''} ${selected ? 'selected' : ''}`}
|
||||
onClick={onClick}
|
||||
role={clickable ? 'button' : undefined}
|
||||
tabIndex={clickable ? 0 : undefined}
|
||||
onKeyDown={clickable ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); } } : undefined}
|
||||
>
|
||||
{gauge ? <Gauge ratio={gauge.ratio} color={gauge.color} value={value} /> : <div className="kpi-value">{value}</div>}
|
||||
<div className="kpi-label">{label}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -155,6 +155,8 @@ export const messages = {
|
||||
dashboard_today_activities: 'Para hoy',
|
||||
dashboard_average_progress: 'Avance promedio',
|
||||
dashboard_my_activities: 'Mis actividades pendientes',
|
||||
dashboard_my_overdue_projects: 'Mis proyectos atrasados',
|
||||
dashboard_back_to_my_activities: 'Ver mis actividades',
|
||||
dashboard_projects_by_status: 'Proyectos por estado',
|
||||
|
||||
projects_title: 'Proyectos',
|
||||
@@ -640,6 +642,8 @@ export const messages = {
|
||||
dashboard_today_activities: 'Due today',
|
||||
dashboard_average_progress: 'Average progress',
|
||||
dashboard_my_activities: 'My pending activities',
|
||||
dashboard_my_overdue_projects: 'My overdue projects',
|
||||
dashboard_back_to_my_activities: 'Show my activities',
|
||||
dashboard_projects_by_status: 'Projects by status',
|
||||
|
||||
projects_title: 'Projects',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useUi } from '../../context/ui-context.jsx';
|
||||
import { useAsync } from '../../hooks/useAsync.js';
|
||||
@@ -9,9 +10,13 @@ export default function DashboardPage() {
|
||||
const { t } = useUi();
|
||||
const navigate = useNavigate();
|
||||
const { data } = useAsync(getSummary, []);
|
||||
// Que muestra el bloque de abajo: null = mis actividades (por defecto), 'overdue' = proyectos atrasados.
|
||||
const [detail, setDetail] = useState(null);
|
||||
const toggleDetail = (key) => setDetail((d) => (d === key ? null : key));
|
||||
|
||||
if (!data) return <div className="text-muted">{t('common_loading')}</div>;
|
||||
const { kpis, myActivities, projectsByStatus } = data;
|
||||
const { kpis, myActivities, projectsByStatus, overdueProjects } = data;
|
||||
const activeTotal = Number(kpis.activeProjects) || 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -19,7 +24,13 @@ export default function DashboardPage() {
|
||||
|
||||
<div className="kpi-grid">
|
||||
<KpiCard label={t('dashboard_active_projects')} value={kpis.activeProjects} />
|
||||
<KpiCard label={t('dashboard_overdue_projects')} value={kpis.overdueProjects} />
|
||||
<KpiCard
|
||||
label={t('dashboard_overdue_projects')}
|
||||
value={kpis.overdueProjects}
|
||||
gauge={{ ratio: activeTotal ? Number(kpis.overdueProjects) / activeTotal : 0, color: 'danger' }}
|
||||
onClick={() => toggleDetail('overdue')}
|
||||
selected={detail === 'overdue'}
|
||||
/>
|
||||
<KpiCard label={t('dashboard_due_soon_projects')} value={kpis.dueSoonProjects} />
|
||||
<KpiCard label={t('dashboard_pending_activities')} value={kpis.pendingActivities} />
|
||||
<KpiCard label={t('dashboard_overdue_activities')} value={kpis.overdueActivities} />
|
||||
@@ -27,6 +38,25 @@ export default function DashboardPage() {
|
||||
<KpiCard label={t('dashboard_average_progress')} value={`${kpis.averageProgress}%`} />
|
||||
</div>
|
||||
|
||||
{detail === 'overdue' ? (
|
||||
<div className="panel-card">
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
||||
<h3>{t('dashboard_my_overdue_projects')}</h3>
|
||||
<button type="button" className="link-button" onClick={() => setDetail(null)}>{t('dashboard_back_to_my_activities')}</button>
|
||||
</div>
|
||||
{overdueProjects.length === 0 && <div className="text-muted">{t('common_empty')}</div>}
|
||||
{overdueProjects.map((p) => (
|
||||
<div key={p.id} onClick={() => navigate(`/proyectos/${p.id}`)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '8px 0', borderBottom: '1px solid rgb(var(--line) / .5)', cursor: 'pointer' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '.82rem', fontWeight: 600 }}>{p.code} — {p.name}</div>
|
||||
<div className="text-muted" style={{ fontSize: '.72rem' }}>{p.owner_name || '—'} · {String(p.estimated_end_date).slice(0, 10)}</div>
|
||||
</div>
|
||||
<div style={{ width: 120 }}><ProgressBar value={Number(p.progress_pct)} /></div>
|
||||
<DueBadge status="overdue" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="panel-card">
|
||||
<h3>{t('dashboard_my_activities')}</h3>
|
||||
{myActivities.length === 0 && <div className="text-muted">{t('common_empty')}</div>}
|
||||
@@ -41,6 +71,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="panel-card">
|
||||
<h3>{t('dashboard_projects_by_status')}</h3>
|
||||
|
||||
@@ -241,6 +241,12 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
|
||||
.kpi-card { background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); padding: 16px; }
|
||||
.kpi-value { font-size: 1.6rem; font-weight: 700; }
|
||||
.kpi-label { font-size: .72rem; color: rgb(var(--muted)); margin-top: 4px; }
|
||||
.kpi-card-clickable { cursor: pointer; transition: border-color .15s ease; }
|
||||
.kpi-card-clickable:hover { border-color: rgb(var(--accent) / .5); }
|
||||
.kpi-card.selected { border-color: rgb(var(--accent)); box-shadow: 0 0 0 1px rgb(var(--accent)); }
|
||||
.kpi-gauge { position: relative; width: 120px; max-width: 100%; }
|
||||
.kpi-gauge svg { display: block; width: 100%; }
|
||||
.kpi-gauge-value { position: absolute; left: 0; right: 0; bottom: 0; text-align: center; font-size: 1.4rem; font-weight: 700; line-height: 1; }
|
||||
.panel-card { background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); padding: 16px; margin-bottom: 16px; }
|
||||
.panel-card h3 { margin: 0 0 12px; font-size: .95rem; }
|
||||
.company-logo-preview {
|
||||
|
||||
Reference in New Issue
Block a user