2026-03-04 10:53:42 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/csv"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (app *App) handleListParticipants(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
search := r.URL.Query().Get("search")
|
|
|
|
|
participants, err := app.listParticipants(search, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
total, checkedIn, _ := app.ticketCounts()
|
|
|
|
|
types, _ := app.ticketTypes()
|
|
|
|
|
writeJSON(w, map[string]any{
|
|
|
|
|
"participants": participants,
|
|
|
|
|
"total": total,
|
|
|
|
|
"checked_in": checkedIn,
|
|
|
|
|
"ticket_types": types,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleGetParticipant(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
p, err := app.getParticipant(id)
|
|
|
|
|
if err != nil || p == nil {
|
|
|
|
|
writeError(w, "not found", http.StatusNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
tickets, _ := app.listTickets(&id, "")
|
|
|
|
|
writeJSON(w, map[string]any{"participant": p, "tickets": tickets})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleCreateParticipant(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
var p Participant
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
|
|
|
|
writeError(w, "invalid request", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if p.PreferredName == "" && p.Email == "" {
|
|
|
|
|
writeError(w, "name or email is required", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
created, err := app.createParticipant(p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
|
writeJSON(w, created)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleUpdateParticipant(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var p Participant
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
|
|
|
|
writeError(w, "invalid request", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
p.ID = id
|
|
|
|
|
if err := app.updateParticipant(p); err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
updated, _ := app.getParticipant(id)
|
|
|
|
|
writeJSON(w, updated)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleDeleteParticipant(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := app.deleteParticipant(id); err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleMergeParticipants reassigns all tickets and volunteers from otherID to
|
|
|
|
|
// canonicalID, then soft-deletes the other participant.
|
|
|
|
|
func (app *App) handleMergeParticipants(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
otherID, err := strconv.Atoi(r.PathValue("other_id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid other_id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := app.mergeParticipants(id, otherID); err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
p, _ := app.getParticipant(id)
|
|
|
|
|
tickets, _ := app.listTickets(&id, "")
|
|
|
|
|
writeJSON(w, map[string]any{"participant": p, "tickets": tickets})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleExportParticipants(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
participants, err := app.listParticipants("", "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "text/csv")
|
|
|
|
|
w.Header().Set("Content-Disposition", `attachment; filename="participants.csv"`)
|
|
|
|
|
wr := csv.NewWriter(w)
|
|
|
|
|
wr.Write([]string{"id", "email", "preferred_name", "phone", "pronouns", "note"})
|
|
|
|
|
for _, p := range participants {
|
|
|
|
|
wr.Write([]string{
|
|
|
|
|
strconv.Itoa(p.ID), p.Email, p.PreferredName, p.Phone, p.Pronouns, p.Note,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
wr.Flush()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 14:19:51 -06:00
|
|
|
func (app *App) handleCreateTicket(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
var t Ticket
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
|
|
|
|
|
writeError(w, "invalid request", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if t.ParticipantID == nil {
|
|
|
|
|
writeError(w, "participant_id is required", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if t.Source == "" {
|
|
|
|
|
t.Source = "manual"
|
|
|
|
|
}
|
|
|
|
|
created, err := app.createTicket(t)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
|
writeJSON(w, created)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 10:53:42 -06:00
|
|
|
func (app *App) handleListTickets(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
tickets, err := app.listTickets(nil, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, map[string]any{"tickets": tickets})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleCheckInTicket(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
claims := claimsFromContext(r)
|
2026-03-10 14:08:00 -05:00
|
|
|
tk, err := app.checkInTicket(id, claims.ParticipantID)
|
2026-03-04 10:53:42 -06:00
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
app.broker.publish("checkin", map[string]any{"type": "ticket", "ticket": tk})
|
|
|
|
|
writeJSON(w, map[string]any{"ticket": tk})
|
|
|
|
|
}
|