238 lines
7.5 KiB
TypeScript
238 lines
7.5 KiB
TypeScript
import { Component, ChangeDetectorRef, ViewChild } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { Router } from '@angular/router';
|
|
import { RouteAssignedResponse } from '../../../core/models/route.model';
|
|
import { saveAs } from 'file-saver';
|
|
import { Table } from 'primeng/table';
|
|
import { TableModule } from 'primeng/table';
|
|
import { InputTextModule } from 'primeng/inputtext';
|
|
import { TooltipModule } from 'primeng/tooltip';
|
|
import { RouteState } from '../../../core/route-state/route';
|
|
import { RutasService } from '../../../core/services/router.service';
|
|
import { PlanningInputService } from '../../../core/services/planning-input.service';
|
|
|
|
@Component({
|
|
standalone: true,
|
|
selector: 'app-assigned-route',
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule,
|
|
TableModule,
|
|
InputTextModule,
|
|
TooltipModule
|
|
],
|
|
templateUrl: './assigned-route.component.html',
|
|
styleUrl: './assigned-route.component.css'
|
|
})
|
|
export class AssignedRouteComponent {
|
|
@ViewChild('dt') table!: Table;
|
|
rutasExpandido = false;
|
|
tablaRutas = '';
|
|
assignedRoutes: RouteAssignedResponse[] = [];
|
|
isLoading = false;
|
|
cols: any[] = [];
|
|
editingRoute: RouteAssignedResponse | null = null;
|
|
editUserId = '';
|
|
editDay = '';
|
|
hasLocalChanges = false;
|
|
editError: string | null = null;
|
|
reoptLoading = false;
|
|
reoptError: string | null = null;
|
|
|
|
constructor(
|
|
private routesState: RouteState,
|
|
private cdr: ChangeDetectorRef,
|
|
private router: Router,
|
|
private rutasService: RutasService,
|
|
private planningInput: PlanningInputService
|
|
) { }
|
|
|
|
private subs: import('rxjs').Subscription[] = [];
|
|
|
|
ngOnInit() {
|
|
this.routesState.resetLoading();
|
|
this.subs.push(
|
|
this.routesState.loading$.subscribe(value => {
|
|
this.isLoading = value;
|
|
this.cdr.detectChanges();
|
|
})
|
|
);
|
|
this.subs.push(
|
|
this.routesState.assignedRoutes$.subscribe(data => {
|
|
this.assignedRoutes = [...data];
|
|
this.cdr.detectChanges();
|
|
})
|
|
);
|
|
this.cols = [
|
|
{ field: 'json.nombre_usuario', header: 'Usuario' },
|
|
{ field: 'json.nombre_pdv', header: 'PDV' },
|
|
{ field: 'json.hora_laboral_semanal_usuario', header: 'Sem. Usuario (hr)' },
|
|
{ field: 'json.hora_minima_semanal_pdv', header: 'Horas Min. PDV' },
|
|
{ field: 'json.horas_trabajo', header: 'Horas Trabajo' },
|
|
{ field: 'json.dia', header: 'Día' },
|
|
{ field: 'json.distancia', header: 'Distancia (km)' },
|
|
{ field: 'json.horas_desplazamiento', header: 'Duración (min)' },
|
|
{ field: '', header: '' }
|
|
];
|
|
|
|
}
|
|
onShowMap(row: RouteAssignedResponse) {
|
|
this.routesState.sendRoute(row);
|
|
this.router.navigate(['/home/assigned-route'], {
|
|
queryParams: { pdv_id: row.json.pdv_id, usuario_id: row.json.usuario_id, dia: row.json.dia }
|
|
});
|
|
}
|
|
|
|
goToOverviewMap() {
|
|
// If table has filtered view, use first visible row's usuario as default; else first assigned
|
|
const target = (this.table?.filteredValue?.[0] as RouteAssignedResponse | undefined) ?? this.assignedRoutes[0];
|
|
if (target) {
|
|
this.router.navigate(['/home/mapa'], {
|
|
queryParams: { usuario_id: target.json.usuario_id, dia: 'Todos' }
|
|
});
|
|
} else {
|
|
this.router.navigate(['/home/mapa']);
|
|
}
|
|
}
|
|
|
|
goToUserMap(usuarioId: string) {
|
|
this.router.navigate(['/home/mapa'], { queryParams: { usuario_id: usuarioId, dia: 'Todos' } });
|
|
}
|
|
|
|
get userOptions(): { id: string; name: string }[] {
|
|
const users = new Map<string, string>();
|
|
for (const route of this.assignedRoutes) users.set(String(route.json.usuario_id), route.json.nombre_usuario);
|
|
return Array.from(users, ([id, name]) => ({ id, name }));
|
|
}
|
|
|
|
get dayOptions(): string[] {
|
|
return this.routesState.getDiasUnicos();
|
|
}
|
|
|
|
startEdit(route: RouteAssignedResponse): void {
|
|
this.editingRoute = route;
|
|
this.editUserId = String(route.json.usuario_id);
|
|
this.editDay = route.json.dia;
|
|
this.editError = null;
|
|
}
|
|
|
|
cancelEdit(): void {
|
|
this.editingRoute = null;
|
|
}
|
|
|
|
saveEdit(): void {
|
|
if (!this.editingRoute || !this.editUserId || !this.editDay) return;
|
|
const capacity = this.routesState.canAssignToDay(
|
|
this.editUserId,
|
|
this.editDay,
|
|
Number(this.editingRoute.json.horas_trabajo || this.editingRoute.json.hora_minima_semanal_pdv || 0),
|
|
this.editingRoute
|
|
);
|
|
if (!capacity.allowed) {
|
|
this.editError = `La asignación excede la capacidad diaria (${capacity.totalHours.toFixed(1)} h de ${capacity.capacityHours.toFixed(1)} h).`;
|
|
return;
|
|
}
|
|
this.routesState.updateAssignedRoute(this.editingRoute, this.editUserId, this.editDay);
|
|
this.hasLocalChanges = true;
|
|
this.editingRoute = null;
|
|
}
|
|
|
|
get canReoptimize(): boolean {
|
|
return this.assignedRoutes.length > 0 && !!this.planningInput.getFormData() && !this.reoptLoading;
|
|
}
|
|
|
|
reoptimizeLocally(): void {
|
|
const data = this.planningInput.getFormData();
|
|
if (!data || !this.assignedRoutes.length || this.reoptLoading) return;
|
|
this.reoptLoading = true;
|
|
this.reoptError = null;
|
|
this.routesState.setLoading(true);
|
|
this.rutasService.reoptimizarRutas(data).subscribe({
|
|
next: response => {
|
|
const items = response as unknown as unknown[];
|
|
const assigned = items.find(item => item && typeof item === 'object' && 'assigned' in item) as { assigned?: unknown } | undefined;
|
|
const noAssigned = items.find(item => item && typeof item === 'object' && 'noAssigned' in item) as { noAssigned?: unknown } | undefined;
|
|
if (assigned?.assigned) this.routesState.setAssignedRoutes(assigned.assigned as never);
|
|
if (noAssigned?.noAssigned) this.routesState.setNoAssignedRoutes(noAssigned.noAssigned as never);
|
|
this.hasLocalChanges = false;
|
|
this.reoptLoading = false;
|
|
this.routesState.setLoading(false);
|
|
this.cdr.detectChanges();
|
|
},
|
|
error: err => {
|
|
this.reoptError = err?.error?.error || err?.message || 'No se pudo reoptimizar la planificación';
|
|
this.reoptLoading = false;
|
|
this.routesState.setLoading(false);
|
|
this.cdr.detectChanges();
|
|
}
|
|
});
|
|
this.editingRoute = null;
|
|
}
|
|
|
|
restoreOriginal(): void {
|
|
this.routesState.restoreOriginalAssignedRoutes();
|
|
this.hasLocalChanges = false;
|
|
this.editingRoute = null;
|
|
}
|
|
toggleRutas() {
|
|
this.rutasExpandido = !this.rutasExpandido;
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.subs.forEach(s => s.unsubscribe());
|
|
}
|
|
|
|
downloadCSV() {
|
|
const headers = [
|
|
'Usuario ID',
|
|
'Usuario',
|
|
'Long Usuario',
|
|
'Lat Usuario',
|
|
'PDV ID',
|
|
'PDV',
|
|
'Long PDV',
|
|
'Lat PDV',
|
|
'Horas Usuario',
|
|
'Horas PDV',
|
|
'Horas Trabajo',
|
|
'Día',
|
|
'Distancia',
|
|
'Duracion'
|
|
];
|
|
|
|
const data =
|
|
this.table.filteredValue && this.table.filteredValue.length
|
|
? this.table.filteredValue
|
|
: this.assignedRoutes;
|
|
|
|
const rows = data.map(r => [
|
|
r.json.usuario_id,
|
|
r.json.nombre_usuario,
|
|
r.json.longitud_usuario,
|
|
r.json.latitud_usuario,
|
|
r.json.pdv_id,
|
|
r.json.nombre_pdv,
|
|
r.json.longitud_pdv,
|
|
r.json.latitud_pdv,
|
|
r.json.hora_laboral_semanal_usuario,
|
|
r.json.hora_minima_semanal_pdv,
|
|
r.json.horas_trabajo,
|
|
r.json.dia,
|
|
r.json.distancia,
|
|
r.json.horas_desplazamiento
|
|
]);
|
|
|
|
const csvContent =
|
|
[headers, ...rows].map(r => r.join(',')).join('\n');
|
|
|
|
const bom = '\uFEFF';
|
|
const blob = new Blob([bom + csvContent], {
|
|
type: 'text/csv;charset=utf-8;'
|
|
});
|
|
|
|
saveAs(blob, 'rutas_planificadas.csv');
|
|
}
|
|
|
|
}
|