- Waitlist per session: entries for pick-sessions tickets name a session; offers use that session's free places, never run past its start, and claim links tick it. Held offers count towards session capacity. - At the door: the Select items pop-up has the session picker, with today's session preselected for new attendees. - On pick-sessions events every ticket shows the session list straight away. "All sessions" tickets record the ticked sessions (RegistrationOption.sessionIds) at one price; scanning, attendance, certificates, reminders and capacity follow them. - Fix: per-session stock limits weren't enforced (takenCount filtered WaitlistEntry by a sessionId column that didn't exist, and the error was swallowed), and the public event page lost its stock info. - Fix: editing a door registration with picked sessions couldn't be saved. - Fix: the offline-scanning service worker served /_next/static cache-first forever, so dev browsers that had opened the scanner ran stale code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
2.8 KiB
JavaScript
76 lines
2.8 KiB
JavaScript
/*
|
|
* Service worker for offline ticket scanning.
|
|
*
|
|
* Only the scanner page and the static files it needs are cached, both network-first
|
|
* with the cached copy as the offline fallback:
|
|
* - /_next/static/*. Not cache-first: under `next dev` these files keep their names when
|
|
* their content changes, so cache-first served stale code on every page, for good.
|
|
* (Production files are hashed and come from the browser's HTTP cache anyway.)
|
|
* - The scanner page itself.
|
|
* - The page can ask (postMessage { type: 'cache-urls', urls }) for the files it has
|
|
* loaded to be cached up front, so it works offline straight after "Enable offline mode".
|
|
* API calls are never cached — offline scans go through IndexedDB instead.
|
|
*/
|
|
// v2: drops v1 caches, which could hold stale development scripts (see above).
|
|
const CACHE = 'hope-scanner-v2';
|
|
const SCANNER_PATH = '/dashboard/staff/ticket-scanning';
|
|
|
|
self.addEventListener('install', () => self.skipWaiting());
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil((async () => {
|
|
const keys = await caches.keys();
|
|
await Promise.all(keys.filter(k => k.startsWith('hope-scanner-') && k !== CACHE).map(k => caches.delete(k)));
|
|
await self.clients.claim();
|
|
})());
|
|
});
|
|
|
|
self.addEventListener('message', (event) => {
|
|
const data = event.data || {};
|
|
if (data.type !== 'cache-urls' || !Array.isArray(data.urls)) return;
|
|
event.waitUntil((async () => {
|
|
const cache = await caches.open(CACHE);
|
|
const sameOrigin = data.urls.filter(u => {
|
|
try { return new URL(u, self.location.origin).origin === self.location.origin; } catch { return false; }
|
|
});
|
|
await Promise.all(sameOrigin.map(u => cache.add(u).catch(() => {})));
|
|
})());
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const req = event.request;
|
|
if (req.method !== 'GET') return;
|
|
const url = new URL(req.url);
|
|
if (url.origin !== self.location.origin) return;
|
|
|
|
if (url.pathname.startsWith('/_next/static/')) {
|
|
event.respondWith((async () => {
|
|
const cache = await caches.open(CACHE);
|
|
try {
|
|
const res = await fetch(req);
|
|
if (res.ok) cache.put(req, res.clone());
|
|
return res;
|
|
} catch {
|
|
const hit = await cache.match(req);
|
|
if (hit) return hit;
|
|
throw new Error('offline and not cached');
|
|
}
|
|
})());
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === SCANNER_PATH) {
|
|
event.respondWith((async () => {
|
|
const cache = await caches.open(CACHE);
|
|
try {
|
|
const res = await fetch(req);
|
|
if (res.ok) cache.put(SCANNER_PATH, res.clone());
|
|
return res;
|
|
} catch {
|
|
const hit = (await cache.match(req, { ignoreSearch: true })) || (await cache.match(SCANNER_PATH));
|
|
if (hit) return hit;
|
|
throw new Error('offline and not cached');
|
|
}
|
|
})());
|
|
}
|
|
});
|