From 3431c4974c82623605b22a364fff4613a81d696b Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 4 Jul 2026 14:50:30 -0400 Subject: [PATCH 1/4] Surface onboarding day-attendance checkmarks on the registration edit form The per-day attendance checkboxes only lived on the Onboarding matrix, so an admin editing a single registration couldn't mark days without leaving the page. Surface the same checkmarks beside the status dropdown, and mirror onboarding's behavior so toggling a day rolls the attendance status forward/back (client-side, respecting deliberate inactive states). DAY_FIELDS were already permitted params, so the boxes save with the form. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../attendance_status_controller.js | 25 ++++++++- app/views/event_registrations/_form.html.erb | 22 +++++++- spec/system/event_registration_edit_spec.rb | 55 +++++++++++++++++++ 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/app/frontend/javascript/controllers/attendance_status_controller.js b/app/frontend/javascript/controllers/attendance_status_controller.js index 62a4a6b5d5..007416009b 100644 --- a/app/frontend/javascript/controllers/attendance_status_controller.js +++ b/app/frontend/javascript/controllers/attendance_status_controller.js @@ -4,9 +4,30 @@ import { Controller } from "@hotwired/stimulus" // Swaps the colored icon inside the status select as the dropdown changes. The // select looks like an ordinary form field (not the roster's autosaving chip), // so an "Unsaved" hint is shown until the form is saved. +// +// The per-day attendance checkboxes drive the status the same way the Onboarding +// matrix does: toggling a day rolls the status forward/back (registered → +// incomplete_attendance → attended), but only while the status is an active one — +// deliberate manual states (cancelled, no_show, transferred_out) are never +// overridden. Mirrors EventRegistration#sync_attendance_status_to_days!. export default class extends Controller { - static targets = ["select", "icon", "dirty"] - static values = { colors: Object, icons: Object, initial: String } + static targets = ["select", "icon", "dirty", "day"] + static values = { colors: Object, icons: Object, initial: String, activeStatuses: Array, dayCount: Number } + + // Recompute the status from how many day checkboxes are checked, then reflect it + // in the dropdown (which re-renders the icon and the unsaved hint via update()). + deriveFromDays() { + if (!this.activeStatusesValue.includes(this.selectTarget.value)) return + + const checked = this.dayTargets.filter((day) => day.checked).length + let derived = "incomplete_attendance" + if (checked === 0) derived = "registered" + else if (checked >= this.dayCountValue) derived = "attended" + + if (this.selectTarget.value === derived) return + this.selectTarget.value = derived + this.update() + } update() { const status = this.selectTarget.value diff --git a/app/views/event_registrations/_form.html.erb b/app/views/event_registrations/_form.html.erb index 4a7a474f28..33c3309544 100644 --- a/app/views/event_registrations/_form.html.erb +++ b/app/views/event_registrations/_form.html.erb @@ -81,7 +81,9 @@ data-controller="attendance-status" data-attendance-status-colors-value="<%= status_icon_colors.to_json %>" data-attendance-status-icons-value="<%= status_icons.to_json %>" - data-attendance-status-initial-value="<%= f.object.status %>"> + data-attendance-status-initial-value="<%= f.object.status %>" + data-attendance-status-active-statuses-value="<%= EventRegistration::ACTIVE_STATUSES.to_json %>" + data-attendance-status-day-count-value="<%= f.object.event.day_count %>">
Registration status
+ + <%# ---- Per-day attendance — the same checkmarks as the Onboarding + matrix. Toggling a day rolls the status above forward/back (see the + attendance-status controller), and the boxes save with the form. ---- %> +
+ Days attended +
+ <% (1..f.object.event.day_count).each do |day| %> + + <% end %> +
+
diff --git a/spec/system/event_registration_edit_spec.rb b/spec/system/event_registration_edit_spec.rb index 216bca7b60..b7bc948a58 100644 --- a/spec/system/event_registration_edit_spec.rb +++ b/spec/system/event_registration_edit_spec.rb @@ -182,6 +182,61 @@ end end + describe "per-day attendance checkboxes" do + it "renders a checkbox per event day, reflecting stored state, and persists changes" do + registration.update!(completed_day_1: true) + + sign_in(admin) + visit edit_event_registration_path(registration) + + # The event spans 3 days, so Days 1-3 show (and no Day 4). + expect(page).to have_field("Day 1", checked: true) + expect(page).to have_field("Day 2", checked: false) + expect(page).to have_field("Day 3", checked: false) + expect(page).to have_no_field("Day 4") + + check "Day 2", allow_label_click: true + click_on "Save changes" + + expect(page).to have_current_path(registrants_event_path(event)) + expect(registration.reload.completed_day_1).to be(true) + expect(registration.completed_day_2).to be(true) + end + + it "derives the attendance status from the checked days and saves both together" do + sign_in(admin) + visit edit_event_registration_path(registration) + + badge = "[data-attendance-status-target='dirty']" + expect(page).to have_no_css(badge, visible: true) + + # Checking every day rolls the status forward to Attended (mirrors onboarding). + check "Day 1", allow_label_click: true + check "Day 2", allow_label_click: true + check "Day 3", allow_label_click: true + + expect(page).to have_css(badge, visible: true, text: "Unsaved") + expect(page).to have_select("event_registration[status]", selected: "Attended") + + click_on "Save changes" + + expect(registration.reload.status).to eq("attended") + expect(registration.completed_day_count).to eq(3) + end + + it "leaves an inactive status untouched when days are toggled" do + registration.update!(status: "cancelled") + + sign_in(admin) + visit edit_event_registration_path(registration) + + check "Day 1", allow_label_click: true + + # Cancelled is a deliberate manual state, so toggling a day never overrides it. + expect(page).to have_select("event_registration[status]", selected: "Cancelled") + end + end + describe "notifications box" do it "lists notifications sent to the registrant" do create(:notification, From 2d7fc9f0f823ed1ebec91bb0f381bc258cf82138 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 4 Jul 2026 15:20:10 -0400 Subject: [PATCH 2/4] Move the days-attended checkmarks into the registration meta strip Group them inline with the "View ticket" / "View submission" links instead of under the status dropdown. The attendance-status controller now spans the whole card so the checkmarks still drive the status select. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/event_registrations/_form.html.erb | 55 +++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/app/views/event_registrations/_form.html.erb b/app/views/event_registrations/_form.html.erb index 33c3309544..8fc98ab258 100644 --- a/app/views/event_registrations/_form.html.erb +++ b/app/views/event_registrations/_form.html.erb @@ -32,7 +32,15 @@ } %> <% current_icon_color = status_icon_colors[f.object.status] || "text-gray-500" %> <% current_icon = status_icons[f.object.status] || "fa-question" %> -
+ <%# The attendance-status controller spans the whole card so the per-day + checkmarks in the meta strip below can drive the status select above. %> +
<%= person_profile_button(f.object.registrant, subtitle: f.object.registrant.preferred_email) %> @@ -77,13 +85,7 @@
<% end %> -
+
Registration status
- - <%# ---- Per-day attendance — the same checkmarks as the Onboarding - matrix. Toggling a day rolls the status above forward/back (see the - attendance-status controller), and the boxes save with the form. ---- %> -
- Days attended -
- <% (1..f.object.event.day_count).each do |day| %> - - <% end %> -
-
@@ -154,6 +138,25 @@ <% end %> <% end %> + + <%# ---- Per-day attendance — the same checkmarks as the Onboarding + matrix. Toggling a day rolls the status select above forward/back + (see the attendance-status controller), and the boxes save with the + form. ---- %> +
+ Days attended +
+ <% (1..f.object.event.day_count).each do |day| %> + + <% end %> +
+
From 1b31f1238c3fe36ab8416e2f92676e3c1e0e2573 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 4 Jul 2026 15:23:10 -0400 Subject: [PATCH 3/4] Right-align the days-attended group with its label stacked above Push it to the right edge of the meta strip and stack the label over the day chips, per the requested layout. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/event_registrations/_form.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/event_registrations/_form.html.erb b/app/views/event_registrations/_form.html.erb index 8fc98ab258..2be2e66757 100644 --- a/app/views/event_registrations/_form.html.erb +++ b/app/views/event_registrations/_form.html.erb @@ -143,9 +143,9 @@ matrix. Toggling a day rolls the status select above forward/back (see the attendance-status controller), and the boxes save with the form. ---- %> -
+
Days attended -
+
<% (1..f.object.event.day_count).each do |day| %>