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

@@ -20,6 +20,7 @@ import {
EnvelopeIcon,
PencilIcon,
EyeIcon,
EyeSlashIcon,
PaperAirplaneIcon,
UserGroupIcon,
MagnifyingGlassIcon,
@@ -63,6 +64,7 @@ export default function AdminEventDetailPage() {
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'confirmed' | 'checked_in' | 'cancelled'>('all');
const [showAddAtDoorModal, setShowAddAtDoorModal] = useState(false);
const [showManualTicketModal, setShowManualTicketModal] = useState(false);
const [showStats, setShowStats] = useState(true);
const [showNoteModal, setShowNoteModal] = useState(false);
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
const [noteText, setNoteText] = useState('');
@@ -496,62 +498,84 @@ export default function AdminEventDetailPage() {
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<UsersIcon className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-bold">{event.capacity}</p>
<p className="text-sm text-gray-500">Capacity</p>
</div>
<div className="mb-6 flex items-center justify-between gap-4">
{showStats ? (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 flex-1">
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<UsersIcon className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-bold">{event.capacity}</p>
<p className="text-sm text-gray-500">Capacity</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircleIcon className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-bold">{confirmedCount}</p>
<p className="text-sm text-gray-500">Confirmed</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<ClockIcon className="w-5 h-5 text-yellow-600" />
</div>
<div>
<p className="text-2xl font-bold">{pendingCount}</p>
<p className="text-sm text-gray-500">Pending</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<TicketIcon className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-2xl font-bold">{checkedInCount}</p>
<p className="text-sm text-gray-500">Checked In</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
<CurrencyDollarIcon className="w-5 h-5 text-gray-600" />
</div>
<div>
<p className="text-2xl font-bold">{formatCurrency(confirmedCount * event.price, event.currency)}</p>
<p className="text-sm text-gray-500">Revenue</p>
</div>
</div>
</Card>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircleIcon className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-bold">{confirmedCount}</p>
<p className="text-sm text-gray-500">Confirmed</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<ClockIcon className="w-5 h-5 text-yellow-600" />
</div>
<div>
<p className="text-2xl font-bold">{pendingCount}</p>
<p className="text-sm text-gray-500">Pending</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<TicketIcon className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-2xl font-bold">{checkedInCount}</p>
<p className="text-sm text-gray-500">Checked In</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
<CurrencyDollarIcon className="w-5 h-5 text-gray-600" />
</div>
<div>
<p className="text-2xl font-bold">{formatCurrency(confirmedCount * event.price, event.currency)}</p>
<p className="text-sm text-gray-500">Revenue</p>
</div>
</div>
</Card>
) : null}
<Button
variant="outline"
size="sm"
onClick={() => setShowStats(!showStats)}
className="flex-shrink-0"
>
{showStats ? (
<>
<EyeSlashIcon className="w-4 h-4 mr-2" />
Hide stats
</>
) : (
<>
<EyeIcon className="w-4 h-4 mr-2" />
Show stats
</>
)}
</Button>
</div>
{/* Tabs */}
@@ -966,9 +990,16 @@ export default function AdminEventDetailPage() {
{/* Add at Door Modal */}
{showAddAtDoorModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray">
<div
className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
onClick={() => setShowAddAtDoorModal(false)}
role="presentation"
>
<Card
className="w-full max-w-md max-h-[90vh] flex flex-col overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray flex-shrink-0">
<h2 className="text-lg font-bold">Add Attendee at Door</h2>
<button
onClick={() => setShowAddAtDoorModal(false)}
@@ -977,7 +1008,7 @@ export default function AdminEventDetailPage() {
<XMarkIcon className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleAddAtDoor} className="p-4 space-y-4">
<form onSubmit={handleAddAtDoor} className="p-4 space-y-4 overflow-y-auto flex-1 min-h-0">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium mb-1">First Name *</label>
@@ -1063,9 +1094,16 @@ export default function AdminEventDetailPage() {
{/* Manual Ticket Modal */}
{showManualTicketModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray">
<div
className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
onClick={() => setShowManualTicketModal(false)}
role="presentation"
>
<Card
className="w-full max-w-md max-h-[90vh] flex flex-col overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray flex-shrink-0">
<div>
<h2 className="text-lg font-bold">Create Manual Ticket</h2>
<p className="text-sm text-gray-500">Attendee will receive a confirmation email with their ticket</p>
@@ -1077,7 +1115,7 @@ export default function AdminEventDetailPage() {
<XMarkIcon className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleManualTicket} className="p-4 space-y-4">
<form onSubmit={handleManualTicket} className="p-4 space-y-4 overflow-y-auto flex-1 min-h-0">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium mb-1">First Name *</label>