diff --git a/app/frontend/javascript/controllers/attendance_status_controller.js b/app/frontend/javascript/controllers/attendance_status_controller.js index 62a4a6b5d..007416009 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 4a7a474f2..2be2e6675 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,11 +85,7 @@
<% end %> -
+
Registration status <% 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 %> +
+
diff --git a/spec/system/event_registration_edit_spec.rb b/spec/system/event_registration_edit_spec.rb index 216bca7b6..ac1e2d4db 100644 --- a/spec/system/event_registration_edit_spec.rb +++ b/spec/system/event_registration_edit_spec.rb @@ -182,6 +182,63 @@ 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" + + # Wait for the save round-trip to land before reading the database. + expect(page).to have_current_path(registrants_event_path(event)) + 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,