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>
218 lines
6.0 KiB
JavaScript
218 lines
6.0 KiB
JavaScript
/**
|
|
* 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');
|
|
}
|
|
})
|
|
);
|
|
});
|