Files
rutero/route-planner/src/app/private/home/route-overview-map/route-overview-map.component.html
T

184 lines
13 KiB
HTML

<div id="map-overview"></div>
<div class="overview-loading" *ngIf="isLoading">
<div class="spinner"></div>
<p>Cargando rutas ORS... {{ orsProgress }}/{{ orsTotal }}</p>
</div>
<div class="overview-sidebar">
<div class="sidebar-brand">
<img src="GLM_completo.png" alt="GomezLee Marketing" class="sidebar-logo" />
</div>
<div class="sidebar-header">
<button type="button" class="back-btn" (click)="goBack()" aria-label="Volver a la planificación">
<i class="pi pi-arrow-left"></i> Volver
</button>
<h2 class="sidebar-title">Mapa de rutas</h2>
</div>
<p class="territory-summary" *ngIf="activeTerritories.length"><i class="pi pi-map-marker"></i> {{ activeTerritories.join(', ') }}</p>
<div class="filters-card" *ngIf="usuarios.length">
<label class="filter-label">Usuario</label>
<select class="filter-select" [(ngModel)]="selectedUsuarioId" (ngModelChange)="onUsuarioChange()">
<option value="Todos">Todos los usuarios</option>
<option *ngFor="let u of usuarios" [value]="u.id">{{ u.nombre }} ({{ u.id }})</option>
</select>
<label class="filter-label">Día</label>
<select class="filter-select" [(ngModel)]="selectedDia" (ngModelChange)="onDiaChange()">
<option value="Todos">Todos</option>
<option *ngFor="let d of dias" [value]="d">{{ d }}</option>
</select>
<div class="filter-stats" *ngIf="filteredRoutes.length">
<span>{{ uniqueFilteredRoutes.length }} PDVs</span>
<span>{{ totalDistancia }} km</span>
<span>{{ totalDuracionMin }} min</span>
</div>
<div class="layer-toggles">
<label class="toggle">
<input type="checkbox" [checked]="showHeat" (change)="toggleHeat()" />
<i class="pi pi-fire"></i> Heatmap (carga)
<span class="heatmap-help" title="El mapa muestra con mayor intensidad las zonas donde se concentra más carga de trabajo en horas.">?</span>
</label>
</div>
<div class="filter-empty" *ngIf="!filteredRoutes.length && !error">
Sin rutas para este filtro.
</div>
</div>
<div class="empty-state" *ngIf="!usuarios.length">
<i class="pi pi-inbox"></i>
<p>No hay rutas cargadas.</p>
<button type="button" class="btn-secondary" (click)="goBack()">Ir a Home</button>
</div>
<div class="error-card" *ngIf="error && usuarios.length">
<i class="pi pi-exclamation-triangle"></i>
{{ error }}
</div>
<section class="info-card unassigned-detail-card" *ngIf="selectedUnassigned">
<div class="info-card-header">
<h3>Gestionar PDV sin ruta</h3>
<button type="button" class="btn-icon" (click)="selectedUnassigned = null"><i class="pi pi-times"></i></button>
</div>
<ng-container *ngIf="editingUnassigned; else pendingSummary">
<div class="pending-edit-grid">
<label>Nombre<input [(ngModel)]="editUnassignedName" type="text"></label>
<label>Horas mínimas<input [(ngModel)]="editUnassignedHours" type="number" min="0.01" step="0.25"></label>
<label>Latitud<input [(ngModel)]="editUnassignedLat" type="number" step="any"></label>
<label>Longitud<input [(ngModel)]="editUnassignedLng" type="number" step="any"></label>
<label class="wide-field">Motivo<input [(ngModel)]="editUnassignedReason" type="text"></label>
</div>
<div class="pending-actions"><button type="button" class="btn-secondary" (click)="saveUnassignedEdit()"><i class="pi pi-check"></i> Guardar</button><button type="button" class="btn-secondary" (click)="cancelUnassignedEdit()">Cancelar</button></div>
</ng-container>
<ng-template #pendingSummary>
<dl class="info-list">
<div><dt>PDV</dt><dd>{{ selectedUnassigned.pdv_nombre }}</dd></div>
<div><dt>Motivo</dt><dd>{{ selectedUnassigned.motivo }}</dd></div>
<div><dt>Coordenadas</dt><dd>{{ selectedUnassigned.latitud }}, {{ selectedUnassigned.longitud }}</dd></div>
</dl>
<div class="pending-edit-grid">
<label>Usuario<select [(ngModel)]="assignmentUser[selectedUnassigned.pdv_id]"><option value="">Seleccionar</option><option *ngFor="let user of unassignedUserOptions" [value]="user.id">{{ user.name }}</option></select></label>
<label>Días<div class="day-checkboxes"><label *ngFor="let day of unassignedDayOptions"><input type="checkbox" [checked]="(assignmentDays[selectedUnassigned.pdv_id] || []).includes(day)" (change)="toggleUnassignedDay(selectedUnassigned.pdv_id, day, $any($event.target).checked)">{{ day.substring(0, 3) }}</label></div></label>
</div>
<div class="pending-actions"><button type="button" class="btn-secondary" (click)="startUnassignedEdit(selectedUnassigned)"><i class="pi pi-pencil"></i> Editar</button><button type="button" class="btn-primary" (click)="assignUnassigned(selectedUnassigned)" [disabled]="assigningUnassignedId === selectedUnassigned.pdv_id"><span *ngIf="assigningUnassignedId === selectedUnassigned.pdv_id" class="spinner-inline"></span><i *ngIf="assigningUnassignedId !== selectedUnassigned.pdv_id" class="pi pi-user-plus"></i> Reasignar</button></div>
<div class="employee-load-list" aria-label="Carga de empleados">
<button type="button" class="employee-load-row" *ngFor="let employee of unassignedEmployeeLoads" [class.selected]="assignmentUser[selectedUnassigned.pdv_id] === employee.id" [class.over-capacity]="employeeLoadStatus(employee) === 'over-capacity'" (click)="selectUnassignedEmployee(employee.id)">
<span class="employee-load-name">{{ employee.name }}</span>
<span class="employee-load-values">{{ employee.workHours | number:'1.0-1' }} / {{ employee.capacityHours | number:'1.0-1' }} h · {{ employee.utilization | number:'1.0-1' }}%</span>
<span class="employee-load-bar" [ngClass]="employeeLoadStatus(employee)"><span [style.width.%]="employeeLoadWidth(employee)"></span></span>
</button>
</div>
</ng-template>
<div class="error-card" *ngIf="unassignedActionError">{{ unassignedActionError }}</div>
</section>
<section class="info-card" *ngIf="selectedRouteDetail">
<div class="info-card-header">
<h3>Detalle de la ruta</h3>
<button type="button" class="btn-icon" (click)="clearSelection()" title="Cerrar detalle" aria-label="Cerrar detalle de ruta">
<i class="pi pi-times"></i>
</button>
</div>
<div class="detail-section">
<h4><i class="pi pi-user"></i> Usuario</h4>
<dl class="info-list">
<div><dt>Nombre</dt><dd>{{ selectedRouteDetail.json.nombre_usuario }}</dd></div>
<div><dt>ID</dt><dd>{{ selectedRouteDetail.json.usuario_id }}</dd></div>
<div><dt>Origen</dt><dd>{{ selectedRouteDetail.json.latitud_usuario }}, {{ selectedRouteDetail.json.longitud_usuario }}</dd></div>
<div><dt>Horas disponibles</dt><dd>{{ selectedRouteDetail.json.hora_laboral_semanal_usuario }} h/semana</dd></div>
</dl>
</div>
<div class="detail-section">
<h4><i class="pi pi-map-marker"></i> PDV</h4>
<dl class="info-list">
<div><dt>Nombre</dt><dd>{{ selectedRouteDetail.json.nombre_pdv }}</dd></div>
<div><dt>ID</dt><dd>{{ selectedRouteDetail.json.pdv_id }}</dd></div>
<div><dt>Día</dt><dd>{{ selectedRouteDetail.json.dia }}</dd></div>
<div><dt>Orden de visita</dt><dd>{{ selectedVisitOrder }}</dd></div>
<div><dt>Origen del tramo</dt><dd>{{ selectedSegmentOrigin }}</dd></div>
<div><dt>Destino</dt><dd><button type="button" class="btn-secondary map-location-button" (click)="openDestinationInGoogleMaps()" title="Abrir ubicación en Google Maps"><i class="pi pi-map"></i> Ver en Google Maps</button></dd></div>
<div><dt>Horas trabajo</dt><dd>{{ selectedRouteDetail.json.horas_trabajo }}</dd></div>
<div><dt>Distancia tramo</dt><dd>{{ selectedSegmentDistance | number:'1.2-2' }} km</dd></div>
<div><dt>Duración tramo</dt><dd>{{ selectedSegmentDurationMinutes | number:'1.0-0' }} min</dd></div>
</dl>
<div class="reassign-actions" *ngIf="!showReassignForm">
<button type="button" class="btn-secondary" (click)="moveSelectedVisit(-1)" [disabled]="!canMoveSelectedVisitUp" aria-label="Mover visita anterior"><i class="pi pi-arrow-up"></i> Subir</button>
<button type="button" class="btn-secondary" (click)="moveSelectedVisit(1)" [disabled]="!canMoveSelectedVisitDown" aria-label="Mover visita posterior"><i class="pi pi-arrow-down"></i> Bajar</button>
<button type="button" class="btn-secondary" (click)="resetSelectedDayOrder()"><i class="pi pi-refresh"></i> Orden automático</button>
<button type="button" class="btn-secondary" (click)="startReassignRoute()"><i class="pi pi-user-edit"></i> Reasignar ruta</button>
</div>
<div class="reassign-form" *ngIf="showReassignForm">
<label>Nuevo usuario<select [(ngModel)]="reassignUserId"><option *ngFor="let user of unassignedUserOptions" [value]="user.id">{{ user.name }}</option></select></label>
<label>Día<select [(ngModel)]="reassignDay"><option *ngFor="let day of unassignedDayOptions" [value]="day">{{ day }}</option></select></label>
<p class="reassign-hint">Se recalcularán la distancia y duración con ruta vial ORS.</p>
<div class="reassign-actions"><button type="button" class="btn-primary" (click)="reassignRoute()" [disabled]="reassigningRoute"><span *ngIf="reassigningRoute" class="spinner-inline"></span><i *ngIf="!reassigningRoute" class="pi pi-check"></i> {{ reassigningRoute ? 'Calculando...' : 'Confirmar' }}</button><button type="button" class="btn-secondary" (click)="cancelReassignRoute()" [disabled]="reassigningRoute">Cancelar</button></div>
<div class="employee-load-list" aria-label="Carga de empleados">
<button type="button" class="employee-load-row" *ngFor="let employee of reassignmentEmployeeLoads" [class.selected]="reassignUserId === employee.id" [class.over-capacity]="employeeLoadStatus(employee) === 'over-capacity'" (click)="selectReassignmentEmployee(employee.id)">
<span class="employee-load-name">{{ employee.name }}</span>
<span class="employee-load-values">{{ employee.workHours | number:'1.0-1' }} / {{ employee.capacityHours | number:'1.0-1' }} h · {{ employee.utilization | number:'1.0-1' }}%</span>
<span class="employee-load-bar" [ngClass]="employeeLoadStatus(employee)"><span [style.width.%]="employeeLoadWidth(employee)"></span></span>
</button>
</div>
<div class="error-card" *ngIf="reassignError">{{ reassignError }}</div>
</div>
</div>
</section>
<section class="daily-routes-card" *ngIf="dailySummaries.length">
<h3>Rutas diarias</h3>
<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</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</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>
<section class="unassigned-bottom-drawer" *ngIf="selectedUsuarioId === 'Todos' && selectedDia === 'Todos' && unassignedRoutes.length" [class.open]="isUnassignedDrawerOpen">
<button type="button" class="unassigned-drawer-toggle" (click)="toggleUnassignedDrawer()" [attr.aria-expanded]="isUnassignedDrawerOpen">
<span><i class="pi pi-exclamation-triangle"></i> PDVs sin ruta ({{ unassignedRoutes.length }})</span>
<i class="pi" [class.pi-chevron-up]="!isUnassignedDrawerOpen" [class.pi-chevron-down]="isUnassignedDrawerOpen"></i>
</button>
<div class="unassigned-drawer-content" *ngIf="isUnassignedDrawerOpen">
<button type="button" class="unassigned-map-item" *ngFor="let pending of unassignedRoutes" [class.selected-pending]="selectedUnassigned?.pdv_id === pending.pdv_id" (click)="selectUnassigned(pending)">
<span class="pending-icon">!</span>
<span class="pending-text"><strong>{{ pending.pdv_nombre }}</strong><small>{{ pending.motivo }}</small></span>
</button>
</div>
</section>