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>
This commit is contained in:
183
sales-bot/templates/dashboard.html
Normal file
183
sales-bot/templates/dashboard.html
Normal file
@@ -0,0 +1,183 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Sales Bot{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1><span>Sales</span> Bot Dashboard</h1>
|
||||
<p class="fecha" id="fecha-actual"></p>
|
||||
</div>
|
||||
<button class="refresh-btn" onclick="cargarDatos()">Actualizar</button>
|
||||
</header>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="label">Ventas Hoy</div>
|
||||
<div class="value" id="ventas-hoy">-</div>
|
||||
<div class="subvalue" id="monto-hoy">$0.00</div>
|
||||
</div>
|
||||
<div class="stat-card green">
|
||||
<div class="label">Ventas del Mes</div>
|
||||
<div class="value" id="ventas-mes">-</div>
|
||||
<div class="subvalue" id="monto-mes">$0.00</div>
|
||||
</div>
|
||||
<div class="stat-card orange">
|
||||
<div class="label">Vendedores Activos Hoy</div>
|
||||
<div class="value" id="vendedores-activos">-</div>
|
||||
</div>
|
||||
<div class="stat-card purple">
|
||||
<div class="label">Meta Diaria</div>
|
||||
<div class="value">3</div>
|
||||
<div class="subvalue">tubos por vendedor</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-grid">
|
||||
<div class="panel">
|
||||
<h2><span class="icon">🏆</span> Ranking del Mes (Tubos)</h2>
|
||||
<ul class="ranking-list" id="ranking-list">
|
||||
<li class="loading"><div class="loading-spinner"></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2><span class="icon">📋</span> Ventas Recientes</h2>
|
||||
<div class="ventas-list" id="ventas-list">
|
||||
<div class="loading"><div class="loading-spinner"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Camera Button for Mobile -->
|
||||
<button class="btn btn-primary" id="btn-camera" onclick="abrirCamara()" style="position: fixed; bottom: 20px; right: 20px; border-radius: 50%; width: 60px; height: 60px; font-size: 24px; display: none;">
|
||||
📷
|
||||
</button>
|
||||
|
||||
<!-- Camera Modal -->
|
||||
<div class="camera-modal" id="camera-modal">
|
||||
<div class="camera-container">
|
||||
<video id="camera-video" autoplay playsinline></video>
|
||||
<div class="camera-controls">
|
||||
<button class="camera-btn close" onclick="cerrarCamara()">✕</button>
|
||||
<button class="camera-btn capture" onclick="capturarFoto()">📷</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="camera-canvas" style="display: none;"></canvas>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Helper functions
|
||||
function formatMoney(amount) {
|
||||
return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(amount);
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
// Data loading functions
|
||||
async function cargarResumen() {
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/resumen');
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('ventas-hoy').textContent = data.ventas_hoy || 0;
|
||||
document.getElementById('monto-hoy').textContent = formatMoney(data.monto_hoy || 0);
|
||||
document.getElementById('ventas-mes').textContent = data.ventas_mes || 0;
|
||||
document.getElementById('monto-mes').textContent = formatMoney(data.monto_mes || 0);
|
||||
document.getElementById('vendedores-activos').textContent = data.vendedores_activos_hoy || 0;
|
||||
document.getElementById('fecha-actual').textContent = new Date().toLocaleDateString('es-MX', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error cargando resumen:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function cargarRanking() {
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/ranking');
|
||||
const data = await res.json();
|
||||
const lista = document.getElementById('ranking-list');
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
lista.innerHTML = '<li class="loading">No hay datos de ventas</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
lista.innerHTML = data.slice(0, 10).map((v, i) => {
|
||||
const posClass = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : 'default';
|
||||
const tubos = v.tubos_totales || 0;
|
||||
const comision = v.comision_total || 0;
|
||||
const ventas = v.cantidad_ventas || 0;
|
||||
const nombre = v.nombre_completo || v.vendedor_username || v.vendedor;
|
||||
const username = v.vendedor_username || v.vendedor;
|
||||
|
||||
return `
|
||||
<li class="ranking-item">
|
||||
<div class="ranking-position ${posClass}">${i + 1}</div>
|
||||
<div class="ranking-info">
|
||||
<div class="ranking-name">${nombre}</div>
|
||||
<div class="ranking-stats">@${username} - ${ventas} ventas - ${v.dias_activos || 0} dias activos</div>
|
||||
</div>
|
||||
<div class="ranking-value">
|
||||
<div class="ranking-tubos">${tubos}</div>
|
||||
${comision > 0 ? `<div class="ranking-comision">+${formatMoney(comision)}</div>` : ''}
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
console.error('Error cargando ranking:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function cargarVentasRecientes() {
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/ventas-recientes');
|
||||
const data = await res.json();
|
||||
const lista = document.getElementById('ventas-list');
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
lista.innerHTML = '<div class="loading">No hay ventas hoy</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
lista.innerHTML = data.map(v => {
|
||||
const nombre = v.nombre_completo || v.vendedor_username;
|
||||
return `
|
||||
<div class="venta-item">
|
||||
<div class="venta-info">
|
||||
<div class="vendedor">${nombre}</div>
|
||||
<div class="cliente">${v.cliente || 'Sin cliente'} - ${formatDate(v.fecha_venta)}</div>
|
||||
</div>
|
||||
<div class="venta-monto">${formatMoney(v.monto || 0)}</div>
|
||||
</div>
|
||||
`}).join('');
|
||||
} catch (e) {
|
||||
console.error('Error cargando ventas:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function cargarDatos() {
|
||||
cargarResumen();
|
||||
cargarRanking();
|
||||
cargarVentasRecientes();
|
||||
}
|
||||
|
||||
// Initialize
|
||||
cargarDatos();
|
||||
setInterval(cargarDatos, 30000);
|
||||
|
||||
// Show camera button on mobile
|
||||
if ('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices) {
|
||||
document.getElementById('btn-camera').style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
<script src="/static/js/camera.js"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user