feat: implement route planning dashboard including KPIs, user workload tracking, and route assignment management features

This commit is contained in:
2026-08-29 11:55:03 -04:00
parent a0ea3fea48
commit cd5c57c5ee
41 changed files with 3811 additions and 151 deletions
@@ -12,13 +12,13 @@
<section class="card upload-card">
<h2 class="section-title">Subir archivos para optimización</h2>
<div class="upload-grid">
<div class="file-input-wrapper">
<label class="file-label" for="pdvFile">
<label class="file-label" for="pdvFile" [class.has-error]="pdvPreview && !pdvPreview.valid" [class.has-success]="pdvPreview?.valid">
<i class="pi pi-file-excel"></i>
<span>Archivo PDVs (Excel)</span>
<span class="file-hint">.xlsx / .xls • máx 10MB</span>
<span class="file-hint">.xlsx / .xls • máx 10MB • máx 2000 filas</span>
</label>
<input
id="pdvFile"
@@ -29,16 +29,39 @@
[class.has-file]="pdvFile"
/>
<div class="file-name" *ngIf="pdvFile">
<i class="pi pi-check-circle"></i>
<i class="pi" [class.pi-check-circle]="pdvPreview?.valid" [class.pi-exclamation-circle]="pdvPreview && !pdvPreview.valid" [class.pi-spin]="pdvPreviewLoading"></i>
{{ pdvFile.name }}
<span *ngIf="pdvPreviewLoading" class="preview-loading">Validando...</span>
</div>
<div class="preview-card" *ngIf="pdvPreviewLoading">
<div class="preview-loading-row"><span class="spinner-inline"></span> Analizando archivo PDV...</div>
</div>
<div class="preview-card preview-success" *ngIf="pdvPreview?.valid">
<div class="preview-header"><i class="pi pi-check-circle"></i> PDV válido — {{ pdvPreview!.totalRows }} filas</div>
<div class="preview-stats">{{ pdvPreview!.validRows }} filas válidas • Columnas: {{ pdvPreview!.headers.join(', ') }}</div>
</div>
<div class="preview-card preview-error" *ngIf="pdvPreview && !pdvPreview.valid">
<div class="preview-header"><i class="pi pi-exclamation-triangle"></i> Errores en PDV ({{ pdvPreview.errors.length }})</div>
<div class="preview-stats" *ngIf="pdvPreview.missingHeaders.length">Faltan: {{ pdvPreview.missingHeaders.join(', ') }}</div>
<div class="preview-stats">{{ pdvPreview.totalRows }} filas leídas • {{ pdvPreview.validRows }} válidas</div>
<ul class="preview-errors">
<li *ngFor="let e of pdvPreview.errors.slice(0, 8)">
<strong>Fila {{ e.row }} — {{ e.field }}:</strong> {{ e.message }}
<span *ngIf="e.value !== undefined && e.value !== null" class="error-value">({{ e.value }})</span>
</li>
<li *ngIf="pdvPreview.errors.length > 8" class="more-errors">+ {{ pdvPreview.errors.length - 8 }} errores más...</li>
</ul>
</div>
</div>
<div class="file-input-wrapper">
<label class="file-label" for="usuariosFile">
<label class="file-label" for="usuariosFile" [class.has-error]="usuariosPreview && !usuariosPreview.valid" [class.has-success]="usuariosPreview?.valid">
<i class="pi pi-file-excel"></i>
<span>Archivo Usuarios (Excel)</span>
<span class="file-hint">.xlsx / .xls • máx 10MB</span>
<span class="file-hint">.xlsx / .xls • máx 10MB • máx 50 filas</span>
</label>
<input
id="usuariosFile"
@@ -49,8 +72,56 @@
[class.has-file]="usuariosFile"
/>
<div class="file-name" *ngIf="usuariosFile">
<i class="pi pi-check-circle"></i>
<i class="pi" [class.pi-check-circle]="usuariosPreview?.valid" [class.pi-exclamation-circle]="usuariosPreview && !usuariosPreview.valid"></i>
{{ usuariosFile.name }}
<span *ngIf="usuariosPreviewLoading" class="preview-loading">Validando...</span>
</div>
<div class="preview-card" *ngIf="usuariosPreviewLoading">
<div class="preview-loading-row"><span class="spinner-inline"></span> Analizando archivo Usuarios...</div>
</div>
<div class="preview-card preview-success" *ngIf="usuariosPreview?.valid">
<div class="preview-header"><i class="pi pi-check-circle"></i> Usuarios válido — {{ usuariosPreview!.totalRows }} filas</div>
<div class="preview-stats">{{ usuariosPreview!.validRows }} filas válidas • Columnas: {{ usuariosPreview!.headers.join(', ') }}</div>
</div>
<div class="preview-card preview-error" *ngIf="usuariosPreview && !usuariosPreview.valid">
<div class="preview-header"><i class="pi pi-exclamation-triangle"></i> Errores en Usuarios ({{ usuariosPreview.errors.length }})</div>
<div class="preview-stats" *ngIf="usuariosPreview.missingHeaders.length">Faltan: {{ usuariosPreview.missingHeaders.join(', ') }}</div>
<div class="preview-stats">{{ usuariosPreview.totalRows }} filas leídas • {{ usuariosPreview.validRows }} válidas</div>
<ul class="preview-errors">
<li *ngFor="let e of usuariosPreview.errors.slice(0, 8)">
<strong>Fila {{ e.row }} — {{ e.field }}:</strong> {{ e.message }}
<span *ngIf="e.value !== undefined && e.value !== null" class="error-value">({{ e.value }})</span>
</li>
<li *ngIf="usuariosPreview.errors.length > 8" class="more-errors">+ {{ usuariosPreview.errors.length - 8 }} errores más...</li>
</ul>
</div>
</div>
</div>
<div class="params-card">
<h3 class="params-title"><i class="pi pi-sliders-h"></i> Parámetros de optimización</h3>
<div class="params-grid">
<div class="param-field">
<label class="param-label" for="radioKm">Radio máximo por PDV</label>
<select id="radioKm" class="param-select" [(ngModel)]="radioKm" (ngModelChange)="onRadioChange()">
<option [ngValue]="30">30 km — Urbano denso</option>
<option [ngValue]="50">50 km — Urbano (default)</option>
<option [ngValue]="80">80 km — Rural disperso</option>
</select>
<small class="param-hint">PDV sin usuario dentro del radio queda sin asignar</small>
</div>
<div class="param-field">
<label class="param-label" for="velocidad">Velocidad promedio</label>
<select id="velocidad" class="param-select" [(ngModel)]="velocidad" (ngModelChange)="onVelocidadChange()">
<option [ngValue]="22">22 km/h — Centro / tráfico denso</option>
<option [ngValue]="28">28 km/h — Urbano (default)</option>
<option [ngValue]="35">35 km/h — Mixto</option>
<option [ngValue]="46">46 km/h — Ruta rápida</option>
</select>
<small class="param-hint">Usado si ORS vial no disponible. En ciudad 22-28 es más real que 46</small>
</div>
</div>
</div>
@@ -65,12 +136,21 @@
type="button"
class="btn-primary"
(click)="cargarRutas()"
[disabled]="!canSubmit || isLoading"
[disabled]="!canSubmit"
[title]="submitDisabledReason || ''"
>
<span *ngIf="isLoading" class="spinner-inline"></span>
<i *ngIf="!isLoading" class="pi pi-upload"></i>
{{ isLoading ? 'Procesando...' : 'Cargar rutas' }}
</button>
<button
type="button"
class="btn-secondary"
*ngIf="isLoading"
(click)="cancelarCarga()"
>
<i class="pi pi-times"></i> Cancelar
</button>
<button
type="button"
class="btn-secondary"
@@ -80,9 +160,14 @@
<i class="pi pi-times"></i> Limpiar
</button>
</div>
<small class="hint-text" *ngIf="submitDisabledReason && !isLoading">{{ submitDisabledReason }}</small>
</div>
</section>
<section class="card kpis-card">
<app-planning-kpis></app-planning-kpis>
</section>
<section class="card">
<app-assigned-route></app-assigned-route>
</section>
@@ -91,4 +176,4 @@
<h2 class="section-title">PDVs sin ruta asignada</h2>
<app-no-assigned-route></app-no-assigned-route>
</section>
</div>
</div>