- 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>
285 lines
7.8 KiB
JavaScript
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'); |