Files
dzinesco aa81eb5adb feat: add advanced analytics and territory management system
- Add comprehensive analytics components with export functionality
- Implement territory management with manager performance tracking
- Add seatmap components for venue layout management
- Create customer management features with modal interface
- Add advanced hooks for dashboard flags and territory data
- Implement seat selection and venue management utilities
- Add type definitions for ticketing and seatmap systems

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 09:25:10 -06:00

285 lines
7.8 KiB
JavaScript

/**
* Service Worker for Offline-First Scanner PWA
* Handles background sync and caching for offline functionality
*/
const CACHE_NAME = 'bct-scanner-v5';
const STATIC_CACHE = 'bct-static-v5';
// Files to cache for offline access - only static assets, no HTML pages
const STATIC_FILES = [
'/manifest.json',
'/vite.svg',
// Add core assets as needed, but NOT HTML pages to avoid auth conflicts
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('Service Worker installing...');
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => {
console.log('Caching static files');
return cache.addAll(STATIC_FILES);
})
.then(() => {
console.log('Service Worker installed successfully');
return self.skipWaiting();
})
.catch(error => {
console.error('Service Worker installation failed:', error);
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker activating...');
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME && cacheName !== STATIC_CACHE) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('Service Worker activated');
return self.clients.claim();
})
);
});
// Fetch event - serve from cache when offline
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Handle navigation requests - use network-first to avoid auth conflicts
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then(response => {
// Always use network response for HTML to ensure fresh auth state
return response;
})
.catch(() => {
// Only serve offline fallback if network completely fails
// Return a minimal offline page instead of cached routes
return new Response(`
<!DOCTYPE html>
<html>
<head>
<title>Offline - Black Canyon Tickets</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: system-ui;
text-align: center;
padding: 2rem;
background: #0f0f23;
color: white;
}
.card {
background: rgba(255,255,255,0.1);
padding: 2rem;
border-radius: 8px;
margin: 2rem auto;
max-width: 400px;
}
</style>
</head>
<body>
<div class="card">
<h1>You're Offline</h1>
<p>Please check your internet connection and try again.</p>
<button onclick="window.location.reload()">Retry</button>
</div>
</body>
</html>
`, {
status: 200,
headers: { 'Content-Type': 'text/html' }
});
})
);
return;
}
// Handle API requests
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request)
.catch(() => {
// API offline - return a meaningful response
return new Response(
JSON.stringify({
error: 'Offline',
message: 'API request queued for when connection is restored'
}),
{
status: 503,
headers: { 'Content-Type': 'application/json' }
}
);
})
);
return;
}
// Handle static assets
event.respondWith(
caches.match(request)
.then(response => {
if (response) {
return response;
}
return fetch(request)
.then(response => {
// Don't cache non-successful responses
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response for caching
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(request, responseToCache);
})
.catch(error => {
console.warn('Failed to cache resource:', error);
});
return response;
})
.catch(error => {
console.warn('Failed to fetch resource:', request.url, error);
// Return a basic 404 response for failed requests
return new Response('Not found', { status: 404 });
});
})
);
});
// Background Sync for scan queue
self.addEventListener('sync', (event) => {
console.log('Background sync triggered:', event.tag);
if (event.tag === 'sync-scans') {
event.waitUntil(syncPendingScans());
}
});
// Sync pending scans from IndexedDB
async function syncPendingScans() {
try {
console.log('Syncing pending scans...');
// This would normally interface with IndexedDB
// For now, we'll just log the sync attempt
// In a real implementation:
// 1. Open IndexedDB connection
// 2. Get all pending scans
// 3. Send each to the verify API
// 4. Update scan records with results
// 5. Handle conflicts and errors
console.log('Scan sync completed');
// Notify clients of sync completion
const clients = await self.clients.matchAll();
clients.forEach(client => {
client.postMessage({
type: 'SYNC_COMPLETE',
timestamp: Date.now()
});
});
} catch (error) {
console.error('Background sync failed:', error);
// Schedule retry with exponential backoff
setTimeout(() => {
self.registration.sync.register('sync-scans');
}, getBackoffDelay());
}
}
// Exponential backoff for failed syncs
let syncRetryCount = 0;
function getBackoffDelay() {
const delays = [1000, 2000, 5000, 10000, 30000]; // 1s, 2s, 5s, 10s, 30s
const delay = delays[Math.min(syncRetryCount, delays.length - 1)];
syncRetryCount++;
return delay;
}
// Reset retry count on successful sync
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SYNC_SUCCESS') {
syncRetryCount = 0;
}
});
// Handle push notifications (for future use)
self.addEventListener('push', (event) => {
console.log('Push message received:', event);
const options = {
body: event.data ? event.data.text() : 'Scanner notification',
icon: '/icon-192x192.png',
badge: '/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'open-scanner',
title: 'Open Scanner',
icon: '/action-icon.png'
}
]
};
event.waitUntil(
self.registration.showNotification('BCT Scanner', options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
console.log('Notification clicked:', event);
event.notification.close();
event.waitUntil(
self.clients.matchAll().then(clients => {
// Check if scanner is already open
const scannerClient = clients.find(client =>
client.url.includes('/scan')
);
if (scannerClient) {
// Focus existing scanner
return scannerClient.focus();
} else {
// Open new scanner window
return self.clients.openWindow('/scan');
}
})
);
});
console.log('Service Worker script loaded');