Added volunteer signup.
This commit is contained in:
parent
ace7f11a60
commit
8dc5d3ed01
12 changed files with 1258 additions and 49 deletions
247
handle_signup.go
Normal file
247
handle_signup.go
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (app *App) handlePublicSignupConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var noteLabel, noteRequired string
|
||||
app.db.QueryRow(`SELECT value FROM config WHERE key = 'volunteer_note_label'`).Scan(¬eLabel)
|
||||
app.db.QueryRow(`SELECT value FROM config WHERE key = 'volunteer_note_required'`).Scan(¬eRequired)
|
||||
if noteLabel == "" {
|
||||
noteLabel = "Additional note"
|
||||
}
|
||||
|
||||
depts, _ := app.listDepartments("")
|
||||
deptList := []map[string]any{}
|
||||
for _, d := range depts {
|
||||
deptList = append(deptList, map[string]any{"id": d.ID, "name": d.Name, "color": d.Color})
|
||||
}
|
||||
|
||||
eventName := app.eventName()
|
||||
|
||||
writeJSON(w, map[string]any{
|
||||
"event_name": eventName,
|
||||
"departments": deptList,
|
||||
"volunteer_note_label": noteLabel,
|
||||
"volunteer_note_required": noteRequired == "true",
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) handlePublicSignup(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
PreferredName string `json:"preferred_name"`
|
||||
TicketName string `json:"ticket_name"`
|
||||
Email string `json:"email"`
|
||||
Pronouns string `json:"pronouns"`
|
||||
Phone string `json:"phone"`
|
||||
DepartmentID *int `json:"department_id"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
body.PreferredName = strings.TrimSpace(body.PreferredName)
|
||||
body.Email = strings.TrimSpace(body.Email)
|
||||
if body.PreferredName == "" || body.Email == "" {
|
||||
writeError(w, "preferred name and email are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var noteRequired string
|
||||
app.db.QueryRow(`SELECT value FROM config WHERE key = 'volunteer_note_required'`).Scan(¬eRequired)
|
||||
if noteRequired == "true" && strings.TrimSpace(body.Note) == "" {
|
||||
writeError(w, "note field is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Don't reveal whether email is already registered
|
||||
existing, _ := app.getVolunteerByEmail(body.Email)
|
||||
if existing != nil {
|
||||
writeJSON(w, map[string]any{"ok": true})
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-match attendee by email or create new
|
||||
var attendeeID *int
|
||||
attendees, _ := app.listAttendees("", "", "")
|
||||
for _, a := range attendees {
|
||||
if strings.EqualFold(a.Email, body.Email) {
|
||||
id := a.ID
|
||||
attendeeID = &id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if attendeeID == nil {
|
||||
name := body.PreferredName
|
||||
if body.TicketName != "" {
|
||||
name = body.TicketName
|
||||
}
|
||||
newAttendee, err := app.createAttendee(Attendee{
|
||||
Name: name,
|
||||
Email: body.Email,
|
||||
Phone: body.Phone,
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
attendeeID = &newAttendee.ID
|
||||
}
|
||||
|
||||
confirmToken, err := generateConfirmationToken()
|
||||
if err != nil {
|
||||
writeError(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vol := Volunteer{
|
||||
AttendeeID: attendeeID,
|
||||
Name: body.PreferredName,
|
||||
PreferredName: body.PreferredName,
|
||||
TicketName: body.TicketName,
|
||||
Email: body.Email,
|
||||
Phone: body.Phone,
|
||||
Pronouns: body.Pronouns,
|
||||
DepartmentID: body.DepartmentID,
|
||||
Note: body.Note,
|
||||
ConfirmationToken: &confirmToken,
|
||||
}
|
||||
|
||||
if _, err := app.createVolunteer(vol); err != nil {
|
||||
writeError(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := app.sendConfirmationEmail(body.Email, body.PreferredName, confirmToken); err != nil {
|
||||
log.Printf("confirmation email to %s failed: %v", body.Email, err)
|
||||
}
|
||||
}()
|
||||
|
||||
writeJSON(w, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func (app *App) handleConfirmEmail(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Token == "" {
|
||||
writeError(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
vol, err := app.getVolunteerByConfirmationToken(body.Token)
|
||||
if err != nil || vol == nil {
|
||||
writeJSON(w, map[string]any{"status": "invalid"})
|
||||
return
|
||||
}
|
||||
|
||||
if vol.EmailConfirmed {
|
||||
writeJSON(w, map[string]any{"status": "already_confirmed"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.confirmVolunteerEmail(vol.ID); err != nil {
|
||||
writeError(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]any{"status": "confirmed"}
|
||||
|
||||
var signupsOpen string
|
||||
app.db.QueryRow(`SELECT value FROM config WHERE key = 'shift_signups_open'`).Scan(&signupsOpen)
|
||||
|
||||
if signupsOpen == "true" && vol.AttendeeID != nil {
|
||||
a, _ := app.getAttendee(*vol.AttendeeID)
|
||||
if a != nil && a.VolunteerToken == nil {
|
||||
t, err := app.generateUniqueToken()
|
||||
if err == nil {
|
||||
app.db.Exec(`UPDATE attendees SET volunteer_token=?, updated_at=? WHERE id=?`, t, now(), a.ID)
|
||||
a.VolunteerToken = &t
|
||||
}
|
||||
}
|
||||
if a != nil && a.VolunteerToken != nil {
|
||||
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 {
|
||||
log.Printf("shift signup email to %s failed: %v", vol.Email, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, response)
|
||||
}
|
||||
|
||||
func (app *App) handleToggleShiftSignups(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Open bool `json:"open"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
val := "false"
|
||||
if body.Open {
|
||||
val = "true"
|
||||
}
|
||||
app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', ?)`, val)
|
||||
|
||||
if body.Open {
|
||||
go app.openShiftSignups()
|
||||
}
|
||||
writeJSON(w, map[string]any{"shift_signups_open": body.Open})
|
||||
}
|
||||
|
||||
func (app *App) openShiftSignups() {
|
||||
// Generate kiosk tokens for confirmed volunteers whose attendees lack one
|
||||
vols, _ := app.listConfirmedVolunteersWithoutKioskToken()
|
||||
for _, v := range vols {
|
||||
if v.AttendeeID == nil {
|
||||
continue
|
||||
}
|
||||
t, err := app.generateUniqueToken()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
app.db.Exec(`UPDATE attendees SET volunteer_token=?, updated_at=? WHERE id=?`, t, now(), *v.AttendeeID)
|
||||
}
|
||||
|
||||
// Email all confirmed volunteers with kiosk links
|
||||
confirmed, _ := queryVolunteers(app.db, `
|
||||
SELECT `+volunteerCols+`
|
||||
FROM volunteers
|
||||
WHERE email_confirmed = 1 AND deleted_at IS NULL AND attendee_id IS NOT NULL`)
|
||||
baseURL := app.resolveBaseURL()
|
||||
sent := 0
|
||||
|
||||
for _, v := range confirmed {
|
||||
if v.AttendeeID == nil || v.Email == "" {
|
||||
continue
|
||||
}
|
||||
a, _ := app.getAttendee(*v.AttendeeID)
|
||||
if a == nil || a.VolunteerToken == nil {
|
||||
continue
|
||||
}
|
||||
kioskLink := fmt.Sprintf("%s/#/v/%s", baseURL, *a.VolunteerToken)
|
||||
name := v.PreferredName
|
||||
if name == "" {
|
||||
name = v.Name
|
||||
}
|
||||
if err := app.sendShiftSignupEmail(v.Email, name, kioskLink); err == nil {
|
||||
sent++
|
||||
} else {
|
||||
log.Printf("shift signup email to %s failed: %v", v.Email, err)
|
||||
}
|
||||
}
|
||||
log.Printf("Shift signups opened: sent %d emails", sent)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue