El Dashboard de Métricas es el centro de control unificado que visualiza todos los KPIs del proyecto SIGA en tiempo real, permitiendo toma de decisiones basada en datos y monitoreo continuo del éxito.
┌─────────────────────────────────────────────────────────────────────┐
│ SIGA EXECUTIVE DASHBOARD │
│ Actualizado: 22/07/2025 14:32:18 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ OPERATIVO FINANCIERO ADOPCIÓN │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ KM VACÍOS │ │ MARGEN/KM │ │ USUARIOS │ │
│ │ 15.7% │ │ €0.61 │ │ 92.5% │ │
│ │ ↓ 3.1% │ │ ↑ €0.04 │ │ ↑ 2.1% │ │
│ │ 🟢 │ │ 🟢 │ │ 🟢 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ UTILIZACIÓN │ │ ROI PROYECTO│ │ NPS SCORE │ │
│ │ 89.3% │ │ 268% │ │ +60 │ │
│ │ ↑ 1.2% │ │ ↑ 22% │ │ ↑ 5pts │ │
│ │ 🟢 │ │ 🟢 │ │ 🟢 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ═══════════════════════════════════════════════════════════════ │
│ │
│ TENDENCIAS (Últimos 30 días) │
│ Km Vacíos: [▇▇▇▇▆▆▅▅▄▄▃▃▂▂▁] -28.7% │
│ Revenue/Veh: [▂▂▃▃▄▄▅▅▆▆▇▇▇▇▇] +18.3% │
│ Decisiones/h: [▄▄▅▅▅▆▆▆▇▇▇▇▇▇▇] +45.2% │
│ │
│ [🔄 Actualizar] [📊 Detalles] [📥 Exportar] [⚙️ Configurar] │
└─────────────────────────────────────────────────────────────────────┤
class RealTimeKPIPanel:
"""Panel de KPIs con actualización en tiempo real"""
def __init__(self):
self.update_interval = 30 # segundos
self.kpi_definitions = self._load_kpi_definitions()
def get_current_kpis(self):
"""Obtiene valores actuales de todos los KPIs"""
kpis = {}
# KPIs Operativos
kpis['operativos'] = {
'km_vacios': {
'valor': self._calculate_empty_km_ratio(),
'target': 20.0,
'variacion': -3.1,
'tendencia': 'descendente',
'sparkline': self._get_sparkline('km_vacios', days=7)
},
'utilizacion': {
'valor': self._calculate_fleet_utilization(),
'target': 85.0,
'variacion': +1.2,
'tendencia': 'ascendente',
'sparkline': self._get_sparkline('utilizacion', days=7)
},
'tiempo_respuesta': {
'valor': 4.2, # minutos
'target': 5.0,
'variacion': -0.8,
'tendencia': 'estable',
'sparkline': self._get_sparkline('tiempo_respuesta', days=7)
},
'otd': {
'valor': 94.8, # %
'target': 92.0,
'variacion': +1.3,
'tendencia': 'ascendente',
'sparkline': self._get_sparkline('otd', days=7)
}
}
# KPIs Financieros
kpis['financieros'] = {
'margen_km': {
'valor': 0.61,
'target': 0.55,
'variacion': +0.04,
'tendencia': 'ascendente',
'sparkline': self._get_sparkline('margen_km', days=7)
},
'roi_proyecto': {
'valor': 268,
'target': 200,
'variacion': +22,
'tendencia': 'ascendente',
'sparkline': self._get_sparkline('roi', days=30)
},
'revenue_vehiculo': {
'valor': 14825,
'target': 13500,
'variacion': +825,
'tendencia': 'ascendente',
'sparkline': self._get_sparkline('revenue_vehiculo', days=7)
}
}
# KPIs de Adopción
kpis['adopcion'] = {
'usuarios_activos': {
'valor': 92.5,
'target': 80.0,
'variacion': +2.1,
'tendencia': 'ascendente',
'sparkline': self._get_sparkline('usuarios_activos', days=7)
},
'nps': {
'valor': 60,
'target': 40,
'variacion': +5,
'tendencia': 'estable',
'sparkline': self._get_sparkline('nps', days=30)
}
}
return kpis
// Componente de mapa de calor en React
const OperationalHeatmap: React.FC = () => {
const [heatmapData, setHeatmapData] = useState<HeatmapData | null>(null);
const [selectedMetric, setSelectedMetric] = useState('demand');
useEffect(() => {
// Actualizar datos cada 60 segundos
const interval = setInterval(async () => {
const data = await fetchHeatmapData(selectedMetric);
setHeatmapData(data);
}, 60000);
return () => clearInterval(interval);
}, [selectedMetric]);
const metrics = {
'demand': {
title: 'Demanda por Zona',
gradient: ['#0000FF', '#00FF00', '#FFFF00', '#FF0000'],
unit: 'cargas/hora'
},
'vehicles': {
title: 'Concentración de Vehículos',
gradient: ['#00FF00', '#FFFF00', '#FFA500', '#FF0000'],
unit: 'vehículos'
},
'efficiency': {
title: 'Eficiencia por Zona',
gradient: ['#FF0000', '#FFA500', '#FFFF00', '#00FF00'],
unit: '% utilización'
}
};
return (
<div className="heatmap-container">
<div className="metric-selector">
{Object.keys(metrics).map(key => (
<button
key={key}
onClick={() => setSelectedMetric(key)}
className={selectedMetric === key ? 'active' : ''}
>
{metrics[key].title}
</button>
))}
</div>
<Map
center={[40.4168, -3.7038]}
zoom={6}
style={{ height: '400px', width: '100%' }}
>
<HeatmapLayer
points={heatmapData?.points || []}
gradient={metrics[selectedMetric].gradient}
max={heatmapData?.max || 1}
radius={25}
/>
<VehicleMarkers vehicles={heatmapData?.vehicles || []} />
<ZoneOverlays zones={heatmapData?.zones || []} />
</Map>
<Legend
gradient={metrics[selectedMetric].gradient}
min={0}
max={heatmapData?.max || 100}
unit={metrics[selectedMetric].unit}
/>
</div>
);
};
def generar_graficos_tendencia():
"""Genera configuración para gráficos de tendencia"""
graficos = {
'evolucion_km_vacios': {
'tipo': 'line',
'titulo': 'Evolución Kilómetros Vacíos',
'datos': {
'labels': ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun'],
'datasets': [{
'label': 'Real',
'data': [32, 29.5, 23.8, 20.1, 17.5, 15.7],
'borderColor': '#4CAF50',
'tension': 0.1
}, {
'label': 'Target',
'data': [32, 30, 25, 22, 20, 20],
'borderColor': '#FF9800',
'borderDash': [5, 5]
}]
},
'opciones': {
'responsive': True,
'plugins': {
'legend': {'position': 'top'},
'tooltip': {'mode': 'index', 'intersect': False}
}
}
},
'comparacion_financiera': {
'tipo': 'bar',
'titulo': 'Comparación Métricas Financieras',
'datos': {
'labels': ['Margen/Km', 'Revenue/Veh', 'Costo/Entrega'],
'datasets': [{
'label': 'Sin SIGA',
'data': [0.48, 12500, 90.48],
'backgroundColor': '#F44336'
}, {
'label': 'Con SIGA',
'data': [0.61, 14825, 72.35],
'backgroundColor': '#4CAF50'
}]
}
},
'radar_performance': {
'tipo': 'radar',
'titulo': 'Performance Global',
'datos': {
'labels': ['Eficiencia', 'Financiero', 'Servicio', 'Adopción', 'Innovación'],
'datasets': [{
'label': 'SIGA',
'data': [92, 88, 95, 93, 85],
'backgroundColor': 'rgba(76, 175, 80, 0.2)',
'borderColor': '#4CAF50'
}, {
'label': 'Objetivo',
'data': [80, 80, 90, 80, 80],
'backgroundColor': 'rgba(255, 152, 0, 0.2)',
'borderColor': '#FF9800'
}]
}
}
}
return graficos
┌─────────────────────────────────────────────────────────────────────┐
│ ALERTAS INTELIGENTES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 🔴 CRÍTICAS (1) │
│ └─ Concentración excesiva Barcelona: 7 vehículos (límite: 5) │
│ Acción: Reposicionar 2 vehículos a Tarragona │
│ Impacto potencial: €1,200 pérdida si no se actúa │
│ │
│ 🟡 IMPORTANTES (3) │
│ ├─ Demanda prevista +40% Valencia próximas 24h │
│ │ Acción: Considerar reposicionamiento preventivo │
│ ├─ Margen/km zona Murcia bajo umbral (€0.35 vs €0.50) │
│ │ Acción: Revisar estrategia de precios │
│ └─ 2 conductores próximos a límite horas (9.5h de 11h) │
│ Acción: Planificar relevos │
│ │
│ 🟢 INFORMATIVAS (5) │
│ ├─ ROI proyecto superó objetivo trimestral │
│ ├─ Nuevo récord diario: solo 12.8% km vacíos │
│ ├─ NPS aumentó 5 puntos este mes │
│ ├─ 3 conductores alcanzaron nivel "Experto" en gamificación │
│ └─ Precisión predicciones ML: 89.3% (mejor histórico) │
│ │
│ [Configurar Alertas] [Historial] [Exportar] [Silenciar] │
└─────────────────────────────────────────────────────────────────────┤
class TemporalComparator:
"""Compara métricas entre diferentes períodos"""
def compare_periods(self, metric: str, period1: DateRange, period2: DateRange):
"""Compara una métrica entre dos períodos"""
# Obtener datos
data1 = self.get_metric_data(metric, period1)
data2 = self.get_metric_data(metric, period2)
# Calcular estadísticas
comparison = {
'metric': metric,
'period1': {
'range': period1,
'avg': np.mean(data1),
'min': np.min(data1),
'max': np.max(data1),
'std': np.std(data1),
'trend': self._calculate_trend(data1)
},
'period2': {
'range': period2,
'avg': np.mean(data2),
'min': np.min(data2),
'max': np.max(data2),
'std': np.std(data2),
'trend': self._calculate_trend(data2)
},
'cambios': {
'avg_change': ((np.mean(data2) - np.mean(data1)) / np.mean(data1)) * 100,
'volatility_change': ((np.std(data2) - np.std(data1)) / np.std(data1)) * 100,
'trend_change': self._compare_trends(data1, data2)
}
}
return comparison
┌─────────────────────────────────────────────────────────────────────┐
│ DISPATCHER DASHBOARD │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ DECISIONES PENDIENTES: 3 PRÓXIMA: MAD-4521 (2 min) │
│ │
│ FLOTA EN TIEMPO REAL │
│ ├─ En ruta con carga: 28 vehículos │
│ ├─ En ruta vacío: 7 vehículos │
│ ├─ Cargando/Descargando: 12 vehículos │
│ └─ Esperando: 3 vehículos │
│ │
│ RECOMENDACIONES ACTIVAS │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 1. BCN-2341: Ir a Tarragona vacío │ │
│ │ Confianza: 94% | Valor: +€580 │ │
│ │ [Aceptar] [Rechazar] [Modificar] │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ MÉTRICAS PERSONALES HOY │
│ Decisiones: 47 | Óptimas: 92% | Tiempo promedio: 2.1 min │
└─────────────────────────────────────────────────────────────────────┤
┌─────────────────────────────────────────────────────────────────────┐
│ EXECUTIVE DASHBOARD │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ RESUMEN MENSUAL │
│ │
│ Ahorro generado: €46,500 (+8% vs mes anterior) │
│ ROI acumulado: 268% (objetivo: 200%) │
│ Proyección anual: €558,000 beneficio neto │
│ │
│ TOP MEJORAS ÁREAS DE ATENCIÓN │
│ 1. Km vacíos: -51% 1. 4 conductores sin adoptar │
│ 2. Margen/km: +35% 2. Zona Murcia bajo rendimiento │
│ 3. OTD: 95% (+7pp) 3. Integración ERP pendiente │
│ │
│ COMPARACIÓN COMPETENCIA │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ KPI │ SIGA │ Industria │ Best-in-Class │ │
│ ├─────────────┼───────┼───────────┼────────────────┤ │
│ │ Km vacíos │ 15.7% │ 25% │ 20% │ │
│ │ Utilización │ 89.3% │ 75% │ 85% │ │
│ │ Margen EBITDA│ 18.2% │ 12.5% │ 15.8% │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┤
def generar_vista_conductor_movil(conductor_id: str):
"""Genera vista optimizada para móvil del conductor"""
vista = {
'header': {
'conductor': get_conductor_info(conductor_id),
'vehiculo': get_vehiculo_actual(conductor_id),
'estado': 'En ruta'
},
'metricas_dia': {
'km_recorridos': 342,
'km_vacios': 28,
'entregas': 6,
'tiempo_conduccion': '7h 23m',
'puntos_gamificacion': 145
},
'proxima_accion': {
'tipo': 'entrega',
'direccion': 'C/ Mayor 123, Valencia',
'hora_estimada': '15:45',
'distancia': '23 km',
'instrucciones_especiales': 'Llamar al llegar'
},
'sugerencia_siga': {
'mensaje': 'Después de Valencia, recomendamos ir a Castellón',
'ahorro_estimado': '45 km vacíos evitados',
'confianza': '91%'
},
'ranking_semanal': {
'posicion': 3,
'total_conductores': 50,
'metrica_principal': 'Menor % km vacíos'
}
}
return vista
def generar_reporte_diario():
"""Genera reporte diario automatizado"""
reporte = {
'fecha': datetime.now().date(),
'resumen_ejecutivo': {
'km_vacios_ayer': 15.2,
'ahorro_dia': 1550,
'decisiones_optimas': 94,
'incidencias': 0
},
'metricas_clave': {
'operativas': get_daily_operational_metrics(),
'financieras': get_daily_financial_metrics(),
'adopcion': get_daily_adoption_metrics()
},
'destacados': [
'Nuevo mínimo histórico km vacíos: 15.2%',
'Juan M. completó 100 decisiones óptimas consecutivas',
'ROI proyecto superó 250% por primera vez'
],
'acciones_recomendadas': [
'Revisar concentración vehículos zona Barcelona',
'Actualizar precios ruta Murcia-Almería',
'Planificar mantenimiento MAD-4521'
]
}
# Enviar por email
send_daily_report(reporte, recipients=['gerencia@empresa.com'])
return reporte
┌─────────────────────────────────────────────────────────────────────┐
│ REPORTE SEMANAL SIGA │
│ Semana 29 - 2025 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ RESUMEN EJECUTIVO │
│ Esta semana logramos nuevos récords en eficiencia operativa │
│ │
│ LOGROS PRINCIPALES │
│ • Km vacíos: 15.7% (mejor marca histórica) │
│ • ROI proyecto: 268% (superó objetivo Q3) │
│ • Adopción: 92.5% (4 nuevos power users) │
│ • Ahorro semanal: €11,625 │
│ │
│ TENDENCIAS (vs semana anterior) │
│ ├─ Decisiones automatizadas: +12% │
│ ├─ Tiempo respuesta promedio: -8% │
│ ├─ Satisfacción clientes: +2 puntos │
│ └─ Precisión predicciones: +3.2% │
│ │
│ ANÁLISIS DETALLADO │
│ [Ver informe completo de 15 páginas adjunto] │
└─────────────────────────────────────────────────────────────────────┤
interface DashboardConfig {
usuario: {
rol: 'dispatcher' | 'gerente' | 'conductor' | 'admin';
preferencias: {
tema: 'claro' | 'oscuro';
idioma: 'es' | 'en';
actualizacion: number; // segundos
notificaciones: boolean;
};
};
metricas: {
visibles: string[];
orden: string[];
targets: Record<string, number>;
alertas: AlertConfig[];
};
widgets: {
layout: WidgetLayout[];
configuracion: Record<string, WidgetConfig>;
};
}
// Configuración por defecto según rol
const defaultConfigs: Record<string, DashboardConfig> = {
dispatcher: {
metricas: {
visibles: ['decisiones_pendientes', 'flota_tiempo_real', 'km_vacios'],
orden: ['operativo', 'tactico', 'estrategico'],
// ...
}
},
gerente: {
metricas: {
visibles: ['roi', 'ahorro_mensual', 'tendencias', 'benchmarking'],
orden: ['financiero', 'operativo', 'adopcion'],
// ...
}
}
};
from fastapi import FastAPI, Query
from typing import Optional, List
app = FastAPI()
@app.get("/api/v1/metrics/current")
async def get_current_metrics(
categories: Optional[List[str]] = Query(None),
include_trends: bool = Query(True),
period: str = Query("1d")
):
"""Endpoint para obtener métricas actuales"""
metrics = {}
if not categories or 'operational' in categories:
metrics['operational'] = get_operational_metrics(period)
if not categories or 'financial' in categories:
metrics['financial'] = get_financial_metrics(period)
if not categories or 'adoption' in categories:
metrics['adoption'] = get_adoption_metrics(period)
if include_trends:
metrics['trends'] = calculate_trends(metrics, period)
return {
'timestamp': datetime.now().isoformat(),
'period': period,
'metrics': metrics,
'health_score': calculate_overall_health(metrics)
}
@app.get("/api/v1/metrics/historical")
async def get_historical_metrics(
metric: str,
start_date: datetime,
end_date: datetime,
granularity: str = Query("hourly", regex="^(hourly|daily|weekly|monthly)$")
):
"""Endpoint para obtener datos históricos de una métrica"""
data = fetch_historical_data(metric, start_date, end_date, granularity)
return {
'metric': metric,
'period': {
'start': start_date,
'end': end_date,
'granularity': granularity
},
'data': data,
'statistics': calculate_statistics(data)
}
dashboard_performance = {
'carga_inicial': '2.1s',
'actualizacion_datos': '145ms',
'render_graficos': '320ms',
'queries_por_minuto': 1247,
'usuarios_concurrentes': 34,
'uptime': '99.98%',
'errores_ultima_hora': 0,
'cache_hit_rate': '87%'
}
El Dashboard de Métricas SIGA proporciona:
"Un dashboard no es solo visualización de datos, es el cerebro visual que guía cada decisión hacia la excelencia operativa."