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:
217
sales-bot/static/service-worker.js
Normal file
217
sales-bot/static/service-worker.js
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Sales Bot - Service Worker for PWA Offline Support
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'salesbot-v1';
|
||||
const RUNTIME_CACHE = 'salesbot-runtime-v1';
|
||||
|
||||
// Assets to cache on install
|
||||
const PRECACHE_ASSETS = [
|
||||
'/dashboard',
|
||||
'/dashboard/analytics',
|
||||
'/dashboard/executive',
|
||||
'/static/css/main.css',
|
||||
'/static/js/app.js',
|
||||
'/static/js/pwa.js',
|
||||
'/static/js/camera.js',
|
||||
'/static/js/charts.js',
|
||||
'/static/manifest.json'
|
||||
];
|
||||
|
||||
// API endpoints to cache with network-first strategy
|
||||
const API_ROUTES = [
|
||||
'/api/dashboard/resumen',
|
||||
'/api/dashboard/ranking',
|
||||
'/api/dashboard/ventas-recientes',
|
||||
'/api/analytics/trends',
|
||||
'/api/analytics/predictions'
|
||||
];
|
||||
|
||||
// Install event - cache core assets
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('[SW] Precaching assets');
|
||||
return cache.addAll(PRECACHE_ASSETS);
|
||||
})
|
||||
.then(() => self.skipWaiting())
|
||||
.catch(err => console.error('[SW] Precache failed:', err))
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter(name => name !== CACHE_NAME && name !== RUNTIME_CACHE)
|
||||
.map(name => {
|
||||
console.log('[SW] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - handle requests
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip external requests
|
||||
if (url.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API requests - Network first, fall back to cache
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(networkFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets - Cache first, fall back to network
|
||||
if (url.pathname.startsWith('/static/')) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML pages - Network first with cache fallback
|
||||
if (request.headers.get('Accept')?.includes('text/html')) {
|
||||
event.respondWith(networkFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Default - Network first
|
||||
event.respondWith(networkFirst(request));
|
||||
});
|
||||
|
||||
// Cache first strategy
|
||||
async function cacheFirst(request) {
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
if (networkResponse.ok) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('[SW] Cache first failed:', error);
|
||||
return new Response('Offline', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
// Network first strategy
|
||||
async function networkFirst(request) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (networkResponse.ok) {
|
||||
const cache = await caches.open(RUNTIME_CACHE);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
// Network failed, try cache
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
console.log('[SW] Serving from cache:', request.url);
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// No cache available
|
||||
if (request.headers.get('Accept')?.includes('text/html')) {
|
||||
return caches.match('/dashboard'); // Fallback to main page
|
||||
}
|
||||
|
||||
if (request.headers.get('Accept')?.includes('application/json')) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'offline',
|
||||
message: 'Sin conexion. Mostrando datos en cache.'
|
||||
}), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('Offline', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
// Handle messages from clients
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
// Background sync for offline actions (if supported)
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'sync-sales') {
|
||||
event.waitUntil(syncSales());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncSales() {
|
||||
// Sync any pending sales when back online
|
||||
console.log('[SW] Syncing pending sales...');
|
||||
// Implementation would go here
|
||||
}
|
||||
|
||||
// Push notifications (if implemented)
|
||||
self.addEventListener('push', (event) => {
|
||||
if (!event.data) return;
|
||||
|
||||
const data = event.data.json();
|
||||
|
||||
const options = {
|
||||
body: data.body || 'Nueva notificacion de Sales Bot',
|
||||
icon: '/static/icons/icon-192x192.png',
|
||||
badge: '/static/icons/icon-72x72.png',
|
||||
vibrate: [100, 50, 100],
|
||||
data: {
|
||||
url: data.url || '/dashboard'
|
||||
}
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title || 'Sales Bot', options)
|
||||
);
|
||||
});
|
||||
|
||||
// Handle notification click
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window' }).then(clientList => {
|
||||
// Focus existing window if available
|
||||
for (const client of clientList) {
|
||||
if (client.url.includes('/dashboard') && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Open new window
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(event.notification.data?.url || '/dashboard');
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user