2026-03-03 11:27:07 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/csv"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (app *App) handleListAttendees(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
q := r.URL.Query()
|
|
|
|
|
attendees, err := app.listAttendees(q.Get("search"), q.Get("ticket_type"), q.Get("checked_in"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
types, _ := app.attendeeTicketTypes()
|
|
|
|
|
total, checkedIn, _ := app.attendeeCounts()
|
|
|
|
|
writeJSON(w, map[string]any{
|
|
|
|
|
"attendees": attendees,
|
|
|
|
|
"ticket_types": types,
|
|
|
|
|
"total": total,
|
|
|
|
|
"checked_in": checkedIn,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleCreateAttendee(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
var a Attendee
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
|
|
|
|
|
writeError(w, "invalid request", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if a.Name == "" {
|
|
|
|
|
writeError(w, "name is required", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
created, err := app.createAttendee(a)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
|
writeJSON(w, created)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleGetAttendee(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
a, err := app.getAttendee(id)
|
|
|
|
|
if err != nil || a == nil {
|
|
|
|
|
writeError(w, "not found", http.StatusNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, a)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleUpdateAttendee(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var a Attendee
|
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
|
|
|
|
|
writeError(w, "invalid request", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if a.Name == "" {
|
|
|
|
|
writeError(w, "name is required", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
a.ID = id
|
|
|
|
|
if err := app.updateAttendee(a); err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
updated, _ := app.getAttendee(id)
|
|
|
|
|
writeJSON(w, updated)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleDeleteAttendee(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.deleteAttendee(id); err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleCheckInAttendee handles POST /api/attendees/:id/checkin.
|
|
|
|
|
// Optional body: {"count": N, "also_volunteer": true}
|
|
|
|
|
// Returns {"attendee": ..., "volunteer": ...} — volunteer is included if also_volunteer=true
|
|
|
|
|
// and the attendee has a linked volunteer record.
|
|
|
|
|
func (app *App) handleCheckInAttendee(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
id, err := strconv.Atoi(r.PathValue("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, "invalid id", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var body struct {
|
|
|
|
|
Count int `json:"count"`
|
|
|
|
|
AlsoVolunteer bool `json:"also_volunteer"`
|
|
|
|
|
}
|
|
|
|
|
body.Count = 1
|
|
|
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
|
|
|
if body.Count < 1 {
|
|
|
|
|
body.Count = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
claims := claimsFromContext(r)
|
|
|
|
|
a, err := app.checkInAttendee(id, claims.UserID, body.Count)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := map[string]any{"attendee": a}
|
|
|
|
|
|
|
|
|
|
if body.AlsoVolunteer {
|
2026-03-04 10:53:42 -06:00
|
|
|
// Try to find volunteer via participant_id first (new model), fall back to attendee_id (legacy).
|
|
|
|
|
var v *Volunteer
|
|
|
|
|
if a != nil {
|
|
|
|
|
p, _ := app.getParticipantByEmail(a.Email)
|
|
|
|
|
if p != nil {
|
|
|
|
|
v, _ = app.getVolunteerByParticipantID(p.ID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if v == nil {
|
|
|
|
|
v, _ = app.getVolunteerByAttendeeID(id)
|
|
|
|
|
}
|
2026-03-03 11:27:07 -06:00
|
|
|
if v != nil {
|
|
|
|
|
if !v.CheckedIn {
|
|
|
|
|
if v2, err := app.checkInVolunteer(v.ID, claims.UserID); err == nil {
|
|
|
|
|
result["volunteer"] = v2
|
|
|
|
|
app.broker.publish("checkin", map[string]any{"type": "volunteer", "volunteer": v2})
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
result["volunteer"] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app.broker.publish("checkin", map[string]any{"type": "attendee", "attendee": a})
|
|
|
|
|
writeJSON(w, result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleExportAttendees(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
attendees, err := app.listAttendees("", "", "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "text/csv")
|
|
|
|
|
w.Header().Set("Content-Disposition", `attachment; filename="attendees.csv"`)
|
|
|
|
|
wr := csv.NewWriter(w)
|
|
|
|
|
wr.Write([]string{"name", "email", "phone", "ticket_id", "ticket_type", "party_size", "checked_in_count", "note", "checked_in"})
|
|
|
|
|
for _, a := range attendees {
|
|
|
|
|
ci := "no"
|
|
|
|
|
if a.CheckedIn {
|
|
|
|
|
ci = "yes"
|
|
|
|
|
}
|
|
|
|
|
wr.Write([]string{
|
|
|
|
|
a.Name, a.Email, a.Phone, a.TicketID, a.TicketType,
|
|
|
|
|
strconv.Itoa(a.PartySize), strconv.Itoa(a.CheckedInCount),
|
|
|
|
|
a.Note, ci,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
wr.Flush()
|
|
|
|
|
}
|