Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
class EventsController < ApplicationController
include AhoyTracking, TagAssignable
skip_before_action :authenticate_user!, only: [ :index, :show, :staff, :details ]
skip_before_action :authenticate_user!, only: [ :index, :show, :staff, :details, :ce_hours ]
skip_before_action :verify_authenticity_token, only: [ :preview ]
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 ]
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 ]

def index
authorize!
Expand Down Expand Up @@ -104,6 +104,20 @@ def details
@event = @event.decorate
end

# Public CE hours page (continuing education requirements, payment, sign-in
# rules). Linked from the registration ticket. When no details are set there
# is nothing to show, so fall back to the event page.
def ce_hours
authorize! @event, to: :ce_hours?

if @event.ce_hours_details.blank?
redirect_to event_path(@event, reg: params[:reg].presence)
return
end

@event = @event.decorate
end

def staff
authorize! @event, to: :staff?
@event = @event.decorate
Expand Down
7 changes: 7 additions & 0 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ def event_details_label
super.presence || "Before you attend"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Mirrors event_details_label — falls back to the default even when an admin clears the field, so the section never renders unlabelled.

end

# Heading shown on the CE hours ticket call-out and its details page. Falls
# back to the default even when an admin clears it, so the section never
# renders unlabelled.
def ce_hours_details_label
super.presence || "CE hours"
end

# Virtual attributes for date/time inputs (Firefox datetime-local compat)
attr_writer :start_date_date, :start_date_time,
:end_date_date, :end_date_time,
Expand Down
3 changes: 3 additions & 0 deletions app/policies/event_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ def google_analytics?
:rhino_description,
:event_details,
:event_details_label,
:ce_hours_details,
:ce_hours_details_label,
:autoshow_cost,
:autoshow_date,
:autoshow_location,
Expand Down Expand Up @@ -150,6 +152,7 @@ def google_analytics?

alias_rule :preview?, to: :edit?
alias_rule :details?, to: :show?
alias_rule :ce_hours?, to: :show?

private

Expand Down
15 changes: 15 additions & 0 deletions app/views/event_registrations/_ticket.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@
<% end %>
<% end %>

<!-- CE hours (continuing education requirements, payment, sign-in rules) -->
<% if event_registration.event.ce_hours_details.present? %>
<%= link_to ce_hours_event_path(event_registration.event, reg: event_registration.slug),
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 %>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Intentionally indigo + graduation-cap to distinguish this call-out from the amber "Before you attend" one directly above, so two stacked call-outs on a ticket read as different things.

<i class="fa-solid fa-graduation-cap text-indigo-500 text-xl shrink-0"></i>

<div class="flex-1 min-w-0 text-left">
<p class="font-semibold text-indigo-900"><%= event_registration.event.ce_hours_details_label %></p>
<p class="text-sm text-indigo-700">Continuing education hours — requirements &amp; next steps</p>
</div>

<i class="fa-solid fa-arrow-right text-indigo-500 shrink-0"></i>
<% end %>
<% end %>

<!-- Additional forms the registrant asked for during registration -->
<% if event_registration.w9_requested? %>
<%= link_to "/documents/awbw-w9.pdf",
Expand Down
37 changes: 37 additions & 0 deletions app/views/events/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,43 @@
</div>
</div>

<%# ---- CE HOURS (ticket call-out + details page) ---- %>
<div class="ce-hours-section" data-controller="dropdown">
<button
type="button"
id="ce_hours_button"
class="
flex items-center justify-between cursor-pointer w-full
bg-gray-500 text-white hover:bg-gray-300 px-3 py-2 rounded-md"
data-action="dropdown#toggle"
data-dropdown-payload-param='[{"ce_hours_details":"hidden"}, {"ce_hours_arrow":"rotate-180"}, {"ce_hours_button":"rounded-md rounded-t-md"}]'>
CE hours
<i id="ce_hours_arrow" class="fa-solid fa-chevron-down w-4 h-4 transition-transform duration-300"></i>
</button>

<div id="ce_hours_details" class="hidden border border-gray-300 p-4 rounded-b-md">
<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>

<div class="form-group">
<%= f.label :ce_hours_details_label, "CE hours section label", class: "block font-medium mb-1 text-gray-700" %>
<%= f.text_field :ce_hours_details_label,
placeholder: "CE hours",
class: "w-full rounded border-gray-300 shadow-sm px-3 py-1.5 text-sm focus:ring-blue-500 focus:border-blue-500" %>
<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>
</div>

<div class="form-group mt-6">
<%= f.label :ce_hours_details, "CE hours content", class: "block font-medium mb-1 text-gray-700" %>
<p class="text-xs text-gray-500 mb-1">
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.
</p>
<%= f.text_area :ce_hours_details, rows: 8,
placeholder: "e.g. <p>AWBW is approved by CAMFT…</p><h3>Before the training</h3><ul><li>Email your license number</li></ul>",
class: "w-full rounded border-gray-300 shadow-sm px-3 py-2 text-sm font-mono" %>
</div>
</div>
</div>

<div class="images-section" data-controller="dropdown">
<button
type="button"
Expand Down
33 changes: 33 additions & 0 deletions app/views/events/ce_hours.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<% content_for(:page_bg_class, "public") %>

<% reg_slug = params[:reg].presence %>
<% back_path = reg_slug ? registration_ticket_path(reg_slug) : event_path(@event) %>
<% back_label = reg_slug ? "Back to ticket" : "Back to event" %>
<div class="max-w-3xl mx-auto px-4 pt-4">
<%= 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 %>
<i class="fa-solid fa-arrow-left text-xs"></i> <%= back_label %>
<% end %>
</div>

<div class="max-w-3xl mx-auto pb-12 pt-6 px-4">
<div class="bg-white rounded-2xl shadow-xl ring-1 ring-black/5 overflow-hidden">
<div class="relative bg-gradient-to-r from-purple-950 to-purple-700 text-white px-6 sm:px-8 py-5">
<div class="flex items-center gap-5">
<div class="shrink-0">
<%= image_tag("logo.png", alt: "Organization logo", class: "h-9 w-auto") %>
</div>
<div class="flex-1">
<h1 class="text-xl font-semibold tracking-wide leading-tight"><%= @event.ce_hours_details_label %></h1>
<p class="text-sm text-blue-200"><%= @event.title %></p>
</div>
</div>
<div class="absolute inset-x-0 bottom-0 h-1 bg-gradient-to-r from-accent via-amber-400 to-accent"></div>
</div>

<div class="px-6 sm:px-8 py-7">
<div class="rich-label prose max-w-none text-gray-700 leading-relaxed prose-headings:text-gray-800 prose-a:text-blue-700">
<%= form_label_html(@event.ce_hours_details) %>
</div>
</div>
</div>
</div>
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
get :background
get :registrants
get :details
get :ce_hours
get :staff
get "staff/edit", action: :edit_staff, as: :edit_staff
patch "staff", action: :update_staff
Expand Down
11 changes: 11 additions & 0 deletions db/migrate/20260617120000_add_ce_hours_details_to_events.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class AddCeHoursDetailsToEvents < ActiveRecord::Migration[8.1]
def up
add_column :events, :ce_hours_details, :text
add_column :events, :ce_hours_details_label, :string, default: "CE hours", null: false
end

def down
remove_column :events, :ce_hours_details, if_exists: true
remove_column :events, :ce_hours_details_label, if_exists: true
end
end
2 changes: 2 additions & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,8 @@
t.boolean "autoshow_title", default: true, null: false
t.boolean "autoshow_videoconference_label", default: true, null: false
t.boolean "autoshow_videoconference_link", default: true, null: false
t.text "ce_hours_details"
t.string "ce_hours_details_label", default: "CE hours", null: false
t.integer "cost_cents"
t.datetime "created_at", null: false
t.integer "created_by_id"
Expand Down
70 changes: 55 additions & 15 deletions db/seeds/dev/events_management.rb
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,46 @@
HTML
end

# Seed the "CE hours" details — the continuing-education requirements, payment,
# and sign-in rules that used to live in a long CE confirmation email. Shown on its
# own ticket-linked page (and via the prominent indigo call-out on the ticket) for
# registrants who requested CE credit. A custom label demonstrates that the heading
# is admin-editable. Only set when blank so admin edits survive a re-seed.
if flagship && flagship.ce_hours_details.blank?
flagship.update!(ce_hours_details_label: "Continuing education", ce_hours_details: <<~HTML.strip)
<p>This training is approved by the California Association of Marriage and Family Therapists (CAMFT, provider #000000) for <strong>12 CE hours</strong>. AWBW is approved to sponsor continuing education for LMFTs, LCSWs, LPCCs, and LEPs.</p>
<h3>Before the training</h3>
<ul>
<li>Provide your license type and license number when you register — we cannot issue a CE certificate without it.</li>
<li>CE hours carry a separate $25 processing fee, payable with your registration.</li>
</ul>
<h3>During the training</h3>
<ul>
<li>You must sign in and out each day. CE hours are awarded only for full attendance — partial credit is not available.</li>
<li>Arrive on time; late arrivals or early departures may forfeit CE eligibility.</li>
</ul>
<h3>After the training</h3>
<ul>
<li>Complete the post-training evaluation within one week.</li>
<li>Your CE certificate will be emailed within three weeks of the training.</li>
</ul>
<p>Questions about continuing education? Reach out and our team will follow up with details.</p>
HTML
end

# The trauma-informed training also offers CE hours, with the default label.
trauma = Event.find_by(title: "Facilitator Training: Trauma-Informed Art Practices")
if trauma && trauma.ce_hours_details.blank?
trauma.update!(ce_hours_details: <<~HTML.strip)
<p>This training is approved by CAMFT (provider #000000) for <strong>18 CE hours</strong> across its three days.</p>
<ul>
<li>Provide your license type and number at registration; a $25 CE processing fee applies.</li>
<li>Daily sign-in/sign-out is required — CE hours are awarded for full attendance only.</li>
<li>Complete the post-training evaluation to receive your certificate within three weeks.</li>
</ul>
HTML
end

puts "Creating Event Registrations…"

# Key people for named scenarios
Expand Down Expand Up @@ -384,9 +424,9 @@
# Kim Davis: cancelled (has user)
if facilitator_training
[
{ person: amy_person, status: "registered", scholarship_requested: true, w9_requested: true, invoice_requested: true },
{ person: maria_j, status: "registered", invoice_requested: true },
{ person: anna_g, status: "attended" },
{ person: amy_person, status: "registered", scholarship_requested: true, w9_requested: true, invoice_requested: true, ce_credit_requested: true },
{ person: maria_j, status: "registered", invoice_requested: true, ce_credit_requested: true },
{ person: anna_g, status: "attended", ce_credit_requested: true },
{ person: mario_j, status: "registered" },
{ person: kim_d, status: "cancelled" }
].each do |data|
Expand All @@ -402,8 +442,8 @@
# Linda Williams: no_show (no user)
if trauma_training
[
{ person: sarah_s, status: "registered", invoice_requested: true },
{ person: jessica_b, status: "registered", scholarship_requested: true },
{ person: sarah_s, status: "registered", invoice_requested: true, ce_credit_requested: true },
{ person: jessica_b, status: "registered", scholarship_requested: true, ce_credit_requested: true },
{ person: angel_g, status: "registered" },
{ person: linda_w, status: "no_show" }
].each do |data|
Expand Down Expand Up @@ -461,16 +501,16 @@
# Create all registrations
registrations_data.each do |data|
next unless data[:event] && data[:person]
next if EventRegistration.exists?(event: data[:event], registrant: data[:person])

EventRegistration.create!(
event: data[:event],
registrant: data[:person],
status: data[:status] || "registered",
scholarship_requested: data[:scholarship_requested] || false,
w9_requested: data[:w9_requested] || false,
invoice_requested: data[:invoice_requested] || false
)

registration = EventRegistration.find_or_initialize_by(event: data[:event], registrant: data[:person])
registration.status = data[:status] || "registered" if registration.new_record?
registration.scholarship_requested ||= data[:scholarship_requested] || false
# Keep the request flags in sync on re-seed so the named scenarios survive an
# existing DB (find_or_initialize no longer recreates these registrations).
registration.w9_requested = data[:w9_requested] || false
registration.invoice_requested = data[:invoice_requested] || false
registration.ce_credit_requested = data[:ce_credit_requested] || false
registration.save!
end

# Give the flagship training its demo cohort: top up to 10 active registrants with
Expand Down
24 changes: 24 additions & 0 deletions spec/requests/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,23 @@
end
end

describe "GET /ce_hours" do
let(:event) { create(:event, :published, :publicly_visible) }

it "renders the CE hours page when details are present" do
event.update!(ce_hours_details_label: "Continuing education", ce_hours_details: "<p>Email your license number</p>")
get ce_hours_event_path(event)
expect(response).to have_http_status(:ok)
expect(response.body).to include("Continuing education")
expect(response.body).to include("Email your license number")
end

it "redirects to the event when details are blank" do
get ce_hours_event_path(event)
expect(response).to redirect_to(event_path(event))
end
end

describe "GET /new" do
context "as admin" do
it "renders successfully" do
Expand Down Expand Up @@ -156,6 +173,13 @@
expect(response.body).to include("event[event_details_label]")
expect(response.body).to include("event[event_details]")
end

it "renders the 'CE hours' toggle with the details fields" do
get edit_event_path(event)
expect(response.body).to include("CE hours")
expect(response.body).to include("event[ce_hours_details_label]")
expect(response.body).to include("event[ce_hours_details]")
end
end

describe "POST /create" do
Expand Down
1 change: 1 addition & 0 deletions spec/views/page_bg_class_alignment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
"app/views/events/registrations/show.html.erb" => "public",
"app/views/events/registrations/invoice.html.erb" => "public",
"app/views/events/details.html.erb" => "public",
"app/views/events/ce_hours.html.erb" => "public",

# ─── bulk payment views ───
"app/views/events/bulk_payments/new.html.erb" => "public",
Expand Down