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
16 changes: 16 additions & 0 deletions app/decorators/event_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/views/events/_payment_badge.html.erb
Original file line number Diff line number Diff line change
@@ -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 %>
<i class="fa-solid fa-circle-check text-green-500"></i> 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 %>
<i class="fa-solid fa-circle-exclamation text-orange-500"></i> Paid β€” <%= dollars_from_cents(payment.amount_cents_remaining) %> unallocated
<% end %>
<% else %>
<span 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 shrink-0">
Expand Down
125 changes: 63 additions & 62 deletions app/views/events/bulk_payments/ticket.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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
%>

<div class="max-w-2xl mx-auto px-4 pt-4">
Expand Down Expand Up @@ -59,79 +63,67 @@
</div>
<% end %>

<!-- Event Image + Title -->
<div class="text-center space-y-4">
<div class="inline-block rounded-lg overflow-hidden border border-gray-200 shadow-sm">
<!-- Event context (what the payment is for). Image stretches to span the
three text rows: title, date/time, cost. -->
<div class="flex items-stretch gap-3 rounded-lg border border-gray-200 bg-white px-4 py-3 text-left">
<div class="w-16 shrink-0 overflow-hidden rounded">
<%= render "assets/display_image",
resource: @event,
width: 48, height: 32,
width: "full", height: "full",
variant: :index,
link: event_path(@event),
file: @event.display_image %>
</div>

<div>
<% if @event.respond_to?(:pre_title) && @event.pre_title.present? %>
<p class="text-base font-semibold text-gray-700 mb-1" style="font-family: 'Telefon Bold', sans-serif"><%= @event.pre_title %></p>
<div class="min-w-0 self-center">
<p class="text-base font-bold text-green-800 leading-tight"><%= link_to @event.title, event_path(@event), class: "hover:underline" %></p>
<p class="text-xs font-semibold text-blue-900 uppercase mt-1"><%= @event.short_date_range %></p>
<% if @event.labelled_cost.present? %>
<p class="text-xs font-semibold text-blue-900 uppercase mt-0.5"><%= @event.labelled_cost %></p>
<% end %>

<h2 class="text-3xl font-bold text-green-800 leading-tight" style="font-family: Lato, sans-serif"><%= link_to @event.title, event_path(@event), class: "hover:underline" %></h2>
</div>
</div>

<!-- Divider (perforation style) -->
<!-- Divider (perforation "ticket tear" separating the event from the
payer/payment portion) -->
<div class="relative my-6">
<div class="border-t border-dashed border-gray-300"></div>
<div class="absolute -left-3 top-1/2 -translate-y-1/2 w-6 h-6 bg-gray-100 rounded-full"></div>
<div class="absolute -right-3 top-1/2 -translate-y-1/2 w-6 h-6 bg-gray-100 rounded-full"></div>
</div>

<!-- Event Details -->
<div class="text-center space-y-2">
<div class="text-xl font-bold text-blue-900 uppercase" style="font-family: Lato, sans-serif"><%= @event.times(display_day: true, display_date: true, styled: true) %></div>

<% if @event.labelled_cost.present? %>
<div class="text-lg font-bold text-purple-900 uppercase" style="font-family: Lato, sans-serif"><%= @event.labelled_cost %></div>
<% end %>

<% if @event.location.present? %>
<div class="text-base text-gray-700"><%= @event.location.name %></div>
<% end %>
</div>

<!-- Payment status -->
<div class="text-center mt-2">
<% if @payment %>
<span class="inline-flex items-center gap-1 rounded-full text-sm font-medium border px-3 py-1 bg-green-50 text-green-700 border-green-200">
<i class="fa-solid fa-circle-check text-green-500"></i> Payment received<% if total_cents.to_i > 0 %> β€” <%= dollars_from_cents(total_cents) %><% end %>
</span>
<% else %>
<span class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-800">
<i class="fa-solid fa-clock text-amber-500"></i> Payment pending<% if total_cents.to_i > 0 %> β€” <%= dollars_from_cents(total_cents) %><% end %>
</span>
<% end %>
</div>

<!-- Invoice -->
<%= 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 %>
<i class="fa-solid fa-file-invoice-dollar text-blue-500 text-xl shrink-0"></i>

<div class="flex-1 min-w-0 text-left">
<p class="font-semibold text-blue-900">View invoice</p>
<p class="text-sm text-blue-700">Itemized invoice for this payment</p>
<!-- Payer + payment status: the payer is who's viewing this ticket, so the
payer and what they owe are the headline. -->
<div class="text-center space-y-3">
<div class="space-y-1">
<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wide">Payer</h3>
<p class="text-2xl font-bold text-gray-900 break-words"><%= payer_name.presence || "β€”" %></p>
<% if payer_organization.present? %>
<p class="text-sm text-gray-500 break-words"><%= payer_organization %></p>
<% end %>
<p class="inline-flex items-center gap-1.5 rounded-full bg-purple-50 text-purple-700 text-sm font-medium px-3 py-1">
<i class="fa-solid fa-users text-purple-400"></i>
Covering <%= attendee_count %> <%= "registrant".pluralize(attendee_count) %>
</p>
</div>

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

<!-- Divider -->
<div class="border-t border-gray-200"></div>
<!-- Payment status -->
<div>
<% if @payment %>
<span class="inline-flex items-center gap-1 rounded-full text-sm font-medium border px-3 py-1 bg-green-50 text-green-700 border-green-200">
<i class="fa-solid fa-circle-check text-green-500"></i> Payment received<% if total_cents.to_i > 0 %> β€” <%= dollars_from_cents(total_cents) %><% end %>
</span>
<% else %>
<span class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-800">
<i class="fa-solid fa-clock text-amber-500"></i> Payment pending<% if total_cents.to_i > 0 %> β€” <%= dollars_from_cents(total_cents) %><% end %>
</span>
<% end %>
</div>
</div>

<!-- Registrants -->
<!-- Registrant details -->
<div>
<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wide text-center mb-3">
Registrants<% if attendees.any? %> (<%= attendees.size %>)<% end %>
Registrant details<% if attendees.any? %> (<%= attendees.size %> of <%= attendee_count %>)<% end %>
</h3>
<% if attendees.any? %>
<div class="overflow-hidden border border-gray-200 rounded-lg bg-white">
Expand All @@ -157,28 +149,37 @@
<% end %>
</div>

<!-- View submission (full form responses) -->
<%= 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 %>
<i class="fa-solid fa-file-lines text-gray-400 text-xl shrink-0"></i>
<!-- Divider -->
<div class="border-t border-gray-200"></div>

<!-- Invoice -->
<%= 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 %>
<i class="fa-solid fa-file-invoice-dollar text-blue-500 text-xl shrink-0"></i>

<div class="flex-1 min-w-0 text-left">
<p class="font-semibold text-gray-800">View submission</p>
<p class="text-sm text-gray-500">The <%= Form::BULK_PAYMENT_PUBLIC_NAME %> form you submitted</p>
<p class="font-semibold text-blue-900">View invoice</p>
<p class="text-sm text-blue-700">Itemized invoice for this payment</p>
</div>

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

<!-- Resend confirmation -->
<div class="text-center">
<!-- Secondary text actions: form responses + resend confirmation -->
<div class="flex flex-col items-center gap-1.5">
<%= 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" %>
</div>

<% if allowed_to?(:dashboard?, @event) %>
<div class="admin-only rounded-lg border border-blue-200 bg-blue-50/50 px-4 py-4 space-y-3">
<div class="admin-only relative rounded-lg border border-blue-200 bg-blue-50/50 px-4 pt-6 pb-4 space-y-3">
<span class="absolute -top-3 left-4 inline-flex items-center gap-1 rounded-full border border-blue-200 bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-700">
<i class="fa-solid fa-lock text-[0.65rem]"></i> Admin only
</span>
<div class="flex items-center justify-between gap-2">
<h2 class="text-xs font-medium text-gray-400 uppercase tracking-wide">Payment allocations</h2>
<%= render "events/payment_badge", payment: @payment %>
Expand Down
27 changes: 27 additions & 0 deletions spec/decorators/event_decorator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion spec/requests/events/bulk_payments_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -284,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

Expand Down