diff --git a/src/modules/events/views/ScanTicketsPage.vue b/src/modules/events/views/ScanTicketsPage.vue index 9d4aa2ce..8e81993 100644 --- a/src/modules/events/views/ScanTicketsPage.vue +++ b/src/modules/events/views/ScanTicketsPage.vue @@ -11,16 +11,19 @@ import { ScanLine, RefreshCw, UserCheck, + Search, } from 'lucide-vue-next' import { format } from 'date-fns' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/input' import { ScrollArea } from '@/components/ui/scroll-area' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import QRScanner from '@/components/ui/qr-scanner.vue' import { useTicketScanner } from '../composables/useTicketScanner' import type { EventTicket } from '../composables/useTicketScanner' import { useEventDetail } from '../composables/useEventDetail' +import { useFuzzySearch } from '@/composables/useFuzzySearch' const route = useRoute() const router = useRouter() @@ -93,6 +96,24 @@ const unregisteredCount = computed( () => allTickets.value.filter(t => !t.registered).length, ) +// Fuzzy match on holder name + ticket id. When the search box is +// empty, Fuse returns the list in its incoming order so our +// unregistered-first sort is preserved. +const { searchQuery, filteredItems: searchedTickets } = useFuzzySearch( + allTickets, + { + fuseOptions: { + keys: [ + { name: 'name', weight: 0.7 }, + { name: 'id', weight: 0.3 }, + ], + threshold: 0.3, + ignoreLocation: true, + }, + matchAllWhenSearchEmpty: true, + }, +) + async function handleManualRegister(ticket: EventTicket) { pendingRegister.value.add(ticket.id) const res = await registerManually(ticket.id) @@ -239,14 +260,27 @@ function fmtTime(iso: string) { + +
+
No tickets sold yet.
++ No tickets match “{{ searchQuery }}”. +