diff --git a/src/modules/activities/composables/useTicketScanner.ts b/src/modules/activities/composables/useTicketScanner.ts index 50006a1..7053f5e 100644 --- a/src/modules/activities/composables/useTicketScanner.ts +++ b/src/modules/activities/composables/useTicketScanner.ts @@ -1,4 +1,4 @@ -import { ref, type Ref } from 'vue' +import { ref, onMounted, type Ref } from 'vue' import { useLocalStorage } from '@vueuse/core' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { NostrTransportService } from '@/modules/base/services/NostrTransportService' @@ -21,16 +21,34 @@ export interface ScanRecord { registeredAt: string } +/** A paid ticket as returned by `events_list_event_tickets`. */ +export interface EventTicket { + id: string + name?: string | null + registered: boolean + registeredAt: string | null +} + +/** Counts + roster snapshot for the event, sourced from the backend. */ +export interface EventStats { + sold: number + registered: number + remaining: number + tickets: EventTicket[] +} + /** * Stateful scanner driver. Owns the camera lifecycle (delegated to * useQRScanner upstream), the QR decode, the - * `events_ticket_register` RPC call, and a session-local scanned - * list persisted to localStorage so a page refresh doesn't ask the - * organizer to rescan tickets they already counted. + * `events_ticket_register` RPC call, an authoritative event roster + * fetched from `events_list_event_tickets`, and a session-local + * dedup cache. * - * Mirrors the LNbits admin Quasar register page's - * `events_scanned_` localStorage key with the - * `activities_scanned_` prefix. + * Counts + the displayed scanned list come from the backend so the + * UI agrees with reality even when a second organizer is scanning + * on another device. The localStorage cache is kept as a silent + * dedup so the 5-fps decode loop doesn't re-fire the RPC on a QR + * the camera held in frame for multiple ticks. */ export function useTicketScanner(activityId: Ref) { const transport = injectService( @@ -50,6 +68,11 @@ export function useTicketScanner(activityId: Ref) { * just succeeded. */ const isPaused = ref(false) + /** Server-authoritative counts + per-ticket registered status. */ + const eventStats = ref(null) + const statsLoading = ref(false) + const statsError = ref(null) + /** Session-local dedup. Hidden from UI; only guards repeat decodes. */ const scanned = useLocalStorage( () => `activities_scanned_${activityId.value}`, [], @@ -61,6 +84,40 @@ export function useTicketScanner(activityId: Ref) { : qrText } + async function refreshStats(): Promise { + if (!activityId.value) return + statsLoading.value = true + statsError.value = null + try { + const data = await transport.call<{ + sold: number + registered: number + remaining: number + tickets: Array<{ + id: string + name?: string | null + registered: boolean + registered_at: string | null + }> + }>('events_list_event_tickets', { event_id: activityId.value }) + eventStats.value = { + sold: data.sold, + registered: data.registered, + remaining: data.remaining, + tickets: data.tickets.map(t => ({ + id: t.id, + name: t.name ?? null, + registered: t.registered, + registeredAt: t.registered_at, + })), + } + } catch (e) { + statsError.value = e instanceof Error ? e.message : String(e) + } finally { + statsLoading.value = false + } + } + async function onDecode(qrText: string): Promise { if (isProcessing.value || isPaused.value) return const ticketId = parseTicketId(qrText).trim() @@ -107,6 +164,9 @@ export function useTicketScanner(activityId: Ref) { // banner and can correct (let the attendee in, deny entry, // etc.) before the next QR comes into frame. isPaused.value = true + // Refresh the roster so the counts strip + scanned tab reflect + // the new state. Fire-and-forget — UI doesn't block on it. + void refreshStats() } } @@ -121,11 +181,19 @@ export function useTicketScanner(activityId: Ref) { isPaused.value = false } + onMounted(() => { + void refreshStats() + }) + return { isProcessing, isPaused, lastScan, scanned, + eventStats, + statsLoading, + statsError, + refreshStats, onDecode, resume, clearScanned, diff --git a/src/modules/activities/views/ScanTicketsPage.vue b/src/modules/activities/views/ScanTicketsPage.vue index 67c51f5..a1b6648 100644 --- a/src/modules/activities/views/ScanTicketsPage.vue +++ b/src/modules/activities/views/ScanTicketsPage.vue @@ -1,12 +1,20 @@