From b7cf6d2db0356e4be0386267f3421f4b2e52c8d2 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 22 Jul 2026 08:42:32 -0400 Subject: [PATCH 1/4] Rework bulk payment ticket around the payer, not the event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The viewer of a bulk payment ticket is the payer covering other people, not an attendee — so the ticket now leads with the event context card, then the payer identity (name, organization, count of registrants covered) and payment status as the headline, followed by the per-person registrant details and the grouped invoice/submission document links. Drops the field labels on the payer block in favor of a prominent name + secondary org line. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../events/bulk_payments/ticket.html.erb | 135 ++++++++++-------- spec/requests/events/bulk_payments_spec.rb | 17 +++ 2 files changed, 90 insertions(+), 62 deletions(-) diff --git a/app/views/events/bulk_payments/ticket.html.erb b/app/views/events/bulk_payments/ticket.html.erb index 111d0b8194..2b106524c7 100644 --- a/app/views/events/bulk_payments/ticket.html.erb +++ b/app/views/events/bulk_payments/ticket.html.erb @@ -2,6 +2,10 @@ <% attendees = @submission.bulk_payment_attendees total_cents = @payment&.amount_cents || @submission.bulk_payment_amount_cents(@event) + answers = @submission.answers_by_identifier + payer_name = [ answers["payer_first_name"], answers["payer_last_name"] ].compact_blank.join(" ").presence || @submission.person.name + payer_organization = answers["payer_organization"] + attendee_count = @submission.bulk_payment_attendee_count %>
@@ -59,23 +63,55 @@
<% end %> - -
-
+ +
+
<%= render "assets/display_image", resource: @event, - width: 48, height: 32, + width: "full", height: "full", variant: :index, link: event_path(@event), file: @event.display_image %>
+
+

<%= link_to @event.title, event_path(@event), class: "hover:underline" %>

+

<%= @event.times(display_day: true, display_date: true, styled: true) %>

+ <% if @event.labelled_cost.present? %> +

<%= @event.labelled_cost %>

+ <% end %> +
+
-
- <% if @event.respond_to?(:pre_title) && @event.pre_title.present? %> -

<%= @event.pre_title %>

+ +
+ + +
+
+

Payer

+

<%= payer_name.presence || "—" %>

+ <% if payer_organization.present? %> +

<%= payer_organization %>

<% end %> +

+ + Covering <%= attendee_count %> <%= "registrant".pluralize(attendee_count) %> +

+
-

<%= link_to @event.title, event_path(@event), class: "hover:underline" %>

+ +
+ <% if @payment %> + + Payment received<% if total_cents.to_i > 0 %> — <%= dollars_from_cents(total_cents) %><% end %> + + <% else %> + + Payment pending<% if total_cents.to_i > 0 %> — <%= dollars_from_cents(total_cents) %><% end %> + + <% end %>
@@ -86,52 +122,10 @@
- -
-
<%= @event.times(display_day: true, display_date: true, styled: true) %>
- - <% if @event.labelled_cost.present? %> -
<%= @event.labelled_cost %>
- <% end %> - - <% if @event.location.present? %> -
<%= @event.location.name %>
- <% end %> -
- - -
- <% if @payment %> - - Payment received<% if total_cents.to_i > 0 %> — <%= dollars_from_cents(total_cents) %><% end %> - - <% else %> - - Payment pending<% if total_cents.to_i > 0 %> — <%= dollars_from_cents(total_cents) %><% end %> - - <% end %> -
- - - <%= link_to event_invoice_path(@event, submission_id: @submission.id, return_to: "bulk_payment_ticket"), - class: "flex items-center gap-3 rounded-xl border-2 border-blue-200 bg-blue-50 px-4 py-3 hover:bg-blue-100 transition-colors" do %> - - -
-

View invoice

-

Itemized invoice for this payment

-
- - - <% end %> - - -
- - +

- Registrants<% if attendees.any? %> (<%= attendees.size %>)<% end %> + Registrant details<% if attendees.any? %> (<%= attendees.size %>)<% end %>

<% if attendees.any? %>
@@ -157,18 +151,35 @@ <% end %>
- - <%= link_to event_bulk_payment_path(@event, reg: @submission.slug), - class: "flex items-center gap-3 rounded-xl border-2 border-gray-200 bg-white px-4 py-3 hover:bg-gray-50 transition-colors" do %> - + +
-
-

View submission

-

The <%= Form::BULK_PAYMENT_PUBLIC_NAME %> form you submitted

-
+ +
+ <%= link_to event_invoice_path(@event, submission_id: @submission.id, return_to: "bulk_payment_ticket"), + class: "flex items-center gap-3 rounded-xl border-2 border-blue-200 bg-blue-50 px-4 py-3 hover:bg-blue-100 transition-colors" do %> + - - <% end %> +
+

View invoice

+

Itemized invoice for this payment

+
+ + + <% end %> + + <%= link_to event_bulk_payment_path(@event, reg: @submission.slug), + class: "flex items-center gap-3 rounded-xl border-2 border-gray-200 bg-white px-4 py-3 hover:bg-gray-50 transition-colors" do %> + + +
+

View submission

+

The <%= Form::BULK_PAYMENT_PUBLIC_NAME %> form you submitted

+
+ + + <% end %> +
diff --git a/spec/requests/events/bulk_payments_spec.rb b/spec/requests/events/bulk_payments_spec.rb index 4fa2d23cc7..16b9c29165 100644 --- a/spec/requests/events/bulk_payments_spec.rb +++ b/spec/requests/events/bulk_payments_spec.rb @@ -272,6 +272,23 @@ def get_ticket expect(response.body).to include("jordan@example.com") end + it "shows the payer's name, organization, and registrants-covered count" do + submission.form_answers.create!(form_field: payer_first_name_field, submitted_answer: "Alex", + question_name_when_answered: "Payer first name") + submission.form_answers.create!(form_field: payer_last_name_field, submitted_answer: "Chen", + question_name_when_answered: "Payer last name") + submission.form_answers.create!(form_field: org_field, submitted_answer: "Bright Futures Academy", + question_name_when_answered: "Organization") + + get_ticket + + expect(response.body).to include("Payer") + expect(response.body).to include("Alex Chen") + expect(response.body).to include("Bright Futures Academy") + # One attendee is listed and no explicit count is set, so the covered count is 1. + expect(response.body).to include("Covering 1 registrant") + end + it "does not show per-person actions like cancelling a registration" do get_ticket From bd55dda3b520bea2eb6e642569b0f46af1209feb Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 22 Jul 2026 09:07:00 -0400 Subject: [PATCH 2/4] Refine bulk payment ticket dividers, count, and admin allocations badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move the perforation "ticket tear" up so it separates the event card from the payer/payment portion, and drop the divider between the payer and the registrant list (they're one unit — the payer covers these registrants). - Registrant details count now reads "(listed of covered)", e.g. "(3 of 3)", since the covered total is already stated above. - Add an explicit "Admin only" badge to the (already admin-gated) payment allocations box. - Payment badge: paid-but-unallocated now reads "unallocated" (not "remaining") and is orange to signal it needs action. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/events/_payment_badge.html.erb | 4 ++-- .../events/bulk_payments/ticket.html.erb | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/views/events/_payment_badge.html.erb b/app/views/events/_payment_badge.html.erb index 96d83d4cf2..a69aeb9bda 100644 --- a/app/views/events/_payment_badge.html.erb +++ b/app/views/events/_payment_badge.html.erb @@ -1,8 +1,8 @@ <% if payment %> <% if payment.amount_cents_remaining > 0 %> <%= link_to payment_path(payment), - class: "inline-flex items-center gap-1 rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 hover:bg-green-200 shrink-0" do %> - Paid — <%= dollars_from_cents(payment.amount_cents_remaining) %> remaining + class: "inline-flex items-center gap-1 rounded-full bg-orange-100 px-2.5 py-0.5 text-xs font-medium text-orange-800 hover:bg-orange-200 shrink-0" do %> + Paid — <%= dollars_from_cents(payment.amount_cents_remaining) %> unallocated <% end %> <% else %> diff --git a/app/views/events/bulk_payments/ticket.html.erb b/app/views/events/bulk_payments/ticket.html.erb index 2b106524c7..3c381f335c 100644 --- a/app/views/events/bulk_payments/ticket.html.erb +++ b/app/views/events/bulk_payments/ticket.html.erb @@ -83,8 +83,13 @@
- -
+ +
+
+
+
+
@@ -115,17 +120,10 @@
- -
-
-
-
-
-

- Registrant details<% if attendees.any? %> (<%= attendees.size %>)<% end %> + Registrant details<% if attendees.any? %> (<%= attendees.size %> of <%= attendee_count %>)<% end %>

<% if attendees.any? %>
@@ -189,7 +187,10 @@
<% if allowed_to?(:dashboard?, @event) %> -
+
+ + Admin only +

Payment allocations

<%= render "events/payment_badge", payment: @payment %> From 7a29c77dadd107cb28989e59229424629c6d6dad Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 22 Jul 2026 09:16:48 -0400 Subject: [PATCH 3/4] Simplify bulk payment ticket: form-responses link + tidier event date/time - Replace the "View submission" card with a plain "View your form responses" text link grouped with the resend-confirmation action, matching the registration ticket. Leaves the invoice as the sole document card. - Event card date/time now reads on one row as " (

<%= link_to @event.title, event_path(@event), class: "hover:underline" %>

-

<%= @event.times(display_day: true, display_date: true, styled: true) %>

+

<%= @event.date_range %> (<%= @event.times %>)

<% if @event.labelled_cost.present? %> -

<%= @event.labelled_cost %>

+

<%= @event.labelled_cost %>

<% end %>
@@ -152,35 +152,24 @@
- -
- <%= link_to event_invoice_path(@event, submission_id: @submission.id, return_to: "bulk_payment_ticket"), - class: "flex items-center gap-3 rounded-xl border-2 border-blue-200 bg-blue-50 px-4 py-3 hover:bg-blue-100 transition-colors" do %> - + + <%= link_to event_invoice_path(@event, submission_id: @submission.id, return_to: "bulk_payment_ticket"), + class: "flex items-center gap-3 rounded-xl border-2 border-blue-200 bg-blue-50 px-4 py-3 hover:bg-blue-100 transition-colors" do %> + -
-

View invoice

-

Itemized invoice for this payment

-
- - - <% end %> - - <%= link_to event_bulk_payment_path(@event, reg: @submission.slug), - class: "flex items-center gap-3 rounded-xl border-2 border-gray-200 bg-white px-4 py-3 hover:bg-gray-50 transition-colors" do %> - - -
-

View submission

-

The <%= Form::BULK_PAYMENT_PUBLIC_NAME %> form you submitted

-
+
+

View invoice

+

Itemized invoice for this payment

+
- - <% end %> -
+ + <% end %> - -
+ +
+ <%= link_to "View your form responses", + event_bulk_payment_path(@event, reg: @submission.slug), + class: "text-sm text-gray-500 hover:text-blue-600 underline" %> <%= button_to "Resend confirmation email", bulk_payment_resend_confirmation_path(@submission.slug), class: "text-sm text-gray-500 hover:text-blue-600 underline bg-transparent border-0 cursor-pointer p-0" %> diff --git a/spec/requests/events/bulk_payments_spec.rb b/spec/requests/events/bulk_payments_spec.rb index 16b9c29165..711a6c32a2 100644 --- a/spec/requests/events/bulk_payments_spec.rb +++ b/spec/requests/events/bulk_payments_spec.rb @@ -301,9 +301,10 @@ def get_ticket expect(response.body).to include("return_to=bulk_payment_ticket") end - it "links 'View submission' to the public submission page" do + it "links 'View your form responses' to the public submission page" do get_ticket + expect(response.body).to include("View your form responses") expect(response.body).to include(event_bulk_payment_path(event, reg: submission.slug)) end From 553ad668101ed03f63d1706e0d7b116bc56d0d6c Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 22 Jul 2026 09:20:30 -0400 Subject: [PATCH 4/4] Show the event date range without weekday on the bulk payment ticket Add EventDecorator#short_date_range (same collapsed range as date_range but without the weekday prefix) and use it for the compact event card, where the weekday and inline time were noise. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/event_decorator.rb | 16 +++++++++++ .../events/bulk_payments/ticket.html.erb | 2 +- spec/decorators/event_decorator_spec.rb | 27 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/app/decorators/event_decorator.rb b/app/decorators/event_decorator.rb index 442ca544fe..814d46ec51 100644 --- a/app/decorators/event_decorator.rb +++ b/app/decorators/event_decorator.rb @@ -58,6 +58,22 @@ def date_range end end + # Same collapsed range as `date_range` but without the weekday prefix + # (e.g. "Sep 20-21, 2026") — for tighter contexts where the weekday is noise. + def short_date_range + s = start_date.in_time_zone(Time.zone) + e = (end_date || start_date).in_time_zone(Time.zone) + return s.strftime("%b %-d, %Y") if s.to_date == e.to_date + + if s.year == e.year && s.month == e.month + "#{s.strftime('%b')} #{s.strftime('%-d')}-#{e.strftime('%-d')}, #{s.strftime('%Y')}" + elsif s.year == e.year + "#{s.strftime('%b %-d')} - #{e.strftime('%b %-d')}, #{s.strftime('%Y')}" + else + "#{s.strftime('%b %-d, %Y')} - #{e.strftime('%b %-d, %Y')}" + end + end + def detail(length: nil) length ? description&.truncate(length) : description end diff --git a/app/views/events/bulk_payments/ticket.html.erb b/app/views/events/bulk_payments/ticket.html.erb index 54f8722d3f..d3df32527f 100644 --- a/app/views/events/bulk_payments/ticket.html.erb +++ b/app/views/events/bulk_payments/ticket.html.erb @@ -76,7 +76,7 @@

<%= link_to @event.title, event_path(@event), class: "hover:underline" %>

-

<%= @event.date_range %> (<%= @event.times %>)

+

<%= @event.short_date_range %>

<% if @event.labelled_cost.present? %>

<%= @event.labelled_cost %>

<% end %> diff --git a/spec/decorators/event_decorator_spec.rb b/spec/decorators/event_decorator_spec.rb index a71071d298..dc53a71588 100644 --- a/spec/decorators/event_decorator_spec.rb +++ b/spec/decorators/event_decorator_spec.rb @@ -111,6 +111,33 @@ end end + describe "#short_date_range" do + it "shows a single date without a weekday when there is no end date" do + event = build(:event, start_date: Time.zone.local(2026, 6, 11, 12), end_date: nil).decorate + expect(event.short_date_range).to eq("Jun 11, 2026") + end + + it "shows a single date when start and end fall on the same day" do + event = build(:event, start_date: Time.zone.local(2026, 6, 11, 9), end_date: Time.zone.local(2026, 6, 11, 17)).decorate + expect(event.short_date_range).to eq("Jun 11, 2026") + end + + it "collapses month and year for a same-month range" do + event = build(:event, start_date: Time.zone.local(2026, 9, 20, 9), end_date: Time.zone.local(2026, 9, 21, 17)).decorate + expect(event.short_date_range).to eq("Sep 20-21, 2026") + end + + it "collapses only the year for a same-year, cross-month range" do + event = build(:event, start_date: Time.zone.local(2026, 6, 30, 9), end_date: Time.zone.local(2026, 7, 2, 17)).decorate + expect(event.short_date_range).to eq("Jun 30 - Jul 2, 2026") + end + + it "shows both years for a cross-year range" do + event = build(:event, start_date: Time.zone.local(2025, 12, 31, 9), end_date: Time.zone.local(2026, 1, 2, 17)).decorate + expect(event.short_date_range).to eq("Dec 31, 2025 - Jan 2, 2026") + end + end + describe "#labelled_cost" do it "returns nil when cost_cents is nil" do event = build(:event, cost_cents: nil).decorate