Refactored ticket code into kiosk code.
This commit is contained in:
parent
72b245d6d6
commit
3eec81af7f
5 changed files with 110 additions and 190 deletions
72
db.go
72
db.go
|
|
@ -176,6 +176,21 @@ func migrateV2(db *sql.DB) error {
|
|||
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")
|
||||
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`)
|
||||
|
|
@ -398,6 +413,7 @@ type Volunteer struct {
|
|||
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"`
|
||||
|
|
@ -1189,7 +1205,7 @@ const volunteerSelect = `v.id, v.participant_id, v.attendee_id,
|
|||
COALESCE(NULLIF(p.pronouns,''), v.pronouns),
|
||||
v.department_id, v.is_lead, v.checked_in, v.checked_in_at,
|
||||
v.confirmed, v.confirmed_at,
|
||||
v.email_confirmed, v.confirmation_token, v.note,
|
||||
v.email_confirmed, v.confirmation_token, 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`
|
||||
|
||||
|
|
@ -1322,14 +1338,13 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) {
|
|||
var v Volunteer
|
||||
var participantID, attendeeID, deptID sql.NullInt64
|
||||
var isLead, checkedIn, confirmed, emailConfirmed int
|
||||
var confirmationToken sql.NullString
|
||||
var confirmedAt sql.NullString
|
||||
var confirmationToken, confirmedAt, kioskCode sql.NullString
|
||||
if err := rows.Scan(
|
||||
&v.ID, &participantID, &attendeeID, &v.Name, &v.PreferredName, &v.TicketName,
|
||||
&v.Email, &v.Phone, &v.Pronouns, &deptID,
|
||||
&isLead, &checkedIn, &v.CheckedInAt,
|
||||
&confirmed, &confirmedAt,
|
||||
&emailConfirmed, &confirmationToken, &v.Note,
|
||||
&emailConfirmed, &confirmationToken, &kioskCode, &v.Note,
|
||||
&v.CreatedAt, &v.UpdatedAt, &v.DeletedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -1352,6 +1367,9 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) {
|
|||
if confirmedAt.Valid {
|
||||
v.ConfirmedAt = &confirmedAt.String
|
||||
}
|
||||
if kioskCode.Valid {
|
||||
v.KioskCode = &kioskCode.String
|
||||
}
|
||||
v.IsLead = isLead == 1
|
||||
v.CheckedIn = checkedIn == 1
|
||||
v.Confirmed = confirmed == 1
|
||||
|
|
@ -1386,19 +1404,43 @@ func (app *App) confirmVolunteerEmail(id int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// listConfirmedVolunteersNeedingCode returns confirmed volunteers whose participant
|
||||
// has no ticket with a code yet.
|
||||
func (app *App) listConfirmedVolunteersNeedingCode() ([]Volunteer, error) {
|
||||
func (app *App) getVolunteerByKioskCode(code string) (*Volunteer, error) {
|
||||
rows, err := queryVolunteers(app.db,
|
||||
`SELECT `+volunteerSelect+` `+volunteerFrom+` WHERE v.kiosk_code = ? AND v.deleted_at IS NULL LIMIT 1`, code)
|
||||
if err != nil || len(rows) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return &rows[0], nil
|
||||
}
|
||||
|
||||
func (app *App) assignKioskCode(id int, code string) error {
|
||||
_, err := app.db.Exec(
|
||||
`UPDATE volunteers SET kiosk_code=?, updated_at=? WHERE id=?`, code, now(), id)
|
||||
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.deleted_at IS NULL
|
||||
AND v.participant_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM tickets t
|
||||
WHERE t.participant_id = v.participant_id
|
||||
AND t.code IS NOT NULL
|
||||
AND t.deleted_at IS NULL
|
||||
)`)
|
||||
WHERE v.email_confirmed = 1 AND v.kiosk_code IS NULL AND v.deleted_at IS NULL`)
|
||||
}
|
||||
|
||||
func (app *App) generateVolunteerKioskCode() (string, error) {
|
||||
for range 10 {
|
||||
t, err := generateToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var count int
|
||||
if err := app.db.QueryRow(`SELECT COUNT(*) FROM volunteers WHERE kiosk_code = ?`, t).Scan(&count); err != nil {
|
||||
return "", fmt.Errorf("check kiosk code uniqueness: %w", err)
|
||||
}
|
||||
if count == 0 {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to generate unique kiosk code")
|
||||
}
|
||||
|
||||
func generateConfirmationToken() (string, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue