Moved properties from Volunteer to Participant.

This commit is contained in:
Pen Anderson 2026-03-06 07:11:19 -06:00
parent fcf5bf1f34
commit 7d56ef2f33
9 changed files with 200 additions and 428 deletions

422
db.go
View file

@ -86,21 +86,24 @@ func migrate(db *sql.DB) error {
ON attendees(name, ticket_id) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS volunteers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
attendee_id INTEGER REFERENCES attendees(id) ON DELETE SET NULL,
name TEXT NOT NULL,
email TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
department_id INTEGER REFERENCES departments(id) ON DELETE SET NULL,
is_lead INTEGER NOT NULL DEFAULT 0,
ready INTEGER NOT NULL DEFAULT 0,
ready_at TEXT,
note TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
deleted_at TEXT
id INTEGER PRIMARY KEY AUTOINCREMENT,
participant_id INTEGER NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
department_id INTEGER REFERENCES departments(id) ON DELETE SET NULL,
is_lead INTEGER NOT NULL DEFAULT 0,
ready INTEGER NOT NULL DEFAULT 0,
ready_at TEXT,
confirmed INTEGER NOT NULL DEFAULT 0,
confirmed_at TEXT,
kiosk_code TEXT,
note TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
deleted_at TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_volunteers_kiosk_code
ON volunteers(kiosk_code) WHERE kiosk_code IS NOT NULL;
CREATE TABLE IF NOT EXISTS shifts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
department_id INTEGER NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
@ -119,19 +122,23 @@ func migrate(db *sql.DB) error {
shift_id INTEGER NOT NULL REFERENCES shifts(id) ON DELETE CASCADE,
confirmed INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
deleted_at TEXT,
PRIMARY KEY (volunteer_id, shift_id)
);
CREATE TABLE IF NOT EXISTS participants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL DEFAULT '',
preferred_name TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
pronouns TEXT NOT NULL DEFAULT '',
note TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
deleted_at TEXT
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL DEFAULT '',
preferred_name TEXT NOT NULL DEFAULT '',
ticket_name TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
pronouns TEXT NOT NULL DEFAULT '',
note TEXT NOT NULL DEFAULT '',
email_confirmed INTEGER NOT NULL DEFAULT 0,
confirmation_token TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
deleted_at TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_participants_email
@ -159,210 +166,9 @@ func migrate(db *sql.DB) error {
if err != nil {
return err
}
return migrateV2(db)
}
// migrateV2 adds new columns to existing databases without data loss.
func migrateV2(db *sql.DB) error {
addColumnIfMissing(db, "attendees", "volunteer_token TEXT UNIQUE")
addColumnIfMissing(db, "attendees", "party_size INTEGER NOT NULL DEFAULT 1")
addColumnIfMissing(db, "attendees", "checked_in_count INTEGER NOT NULL DEFAULT 0")
addColumnIfMissing(db, "shifts", "position INTEGER NOT NULL DEFAULT 0")
addColumnIfMissing(db, "volunteer_shifts", "deleted_at TEXT")
addColumnIfMissing(db, "volunteers", "preferred_name TEXT NOT NULL DEFAULT ''")
addColumnIfMissing(db, "volunteers", "ticket_name TEXT NOT NULL DEFAULT ''")
addColumnIfMissing(db, "volunteers", "pronouns TEXT NOT NULL DEFAULT ''")
addColumnIfMissing(db, "volunteers", "email_confirmed INTEGER NOT NULL DEFAULT 0")
addColumnIfMissing(db, "volunteers", "confirmation_token TEXT")
addColumnIfMissing(db, "volunteers", "confirmed INTEGER NOT NULL DEFAULT 0")
addColumnIfMissing(db, "volunteers", "confirmed_at TEXT")
addColumnIfMissing(db, "volunteers", "kiosk_code TEXT")
renameColumnIfExists(db, "volunteers", "checked_in", "ready")
renameColumnIfExists(db, "volunteers", "checked_in_at", "ready_at")
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_volunteers_kiosk_code ON volunteers(kiosk_code) WHERE kiosk_code IS NOT NULL`)
// Migrate kiosk codes from tickets to volunteers (idempotent).
db.Exec(`
UPDATE volunteers SET kiosk_code = (
SELECT t.code FROM tickets t
WHERE t.participant_id = volunteers.participant_id
AND t.code IS NOT NULL AND t.deleted_at IS NULL
LIMIT 1
) WHERE kiosk_code IS NULL AND participant_id IS NOT NULL`)
// Delete stub tickets whose code has been migrated to the volunteer.
db.Exec(`
DELETE FROM tickets
WHERE source = 'manual' AND external_id = '' AND code IS NOT NULL
AND participant_id IN (SELECT id FROM volunteers WHERE kiosk_code IS NOT NULL)`)
// Widen the uniqueness constraint from name-only to (name, ticket_id).
db.Exec(`DROP INDEX IF EXISTS idx_attendees_name`)
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_attendees_name_ticket ON attendees(name, ticket_id) WHERE deleted_at IS NULL`)
return migrateV3(db)
}
// migrateV3 populates participants + tickets from attendees/volunteers,
// and links volunteers to participants via participant_id.
func migrateV3(db *sql.DB) error {
addColumnIfMissing(db, "volunteers", "participant_id INTEGER REFERENCES participants(id)")
addColumnIfMissing(db, "participants", "ticket_name TEXT NOT NULL DEFAULT ''")
// Seed participants from volunteers first (better name data: preferred_name).
db.Exec(`
INSERT OR IGNORE INTO participants (email, preferred_name, phone, pronouns, created_at, updated_at)
SELECT
LOWER(email),
CASE WHEN preferred_name != '' THEN preferred_name ELSE name END,
phone,
pronouns,
created_at,
created_at
FROM volunteers
WHERE email != '' AND deleted_at IS NULL`)
// Fill in from attendees for emails not yet in participants.
db.Exec(`
INSERT OR IGNORE INTO participants (email, preferred_name, phone, created_at, updated_at)
SELECT LOWER(email), name, phone, created_at, created_at
FROM attendees
WHERE email != '' AND deleted_at IS NULL`)
// Attendees with no email: create a placeholder participant so tickets aren't orphaned.
rows, _ := db.Query(`SELECT id, name, created_at FROM attendees WHERE email = '' AND deleted_at IS NULL`)
if rows != nil {
type stub struct {
id, name, createdAt string
}
var stubs []stub
for rows.Next() {
var s stub
rows.Scan(&s.id, &s.name, &s.createdAt)
stubs = append(stubs, s)
}
rows.Close()
for _, s := range stubs {
placeholder := fmt.Sprintf("ticket-%s@unknown", s.id)
db.Exec(`INSERT OR IGNORE INTO participants (email, preferred_name, created_at, updated_at) VALUES (?, ?, ?, ?)`,
placeholder, s.name, s.createdAt, s.createdAt)
}
}
// Link volunteers to participants via email.
db.Exec(`
UPDATE volunteers SET participant_id = (
SELECT p.id FROM participants p WHERE LOWER(p.email) = LOWER(volunteers.email)
)
WHERE participant_id IS NULL AND email != ''`)
// Seed tickets from attendees (1 ticket per attendee row).
db.Exec(`
INSERT OR IGNORE INTO tickets (participant_id, name, ticket_type, source, external_id, order_id, code, checked_in_at, checked_in_by, created_at, updated_at, deleted_at)
SELECT
p.id,
a.name,
a.ticket_type,
CASE WHEN a.ticket_id != '' THEN 'crowdwork' ELSE 'manual' END,
a.ticket_id,
a.ticket_id,
a.volunteer_token,
a.checked_in_at,
a.checked_in_by,
a.created_at,
a.updated_at,
a.deleted_at
FROM attendees a
JOIN participants p ON LOWER(p.email) = LOWER(a.email) OR p.email = 'ticket-' || a.id || '@unknown'`)
// Volunteers whose participant has no ticket: create a stub ticket so they can get a kiosk code.
db.Exec(`
INSERT OR IGNORE INTO tickets (participant_id, source, created_at, updated_at)
SELECT DISTINCT v.participant_id, 'manual', v.created_at, v.created_at
FROM volunteers v
WHERE v.participant_id IS NOT NULL
AND v.deleted_at IS NULL
AND NOT EXISTS (SELECT 1 FROM tickets t WHERE t.participant_id = v.participant_id AND t.deleted_at IS NULL)`)
return migrateV4(db)
}
// migrateV4 renames roles: volunteer_lead→colead, coordinator→staffing, gate→gatekeeper.
func migrateV4(db *sql.DB) error {
var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE role IN ('volunteer_lead','coordinator','gate')`).Scan(&count); err != nil || count == 0 {
return nil
}
if _, err := db.Exec(`PRAGMA foreign_keys = OFF`); err != nil {
return err
}
stmts := []string{
`CREATE TABLE users_v4 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin','ticketing','staffing','colead','gatekeeper')),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)`,
`INSERT INTO users_v4 (id, username, password_hash, role, created_at)
SELECT id, username, password_hash,
CASE role
WHEN 'volunteer_lead' THEN 'colead'
WHEN 'coordinator' THEN 'staffing'
WHEN 'gate' THEN 'gatekeeper'
ELSE role
END,
created_at
FROM users`,
`DROP TABLE users`,
`ALTER TABLE users_v4 RENAME TO users`,
`PRAGMA foreign_keys = ON`,
}
for _, s := range stmts {
if _, err := db.Exec(s); err != nil {
db.Exec(`PRAGMA foreign_keys = ON`)
return fmt.Errorf("migrateV4: %w", err)
}
}
return nil
}
func addColumnIfMissing(db *sql.DB, table, colDef string) {
colName := strings.Fields(colDef)[0]
rows, err := db.Query(`PRAGMA table_info("` + table + `")`)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var cid, notNull, pk int
var name, typ string
var dflt sql.NullString
rows.Scan(&cid, &name, &typ, &notNull, &dflt, &pk)
if name == colName {
return
}
}
db.Exec(`ALTER TABLE "` + table + `" ADD COLUMN ` + colDef)
}
func renameColumnIfExists(db *sql.DB, table, oldName, newName string) {
found := false
rows, err := db.Query(`PRAGMA table_info("` + table + `")`)
if err != nil {
return
}
for rows.Next() {
var cid, notNull, pk int
var name, typ string
var dflt sql.NullString
rows.Scan(&cid, &name, &typ, &notNull, &dflt, &pk)
if name == oldName {
found = true
}
}
rows.Close()
if found {
db.Exec(`ALTER TABLE "` + table + `" RENAME COLUMN "` + oldName + `" TO "` + newName + `"`)
}
}
// --- Types ---
const attendeeCols = `id, name, email, phone, ticket_id, ticket_type, volunteer_token,
@ -420,40 +226,40 @@ type Department struct {
}
type Volunteer struct {
ID int `json:"id"`
ParticipantID *int `json:"participant_id,omitempty"`
AttendeeID *int `json:"attendee_id,omitempty"` // deprecated; kept for migration compat
Name string `json:"name"`
PreferredName string `json:"preferred_name"`
Email string `json:"email"`
Phone string `json:"phone"`
Pronouns string `json:"pronouns"`
DepartmentID *int `json:"department_id,omitempty"`
IsLead bool `json:"is_lead"`
Ready bool `json:"ready"`
ReadyAt *string `json:"ready_at,omitempty"`
Confirmed bool `json:"confirmed"`
ConfirmedAt *string `json:"confirmed_at,omitempty"`
EmailConfirmed bool `json:"email_confirmed"`
ConfirmationToken *string `json:"-"`
KioskCode *string `json:"kiosk_code,omitempty"`
Note string `json:"note"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
DeletedAt *string `json:"deleted_at,omitempty"`
}
type Participant struct {
ID int `json:"id"`
Email string `json:"email"`
PreferredName string `json:"preferred_name"`
TicketName string `json:"ticket_name"`
Phone string `json:"phone"`
Pronouns string `json:"pronouns"`
ParticipantID int `json:"participant_id"`
DepartmentID *int `json:"department_id,omitempty"`
IsLead bool `json:"is_lead"`
Ready bool `json:"ready"`
ReadyAt *string `json:"ready_at,omitempty"`
Confirmed bool `json:"confirmed"`
ConfirmedAt *string `json:"confirmed_at,omitempty"`
KioskCode *string `json:"kiosk_code,omitempty"`
Note string `json:"note"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
DeletedAt *string `json:"deleted_at,omitempty"`
// Populated via JOIN from participant, not stored on volunteers table:
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone"`
Pronouns string `json:"pronouns"`
EmailConfirmed bool `json:"email_confirmed"`
}
type Participant struct {
ID int `json:"id"`
Email string `json:"email"`
PreferredName string `json:"preferred_name"`
TicketName string `json:"ticket_name"`
Phone string `json:"phone"`
Pronouns string `json:"pronouns"`
Note string `json:"note"`
EmailConfirmed bool `json:"email_confirmed"`
ConfirmationToken *string `json:"-"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
DeletedAt *string `json:"deleted_at,omitempty"`
}
type Ticket struct {
@ -884,7 +690,7 @@ func (app *App) attendeeCounts() (total, checkedIn int, err error) {
// --- Participants ---
const participantCols = `id, email, preferred_name, ticket_name, phone, pronouns, note, created_at, updated_at, deleted_at`
const participantCols = `id, email, preferred_name, ticket_name, phone, pronouns, note, email_confirmed, confirmation_token, created_at, updated_at, deleted_at`
func (app *App) listParticipants(search, since string) ([]Participant, error) {
var q string
@ -924,8 +730,8 @@ func (app *App) getParticipantByEmail(email string) (*Participant, error) {
func (app *App) createParticipant(p Participant) (*Participant, error) {
res, err := app.db.Exec(
`INSERT INTO participants (email, preferred_name, ticket_name, phone, pronouns, note, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
strings.ToLower(p.Email), p.PreferredName, p.TicketName, p.Phone, p.Pronouns, p.Note, now(),
`INSERT INTO participants (email, preferred_name, ticket_name, phone, pronouns, note, email_confirmed, confirmation_token, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
strings.ToLower(p.Email), p.PreferredName, p.TicketName, p.Phone, p.Pronouns, p.Note, boolInt(p.EmailConfirmed), p.ConfirmationToken, now(),
)
if err != nil {
return nil, err
@ -980,12 +786,19 @@ func queryParticipants(db *sql.DB, q string, args ...any) ([]Participant, error)
var result []Participant
for rows.Next() {
var p Participant
var emailConfirmed int
var confirmationToken sql.NullString
if err := rows.Scan(
&p.ID, &p.Email, &p.PreferredName, &p.TicketName, &p.Phone, &p.Pronouns, &p.Note,
&emailConfirmed, &confirmationToken,
&p.CreatedAt, &p.UpdatedAt, &p.DeletedAt,
); err != nil {
return nil, err
}
p.EmailConfirmed = emailConfirmed == 1
if confirmationToken.Valid {
p.ConfirmationToken = &confirmationToken.String
}
result = append(result, p)
}
return result, rows.Err()
@ -1217,23 +1030,13 @@ func queryDepartments(db *sql.DB, q string, args ...any) ([]Department, error) {
// --- Volunteers ---
// volunteerSelect / volunteerFrom are used together for all volunteer queries.
// Personal fields (name, email, phone, pronouns) come from the joined participant when available,
// falling back to the volunteer's own columns for legacy rows.
const volunteerSelect = `v.id, v.participant_id, v.attendee_id,
COALESCE(NULLIF(p.preferred_name,''), NULLIF(v.preferred_name,''), v.name),
COALESCE(NULLIF(p.preferred_name,''), NULLIF(v.preferred_name,''), v.name),
COALESCE(NULLIF(p.email,''), v.email),
COALESCE(NULLIF(p.phone,''), v.phone),
COALESCE(NULLIF(p.pronouns,''), v.pronouns),
const volunteerSelect = `v.id, v.participant_id,
p.preferred_name, p.email, p.phone, p.pronouns,
v.department_id, v.is_lead, v.ready, v.ready_at,
v.confirmed, v.confirmed_at,
v.email_confirmed, v.confirmation_token, v.kiosk_code, v.note,
p.email_confirmed, v.kiosk_code, v.note,
v.created_at, v.updated_at, v.deleted_at`
const volunteerFrom = `FROM volunteers v LEFT JOIN participants p ON p.id = v.participant_id`
// volunteerCols is kept for backward-compat references that expect unqualified column names.
const volunteerCols = `id, attendee_id, name, preferred_name, email, phone, pronouns, department_id, is_lead, ready, ready_at, email_confirmed, confirmation_token, note, created_at, updated_at, deleted_at`
const volunteerFrom = `FROM volunteers v INNER JOIN participants p ON p.id = v.participant_id`
func (app *App) listVolunteers(search string, deptID *int, since string) ([]Volunteer, error) {
q := `SELECT ` + volunteerSelect + ` ` + volunteerFrom + ` WHERE 1=1`
@ -1245,15 +1048,15 @@ func (app *App) listVolunteers(search string, deptID *int, since string) ([]Volu
q += ` AND v.deleted_at IS NULL`
}
if search != "" {
q += ` AND (v.name LIKE ? OR v.email LIKE ? OR p.preferred_name LIKE ? OR p.email LIKE ?)`
q += ` AND (p.preferred_name LIKE ? OR p.email LIKE ?)`
s := "%" + search + "%"
args = append(args, s, s, s, s)
args = append(args, s, s)
}
if deptID != nil {
q += ` AND v.department_id = ?`
args = append(args, *deptID)
}
q += ` ORDER BY COALESCE(NULLIF(p.preferred_name,''), v.name)`
q += ` ORDER BY p.preferred_name`
return queryVolunteers(app.db, q, args...)
}
@ -1266,15 +1069,6 @@ func (app *App) getVolunteer(id int) (*Volunteer, error) {
return &rows[0], nil
}
func (app *App) getVolunteerByAttendeeID(attendeeID int) (*Volunteer, error) {
rows, err := queryVolunteers(app.db,
`SELECT `+volunteerSelect+` `+volunteerFrom+` WHERE v.attendee_id = ? AND v.deleted_at IS NULL LIMIT 1`, attendeeID)
if err != nil || len(rows) == 0 {
return nil, err
}
return &rows[0], nil
}
func (app *App) getVolunteerByParticipantID(participantID int) (*Volunteer, error) {
rows, err := queryVolunteers(app.db,
`SELECT `+volunteerSelect+` `+volunteerFrom+` WHERE v.participant_id = ? AND v.deleted_at IS NULL LIMIT 1`, participantID)
@ -1286,10 +1080,9 @@ func (app *App) getVolunteerByParticipantID(participantID int) (*Volunteer, erro
func (app *App) createVolunteer(v Volunteer) (*Volunteer, error) {
res, err := app.db.Exec(
`INSERT INTO volunteers (participant_id, attendee_id, name, preferred_name, email, phone, pronouns, department_id, is_lead, email_confirmed, confirmation_token, note, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
v.ParticipantID, v.AttendeeID, v.Name, v.PreferredName, v.Email, v.Phone, v.Pronouns,
v.DepartmentID, boolInt(v.IsLead), boolInt(v.EmailConfirmed), v.ConfirmationToken, v.Note, now(),
`INSERT INTO volunteers (participant_id, department_id, is_lead, note, updated_at)
VALUES (?, ?, ?, ?, ?)`,
v.ParticipantID, v.DepartmentID, boolInt(v.IsLead), v.Note, now(),
)
if err != nil {
return nil, err
@ -1300,9 +1093,8 @@ func (app *App) createVolunteer(v Volunteer) (*Volunteer, error) {
func (app *App) updateVolunteer(v Volunteer) error {
_, err := app.db.Exec(
`UPDATE volunteers SET participant_id=?, attendee_id=?, name=?, preferred_name=?, email=?, phone=?, pronouns=?, department_id=?, is_lead=?, note=?, updated_at=?
`UPDATE volunteers SET department_id=?, is_lead=?, note=?, updated_at=?
WHERE id=? AND deleted_at IS NULL`,
v.ParticipantID, v.AttendeeID, v.Name, v.PreferredName, v.Email, v.Phone, v.Pronouns,
v.DepartmentID, boolInt(v.IsLead), v.Note, now(), v.ID,
)
return err
@ -1315,8 +1107,6 @@ func (app *App) deleteVolunteer(id int) error {
return err
}
// markVolunteerReady marks the volunteer as ready and, if linked to an attendee,
// also increments the attendee's checked_in_count.
func (app *App) markVolunteerReady(id, userID int) (*Volunteer, error) {
t := now()
_, err := app.db.Exec(
@ -1327,14 +1117,7 @@ func (app *App) markVolunteerReady(id, userID int) (*Volunteer, error) {
if err != nil {
return nil, err
}
v, err := app.getVolunteer(id)
if err != nil || v == nil {
return v, err
}
if v.AttendeeID != nil {
app.checkInAttendee(*v.AttendeeID, userID, 1)
}
return v, nil
return app.getVolunteer(id)
}
func (app *App) confirmVolunteer(id int) (*Volunteer, error) {
@ -1359,34 +1142,23 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) {
var result []Volunteer
for rows.Next() {
var v Volunteer
var participantID, attendeeID, deptID sql.NullInt64
var deptID sql.NullInt64
var isLead, ready, confirmed, emailConfirmed int
var confirmationToken, confirmedAt, kioskCode sql.NullString
var confirmedAt, kioskCode sql.NullString
if err := rows.Scan(
&v.ID, &participantID, &attendeeID, &v.Name, &v.PreferredName,
&v.Email, &v.Phone, &v.Pronouns, &deptID,
&isLead, &ready, &v.ReadyAt,
&v.ID, &v.ParticipantID,
&v.Name, &v.Email, &v.Phone, &v.Pronouns,
&deptID, &isLead, &ready, &v.ReadyAt,
&confirmed, &confirmedAt,
&emailConfirmed, &confirmationToken, &kioskCode, &v.Note,
&emailConfirmed, &kioskCode, &v.Note,
&v.CreatedAt, &v.UpdatedAt, &v.DeletedAt,
); err != nil {
return nil, err
}
if participantID.Valid {
id := int(participantID.Int64)
v.ParticipantID = &id
}
if attendeeID.Valid {
id := int(attendeeID.Int64)
v.AttendeeID = &id
}
if deptID.Valid {
id := int(deptID.Int64)
v.DepartmentID = &id
}
if confirmationToken.Valid {
v.ConfirmationToken = &confirmationToken.String
}
if confirmedAt.Valid {
v.ConfirmedAt = &confirmedAt.String
}
@ -1404,7 +1176,7 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) {
func (app *App) getVolunteerByEmail(email string) (*Volunteer, error) {
rows, err := queryVolunteers(app.db,
`SELECT `+volunteerSelect+` `+volunteerFrom+` WHERE LOWER(v.email) = LOWER(?) AND v.deleted_at IS NULL LIMIT 1`, email)
`SELECT `+volunteerSelect+` `+volunteerFrom+` WHERE LOWER(p.email) = LOWER(?) AND v.deleted_at IS NULL LIMIT 1`, email)
if err != nil || len(rows) == 0 {
return nil, err
}
@ -1413,17 +1185,24 @@ func (app *App) getVolunteerByEmail(email string) (*Volunteer, error) {
func (app *App) getVolunteerByConfirmationToken(token string) (*Volunteer, error) {
rows, err := queryVolunteers(app.db,
`SELECT `+volunteerSelect+` `+volunteerFrom+` WHERE v.confirmation_token = ? AND v.deleted_at IS NULL LIMIT 1`, token)
`SELECT `+volunteerSelect+` `+volunteerFrom+` WHERE p.confirmation_token = ? AND v.deleted_at IS NULL LIMIT 1`, token)
if err != nil || len(rows) == 0 {
return nil, err
}
return &rows[0], nil
}
func (app *App) confirmVolunteerEmail(id int) error {
func (app *App) confirmParticipantEmail(participantID int) error {
_, err := app.db.Exec(
`UPDATE volunteers SET email_confirmed = 1, confirmation_token = NULL, updated_at = ? WHERE id = ?`,
now(), id)
`UPDATE participants SET email_confirmed = 1, confirmation_token = NULL, updated_at = ? WHERE id = ?`,
now(), participantID)
return err
}
func (app *App) setParticipantConfirmationToken(participantID int, token string) error {
_, err := app.db.Exec(
`UPDATE participants SET confirmation_token = ?, updated_at = ? WHERE id = ?`,
token, now(), participantID)
return err
}
@ -1442,11 +1221,10 @@ func (app *App) assignKioskCode(id int, code string) error {
return err
}
// listVolunteersNeedingKioskCode returns email-confirmed volunteers without a kiosk code.
func (app *App) listVolunteersNeedingKioskCode() ([]Volunteer, error) {
return queryVolunteers(app.db, `
SELECT `+volunteerSelect+` `+volunteerFrom+`
WHERE v.email_confirmed = 1 AND v.kiosk_code IS NULL AND v.deleted_at IS NULL`)
WHERE p.email_confirmed = 1 AND v.kiosk_code IS NULL AND v.deleted_at IS NULL`)
}
func (app *App) generateVolunteerKioskCode() (string, error) {
@ -1658,7 +1436,7 @@ var errShiftFull = fmt.Errorf("shift is full")
func (app *App) unassignShift(volunteerID, shiftID int) error {
_, err := app.db.Exec(
`UPDATE volunteer_shifts SET deleted_at = ?, updated_at = ? WHERE volunteer_id=? AND shift_id=?`,
`UPDATE volunteer_shifts SET deleted_at=?, updated_at=? WHERE volunteer_id=? AND shift_id=?`,
now(), now(), volunteerID, shiftID,
)
return err