Added Participant list to Gate kiosk.
This commit is contained in:
parent
d439306657
commit
62b3dece84
1 changed files with 61 additions and 6 deletions
|
|
@ -7,6 +7,8 @@
|
||||||
let { session, onLogout } = $props()
|
let { session, onLogout } = $props()
|
||||||
|
|
||||||
let search = $state('')
|
let search = $state('')
|
||||||
|
let manuallySelectedId = $state(null)
|
||||||
|
let showAll = $state(false)
|
||||||
let error = $state('')
|
let error = $state('')
|
||||||
let scannerMsg = $state('')
|
let scannerMsg = $state('')
|
||||||
let qrSupported = $state(false)
|
let qrSupported = $state(false)
|
||||||
|
|
@ -44,22 +46,44 @@
|
||||||
return ($tickets ?? []).find(t => t.external_id?.toLowerCase() === sl) ?? null
|
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(() => {
|
const filteredParticipants = $derived.by(() => {
|
||||||
if (matchedTicket) return []
|
if (matchedTicket) return []
|
||||||
const s = search.trim().toLowerCase()
|
const s = search.trim().toLowerCase()
|
||||||
if (!s || s.length < 2) return []
|
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 ?? [])
|
return ($participants ?? [])
|
||||||
.filter(p =>
|
.filter(p =>
|
||||||
p.preferred_name?.toLowerCase().includes(s) ||
|
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 || ''))
|
.sort((a, b) => (a.preferred_name || '').localeCompare(b.preferred_name || ''))
|
||||||
.slice(0, 8)
|
.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(() => {
|
const selectedParticipant = $derived.by(() => {
|
||||||
|
if (manuallySelectedId) {
|
||||||
|
return ($participants ?? []).find(p => p.id === manuallySelectedId) ?? null
|
||||||
|
}
|
||||||
if (filteredParticipants.length === 1) return filteredParticipants[0]
|
if (filteredParticipants.length === 1) return filteredParticipants[0]
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
@ -165,6 +189,9 @@
|
||||||
{scanning ? '■ Stop' : '⊡ Scan QR'}
|
{scanning ? '■ Stop' : '⊡ Scan QR'}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
<button class="gbtn gbtn-ghost" onclick={() => { showAll = !showAll; search = ''; manuallySelectedId = null }}>
|
||||||
|
{showAll ? '✕ Close' : '☰ Browse'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if scanning}
|
{#if scanning}
|
||||||
|
|
@ -211,7 +238,13 @@
|
||||||
{:else if selectedParticipant}
|
{:else if selectedParticipant}
|
||||||
{@const pts = ticketsFor(selectedParticipant.id)}
|
{@const pts = ticketsFor(selectedParticipant.id)}
|
||||||
<div class="gate-match">
|
<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}
|
{#if selectedParticipant.email}
|
||||||
<div class="gate-match-sub text-muted">{selectedParticipant.email}</div>
|
<div class="gate-match-sub text-muted">{selectedParticipant.email}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -243,7 +276,7 @@
|
||||||
{#each filteredParticipants as p}
|
{#each filteredParticipants as p}
|
||||||
{@const pts = ticketsFor(p.id)}
|
{@const pts = ticketsFor(p.id)}
|
||||||
{@const ci = pts.filter(t => t.checked_in_at).length}
|
{@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>
|
<span>
|
||||||
<strong>{p.preferred_name || p.email || '(unknown)'}</strong>
|
<strong>{p.preferred_name || p.email || '(unknown)'}</strong>
|
||||||
{#if p.email && p.preferred_name}
|
{#if p.email && p.preferred_name}
|
||||||
|
|
@ -261,6 +294,27 @@
|
||||||
<div class="gate-msg gate-msg-warn">No matching participants or tickets found.</div>
|
<div class="gate-msg gate-msg-warn">No matching participants or tickets found.</div>
|
||||||
{/if}
|
{/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 -->
|
<!-- Recent check-ins -->
|
||||||
<div class="gate-recent">
|
<div class="gate-recent">
|
||||||
<div class="gate-recent-title">Recent Check-ins</div>
|
<div class="gate-recent-title">Recent Check-ins</div>
|
||||||
|
|
@ -385,7 +439,8 @@
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
margin-bottom: 1rem;
|
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-match-sub { color: var(--c-muted); font-size: 0.875rem; }
|
||||||
.gate-party {
|
.gate-party {
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue