2026-03-03 11:27:07 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/csv"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-04 10:53:42 -06:00
|
|
|
// handleGenerateTokens creates codes for all tickets that don't have one.
|
2026-03-03 11:27:07 -06:00
|
|
|
func (app *App) handleGenerateTokens(w http.ResponseWriter, r *http.Request) {
|
2026-03-04 10:53:42 -06:00
|
|
|
count, err := app.generateCodesForAll()
|
2026-03-03 11:27:07 -06:00
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, map[string]any{"generated": count})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleExportTokenLinks streams a CSV download with token signup links,
|
|
|
|
|
// compatible with MailChimp / Zeffy bulk-send workflows.
|
|
|
|
|
func (app *App) handleExportTokenLinks(w http.ResponseWriter, r *http.Request) {
|
2026-03-04 10:53:42 -06:00
|
|
|
tickets, err := app.listTickets(nil, "")
|
2026-03-03 11:27:07 -06:00
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
baseURL := app.baseURL
|
|
|
|
|
if baseURL == "" {
|
|
|
|
|
app.db.QueryRow(`SELECT value FROM config WHERE key = 'base_url'`).Scan(&baseURL)
|
|
|
|
|
}
|
|
|
|
|
baseURL = strings.TrimRight(baseURL, "/")
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "text/csv")
|
|
|
|
|
w.Header().Set("Content-Disposition", `attachment; filename="volunteer-tokens.csv"`)
|
|
|
|
|
wr := csv.NewWriter(w)
|
|
|
|
|
wr.Write([]string{"Email Address", "First Name", "Token", "Signup Link"})
|
2026-03-04 10:53:42 -06:00
|
|
|
for _, tk := range tickets {
|
|
|
|
|
if tk.Code == nil || tk.ParticipantID == nil {
|
2026-03-03 11:27:07 -06:00
|
|
|
continue
|
|
|
|
|
}
|
2026-03-04 10:53:42 -06:00
|
|
|
p, _ := app.getParticipant(*tk.ParticipantID)
|
|
|
|
|
if p == nil || p.Email == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
firstName := p.PreferredName
|
|
|
|
|
if firstName == "" {
|
|
|
|
|
firstName = tk.Name
|
|
|
|
|
}
|
|
|
|
|
if parts := strings.Fields(firstName); len(parts) > 0 {
|
2026-03-03 11:27:07 -06:00
|
|
|
firstName = parts[0]
|
|
|
|
|
}
|
2026-03-04 10:53:42 -06:00
|
|
|
link := fmt.Sprintf("%s/v/%s", baseURL, *tk.Code)
|
|
|
|
|
wr.Write([]string{p.Email, firstName, *tk.Code, link})
|
2026-03-03 11:27:07 -06:00
|
|
|
}
|
|
|
|
|
wr.Flush()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 10:53:42 -06:00
|
|
|
// handleEmailToken sends a token email to a single ticket's participant.
|
2026-03-03 11:27:07 -06:00
|
|
|
func (app *App) handleEmailToken(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-04 10:53:42 -06:00
|
|
|
tk, err := app.getTicket(id)
|
|
|
|
|
if err != nil || tk == nil {
|
2026-03-03 11:27:07 -06:00
|
|
|
writeError(w, "not found", http.StatusNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-04 10:53:42 -06:00
|
|
|
if err := app.sendTicketTokenEmail(*tk); err != nil {
|
2026-03-03 11:27:07 -06:00
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, map[string]any{"ok": true})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 10:53:42 -06:00
|
|
|
// handleEmailAllTokens bulk-sends token emails to all tickets that have a code and participant email.
|
2026-03-03 11:27:07 -06:00
|
|
|
func (app *App) handleEmailAllTokens(w http.ResponseWriter, r *http.Request) {
|
2026-03-04 10:53:42 -06:00
|
|
|
tickets, err := app.listTickets(nil, "")
|
2026-03-03 11:27:07 -06:00
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var sent, skipped int
|
|
|
|
|
var errors []string
|
2026-03-04 10:53:42 -06:00
|
|
|
for _, tk := range tickets {
|
|
|
|
|
if tk.Code == nil || tk.ParticipantID == nil {
|
2026-03-03 11:27:07 -06:00
|
|
|
skipped++
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-03-04 10:53:42 -06:00
|
|
|
if err := app.sendTicketTokenEmail(tk); err != nil {
|
|
|
|
|
errors = append(errors, fmt.Sprintf("ticket %d: %v", tk.ID, err))
|
2026-03-03 11:27:07 -06:00
|
|
|
skipped++
|
|
|
|
|
} else {
|
|
|
|
|
sent++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if errors == nil {
|
|
|
|
|
errors = []string{}
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, map[string]any{"sent": sent, "skipped": skipped, "errors": errors})
|
|
|
|
|
}
|