Added volunteer signup.
This commit is contained in:
parent
ace7f11a60
commit
8dc5d3ed01
12 changed files with 1258 additions and 49 deletions
105
db.go
105
db.go
|
|
@ -135,6 +135,11 @@ func migrateV2(db *sql.DB) error {
|
|||
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")
|
||||
// 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`)
|
||||
|
|
@ -217,19 +222,24 @@ type Department struct {
|
|||
}
|
||||
|
||||
type Volunteer struct {
|
||||
ID int `json:"id"`
|
||||
AttendeeID *int `json:"attendee_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
DepartmentID *int `json:"department_id,omitempty"`
|
||||
IsLead bool `json:"is_lead"`
|
||||
CheckedIn bool `json:"checked_in"`
|
||||
CheckedInAt *string `json:"checked_in_at,omitempty"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
DeletedAt *string `json:"deleted_at,omitempty"`
|
||||
ID int `json:"id"`
|
||||
AttendeeID *int `json:"attendee_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
PreferredName string `json:"preferred_name"`
|
||||
TicketName string `json:"ticket_name"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Pronouns string `json:"pronouns"`
|
||||
DepartmentID *int `json:"department_id,omitempty"`
|
||||
IsLead bool `json:"is_lead"`
|
||||
CheckedIn bool `json:"checked_in"`
|
||||
CheckedInAt *string `json:"checked_in_at,omitempty"`
|
||||
EmailConfirmed bool `json:"email_confirmed"`
|
||||
ConfirmationToken *string `json:"-"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
DeletedAt *string `json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
type Shift struct {
|
||||
|
|
@ -719,7 +729,7 @@ func queryDepartments(db *sql.DB, q string, args ...any) ([]Department, error) {
|
|||
|
||||
// --- Volunteers ---
|
||||
|
||||
const volunteerCols = `id, attendee_id, name, email, phone, department_id, is_lead, checked_in, checked_in_at, note, created_at, updated_at, deleted_at`
|
||||
const volunteerCols = `id, attendee_id, name, preferred_name, ticket_name, email, phone, pronouns, department_id, is_lead, checked_in, checked_in_at, email_confirmed, confirmation_token, note, created_at, updated_at, deleted_at`
|
||||
|
||||
func (app *App) listVolunteers(search string, deptID *int, since string) ([]Volunteer, error) {
|
||||
q := `SELECT ` + volunteerCols + ` FROM volunteers WHERE 1=1`
|
||||
|
|
@ -763,9 +773,10 @@ func (app *App) getVolunteerByAttendeeID(attendeeID int) (*Volunteer, error) {
|
|||
|
||||
func (app *App) createVolunteer(v Volunteer) (*Volunteer, error) {
|
||||
res, err := app.db.Exec(
|
||||
`INSERT INTO volunteers (attendee_id, name, email, phone, department_id, is_lead, note, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
v.AttendeeID, v.Name, v.Email, v.Phone, v.DepartmentID, boolInt(v.IsLead), v.Note, now(),
|
||||
`INSERT INTO volunteers (attendee_id, name, preferred_name, ticket_name, email, phone, pronouns, department_id, is_lead, email_confirmed, confirmation_token, note, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
v.AttendeeID, v.Name, v.PreferredName, v.TicketName, v.Email, v.Phone, v.Pronouns,
|
||||
v.DepartmentID, boolInt(v.IsLead), boolInt(v.EmailConfirmed), v.ConfirmationToken, v.Note, now(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -776,9 +787,10 @@ func (app *App) createVolunteer(v Volunteer) (*Volunteer, error) {
|
|||
|
||||
func (app *App) updateVolunteer(v Volunteer) error {
|
||||
_, err := app.db.Exec(
|
||||
`UPDATE volunteers SET attendee_id=?, name=?, email=?, phone=?, department_id=?, is_lead=?, note=?, updated_at=?
|
||||
`UPDATE volunteers SET attendee_id=?, name=?, preferred_name=?, ticket_name=?, email=?, phone=?, pronouns=?, department_id=?, is_lead=?, note=?, updated_at=?
|
||||
WHERE id=? AND deleted_at IS NULL`,
|
||||
v.AttendeeID, v.Name, v.Email, v.Phone, v.DepartmentID, boolInt(v.IsLead), v.Note, now(), v.ID,
|
||||
v.AttendeeID, v.Name, v.PreferredName, v.TicketName, v.Email, v.Phone, v.Pronouns,
|
||||
v.DepartmentID, boolInt(v.IsLead), v.Note, now(), v.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
@ -822,10 +834,13 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) {
|
|||
for rows.Next() {
|
||||
var v Volunteer
|
||||
var attendeeID, deptID sql.NullInt64
|
||||
var isLead, checkedIn int
|
||||
var isLead, checkedIn, emailConfirmed int
|
||||
var confirmationToken sql.NullString
|
||||
if err := rows.Scan(
|
||||
&v.ID, &attendeeID, &v.Name, &v.Email, &v.Phone, &deptID,
|
||||
&isLead, &checkedIn, &v.CheckedInAt, &v.Note,
|
||||
&v.ID, &attendeeID, &v.Name, &v.PreferredName, &v.TicketName,
|
||||
&v.Email, &v.Phone, &v.Pronouns, &deptID,
|
||||
&isLead, &checkedIn, &v.CheckedInAt,
|
||||
&emailConfirmed, &confirmationToken, &v.Note,
|
||||
&v.CreatedAt, &v.UpdatedAt, &v.DeletedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -838,13 +853,59 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) {
|
|||
id := int(deptID.Int64)
|
||||
v.DepartmentID = &id
|
||||
}
|
||||
if confirmationToken.Valid {
|
||||
v.ConfirmationToken = &confirmationToken.String
|
||||
}
|
||||
v.IsLead = isLead == 1
|
||||
v.CheckedIn = checkedIn == 1
|
||||
v.EmailConfirmed = emailConfirmed == 1
|
||||
result = append(result, v)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (app *App) getVolunteerByEmail(email string) (*Volunteer, error) {
|
||||
rows, err := queryVolunteers(app.db,
|
||||
`SELECT `+volunteerCols+` FROM volunteers WHERE LOWER(email) = LOWER(?) AND deleted_at IS NULL LIMIT 1`, email)
|
||||
if err != nil || len(rows) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return &rows[0], nil
|
||||
}
|
||||
|
||||
func (app *App) getVolunteerByConfirmationToken(token string) (*Volunteer, error) {
|
||||
rows, err := queryVolunteers(app.db,
|
||||
`SELECT `+volunteerCols+` FROM volunteers WHERE confirmation_token = ? AND 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 {
|
||||
_, err := app.db.Exec(
|
||||
`UPDATE volunteers SET email_confirmed = 1, confirmation_token = NULL, updated_at = ? WHERE id = ?`,
|
||||
now(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (app *App) listConfirmedVolunteersWithoutKioskToken() ([]Volunteer, error) {
|
||||
return queryVolunteers(app.db, `
|
||||
SELECT `+volunteerCols+`
|
||||
FROM volunteers
|
||||
WHERE email_confirmed = 1 AND deleted_at IS NULL
|
||||
AND attendee_id IS NOT NULL
|
||||
AND (SELECT a.volunteer_token FROM attendees a WHERE a.id = volunteers.attendee_id) IS NULL`)
|
||||
}
|
||||
|
||||
func generateConfirmationToken() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", fmt.Errorf("read random: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("%x", b), nil
|
||||
}
|
||||
|
||||
// --- Shifts ---
|
||||
|
||||
func (app *App) listShifts(deptID *int, day, since string) ([]Shift, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue