diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e7b7afbda9..217463f976 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -54,6 +54,7 @@ When changing a model or controller, check whether these related files need upda - Avoid `update_all` unless explicitly intended - Prefer service objects under app/services/ - Prefer POROs over concerns when possible +- **Prefer decorators (Draper, app/decorators/) over view helpers for model-specific presentation** — when display logic is "about a record" (labels, badges, formatted attributes, status pills), put it on that model's decorator and call `record.decorate.thing`. Reserve `app/helpers/` for generic, cross-model view utilities that aren't tied to one model. Decorators keep presentation testable and out of ERB. - Use `after_commit` instead of `after_save` for side effects ## RuboCop (rubocop-rails-omakase) diff --git a/CLAUDE.md b/CLAUDE.md index dbd052f2ff..40356d2cb1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,6 +54,7 @@ When changing a model or controller, check whether these related files need upda - Avoid `update_all` unless explicitly intended - Prefer service objects under app/services/ - Prefer POROs over concerns when possible +- **Prefer decorators (Draper, app/decorators/) over view helpers for model-specific presentation** — when display logic is "about a record" (labels, badges, formatted attributes, status pills), put it on that model's decorator and call `record.decorate.thing`. Reserve `app/helpers/` for generic, cross-model view utilities that aren't tied to one model. Decorators keep presentation testable and out of ERB. - Use `after_commit` instead of `after_save` for side effects ## RuboCop (rubocop-rails-omakase) diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 74648f3805..feacac6827 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -82,11 +82,15 @@ def registrants authorize! @event, to: :registrants? @event = @event.decorate scope = @event.event_registrations - .includes(:comments, :organizations, registrant: [ :user, :contact_methods, { avatar_attachment: :blob }, { affiliations: :organization } ]) + .includes(:comments, :organizations, { continuing_education_registrations: [ :professional_license, :allocations ] }, registrant: [ :user, :contact_methods, { avatar_attachment: :blob }, { affiliations: :organization } ]) .joins(:registrant) scope = scope.keyword(params[:keyword]) if params[:keyword].present? scope = scope.payment_status(params[:payment_status]) if params[:payment_status].present? scope = scope.scholarship_status(params[:scholarship]) if params[:scholarship].present? + scope = scope.ce_status(params[:ce_status]) if params[:ce_status].present? + scope = scope.comment_status(params[:comment_status]) if params[:comment_status].present? + scope = scope.organization_status(params[:org_status], @event) if params[:org_status].present? + scope = scope.account_status(params[:account_status]) if params[:account_status].present? scope = scope.registrant_ids(params[:registrant_ids]) if params[:registrant_ids].present? scope = scope.registrant_state(params[:state]) if params[:state].present? scope = scope.registrant_county(params[:county]) if params[:county].present? @@ -105,6 +109,7 @@ def registrants @event_registrations = scope.order(Arel.sql("people.first_name, people.last_name")) @dashboard = EventDashboard.new(@event) + @ce_eligible = @event.ce_eligible? emails = @event_registrations.map { |r| r.registrant.preferred_email&.downcase }.compact @duplicate_emails = emails.tally.select { |_, count| count > 1 }.keys.to_set @@ -314,6 +319,7 @@ def create_bulk_payment def preview_reminder authorize! @event @event = @event.decorate + @ce_eligible = @event.ce_eligible? @event_registrations = @event.event_registrations .includes( :event, :organizations, :comments, @@ -325,8 +331,11 @@ def preview_reminder # Filters keep every registrant in the list and only flag who still matches, # so the recipient checkboxes pre-check the matched set rather than removing - # rows. See app/views/events/_reminder_recipients.html.erb. - recipient_filter = ReminderRecipientFilter.new(@event_registrations, params) + # rows. See app/views/events/_reminder_recipients.html.erb. The dropdown + # filters reuse the registrants-roster scopes (via the event), so both pages + # stay in sync; @dashboard supplies the state/county options. + @dashboard = EventDashboard.new(@event) + recipient_filter = ReminderRecipientFilter.new(@event_registrations, params, event: @event) @matched_ids = recipient_filter.matched_ids @filtering = recipient_filter.filtering? @@ -535,15 +544,17 @@ def allocated_cents_by_registration(registrations) def event_registrations_csv_string require "csv" cost_required = @event.cost_cents.to_i > 0 + include_ce = @event.ce_eligible? headers = [ "First name", "Last name", "Email", "Phone", "Organization", "Scholarship recipient", "Scholarship tasks completed", "Payment status", "Intends to pay", "Payment total" ] + headers << "CE status" if include_ce CSV.generate(headers: headers, write_headers: true) do |csv_out| @event_registrations.each do |registration| - csv_out << event_registration_csv_row(registration, cost_required) + csv_out << event_registration_csv_row(registration, cost_required, include_ce) end end end - def event_registration_csv_row(registration, cost_required) + def event_registration_csv_row(registration, cost_required, include_ce = false) person = registration.registrant orgs = person.affiliations .select { |a| !a.inactive? && (a.end_date.nil? || a.end_date >= Date.current) } @@ -552,7 +563,7 @@ def event_registration_csv_row(registration, cost_required) total_cents = registration.allocations_sum payment_total = total_cents.positive? ? format("%.2f", total_cents / 100.0) : "" payment_status = cost_required ? registration.payment_status_label : "" - [ + row = [ person.first_name, person.last_name, person.preferred_email.presence || "", @@ -564,17 +575,20 @@ def event_registration_csv_row(registration, cost_required) registration.intends_to_pay? ? "Yes" : "No", payment_total ] + row << registration.ce_status_label.to_s if include_ce + row end def onboarding_csv_string require "csv" cost_required = @event.cost_cents.to_i > 0 + include_ce = @event.ce_eligible? day_count = @event.day_count headers = [ "First name", "Last name", "Email", "Organization", "Program type" ] headers += [ "Payment status", "Fees due", "Paid amount" ] if cost_required headers << "Fee note" headers += [ "Discounted amount", "Scholarship amount", "Scholarship grant", "Scholarship tasks completed" ] - headers += [ "CE requested", "CE hours", "CE amount", "CE license" ] + headers += [ "CE requested", "CE hours", "CE amount", "CE license" ] if include_ce headers += EventRegistration::CHECKLIST_STEPS.values headers += [ "Portal user status", "Portal access" ] headers += (1..day_count).map { |day| "Day #{day}" } @@ -583,12 +597,12 @@ def onboarding_csv_string CSV.generate(headers: headers, write_headers: true) do |csv_out| @event_registrations.each do |registration| - csv_out << onboarding_csv_row(registration, cost_required, day_count) + csv_out << onboarding_csv_row(registration, cost_required, day_count, include_ce) end end end - def onboarding_csv_row(registration, cost_required, day_count) + def onboarding_csv_row(registration, cost_required, day_count, include_ce = false) person = registration.registrant scholarship = registration.scholarships.first statuses = registration.program_statuses.map { |status| status.to_s.titleize }.join(", ") @@ -611,11 +625,13 @@ def onboarding_csv_row(registration, cost_required, day_count) row << (scholarship ? helpers.dollars_from_cents(scholarship.amount_cents) : "") row << (scholarship ? (scholarship.grant&.name.presence || "Unfunded") : "") row << onboarding_scholarship_tasks_csv(registration) - ce_hours = registration.ce_hours_requested.to_i - row << (registration.ce_credit_requested? ? "Yes" : "No") - row << (ce_hours.positive? ? ce_hours : "") - row << (registration.ce_amount_owed_cents.positive? ? helpers.dollars_from_cents(registration.ce_amount_owed_cents) : "") - row << registration.ce_license_number.to_s + if include_ce + ce_hours = registration.ce_hours_requested.to_i + row << (registration.ce_credit_requested? ? "Yes" : "No") + row << (ce_hours.positive? ? ce_hours : "") + row << (registration.ce_amount_owed_cents.positive? ? helpers.dollars_from_cents(registration.ce_amount_owed_cents) : "") + row << registration.ce_license_number.to_s + end EventRegistration::CHECKLIST_STEPS.each_key do |step| row << (registration.checklist_step_completed?(step) ? "Yes" : "No") end diff --git a/app/frontend/javascript/controllers/column_toggle_controller.js b/app/frontend/javascript/controllers/column_toggle_controller.js index 7992e4da08..0329c4ad95 100644 --- a/app/frontend/javascript/controllers/column_toggle_controller.js +++ b/app/frontend/javascript/controllers/column_toggle_controller.js @@ -1,23 +1,26 @@ import { Controller } from "@hotwired/stimulus" // Connects to data-controller="column-toggle" -// Toggles visibility of table columns marked with a data attribute. +// Each instance is a single slide switch that shows/hides the table columns +// whose data-column-toggle-col value matches this switch's `group` value, so a +// table can host several independent column toggles (e.g. "User confirmation", +// "CE status"). Columns live outside the switch's element, under a shared +// [data-column-toggle-root] ancestor. export default class extends Controller { static targets = ["toggle", "track", "knob"] + static values = { group: String } toggle() { const checked = this.toggleTarget.checked - this.element.querySelectorAll("[data-column-toggle-col]").forEach((el) => { + const root = this.element.closest("[data-column-toggle-root]") || document + + root.querySelectorAll(`[data-column-toggle-col="${this.groupValue}"]`).forEach((el) => { el.classList.toggle("hidden", !checked) }) - if (this.hasTrackTarget) { - this.trackTarget.classList.toggle("bg-gray-300", !checked) - this.trackTarget.classList.toggle("bg-blue-600", checked) - } - if (this.hasKnobTarget) { - this.knobTarget.style.transform = checked ? "translateX(16px)" : "" - } + this.trackTarget.classList.toggle("bg-gray-300", !checked) + this.trackTarget.classList.toggle("bg-blue-600", checked) + this.knobTarget.style.transform = checked ? "translateX(16px)" : "" } } diff --git a/app/frontend/stylesheets/application.tailwind.css b/app/frontend/stylesheets/application.tailwind.css index bd63158acb..a40cce757d 100644 --- a/app/frontend/stylesheets/application.tailwind.css +++ b/app/frontend/stylesheets/application.tailwind.css @@ -33,6 +33,7 @@ @source inline("{hover:,}bg-{indigo,purple,teal,violet,orange,rose,blue,sky,emerald,slate,lime,stone,amber,green,yellow,gray,cyan,fuchsia,pink}-{50,{100..900..100},950}"); @source inline("{hover:,}border-{indigo,purple,teal,violet,orange,rose,blue,sky,emerald,slate,lime,stone,amber,green,yellow,gray,cyan,fuchsia,pink}-{200,300}"); +@source inline("ring-{indigo,purple,teal,violet,orange,rose,blue,sky,emerald,slate,lime,stone,amber,green,yellow,gray,cyan,fuchsia,pink}-{200,300,400,500}"); @source inline("{hover:,}text-{indigo,purple,teal,violet,orange,rose,blue,sky,emerald,slate,lime,stone,amber,green,yellow,gray,cyan,fuchsia,pink}-{50,{100..900..100},950}"); /* Per-field form layout widths (generated from FormField::WIDTH_GRID_SPANS, which diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 25a706efda..2871b44ba8 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -12,6 +12,19 @@ def onboarding_event_row_path(event_or_id, registration_id) onboarding_event_path(event_or_id, anchor: onboarding_row_id(registration_id), highlight: registration_id) end + # Stable anchor id for a registrant's row on the Registrants roster, so back + # links (e.g. from a viewed submission) can scroll to and highlight it. + def registrant_row_id(record_or_id) + id = record_or_id.respond_to?(:id) ? record_or_id.id : record_or_id + "registrant-row-#{id}" + end + + # Path back to a specific registrant's row on the Registrants roster (scrolls + # to and highlights it). Accepts an Event or event id, and a registration id. + def registrants_event_row_path(event_or_id, registration_id) + registrants_event_path(event_or_id, anchor: registrant_row_id(registration_id), highlight: registration_id) + end + # Ordered column descriptors for the event Onboarding matrix. The array index # is the table-sort column index, so the header row and every body row iterate # this same list — keeping header buttons and cell positions aligned no matter @@ -39,10 +52,12 @@ def onboarding_columns(event) columns << { key: "scholarship_amount", label: "Scholarship amount", kind: :scholarship_amount, sortable: true, align: "center", toggle: "scholarship_amount" } columns << { key: "funder", label: "Scholarship grant", kind: :funder, sortable: true, align: "left", toggle: "funder" } columns << { key: "scholarship_tasks_completed", label: "Scholarship tasks done", kind: :scholarship_tasks, sortable: true, align: "center", toggle: "scholarship_tasks_completed" } - columns << { key: "ce_requested", label: "CE requested", kind: :ce_requested, sortable: true, align: "center", toggle: "ce_requested" } - columns << { key: "ce_hours", label: "CE hours", kind: :ce_hours, sortable: true, align: "center", toggle: "ce_hours" } - columns << { key: "ce_amount", label: "CE amount", kind: :ce_amount, sortable: true, align: "center", toggle: "ce_amount" } - columns << { key: "ce_license", label: "License #", kind: :ce_license, sortable: true, align: "center", toggle: "ce_license" } + if event.ce_eligible? + columns << { key: "ce_requested", label: "CE requested", kind: :ce_requested, sortable: true, align: "center", toggle: "ce_requested" } + columns << { key: "ce_hours", label: "CE hours", kind: :ce_hours, sortable: true, align: "center", toggle: "ce_hours" } + columns << { key: "ce_amount", label: "CE amount", kind: :ce_amount, sortable: true, align: "center", toggle: "ce_amount" } + columns << { key: "ce_license", label: "License #", kind: :ce_license, sortable: true, align: "center", toggle: "ce_license" } + end columns << { key: "fee_note", label: "Fee note", kind: :fee_note, sortable: false, align: "center", toggle: "fee_note" } columns << { key: "portal_invite", label: "Portal invite", kind: :portal_invite, sortable: true, align: "center", toggle: "portal_invite" } EventRegistration::CHECKLIST_STEPS.each do |step, label| diff --git a/app/helpers/person_helper.rb b/app/helpers/person_helper.rb index 6044bb2ef5..e2d97eee10 100644 --- a/app/helpers/person_helper.rb +++ b/app/helpers/person_helper.rb @@ -1,5 +1,12 @@ module PersonHelper - def person_profile_button(person, truncate_at: nil, subtitle: nil, display_name: nil, data: {}, inactive: false, path_params: {}, width_class: "w-full") + def person_profile_button(person, truncate_at: nil, subtitle: nil, display_name: nil, data: {}, inactive: false, path_params: {}, width_class: "w-full", compact: false) + # Compact mode shrinks the whole control (padding, avatar, type) for dense + # tables like the registrants roster where horizontal space is at a premium. + padding = compact ? "px-2 py-1" : "px-4 py-2" + avatar_size = compact ? "w-6 h-6" : "w-10 h-10" + initial_text_size = compact ? "text-xs" : "text-lg" + name_text_size = compact ? "text-xs" : "" + if inactive bg = "bg-gray-100" hover_bg = "hover:bg-gray-200" @@ -19,7 +26,7 @@ def person_profile_button(person, truncate_at: nil, subtitle: nil, display_name: data: { turbo_prefetch: false }.merge(data), title: hover_title, class: "group relative flex items-center gap-2 - #{width_class} px-4 py-2 + #{width_class} #{padding} border #{border} #{bg} #{hover_bg} rounded-lg transition-colors duration-200 font-medium shadow-sm leading-none @@ -29,11 +36,11 @@ def person_profile_button(person, truncate_at: nil, subtitle: nil, display_name: # --- Avatar --- avatar = if person.avatar.present? image_tag person.avatar.variant(:thumbnail), - class: "w-10 h-10 rounded-full object-cover border border-gray-300 shadow-sm flex-shrink-0" + class: "#{avatar_size} rounded-full object-cover border border-gray-300 shadow-sm flex-shrink-0" else content_tag(:span, person.name.to_s.first.to_s.upcase, - class: "w-10 h-10 rounded-full flex items-center justify-center - bg-sky-200 text-sky-700 font-bold text-lg + class: "#{avatar_size} rounded-full flex items-center justify-center + bg-sky-200 text-sky-700 font-bold #{initial_text_size} border border-sky-300 shadow-sm flex-shrink-0") end @@ -43,7 +50,7 @@ def person_profile_button(person, truncate_at: nil, subtitle: nil, display_name: name = content_tag( :span, display_name, - class: "font-semibold #{text} truncate" + class: "font-semibold #{name_text_size} #{text} truncate" ) subtitle_tag = if subtitle.present? diff --git a/app/models/event_registration.rb b/app/models/event_registration.rb index b776d45898..96cb0e45ee 100644 --- a/app/models/event_registration.rb +++ b/app/models/event_registration.rb @@ -147,6 +147,63 @@ class EventRegistration < ApplicationRecord else all end } + # Mirrors ReminderRecipientFilter#matches_ce_status?. The "license"/"hours" + # sub-statuses only make sense for someone who requested CE credit, so they're + # gated on it. "paid" has no CE-specific payment record yet, so it falls back + # to the registrant being paid in full. + scope :ce_status, ->(value) { + case value + when "requested" then where(ce_credit_requested: true) + when "license_not_provided" then where(ce_credit_requested: true).where(ce_license_number: [ nil, "" ]) + when "hours_not_provided" then where(ce_credit_requested: true).where("COALESCE(ce_hours_requested, 0) <= 0") + when "paid" then where(ce_credit_requested: true).merge(paid_in_full) + else all + end + } + scope :comment_status, ->(value) { + commented = Comment.where(commentable_type: "EventRegistration").select(:commentable_id) + case value + when "none" then where.not(id: commented) + when "present" then where(id: commented) + when "flagged" then where(id: Comment.where(commentable_type: "EventRegistration", flagged: true).select(:commentable_id)) + else all + end + } + # Mirrors EventRegistration#account_status (none / has_access / invited / + # no_access) as a DB filter, joining the registrant's login account. + scope :account_status, ->(value) { + # Guard every subquery against NULL person_id (system/audit users) — a NULL in + # a NOT IN list makes the whole comparison return no rows. + with_user = User.where.not(person_id: nil).select(:person_id) + has_access = User.has_access.where.not(person_id: nil).select(:person_id) + invited = User.where.not(person_id: nil).where.not(welcome_instructions_sent_at: nil).select(:person_id) + case value + when "none" then where.not(registrant_id: with_user) + when "has_access" then where(registrant_id: has_access) + when "invited" then where(registrant_id: invited).where.not(registrant_id: has_access) + when "no_access" then where(registrant_id: with_user).where.not(registrant_id: has_access).where.not(registrant_id: invited) + else all + end + } + # "linked" = at least one organization linked; "pending" = the registrant + # submitted an agency name on the event's registration form but nothing is + # linked yet (mirrors the Pending chip on the roster). Needs the event to + # resolve its registration form's agency_name field. + scope :organization_status, ->(value, event) { + linked = EventRegistrationOrganization.select(:event_registration_id) + case value + when "linked" then where(id: linked) + when "pending" + field = event.registration_form&.form_fields&.find_by(field_identifier: "agency_name") + next none unless field + submitted = FormAnswer.joins(:form_submission) + .where(form_field_id: field.id, form_submissions: { form_id: event.registration_form.id }) + .where.not(submitted_answer: [ nil, "" ]) + .select(Arel.sql("form_submissions.person_id")) + where(registrant_id: submitted).where.not(id: linked) + else all + end + } scope :keyword, ->(term) { return none if term.blank? @@ -262,6 +319,17 @@ def ce_license_provided? ce_license_number.present? end + # A short label summarizing the registrant's CE credit standing, matching the + # ce_status filter buckets. Nil when CE credit was not requested, so callers + # can render a placeholder. "Incomplete" takes precedence over "Paid" because + # a missing license/hours is the actionable state regardless of payment. + def ce_status_label + return unless ce_credit_requested? + return "Incomplete" if !ce_license_provided? || ce_hours_requested.to_i <= 0 + return "Paid" if paid_in_full? + "Requested" + end + # What the registrant owes for their requested CE hours, in cents, at the # default hourly rate. Zero when no hours were requested. def ce_amount_owed_cents diff --git a/app/services/reminder_recipient_filter.rb b/app/services/reminder_recipient_filter.rb index adcc677b8a..b0abe89ef5 100644 --- a/app/services/reminder_recipient_filter.rb +++ b/app/services/reminder_recipient_filter.rb @@ -6,18 +6,26 @@ # like scholarships, grants, organizations, comments and the registrant's user # account without extra per-filter SQL. class ReminderRecipientFilter - FILTER_KEYS = %i[ - name reg_org grantor comment email payment_status scholarship_status - account_status ce_status + # Free-text inputs matched in memory against the loaded registrations. + TEXT_KEYS = %i[ name reg_org grantor comment email ].freeze + # Dropdown filters shared with the registrants roster. They reuse the + # EventRegistration scopes (run once as a query) so both pages stay in sync — + # same param names, options, and semantics. + DROPDOWN_KEYS = %i[ + attendance_status payment_status ce_status scholarship comment_status + org_status account_status state county ].freeze + FILTER_KEYS = (TEXT_KEYS + DROPDOWN_KEYS).freeze - def initialize(event_registrations, params) + def initialize(event_registrations, params, event: nil) @event_registrations = event_registrations @params = params + @event = event || event_registrations.first&.event end def matched_ids - @event_registrations.select { |reg| matches?(reg) }.map(&:id).to_set + text_matched = @event_registrations.select { |reg| matches_text?(reg) }.map(&:id).to_set + text_matched & dropdown_matched_ids end # True when at least one filter is narrowing the list. The view uses this to @@ -29,16 +37,31 @@ def filtering? private - def matches?(reg) + def matches_text?(reg) matches_name?(reg) && matches_reg_org?(reg) && matches_grantor?(reg) && matches_comment?(reg) && - matches_email?(reg) && - matches_payment_status?(reg) && - matches_scholarship_status?(reg) && - matches_account_status?(reg) && - matches_ce_status?(reg) + matches_email?(reg) + end + + # Apply the registrants-roster scopes for whichever dropdowns are set, then + # return the matching ids. With no dropdown filter this is every registration, + # so the text-match set passes through unchanged. + def dropdown_matched_ids + return @event_registrations.map(&:id).to_set if @event.nil? + + scope = @event.event_registrations + scope = scope.attendance_status(@params[:attendance_status]) if @params[:attendance_status].present? + scope = scope.payment_status(@params[:payment_status]) if @params[:payment_status].present? + scope = scope.ce_status(@params[:ce_status]) if @params[:ce_status].present? + scope = scope.scholarship_status(@params[:scholarship]) if @params[:scholarship].present? + scope = scope.comment_status(@params[:comment_status]) if @params[:comment_status].present? + scope = scope.organization_status(@params[:org_status], @event) if @params[:org_status].present? + scope = scope.account_status(@params[:account_status]) if @params[:account_status].present? + scope = scope.registrant_state(@params[:state]) if @params[:state].present? + scope = scope.registrant_county(@params[:county]) if @params[:county].present? + scope.pluck(:id).to_set end def matches_name?(reg) @@ -83,46 +106,6 @@ def registrant_emails(reg) .compact_blank.map(&:downcase) end - def matches_payment_status?(reg) - case @params[:payment_status].presence - when "paid" then reg.paid_in_full? - when "unpaid" then reg.event.cost_cents.to_i > 0 && !reg.paid_in_full? - when "intends_to_pay" then reg.intends_to_pay? - when "paid_or_intends" then reg.payment_access_granted? - else true - end - end - - def matches_scholarship_status?(reg) - case @params[:scholarship_status].presence - when "requested" then reg.scholarship_requested? - when "allocated" then reg.scholarships.any? - when "tasks_completed" - reg.scholarships.any? && reg.scholarships.all?(&:tasks_completed?) - else true - end - end - - def matches_account_status?(reg) - status = @params[:account_status].presence - return true if status.blank? - reg.account_status == status - end - - # CE sub-statuses (missing license / missing hours) only make sense for someone - # who actually requested CE credit, so they're gated on that. "paid" has no - # CE-specific payment record yet, so it falls back to the registrant being paid - # in full. - def matches_ce_status?(reg) - case @params[:ce_status].presence - when "requested" then reg.ce_credit_requested? - when "license_not_provided" then reg.ce_credit_requested? && !reg.ce_license_provided? - when "hours_not_provided" then reg.ce_credit_requested? && reg.ce_hours_requested.to_i <= 0 - when "paid" then reg.ce_credit_requested? && reg.paid_in_full? - else true - end - end - # Text filters accept several values separated by "--" and match a registrant # when ANY of them hits (e.g. "amy--aisha" keeps both Amy and Aisha). Single # hyphens inside a value are preserved. Returns true when no term was typed. diff --git a/app/views/allocations/index.html.erb b/app/views/allocations/index.html.erb index df4fc5dcb9..a24f123c5a 100644 --- a/app/views/allocations/index.html.erb +++ b/app/views/allocations/index.html.erb @@ -11,7 +11,7 @@ Bulk payments <% end %> <% elsif @allocatable.is_a?(EventRegistration) && params[:return_to] == "registrants" %> - <%= link_to registrants_event_path(@allocatable.event), class: "text-sm text-gray-500 hover:text-gray-700" do %> + <%= link_to registrants_event_row_path(@allocatable.event, @allocatable.id), class: "text-sm text-gray-500 hover:text-gray-700" do %> Registrants <% end %> <% elsif @allocatable.is_a?(EventRegistration) && params[:return_to] == "onboarding" %> @@ -23,7 +23,7 @@ Registration <% end %> <% elsif @allocatable.respond_to?(:event) %> - <%= link_to registrants_event_path(@allocatable.event), class: "text-sm text-gray-500 hover:text-gray-700" do %> + <%= link_to registrants_event_row_path(@allocatable.event, @allocatable.id), class: "text-sm text-gray-500 hover:text-gray-700" do %> Registrants <% end %> <% else %> diff --git a/app/views/event_registrations/_form.html.erb b/app/views/event_registrations/_form.html.erb index 13a1091ad2..9cd5247c47 100644 --- a/app/views/event_registrations/_form.html.erb +++ b/app/views/event_registrations/_form.html.erb @@ -137,8 +137,8 @@ <% active_orgs = f.object.registrant.affiliations.select { |a| !a.inactive? && (a.end_date.nil? || a.end_date >= Date.current) }.map(&:organization).compact.uniq.sort_by(&:name) %> <% connected_org_ids = f.object.organizations.map(&:id) %> <% addable_orgs = active_orgs.reject { |org| connected_org_ids.include?(org.id) } %> -