Files
consultoria-as 9936deaa90 feat: Implementar PWA, Analytics, Reportes PDF y mejoras OCR
FASE 1 - PWA y Frontend:
- Crear templates/base.html, dashboard.html, analytics.html, executive.html
- Crear static/css/main.css con diseño responsivo
- Agregar static/js/app.js, pwa.js, camera.js, charts.js
- Implementar manifest.json y service-worker.js para PWA
- Soporte para captura de tickets desde cámara móvil

FASE 2 - Analytics:
- Crear módulo analytics/ con predictions.py, trends.py, comparisons.py
- Implementar predicción básica con promedio móvil + tendencia lineal
- Agregar endpoints /api/analytics/trends, predictions, comparisons
- Integrar Chart.js para gráficas interactivas

FASE 3 - Reportes PDF:
- Crear módulo reports/ con pdf_generator.py
- Implementar SalesReportPDF con generar_reporte_diario y ejecutivo
- Agregar comando /reporte [diario|semanal|ejecutivo]
- Agregar endpoints /api/reports/generate y /api/reports/download

FASE 4 - Mejoras OCR:
- Crear módulo ocr/ con processor.py, preprocessor.py, patterns.py
- Implementar AmountDetector con patrones múltiples de montos
- Agregar preprocesador adaptativo con pipelines para diferentes condiciones
- Soporte para corrección de rotación (deskew) y threshold Otsu

Dependencias agregadas:
- reportlab, matplotlib (PDF)
- scipy, pandas (analytics)
- imutils, deskew, cachetools (OCR)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 03:26:16 +00:00

321 lines
11 KiB
HTML

{% extends "base.html" %}
{% block title %}Dashboard Ejecutivo - Sales Bot{% endblock %}
{% block content %}
<header class="page-header">
<div>
<h1><span>Dashboard</span> Ejecutivo</h1>
<p class="fecha" id="fecha-actual"></p>
</div>
<div>
<button class="btn btn-primary" onclick="generarReportePDF()">Descargar PDF</button>
</div>
</header>
<!-- KPIs Principales -->
<div class="stats-grid">
<div class="stat-card">
<div class="label">Ventas Hoy</div>
<div class="value" id="exec-ventas-hoy">-</div>
<div class="subvalue" id="exec-vs-ayer"></div>
</div>
<div class="stat-card green">
<div class="label">Ventas Mes</div>
<div class="value" id="exec-ventas-mes">-</div>
<div class="subvalue" id="exec-vs-mes-ant"></div>
</div>
<div class="stat-card orange">
<div class="label">Comisiones Mes</div>
<div class="value" id="exec-comisiones">-</div>
<div class="subvalue">total equipo</div>
</div>
<div class="stat-card purple">
<div class="label">Meta Cumplida</div>
<div class="value" id="exec-meta-pct">-</div>
<div class="subvalue" id="exec-meta-detalle"></div>
</div>
</div>
<!-- Grafica Principal -->
<div class="panel" style="margin-bottom: 20px;">
<h2><span class="icon">&#128200;</span> Rendimiento Mensual</h2>
<div class="chart-container" style="height: 350px;">
<canvas id="chart-mensual"></canvas>
</div>
</div>
<div class="main-grid">
<!-- Top Performers -->
<div class="panel">
<h2><span class="icon">&#127942;</span> Top Performers del Mes</h2>
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>Vendedor</th>
<th>Tubos</th>
<th>Comision</th>
<th>Racha</th>
</tr>
</thead>
<tbody id="top-performers">
<tr><td colspan="5" class="loading">Cargando...</td></tr>
</tbody>
</table>
</div>
<!-- Metricas Adicionales -->
<div class="panel">
<h2><span class="icon">&#128202;</span> Metricas Clave</h2>
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-value" id="metric-promedio-dia">-</div>
<div class="kpi-label">Promedio/Dia</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="metric-mejor-dia">-</div>
<div class="kpi-label">Mejor Dia</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="metric-vendedores">-</div>
<div class="kpi-label">Vendedores Activos</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="metric-ticket-prom">-</div>
<div class="kpi-label">Ticket Promedio</div>
</div>
</div>
<h3 style="margin: 20px 0 15px;">Prediccion Proxima Semana</h3>
<div id="prediccion-exec" style="background: rgba(0,212,255,0.1); padding: 20px; border-radius: 12px; text-align: center;">
<div style="font-size: 36px; font-weight: bold; color: var(--primary);" id="pred-monto">-</div>
<div style="color: var(--text-secondary); margin-top: 5px;">Monto estimado</div>
<div style="margin-top: 10px;" id="pred-tendencia"></div>
</div>
</div>
</div>
<!-- Comparativa -->
<div class="panel" style="margin-top: 20px;">
<h2><span class="icon">&#128197;</span> Comparativa Mensual</h2>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-comparativa-meses"></canvas>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let chartMensual = null;
let chartComparativa = null;
async function cargarDashboardEjecutivo() {
document.getElementById('fecha-actual').textContent = new Date().toLocaleDateString('es-MX', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
});
try {
// Cargar resumen
const resResumen = await fetch('/api/dashboard/resumen');
const resumen = await resResumen.json();
document.getElementById('exec-ventas-hoy').textContent = formatMoney(resumen.monto_hoy || 0);
document.getElementById('exec-ventas-mes').textContent = formatMoney(resumen.monto_mes || 0);
// Cargar ranking
const resRanking = await fetch('/api/dashboard/ranking');
const ranking = await resRanking.json();
if (ranking && ranking.length > 0) {
renderTopPerformers(ranking);
calcularMetricas(ranking);
}
// Cargar tendencias
const resTrends = await fetch('/api/analytics/trends?days=30');
const trends = await resTrends.json();
if (trends.labels) {
renderChartMensual(trends);
}
// Cargar predicciones
const resPred = await fetch('/api/analytics/predictions?period=30');
const predictions = await resPred.json();
if (predictions) {
document.getElementById('pred-monto').textContent = formatMoney(predictions.next_week || 0);
const trendText = predictions.trend === 'increasing' ? '&#8599; Tendencia al alza' :
predictions.trend === 'decreasing' ? '&#8600; Tendencia a la baja' : '&#8594; Tendencia estable';
const trendColor = predictions.trend === 'increasing' ? 'var(--secondary)' :
predictions.trend === 'decreasing' ? '#ff4444' : 'var(--text-secondary)';
document.getElementById('pred-tendencia').innerHTML = `<span style="color: ${trendColor}">${trendText}</span>`;
}
// Cargar comparativas
const resComp = await fetch('/api/analytics/comparisons?type=monthly');
const comp = await resComp.json();
if (comp) {
renderChartComparativa(comp);
const diff = ((comp.current_month - comp.previous_month) / comp.previous_month * 100).toFixed(1);
const sign = diff > 0 ? '+' : '';
document.getElementById('exec-vs-mes-ant').textContent = `${sign}${diff}% vs mes anterior`;
document.getElementById('exec-vs-mes-ant').style.color = diff > 0 ? 'var(--secondary)' : '#ff4444';
}
} catch (e) {
console.error('Error cargando dashboard ejecutivo:', e);
}
}
function renderTopPerformers(ranking) {
const tbody = document.getElementById('top-performers');
const top5 = ranking.slice(0, 5);
tbody.innerHTML = top5.map((v, i) => {
const medalla = i === 0 ? '&#129351;' : i === 1 ? '&#129352;' : i === 2 ? '&#129353;' : (i + 1);
return `
<tr>
<td>${medalla}</td>
<td><strong>${v.vendedor_username || v.vendedor}</strong></td>
<td>${v.tubos_totales || 0}</td>
<td>${formatMoney(v.comision_total || 0)}</td>
<td>${v.racha || 0} dias</td>
</tr>
`;
}).join('');
// Calcular comisiones totales
const totalComisiones = ranking.reduce((sum, v) => sum + (v.comision_total || 0), 0);
document.getElementById('exec-comisiones').textContent = formatMoney(totalComisiones);
// Calcular meta cumplida
const vendedoresConMeta = ranking.filter(v => (v.tubos_totales || 0) >= 3).length;
const pctMeta = Math.round((vendedoresConMeta / ranking.length) * 100);
document.getElementById('exec-meta-pct').textContent = `${pctMeta}%`;
document.getElementById('exec-meta-detalle').textContent = `${vendedoresConMeta}/${ranking.length} vendedores`;
}
function calcularMetricas(ranking) {
const totalVendedores = ranking.length;
document.getElementById('metric-vendedores').textContent = totalVendedores;
// Estas metricas se calcularian con datos reales
document.getElementById('metric-promedio-dia').textContent = '-';
document.getElementById('metric-mejor-dia').textContent = '-';
document.getElementById('metric-ticket-prom').textContent = '-';
}
function renderChartMensual(data) {
const ctx = document.getElementById('chart-mensual').getContext('2d');
if (chartMensual) chartMensual.destroy();
chartMensual = new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: 'Ventas',
data: data.ventas,
borderColor: '#00d4ff',
backgroundColor: 'rgba(0, 212, 255, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: {
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#888' }
},
y: {
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#888' }
}
}
}
});
}
function renderChartComparativa(data) {
const ctx = document.getElementById('chart-comparativa-meses').getContext('2d');
if (chartComparativa) chartComparativa.destroy();
const meses = data.months || ['Mes -2', 'Mes -1', 'Mes Actual'];
const valores = data.values || [data.month_2 || 0, data.previous_month || 0, data.current_month || 0];
chartComparativa = new Chart(ctx, {
type: 'bar',
data: {
labels: meses,
datasets: [{
label: 'Monto',
data: valores,
backgroundColor: ['rgba(255,255,255,0.1)', 'rgba(255,255,255,0.2)', '#00d4ff']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: {
grid: { display: false },
ticks: { color: '#888' }
},
y: {
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#888' }
}
}
}
});
}
async function generarReportePDF() {
try {
const res = await fetch('/api/reports/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'executive' })
});
const data = await res.json();
if (data.download_url) {
window.open(data.download_url, '_blank');
} else if (data.error) {
alert('Error generando reporte: ' + data.error);
} else {
alert('Reporte generado. Se enviara al canal de Mattermost.');
}
} catch (e) {
console.error('Error generando PDF:', e);
alert('Error generando el reporte PDF');
}
}
function formatMoney(amount) {
return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(amount);
}
// Initialize
cargarDashboardEjecutivo();
</script>
{% endblock %}