Added Participant list to Gate kiosk.

This commit is contained in:
Pen Anderson 2026-03-05 10:35:27 -06:00
parent d439306657
commit 62b3dece84

View file

@ -7,6 +7,8 @@
let { session, onLogout } = $props()
let search = $state('')
let manuallySelectedId = $state(null)
let showAll = $state(false)
let error = $state('')
let scannerMsg = $state('')
let qrSupported = $state(false)
@ -44,22 +46,44 @@
return ($tickets ?? []).find(t => t.external_id?.toLowerCase() === sl) ?? null
})
// Name/email search across participants
const allParticipantsSorted = $derived.by(() =>
($participants ?? [])
.filter(p => !p.deleted_at)
.sort((a, b) => (a.preferred_name || a.email || '').localeCompare(b.preferred_name || b.email || ''))
)
// Clear manual selection whenever search text changes
$effect(() => {
search
manuallySelectedId = null
})
// Name/email/ticket-name search across participants
const filteredParticipants = $derived.by(() => {
if (matchedTicket) return []
const s = search.trim().toLowerCase()
if (!s || s.length < 2) return []
const byTicketName = new Set(
($tickets ?? [])
.filter(t => t.name?.toLowerCase().includes(s))
.map(t => t.participant_id)
.filter(Boolean)
)
return ($participants ?? [])
.filter(p =>
p.preferred_name?.toLowerCase().includes(s) ||
p.email?.toLowerCase().includes(s)
p.email?.toLowerCase().includes(s) ||
byTicketName.has(p.id)
)
.sort((a, b) => (a.preferred_name || '').localeCompare(b.preferred_name || ''))
.slice(0, 8)
})
// Auto-select when exactly one participant matches
// Manual selection takes priority; fall back to auto-select on single match
const selectedParticipant = $derived.by(() => {
if (manuallySelectedId) {
return ($participants ?? []).find(p => p.id === manuallySelectedId) ?? null
}
if (filteredParticipants.length === 1) return filteredParticipants[0]
return null
})
@ -165,6 +189,9 @@
{scanning ? '■ Stop' : '⊡ Scan QR'}
</button>
{/if}
<button class="gbtn gbtn-ghost" onclick={() => { showAll = !showAll; search = ''; manuallySelectedId = null }}>
{showAll ? '✕ Close' : '☰ Browse'}
</button>
</div>
{#if scanning}
@ -211,7 +238,13 @@
{:else if selectedParticipant}
{@const pts = ticketsFor(selectedParticipant.id)}
<div class="gate-match">
<div class="gate-match-name">{selectedParticipant.preferred_name || selectedParticipant.email || '(unknown)'}</div>
<div class="gate-match-name-row">
<div class="gate-match-name">{selectedParticipant.preferred_name || selectedParticipant.email || '(unknown)'}</div>
{#if manuallySelectedId}
<button class="gbtn gbtn-ghost" style="font-size:0.8rem;padding:0.3rem 0.6rem"
onclick={() => manuallySelectedId = null}>← Back</button>
{/if}
</div>
{#if selectedParticipant.email}
<div class="gate-match-sub text-muted">{selectedParticipant.email}</div>
{/if}
@ -243,7 +276,7 @@
{#each filteredParticipants as p}
{@const pts = ticketsFor(p.id)}
{@const ci = pts.filter(t => t.checked_in_at).length}
<button class="gate-result-row" onclick={() => search = p.preferred_name || p.email || ''}>
<button class="gate-result-row" onclick={() => manuallySelectedId = p.id}>
<span>
<strong>{p.preferred_name || p.email || '(unknown)'}</strong>
{#if p.email && p.preferred_name}
@ -261,6 +294,27 @@
<div class="gate-msg gate-msg-warn">No matching participants or tickets found.</div>
{/if}
<!-- Browse all participants -->
{#if showAll && !matchedTicket && !selectedParticipant && search.trim().length < 2}
<div class="gate-results">
{#each allParticipantsSorted as p}
{@const pts = ticketsFor(p.id)}
{@const ci = pts.filter(t => t.checked_in_at).length}
<button class="gate-result-row" onclick={() => { manuallySelectedId = p.id; showAll = false }}>
<span>
<strong>{p.preferred_name || p.email || '(unknown)'}</strong>
{#if p.email && p.preferred_name}
<span class="text-muted" style="font-size:0.8rem"> · {p.email}</span>
{/if}
</span>
<span class="badge {ci === pts.length && pts.length > 0 ? 'badge-checked' : ci > 0 ? 'badge-partial' : 'badge-unchecked'}">
{pts.length > 0 ? `${ci}/${pts.length}` : 'No ticket'}
</span>
</button>
{/each}
</div>
{/if}
<!-- Recent check-ins -->
<div class="gate-recent">
<div class="gate-recent-title">Recent Check-ins</div>
@ -385,7 +439,8 @@
padding: 1.25rem;
margin-bottom: 1rem;
}
.gate-match-name { font-size: 1.4rem; font-weight: 700; margin-bottom: 0.2rem; }
.gate-match-name-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.2rem; }
.gate-match-name { font-size: 1.4rem; font-weight: 700; }
.gate-match-sub { color: var(--c-muted); font-size: 0.875rem; }
.gate-party {
margin: 0.5rem 0;