From 968f2aad0e42e18f3fa6659a3768c8eba8b5dccc Mon Sep 17 00:00:00 2001
From: Mae Beale
Date: Fri, 23 Oct 2026 10:51:15 -0400
Subject: [PATCH 1/5] Start: let reconcile handle cancelled training
registrants
Co-Authored-By: Claude Opus 5.5 (1M context)
From a5ab7b9b24bc868255962e54366d950ea52faa0d Mon Sep 17 00:00:00 2001
From: Mae Beale
Date: Fri, 23 Oct 2026 10:55:32 -0400
Subject: [PATCH 2/5] Settle cancelled training registrants' affiliations
before the event
A registrant who cancels will never attend, so waiting for the training to end
before removing the affiliation it minted left a stale facilitator row in place
and, worse, a pre-event run offered to re-create one. Rows minted off an upcoming
training also read as 'Upcoming' rather than active, which filed them as already
ended instead of pending.
Co-Authored-By: Claude Opus 5.5 (1M context)
---
.../reconcile_affiliations_controller.rb | 3 +-
.../affiliation_services/reconcile_person.rb | 25 ++++-
.../_person_group.html.erb | 64 +++++++++++++
.../reconcile_affiliations/index.html.erb | 93 +++++--------------
...ncile-cancelled-registrants-right-away.yml | 13 +++
.../events/reconcile_affiliations_spec.rb | 22 +++++
.../reconcile_person_spec.rb | 50 ++++++++++
7 files changed, 196 insertions(+), 74 deletions(-)
create mode 100644 app/views/events/reconcile_affiliations/_person_group.html.erb
create mode 100644 db/seeds/features/2026-10-23-reconcile-cancelled-registrants-right-away.yml
diff --git a/app/controllers/events/reconcile_affiliations_controller.rb b/app/controllers/events/reconcile_affiliations_controller.rb
index 80d9769915..0cd6e29b2e 100644
--- a/app/controllers/events/reconcile_affiliations_controller.rb
+++ b/app/controllers/events/reconcile_affiliations_controller.rb
@@ -10,7 +10,8 @@ def index
track_view("events.reconcile_affiliations", { event_id: @event.id })
reconcile = AffiliationServices::ReconcileEvent.new(@event)
- @person_groups = reconcile.actionable_person_groups
+ @cancelled_groups, @person_groups = reconcile.actionable_person_groups
+ .partition { |group| group[:registration]&.status == "cancelled" }
@skipped_sections = reconcile.skipped_reason_sections
@has_rows = reconcile.any_rows?
# Restore the admin's per-row radio choices when they come back from confirm.
diff --git a/app/services/affiliation_services/reconcile_person.rb b/app/services/affiliation_services/reconcile_person.rb
index 2e0cdaeafb..15099c07ad 100644
--- a/app/services/affiliation_services/reconcile_person.rb
+++ b/app/services/affiliation_services/reconcile_person.rb
@@ -26,6 +26,7 @@ def actionable?
# ended without a dedicated column (ADR-0003 D6b).
COMMENT_TOPIC = "Reconciliation".freeze
NOT_ATTENDED = "Didn't attend — no affiliation created".freeze
+ CANCELLED = "Cancelled — no affiliation created".freeze
# `registered` after the event is a gap in the record, not an outcome:
# #attendance_recorded? deliberately excludes it. Acting on it would be acting
# on missing data, so the row is surfaced for an admin to resolve instead.
@@ -204,6 +205,10 @@ def transferred_out?
registration_here&.transferred_out?
end
+ def cancelled_here?
+ registration_here&.status == "cancelled"
+ end
+
# A transfer isn't a failure — they're training somewhere else. The affiliation
# follows them: it re-dates to the destination event and re-points at that
# registration, rather than being ended or deleted (ADR-0003 D6d).
@@ -251,11 +256,11 @@ def needs_affiliation?
def classify(affiliation)
if completed_training?
- return Decision.new(affiliation:, action: :noop, reason: ACTIVE_ATTENDED) if affiliation.active?
+ return Decision.new(affiliation:, action: :noop, reason: ACTIVE_ATTENDED) if open?(affiliation)
return Decision.new(affiliation:, action: :reactivate) if ended_for_this_training?(affiliation)
Decision.new(affiliation:, action: :noop, reason: LAPSED)
- elsif !affiliation.active?
+ elsif !open?(affiliation)
reason = ended_by_reconciliation?(affiliation) ? ALREADY_DEACTIVATED : ALREADY_ENDED
Decision.new(affiliation:, action: :noop, reason:)
elsif attendance_unrecorded?
@@ -271,6 +276,12 @@ def classify(affiliation)
end
end
+ # A row minted off an upcoming training starts on its date, so it reads as
+ # upcoming until then — still open, not ended.
+ def open?(affiliation)
+ affiliation.active? || affiliation.upcoming?
+ end
+
# Only the row this training minted, and only when the person didn't turn up at
# all. A partial attendance is kept as an auditable record that they were here —
# it confers no facilitation either way (ADR-0001 D8a), but the row and its
@@ -284,12 +295,18 @@ def partially_attended?
end
# Waits for the governing training to end, so a pre-event run never deactivates.
- # A hand-entered row has no source training, so it waits on this event.
+ # A hand-entered row has no source training, so it waits on this event. A
+ # cancellation settles this event's outcome early, so its rows are ready at once.
def deactivation_ready?(affiliation)
- affiliation.event_registration_id ? affiliation.event_registration&.event&.ended? : @event.ended?
+ if affiliation.event_registration_id && !minted_here?(affiliation)
+ return affiliation.event_registration&.event&.ended?
+ end
+
+ @event.ended? || cancelled_here?
end
def creation_decision
+ return Decision.new(action: :noop, reason: CANCELLED) if cancelled_here? && !completed_training?
return Decision.new(action: :create) if !@event.ended? || completed_training?
Decision.new(action: :noop, reason: NOT_ATTENDED)
diff --git a/app/views/events/reconcile_affiliations/_person_group.html.erb b/app/views/events/reconcile_affiliations/_person_group.html.erb
new file mode 100644
index 0000000000..ba2e69f138
--- /dev/null
+++ b/app/views/events/reconcile_affiliations/_person_group.html.erb
@@ -0,0 +1,64 @@
+<% color_class = {
+ red: "has-[:checked]:bg-red-50 has-[:checked]:text-red-700 has-[:checked]:border-red-300",
+ green: "has-[:checked]:bg-green-50 has-[:checked]:text-green-700 has-[:checked]:border-green-300",
+ blue: "has-[:checked]:bg-blue-50 has-[:checked]:text-blue-700 has-[:checked]:border-blue-300",
+ gray: "has-[:checked]:bg-gray-100 has-[:checked]:text-gray-800 has-[:checked]:border-gray-400"
+ } %>
+<% fa_hint = "facilitator affiliation (FA)" %>
+<% outcome_options = {
+ create: [ [ "create", "Create new FA", :blue, fa_hint ], [ "keep", "Skip", :gray ] ],
+ deactivate: [ [ "keep", "Keep active", :green ], [ "delete", "Delete affiliation", :red ], [ "deactivate", "Deactivate affiliation", :red ] ],
+ delete: [ [ "delete", "Delete", :red ], [ "keep", "Keep", :gray ] ],
+ reactivate: [ [ "reactivate", "Reactivate", :green ], [ "create", "Create new FA", :blue, fa_hint ], [ "keep", "Leave inactive", :gray ] ],
+ retarget: [ [ "retarget", "Move to the new event", :blue ], [ "keep", "Leave as is", :gray ] ]
+ } %>
+<% button_base = "group relative inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-500 shadow-sm cursor-pointer hover:bg-gray-50" %>
+
+
+
+ <% group[:rows].each do |row| %>
+ <%# A row this training created has nothing to preserve, so Delete leads and
+ Deactivate is shown but not selectable — the tooltip says why. %>
+ <% default_choice = row.action == :deactivate && row.minted_here ? "delete" : row.action.to_s %>
+ <% chosen = (pre_outcome&.dig(row.key)) || default_choice %>
+
+ <% outcome_options[row.action].each do |value, label, color, hint| %>
+ <% unavailable = value == "deactivate" && row.minted_here %>
+
+ <% end %>
+
+
+ <% end %>
+
+
diff --git a/app/views/events/reconcile_affiliations/index.html.erb b/app/views/events/reconcile_affiliations/index.html.erb
index db43aa5550..658ddf9e92 100644
--- a/app/views/events/reconcile_affiliations/index.html.erb
+++ b/app/views/events/reconcile_affiliations/index.html.erb
@@ -18,7 +18,8 @@
later marked attended, an ending this step applied is undone rather than leaving them with a
second affiliation for an engagement that never broke. A return after a real lapse is still a
new affiliation, so the lapse stays visible. Anyone still marked Registered is left alone and listed
- below — that's an unfilled roster, not an outcome. Job affiliations are never touched.
+ below — that's an unfilled roster, not an outcome. A cancellation doesn't wait for the training to end:
+ it's listed in its own section so the affiliation can be settled right away. Job affiliations are never touched.
Only the row this training created is deleted — it recorded an assumption that never came true. An
@@ -35,7 +36,7 @@
<% if @event.affiliations_reconciled_at %>
Last reconciled <%= @event.affiliations_reconciled_at.to_fs(:long) %>.
<% end %>
- <% if @person_groups.any? && @event.affiliations_reconciliation_stale? %>
+ <% if (@cancelled_groups.any? || @person_groups.any?) && @event.affiliations_reconciliation_stale? %>
Attendance has changed since the last reconciliation — apply again below to bring affiliations up to date.
<% end %>
@@ -53,24 +54,8 @@
<%# One set of day ticks per registration, wherever it appears: the turbo replace
targets them by id, and a person with two linked orgs has rows in both sections. %>
<% ticked = Set.new %>
- <% if @person_groups.any? %>
- <% actionable_count = @person_groups.sum { |g| g[:rows].size } %>
- <% color_class = {
- red: "has-[:checked]:bg-red-50 has-[:checked]:text-red-700 has-[:checked]:border-red-300",
- green: "has-[:checked]:bg-green-50 has-[:checked]:text-green-700 has-[:checked]:border-green-300",
- blue: "has-[:checked]:bg-blue-50 has-[:checked]:text-blue-700 has-[:checked]:border-blue-300",
- gray: "has-[:checked]:bg-gray-100 has-[:checked]:text-gray-800 has-[:checked]:border-gray-400"
- } %>
- <% fa_hint = "facilitator affiliation (FA)" %>
- <% outcome_options = {
- create: [ [ "create", "Create new FA", :blue, fa_hint ], [ "keep", "Skip", :gray ] ],
- deactivate: [ [ "keep", "Keep active", :green ], [ "delete", "Delete affiliation", :red ], [ "deactivate", "Deactivate affiliation", :red ] ],
- delete: [ [ "delete", "Delete", :red ], [ "keep", "Keep", :gray ] ],
- reactivate: [ [ "reactivate", "Reactivate", :green ], [ "create", "Create new FA", :blue, fa_hint ], [ "keep", "Leave inactive", :gray ] ],
- retarget: [ [ "retarget", "Move to the new event", :blue ], [ "keep", "Leave as is", :gray ] ]
- } %>
- <% button_base = "group relative inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-500 shadow-sm cursor-pointer hover:bg-gray-50" %>
-
+ <% if @cancelled_groups.any? || @person_groups.any? %>
+ <% actionable_count = (@cancelled_groups + @person_groups).sum { |g| g[:rows].size } %>
To reconcile (<%= actionable_count %>)
@@ -82,58 +67,28 @@
can't nest — so the radios join it via the HTML form= attribute. %>
<%= form_with url: reconcile_affiliations_event_path(@event), method: :post, id: "reconcile_form", data: { turbo: false } do %><% end %>
-
- <% group[:rows].each do |row| %>
- <%# A row this training created has nothing to preserve, so Delete leads and
- Deactivate is shown but not selectable — the tooltip says why. %>
- <% default_choice = row.action == :deactivate && row.minted_here ? "delete" : row.action.to_s %>
- <% chosen = (@pre_outcome && @pre_outcome[row.key]) || default_choice %>
-
<%= link_to "Cancel", registrants_event_path(@event), class: "text-sm #{eyebrow_link_class}" %>
diff --git a/db/seeds/features/2026-10-23-reconcile-cancelled-registrants-right-away.yml b/db/seeds/features/2026-10-23-reconcile-cancelled-registrants-right-away.yml
new file mode 100644
index 0000000000..fed22c9d14
--- /dev/null
+++ b/db/seeds/features/2026-10-23-reconcile-cancelled-registrants-right-away.yml
@@ -0,0 +1,13 @@
+name: "Reconcile cancelled registrants before the training"
+area: events
+display_status: admin_facing
+released_on: 2026-10-23
+action_path: "/events/1/reconcile_affiliations"
+pr_number: 2803
+summary: >-
+ Reconcile affiliations now lists cancelled registrants in their own section, so
+ the facilitator affiliation their registration created can be deleted right away
+ instead of after the training ends.
+pro_tips:
+ - "Someone who cancelled and never trained should have the affiliation deleted, not deactivated — there's no facilitation history to keep."
+ - "Registrants still marked Registered are listed under Not reconciled for reference until the training ends."
diff --git a/spec/requests/events/reconcile_affiliations_spec.rb b/spec/requests/events/reconcile_affiliations_spec.rb
index 6afa653173..fe02ed745d 100644
--- a/spec/requests/events/reconcile_affiliations_spec.rb
+++ b/spec/requests/events/reconcile_affiliations_spec.rb
@@ -53,6 +53,28 @@ def registrant_with_affiliation(status:)
expect(response.body).to include("Create new")
end
+ it "gives cancelled registrants their own section before the event, with the deletion preselected" do
+ upcoming = create(:event, facilitator_training: true, start_date: 3.days.from_now, end_date: 5.days.from_now)
+ cancelled = create(:event_registration, event: upcoming, status: "cancelled")
+ registered = create(:event_registration, event: upcoming, status: "registered")
+ [ cancelled, registered ].each do |reg|
+ create(:event_registration_organization, event_registration: reg, organization: organization)
+ end
+ minted = create(:affiliation, person: cancelled.registrant, organization: organization, title: "Facilitator",
+ start_date: upcoming.starts_on, event_registration: cancelled)
+ create(:affiliation, person: registered.registrant, organization: organization, title: "Facilitator",
+ start_date: upcoming.starts_on, event_registration: registered)
+
+ get reconcile_affiliations_event_path(upcoming)
+
+ page = Nokogiri::HTML(response.body)
+ section = page.at_css("#reconcile_cancelled")
+ expect(section.text).to include(cancelled.registrant.name)
+ expect(section.text).not_to include(registered.registrant.name)
+ expect(section.at_css("#outcome_aff\\:#{minted.id}_delete")["checked"]).to be_present
+ expect(page.text).to include("Training hasn't ended yet")
+ end
+
it "previews a facilitator affiliation on a non-training event as a deletion" do
non_training = create(:event, :ended, facilitator_training: false)
person = create(:person)
diff --git a/spec/services/affiliation_services/reconcile_person_spec.rb b/spec/services/affiliation_services/reconcile_person_spec.rb
index 6de512cb8a..431b12bb03 100644
--- a/spec/services/affiliation_services/reconcile_person_spec.rb
+++ b/spec/services/affiliation_services/reconcile_person_spec.rb
@@ -47,6 +47,27 @@ def reconcile(registration, **options)
expect(Affiliation.exists?(affiliation.id)).to be(false)
end
+ it "deletes the minted row as soon as the person cancels, without waiting for the training to end" do
+ reg = training_registration(status: "cancelled", ended: false)
+ affiliation = owned_facilitator(registration: reg, start_date: Date.current)
+
+ reconcile(reg)
+
+ expect(Affiliation.exists?(affiliation.id)).to be(false)
+ end
+
+ it "waits on another upcoming training's row even when this registration is cancelled" do
+ reg = training_registration(status: "cancelled", ended: false)
+ other = training_registration(status: "registered", ended: false)
+ affiliation = owned_facilitator(registration: other, start_date: Date.current)
+
+ plan = described_class.new(person: person, organization: organization, event: reg.event,
+ registration: reg, include_unowned: true).plan
+
+ expect(plan.map(&:reason)).to eq([ described_class::TRAINING_PENDING ])
+ expect(affiliation.reload).to be_active
+ end
+
# They were here for part of it. Deleting the row would erase that; ending it
# records a facilitator term that was brief rather than one that never began.
it "ends the minted row rather than deleting it for a partial attendance" do
@@ -86,6 +107,25 @@ def reconcile(registration, **options)
expect(plan.first.reason).to eq(described_class::TRAINING_PENDING)
end
+ it "files a row starting on an upcoming training's date as pending, not as already ended" do
+ reg = training_registration(status: "registered", ended: false)
+ owned_facilitator(registration: reg)
+
+ plan = described_class.new(person: person, organization: organization,
+ event: reg.event, registration: reg).plan
+
+ expect(plan.map(&:reason)).to eq([ described_class::TRAINING_PENDING ])
+ end
+
+ it "deletes an upcoming training's row once the person cancels" do
+ reg = training_registration(status: "cancelled", ended: false)
+ affiliation = owned_facilitator(registration: reg)
+
+ reconcile(reg)
+
+ expect(Affiliation.exists?(affiliation.id)).to be(false)
+ end
+
it "deletes it on the day a one-day training ends, with no reliance on the inactive flag" do
event = create(:event, facilitator_training: true, start_date: 3.hours.ago,
end_date: 1.hour.ago, registration_close_date: 4.hours.ago)
@@ -459,6 +499,16 @@ def transferred_out(destination_event:, link_org: organization)
.to change { person.affiliations.facilitators.where(organization: organization).count }.by(1)
end
+ it "proposes no affiliation for a registrant who cancelled before the training" do
+ reg = training_registration(status: "cancelled", ended: false)
+
+ plan = described_class.new(person: person, organization: organization, event: reg.event,
+ registration: reg).plan
+
+ expect(plan.map(&:action)).to eq([ :noop ])
+ expect(plan.first.reason).to eq(described_class::CANCELLED)
+ end
+
it "proposes nothing when the caller passes no registration to own the new row" do
reg = training_registration(status: "attended")
From 231eb6b1648a844bc56b82445537a62501fa37a2 Mon Sep 17 00:00:00 2001
From: Mae Beale
Date: Fri, 23 Oct 2026 11:00:00 -0400
Subject: [PATCH 3/5] Note job affiliations wherever reconcile offers a delete
Deleting a facilitator row can leave the person with no link to the
organization at all; showing whether a job affiliation remains lets the admin
see that before choosing.
Co-Authored-By: Claude Opus 5.5 (1M context)
---
.../affiliation_services/reconcile_event.rb | 13 ++++-
.../_person_group.html.erb | 24 ++++++---
...ncile-cancelled-registrants-right-away.yml | 1 +
.../events/reconcile_affiliations_spec.rb | 49 +++++++++++++++++++
4 files changed, 77 insertions(+), 10 deletions(-)
diff --git a/app/services/affiliation_services/reconcile_event.rb b/app/services/affiliation_services/reconcile_event.rb
index 513ac38de5..7fed6ba6ee 100644
--- a/app/services/affiliation_services/reconcile_event.rb
+++ b/app/services/affiliation_services/reconcile_event.rb
@@ -16,10 +16,12 @@ def initialize(event)
end
# `other_facilitators` are the person's active facilitator affiliations with orgs
- # they did NOT link on this event.
+ # they did NOT link on this event; `jobs_by_org` their open job affiliations with
+ # the orgs they did.
def actionable_person_groups
all_rows.select(&:actionable?).group_by(&:person).map do |person, rows|
- { person:, registration: rows.first.registration, rows:, other_facilitators: other_facilitators(person) }
+ { person:, registration: rows.first.registration, rows:, other_facilitators: other_facilitators(person),
+ jobs_by_org: jobs_by_org(person) }
end
end
@@ -123,6 +125,13 @@ def other_facilitators(person)
.to_a
end
+ def jobs_by_org(person)
+ person.affiliations.active_or_pending
+ .where(organization_id: linked_org_ids(person))
+ .reject(&:facilitator?)
+ .group_by(&:organization_id)
+ end
+
def linked_org_ids(person)
linked_organizations(registrations_by_person[person]).map(&:id)
end
diff --git a/app/views/events/reconcile_affiliations/_person_group.html.erb b/app/views/events/reconcile_affiliations/_person_group.html.erb
index ba2e69f138..bc912ec3fe 100644
--- a/app/views/events/reconcile_affiliations/_person_group.html.erb
+++ b/app/views/events/reconcile_affiliations/_person_group.html.erb
@@ -38,16 +38,24 @@
<%# A row this training created has nothing to preserve, so Delete leads and
Deactivate is shown but not selectable — the tooltip says why. %>
<% default_choice = row.action == :deactivate && row.minted_here ? "delete" : row.action.to_s %>
- <% chosen = (pre_outcome&.dig(row.key)) || default_choice %>
+ <% chosen = pre_outcome&.dig(row.key) || default_choice %>
<% if reason == AffiliationServices::ReconcilePerson::ATTENDANCE_NOT_RECORDED && ticked.add?(row.registration&.id) %>
<%= render "attendance_day_ticks", registration: row.registration, event: @event %>
<% end %>
diff --git a/db/seeds/features/2026-10-23-reconcile-cancelled-registrants-right-away.yml b/db/seeds/features/2026-10-23-reconcile-cancelled-registrants-right-away.yml
index 29da51f57d..3f75e1896e 100644
--- a/db/seeds/features/2026-10-23-reconcile-cancelled-registrants-right-away.yml
+++ b/db/seeds/features/2026-10-23-reconcile-cancelled-registrants-right-away.yml
@@ -11,4 +11,4 @@ summary: >-
pro_tips:
- "Someone who cancelled and never trained should have the affiliation deleted, not deactivated — there's no facilitation history to keep."
- "Registrants still marked Registered are listed under Not reconciled for reference until the training ends."
- - "Any row offering Delete notes whether the person still has a job affiliation with that organization, so you know if deleting leaves them unlinked."
+ - "Rows for cancelled, no-show, and transferred registrants — and any row offering Delete — note whether the person still has a job affiliation with that organization."
diff --git a/spec/requests/events/reconcile_affiliations_spec.rb b/spec/requests/events/reconcile_affiliations_spec.rb
index 27a61db153..3311626005 100644
--- a/spec/requests/events/reconcile_affiliations_spec.rb
+++ b/spec/requests/events/reconcile_affiliations_spec.rb
@@ -124,6 +124,34 @@ def registrant_with_affiliation(status:)
expect(response.body).not_to include("job affiliation")
end
+ it "notes job affiliations for a transferred-out registrant" do
+ person, _affiliation = registrant_with_affiliation(status: "registered")
+ source = person.event_registrations.find_by(event: event)
+ destination_event = create(:event, facilitator_training: true, start_date: 3.months.from_now,
+ end_date: 3.months.from_now + 1.day, registration_close_date: 2.months.from_now)
+ destination = create(:event_registration, registrant: person, event: destination_event, status: "registered")
+ create(:event_registration_organization, event_registration: destination, organization: organization)
+ destination.update!(transferred_from_registration: source)
+ source.update!(status: "transferred_out")
+ create(:affiliation, person: person, organization: organization, title: "Counselor", start_date: 1.year.ago.to_date)
+
+ get reconcile_affiliations_event_path(event)
+
+ expect(response.body).to include("Move to the new event")
+ expect(response.body).to include("Job affiliation: Counselor")
+ end
+
+ it "notes job affiliations on a no-show listed under Not reconciled" do
+ person = create(:person)
+ reg = create(:event_registration, event: event, registrant: person, status: "no_show")
+ create(:event_registration_organization, event_registration: reg, organization: organization)
+
+ get reconcile_affiliations_event_path(event)
+
+ expect(response.body).to include("Didn't attend — no affiliation created")
+ expect(response.body).to include("No job affiliation")
+ end
+
it "previews a facilitator affiliation on a non-training event as a deletion" do
non_training = create(:event, :ended, facilitator_training: false)
person = create(:person)
From cb099fe89e0baf49daf6113f8d0273b118ecb3c2 Mon Sep 17 00:00:00 2001
From: Mae Beale
Date: Fri, 23 Oct 2026 11:12:58 -0400
Subject: [PATCH 5/5] Show each job affiliation's dates in the reconcile note
Whether a job affiliation is current, upcoming, or about to end changes what
deleting the facilitator row leaves behind.
Co-Authored-By: Claude Opus 5.5 (1M context)
---
.../reconcile_affiliations/_job_note.html.erb | 3 ++-
spec/requests/events/reconcile_affiliations_spec.rb | 13 ++++++++++++-
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/app/views/events/reconcile_affiliations/_job_note.html.erb b/app/views/events/reconcile_affiliations/_job_note.html.erb
index 220f53f6df..c62c72a664 100644
--- a/app/views/events/reconcile_affiliations/_job_note.html.erb
+++ b/app/views/events/reconcile_affiliations/_job_note.html.erb
@@ -1,3 +1,4 @@
+<% labels = jobs.map { |job| "#{job.title.presence || "Untitled"} · #{job.decorate.period_label}" } %>