Files
sales-bot-stacks/sales-bot/templates/analytics.html
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 %}Analytics - Sales Bot{% endblock %}
{% block content %}
<header class="page-header">
<div>
<h1><span>Analytics</span> Dashboard</h1>
<p class="fecha" id="fecha-actual"></p>
</div>
<div>
<select id="periodo-select" class="btn btn-secondary" onchange="cambiarPeriodo()">
<option value="7">Ultimos 7 dias</option>
<option value="30" selected>Ultimos 30 dias</option>
<option value="90">Ultimos 90 dias</option>
</select>
</div>
</header>
<!-- KPIs Row -->
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-value" id="kpi-total-ventas">-</div>
<div class="kpi-label">Total Ventas</div>
<div class="kpi-trend" id="kpi-trend-ventas"></div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-monto-total">-</div>
<div class="kpi-label">Monto Total</div>
<div class="kpi-trend" id="kpi-trend-monto"></div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-tubos">-</div>
<div class="kpi-label">Tubos Vendidos</div>
<div class="kpi-trend" id="kpi-trend-tubos"></div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-promedio">-</div>
<div class="kpi-label">Promedio/Dia</div>
<div class="kpi-trend" id="kpi-trend-promedio"></div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpi-prediccion">-</div>
<div class="kpi-label">Prediccion Prox. Semana</div>
<div class="kpi-trend" id="kpi-trend-prediccion"></div>
</div>
</div>
<!-- Charts -->
<div class="main-grid">
<div class="panel" style="grid-column: span 2;">
<h2><span class="icon">&#128200;</span> Tendencia de Ventas</h2>
<div class="chart-container">
<canvas id="chart-tendencia"></canvas>
</div>
</div>
</div>
<div class="main-grid" style="margin-top: 20px;">
<div class="panel">
<h2><span class="icon">&#128202;</span> Ventas por Vendedor</h2>
<div class="chart-container">
<canvas id="chart-vendedores"></canvas>
</div>
</div>
<div class="panel">
<h2><span class="icon">&#128197;</span> Comparativa Semanal</h2>
<div class="chart-container">
<canvas id="chart-comparativa"></canvas>
</div>
</div>
</div>
<!-- Prediction Info -->
<div class="panel" style="margin-top: 20px;">
<h2><span class="icon">&#129302;</span> Prediccion de Ventas</h2>
<div class="main-grid">
<div>
<p style="color: var(--text-secondary); margin-bottom: 15px;">
Basado en el promedio movil y tendencia lineal de los ultimos <span id="pred-dias">30</span> dias:
</p>
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-value" id="pred-tomorrow">-</div>
<div class="kpi-label">Manana</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="pred-week">-</div>
<div class="kpi-label">Proxima Semana</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="pred-confidence">-</div>
<div class="kpi-label">Confianza</div>
</div>
</div>
</div>
<div>
<h3 style="margin-bottom: 10px;">Tendencia Detectada</h3>
<div id="pred-trend-info" style="font-size: 48px; text-align: center; padding: 20px;">
-
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="/static/js/charts.js"></script>
<script>
let chartTendencia = null;
let chartVendedores = null;
let chartComparativa = null;
async function cargarAnalytics() {
const dias = document.getElementById('periodo-select').value;
document.getElementById('pred-dias').textContent = dias;
try {
// Load trends
const resTrends = await fetch(`/api/analytics/trends?days=${dias}`);
const trends = await resTrends.json();
if (trends.labels && trends.ventas) {
renderTrendChart(trends);
updateKPIs(trends);
}
// Load predictions
const resPred = await fetch(`/api/analytics/predictions?period=${dias}`);
const predictions = await resPred.json();
if (predictions) {
document.getElementById('pred-tomorrow').textContent = formatMoney(predictions.next_day || 0);
document.getElementById('pred-week').textContent = formatMoney(predictions.next_week || 0);
document.getElementById('pred-confidence').textContent = Math.round((predictions.confidence || 0) * 100) + '%';
document.getElementById('kpi-prediccion').textContent = formatMoney(predictions.next_week || 0);
const trendIcon = predictions.trend === 'increasing' ? '&#8599; Subiendo' :
predictions.trend === 'decreasing' ? '&#8600; Bajando' : '&#8594; Estable';
const trendColor = predictions.trend === 'increasing' ? 'var(--secondary)' :
predictions.trend === 'decreasing' ? '#ff4444' : 'var(--text-secondary)';
document.getElementById('pred-trend-info').innerHTML = `<span style="color: ${trendColor}">${trendIcon}</span>`;
const trendClass = predictions.trend === 'increasing' ? 'up' :
predictions.trend === 'decreasing' ? 'down' : 'stable';
document.getElementById('kpi-trend-prediccion').className = `kpi-trend ${trendClass}`;
document.getElementById('kpi-trend-prediccion').textContent = predictions.trend === 'increasing' ? '+' : '-';
}
// Load vendor comparison
const resRanking = await fetch('/api/dashboard/ranking');
const ranking = await resRanking.json();
if (ranking && ranking.length > 0) {
renderVendedoresChart(ranking);
}
// Load weekly comparison
const resComp = await fetch('/api/analytics/comparisons?type=weekly');
const comparisons = await resComp.json();
if (comparisons) {
renderComparativaChart(comparisons);
}
} catch (e) {
console.error('Error cargando analytics:', e);
}
document.getElementById('fecha-actual').textContent = new Date().toLocaleDateString('es-MX', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
});
}
function updateKPIs(trends) {
const totalVentas = trends.ventas.reduce((a, b) => a + b, 0);
const promedio = totalVentas / trends.ventas.length;
document.getElementById('kpi-total-ventas').textContent = trends.ventas.length;
document.getElementById('kpi-monto-total').textContent = formatMoney(totalVentas);
document.getElementById('kpi-tubos').textContent = trends.tubos?.reduce((a, b) => a + b, 0) || '-';
document.getElementById('kpi-promedio').textContent = formatMoney(promedio);
}
function renderTrendChart(data) {
const ctx = document.getElementById('chart-tendencia').getContext('2d');
if (chartTendencia) chartTendencia.destroy();
chartTendencia = 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
},
{
label: 'Prediccion',
data: data.prediccion || [],
borderColor: '#00ff88',
borderDash: [5, 5],
fill: false,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: { color: '#888' }
}
},
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 renderVendedoresChart(ranking) {
const ctx = document.getElementById('chart-vendedores').getContext('2d');
if (chartVendedores) chartVendedores.destroy();
const top5 = ranking.slice(0, 5);
chartVendedores = new Chart(ctx, {
type: 'bar',
data: {
labels: top5.map(v => v.vendedor_username || v.vendedor),
datasets: [{
label: 'Tubos',
data: top5.map(v => v.tubos_totales || 0),
backgroundColor: ['#ffd700', '#c0c0c0', '#cd7f32', '#00d4ff', '#00ff88']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
plugins: {
legend: { display: false }
},
scales: {
x: {
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#888' }
},
y: {
grid: { display: false },
ticks: { color: '#fff' }
}
}
}
});
}
function renderComparativaChart(data) {
const ctx = document.getElementById('chart-comparativa').getContext('2d');
if (chartComparativa) chartComparativa.destroy();
chartComparativa = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Semana Anterior', 'Esta Semana'],
datasets: [{
label: 'Monto',
data: [data.previous_week || 0, data.current_week || 0],
backgroundColor: ['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' }
}
}
}
});
}
function cambiarPeriodo() {
cargarAnalytics();
}
function formatMoney(amount) {
return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(amount);
}
// Initialize
cargarAnalytics();
</script>
{% endblock %}