Set up Unconfirmed -> Registered -> Confirmed -> Ready flow for Volunteers

This commit is contained in:
Pen Anderson 2026-03-05 15:52:40 -06:00
parent 62b3dece84
commit 72b245d6d6
7 changed files with 95 additions and 20 deletions

View file

@ -8,7 +8,7 @@
let search = $state('')
let filterDept = $state('')
let filterChecked = $state('')
let filterStatus = $state('')
let error = $state('')
let showAdd = $state(false)
let adding = $state(false)
@ -20,6 +20,7 @@
const role = $derived(session?.user?.role ?? '')
const canManage = $derived(['admin', 'ticketing', 'staffing', 'colead'].includes(role))
const canConfirm = $derived(['admin', 'staffing', 'colead'].includes(role))
const myDeptIDs = $derived(session?.user?.department_ids ?? [])
// Auto-filter coleads to their department on mount
@ -33,6 +34,7 @@
db.volunteers.filter(v => !v.deleted_at).toArray()
)
const allParticipants = liveQuery(() => db.participants.toArray())
const allTickets = liveQuery(() => db.tickets.filter(t => !t.deleted_at).toArray())
const allDepts = liveQuery(() =>
db.departments.filter(d => !d.deleted_at).toArray()
.then(arr => arr.sort((a, b) => a.name.localeCompare(b.name)))
@ -44,8 +46,10 @@
return list
.filter(v => {
if (filterDept && v.department_id !== parseInt(filterDept)) return false
if (filterChecked === 'true' && !v.checked_in) return false
if (filterChecked === 'false' && v.checked_in) return false
if (filterStatus === 'unconfirmed' && v.email_confirmed) return false
if (filterStatus === 'registered' && (!v.email_confirmed || v.confirmed)) return false
if (filterStatus === 'confirmed' && (!v.confirmed || v.checked_in)) return false
if (filterStatus === 'ready' && !v.checked_in) return false
if (s && !v.name.toLowerCase().includes(s) &&
!(v.email || '').toLowerCase().includes(s)) return false
return true
@ -62,6 +66,15 @@
}
}
async function confirmVolunteer(v) {
try {
const updated = await api.volunteers.confirm(v.id)
await db.volunteers.put(updated)
} catch (err) {
error = err.message
}
}
async function addVolunteer(e) {
e.preventDefault()
adding = true
@ -110,6 +123,11 @@
return ($allDepts ?? []).find(d => d.id === id)
}
function participantHasTickets(participantId) {
if (!participantId) return false
return ($allTickets ?? []).some(t => t.participant_id === participantId)
}
function participantFor(id) {
return ($allParticipants ?? []).find(p => p.id === id) ?? null
}
@ -181,10 +199,12 @@
{/each}
</select>
{/if}
<select bind:value={filterChecked} style="width:auto">
<option value="">All</option>
<option value="false">Not ready</option>
<option value="true">Ready</option>
<select bind:value={filterStatus} style="width:auto">
<option value="">All statuses</option>
<option value="unconfirmed">Unconfirmed</option>
<option value="registered">Registered</option>
<option value="confirmed">Confirmed</option>
<option value="ready">Ready</option>
</select>
<span class="text-muted" style="font-size:0.85rem;white-space:nowrap">
{filtered.length} shown
@ -219,7 +239,9 @@
<span class="badge badge-lead" style="margin-left:0.4rem">Co-Lead</span>
{/if}
{#if !v.participant_id}
<span class="badge badge-unchecked" style="margin-left:0.4rem" title="Not linked to a participant — no ticket record">No ticket</span>
<span class="badge badge-unchecked" style="margin-left:0.4rem" title="Not linked to a participant">No ticket</span>
{:else if !participantHasTickets(v.participant_id)}
<span class="badge badge-partial" style="margin-left:0.4rem" title="Registered as volunteer but no ticket on file">No ticket</span>
{/if}
{#if v.email}
<div class="text-muted" style="font-size:0.78rem">{v.email}</div>
@ -236,9 +258,15 @@
{/if}
</td>
<td class="td-status">
<span class="badge {v.checked_in ? 'badge-checked' : v.email_confirmed ? 'badge-confirmed' : 'badge-unchecked'}">
{v.checked_in ? 'Ready' : v.email_confirmed ? 'Confirmed' : 'Unconfirmed'}
</span>
{#if v.checked_in}
<span class="badge badge-checked">Ready</span>
{:else if v.confirmed}
<span class="badge badge-confirmed">Confirmed</span>
{:else if v.email_confirmed}
<span class="badge badge-registered">Registered</span>
{:else}
<span class="badge badge-unchecked">Unconfirmed</span>
{/if}
{#if v.checked_in_at}
<div class="text-muted" style="font-size:0.75rem">
{new Date(v.checked_in_at).toLocaleTimeString()}
@ -252,6 +280,9 @@
</td>
{#if canManage}
<td class="td-actions">
{#if canConfirm && v.email_confirmed && !v.confirmed && v.department_id}
<button class="btn btn-primary btn-sm" onclick={() => confirmVolunteer(v)}>Confirm</button>
{/if}
<button class="btn btn-ghost btn-sm" onclick={() => toggleLead(v)}
title={v.is_lead ? 'Remove co-lead' : 'Mark as co-lead'}>
{v.is_lead ? ' Co-Lead' : '+ Co-Lead'}