feat: version 2.17.6 -- Panel: "Proyectos atrasados" en porcentaje con "N de M"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Changelog - project.linkedcorp.net
|
# Changelog - project.linkedcorp.net
|
||||||
|
|
||||||
|
## [2.17.6] - 2026-10-21
|
||||||
|
### Changed
|
||||||
|
- Panel → "Proyectos atrasados": el centro del medidor muestra el porcentaje (atrasados sobre
|
||||||
|
activos) y debajo "N de M", para que se entienda de cuántos.
|
||||||
|
|
||||||
## [2.17.5] - 2026-10-20
|
## [2.17.5] - 2026-10-20
|
||||||
### Added
|
### Added
|
||||||
- Panel: la tarjeta "Proyectos atrasados" muestra un medidor (atrasados sobre activos) con el
|
- Panel: la tarjeta "Proyectos atrasados" muestra un medidor (atrasados sobre activos) con el
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "2.17.5",
|
"version": "2.17.6",
|
||||||
"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.17.5",
|
"version": "2.17.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// Medidor semicircular (tipo odometro): arco de fondo + arco relleno segun ratio (0..1), con el
|
// 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.
|
// valor del KPI al centro. Sin libreria -- un SVG con stroke-dasharray sobre el semicirculo.
|
||||||
function Gauge({ ratio, color, value }) {
|
function Gauge({ ratio, color, value, caption }) {
|
||||||
const r = 40;
|
const r = 40;
|
||||||
const length = Math.PI * r;
|
const length = Math.PI * r;
|
||||||
const clamped = Math.max(0, Math.min(1, ratio || 0));
|
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`;
|
const arc = `M ${50 - r} 50 A ${r} ${r} 0 0 1 ${50 + r} 50`;
|
||||||
return (
|
return (
|
||||||
<div className="kpi-gauge">
|
<div className="kpi-gauge">
|
||||||
|
<div className="kpi-gauge-dial">
|
||||||
<svg viewBox="0 0 100 56" aria-hidden="true">
|
<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(--line))" strokeWidth="9" strokeLinecap="round" />
|
||||||
<path
|
<path
|
||||||
@@ -17,6 +18,8 @@ function Gauge({ ratio, color, value }) {
|
|||||||
</svg>
|
</svg>
|
||||||
<div className="kpi-gauge-value">{value}</div>
|
<div className="kpi-gauge-value">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{caption && <div className="kpi-gauge-caption">{caption}</div>}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +33,7 @@ export function KpiCard({ label, value, gauge, onClick, selected }) {
|
|||||||
tabIndex={clickable ? 0 : undefined}
|
tabIndex={clickable ? 0 : undefined}
|
||||||
onKeyDown={clickable ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); } } : 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>}
|
{gauge ? <Gauge ratio={gauge.ratio} color={gauge.color} value={value} caption={gauge.caption} /> : <div className="kpi-value">{value}</div>}
|
||||||
<div className="kpi-label">{label}</div>
|
<div className="kpi-label">{label}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ export const messages = {
|
|||||||
dashboard_average_progress: 'Avance promedio',
|
dashboard_average_progress: 'Avance promedio',
|
||||||
dashboard_my_activities: 'Mis actividades pendientes',
|
dashboard_my_activities: 'Mis actividades pendientes',
|
||||||
dashboard_my_overdue_projects: 'Mis proyectos atrasados',
|
dashboard_my_overdue_projects: 'Mis proyectos atrasados',
|
||||||
|
common_of: 'de',
|
||||||
dashboard_back_to_my_activities: 'Ver mis actividades',
|
dashboard_back_to_my_activities: 'Ver mis actividades',
|
||||||
dashboard_projects_by_status: 'Proyectos por estado',
|
dashboard_projects_by_status: 'Proyectos por estado',
|
||||||
|
|
||||||
@@ -643,6 +644,7 @@ export const messages = {
|
|||||||
dashboard_average_progress: 'Average progress',
|
dashboard_average_progress: 'Average progress',
|
||||||
dashboard_my_activities: 'My pending activities',
|
dashboard_my_activities: 'My pending activities',
|
||||||
dashboard_my_overdue_projects: 'My overdue projects',
|
dashboard_my_overdue_projects: 'My overdue projects',
|
||||||
|
common_of: 'of',
|
||||||
dashboard_back_to_my_activities: 'Show my activities',
|
dashboard_back_to_my_activities: 'Show my activities',
|
||||||
dashboard_projects_by_status: 'Projects by status',
|
dashboard_projects_by_status: 'Projects by status',
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export default function DashboardPage() {
|
|||||||
if (!data) return <div className="text-muted">{t('common_loading')}</div>;
|
if (!data) return <div className="text-muted">{t('common_loading')}</div>;
|
||||||
const { kpis, myActivities, projectsByStatus, overdueProjects } = data;
|
const { kpis, myActivities, projectsByStatus, overdueProjects } = data;
|
||||||
const activeTotal = Number(kpis.activeProjects) || 0;
|
const activeTotal = Number(kpis.activeProjects) || 0;
|
||||||
|
const overdueRatio = activeTotal ? Number(kpis.overdueProjects) / activeTotal : 0;
|
||||||
|
const overduePct = Math.round(overdueRatio * 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -26,8 +28,8 @@ export default function DashboardPage() {
|
|||||||
<KpiCard label={t('dashboard_active_projects')} value={kpis.activeProjects} />
|
<KpiCard label={t('dashboard_active_projects')} value={kpis.activeProjects} />
|
||||||
<KpiCard
|
<KpiCard
|
||||||
label={t('dashboard_overdue_projects')}
|
label={t('dashboard_overdue_projects')}
|
||||||
value={kpis.overdueProjects}
|
value={`${overduePct}%`}
|
||||||
gauge={{ ratio: activeTotal ? Number(kpis.overdueProjects) / activeTotal : 0, color: 'danger' }}
|
gauge={{ ratio: overdueRatio, color: 'danger', caption: `${kpis.overdueProjects} ${t('common_of')} ${activeTotal}` }}
|
||||||
onClick={() => toggleDetail('overdue')}
|
onClick={() => toggleDetail('overdue')}
|
||||||
selected={detail === 'overdue'}
|
selected={detail === 'overdue'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -244,9 +244,11 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
|
|||||||
.kpi-card-clickable { cursor: pointer; transition: border-color .15s ease; }
|
.kpi-card-clickable { cursor: pointer; transition: border-color .15s ease; }
|
||||||
.kpi-card-clickable:hover { border-color: rgb(var(--accent) / .5); }
|
.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-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 { width: 120px; max-width: 100%; }
|
||||||
|
.kpi-gauge-dial { position: relative; }
|
||||||
.kpi-gauge svg { display: block; 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; }
|
.kpi-gauge-value { position: absolute; left: 0; right: 0; bottom: 0; text-align: center; font-size: 1.4rem; font-weight: 700; line-height: 1; }
|
||||||
|
.kpi-gauge-caption { text-align: center; font-size: .68rem; color: rgb(var(--muted)); margin-top: 2px; }
|
||||||
.panel-card { background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); padding: 16px; margin-bottom: 16px; }
|
.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; }
|
.panel-card h3 { margin: 0 0 12px; font-size: .95rem; }
|
||||||
.company-logo-preview {
|
.company-logo-preview {
|
||||||
|
|||||||
Reference in New Issue
Block a user