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:
320
sales-bot/templates/analytics.html
Normal file
320
sales-bot/templates/analytics.html
Normal file
@@ -0,0 +1,320 @@
|
||||
{% 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">📈</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">📊</span> Ventas por Vendedor</h2>
|
||||
<div class="chart-container">
|
||||
<canvas id="chart-vendedores"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2><span class="icon">📅</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">🤖</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' ? '↗ Subiendo' :
|
||||
predictions.trend === 'decreasing' ? '↘ Bajando' : '→ 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 %}
|
||||
55
sales-bot/templates/base.html
Normal file
55
sales-bot/templates/base.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="theme-color" content="#00d4ff">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Sales Bot">
|
||||
<meta name="description" content="Sales Bot - Sistema de seguimiento de ventas">
|
||||
|
||||
<title>{% block title %}Sales Bot{% endblock %}</title>
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/static/manifest.json">
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/static/icons/icon-192x192.png">
|
||||
<link rel="apple-touch-icon" href="/static/icons/icon-192x192.png">
|
||||
|
||||
<!-- Chart.js CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="nav-brand">
|
||||
<span class="brand-icon">S</span>
|
||||
<span class="brand-text">Sales Bot</span>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="/dashboard" class="nav-link {% if active_page == 'dashboard' %}active{% endif %}">Dashboard</a>
|
||||
<a href="/dashboard/analytics" class="nav-link {% if active_page == 'analytics' %}active{% endif %}">Analytics</a>
|
||||
<a href="/dashboard/executive" class="nav-link {% if active_page == 'executive' %}active{% endif %}">Ejecutivo</a>
|
||||
</div>
|
||||
<div class="nav-actions">
|
||||
<button class="refresh-btn" onclick="window.location.reload()">Actualizar</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script src="/static/js/pwa.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
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 %}
|
||||
320
sales-bot/templates/executive.html
Normal file
320
sales-bot/templates/executive.html
Normal file
@@ -0,0 +1,320 @@
|
||||
{% 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">📈</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">🏆</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">📊</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">📅</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' ? '↗ Tendencia al alza' :
|
||||
predictions.trend === 'decreasing' ? '↘ Tendencia a la baja' : '→ 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 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : (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 %}
|
||||
Reference in New Issue
Block a user