feat: eliminar filtro de tiempo de distancia en las rutas
This commit is contained in:
@@ -31,10 +31,6 @@
|
||||
<div class="kpi-icon"><i class="pi pi-compass"></i></div>
|
||||
<div><span class="kpi-label">Distancia total</span><strong>{{ summary.totalDistance | number:'1.0-1' }} <em>km</em></strong><small>{{ summary.averageDistance | number:'1.0-1' }} km promedio por ruta</small></div>
|
||||
</article>
|
||||
<article class="kpi-card kpi-blue">
|
||||
<div class="kpi-icon"><i class="pi pi-clock"></i></div>
|
||||
<div><span class="kpi-label">Desplazamiento total</span><strong>{{ summary.totalTravelHours | number:'1.0-1' }} <em>h</em></strong><small>{{ summary.averageTravelHours | number:'1.0-1' }} h promedio por ruta</small></div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="kpi-grid operational-grid" *ngIf="hasData">
|
||||
|
||||
@@ -14,9 +14,7 @@ interface KpiSummary {
|
||||
users: number;
|
||||
days: number;
|
||||
totalDistance: number;
|
||||
totalTravelHours: number;
|
||||
averageDistance: number;
|
||||
averageTravelHours: number;
|
||||
totalWorkHours: number;
|
||||
averageRoutesPerUser: number;
|
||||
averageUtilization: number;
|
||||
@@ -95,7 +93,6 @@ export class PlanningKpisComponent implements OnInit, OnDestroy {
|
||||
const totalPdvs = new Set([...assignedPdvIds, ...unassignedPdvIds]).size;
|
||||
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;
|
||||
@@ -146,9 +143,7 @@ export class PlanningKpisComponent implements OnInit, OnDestroy {
|
||||
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,
|
||||
@@ -186,9 +181,7 @@ export class PlanningKpisComponent implements OnInit, OnDestroy {
|
||||
users: 0,
|
||||
days: 0,
|
||||
totalDistance: 0,
|
||||
totalTravelHours: 0,
|
||||
averageDistance: 0,
|
||||
averageTravelHours: 0,
|
||||
totalWorkHours: 0,
|
||||
averageRoutesPerUser: 0,
|
||||
averageUtilization: 0,
|
||||
|
||||
@@ -357,6 +357,13 @@
|
||||
|
||||
.daily-routes-card { padding: 12px; border: 1px solid var(--glm-gris-medio); border-top: 3px solid var(--glm-azul); border-radius: 4px; background: var(--glm-surface); }
|
||||
.daily-routes-card h3 { margin: 0 0 8px; color: var(--glm-azul); font-size: 0.82rem; text-transform: uppercase; }
|
||||
.daily-route-group { border-bottom: 1px solid var(--glm-gris-claro); }
|
||||
.daily-route-group:last-child { border-bottom: 0; }
|
||||
.daily-route-toggle { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 0; border: 0; background: transparent; color: var(--glm-gris-oscuro); cursor: pointer; font: inherit; text-align: left; }
|
||||
.daily-route-toggle:hover { background: var(--glm-gris-claro); }
|
||||
.daily-route-toggle-title { color: var(--glm-azul); font-weight: 700; }
|
||||
.daily-route-toggle > span:last-child { color: var(--glm-acero); font-size: 0.78rem; }
|
||||
.daily-route-group-content { padding-left: 14px; }
|
||||
.daily-route { display: grid; gap: 3px; padding: 8px 0; border-bottom: 1px solid var(--glm-gris-claro); color: var(--glm-gris-oscuro); font-size: 0.78rem; }
|
||||
.daily-route:last-child { border-bottom: 0; }
|
||||
.daily-route span, .daily-route small { color: var(--glm-acero); }
|
||||
|
||||
+13
-20
@@ -31,7 +31,7 @@
|
||||
</select>
|
||||
|
||||
<div class="filter-stats" *ngIf="filteredRoutes.length">
|
||||
<span>{{ filteredRoutes.length }} PDVs</span>
|
||||
<span>{{ uniqueFilteredRoutes.length }} PDVs</span>
|
||||
<span>{{ totalDistancia }} km</span>
|
||||
<span>{{ totalDuracionMin }} min</span>
|
||||
</div>
|
||||
@@ -152,26 +152,19 @@
|
||||
|
||||
<section class="daily-routes-card" *ngIf="dailySummaries.length">
|
||||
<h3>Rutas diarias</h3>
|
||||
<article class="daily-route" *ngFor="let day of dailySummaries" [class.over-capacity]="day.exceedsCapacity">
|
||||
<strong>{{ day.userName }} · {{ day.day }}</strong>
|
||||
<span>{{ day.routes.length }} paradas · {{ day.distanceKm | number:'1.1-1' }} km · {{ (day.travelHours * 60) | number:'1.0-0' }} min</span>
|
||||
<small [attr.aria-label]="day.exceedsCapacity ? 'Capacidad excedida' : 'Dentro de capacidad'">{{ day.totalHours | number:'1.1-1' }} / {{ day.capacityHours | number:'1.1-1' }} h {{ day.exceedsCapacity ? '· Excede capacidad' : '· Capacidad disponible' }}</small>
|
||||
<article class="daily-route-group" *ngFor="let group of dailyGroups">
|
||||
<button type="button" class="daily-route-toggle" (click)="toggleDailyRouteDay(group.day)" [attr.aria-expanded]="isDailyRouteDayExpanded(group.day)">
|
||||
<span class="daily-route-toggle-title"><i class="pi" [class.pi-chevron-right]="!isDailyRouteDayExpanded(group.day)" [class.pi-chevron-down]="isDailyRouteDayExpanded(group.day)"></i> {{ group.day }}</span>
|
||||
<span>{{ group.totalStops }} paradas · {{ group.totalDistance | number:'1.1-1' }} km · {{ (group.totalTravelHours * 60) | number:'1.0-0' }} min</span>
|
||||
</button>
|
||||
<div class="daily-route-group-content" *ngIf="isDailyRouteDayExpanded(group.day)">
|
||||
<article class="daily-route" *ngFor="let route of group.routes" [class.over-capacity]="route.exceedsCapacity">
|
||||
<strong>{{ route.userName }}</strong>
|
||||
<span>{{ route.routes.length }} paradas · {{ route.distanceKm | number:'1.1-1' }} km · {{ (route.travelHours * 60) | number:'1.0-0' }} min</span>
|
||||
<small [attr.aria-label]="route.exceedsCapacity ? 'Capacidad excedida' : 'Dentro de capacidad'">{{ route.totalHours | number:'1.1-1' }} / {{ route.capacityHours | number:'1.1-1' }} h {{ route.exceedsCapacity ? '· Excede capacidad' : '· Capacidad disponible' }}</small>
|
||||
</article>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="routes-list" *ngIf="filteredRoutes.length">
|
||||
<h3>{{ selectedUsuarioId === 'Todos' ? 'PDVs de todos los usuarios' : 'PDVs del usuario' }} ({{ filteredRoutes.length }})</h3>
|
||||
<ul>
|
||||
<li *ngFor="let r of filteredRoutes; let i = index" tabindex="0" role="button"
|
||||
[class.selected]="selectedRouteDetail && selectedRouteDetail.json.pdv_id === r.json.pdv_id && selectedRouteDetail.json.usuario_id === r.json.usuario_id && selectedRouteDetail.json.dia === r.json.dia"
|
||||
(click)="selectRouteFromList(r)" (keydown.enter)="selectRouteFromList(r)" (keydown.space)="selectRouteFromList(r); $event.preventDefault()">
|
||||
<span class="idx">{{ routeVisitOrder(r) }}</span>
|
||||
<div class="li-main">
|
||||
<strong>{{ r.json.nombre_pdv }}</strong>
|
||||
<small>{{ selectedUsuarioId === 'Todos' ? r.json.nombre_usuario + ' • ' : '' }}{{ r.json.dia }} • {{ routeSegmentDistance(r) | number:'1.2-2' }} km • {{ routeSegmentDurationMinutes(r) | number:'1.0-0' }} min</small>
|
||||
</div>
|
||||
<i class="pi pi-chevron-right"></i>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+63
-4
@@ -11,6 +11,14 @@ import { RouteAssignedResponse, RouteNoAssignedResponse } from '../../../core/mo
|
||||
import { colorForUsuario } from '../../../shared/utils/palette.utils';
|
||||
import { DailyRouteSummary, haversineKm, sequenceRoutes, summarizeDailyRoutes } from '../../../shared/utils/route-sequencing.utils';
|
||||
|
||||
interface DailyRouteGroup {
|
||||
day: string;
|
||||
routes: DailyRouteSummary[];
|
||||
totalStops: number;
|
||||
totalDistance: number;
|
||||
totalTravelHours: number;
|
||||
}
|
||||
|
||||
interface MapRoute {
|
||||
data: RouteAssignedResponse;
|
||||
color: string;
|
||||
@@ -53,6 +61,7 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
dias: string[] = [];
|
||||
selectedUsuarioId: string = 'Todos';
|
||||
selectedDia: string = 'Todos';
|
||||
expandedDailyRouteDay: string | null = null;
|
||||
selectedRouteDetail: RouteAssignedResponse | null = null;
|
||||
|
||||
filteredRoutes: RouteAssignedResponse[] = [];
|
||||
@@ -81,16 +90,41 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
reassignError: string | null = null;
|
||||
|
||||
// Summary stats
|
||||
get uniqueFilteredRoutes(): RouteAssignedResponse[] {
|
||||
const unique = new Map<string, RouteAssignedResponse>();
|
||||
for (const route of this.filteredRoutes) {
|
||||
const pdvId = String(route.json.pdv_id);
|
||||
if (!unique.has(pdvId)) unique.set(pdvId, route);
|
||||
}
|
||||
return Array.from(unique.values());
|
||||
}
|
||||
|
||||
get totalDistancia(): string {
|
||||
const uniqueIds = new Set(this.uniqueFilteredRoutes.map(route => String(route.json.pdv_id)));
|
||||
const sum = this.mapRoutes.length
|
||||
? this.mapRoutes.reduce((acc, route) => acc + (route.segmentDistanceKm ?? haversineKm(route.origin, route.destination)), 0)
|
||||
: this.filteredRoutes.reduce((acc, route) => acc + Number(route.json.distancia), 0);
|
||||
? this.mapRoutes
|
||||
.filter(route => uniqueIds.has(String(route.data.json.pdv_id)))
|
||||
.reduce((acc, route) => {
|
||||
const firstSegment = this.mapRoutes.find(candidate => String(candidate.data.json.pdv_id) === String(route.data.json.pdv_id));
|
||||
return firstSegment === route
|
||||
? acc + (route.segmentDistanceKm ?? haversineKm(route.origin, route.destination))
|
||||
: acc;
|
||||
}, 0)
|
||||
: this.uniqueFilteredRoutes.reduce((acc, route) => acc + Number(route.json.distancia), 0);
|
||||
return sum.toFixed(2);
|
||||
}
|
||||
get totalDuracionMin(): string {
|
||||
const uniqueIds = new Set(this.uniqueFilteredRoutes.map(route => String(route.json.pdv_id)));
|
||||
const sum = this.mapRoutes.length
|
||||
? this.mapRoutes.reduce((acc, route) => acc + (route.segmentDurationHours ?? Number(route.data.json.horas_desplazamiento)) * 60, 0)
|
||||
: this.filteredRoutes.reduce((acc, route) => acc + Number(route.json.horas_desplazamiento) * 60, 0);
|
||||
? this.mapRoutes
|
||||
.filter(route => uniqueIds.has(String(route.data.json.pdv_id)))
|
||||
.reduce((acc, route) => {
|
||||
const firstSegment = this.mapRoutes.find(candidate => String(candidate.data.json.pdv_id) === String(route.data.json.pdv_id));
|
||||
return firstSegment === route
|
||||
? acc + (route.segmentDurationHours ?? Number(route.data.json.horas_desplazamiento)) * 60
|
||||
: acc;
|
||||
}, 0)
|
||||
: this.uniqueFilteredRoutes.reduce((acc, route) => acc + Number(route.json.horas_desplazamiento) * 60, 0);
|
||||
return sum.toFixed(0);
|
||||
}
|
||||
|
||||
@@ -98,6 +132,31 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
return summarizeDailyRoutes(this.filteredRoutes);
|
||||
}
|
||||
|
||||
get dailyGroups(): DailyRouteGroup[] {
|
||||
const order = ['Lunes', 'Martes', 'Miércoles', 'Miercoles', 'Jueves', 'Viernes'];
|
||||
const groups = new Map<string, DailyRouteSummary[]>();
|
||||
for (const summary of this.dailySummaries) {
|
||||
const routes = groups.get(summary.day) ?? [];
|
||||
routes.push(summary);
|
||||
groups.set(summary.day, routes);
|
||||
}
|
||||
return Array.from(groups, ([day, routes]) => ({
|
||||
day,
|
||||
routes,
|
||||
totalStops: routes.reduce((sum, route) => sum + route.routes.length, 0),
|
||||
totalDistance: routes.reduce((sum, route) => sum + route.distanceKm, 0),
|
||||
totalTravelHours: routes.reduce((sum, route) => sum + route.travelHours, 0)
|
||||
})).sort((a, b) => order.indexOf(a.day) - order.indexOf(b.day));
|
||||
}
|
||||
|
||||
toggleDailyRouteDay(day: string): void {
|
||||
this.expandedDailyRouteDay = this.expandedDailyRouteDay === day ? null : day;
|
||||
}
|
||||
|
||||
isDailyRouteDayExpanded(day: string): boolean {
|
||||
return this.expandedDailyRouteDay === day;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private routeState: RouteState,
|
||||
private orsService: OrsService,
|
||||
|
||||
Reference in New Issue
Block a user