Switched to path routing. Added data management.
This commit is contained in:
parent
8dc5d3ed01
commit
4bba0ed3a0
14 changed files with 256 additions and 46 deletions
|
|
@ -8,6 +8,7 @@ Turnpike handles gate check-in, volunteer scheduling, and department coordinatio
|
|||
|
||||
- **Attendee management** — CSV import (CrowdWork/Zeffy auto-detected), party-size tracking, search, check-in
|
||||
- **Volunteer scheduling** — departments, shifts with capacity, conflict detection, drag-and-drop reordering
|
||||
- **Public volunteer signup** — self-registration form with email confirmation, auto-attendee linking
|
||||
- **Volunteer kiosk** — token-authenticated self-service shift signup, no login required
|
||||
- **Gate check-in** — full-screen UI with QR scanner, party check-in ("2/3 checked in"), volunteer dual check-in
|
||||
- **Schedule board** — department leads and coordinators manage shift assignments with conflict awareness
|
||||
|
|
@ -90,7 +91,7 @@ The Vite dev server runs on `:5173` and proxies `/api` requests to the Go server
|
|||
|
||||
## Documentation
|
||||
|
||||
- [Usage Guide](docs/USAGE.md) — event setup, attendee import, volunteer kiosk, gate check-in, schedule board
|
||||
- [Usage Guide](docs/USAGE.md) — event setup, attendee import, volunteer signup, volunteer kiosk, gate check-in, schedule board
|
||||
- [Installation Guide](docs/INSTALLATION.md) — building, deploying, systemd, Docker, NixOS, reverse proxy, backup
|
||||
|
||||
## License
|
||||
|
|
|
|||
|
|
@ -65,6 +65,36 @@ The import result shows `inserted` (new records), `grouped` (merged into existin
|
|||
|
||||
Re-importing the same CSV is safe — existing records are skipped, not duplicated.
|
||||
|
||||
## Volunteer Signup
|
||||
|
||||
Turnpike provides a public signup form for volunteers at `/#/volunteer-signup`. No login is required.
|
||||
|
||||
### Signup flow
|
||||
|
||||
1. Volunteer visits the signup form and fills in: preferred name (required), ticket name, email (required), pronouns, phone, department preference, and an optional note.
|
||||
2. Turnpike creates a volunteer record and auto-links it to an existing attendee by email match, or creates a new attendee record.
|
||||
3. A confirmation email is sent with a unique link (`/#/confirm/{token}`).
|
||||
4. The volunteer clicks the link to confirm their email.
|
||||
5. If shift signups are already open, the confirmation page includes a link to the kiosk for shift selection.
|
||||
|
||||
Duplicate signups with the same email silently succeed — no error is shown and no duplicate is created. This prevents email enumeration.
|
||||
|
||||
### Configuring the signup form
|
||||
|
||||
In **Settings**, the "Volunteer Signup" card controls:
|
||||
|
||||
- **Note field label** — customize the label shown on the form (default: "Additional note")
|
||||
- **Note field required** — when checked, volunteers must fill in the note to submit
|
||||
|
||||
### Opening shift signups
|
||||
|
||||
In **Settings**, the "Shift Signups" card has an open/close toggle:
|
||||
|
||||
- **Opening** signups generates kiosk tokens for all confirmed volunteers and emails them their shift signup links. A confirmation dialog warns before sending.
|
||||
- **Closing** signups prevents new kiosk links from being issued on confirmation, but existing links continue to work.
|
||||
|
||||
If a volunteer confirms their email while signups are already open, they receive their kiosk link immediately in the confirmation response and via email.
|
||||
|
||||
## Managing Volunteers
|
||||
|
||||
Under **Volunteers**, you can:
|
||||
|
|
|
|||
4
email.go
4
email.go
|
|
@ -133,7 +133,7 @@ func (app *App) sendTokenEmail(a Attendee) error {
|
|||
|
||||
cfg := app.loadSMTPConfig()
|
||||
eventName := app.eventName()
|
||||
link := fmt.Sprintf("%s/#/v/%s", app.resolveBaseURL(), *a.VolunteerToken)
|
||||
link := fmt.Sprintf("%s/v/%s", app.resolveBaseURL(), *a.VolunteerToken)
|
||||
subject := fmt.Sprintf("Your volunteer link for %s", eventName)
|
||||
body := fmt.Sprintf(
|
||||
"Hi %s,\n\nThank you for volunteering at %s!\n\nYour volunteer token: %s\nYour signup link: %s\n\nUse this link to sign up for available shifts in your department.\n\nSee you there!\n",
|
||||
|
|
@ -146,7 +146,7 @@ func (app *App) sendTokenEmail(a Attendee) error {
|
|||
func (app *App) sendConfirmationEmail(to, name, confirmToken string) error {
|
||||
cfg := app.loadSMTPConfig()
|
||||
eventName := app.eventName()
|
||||
link := fmt.Sprintf("%s/#/confirm/%s", app.resolveBaseURL(), confirmToken)
|
||||
link := fmt.Sprintf("%s/confirm/%s", app.resolveBaseURL(), confirmToken)
|
||||
subject := fmt.Sprintf("Please confirm your email for %s", eventName)
|
||||
body := fmt.Sprintf(
|
||||
"Hi %s,\n\nThank you for signing up to volunteer at %s!\n\nPlease confirm your email address by visiting:\n%s\n\nIf you did not sign up, you can safely ignore this email.\n",
|
||||
|
|
|
|||
|
|
@ -23,16 +23,22 @@
|
|||
|
||||
let session = $state(null)
|
||||
let loading = $state(true)
|
||||
let route = $state(window.location.hash || '#/')
|
||||
let route = $state(window.location.pathname)
|
||||
let updateAvailable = $state(false)
|
||||
let mobileNavOpen = $state(false)
|
||||
|
||||
// Check if this is a public page (no auth needed)
|
||||
const kioskToken = $derived(window.location.hash.match(/^#\/v\/([A-Z0-9]+)/i)?.[1] ?? '')
|
||||
const isVolunteerSignup = $derived(window.location.hash.startsWith('#/volunteer-signup'))
|
||||
const isConfirmEmail = $derived(window.location.hash.startsWith('#/confirm/'))
|
||||
const kioskToken = $derived(route.match(/^\/v\/([A-Z0-9]+)/i)?.[1] ?? '')
|
||||
const isVolunteerSignup = $derived(route.startsWith('/volunteer-signup'))
|
||||
const isConfirmEmail = $derived(route.startsWith('/confirm/'))
|
||||
const isPublicPage = $derived(!!kioskToken || isVolunteerSignup || isConfirmEmail)
|
||||
|
||||
function navigate(path) {
|
||||
history.pushState(null, '', path)
|
||||
route = path
|
||||
mobileNavOpen = false
|
||||
}
|
||||
|
||||
async function checkVersion() {
|
||||
try {
|
||||
const res = await fetch('/api/version')
|
||||
|
|
@ -56,8 +62,8 @@
|
|||
startSSE()
|
||||
startSyncLoop()
|
||||
}
|
||||
window.addEventListener('hashchange', () => {
|
||||
route = window.location.hash || '#/'
|
||||
window.addEventListener('popstate', () => {
|
||||
route = window.location.pathname
|
||||
mobileNavOpen = false
|
||||
})
|
||||
|
||||
|
|
@ -67,17 +73,17 @@
|
|||
|
||||
function onLogin(s) {
|
||||
session = s
|
||||
window.location.hash = '#/'
|
||||
navigate('/')
|
||||
syncPull().then(() => { startSSE(); startSyncLoop() })
|
||||
}
|
||||
|
||||
async function onLogout() {
|
||||
await clearSession()
|
||||
session = null
|
||||
window.location.hash = '#/login'
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
const path = $derived(route.replace(/^#/, '') || '/')
|
||||
const path = $derived(route || '/')
|
||||
const role = $derived(session?.user?.role ?? '')
|
||||
</script>
|
||||
|
||||
|
|
@ -107,7 +113,7 @@
|
|||
{#if mobileNavOpen}
|
||||
<div class="nav-overlay" onclick={() => mobileNavOpen = false} onkeydown={() => {}}></div>
|
||||
{/if}
|
||||
<Nav {session} {onLogout} active={path} open={mobileNavOpen} />
|
||||
<Nav {session} {onLogout} {navigate} active={path} open={mobileNavOpen} />
|
||||
<div class="main">
|
||||
<header class="mobile-header">
|
||||
<button class="hamburger" onclick={() => mobileNavOpen = !mobileNavOpen} aria-label="Menu">
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function apiFetch(path, options = {}) {
|
|||
const res = await fetch(path, { ...options, headers })
|
||||
if (res.status === 401) {
|
||||
await db.session.clear()
|
||||
window.location.hash = '#/login'
|
||||
window.location.pathname = '/login'
|
||||
throw new Error('unauthorized')
|
||||
}
|
||||
return res
|
||||
|
|
@ -110,6 +110,11 @@ export const api = {
|
|||
update: (data) => apiJSON('/api/settings', { method: 'PUT', body: JSON.stringify(data) }),
|
||||
testEmail: (to) => apiJSON('/api/settings/test-email', { method: 'POST', body: JSON.stringify({ to }) }),
|
||||
toggleShiftSignups: (open) => apiJSON('/api/settings/shift-signups', { method: 'POST', body: JSON.stringify({ open }) }),
|
||||
resetAttendees: () => apiJSON('/api/settings/reset-attendees', { method: 'POST' }),
|
||||
resetVolunteers: () => apiJSON('/api/settings/reset-volunteers', { method: 'POST' }),
|
||||
resetShifts: () => apiJSON('/api/settings/reset-shifts', { method: 'POST' }),
|
||||
resetDepartments: () => apiJSON('/api/settings/reset-departments', { method: 'POST' }),
|
||||
resetVolunteerShifts: () => apiJSON('/api/settings/reset-volunteer-shifts', { method: 'POST' }),
|
||||
},
|
||||
signup: {
|
||||
config: () => kioskFetch('/api/public/signup-config'),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { LayoutDashboard, ClipboardCheck, Heart, Hexagon, Clock, CalendarDays, Upload, Users, Settings, LogOut } from 'lucide-svelte'
|
||||
|
||||
let { session, active, onLogout, open = false } = $props()
|
||||
let { session, active, onLogout, navigate, open = false } = $props()
|
||||
|
||||
const role = $derived(session?.user?.role ?? '')
|
||||
|
||||
|
|
@ -9,45 +9,45 @@
|
|||
|
||||
const links = $derived.by(() => {
|
||||
if (role === 'ticketing') return [
|
||||
{ href: '#/attendees', label: 'Attendees', icon: ClipboardCheck },
|
||||
{ href: '#/import', label: 'Import', icon: Upload },
|
||||
{ href: '/attendees', label: 'Attendees', icon: ClipboardCheck },
|
||||
{ href: '/import', label: 'Import', icon: Upload },
|
||||
]
|
||||
if (role === 'volunteer_lead') return [
|
||||
{ href: '#/', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '#/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '#/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '/', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '/departments', label: 'Departments', icon: Hexagon },
|
||||
]
|
||||
if (role === 'coordinator') return [
|
||||
{ href: '#/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '#/schedule', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '#/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '#/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '#/shifts', label: 'Shifts', icon: Clock },
|
||||
{ href: '/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '/schedule', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '/shifts', label: 'Shifts', icon: Clock },
|
||||
]
|
||||
return [
|
||||
{ href: '#/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '#/attendees', label: 'Attendees', icon: ClipboardCheck },
|
||||
{ href: '#/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '#/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '#/shifts', label: 'Shifts', icon: Clock },
|
||||
{ href: '#/schedule', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '#/import', label: 'Import', icon: Upload },
|
||||
{ href: '#/users', label: 'Users', icon: Users },
|
||||
{ href: '#/settings', label: 'Settings', icon: Settings },
|
||||
{ href: '/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '/attendees', label: 'Attendees', icon: ClipboardCheck },
|
||||
{ href: '/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '/shifts', label: 'Shifts', icon: Clock },
|
||||
{ href: '/schedule', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '/import', label: 'Import', icon: Upload },
|
||||
{ href: '/users', label: 'Users', icon: Users },
|
||||
{ href: '/settings', label: 'Settings', icon: Settings },
|
||||
]
|
||||
})
|
||||
|
||||
function isActive(href) {
|
||||
const p = href.replace(/^#/, '')
|
||||
if (p === '/') return active === '/' || active === ''
|
||||
return active.startsWith(p)
|
||||
if (href === '/') return active === '/' || active === ''
|
||||
return active.startsWith(href)
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="sidebar" class:open>
|
||||
<div class="sidebar-brand">Turn<span>pike</span></div>
|
||||
{#each links as link}
|
||||
<a href={link.href} class="nav-link" class:active={isActive(link.href)}>
|
||||
<a href={link.href} class="nav-link" class:active={isActive(link.href)}
|
||||
onclick={(e) => { e.preventDefault(); navigate(link.href) }}>
|
||||
<link.icon {...iconProps} />
|
||||
{link.label}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
let error = $state('')
|
||||
|
||||
onMount(async () => {
|
||||
const match = window.location.hash.match(/^#\/confirm\/(.+)/)
|
||||
const match = window.location.pathname.match(/^\/confirm\/(.+)/)
|
||||
const token = match?.[1] ?? ''
|
||||
if (!token) {
|
||||
status = 'invalid'
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
import { onMount } from 'svelte'
|
||||
import { api } from '../api.js'
|
||||
|
||||
// Token comes from the URL hash: /#/v/TOKEN
|
||||
const token = $derived(window.location.hash.match(/^#\/v\/([A-Z0-9]+)/i)?.[1] ?? '')
|
||||
const token = $derived(window.location.pathname.match(/^\/v\/([A-Z0-9]+)/i)?.[1] ?? '')
|
||||
|
||||
let state = $state(null) // { volunteer, shifts, available }
|
||||
let loading = $state(true)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function resetModel(label, fn) {
|
||||
if (!confirm(`PERMANENTLY DELETE all ${label}? This cannot be undone.`)) return
|
||||
error = ''
|
||||
success = ''
|
||||
try {
|
||||
const result = await fn()
|
||||
success = `Deleted ${result.deleted} ${label}.`
|
||||
} catch (err) {
|
||||
error = err.message
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTest() {
|
||||
if (!testEmail) return
|
||||
testing = true
|
||||
|
|
@ -186,12 +198,12 @@
|
|||
Note field is required
|
||||
</label>
|
||||
<p class="text-muted" style="font-size:0.78rem;margin-top:0.75rem">
|
||||
Signup form: <a href="/#/volunteer-signup" target="_blank" style="color:var(--c-accent)">/#/volunteer-signup</a>
|
||||
Signup form: <a href="/volunteer-signup" target="_blank" style="color:var(--c-accent)">/volunteer-signup</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Shift Signups -->
|
||||
<div class="card">
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:1rem">Shift Signups</h2>
|
||||
<div style="display:flex;align-items:center;gap:1rem">
|
||||
<span style="font-size:0.875rem">
|
||||
|
|
@ -212,5 +224,30 @@
|
|||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Data Management -->
|
||||
<div class="card">
|
||||
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:0.5rem">Data Management</h2>
|
||||
<p class="text-muted" style="font-size:0.78rem;margin-bottom:1rem">
|
||||
Permanently delete all records of a given type. This cannot be undone.
|
||||
</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.5rem">
|
||||
<button class="btn btn-danger" onclick={() => resetModel('attendees', api.settings.resetAttendees)}>
|
||||
Delete All Attendees
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick={() => resetModel('volunteers', api.settings.resetVolunteers)}>
|
||||
Delete All Volunteers
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick={() => resetModel('shifts', api.settings.resetShifts)}>
|
||||
Delete All Shifts
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick={() => resetModel('departments', api.settings.resetDepartments)}>
|
||||
Delete All Departments
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick={() => resetModel('volunteer shift assignments', api.settings.resetVolunteerShifts)}>
|
||||
Delete All Shift Assignments
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -79,6 +79,61 @@ func (app *App) handleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
|||
app.handleGetSettings(w, r)
|
||||
}
|
||||
|
||||
func (app *App) handleResetAttendees(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := app.db.Exec(`DELETE FROM attendees`)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
|
||||
writeJSON(w, map[string]any{"deleted": n})
|
||||
}
|
||||
|
||||
func (app *App) handleResetVolunteers(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := app.db.Exec(`DELETE FROM volunteers`)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
|
||||
writeJSON(w, map[string]any{"deleted": n})
|
||||
}
|
||||
|
||||
func (app *App) handleResetShifts(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := app.db.Exec(`DELETE FROM shifts`)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
|
||||
writeJSON(w, map[string]any{"deleted": n})
|
||||
}
|
||||
|
||||
func (app *App) handleResetDepartments(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := app.db.Exec(`DELETE FROM departments`)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
|
||||
writeJSON(w, map[string]any{"deleted": n})
|
||||
}
|
||||
|
||||
func (app *App) handleResetVolunteerShifts(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := app.db.Exec(`DELETE FROM volunteer_shifts`)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
|
||||
writeJSON(w, map[string]any{"deleted": n})
|
||||
}
|
||||
|
||||
func (app *App) handleTestEmail(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
To string `json:"to"`
|
||||
|
|
|
|||
|
|
@ -55,6 +55,78 @@ func TestUpdateSettings(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResetAttendees(t *testing.T) {
|
||||
app := testApp(t)
|
||||
admin := testAdminUser(t, app)
|
||||
token := testToken(t, app, admin)
|
||||
mux := testMux(app)
|
||||
|
||||
app.createAttendee(Attendee{Name: "Titania", Email: "titania@example.com"})
|
||||
app.createAttendee(Attendee{Name: "Oberon", Email: "oberon@example.com"})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-attendees", nil, token))
|
||||
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("status = %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
result := parseJSON(t, w)
|
||||
if result["deleted"] != float64(2) {
|
||||
t.Fatalf("deleted = %v, want 2", result["deleted"])
|
||||
}
|
||||
|
||||
attendees, _ := app.listAttendees("", "", "")
|
||||
if len(attendees) != 0 {
|
||||
t.Fatalf("attendees remaining = %d, want 0", len(attendees))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetAttendeesRequiresAdmin(t *testing.T) {
|
||||
app := testApp(t)
|
||||
gate := testUserWithRole(t, app, "gate1", "gate", []int{})
|
||||
token := testToken(t, app, gate)
|
||||
mux := testMux(app)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-attendees", nil, token))
|
||||
|
||||
if w.Code != 403 {
|
||||
t.Fatalf("status = %d, want 403", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetDepartmentsCascadesShifts(t *testing.T) {
|
||||
app := testApp(t)
|
||||
admin := testAdminUser(t, app)
|
||||
token := testToken(t, app, admin)
|
||||
mux := testMux(app)
|
||||
|
||||
dept, _ := app.createDepartment(Department{Name: "Rangers"})
|
||||
app.createShift(Shift{DepartmentID: dept.ID, Day: "2026-03-01", StartTime: "09:00", EndTime: "12:00", Capacity: 5})
|
||||
|
||||
shifts, _ := app.listShifts(nil, "", "")
|
||||
if len(shifts) != 1 {
|
||||
t.Fatalf("shifts before reset = %d, want 1", len(shifts))
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-departments", nil, token))
|
||||
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("status = %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
depts, _ := app.listDepartments("")
|
||||
if len(depts) != 0 {
|
||||
t.Fatalf("departments remaining = %d, want 0", len(depts))
|
||||
}
|
||||
|
||||
shifts, _ = app.listShifts(nil, "", "")
|
||||
if len(shifts) != 0 {
|
||||
t.Fatalf("shifts should cascade-delete, remaining = %d", len(shifts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsNonAdminRejected(t *testing.T) {
|
||||
app := testApp(t)
|
||||
gate := testUserWithRole(t, app, "gateuser", "gate", []int{})
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ func (app *App) handleConfirmEmail(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
if a != nil && a.VolunteerToken != nil {
|
||||
kioskLink := fmt.Sprintf("%s/#/v/%s", app.resolveBaseURL(), *a.VolunteerToken)
|
||||
kioskLink := fmt.Sprintf("%s/v/%s", app.resolveBaseURL(), *a.VolunteerToken)
|
||||
response["kiosk_link"] = kioskLink
|
||||
go func() {
|
||||
if err := app.sendShiftSignupEmail(vol.Email, vol.PreferredName, kioskLink); err != nil {
|
||||
|
|
@ -232,7 +232,7 @@ func (app *App) openShiftSignups() {
|
|||
if a == nil || a.VolunteerToken == nil {
|
||||
continue
|
||||
}
|
||||
kioskLink := fmt.Sprintf("%s/#/v/%s", baseURL, *a.VolunteerToken)
|
||||
kioskLink := fmt.Sprintf("%s/v/%s", baseURL, *a.VolunteerToken)
|
||||
name := v.PreferredName
|
||||
if name == "" {
|
||||
name = v.Name
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func (app *App) handleExportTokenLinks(w http.ResponseWriter, r *http.Request) {
|
|||
if parts := strings.Fields(a.Name); len(parts) > 0 {
|
||||
firstName = parts[0]
|
||||
}
|
||||
link := fmt.Sprintf("%s/#/v/%s", baseURL, *a.VolunteerToken)
|
||||
link := fmt.Sprintf("%s/v/%s", baseURL, *a.VolunteerToken)
|
||||
wr.Write([]string{a.Email, firstName, *a.VolunteerToken, link})
|
||||
}
|
||||
wr.Flush()
|
||||
|
|
|
|||
5
main.go
5
main.go
|
|
@ -141,6 +141,11 @@ func (app *App) registerRoutes(mux *http.ServeMux) {
|
|||
mux.HandleFunc("GET /api/settings", auth(app.handleGetSettings, "admin"))
|
||||
mux.HandleFunc("PUT /api/settings", auth(app.handleUpdateSettings, "admin"))
|
||||
mux.HandleFunc("POST /api/settings/test-email", auth(app.handleTestEmail, "admin"))
|
||||
mux.HandleFunc("POST /api/settings/reset-attendees", auth(app.handleResetAttendees, "admin"))
|
||||
mux.HandleFunc("POST /api/settings/reset-volunteers", auth(app.handleResetVolunteers, "admin"))
|
||||
mux.HandleFunc("POST /api/settings/reset-shifts", auth(app.handleResetShifts, "admin"))
|
||||
mux.HandleFunc("POST /api/settings/reset-departments", auth(app.handleResetDepartments, "admin"))
|
||||
mux.HandleFunc("POST /api/settings/reset-volunteer-shifts", auth(app.handleResetVolunteerShifts, "admin"))
|
||||
|
||||
mux.HandleFunc("POST /api/import", auth(app.handleImport, "admin", "ticketing"))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue