82 lines
7.7 KiB
TypeScript
82 lines
7.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Link } from 'react-router';
|
|
import { useAuth } from '../auth/AuthContext';
|
|
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
|
import { Icon } from '../components/Icon';
|
|
import { getHealth } from '../lib/api';
|
|
import type { HealthResponse } from '../lib/api';
|
|
import { getDashboardSummaryF4 } from '../lib/dashboardF4Api';
|
|
import type { DashboardSummaryF4 } from '../lib/dashboardF4Api';
|
|
import { actionLabel, formatDate, formatDateOnly } from '../lib/format';
|
|
|
|
function todayDate(): string {
|
|
const now = new Date();
|
|
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60_000);
|
|
return local.toISOString().slice(0, 10);
|
|
}
|
|
|
|
export function DashboardPage() {
|
|
const { user, hasPermission } = useAuth();
|
|
const [summary, setSummary] = useState<DashboardSummaryF4 | null>(null);
|
|
const [health, setHealth] = useState<HealthResponse | null>(null);
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
Promise.all([getDashboardSummaryF4(), getHealth()])
|
|
.then(([dashboard, service]) => { setSummary(dashboard); setHealth(service); })
|
|
.catch((requestError) => setError(errorMessage(requestError)));
|
|
}, []);
|
|
|
|
return <section>
|
|
<div className="page-heading dashboard-heading">
|
|
<div><span className="eyebrow">RESUMEN OPERATIVO</span><h1>Buen día, {user?.firstName}</h1><p>Lo que requiere atención en Inspecciones, Actas, verificaciones e Informes.</p></div>
|
|
<span className={`health-pill ${health?.status === 'ok' ? 'ok' : ''}`}><span />{health?.status === 'ok' ? 'Sistema operativo' : 'Verificando sistema'}</span>
|
|
</div>
|
|
|
|
{error && <Alert>{error}</Alert>}
|
|
{!summary ? <LoadingBlock label="Cargando panel…" /> : <>
|
|
<div className="dashboard-section-heading"><div><span className="eyebrow">HOY</span><h2>Qué requiere atención</h2></div></div>
|
|
<div className="stat-grid operational-stat-grid">
|
|
<Link className="stat-card" to="/inspecciones?status=PLANNED"><div className="stat-icon blue"><Icon name="calendar" /></div><div><small>INSPECCIONES PLANIFICADAS</small><strong>{summary.counts.plannedInspections}</strong><span>Pendientes de iniciar</span></div></Link>
|
|
<Link className="stat-card" to="/hallazgos?workflow=VERIFICATION_OVERDUE"><div className="stat-icon danger"><Icon name="alert" /></div><div><small>VERIFICACIONES VENCIDAS</small><strong>{summary.counts.overdueControls}</strong><span>Hallazgos que requieren control técnico</span></div></Link>
|
|
<Link className="stat-card" to="/seguimiento-actas"><div className="stat-icon observed"><Icon name="clipboard" /></div><div><small>ACTAS EN SEGUIMIENTO</small><strong>{summary.counts.actsInFollowUp}</strong><span>Respuestas, compromisos y regularización</span></div></Link>
|
|
<Link className="stat-card" to="/informes?view=pending"><div className="stat-icon violet"><Icon name="clipboard" /></div><div><small>ACTAS SIN INFORME</small><strong>{summary.counts.sealedActsWithoutReport}</strong><span>Actas selladas que todavía no tienen Informe</span></div></Link>
|
|
</div>
|
|
|
|
<div className="dashboard-section-heading compact"><div><span className="eyebrow">INVENTARIOS Y DOCUMENTOS</span><h2>Estado general</h2></div></div>
|
|
<div className="stat-grid secondary-stat-grid">
|
|
<Link className="stat-card compact" to="/inventarios"><div className="stat-icon blue"><Icon name="layers" /></div><div><small>INVENTARIO REAL</small><strong>{summary.counts.totalAssets}</strong><span>Instancias físicas registradas</span></div></Link>
|
|
<Link className="stat-card compact" to="/inventarios?quick=location"><div className="stat-icon gray"><Icon name="map" /></div><div><small>SIN UBICACIÓN</small><strong>{summary.counts.assetsWithoutGeometry}</strong><span>Inventarios reales sin geometría</span></div></Link>
|
|
<Link className="stat-card compact" to="/informes"><div className="stat-icon gray"><Icon name="audit" /></div><div><small>INFORMES EN PREPARACIÓN</small><strong>{summary.counts.reportsWorking}</strong><span>Informe creado, todavía no oficializado · {summary.counts.reportsOfficialized} oficializados</span></div></Link>
|
|
<Link className="stat-card compact" to="/hallazgos/planificacion"><div className="stat-icon green"><Icon name="calendar" /></div><div><small>PRÓXIMAS VERIFICACIONES</small><strong>{summary.counts.controlsNext30Days}</strong><span>{summary.counts.findingsWithoutControlDate} Hallazgos abiertos sin fecha de control</span></div></Link>
|
|
</div>
|
|
|
|
{hasPermission('inspection_findings.read') && <article className="panel upcoming-controls-panel">
|
|
<div className="panel-heading"><div><span className="eyebrow">AGENDA TÉCNICA</span><h2>Próximos controles</h2><p className="section-copy">Hallazgos abiertos con una fecha de verificación definida. Este seguimiento técnico permite verificar regularización y detectar reincidencias.</p></div><Link className="text-link" to="/hallazgos/planificacion">Planificar verificaciones <Icon name="chevron" size={15} /></Link></div>
|
|
<div className="activity-list">
|
|
{summary.upcomingControls.length === 0 && <p className="muted">Todavía no hay controles programados.</p>}
|
|
{summary.upcomingControls.map((control) => <Link className="activity-item control-item" key={control.id} to={`/hallazgos/${control.id}`}><span className={`activity-dot ${control.nextControlOn < todayDate() ? 'overdue-dot' : ''}`} /><div><strong>{control.title}</strong><p>{control.assetCode} · {control.assetName} · {control.actCode}</p></div><time>{formatDateOnly(control.nextControlOn)}</time></Link>)}
|
|
</div>
|
|
</article>}
|
|
|
|
<div className="dashboard-grid">
|
|
<article className="panel">
|
|
<div className="panel-heading"><div><span className="eyebrow">ACTIVIDAD DE CAMPO</span><h2>Últimas actividades de los inspectores</h2><p className="section-copy">Acciones operativas realizadas por inspectores asignados.</p></div></div>
|
|
<div className="activity-list">{summary.recentInspectorActivity.length === 0 ? <p className="muted">Todavía no hay actividad operativa de inspectores registrada.</p> : summary.recentInspectorActivity.map((event) => <div className="activity-item" key={event.id}><span className="activity-dot" /><div><strong>{actionLabel(event.action)}</strong><p>{event.actorUsername ?? 'Inspector'}{event.entityType ? ` · ${event.entityType}` : ''}</p></div><time>{formatDate(event.occurredAt)}</time></div>)}</div>
|
|
</article>
|
|
|
|
<article className="panel quick-panel">
|
|
<span className="eyebrow">ACCESOS RÁPIDOS</span><h2>Administración</h2>
|
|
<div className="quick-links">
|
|
<Link to="/inventarios"><Icon name="layers" /><span><strong>Inventarios</strong><small>Instancias reales organizadas por empresa, área y jerarquía</small></span><Icon name="chevron" /></Link>
|
|
{hasPermission('asset_types.read') && <Link to="/admin/asset-types"><Icon name="layers" /><span><strong>Configuración de Inventarios</strong><small>Jerarquía, tipos y campos configurables</small></span><Icon name="chevron" /></Link>}
|
|
{hasPermission('finding_catalog.manage') && <Link to="/admin/finding-catalog"><Icon name="alert" /><span><strong>Catálogo de hallazgos</strong><small>Hallazgos aplicables según el tipo de Subinstalación</small></span><Icon name="chevron" /></Link>}
|
|
{hasPermission('users.read') && <Link to="/admin/users"><Icon name="users" /><span><strong>Usuarios</strong><small>Accesos y roles</small></span><Icon name="chevron" /></Link>}
|
|
</div>
|
|
<div className="system-footnote">API {health?.version ?? '—'} · Base {health?.database === 'ok' ? 'operativa' : 'sin verificar'}</div>
|
|
</article>
|
|
</div>
|
|
</>}
|
|
</section>;
|
|
}
|