diff --git a/app/controllers/event_registrations_controller.rb b/app/controllers/event_registrations_controller.rb index bfb8ece42..c3f3740b2 100644 --- a/app/controllers/event_registrations_controller.rb +++ b/app/controllers/event_registrations_controller.rb @@ -2,7 +2,7 @@ class EventRegistrationsController < ApplicationController require "csv" # show redirects to slug URL; kept for backwards compatibility - before_action :set_event_registration, only: [ :show, :edit, :update, :destroy, :update_onboarding ] + before_action :set_event_registration, only: [ :show, :edit, :update, :destroy, :update_onboarding, :toggle_certificate_issued ] def index authorize! @@ -156,6 +156,17 @@ def update_onboarding end end + # Inline toggle of the "certificate issued" flag from the registrants roster. + # Replaces just the cell so the whole roster doesn't re-render on every toggle. + def toggle_certificate_issued + authorize! @event_registration, to: :toggle_certificate_issued? + @event_registration.mark_certificate_issued!(ActiveModel::Type::Boolean.new.cast(params[:value])) + respond_to do |format| + format.turbo_stream + format.html { redirect_to helpers.registrants_event_row_path(@event_registration.event, @event_registration.id) } + end + end + def confirm @event_registration = EventRegistration.includes(registrant: :user, event: :location).find(params[:id]) authorize! @event_registration, to: :confirm? diff --git a/app/models/event_registration.rb b/app/models/event_registration.rb index 5d6551da3..fbeef8ada 100644 --- a/app/models/event_registration.rb +++ b/app/models/event_registration.rb @@ -551,6 +551,23 @@ def ce_certificate_issued? continuing_education_registrations.all? { |c| c.certificate_sent_at.present? } end + # The registration's completion certificate, as shown by the registrants-roster + # toggle. For a CE-eligible registration that's the CE certificate + # (certificate_sent_at on its CE registrations, so it stays in sync with the CE + # edit page); otherwise the registration's own certificate_sent_at (Certifiable). + def certificate_issued? + ce_registered? ? ce_certificate_issued? : certificate_sent? + end + + def mark_certificate_issued!(issued) + at = issued ? Time.current : nil + if ce_registered? + continuing_education_registrations.each { |c| c.update!(certificate_sent_at: at) } + else + update!(certificate_sent_at: at) + end + end + # True when a CE registration exists and every one is fully paid. def ce_paid_in_full? return false unless ce_registered? diff --git a/app/policies/event_registration_policy.rb b/app/policies/event_registration_policy.rb index c7ba2458b..dc82897d6 100644 --- a/app/policies/event_registration_policy.rb +++ b/app/policies/event_registration_policy.rb @@ -18,6 +18,9 @@ def unlink_organization? = admin? # Editing the onboarding matrix is an admin management action; event owners # (the event's creator) manage their own events' onboarding too. def update_onboarding? = admin? || event_owner? + # Marking a certificate issued from the registrants roster is an event-management + # action, so mirror the roster's audience (admins and the event's owner). + def toggle_certificate_issued? = admin? || event_owner? relation_scope do |relation| diff --git a/app/views/event_registrations/_certificate_issued_toggle.html.erb b/app/views/event_registrations/_certificate_issued_toggle.html.erb new file mode 100644 index 000000000..fe0948a07 --- /dev/null +++ b/app/views/event_registrations/_certificate_issued_toggle.html.erb @@ -0,0 +1,15 @@ +<%# Inline "certificate issued" slider toggle, targeted by id for turbo replacement. + The track is a direct sibling of the checkbox so peer-checked applies (it only + reaches later siblings, not their descendants); the knob is the track's ::after. %> + + <%= form_with url: toggle_certificate_issued_event_registration_path(registration), method: :patch, class: "inline" do %> + <%= hidden_field_tag :value, "0" %> + + <% end %> + diff --git a/app/views/event_registrations/toggle_certificate_issued.turbo_stream.erb b/app/views/event_registrations/toggle_certificate_issued.turbo_stream.erb new file mode 100644 index 000000000..d2c8af0e0 --- /dev/null +++ b/app/views/event_registrations/toggle_certificate_issued.turbo_stream.erb @@ -0,0 +1,4 @@ +<%# Replace just the toggled cell — avoids re-rendering the whole roster. %> +<%= turbo_stream.replace dom_id(@event_registration, :certificate_issued) do %> + <%= render "event_registrations/certificate_issued_toggle", registration: @event_registration %> +<% end %> diff --git a/app/views/events/_registrants_results.html.erb b/app/views/events/_registrants_results.html.erb index 5fd73532b..214645c17 100644 --- a/app/views/events/_registrants_results.html.erb +++ b/app/views/events/_registrants_results.html.erb @@ -130,6 +130,24 @@ + + <%# Off by default — Certificate issued is hidden until the admin reveals it. %> + @@ -193,6 +211,8 @@ <%= render "shared/sortable_header", label: "Status", index: status_index %> + <%# Off by default; kept out of the sortable index range (last data column). %> + Certificate @@ -449,6 +469,9 @@ issues: readiness.status_issues, subtext: readiness.status_reason %> + + <%= render "event_registrations/certificate_issued_toggle", registration: registration %> + <%= link_to "Edit", edit_event_registration_path(registration, return_to: "registrants"), class: "text-gray-500 hover:text-gray-700 underline", data: { turbo_frame: "_top" } %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index bf3135717..034383fc9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -105,6 +105,7 @@ post :create_organization delete :unlink_organization patch :update_onboarding + patch :toggle_certificate_issued end resources :comments, only: [ :index, :create, :update ] end diff --git a/spec/requests/event_registrations_spec.rb b/spec/requests/event_registrations_spec.rb index a244bd7d6..4267c6d42 100644 --- a/spec/requests/event_registrations_spec.rb +++ b/spec/requests/event_registrations_spec.rb @@ -268,6 +268,42 @@ def toggle_day(field, value) end end + describe "PATCH /event_registrations/:id/toggle_certificate_issued" do + let(:registration) { create(:event_registration, event: event) } + + def toggle_certificate(value) + patch toggle_certificate_issued_event_registration_path(registration), + params: { value: value }, + headers: { "Accept" => "text/vnd.turbo-stream.html" } + end + + it "marks the registration's certificate issued" do + toggle_certificate("1") + expect(registration.reload.certificate_issued?).to be(true) + end + + it "clears the certificate when unchecked" do + registration.mark_certificate_sent! + toggle_certificate("0") + expect(registration.reload.certificate_issued?).to be(false) + end + + it "drives the CE certificate for a CE registration, so it stays in sync with the CE edit page" do + ce = create(:continuing_education_registration, event_registration: registration) + + toggle_certificate("1") + expect(ce.reload.certificate_sent_at).to be_present + + toggle_certificate("0") + expect(ce.reload.certificate_sent_at).to be_nil + end + + it "replaces just the toggled cell in the turbo stream" do + toggle_certificate("1") + expect(response.body).to include("certificate_issued_event_registration_#{registration.id}") + end + end + describe "POST /event_registrations" do it "creates registration and redirects admin to confirm page" do expect { @@ -1093,6 +1129,14 @@ def details_open?(body, heading) end end + describe "PATCH /event_registrations/:id/toggle_certificate_issued" do + it "is forbidden for the registrant themselves" do + patch toggle_certificate_issued_event_registration_path(existing_registration), params: { value: "1" } + expect(response).to redirect_to(root_path) + expect(existing_registration.reload.certificate_issued?).to be(false) + end + end + describe "POST /event_registrations" do context "when no registration exists yet" do it "creates a new EventRegistration" do