feat: añadir lista de empleados con carga al reasignar ruta
This commit is contained in:
+95
-21
@@ -16,7 +16,14 @@ interface DailyRouteGroup {
|
||||
routes: DailyRouteSummary[];
|
||||
totalStops: number;
|
||||
totalDistance: number;
|
||||
totalTravelHours: number;
|
||||
}
|
||||
|
||||
interface EmployeeLoadOption {
|
||||
id: string;
|
||||
name: string;
|
||||
workHours: number;
|
||||
capacityHours: number;
|
||||
utilization: number;
|
||||
}
|
||||
|
||||
interface MapRoute {
|
||||
@@ -53,7 +60,6 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
private selectedMapRoute: MapRoute | null = null;
|
||||
private heatLayer?: any;
|
||||
showHeat = true;
|
||||
heatWeightMode: 'uniform' | 'horas_trabajo' = 'horas_trabajo';
|
||||
private heatPointsCache: [number, number, number][] = [];
|
||||
private unassignedMarkers = new Map<number, L.Marker>();
|
||||
|
||||
@@ -72,6 +78,7 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
orsTotal = 0;
|
||||
error: string | null = null;
|
||||
unassignedRoutes: RouteNoAssignedResponse[] = [];
|
||||
isUnassignedDrawerOpen = false;
|
||||
selectedUnassigned: RouteNoAssignedResponse | null = null;
|
||||
editingUnassigned: RouteNoAssignedResponse | null = null;
|
||||
editUnassignedName = '';
|
||||
@@ -144,8 +151,7 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
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)
|
||||
totalDistance: routes.reduce((sum, route) => sum + route.distanceKm, 0)
|
||||
})).sort((a, b) => order.indexOf(a.day) - order.indexOf(b.day));
|
||||
}
|
||||
|
||||
@@ -157,6 +163,20 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
return this.expandedDailyRouteDay === day;
|
||||
}
|
||||
|
||||
openDestinationInGoogleMaps(): void {
|
||||
if (!this.selectedRouteDetail) return;
|
||||
const { latitud_pdv, longitud_pdv } = this.selectedRouteDetail.json;
|
||||
const latitude = Number(latitud_pdv);
|
||||
const longitude = Number(longitud_pdv);
|
||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return;
|
||||
const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${latitude},${longitude}`)}`;
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
toggleUnassignedDrawer(): void {
|
||||
this.isUnassignedDrawerOpen = !this.isUnassignedDrawerOpen;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private routeState: RouteState,
|
||||
private orsService: OrsService,
|
||||
@@ -271,7 +291,11 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
private initMap() {
|
||||
this.map = L.map('map-overview').setView([18.4861, -69.9312], 10);
|
||||
const pdvPane = this.map.createPane('pdvPane');
|
||||
pdvPane.style.zIndex = '650';
|
||||
const heatPane = this.map.createPane('heatPane');
|
||||
pdvPane.style.zIndex = '750';
|
||||
heatPane.style.zIndex = '700';
|
||||
const popupPane = this.map.getPane('popupPane');
|
||||
if (popupPane) popupPane.style.zIndex = '1000';
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(this.map);
|
||||
@@ -538,6 +562,9 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
}
|
||||
|
||||
private selectRoute(mr: MapRoute) {
|
||||
this.selectedUnassigned = null;
|
||||
this.editingUnassigned = null;
|
||||
this.unassignedActionError = null;
|
||||
this.selectedRouteDetail = mr.data;
|
||||
this.highlightRoute(mr.data);
|
||||
this.fitSelectedRoute(mr);
|
||||
@@ -545,6 +572,9 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
}
|
||||
|
||||
selectRouteFromList(route: RouteAssignedResponse): void {
|
||||
this.selectedUnassigned = null;
|
||||
this.editingUnassigned = null;
|
||||
this.unassignedActionError = null;
|
||||
const mapRoute = this.mapRoutes.find(mr => this.sameRoute(mr.data, route));
|
||||
if (mapRoute) {
|
||||
this.selectRoute(mapRoute);
|
||||
@@ -693,14 +723,62 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
left.json.dia === right.json.dia;
|
||||
}
|
||||
|
||||
private getEmployeeLoads(excluding?: RouteAssignedResponse): EmployeeLoadOption[] {
|
||||
const profiles = new Map<string, RouteAssignedResponse>();
|
||||
const workHours = new Map<string, number>();
|
||||
for (const route of this.routeState.getAssignedSnapshot()) {
|
||||
const id = String(route.json.usuario_id);
|
||||
if (!profiles.has(id)) profiles.set(id, route);
|
||||
if (route !== excluding) {
|
||||
workHours.set(id, (workHours.get(id) ?? 0) + Number(route.json.horas_trabajo || route.json.hora_minima_semanal_pdv || 0));
|
||||
}
|
||||
}
|
||||
return Array.from(profiles, ([id, profile]) => {
|
||||
const capacityHours = Number(profile.json.hora_laboral_semanal_usuario || 0);
|
||||
const assignedHours = workHours.get(id) ?? 0;
|
||||
return {
|
||||
id,
|
||||
name: profile.json.nombre_usuario,
|
||||
workHours: assignedHours,
|
||||
capacityHours,
|
||||
utilization: capacityHours ? (assignedHours / capacityHours) * 100 : 0
|
||||
};
|
||||
}).sort((a, b) => a.utilization - b.utilization || a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
get unassignedUserOptions(): { id: string; name: string }[] {
|
||||
return this.routeState.getUsuariosUnicos().map(user => ({ id: user.id, name: user.nombre }));
|
||||
}
|
||||
|
||||
get unassignedEmployeeLoads(): EmployeeLoadOption[] {
|
||||
return this.getEmployeeLoads();
|
||||
}
|
||||
|
||||
get reassignmentEmployeeLoads(): EmployeeLoadOption[] {
|
||||
return this.getEmployeeLoads(this.selectedRouteDetail ?? undefined);
|
||||
}
|
||||
|
||||
get unassignedDayOptions(): string[] {
|
||||
return this.routeState.getDiasUnicos();
|
||||
}
|
||||
|
||||
employeeLoadWidth(employee: EmployeeLoadOption): number {
|
||||
return Math.min(Math.max(employee.utilization, 0), 100);
|
||||
}
|
||||
|
||||
employeeLoadStatus(employee: EmployeeLoadOption): 'ok' | 'warning' | 'over-capacity' {
|
||||
return employee.utilization > 100 ? 'over-capacity' : employee.utilization > 90 ? 'warning' : 'ok';
|
||||
}
|
||||
|
||||
selectUnassignedEmployee(employeeId: string): void {
|
||||
if (!this.selectedUnassigned) return;
|
||||
this.assignmentUser[String(this.selectedUnassigned.pdv_id)] = employeeId;
|
||||
}
|
||||
|
||||
selectReassignmentEmployee(employeeId: string): void {
|
||||
this.reassignUserId = employeeId;
|
||||
}
|
||||
|
||||
toggleUnassignedDay(pdvId: number, day: string, checked: boolean): void {
|
||||
const key = String(pdvId);
|
||||
const days = new Set(this.assignmentDays[key] ?? []);
|
||||
@@ -711,6 +789,8 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
selectUnassigned(route: RouteNoAssignedResponse): void {
|
||||
this.selectedUnassigned = route;
|
||||
this.selectedRouteDetail = null;
|
||||
this.showReassignForm = false;
|
||||
this.reassignError = null;
|
||||
this.map?.setView([route.latitud, route.longitud], Math.max(this.map.getZoom(), 14));
|
||||
this.unassignedMarkers.get(route.pdv_id)?.openPopup();
|
||||
this.cdr.detectChanges();
|
||||
@@ -815,13 +895,12 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
}
|
||||
|
||||
private computeWeight(j: RouteAssignedResponse['json']): number {
|
||||
if (this.heatWeightMode === 'uniform') return 0.8;
|
||||
const v = parseFloat(String(j.horas_trabajo));
|
||||
if (isNaN(v)) return 0.8;
|
||||
if (isNaN(v)) return 1;
|
||||
const vals = this.filteredRoutes.map(r => parseFloat(String(r.json.horas_trabajo))).filter(n => !isNaN(n));
|
||||
if (!vals.length) return 0.8;
|
||||
if (!vals.length) return 1;
|
||||
const min = Math.min(...vals), max = Math.max(...vals);
|
||||
if (max === min) return 0.8;
|
||||
if (max === min) return 1;
|
||||
return 0.3 + 0.7 * (v - min) / (max - min);
|
||||
}
|
||||
|
||||
@@ -832,8 +911,13 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
}
|
||||
if (!this.showHeat || !this.heatPointsCache.length || !this.map) return;
|
||||
this.heatLayer = (L as any).heatLayer(this.heatPointsCache, {
|
||||
radius: 25, blur: 18, maxZoom: 17, minOpacity: 0.4,
|
||||
gradient: { 0.4: '#6CC24A', 0.65: '#FF6A13', 1.0: '#D32F2F' }
|
||||
pane: 'heatPane',
|
||||
radius: 32,
|
||||
blur: 24,
|
||||
maxZoom: 18,
|
||||
minOpacity: 0.55,
|
||||
max: 1.8,
|
||||
gradient: { 0.2: '#b7f05f', 0.45: '#6CC24A', 0.7: '#FF6A13', 1.0: '#D32F2F' }
|
||||
}).addTo(this.map);
|
||||
}
|
||||
|
||||
@@ -848,16 +932,6 @@ export class RouteOverviewMapComponent implements OnInit, OnDestroy, AfterViewIn
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
setHeatWeight(mode: 'uniform' | 'horas_trabajo') {
|
||||
this.heatWeightMode = mode;
|
||||
this.heatPointsCache = this.filteredRoutes.map(r => {
|
||||
const j = r.json;
|
||||
return [Number(j.latitud_pdv), Number(j.longitud_pdv), this.computeWeight(j)] as [number, number, number];
|
||||
});
|
||||
this.updateHeatLayer();
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
private toggleLayerVisibility() {
|
||||
this.mapRoutes.forEach(mr => {
|
||||
if (mr.fallbackLine) (mr.fallbackLine as any).setStyle({ opacity: 0.6 });
|
||||
|
||||
Reference in New Issue
Block a user