Established Participants and Tickets model. Migrated concepts.

This commit is contained in:
Pen Anderson 2026-03-04 10:53:42 -06:00
parent 0df93e1886
commit cd8e1e3b3b
22 changed files with 1345 additions and 191 deletions

View file

@ -68,32 +68,23 @@ func (app *App) handlePublicSignup(w http.ResponseWriter, r *http.Request) {
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
}
// Find or create participant by email.
name := body.PreferredName
if body.TicketName != "" {
name = body.TicketName
}
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
participant, _, err := app.upsertParticipant(body.Email, name)
if err != nil {
writeError(w, "internal error", http.StatusInternalServerError)
return
}
// Update participant's personal details if they signed up with more info.
if body.Phone != "" || body.Pronouns != "" {
app.db.Exec(`UPDATE participants SET
phone = CASE WHEN phone = '' THEN ? ELSE phone END,
pronouns = CASE WHEN pronouns = '' THEN ? ELSE pronouns END,
updated_at = ?
WHERE id = ?`, body.Phone, body.Pronouns, now(), participant.ID)
}
confirmToken, err := generateConfirmationToken()
@ -103,7 +94,7 @@ func (app *App) handlePublicSignup(w http.ResponseWriter, r *http.Request) {
}
vol := Volunteer{
AttendeeID: attendeeID,
ParticipantID: &participant.ID,
Name: body.PreferredName,
PreferredName: body.PreferredName,
TicketName: body.TicketName,
@ -159,17 +150,39 @@ func (app *App) handleConfirmEmail(w http.ResponseWriter, r *http.Request) {
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 signupsOpen == "true" && vol.ParticipantID != nil {
// Find a ticket with a code, or create/assign one.
tickets, _ := app.listTickets(vol.ParticipantID, "")
var code *string
for _, tk := range tickets {
if tk.Code != nil {
code = tk.Code
break
}
}
if a != nil && a.VolunteerToken != nil {
kioskLink := fmt.Sprintf("%s/v/%s", app.resolveBaseURL(), *a.VolunteerToken)
if code == nil {
// No coded ticket — find any ticket or create a stub, then generate code.
var ticketID int
if len(tickets) > 0 {
ticketID = tickets[0].ID
} else {
stub, err := app.createTicket(Ticket{
ParticipantID: vol.ParticipantID,
Source: "manual",
})
if err == nil {
ticketID = stub.ID
}
}
if ticketID > 0 {
if t, err := app.generateUniqueToken(); err == nil {
app.db.Exec(`UPDATE tickets SET code=?, updated_at=? WHERE id=?`, t, now(), ticketID)
code = &t
}
}
}
if code != nil {
kioskLink := fmt.Sprintf("%s/v/%s", app.resolveBaseURL(), *code)
response["kiosk_link"] = kioskLink
go func() {
if err := app.sendShiftSignupEmail(vol.Email, vol.PreferredName, kioskLink); err != nil {
@ -203,36 +216,57 @@ func (app *App) handleToggleShiftSignups(w http.ResponseWriter, r *http.Request)
}
func (app *App) openShiftSignups() {
// Generate kiosk tokens for confirmed volunteers whose attendees lack one
vols, _ := app.listConfirmedVolunteersWithoutKioskToken()
// Generate codes for tickets belonging to confirmed volunteers that have no code yet.
vols, _ := app.listConfirmedVolunteersNeedingCode()
for _, v := range vols {
if v.AttendeeID == nil {
if v.ParticipantID == nil {
continue
}
// Find any ticket for this participant, or create a stub one.
tickets, _ := app.listTickets(v.ParticipantID, "")
var ticketID int
if len(tickets) > 0 {
ticketID = tickets[0].ID
} else {
stub, err := app.createTicket(Ticket{
ParticipantID: v.ParticipantID,
Source: "manual",
})
if err != nil {
continue
}
ticketID = stub.ID
}
t, err := app.generateUniqueToken()
if err != nil {
continue
}
app.db.Exec(`UPDATE attendees SET volunteer_token=?, updated_at=? WHERE id=?`, t, now(), *v.AttendeeID)
app.db.Exec(`UPDATE tickets SET code=?, updated_at=? WHERE id=?`, t, now(), ticketID)
}
// Email all confirmed volunteers with kiosk links
// Email all confirmed volunteers that now have a ticket with a code.
confirmed, _ := queryVolunteers(app.db, `
SELECT `+volunteerCols+`
FROM volunteers
WHERE email_confirmed = 1 AND deleted_at IS NULL AND attendee_id IS NOT NULL`)
SELECT `+volunteerSelect+` `+volunteerFrom+`
WHERE v.email_confirmed = 1 AND v.deleted_at IS NULL AND v.participant_id IS NOT NULL`)
baseURL := app.resolveBaseURL()
sent := 0
for _, v := range confirmed {
if v.AttendeeID == nil || v.Email == "" {
if v.ParticipantID == nil || v.Email == "" {
continue
}
a, _ := app.getAttendee(*v.AttendeeID)
if a == nil || a.VolunteerToken == nil {
tickets, _ := app.listTickets(v.ParticipantID, "")
var code *string
for _, tk := range tickets {
if tk.Code != nil {
code = tk.Code
break
}
}
if code == nil {
continue
}
kioskLink := fmt.Sprintf("%s/v/%s", baseURL, *a.VolunteerToken)
kioskLink := fmt.Sprintf("%s/v/%s", baseURL, *code)
name := v.PreferredName
if name == "" {
name = v.Name