178 lines
11 KiB
HTML
178 lines
11 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>
|
|
|
|
<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>{{ filteredRoutes.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)
|
|
</label>
|
|
<div class="heat-opts" *ngIf="showHeat">
|
|
<label><input type="radio" name="hw" value="uniform" [checked]="heatWeightMode==='uniform'" (change)="setHeatWeight('uniform')" /> Peso 1 (densidad)</label>
|
|
<label><input type="radio" name="hw" value="horas_trabajo" [checked]="heatWeightMode==='horas_trabajo'" (change)="setHeatWeight('horas_trabajo')" /> Por carga (horas)</label>
|
|
</div>
|
|
</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="unassigned-map-card" *ngIf="selectedUsuarioId === 'Todos' && selectedDia === 'Todos' && unassignedRoutes.length">
|
|
<div class="info-card-header">
|
|
<h3>PDVs sin ruta ({{ unassignedRoutes.length }})</h3>
|
|
</div>
|
|
<div class="unassigned-map-list">
|
|
<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>
|
|
|
|
<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>
|
|
</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>{{ selectedRouteDetail.json.latitud_pdv }}, {{ selectedRouteDetail.json.longitud_pdv }}</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="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" *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>
|
|
</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>
|