Set co-leads to confirmed automatically, and updated test and documents.
This commit is contained in:
parent
ab3d9a0409
commit
cc4dd76438
3 changed files with 151 additions and 6 deletions
|
|
@ -83,7 +83,7 @@ In **Settings**, the "Volunteer Signup" card controls:
|
|||
|
||||
In **Settings**, the "Shift Signups" card has an open/close toggle:
|
||||
|
||||
- **Opening** signups generates kiosk codes for all confirmed volunteers and emails them their shift signup links. A confirmation dialog warns before sending.
|
||||
- **Opening** signups generates kiosk codes for all registered (email-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.
|
||||
|
|
@ -92,9 +92,9 @@ If a volunteer confirms their email while signups are already open, they receive
|
|||
|
||||
Under **Volunteers**, you can:
|
||||
|
||||
- Create volunteers manually (name, email, department)
|
||||
- Assign volunteers to departments
|
||||
- Mark volunteers as co-leads
|
||||
- Create volunteers manually (name, email, department, co-lead, note)
|
||||
- Edit existing volunteers (department, co-lead, note) via the inline Edit button
|
||||
- Confirm registered volunteers (admin, staffing, colead)
|
||||
- Mark volunteers as ready (briefed at the volunteer station)
|
||||
|
||||
### Volunteer statuses
|
||||
|
|
@ -106,7 +106,7 @@ Under **Volunteers**, you can:
|
|||
| **Confirmed** | Staff has verified the volunteer is assigned and ready to be scheduled | Admin, staffing, or co-lead |
|
||||
| **Ready** | Briefed at the volunteer station, cleared to report for shifts | Gate staff at check-in |
|
||||
|
||||
**Confirmation** is a deliberate staff action — it signals that a volunteer has been assigned to a department and you're expecting them. Only volunteers who have been assigned a department can be confirmed. Use the **Confirm** button on a registered volunteer's row.
|
||||
**Confirmation** is a deliberate staff action — it signals that you're expecting the volunteer for shifts. Use the **Confirm** button on a registered volunteer's row. Marking a volunteer as a co-lead (`is_lead`) automatically confirms them.
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ The Volunteer Kiosk is the public-facing flow for volunteers: signup, email conf
|
|||
Kiosk links are generated and distributed automatically through the volunteer signup flow:
|
||||
|
||||
1. Volunteers sign up via the public signup form (`/volunteer-signup`) and confirm their email.
|
||||
2. In **Settings**, open shift signups. This generates kiosk codes for all confirmed volunteers and emails them their links. A confirmation dialog warns before sending.
|
||||
2. In **Settings**, open shift signups. This generates kiosk codes for all registered (email-confirmed) volunteers and emails them their links. A confirmation dialog warns before sending.
|
||||
3. If a volunteer confirms their email while signups are already open, they receive their kiosk link immediately.
|
||||
|
||||
**Set base URL** — in Settings, set the public base URL (e.g., `https://turnpike.example.com`). Kiosk links use this URL.
|
||||
|
|
|
|||
|
|
@ -109,6 +109,10 @@ func (app *App) handleUpdateVolunteer(w http.ResponseWriter, r *http.Request) {
|
|||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if v.IsLead {
|
||||
app.confirmVolunteer(id)
|
||||
}
|
||||
updated, _ := app.getVolunteer(id)
|
||||
writeJSON(w, updated)
|
||||
}
|
||||
|
|
|
|||
141
handle_volunteers_test.go
Normal file
141
handle_volunteers_test.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfirmVolunteer(t *testing.T) {
|
||||
app := testApp(t)
|
||||
mux := testMux(app)
|
||||
admin := testAdminUser(t, app)
|
||||
tok := testToken(t, app, admin)
|
||||
|
||||
dept, _ := app.createDepartment(Department{Name: "Gate"})
|
||||
deptID := dept.ID
|
||||
v, _ := app.createVolunteer(Volunteer{
|
||||
Name: "Titania", Email: "titania@test.com",
|
||||
DepartmentID: &deptID, EmailConfirmed: true,
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/confirm", nil, tok))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
result := parseJSON(t, w)
|
||||
vol := result["confirmed"]
|
||||
if vol != true {
|
||||
t.Error("expected confirmed=true in response")
|
||||
}
|
||||
|
||||
got, _ := app.getVolunteer(v.ID)
|
||||
if got == nil || !got.Confirmed {
|
||||
t.Error("volunteer should be confirmed in DB")
|
||||
}
|
||||
if got.ConfirmedAt == nil {
|
||||
t.Error("confirmed_at should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmVolunteerIdempotent(t *testing.T) {
|
||||
app := testApp(t)
|
||||
mux := testMux(app)
|
||||
admin := testAdminUser(t, app)
|
||||
tok := testToken(t, app, admin)
|
||||
|
||||
v, _ := app.createVolunteer(Volunteer{Name: "Puck", Email: "puck@test.com", EmailConfirmed: true})
|
||||
|
||||
// Confirm twice — second should be a no-op, not an error.
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/confirm", nil, tok))
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("first confirm: %d", w.Code)
|
||||
}
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/confirm", nil, tok))
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("second confirm: %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmVolunteerRequiresRole(t *testing.T) {
|
||||
app := testApp(t)
|
||||
mux := testMux(app)
|
||||
|
||||
// Ticketing role should NOT be able to confirm volunteers.
|
||||
ticketing := testUserWithRole(t, app, "ticket_lead", "ticketing", nil)
|
||||
tok := testToken(t, app, ticketing)
|
||||
|
||||
v, _ := app.createVolunteer(Volunteer{Name: "Helena", EmailConfirmed: true})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/confirm", nil, tok))
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403 for ticketing role, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateVolunteerDepartment(t *testing.T) {
|
||||
app := testApp(t)
|
||||
mux := testMux(app)
|
||||
admin := testAdminUser(t, app)
|
||||
tok := testToken(t, app, admin)
|
||||
|
||||
dept, _ := app.createDepartment(Department{Name: "Gate"})
|
||||
v, _ := app.createVolunteer(Volunteer{Name: "Hermia"})
|
||||
|
||||
// Assign department via update.
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, testAuthRequest("PUT", "/api/volunteers/"+itoa(v.ID), map[string]any{
|
||||
"name": "Hermia", "department_id": dept.ID,
|
||||
}, tok))
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
got, _ := app.getVolunteer(v.ID)
|
||||
if got.DepartmentID == nil || *got.DepartmentID != dept.ID {
|
||||
t.Errorf("department_id = %v, want %d", got.DepartmentID, dept.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateVolunteerCoLeadAutoConfirms(t *testing.T) {
|
||||
app := testApp(t)
|
||||
mux := testMux(app)
|
||||
admin := testAdminUser(t, app)
|
||||
tok := testToken(t, app, admin)
|
||||
|
||||
dept, _ := app.createDepartment(Department{Name: "Build"})
|
||||
deptID := dept.ID
|
||||
v, _ := app.createVolunteer(Volunteer{
|
||||
Name: "Lysander", Email: "lys@test.com",
|
||||
DepartmentID: &deptID, EmailConfirmed: true,
|
||||
})
|
||||
|
||||
// Verify not confirmed before update.
|
||||
got, _ := app.getVolunteer(v.ID)
|
||||
if got.Confirmed {
|
||||
t.Fatal("should not be confirmed before update")
|
||||
}
|
||||
|
||||
// Update is_lead=true should auto-confirm.
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, testAuthRequest("PUT", "/api/volunteers/"+itoa(v.ID), map[string]any{
|
||||
"name": "Lysander", "department_id": deptID, "is_lead": true,
|
||||
}, tok))
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
got, _ = app.getVolunteer(v.ID)
|
||||
if !got.IsLead {
|
||||
t.Error("expected is_lead=true")
|
||||
}
|
||||
if !got.Confirmed {
|
||||
t.Error("co-lead should be auto-confirmed")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue