Compare commits
No commits in common. "07f7d3d245cebb3e112647076e0c4f4258833fc8" and "2b409c65c1c00954b4e252e5b73849c4616f47e1" have entirely different histories.
07f7d3d245
...
2b409c65c1
6 changed files with 22 additions and 85 deletions
|
|
@ -9,8 +9,8 @@ Turnpike handles gate check-in, volunteer scheduling, and department coordinatio
|
|||
- **Participant & ticket management** — CSV import (CrowdWork/Zeffy auto-detected), search, check-in
|
||||
- **Volunteer scheduling** — departments, shifts with capacity, conflict detection, reordering
|
||||
- **Public volunteer signup** — self-registration form with email confirmation, auto-participant linking
|
||||
- **Volunteer kiosk** — public volunteer flow: signup, email confirmation, code-authenticated shift self-scheduling
|
||||
- **Gate kiosk** — full-screen check-in UI with QR scanner for gatekeepers
|
||||
- **Volunteer kiosk** — code-authenticated self-service shift signup, no login required
|
||||
- **Gate check-in** — full-screen UI with QR scanner, volunteer dual check-in
|
||||
- **Schedule** — create shifts, assign volunteers, manage assignments with conflict awareness
|
||||
- **Role-based access** — admin, ticketing, staffing, colead (department-scoped), gatekeeper
|
||||
- **Offline-first PWA** — installs on phones/tablets, full offline check-in with background sync
|
||||
|
|
@ -64,7 +64,7 @@ See [docs/INSTALLATION.md](docs/INSTALLATION.md) for systemd, Docker, NixOS, and
|
|||
| `ticketing` | Participants, tickets, import. No user management |
|
||||
| `staffing` | All departments: volunteers, shifts, schedule. No user management or settings |
|
||||
| `colead` | Own department only: volunteers and shifts scoped to assigned department(s) |
|
||||
| `gatekeeper` | Full-screen Gate Kiosk with QR scanner. No access to other pages |
|
||||
| `gatekeeper` | Full-screen check-in UI with QR scanner. No access to other pages |
|
||||
|
||||
See [docs/USAGE.md](docs/USAGE.md) for detailed workflow documentation.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ After logging in, create accounts for your team under **Users**. Each user gets
|
|||
| **ticketing** | Participants, Tickets, Import | Manage participants and tickets, run CSV imports |
|
||||
| **staffing** | Dashboard, Schedule, Volunteers, Departments | Manage volunteers, departments, and shifts across all departments. No user management or settings |
|
||||
| **colead** | Dashboard, Schedule, Volunteers | Manage volunteers and shifts within their assigned department(s) only |
|
||||
| **gatekeeper** | Full-screen Gate Kiosk | Check in ticket holders (search + QR scan). No access to other pages |
|
||||
| **gatekeeper** | Full-screen Gate UI | Check in ticket holders (search + QR scan). No access to other pages |
|
||||
|
||||
Coleads are scoped to one or more departments. When creating a colead user, assign their department(s).
|
||||
|
||||
|
|
@ -95,15 +95,7 @@ Under **Volunteers**, you can:
|
|||
- Create volunteers manually (name, email, department)
|
||||
- Assign volunteers to departments
|
||||
- Mark volunteers as co-leads
|
||||
- Mark volunteers as ready (briefed at the volunteer station)
|
||||
|
||||
### Volunteer statuses
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| **Unconfirmed** | Signed up but hasn't confirmed their email |
|
||||
| **Confirmed** | Email confirmed, not yet briefed |
|
||||
| **Ready** | Briefed at the volunteer station, has what they need to report for shifts |
|
||||
- Check in volunteers
|
||||
|
||||
Volunteers are separate from participants. A person can be both a ticket holder and a volunteer. When a volunteer signs up via the public form, they are automatically linked to their participant record by email.
|
||||
|
||||
|
|
@ -125,7 +117,7 @@ Shifts can be reordered within a department to reflect priority or sequence usin
|
|||
|
||||
## Volunteer Kiosk
|
||||
|
||||
The Volunteer Kiosk is the public-facing flow for volunteers: signup, email confirmation, and shift self-scheduling. The shift scheduling page lets volunteers self-select shifts without logging in.
|
||||
The kiosk lets volunteers self-select shifts without logging in.
|
||||
|
||||
### Setup
|
||||
|
||||
|
|
@ -153,16 +145,15 @@ No login is required. The kiosk code authenticates the request.
|
|||
|
||||
Kiosk codes use the character set `A-Z, 2-9` (excluding 0/O, 1/I/L to avoid ambiguity when reading aloud or on printed badges).
|
||||
|
||||
## Gate Kiosk
|
||||
## Gate Check-In
|
||||
|
||||
Users with the **gatekeeper** role see a dedicated full-screen Gate Kiosk:
|
||||
Users with the **gatekeeper** role see a dedicated full-screen UI:
|
||||
|
||||
- **QR scanner** — uses the device camera via the BarcodeDetector API. Scanned codes populate the search field.
|
||||
- **Search** — type a name to filter tickets in real-time (searches local IndexedDB, works offline).
|
||||
- **Volunteer dual check-in** — if a ticket holder is also a volunteer, the gate UI shows their volunteer status and offers to check in both simultaneously.
|
||||
- **Recent check-ins** — the last 10 check-ins are shown for quick reference.
|
||||
|
||||
Admins and ticketing leads can also check in tickets directly from the **Participants** page by expanding a participant's tickets.
|
||||
|
||||
Gate devices should install Turnpike as a PWA (Add to Home Screen) for the best experience. Check-ins are stored locally and sync when connectivity is available.
|
||||
|
||||
## Schedule
|
||||
|
|
|
|||
|
|
@ -207,32 +207,4 @@ tr:hover td { background: rgba(255,255,255,0.02); }
|
|||
}
|
||||
.page { padding: 1rem; }
|
||||
.stats { grid-template-columns: repeat(2, 1fr); }
|
||||
|
||||
/* Touch targets */
|
||||
.btn { min-height: 44px; padding: 0.6rem 1rem; }
|
||||
.btn-sm { min-height: 44px; padding: 0.5rem 0.75rem; font-size: 0.85rem; }
|
||||
|
||||
/* Page header & actions */
|
||||
.page-header { flex-wrap: wrap; gap: 0.75rem; }
|
||||
.page-title { width: 100%; }
|
||||
.actions { flex-wrap: wrap; }
|
||||
|
||||
/* Search bar */
|
||||
.search-bar { flex-wrap: wrap; }
|
||||
.search-bar input { max-width: none; flex: 1 1 100%; }
|
||||
|
||||
/* Table → card layout */
|
||||
.table-wrap { overflow-x: visible; }
|
||||
table { display: block; }
|
||||
thead { display: none; }
|
||||
tbody { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
tr { display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem; align-items: center;
|
||||
padding: 0.75rem; border: 1px solid var(--c-border); border-radius: var(--radius-lg);
|
||||
background: var(--c-surface); }
|
||||
tr:hover td { background: transparent; }
|
||||
td { display: inline; padding: 0; border: none; }
|
||||
td:empty { display: none; }
|
||||
|
||||
/* Forms */
|
||||
.form-grid { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@
|
|||
{#if showAdd && canManage}
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<form onsubmit={addParticipant}>
|
||||
<div class="form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||
<div class="form-group">
|
||||
<label for="p-name">Name</label>
|
||||
<input id="p-name" bind:value={newName} placeholder="Preferred name" />
|
||||
|
|
@ -362,7 +362,7 @@
|
|||
onclick={mergeMode && mergeSource?.id !== p.id ? () => { mergeTarget = p } : null}
|
||||
style={mergeMode && mergeSource?.id !== p.id ? 'cursor:pointer' : ''}
|
||||
>
|
||||
<td class="td-name">
|
||||
<td>
|
||||
<strong>{p.preferred_name || '—'}</strong>
|
||||
{#if p.pronouns}
|
||||
<span class="text-muted" style="font-size:0.78rem"> · {p.pronouns}</span>
|
||||
|
|
@ -397,7 +397,7 @@
|
|||
{/if}
|
||||
</td>
|
||||
{#if canManage}
|
||||
<td class="td-actions">
|
||||
<td>
|
||||
{#if !mergeMode}
|
||||
<button class="btn btn-ghost btn-sm" onclick={(e) => { e.stopPropagation(); startEdit(p) }}
|
||||
title="Edit participant">✎</button>
|
||||
|
|
@ -504,14 +504,4 @@
|
|||
.edit-fields { display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
||||
.edit-fields input { flex: 1; min-width: 120px; font-size: 0.825rem; padding: 0.3rem 0.5rem; width: auto; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.td-name { width: 100%; }
|
||||
.td-actions { width: 100%; }
|
||||
.ticket-rows { padding: 0; border: none; border-radius: 0; margin-top: -0.5rem; }
|
||||
.ticket-rows td { width: 100%; }
|
||||
.ticket-row { flex-direction: column; gap: 0.35rem; }
|
||||
.ticket-row div:last-child { text-align: left; }
|
||||
.edit-row { padding: 0.75rem; }
|
||||
.edit-row td { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@
|
|||
{#if showAdd}
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<form onsubmit={addUser}>
|
||||
<div class="form-grid" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem">
|
||||
<div class="form-group">
|
||||
<label for="u-username">Username *</label>
|
||||
<input id="u-username" bind:value={newUsername} required placeholder="username" autocomplete="off" />
|
||||
|
|
@ -213,8 +213,8 @@
|
|||
<tbody>
|
||||
{#each users as u (u.id)}
|
||||
{#if editID === u.id}
|
||||
<tr class="edit-row">
|
||||
<td class="td-name"><strong>{u.username}</strong> {#if u.id === me}<span class="badge badge-role">you</span>{/if}</td>
|
||||
<tr>
|
||||
<td><strong>{u.username}</strong> {#if u.id === me}<span class="badge badge-role">you</span>{/if}</td>
|
||||
<td>
|
||||
<select bind:value={editRole} style="width:auto;margin:0">
|
||||
{#each roles as r}
|
||||
|
|
@ -239,7 +239,7 @@
|
|||
placeholder="New password (leave blank to keep)"
|
||||
style="margin-top:0.5rem" autocomplete="new-password" />
|
||||
</td>
|
||||
<td class="td-actions">
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary btn-sm" onclick={() => saveUser(u)} disabled={saving}>
|
||||
{saving ? '…' : 'Save'}
|
||||
|
|
@ -250,7 +250,7 @@
|
|||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td class="td-name">
|
||||
<td>
|
||||
<strong>{u.username}</strong>
|
||||
{#if u.id === me}
|
||||
<span class="badge badge-role" style="margin-left:0.4rem">you</span>
|
||||
|
|
@ -258,7 +258,7 @@
|
|||
</td>
|
||||
<td><span class="badge badge-role">{roleLabel(u.role)}</span></td>
|
||||
<td class="text-muted">{deptNamesFor(u.department_ids || [])}</td>
|
||||
<td class="td-actions">
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick={() => startEdit(u)}>Edit</button>
|
||||
{#if u.id !== me}
|
||||
|
|
@ -274,11 +274,3 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media (max-width: 640px) {
|
||||
.td-name { width: 100%; }
|
||||
.td-actions { width: 100%; }
|
||||
.edit-row td { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
{#if showAdd && canManage}
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<form onsubmit={addVolunteer}>
|
||||
<div class="form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||
<div class="form-group">
|
||||
<label for="v-name">Name *</label>
|
||||
<input id="v-name" bind:value={newName} required placeholder="Full name" />
|
||||
|
|
@ -213,7 +213,7 @@
|
|||
{@const dept = deptFor(v.department_id)}
|
||||
{@const participant = participantFor(v.participant_id)}
|
||||
<tr>
|
||||
<td class="td-name">
|
||||
<td>
|
||||
<strong>{v.name}</strong>
|
||||
{#if v.is_lead}
|
||||
<span class="badge badge-lead" style="margin-left:0.4rem">Co-Lead</span>
|
||||
|
|
@ -245,13 +245,13 @@
|
|||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="td-ready">
|
||||
<td>
|
||||
{#if !v.checked_in}
|
||||
<CheckInButton onclick={() => checkIn(v)} />
|
||||
{/if}
|
||||
</td>
|
||||
{#if canManage}
|
||||
<td class="td-actions">
|
||||
<td>
|
||||
<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'}
|
||||
|
|
@ -266,11 +266,3 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media (max-width: 640px) {
|
||||
.td-name { width: 100%; }
|
||||
.td-ready { width: 100%; }
|
||||
.td-actions { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue