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>
184 lines
6.5 KiB
HTML
184 lines
6.5 KiB
HTML
{% 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 %}
|