Mobile scanner redesign + backend live search

- Scanner page: fullscreen mobile-first layout, Scan/Search/Recent tabs
- Scan tab: auto-start camera, switch camera, vibration/sound feedback
- Valid/invalid fullscreen states, confirm check-in, auto-return to camera
- Search tab: live backend search (300ms debounce), tap card for detail + check-in
- Recent tab: last 20 check-ins, session counter
- Backend: GET /api/tickets/search (live search), GET /api/tickets/stats/checkin
- Admin layout: hide sidebar on scanner page; fix hooks order (no early return before useEffect)
- Back button to dashboard/events (staff → events, others → admin)
- API: searchLive, getCheckinStats, LiveSearchResult; PostgreSQL LOWER cast for UUID

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-02-14 04:26:44 +00:00
parent b9f46b02cc
commit 62bf048680
8 changed files with 1125 additions and 459 deletions

View File

@@ -92,6 +92,27 @@ export const ticketsApi = {
method: 'POST',
body: JSON.stringify({ code, eventId }),
}),
// Search tickets by name/email (for scanner manual search)
search: (query: string, eventId?: string) =>
fetchApi<{ tickets: TicketSearchResult[] }>('/api/tickets/search', {
method: 'POST',
body: JSON.stringify({ query, eventId }),
}),
// Get event check-in stats (for scanner header counter)
getCheckinStats: (eventId: string) =>
fetchApi<{ eventId: string; capacity: number; checkedIn: number; totalActive: number }>(
`/api/tickets/stats/checkin?eventId=${eventId}`
),
// Live search tickets (GET - for scanner live search with debounce)
searchLive: (q: string, eventId?: string) => {
const params = new URLSearchParams();
params.set('q', q);
if (eventId) params.set('eventId', eventId);
return fetchApi<{ tickets: LiveSearchResult[] }>(`/api/tickets/search?${params}`);
},
checkin: (id: string) =>
fetchApi<{ ticket: Ticket & { attendeeName?: string }; event?: { id: string; title: string }; message: string }>(`/api/tickets/${id}/checkin`, {
@@ -508,6 +529,39 @@ export interface TicketValidationResult {
error?: string;
}
export interface TicketSearchResult {
id: string;
qrCode: string;
attendeeName: string;
attendeeEmail?: string;
attendeePhone?: string;
status: string;
checkinAt?: string;
event?: {
id: string;
title: string;
startDatetime: string;
location: string;
} | null;
}
export interface LiveSearchResult {
ticket_id: string;
name: string;
email?: string;
status: string;
checked_in: boolean;
checkinAt?: string;
event_id: string;
qrCode: string;
event?: {
id: string;
title: string;
startDatetime: string;
location: string;
} | null;
}
export interface Payment {
id: string;
ticketId: string;