- <%= link_to @event.title, edit_event_path(@event), class: "text-sm font-semibold text-gray-500 uppercase tracking-wide hover:text-gray-700" %>
+ <%= link_to @event.decorate.compact_label, edit_event_path(@event), title: @event.title, class: "text-sm font-semibold text-gray-500 uppercase tracking-wide hover:text-gray-700" %>
<% if @event.start_date.present? %>
<% end %>
diff --git a/app/views/events/dashboard.html.erb b/app/views/events/dashboard.html.erb
index bbafbc2861..1746b86b5d 100644
--- a/app/views/events/dashboard.html.erb
+++ b/app/views/events/dashboard.html.erb
@@ -8,7 +8,7 @@
<%# Centered title block %>
- <%= link_to @event.title, edit_event_path(@event), class: "text-sm font-semibold text-gray-500 uppercase tracking-wide hover:text-gray-700" %>
+ <%= link_to @event.decorate.compact_label, edit_event_path(@event), title: @event.title, class: "text-sm font-semibold text-gray-500 uppercase tracking-wide hover:text-gray-700" %>
<% if @event.start_date.present? %>
<%= @event.date_range %>
diff --git a/app/views/events/revenue.html.erb b/app/views/events/revenue.html.erb
index 9c5d6626f6..b7366887ed 100644
--- a/app/views/events/revenue.html.erb
+++ b/app/views/events/revenue.html.erb
@@ -112,7 +112,7 @@
- <%= row.event.title %>
+ <%= row.event.compact_label %>
<%= row.event.date_range %>
diff --git a/app/views/organizations/sections/_events.html.erb b/app/views/organizations/sections/_events.html.erb
index b3949771c8..03a1b83403 100644
--- a/app/views/organizations/sections/_events.html.erb
+++ b/app/views/organizations/sections/_events.html.erb
@@ -1,5 +1,23 @@
<%= turbo_frame_tag "organization_events_section" do %>
<% if events.any? %>
+ <% if allowed_to?(:manage?, Organization) %>
+ <% decorated_org = organization.decorate %>
+
+
Program status by event
+
+ <% events.each do |event| %>
+ <% status = decorated_org.facilitator_status_as_of(event.start_date) %>
+ <%= link_to event_path(event),
+ title: event.title,
+ data: { turbo_frame: "_top" },
+ class: "inline-flex items-center rounded-full text-xs font-medium border px-2.5 py-0.5 #{OrganizationDecorator.program_status_classes(status)}" do %>
+ <%= status.to_s.titleize %> · <%= event.decorate.compact_label.truncate(24) %>
+ <% end %>
+ <% end %>
+
+
+ <% end %>
+
<% events.each do |event| %>
<% decorated = event.decorate %>
diff --git a/db/migrate/20260715041414_add_abbreviation_to_events.rb b/db/migrate/20260715041414_add_abbreviation_to_events.rb
new file mode 100644
index 0000000000..b59d52aa7e
--- /dev/null
+++ b/db/migrate/20260715041414_add_abbreviation_to_events.rb
@@ -0,0 +1,9 @@
+class AddAbbreviationToEvents < ActiveRecord::Migration[8.1]
+ def up
+ add_column :events, :abbreviation, :string unless column_exists?(:events, :abbreviation)
+ end
+
+ def down
+ remove_column :events, :abbreviation, if_exists: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 39ed76ab04..f2a6ddb14b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -522,6 +522,7 @@
end
create_table "events", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.string "abbreviation"
t.boolean "autoshow_cost", default: true, null: false
t.boolean "autoshow_date", default: true, null: false
t.boolean "autoshow_location", default: true, null: false
diff --git a/spec/decorators/event_decorator_spec.rb b/spec/decorators/event_decorator_spec.rb
index 8d248cfedf..a71071d298 100644
--- a/spec/decorators/event_decorator_spec.rb
+++ b/spec/decorators/event_decorator_spec.rb
@@ -23,6 +23,18 @@
end
end
+ describe "#compact_label" do
+ it "returns the abbreviation when present" do
+ event = build(:event, title: "Trauma-Informed Onsite", abbreviation: "TOS205").decorate
+ expect(event.compact_label).to eq("TOS205")
+ end
+
+ it "falls back to the full title when abbreviation is blank" do
+ event = build(:event, title: "Trauma-Informed Onsite", abbreviation: "").decorate
+ expect(event.compact_label).to eq("Trauma-Informed Onsite")
+ end
+ end
+
describe "#videoconference_room" do
it "pulls the Zoom meeting ID from the join URL and groups the digits" do
event = build(:event, videoconference_url: "https://awbw-org.zoom.us/j/88285411273").decorate
diff --git a/spec/decorators/organization_decorator_spec.rb b/spec/decorators/organization_decorator_spec.rb
index 638447c9bf..69502ce06d 100644
--- a/spec/decorators/organization_decorator_spec.rb
+++ b/spec/decorators/organization_decorator_spec.rb
@@ -42,6 +42,36 @@
end
end
+ describe "#facilitator_status_as_of" do
+ let(:organization) { create(:organization) }
+ let(:person) { create(:person) }
+ let(:reference) { Date.new(2026, 6, 1) }
+
+ it "is :new when there are no facilitator affiliations starting before the date" do
+ expect(organization.decorate.facilitator_status_as_of(reference)).to eq(:new)
+ end
+
+ it "ignores facilitator affiliations that start on or after the date" do
+ create(:affiliation, organization: organization, person: person, title: "Facilitator", start_date: reference)
+ expect(organization.reload.decorate.facilitator_status_as_of(reference)).to eq(:new)
+ end
+
+ it "is :ongoing when an earlier facilitator affiliation is still active on the date" do
+ create(:affiliation, organization: organization, person: person, title: "Facilitator", start_date: reference - 1.year, end_date: nil)
+ expect(organization.reload.decorate.facilitator_status_as_of(reference)).to eq(:ongoing)
+ end
+
+ it "is :reinstated when earlier facilitator affiliations all ended before the date" do
+ create(:affiliation, organization: organization, person: person, title: "Facilitator", start_date: reference - 2.years, end_date: reference - 1.year)
+ expect(organization.reload.decorate.facilitator_status_as_of(reference)).to eq(:reinstated)
+ end
+
+ it "ignores non-facilitator affiliations" do
+ create(:affiliation, organization: organization, person: person, title: "Volunteer", start_date: reference - 1.year, end_date: nil)
+ expect(organization.reload.decorate.facilitator_status_as_of(reference)).to eq(:new)
+ end
+ end
+
describe "#agency_type_option" do
it "returns a recognized type unchanged" do
organization = create(:organization, agency_type: "For-profit")
diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb
index bf08395bd9..187f746484 100644
--- a/spec/requests/events_spec.rb
+++ b/spec/requests/events_spec.rb
@@ -121,6 +121,14 @@ def offer_ce!(target_event)
expect(response.body).not_to include("Free open house")
end
+ it "labels an event by its abbreviation when set, keeping the full title as a tooltip" do
+ paid_training.update!(abbreviation: "TAC261")
+ sign_in admin
+ get revenue_events_path
+ expect(response.body).to include("TAC261")
+ expect(response.body).to include('title="TAC 261"')
+ end
+
it "returns to the originating event's dashboard when arrived from it" do
sign_in admin
get revenue_events_path(return_to: "dashboard", event_id: paid_training.id)
diff --git a/spec/requests/organizations_events_section_spec.rb b/spec/requests/organizations_events_section_spec.rb
index 68277bece3..35e5af8a63 100644
--- a/spec/requests/organizations_events_section_spec.rb
+++ b/spec/requests/organizations_events_section_spec.rb
@@ -47,13 +47,31 @@ def register(event:, status: "registered")
end
it "lists each event once even when several members attended it" do
- event = create(:event, title: "Shared Event")
+ event = create(:event, title: "Shared Event", abbreviation: "SE1")
register(event: event)
register(event: event)
get_events_section
- expect(response.body.scan("Shared Event").size).to eq(1)
+ # One event card, and one deduped program-status chip (keyed by its
+ # abbreviation) — not one per registration. The card renders the title as a
+ # text node (">Shared Event"); the chip references it only in a title="…"
+ # tooltip, so the card count keys off the leading ">".
+ expect(response.body.scan(/>\s*Shared Event/).size).to eq(1)
+ expect(response.body.scan("SE1").size).to eq(1)
+ end
+
+ it "shows an admin program-status chip labeled with the event abbreviation" do
+ event = create(:event, title: "Trauma-Informed Onsite", abbreviation: "TOS205", start_date: 2.days.from_now)
+ person = create(:person)
+ create(:affiliation, organization: organization, person: person, title: "Facilitator", start_date: 1.year.ago, end_date: nil)
+ registration = create(:event_registration, registrant: person, event: event, status: "registered")
+ registration.event_registration_organizations.create!(organization: organization)
+
+ get_events_section
+
+ expect(response.body).to include("TOS205")
+ expect(response.body).to include("Ongoing")
end
it "renders the section heading and lazy frame on the profile page" do