Skip to content

Commit c263f20

Browse files
maebealeclaude
andcommitted
Add a CE hours detail section to the registration ticket
CE requirements (CAMFT approval, license-number and payment rules, sign-in, post-training evaluation) used to live in a long confirmation email. Surface them per event the same way "Before you attend" works: a ticket call-out linking to a sanitized-HTML details page, so admins can keep the embedded links instead of re-sending an email each time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 590cd18 commit c263f20

10 files changed

Lines changed: 149 additions & 2 deletions

File tree

app/controllers/events_controller.rb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
class EventsController < ApplicationController
22
include AhoyTracking, TagAssignable
3-
skip_before_action :authenticate_user!, only: [ :index, :show, :staff, :details ]
3+
skip_before_action :authenticate_user!, only: [ :index, :show, :staff, :details, :ce_hours ]
44
skip_before_action :verify_authenticity_token, only: [ :preview ]
5-
before_action :set_event, only: %i[ show edit update destroy preview dashboard background registrants details staff edit_staff update_staff recipients bulk_payments preview_reminder send_reminder copy_registration_form allocate_bulk_payment create_bulk_payment ]
5+
before_action :set_event, only: %i[ show edit update destroy preview dashboard background registrants details ce_hours staff edit_staff update_staff recipients bulk_payments preview_reminder send_reminder copy_registration_form allocate_bulk_payment create_bulk_payment ]
66

77
def index
88
authorize!
@@ -104,6 +104,20 @@ def details
104104
@event = @event.decorate
105105
end
106106

107+
# Public CE hours page (continuing education requirements, payment, sign-in
108+
# rules). Linked from the registration ticket. When no details are set there
109+
# is nothing to show, so fall back to the event page.
110+
def ce_hours
111+
authorize! @event, to: :ce_hours?
112+
113+
if @event.ce_hours_details.blank?
114+
redirect_to event_path(@event, reg: params[:reg].presence)
115+
return
116+
end
117+
118+
@event = @event.decorate
119+
end
120+
107121
def staff
108122
authorize! @event, to: :staff?
109123
@event = @event.decorate

app/models/event.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ def event_details_label
156156
super.presence || "Before you attend"
157157
end
158158

159+
# Heading shown on the CE hours ticket call-out and its details page. Falls
160+
# back to the default even when an admin clears it, so the section never
161+
# renders unlabelled.
162+
def ce_hours_details_label
163+
super.presence || "CE hours"
164+
end
165+
159166
# Virtual attributes for date/time inputs (Firefox datetime-local compat)
160167
attr_writer :start_date_date, :start_date_time,
161168
:end_date_date, :end_date_time,

app/policies/event_policy.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ def google_analytics?
112112
:rhino_description,
113113
:event_details,
114114
:event_details_label,
115+
:ce_hours_details,
116+
:ce_hours_details_label,
115117
:autoshow_cost,
116118
:autoshow_date,
117119
:autoshow_location,
@@ -150,6 +152,7 @@ def google_analytics?
150152

151153
alias_rule :preview?, to: :edit?
152154
alias_rule :details?, to: :show?
155+
alias_rule :ce_hours?, to: :show?
153156

154157
private
155158

app/views/event_registrations/_ticket.html.erb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@
109109
<% end %>
110110
<% end %>
111111

112+
<!-- CE hours (continuing education requirements, payment, sign-in rules) -->
113+
<% if event_registration.event.ce_hours_details.present? %>
114+
<%= link_to ce_hours_event_path(event_registration.event, reg: event_registration.slug),
115+
class: "flex items-center gap-3 rounded-xl border-2 border-indigo-300 bg-indigo-50 px-4 py-3 hover:bg-indigo-100 transition-colors" do %>
116+
<i class="fa-solid fa-graduation-cap text-indigo-500 text-xl shrink-0"></i>
117+
118+
<div class="flex-1 min-w-0 text-left">
119+
<p class="font-semibold text-indigo-900"><%= event_registration.event.ce_hours_details_label %></p>
120+
<p class="text-sm text-indigo-700">Continuing education hours — requirements &amp; next steps</p>
121+
</div>
122+
123+
<i class="fa-solid fa-arrow-right text-indigo-500 shrink-0"></i>
124+
<% end %>
125+
<% end %>
126+
112127
<!-- Additional forms the registrant asked for during registration -->
113128
<% if event_registration.w9_requested? %>
114129
<%= link_to "/documents/awbw-w9.pdf",

app/views/events/_form.html.erb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,43 @@
454454
</div>
455455
</div>
456456

457+
<%# ---- CE HOURS (ticket call-out + details page) ---- %>
458+
<div class="ce-hours-section" data-controller="dropdown">
459+
<button
460+
type="button"
461+
id="ce_hours_button"
462+
class="
463+
flex items-center justify-between cursor-pointer w-full
464+
bg-gray-500 text-white hover:bg-gray-300 px-3 py-2 rounded-md"
465+
data-action="dropdown#toggle"
466+
data-dropdown-payload-param='[{"ce_hours_details":"hidden"}, {"ce_hours_arrow":"rotate-180"}, {"ce_hours_button":"rounded-md rounded-t-md"}]'>
467+
CE hours
468+
<i id="ce_hours_arrow" class="fa-solid fa-chevron-down w-4 h-4 transition-transform duration-300"></i>
469+
</button>
470+
471+
<div id="ce_hours_details" class="hidden border border-gray-300 p-4 rounded-b-md">
472+
<p class="text-sm text-gray-500 mb-4">Continuing education info shown on the registration ticket as a prominent call-out linking to its own details page — CAMFT approval, license-number and payment requirements, sign-in rules, and the post-training evaluation (the kind of thing that used to live in a long CE confirmation email).</p>
473+
474+
<div class="form-group">
475+
<%= f.label :ce_hours_details_label, "CE hours section label", class: "block font-medium mb-1 text-gray-700" %>
476+
<%= f.text_field :ce_hours_details_label,
477+
placeholder: "CE hours",
478+
class: "w-full rounded border-gray-300 shadow-sm px-3 py-1.5 text-sm focus:ring-blue-500 focus:border-blue-500" %>
479+
<p class="text-gray-500 text-sm mt-1">Heading for the CE hours call-out on the ticket and the details page (e.g. "CE hours", "Continuing education").</p>
480+
</div>
481+
482+
<div class="form-group mt-6">
483+
<%= f.label :ce_hours_details, "CE hours content", class: "block font-medium mb-1 text-gray-700" %>
484+
<p class="text-xs text-gray-500 mb-1">
485+
CE requirements, payment, sign-in rules, and the post-training evaluation — shown on its own page linked prominently from the ticket. Leave blank to hide it entirely. Accepts basic HTML — bold, italics, links, lists, headings, and line breaks.
486+
</p>
487+
<%= f.text_area :ce_hours_details, rows: 8,
488+
placeholder: "e.g. <p>AWBW is approved by CAMFT…</p><h3>Before the training</h3><ul><li>Email your license number</li></ul>",
489+
class: "w-full rounded border-gray-300 shadow-sm px-3 py-2 text-sm font-mono" %>
490+
</div>
491+
</div>
492+
</div>
493+
457494
<div class="images-section" data-controller="dropdown">
458495
<button
459496
type="button"

app/views/events/ce_hours.html.erb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<% content_for(:page_bg_class, "public") %>
2+
3+
<% reg_slug = params[:reg].presence %>
4+
<% back_path = reg_slug ? registration_ticket_path(reg_slug) : event_path(@event) %>
5+
<% back_label = reg_slug ? "Back to ticket" : "Back to event" %>
6+
<div class="max-w-3xl mx-auto px-4 pt-4">
7+
<%= link_to back_path, class: "inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 px-2 py-1" do %>
8+
<i class="fa-solid fa-arrow-left text-xs"></i> <%= back_label %>
9+
<% end %>
10+
</div>
11+
12+
<div class="max-w-3xl mx-auto pb-12 pt-6 px-4">
13+
<div class="bg-white rounded-2xl shadow-xl ring-1 ring-black/5 overflow-hidden">
14+
<div class="relative bg-gradient-to-r from-purple-950 to-purple-700 text-white px-6 sm:px-8 py-5">
15+
<div class="flex items-center gap-5">
16+
<div class="shrink-0">
17+
<%= image_tag("logo.png", alt: "Organization logo", class: "h-9 w-auto") %>
18+
</div>
19+
<div class="flex-1">
20+
<h1 class="text-xl font-semibold tracking-wide leading-tight"><%= @event.ce_hours_details_label %></h1>
21+
<p class="text-sm text-blue-200"><%= @event.title %></p>
22+
</div>
23+
</div>
24+
<div class="absolute inset-x-0 bottom-0 h-1 bg-gradient-to-r from-accent via-amber-400 to-accent"></div>
25+
</div>
26+
27+
<div class="px-6 sm:px-8 py-7">
28+
<div class="rich-label prose max-w-none text-gray-700 leading-relaxed prose-headings:text-gray-800 prose-a:text-blue-700">
29+
<%= form_label_html(@event.ce_hours_details) %>
30+
</div>
31+
</div>
32+
</div>
33+
</div>

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
get :background
130130
get :registrants
131131
get :details
132+
get :ce_hours
132133
get :staff
133134
get "staff/edit", action: :edit_staff, as: :edit_staff
134135
patch "staff", action: :update_staff
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class AddCeHoursDetailsToEvents < ActiveRecord::Migration[8.1]
2+
def up
3+
add_column :events, :ce_hours_details, :text
4+
add_column :events, :ce_hours_details_label, :string, default: "CE hours", null: false
5+
end
6+
7+
def down
8+
remove_column :events, :ce_hours_details, if_exists: true
9+
remove_column :events, :ce_hours_details_label, if_exists: true
10+
end
11+
end

db/schema.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,8 @@
492492
t.boolean "autoshow_title", default: true, null: false
493493
t.boolean "autoshow_videoconference_label", default: true, null: false
494494
t.boolean "autoshow_videoconference_link", default: true, null: false
495+
t.text "ce_hours_details"
496+
t.string "ce_hours_details_label", default: "CE hours", null: false
495497
t.integer "cost_cents"
496498
t.datetime "created_at", null: false
497499
t.integer "created_by_id"

spec/requests/events_spec.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,23 @@
111111
end
112112
end
113113

114+
describe "GET /ce_hours" do
115+
let(:event) { create(:event, :published, :publicly_visible) }
116+
117+
it "renders the CE hours page when details are present" do
118+
event.update!(ce_hours_details_label: "Continuing education", ce_hours_details: "<p>Email your license number</p>")
119+
get ce_hours_event_path(event)
120+
expect(response).to have_http_status(:ok)
121+
expect(response.body).to include("Continuing education")
122+
expect(response.body).to include("Email your license number")
123+
end
124+
125+
it "redirects to the event when details are blank" do
126+
get ce_hours_event_path(event)
127+
expect(response).to redirect_to(event_path(event))
128+
end
129+
end
130+
114131
describe "GET /new" do
115132
context "as admin" do
116133
it "renders successfully" do
@@ -156,6 +173,13 @@
156173
expect(response.body).to include("event[event_details_label]")
157174
expect(response.body).to include("event[event_details]")
158175
end
176+
177+
it "renders the 'CE hours' toggle with the details fields" do
178+
get edit_event_path(event)
179+
expect(response.body).to include("CE hours")
180+
expect(response.body).to include("event[ce_hours_details_label]")
181+
expect(response.body).to include("event[ce_hours_details]")
182+
end
159183
end
160184

161185
describe "POST /create" do

0 commit comments

Comments
 (0)