import { Component, OnDestroy, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Subscription } from 'rxjs'; import { RouteState } from '../../../core/route-state/route'; import { PlanningExportService } from '../../../shared/services/planning-export.service'; import { summarizeDailyRoutes } from '../../../shared/utils/route-sequencing.utils'; import { RouteAssignedResponse } from '../../../core/models/route.model'; interface KpiSummary { totalPdvs: number; assignedPdvs: number; unassignedPdvs: number; coverage: number; users: number; days: number; totalDistance: number; totalTravelHours: number; averageDistance: number; averageTravelHours: number; totalWorkHours: number; averageRoutesPerUser: number; averageUtilization: number; assignmentBalance: number; } interface ReasonSummary { reason: string; count: number; } interface UserLoad { id: string; name: string; routes: number; distance: number; workHours: number; capacityHours: number; utilization: number; status: 'ok' | 'warning' | 'over-capacity'; alertMessage: string; } @Component({ standalone: true, selector: 'app-planning-kpis', imports: [CommonModule], templateUrl: './planning-kpis.component.html', styleUrl: './planning-kpis.component.css' }) export class PlanningKpisComponent implements OnInit, OnDestroy { summary: KpiSummary = this.emptySummary(); reasons: ReasonSummary[] = []; userLoads: UserLoad[] = []; hasData = false; private readonly subs: Subscription[] = []; constructor( private routeState: RouteState, private exportService: PlanningExportService ) {} ngOnInit(): void { this.subs.push( this.routeState.assignedRoutes$.subscribe(() => this.refresh()), this.routeState.noAssignedRoutes$.subscribe(() => this.refresh()) ); } ngOnDestroy(): void { this.subs.forEach(subscription => subscription.unsubscribe()); } barWidth(value: number): number { return Math.min(Math.max(value, 0), 100); } utilizationClass(load: UserLoad): string { return load.status; } exportPlanning(): void { this.exportService.export(); } private refresh(): void { const assigned = this.routeState.getAssignedSnapshot(); const unassigned = this.routeState.getNoAssignedSnapshot(); const totalPdvs = assigned.length + unassigned.length; const dailyRoutes = summarizeDailyRoutes(assigned); const totalDistance = dailyRoutes.reduce((sum, day) => sum + day.distanceKm, 0); const totalTravelHours = dailyRoutes.reduce((sum, day) => sum + day.travelHours, 0); const totalWorkHours = dailyRoutes.reduce((sum, day) => sum + day.workHours, 0); const users = new Set(assigned.map(route => String(route.json.usuario_id))).size; const days = new Set(assigned.map(route => route.json.dia)).size; const loads = new Map(); for (const route of assigned) { const key = String(route.json.usuario_id); const current = loads.get(key) ?? { id: key, name: route.json.nombre_usuario, routes: 0, distance: 0, workHours: 0, capacityHours: this.numberValue(route.json.hora_laboral_semanal_usuario), utilization: 0, status: 'ok', alertMessage: '' }; current.routes++; // Sequential distance and work are aggregated per user below. loads.set(key, current); } this.userLoads = Array.from(loads.values()).map(load => { const userDays = dailyRoutes.filter(day => day.userId === load.id); load.distance = userDays.reduce((sum, day) => sum + day.distanceKm, 0); load.workHours = userDays.reduce((sum, day) => sum + day.workHours, 0); load.utilization = load.capacityHours ? (load.workHours / load.capacityHours) * 100 : 0; load.status = load.utilization > 100 ? 'over-capacity' : load.utilization > 90 ? 'warning' : 'ok'; load.alertMessage = load.status === 'over-capacity' ? `Alerta: ${load.name} supera el 100% de su capacidad por carga (${load.workHours.toFixed(1)} h de ${load.capacityHours.toFixed(1)} h). Reasigna PDVs o reduce horas.` : load.status === 'warning' ? `${load.name} está sobre el 90% de su capacidad por carga (${load.workHours.toFixed(1)} h de ${load.capacityHours.toFixed(1)} h).` : `${load.name} está dentro de capacidad por carga (${load.workHours.toFixed(1)} h de ${load.capacityHours.toFixed(1)} h).`; return load; }).sort((a, b) => b.routes - a.routes || b.workHours - a.workHours); const routeCounts = this.userLoads.map(load => load.routes); const assignmentBalance = routeCounts.length ? Math.max(...routeCounts) - Math.min(...routeCounts) : 0; const averageUtilization = this.userLoads.length ? this.userLoads.reduce((sum, load) => sum + load.utilization, 0) / this.userLoads.length : 0; this.summary = { totalPdvs, assignedPdvs: assigned.length, unassignedPdvs: unassigned.length, coverage: totalPdvs ? (assigned.length / totalPdvs) * 100 : 0, users, days, totalDistance, totalTravelHours, averageDistance: assigned.length ? totalDistance / assigned.length : 0, averageTravelHours: assigned.length ? totalTravelHours / assigned.length : 0, totalWorkHours, averageRoutesPerUser: users ? assigned.length / users : 0, averageUtilization, assignmentBalance }; const reasonCounts = new Map(); for (const route of unassigned) { const reason = route.motivo?.trim() || 'Sin motivo especificado'; reasonCounts.set(reason, (reasonCounts.get(reason) ?? 0) + 1); } this.reasons = Array.from(reasonCounts.entries()) .map(([reason, count]) => ({ reason, count })) .sort((a, b) => b.count - a.count); this.hasData = totalPdvs > 0; } private numberValue(value: number | string | null | undefined): number { const result = Number(value); return Number.isFinite(result) ? result : 0; } private emptySummary(): KpiSummary { return { totalPdvs: 0, assignedPdvs: 0, unassignedPdvs: 0, coverage: 0, users: 0, days: 0, totalDistance: 0, totalTravelHours: 0, averageDistance: 0, averageTravelHours: 0, totalWorkHours: 0, averageRoutesPerUser: 0, averageUtilization: 0, assignmentBalance: 0 }; } }