diff --git a/db.go b/db.go index c7a1089..cef0e8f 100644 --- a/db.go +++ b/db.go @@ -93,8 +93,8 @@ func migrate(db *sql.DB) error { phone TEXT NOT NULL DEFAULT '', department_id INTEGER REFERENCES departments(id) ON DELETE SET NULL, is_lead INTEGER NOT NULL DEFAULT 0, - checked_in INTEGER NOT NULL DEFAULT 0, - checked_in_at TEXT, + 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')), @@ -177,6 +177,8 @@ func migrateV2(db *sql.DB) error { 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(` @@ -340,6 +342,24 @@ func addColumnIfMissing(db *sql.DB, table, colDef string) { db.Exec(`ALTER TABLE "` + table + `" ADD COLUMN ` + colDef) } +func renameColumnIfExists(db *sql.DB, table, oldName, newName string) { + 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, ¬Null, &dflt, &pk) + if name == oldName { + db.Exec(`ALTER TABLE "` + table + `" RENAME COLUMN "` + oldName + `" TO "` + newName + `"`) + return + } + } +} + // --- Types --- const attendeeCols = `id, name, email, phone, ticket_id, ticket_type, volunteer_token, @@ -407,8 +427,8 @@ type Volunteer struct { 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"` + 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"` @@ -1203,14 +1223,14 @@ const volunteerSelect = `v.id, v.participant_id, v.attendee_id, COALESCE(NULLIF(p.email,''), v.email), COALESCE(NULLIF(p.phone,''), v.phone), COALESCE(NULLIF(p.pronouns,''), v.pronouns), - v.department_id, v.is_lead, v.checked_in, v.checked_in_at, + 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, 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, checked_in, checked_in_at, email_confirmed, confirmation_token, note, created_at, updated_at, deleted_at` +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` func (app *App) listVolunteers(search string, deptID *int, since string) ([]Volunteer, error) { q := `SELECT ` + volunteerSelect + ` ` + volunteerFrom + ` WHERE 1=1` @@ -1292,13 +1312,13 @@ func (app *App) deleteVolunteer(id int) error { return err } -// checkInVolunteer marks the volunteer as checked in and, if linked to an attendee, +// markVolunteerReady marks the volunteer as ready and, if linked to an attendee, // also increments the attendee's checked_in_count. -func (app *App) checkInVolunteer(id, userID int) (*Volunteer, error) { +func (app *App) markVolunteerReady(id, userID int) (*Volunteer, error) { t := now() _, err := app.db.Exec( - `UPDATE volunteers SET checked_in=1, checked_in_at=?, updated_at=? - WHERE id=? AND deleted_at IS NULL AND checked_in=0`, + `UPDATE volunteers SET ready=1, ready_at=?, updated_at=? + WHERE id=? AND deleted_at IS NULL AND ready=0`, t, t, id, ) if err != nil { @@ -1337,12 +1357,12 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) { for rows.Next() { var v Volunteer var participantID, attendeeID, deptID sql.NullInt64 - var isLead, checkedIn, confirmed, emailConfirmed int + var isLead, ready, confirmed, emailConfirmed int var confirmationToken, confirmedAt, kioskCode sql.NullString if err := rows.Scan( &v.ID, &participantID, &attendeeID, &v.Name, &v.PreferredName, &v.Email, &v.Phone, &v.Pronouns, &deptID, - &isLead, &checkedIn, &v.CheckedInAt, + &isLead, &ready, &v.ReadyAt, &confirmed, &confirmedAt, &emailConfirmed, &confirmationToken, &kioskCode, &v.Note, &v.CreatedAt, &v.UpdatedAt, &v.DeletedAt, @@ -1371,7 +1391,7 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) { v.KioskCode = &kioskCode.String } v.IsLead = isLead == 1 - v.CheckedIn = checkedIn == 1 + v.Ready = ready == 1 v.Confirmed = confirmed == 1 v.EmailConfirmed = emailConfirmed == 1 result = append(result, v) diff --git a/frontend/src/api.js b/frontend/src/api.js index 6700d4b..686faa8 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -78,7 +78,7 @@ export const api = { create: (data) => apiJSON('/api/volunteers', { method: 'POST', body: JSON.stringify(data) }), update: (id, data) => apiJSON(`/api/volunteers/${id}`, { method: 'PUT', body: JSON.stringify(data) }), delete: (id) => apiFetch(`/api/volunteers/${id}`, { method: 'DELETE' }), - checkIn: (id) => apiJSON(`/api/volunteers/${id}/checkin`, { method: 'POST' }), + markReady: (id) => apiJSON(`/api/volunteers/${id}/ready`, { method: 'POST' }), confirm: (id) => apiJSON(`/api/volunteers/${id}/confirm`, { method: 'POST' }), assignShift: (id, shiftId) => apiFetch(`/api/volunteers/${id}/shifts`, { method: 'POST', body: JSON.stringify({ shift_id: shiftId }) }), unassignShift: (id, shiftId) => apiFetch(`/api/volunteers/${id}/shifts/${shiftId}`, { method: 'DELETE' }), diff --git a/frontend/src/components/CheckInButton.svelte b/frontend/src/components/CheckInButton.svelte index f0af073..cbd4bd2 100644 --- a/frontend/src/components/CheckInButton.svelte +++ b/frontend/src/components/CheckInButton.svelte @@ -9,5 +9,5 @@ diff --git a/frontend/src/pages/Dashboard.svelte b/frontend/src/pages/Dashboard.svelte index 9a69d10..e5a26de 100644 --- a/frontend/src/pages/Dashboard.svelte +++ b/frontend/src/pages/Dashboard.svelte @@ -31,7 +31,7 @@ return vols }) const volTotal = $derived(volunteers.length) - const volCheckedIn = $derived(volunteers.filter(v => v.checked_in).length) + const volCheckedIn = $derived(volunteers.filter(v => v.ready).length) const volLeads = $derived(volunteers.filter(v => v.is_lead).length) // Shift stats (scoped for colead) diff --git a/frontend/src/pages/Volunteers.svelte b/frontend/src/pages/Volunteers.svelte index 441e415..f477e64 100644 --- a/frontend/src/pages/Volunteers.svelte +++ b/frontend/src/pages/Volunteers.svelte @@ -54,8 +54,8 @@ if (filterDept && v.department_id !== parseInt(filterDept)) return false if (filterStatus === 'unconfirmed' && v.email_confirmed) return false if (filterStatus === 'registered' && (!v.email_confirmed || v.confirmed)) return false - if (filterStatus === 'confirmed' && (!v.confirmed || v.checked_in)) return false - if (filterStatus === 'ready' && !v.checked_in) return false + if (filterStatus === 'confirmed' && (!v.confirmed || v.ready)) return false + if (filterStatus === 'ready' && !v.ready) return false if (s && !v.name.toLowerCase().includes(s) && !(v.email || '').toLowerCase().includes(s)) return false return true @@ -63,9 +63,9 @@ .sort((a, b) => a.name.localeCompare(b.name)) }) - async function checkIn(v) { + async function markReady(v) { try { - const updated = await api.volunteers.checkIn(v.id) + const updated = await api.volunteers.markReady(v.id) await db.volunteers.put(updated) } catch (err) { error = err.message @@ -314,7 +314,7 @@ {/if} - {#if v.checked_in} + {#if v.ready} Ready {:else if v.confirmed} Confirmed @@ -323,15 +323,15 @@ {:else} Unconfirmed {/if} - {#if v.checked_in_at} + {#if v.ready_at}
- {new Date(v.checked_in_at).toLocaleTimeString()} + {new Date(v.ready_at).toLocaleTimeString()}
{/if} - {#if !v.checked_in} - checkIn(v)} /> + {#if v.confirmed && !v.ready} + markReady(v)} /> {/if} {#if canManage} diff --git a/handle_volunteers.go b/handle_volunteers.go index 584927b..845bddd 100644 --- a/handle_volunteers.go +++ b/handle_volunteers.go @@ -130,14 +130,14 @@ func (app *App) handleDeleteVolunteer(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func (app *App) handleCheckInVolunteer(w http.ResponseWriter, r *http.Request) { +func (app *App) handleMarkVolunteerReady(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) - v, err := app.checkInVolunteer(id, claims.UserID) + v, err := app.markVolunteerReady(id, claims.UserID) if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return diff --git a/main.go b/main.go index ea79877..be9fc53 100644 --- a/main.go +++ b/main.go @@ -125,7 +125,7 @@ func (app *App) registerRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/volunteers/{id}", auth(app.handleGetVolunteer, "admin", "ticketing", "staffing", "colead")) mux.HandleFunc("PUT /api/volunteers/{id}", auth(app.handleUpdateVolunteer, "admin", "ticketing", "staffing", "colead")) mux.HandleFunc("DELETE /api/volunteers/{id}", auth(app.handleDeleteVolunteer, "admin", "ticketing", "staffing", "colead")) - mux.HandleFunc("POST /api/volunteers/{id}/checkin", auth(app.handleCheckInVolunteer, "admin", "ticketing", "staffing", "colead")) + mux.HandleFunc("POST /api/volunteers/{id}/ready", auth(app.handleMarkVolunteerReady, "admin", "ticketing", "staffing", "colead")) mux.HandleFunc("POST /api/volunteers/{id}/confirm", auth(app.handleConfirmVolunteer, "admin", "staffing", "colead")) mux.HandleFunc("POST /api/volunteers/{id}/shifts", auth(app.handleAssignShift, "admin", "ticketing", "staffing", "colead")) mux.HandleFunc("DELETE /api/volunteers/{id}/shifts/{shift_id}", auth(app.handleUnassignShift, "admin", "ticketing", "staffing", "colead"))