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:
238
sales-bot/static/js/app.js
Normal file
238
sales-bot/static/js/app.js
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Sales Bot - Main Application JavaScript
|
||||
*/
|
||||
|
||||
// Utility functions
|
||||
const Utils = {
|
||||
formatMoney(amount) {
|
||||
return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(amount || 0);
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('es-MX', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' });
|
||||
},
|
||||
|
||||
formatDateTime(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
return `${this.formatDate(dateStr)} ${this.formatTime(dateStr)}`;
|
||||
},
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
// Create notification element
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 25px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.3s ease;
|
||||
background: ${type === 'success' ? '#00ff88' : type === 'error' ? '#ff4444' : '#00d4ff'};
|
||||
color: ${type === 'success' || type === 'info' ? '#000' : '#fff'};
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// API Client
|
||||
const API = {
|
||||
async get(endpoint) {
|
||||
try {
|
||||
const response = await fetch(endpoint);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`API GET ${endpoint}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async post(endpoint, data) {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`API POST ${endpoint}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Dashboard module
|
||||
const Dashboard = {
|
||||
async loadSummary() {
|
||||
try {
|
||||
const data = await API.get('/api/dashboard/resumen');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error loading summary:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async loadRanking() {
|
||||
try {
|
||||
const data = await API.get('/api/dashboard/ranking');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error loading ranking:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadRecentSales() {
|
||||
try {
|
||||
const data = await API.get('/api/dashboard/ventas-recientes');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error loading recent sales:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Analytics module
|
||||
const Analytics = {
|
||||
async loadTrends(days = 30) {
|
||||
try {
|
||||
const data = await API.get(`/api/analytics/trends?days=${days}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error loading trends:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async loadPredictions(period = 30) {
|
||||
try {
|
||||
const data = await API.get(`/api/analytics/predictions?period=${period}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error loading predictions:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async loadComparisons(type = 'monthly') {
|
||||
try {
|
||||
const data = await API.get(`/api/analytics/comparisons?type=${type}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error loading comparisons:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Offline support
|
||||
const OfflineManager = {
|
||||
isOnline: navigator.onLine,
|
||||
|
||||
init() {
|
||||
window.addEventListener('online', () => {
|
||||
this.isOnline = true;
|
||||
Utils.showNotification('Conexion restaurada', 'success');
|
||||
this.syncData();
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.isOnline = false;
|
||||
Utils.showNotification('Sin conexion - Modo offline', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
async cacheData(key, data) {
|
||||
try {
|
||||
localStorage.setItem(`salesbot_${key}`, JSON.stringify({
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Error caching data:', e);
|
||||
}
|
||||
},
|
||||
|
||||
getCachedData(key, maxAge = 300000) { // 5 minutes default
|
||||
try {
|
||||
const cached = localStorage.getItem(`salesbot_${key}`);
|
||||
if (!cached) return null;
|
||||
|
||||
const { data, timestamp } = JSON.parse(cached);
|
||||
if (Date.now() - timestamp > maxAge) return null;
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
syncData() {
|
||||
// Sync any pending data when back online
|
||||
console.log('Syncing data...');
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
OfflineManager.init();
|
||||
});
|
||||
|
||||
// Add CSS animations
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Export for use in templates
|
||||
window.Utils = Utils;
|
||||
window.API = API;
|
||||
window.Dashboard = Dashboard;
|
||||
window.Analytics = Analytics;
|
||||
Reference in New Issue
Block a user