From f8ce578793c50d598593330adc235bf42dc78b14 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Sat, 29 Aug 2026 22:26:47 -0700 Subject: [PATCH 01/38] Card grant improvements --- app/assets/stylesheets/application.scss | 1 + .../stylesheets/components/_card_grants.scss | 223 ++++++++++++++++++ app/views/card_grants/_actions.html.erb | 2 +- app/views/card_grants/_card_details.html.erb | 220 ++++++++--------- app/views/card_grants/_field.html.erb | 18 ++ app/views/card_grants/_grant_details.html.erb | 112 +++++---- app/views/card_grants/_header_banner.html.erb | 46 ---- app/views/card_grants/_hero.html.erb | 113 +++++++++ .../card_grants/_section_heading.html.erb | 9 + app/views/card_grants/_transactions.html.erb | 6 +- app/views/card_grants/show.html.erb | 110 +++++---- spec/controllers/card_grants_show_spec.rb | 50 ++++ 12 files changed, 633 insertions(+), 277 deletions(-) create mode 100644 app/assets/stylesheets/components/_card_grants.scss create mode 100644 app/views/card_grants/_field.html.erb delete mode 100644 app/views/card_grants/_header_banner.html.erb create mode 100644 app/views/card_grants/_hero.html.erb create mode 100644 app/views/card_grants/_section_heading.html.erb create mode 100644 spec/controllers/card_grants_show_spec.rb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 9aef2222f1..15c2898821 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -303,6 +303,7 @@ summary { @import 'components/banners'; @import 'components/buttons'; @import 'components/cards'; +@import 'components/card_grants'; @import 'components/checks'; @import 'components/comments'; @import 'components/conversation'; diff --git a/app/assets/stylesheets/components/_card_grants.scss b/app/assets/stylesheets/components/_card_grants.scss new file mode 100644 index 0000000000..cec5870ac0 --- /dev/null +++ b/app/assets/stylesheets/components/_card_grants.scss @@ -0,0 +1,223 @@ +// The card grant detail page reads top-to-bottom as a guide: what you were +// given, how to spend it, what the rules are, and where it went. + +.grant-hero { + display: grid; + grid-template-columns: minmax(0, 1fr); + padding: 0; + + @media (min-width: 56em) { + grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); + } +} + +.grant-hero__main { + padding: 1.5rem; +} + +.grant-hero__aside { + padding: 1.5rem; + border-top: 1px solid rgba(200, 200, 200, 0.3); + background-color: rgba(map-get($palette, snow), 0.75); + + @media (min-width: 56em) { + border-top: 0; + border-left: 1px solid rgba(200, 200, 200, 0.3); + } + + html[data-dark='true'] & { + border-color: rgba(255, 255, 255, 0.125); + background-color: rgba(map-get($palette, dark), 0.4); + } +} + +.grant-hero__amount { + margin: 0; + font-size: 3rem; + font-weight: 700; + line-height: 1; + letter-spacing: -0.03em; + font-variant-numeric: tabular-nums; + + @media (max-width: 32em) { + font-size: 2.25rem; + } +} + +.grant-hero__date { + margin: 0; + font-size: 1.5rem; + font-weight: 700; + line-height: 1.125; + letter-spacing: -0.02em; +} + +// A single bar: how much of the grant is still yours to spend. +.grant-progress { + height: 6px; + border-radius: 999px; + background-color: rgba(map-get($palette, smoke), 0.8); + overflow: hidden; + + html[data-dark='true'] & { + background-color: rgba(255, 255, 255, 0.1); + } +} + +.grant-progress__fill { + display: block; + height: 100%; + border-radius: inherit; + background-color: map-get($palette, success); + transition: width 0.25s ease-in-out; + + &.grant-progress__fill--muted { + background-color: map-get($palette, muted); + } +} + +// Numbered marker for each step of the page, echoing `.steps` without the list. +.grant-step { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 1.75rem; + height: 1.75rem; + border-radius: 50%; + background-color: map-get($palette, black); + color: $white; + font-size: 0.875rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + + html[data-dark='true'] & { + background-color: map-get($palette, smoke); + color: map-get($palette, black); + } +} + +.grant-section__heading { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.25rem 0.75rem; + margin: 2rem 0 0.75rem; + border-bottom: 0; + padding-bottom: 0; +} + +// Two panes divided by a hairline; the 1px grid gap is the divider. +.grant-split { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 1px; + padding: 0; + background-color: rgba(200, 200, 200, 0.3); + + html[data-dark='true'] & { + background-color: rgba(255, 255, 255, 0.125); + } + + // With nothing in the second pane, don't stretch the card across the page. + &.grant-split--single { + max-width: 24rem; + } + + @media (min-width: 56em) { + grid-template-columns: minmax(0, 21rem) minmax(0, 1fr); + + &.grant-split--single { + grid-template-columns: minmax(0, 1fr); + } + } +} + +.grant-split__pane { + padding: 1.5rem; + background-color: var(--card-bg); +} + +// Label / value / copy rows, e.g. the card & billing details. +.grant-fields { + display: flex; + flex-direction: column; +} + +.grant-field { + display: grid; + grid-template-columns: minmax(6rem, 11rem) minmax(0, 1fr) auto; + align-items: center; + gap: 0.25rem 1rem; + padding: 0.625rem 0; + border-top: 1px solid rgba(200, 200, 200, 0.3); + + &:first-child { + border-top: 0; + } + + html[data-dark='true'] & { + border-color: rgba(255, 255, 255, 0.125); + } + + @media (max-width: 32em) { + grid-template-columns: minmax(0, 1fr) auto; + + > .grant-field__label { + grid-column: 1 / -1; + } + } +} + +.grant-field__label { + display: inline-flex; + align-items: center; + gap: 4px; + color: map-get($palette, muted); +} + +.grant-field__value { + font-weight: 600; + font-variant-numeric: tabular-nums; + overflow-wrap: anywhere; +} + +.grant-copy { + height: 30px !important; + min-height: 30px !important; + min-width: 4.5rem; + padding: 0 0.75rem !important; + font-size: 0.875rem; + color: map-get($palette, info); +} + +// Hairline-separated grid of facts; the 1px gap doubles as the divider. +.grant-facts { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); + gap: 1px; + padding: 0; + background-color: rgba(200, 200, 200, 0.3); + + html[data-dark='true'] & { + background-color: rgba(255, 255, 255, 0.125); + } +} + +.grant-fact { + padding: 1.125rem 1.25rem; + background-color: var(--card-bg); +} + +.grant-fact__label { + color: map-get($palette, muted); + font-size: 0.875rem; +} + +.grant-fact__value { + margin: 0.125rem 0 0; + font-size: 1.125rem; + font-weight: 600; + line-height: 1.3; + overflow-wrap: anywhere; +} diff --git a/app/views/card_grants/_actions.html.erb b/app/views/card_grants/_actions.html.erb index d912d8005a..985e49059a 100644 --- a/app/views/card_grants/_actions.html.erb +++ b/app/views/card_grants/_actions.html.erb @@ -1,7 +1,7 @@ <%# locals: (card_grant:) %> <% if card_grant.active? %> -
+

Controls

<% if organizer_signed_in?(as: :member) %> <%= render "stripe_cards/actions/freeze", stripe_card: card_grant.stripe_card if card_grant.stripe_card.present? %> diff --git a/app/views/card_grants/_card_details.html.erb b/app/views/card_grants/_card_details.html.erb index 59e0d2a406..2f971597a5 100644 --- a/app/views/card_grants/_card_details.html.erb +++ b/app/views/card_grants/_card_details.html.erb @@ -1,147 +1,123 @@ <%# locals: (card_grant:) %> <% if (stripe_card = card_grant.stripe_card).present? %> -
-
-

How do I make purchases on this card?

-

Here's all the info you might need:

+ <% cardholder = stripe_card.cardholder %> + <% owner = stripe_card.user == current_user && stripe_card.virtual? %> + <% revealed = owner && @show_card_details %> + <%# Everyone with access sees the card itself; the numbers on it are for the + grantee (and auditors looking into a report). %> + <% show_fields = card_grant.user == current_user || current_user&.auditor? %> + <% default_address = stripe_card.user == current_user && cardholder.default_billing_address? %> -
-
- Status - - <%= status_badge stripe_card.status_badge_type %> - <%= stripe_card.status_text.capitalize %> - -
+
"> +
+
"> + <%= render "stripe_cards/stripe_card", stripe_card:, headless: true %> +
- <% if stripe_card.frozen? && stripe_card.last_frozen_by && stripe_card.last_frozen_by != User.system_user %> -
- Frozen by - <%= user_mention stripe_card.last_frozen_by %> -
+ <% if owner %> + <%= link_to card_grant_path(card_grant, show_details: (!@show_card_details).to_s), + class: "btn w-full mt-4", + data: { turbo: true } do %> + <%= inline_icon @show_card_details ? "view-fill" : "view" %> + <%= @show_card_details ? "Hide card details" : "Show card details" %> <% end %> + <% end %> -
- Card number -
- <% if stripe_card.user == current_user && stripe_card.virtual? %> - <% if @show_card_details %> - <%= copy_to_clipboard(stripe_card.full_card_number, class: "w-fit", tooltip_direction: "e") do %> - <%= stripe_card.formatted_card_number %> - <% end %> - <% else %> - <%= stripe_card.hidden_card_number_with_last_four %> - <% end %> - <%= link_to card_grant_path(card_grant, show_details: (!@show_card_details).to_s), data: { turbo: true } do %> - <%= inline_icon @show_card_details ? "view-fill" : "view", class: "align-middle" %> - <% end %> - <% else %> - <%= stripe_card.hidden_card_number_with_last_four %> -
- <%= inline_icon "private", size: 32, class: "muted" %> -
- <% end %> -
-
+ <% if stripe_card.frozen? && stripe_card.last_frozen_by && stripe_card.last_frozen_by != User.system_user %> +

+ Frozen by <%= user_mention stripe_card.last_frozen_by %> +

+ <% end %> +
-
- - Expiration date - - <%= inline_icon "important", size: 18, style: "position: unset;" %> - - - <%= copy_to_clipboard(render_short_exp_date(stripe_card), class: "w-fit", tooltip_direction: "e") { render_exp_date stripe_card } %> -
+ <% details_pane = capture do %> +

Card details

+
+ <%= render "card_grants/field", + label: "Card number", + monospace: true, + value: revealed ? stripe_card.formatted_card_number : stripe_card.hidden_card_number_with_last_four, + copy: revealed ? stripe_card.full_card_number : nil, + hint: owner ? nil : "Hidden to protect security", + hint_icon: owner ? "info" : "private", + hint_class: "muted" %> -
- CVC -
- <% if stripe_card.user == current_user && stripe_card.virtual? %> - <% if @show_card_details %> - <%= copy_to_clipboard(stripe_card.cvc, class: "w-fit", tooltip_direction: "e") do %> - <%= stripe_card.cvc %> - <% end %> - <% else %> - <%= stripe_card.hidden_cvc %> - <% end %> - <% else %> - <%= stripe_card.hidden_cvc %> -
- <%= inline_icon "private", size: 32, class: "muted" %> -
- <% end %> -
-
+ <%= render "card_grants/field", + label: "Expiry", + monospace: true, + value: render_exp_date(stripe_card), + copy: render_short_exp_date(stripe_card), + hint: "This is what you enter when asked for expiration date at checkout. It's different from your grant's spend by date.", + hint_icon: "important", + hint_class: "text-yellow" %> + + <%= render "card_grants/field", + label: "CVC", + monospace: true, + value: revealed ? stripe_card.cvc : stripe_card.hidden_cvc, + copy: revealed ? stripe_card.cvc : nil %> <% if stripe_card.cash_withdrawal_enabled? %> -
- PIN -
- •••• - <%= inline_icon @show_card_details ? "view-fill" : "view", class: "align-middle", "data-action": "click->pin#view" %> -
+
+ PIN + •••• +
<% end %> +
-
- - Address - <% if stripe_card.cardholder.stripe_billing_address_line2.presence %> line 1<% end %> - <% if stripe_card.user == current_user && stripe_card.cardholder.default_billing_address? %> - <%= link_to settings_address_path, class: "info tooltipped tooltipped--e", "aria-label": "You're using HCB's default billing address, not your home address. You can change it in your user settings if you'd like.", style: "display: flex;", data: { turbo: false } do %> - <%= inline_icon "info", size: 18, style: "position: unset;" %> - <% end %> - <% end %> - - <%= copy_to_clipboard(stripe_card.cardholder.stripe_billing_address_line1, class: "w-fit", tooltip_direction: "e") %> -
+

+ Billing address + Use this, not your home address. +

+
+ <%= render "card_grants/field", + label: cardholder.stripe_billing_address_line2.presence ? "Address line 1" : "Address", + value: cardholder.stripe_billing_address_line1, + copy: cardholder.stripe_billing_address_line1, + hint: default_address ? "You're using HCB's default billing address, not your home address. You can change it in your user settings if you'd like." : nil %> - <% if stripe_card.cardholder.stripe_billing_address_line2.presence %> -
- Address line 2 - <%= copy_to_clipboard(stripe_card.cardholder.stripe_billing_address_line2, class: "w-fit", tooltip_direction: "e") %> -
+ <% if cardholder.stripe_billing_address_line2.presence %> + <%= render "card_grants/field", + label: "Address line 2", + value: cardholder.stripe_billing_address_line2, + copy: cardholder.stripe_billing_address_line2 %> <% end %> -
- City - <%= copy_to_clipboard(stripe_card.cardholder.stripe_billing_address_city, class: "w-fit", tooltip_direction: "e") %> -
+ <%= render "card_grants/field", + label: "City", + value: cardholder.stripe_billing_address_city, + copy: cardholder.stripe_billing_address_city %> -
- State - <%= copy_to_clipboard(stripe_card.cardholder.stripe_billing_address_state, class: "w-fit", tooltip_direction: "e") %> -
+ <%= render "card_grants/field", + label: "State", + value: cardholder.stripe_billing_address_state, + copy: cardholder.stripe_billing_address_state %> -
- ZIP/Postal code - <%= copy_to_clipboard(stripe_card.stripe_cardholder.address_postal_code, class: "w-fit", tooltip_direction: "e") %> -
+ <%= render "card_grants/field", + label: "ZIP/Postal code", + value: stripe_card.stripe_cardholder.address_postal_code, + copy: stripe_card.stripe_cardholder.address_postal_code %> <% if stripe_card.user.phone_number %> -
- - Phone number - - <%= inline_icon "info", size: 18, style: "position: unset;" %> - - - <%= copy_to_clipboard(stripe_card.user.pretty_phone_number, class: "w-fit", tooltip_direction: "e") %> -
+ <%= render "card_grants/field", + label: "Phone number", + value: stripe_card.user.pretty_phone_number, + copy: stripe_card.user.pretty_phone_number, + hint: "This is your phone number — use this if asked during checkout." %> <% end %> +
+ <% end %> -
- Type - <%= stripe_card.virtual? ? "Virtual" : "Physical" %> -
- -
- Network - Visa -
-
-
+ <% if card_grant.user == current_user %> +
<%= details_pane %>
+ <% elsif current_user&.auditor? %> + <% admin_tool("grant-split__pane") do %><%= details_pane %><% end %> + <% end %>
<% end %> diff --git a/app/views/card_grants/_field.html.erb b/app/views/card_grants/_field.html.erb new file mode 100644 index 0000000000..1c7e06b392 --- /dev/null +++ b/app/views/card_grants/_field.html.erb @@ -0,0 +1,18 @@ +<%# locals: (label:, value:, copy: nil, hint: nil, hint_icon: "info", hint_class: "info", monospace: false, mask: true) %> + +
"> + + <%= label %> + <% if hint.present? %> + + <%= inline_icon hint_icon, size: 18, style: "position: unset;" %> + + <% end %> + + "><%= value %> + <% if copy.present? %> + <%= copy_to_clipboard(copy, tooltip_direction: "w", class: "btn btn-small grant-copy", "data-clipboard-confirm-text-value": "Copied!") { "Copy" } %> + <% else %> + + <% end %> +
diff --git a/app/views/card_grants/_grant_details.html.erb b/app/views/card_grants/_grant_details.html.erb index 1ccbead6e8..b26e0b5fc4 100644 --- a/app/views/card_grants/_grant_details.html.erb +++ b/app/views/card_grants/_grant_details.html.erb @@ -1,67 +1,63 @@ <%# locals: (card_grant:) %> -<% if (stripe_card = card_grant.stripe_card).present? %> -
-
-

- This is information about your grant: -
- - If there are any issues with your grant amount, spend by, or allowed merchants, - please reach out to the <%= card_grant.event.name %> team. HCB staff are unable - to provide assistance with these requests. - +

+
+

Merchants

+

+ <% if card_grant.allowed_merchants.any? %> + <%= card_grant.allowed_merchant_names.to_sentence %> + <% else %> + Anywhere + <% end %> +

+ <% if card_grant.allowed_merchants.any? %> +

+ IDs: <%= card_grant.allowed_merchants.join(", ") %>

+ <% end %> +
-
-

- - Spend by - - <%= inline_icon "important", size: 18, style: "position: unset;" %> - - - <%= format_date card_grant.expiration_at %> -

+
+

Categories

+

+ <%= card_grant.allowed_category_names.to_sentence.presence || "Anything" %> +

+
- <% if card_grant.one_time_use %> -

- - One time use - - <%= inline_icon "info", size: 18, style: "position: unset;" %> - - - <%= card_grant.one_time_use ? "Yes" : "No" %> -

- <% end %> +
+

Deadline

+

<%= format_date card_grant.expiration_at %>

+

Unspent funds return to <%= card_grant.event.name %>.

+
-

- Allowed merchants - <% if card_grant.allowed_merchants.any? %> - <%= card_grant.allowed_merchant_names.join(", ") %>
- (IDs: <%= card_grant.allowed_merchants.join(", ") %>)
- <% else %> - All merchants allowed - <% end %> -

+ <% if card_grant.one_time_use %> +
+

One time use

+

Yes

+

The card freezes itself after your first purchase.

+
+ <% end %> -

- Allowed categories - <% if (allowed_categories = card_grant.allowed_category_names.join(", ").presence) %> - <%= allowed_categories %> - <% else %> - All categories allowed - <% end %> -

+ <% if card_grant.keyword_lock.present? %> +
+

Merchant keyword lock

+

<%= card_grant.keyword_lock %>

+

Merchant names must contain this.

+
+ <% end %> - <% if card_grant.keyword_lock.present? %> -

- Merchant keyword lock - <%= card_grant.keyword_lock %> -

- <% end %> -
+ <% if card_grant.purpose.present? %> +
+

Purpose

+

<%= card_grant.purpose %>

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

+ + If there are any issues with the grant amount, spend by date, or allowed + merchants, please reach out to the <%= card_grant.event.name %> team. HCB + staff are unable to help with these requests. + +

diff --git a/app/views/card_grants/_header_banner.html.erb b/app/views/card_grants/_header_banner.html.erb deleted file mode 100644 index 0f6aa37d73..0000000000 --- a/app/views/card_grants/_header_banner.html.erb +++ /dev/null @@ -1,46 +0,0 @@ -<%# locals: (card_grant:, footer: nil) %> - -
"> -
- <% if organizer_signed_in?(as: :member) %> - <%= render "card_grants/actions/edit", card_grant: card_grant %> - <% end %> -
-
-
-
-

- Grant to   <%= user_mention(card_grant.user, class: "mr-2") %> for <%= render_money card_grant.amount %> -

-

- <%= status_badge card_grant.status_badge_type %> - <%= card_grant.state_text %> - <% if card_grant.purpose.present? %> - • <%= card_grant.purpose %> - <% end %> -

-
- -
-

- Sent by - <%= user_mention card_grant.sent_by %> -

-

- Funded by - <%= link_to card_grant.event.name, card_grant.event %> -

-

- Sent - - <%= time_ago_in_words card_grant.created_at %> ago - -

-
-
- <% if defined?(footer) && footer.present? %> -
- <%= footer %> -
- <% end %> -
diff --git a/app/views/card_grants/_hero.html.erb b/app/views/card_grants/_hero.html.erb new file mode 100644 index 0000000000..44b0ad15b0 --- /dev/null +++ b/app/views/card_grants/_hero.html.erb @@ -0,0 +1,113 @@ +<%# locals: (card_grant:, footer: nil) %> + +<% own = card_grant.user == current_user %> +<% total = card_grant.amount %> +<% remaining = card_grant.balance %> +<% remaining_fraction = total.positive? ? [[remaining.cents.to_f / total.cents, 0].max, 1].min : 0 %> +<% days_left = (card_grant.expiration_at.to_date - Date.current).to_i %> +<% spendable = card_grant.active? && !card_grant.pending_invite? %> + +
+
+
+

<%= own ? "Your grant" : "Card grant" %>

+ + <%= status_badge card_grant.status_badge_type %><%= card_grant.state_text %> + + <% if card_grant.purpose.present? %> + for <%= card_grant.purpose %> + <% end %> +
+ +
+

<%= render_money remaining %>

+

+ <% if remaining == total %> + to spend + <% else %> + left to spend of <%= render_money total %> + <% end %> +

+
+ +
+ " + style="width: <%= number_to_percentage(remaining_fraction * 100, precision: 2) %>"> +
+ +

+ <% if card_grant.pending_invite? %> + Activate the card below and you can start spending straight away. + <% elsif own %> + Spend it with the card below. You don't need to ask anyone first, and you + don't need to pay anything back. + <% else %> + <%= card_grant.user.first_name %> can spend this with the card issued to + them — no reimbursement paperwork on either side. + <% end %> +

+ + <% if card_grant.stripe_card&.canceled? %> +

+ Refunds received after cancelation will be sent to <%= card_grant.event.name %>. + <% if card_grant.reimbursement_report.present? %> + The funds will not automatically be contributed towards the reimbursement report cap. + <% end %> +

+ <% elsif card_grant.stripe_card&.frozen? %> +

+ Refunds received while frozen will still process and be returned to the card. +

+ <% end %> +
+ + + + <% if footer.present? %> +
+ <%= footer %> +
+ <% end %> +
diff --git a/app/views/card_grants/_section_heading.html.erb b/app/views/card_grants/_section_heading.html.erb new file mode 100644 index 0000000000..1817cec5c3 --- /dev/null +++ b/app/views/card_grants/_section_heading.html.erb @@ -0,0 +1,9 @@ +<%# locals: (number:, title:, subtitle: nil) %> + +

+ + <%= title %> + <% if subtitle.present? %> + <%= subtitle %> + <% end %> +

diff --git a/app/views/card_grants/_transactions.html.erb b/app/views/card_grants/_transactions.html.erb index 8d83a75221..414fe44899 100644 --- a/app/views/card_grants/_transactions.html.erb +++ b/app/views/card_grants/_transactions.html.erb @@ -1,6 +1,8 @@ -<%# locals: (hcb_codes: nil, is_public: false, ledger:, items:) %> +<%# locals: (hcb_codes: nil, is_public: false, ledger:, items:, heading: true) %> -

Transactions on this card

+<% if heading %> +

Transactions on this card

+<% end %> <% if Flipper.enabled?(:new_ledger_everywhere_2026_07_13, current_user) %> <%= render "ledgers/ledger", ledger: @ledger, items: @items, show_author: false %> <% else %> diff --git a/app/views/card_grants/show.html.erb b/app/views/card_grants/show.html.erb index 495125d216..f57c266705 100644 --- a/app/views/card_grants/show.html.erb +++ b/app/views/card_grants/show.html.erb @@ -5,8 +5,12 @@ <%= render "users/nav", selected: :cards %> <% end %> +<% own = @card_grant.user == current_user %> +<% awaiting_activation = own && @card_grant.pending_invite? %> +<% stripe_card = @card_grant.stripe_card %> + <% footer = nil %> -<% if @card_grant.user == current_user && @card_grant.pending_invite? %> +<% if awaiting_activation %> <% footer = capture do %>
@@ -20,7 +24,22 @@ <% end %> <% end %> -<%= render partial: "card_grants/header_banner", locals: { card_grant: @card_grant, footer: } %> +
+ <% if organizer_signed_in? %> + <%= link_to "Grants", event_card_grant_overview_path(@card_grant.event), class: "breadcrumb" %> + <% else %> + <%= link_to "Grants", my_cards_path, class: "breadcrumb" %> + <% end %> + <%= @card_grant.user.initial_name %> + + <% if organizer_signed_in?(as: :member) %> + <%= render "card_grants/actions/edit", card_grant: @card_grant %> + <% end %> +
+ +<%= render partial: "card_grants/hero", locals: { card_grant: @card_grant, footer: } %> + +<%= render partial: "card_grants/canceled_warning", locals: { card_grant: @card_grant } %> <% if @card_grant.instructions.present? %> <%= render "application/callout", title: "Instructions from #{@card_grant.event.name}", type: "info", class: "mb-4" do %> @@ -30,56 +49,51 @@ <% end %> <% end %> -
mb-5" style="gap: 3rem"> - <%# Grantee has pending invite %> - <% if @card_grant.user == current_user && @card_grant.pending_invite? %> - <% if @card_grant.converted_to_reimbursement_report? %> -
- <%= blankslate "This grant was converted into a reimbursement report." %> - <% if policy(@card_grant.reimbursement_report).show? %> -

- <%= link_to "View reimbursement report", @card_grant.reimbursement_report %> -

- <% end %> -
- <% elsif @card_grant.canceled? %> - <%= blankslate "Sorry, this grant was canceled!", class: "flex-grow" %> - <% else %> - <%= render partial: "card_grants/invitation", locals: { card_grant: @card_grant } %> +<% if awaiting_activation %> + <%# The grantee hasn't accepted the invite, so there's nothing to spend yet. %> + <% if @card_grant.converted_to_reimbursement_report? %> + <%= blankslate "This grant was converted into a reimbursement report." %> + <% if policy(@card_grant.reimbursement_report).show? %> +

+ <%= link_to "View reimbursement report", @card_grant.reimbursement_report %> +

<% end %> - <%# Grantee has accepted invite %> - <% elsif @card_grant.user.admin? || organizer_signed_in? || @card_grant.user == current_user %> -
- <% if @card_grant.stripe_card %> -
-
"> - <%= render "stripe_cards/stripe_card", stripe_card: @card_grant.stripe_card, headless: true %> -
- <%= render partial: "card_grants/balance", locals: { card_grant: @card_grant } %> - <%= render partial: "card_grants/canceled_warning", locals: { card_grant: @card_grant } %> - <%= render partial: "card_grants/actions", locals: { card_grant: @card_grant } %> -
- <% end %> -
- <% if @card_grant.stripe_card %> - <% admin_tool_if @card_grant.user != current_user do %> - <%= render partial: "card_grants/card_details", locals: { card_grant: @card_grant } %> - <% end %> - <% end %> - <%= render partial: "card_grants/grant_details", locals: { card_grant: @card_grant } %> - - <%= render partial: "card_grant/pre_authorizations/organizer_info", locals: { pre_authorization: @card_grant.pre_authorization, link_to_pre_authorization: true } if @card_grant.pre_authorization.present? && organizer_signed_in? %> + <% elsif @card_grant.canceled? %> + <%= blankslate "Sorry, this grant was canceled!" %> + <% else %> + <%= render partial: "card_grants/invitation", locals: { card_grant: @card_grant } %> + <% end %> +<% elsif @card_grant.user.admin? || organizer_signed_in? || own %> + <% if stripe_card.present? %> + <% card_type = stripe_card.virtual? ? "Virtual" : "Physical" %> + <%= render "card_grants/section_heading", + number: 1, + title: own ? "Pay with your card" : "The card", + subtitle: if !own + "#{card_type} Visa issued to #{@card_grant.user.first_name}." + elsif stripe_card.virtual? + "Virtual Visa — type these into any checkout page." + else + "Physical Visa — tap, swipe, or type these in at checkout." + end %> -
-
- <%= render partial: "card_grants/canceled_warning", locals: { card_grant: @card_grant } unless @card_grant.stripe_card %> -
-
+ <%= render partial: "card_grants/card_details", locals: { card_grant: @card_grant } %> + <%= render partial: "card_grants/actions", locals: { card_grant: @card_grant } %> <% end %> -
-<% unless @card_grant.user == current_user && @card_grant.pending_invite? %> - <%= render partial: "card_grants/transactions", locals: { hcb_codes: @hcb_codes, ledger: @ledger, items: @items } %> + <%= render "card_grants/section_heading", + number: stripe_card.present? ? 2 : 1, + title: "What you can spend it on" %> + + <%= render partial: "card_grants/grant_details", locals: { card_grant: @card_grant } %> + + <%= render partial: "card_grant/pre_authorizations/organizer_info", locals: { pre_authorization: @card_grant.pre_authorization, link_to_pre_authorization: true } if @card_grant.pre_authorization.present? && organizer_signed_in? %> + + <%= render "card_grants/section_heading", + number: stripe_card.present? ? 3 : 2, + title: "Where it's gone" %> + + <%= render partial: "card_grants/transactions", locals: { hcb_codes: @hcb_codes, ledger: @ledger, items: @items, heading: false } %> <% end %> <% if policy(@card_grant.comments.build).create? %> diff --git a/spec/controllers/card_grants_show_spec.rb b/spec/controllers/card_grants_show_spec.rb new file mode 100644 index 0000000000..99e81a3679 --- /dev/null +++ b/spec/controllers/card_grants_show_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe CardGrantsController do + include SessionSupport + render_views + + describe "#show" do + let(:event) { create(:event, :with_positive_balance, plan_type: Event::Plan::HackClubAffiliate) } + + it "renders the spending guide for the grantee" do + card_grant = create(:card_grant, event:) + create_session(card_grant.user, verified: true) + + get(:show, params: { id: card_grant.hashid }) + + expect(response).to have_http_status(:ok) + expect(response.body).to include("Pay with your card") + expect(response.body).to include("What you can spend it on") + expect(response.body).to include("Where it's gone") + end + + it "renders the activation prompt before the card exists" do + card_grant = create(:card_grant, event:) + card_grant.update!(stripe_card: nil) + create_session(card_grant.user, verified: true) + + get(:show, params: { id: card_grant.hashid }) + + expect(response).to have_http_status(:ok) + expect(response.body).to include("Activate your grant card") + expect(response.body).not_to include("Pay with your card") + end + + it "renders for an organizer without exposing the card numbers" do + organizer = create(:user) + card_grant = create(:card_grant, event:) + create(:organizer_position, user: organizer, event:) + create_session(organizer, verified: true) + + get(:show, params: { id: card_grant.hashid }) + + expect(response).to have_http_status(:ok) + expect(response.body).to include("The card") + expect(response.body).not_to include("Show card details") + expect(response.body).not_to include("Billing address") + end + end +end From e47675f5ed804b8876c8455d61383c4d41be02c9 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Sat, 29 Aug 2026 22:27:57 -0700 Subject: [PATCH 02/38] Update _hero.html.erb --- app/views/card_grants/_hero.html.erb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/views/card_grants/_hero.html.erb b/app/views/card_grants/_hero.html.erb index 44b0ad15b0..fa10406f3b 100644 --- a/app/views/card_grants/_hero.html.erb +++ b/app/views/card_grants/_hero.html.erb @@ -36,14 +36,11 @@

- <% if card_grant.pending_invite? %> + <% if card_grant.pending_invite? && !own %> Activate the card below and you can start spending straight away. - <% elsif own %> - Spend it with the card below. You don't need to ask anyone first, and you - don't need to pay anything back. <% else %> <%= card_grant.user.first_name %> can spend this with the card issued to - them — no reimbursement paperwork on either side. + them <% end %>

From f9906972340f5eb4e9a268c2836dfbcb77b6145e Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Sun, 30 Aug 2026 15:47:34 -0700 Subject: [PATCH 03/38] Improve layout --- .../stylesheets/components/_card_grants.scss | 251 +++++++++--------- app/views/card_grants/_actions.html.erb | 32 +-- app/views/card_grants/_card_details.html.erb | 174 ++++++------ app/views/card_grants/_field.html.erb | 18 -- app/views/card_grants/_grant_details.html.erb | 87 +++--- app/views/card_grants/_hero.html.erb | 140 ++++------ .../card_grants/_section_heading.html.erb | 11 +- .../_convert_to_reimbursement_report.html.erb | 16 +- .../card_grants/actions/_return.html.erb | 18 +- app/views/card_grants/show.html.erb | 55 +--- .../stripe_cards/actions/_freeze.html.erb | 51 ++-- spec/controllers/card_grants_show_spec.rb | 19 +- 12 files changed, 391 insertions(+), 481 deletions(-) delete mode 100644 app/views/card_grants/_field.html.erb diff --git a/app/assets/stylesheets/components/_card_grants.scss b/app/assets/stylesheets/components/_card_grants.scss index cec5870ac0..6f2fecfd2c 100644 --- a/app/assets/stylesheets/components/_card_grants.scss +++ b/app/assets/stylesheets/components/_card_grants.scss @@ -1,223 +1,234 @@ -// The card grant detail page reads top-to-bottom as a guide: what you were -// given, how to spend it, what the rules are, and where it went. +// Card grant page. Built on `.card`, the HCB palette and `card--background-image` +// so it sits inside HCB, but with more air and bigger numbers than the dense +// `.details` lists — this page is read by grantees, not accountants. + +$grant-hairline: rgba(200, 200, 200, 0.35); + +@mixin grant-hairline($side) { + border-#{$side}: 1px solid $grant-hairline; + + html[data-dark='true'] & { + border-#{$side}-color: rgba(255, 255, 255, 0.125); + } +} .grant-hero { + padding: 1.75rem; + + @media (max-width: 40em) { + padding: 1.25rem; + } +} + +.grant-hero__grid { display: grid; + gap: 1.5rem; grid-template-columns: minmax(0, 1fr); - padding: 0; @media (min-width: 56em) { - grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); + grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); + gap: 2.5rem; } } -.grant-hero__main { - padding: 1.5rem; -} - .grant-hero__aside { - padding: 1.5rem; - border-top: 1px solid rgba(200, 200, 200, 0.3); - background-color: rgba(map-get($palette, snow), 0.75); + @include grant-hairline(top); + padding-top: 1.25rem; @media (min-width: 56em) { border-top: 0; - border-left: 1px solid rgba(200, 200, 200, 0.3); - } - - html[data-dark='true'] & { - border-color: rgba(255, 255, 255, 0.125); - background-color: rgba(map-get($palette, dark), 0.4); + padding-top: 0; + padding-left: 2.5rem; + @include grant-hairline(left); } } .grant-hero__amount { + display: block; margin: 0; - font-size: 3rem; + font-size: 3.25rem; font-weight: 700; line-height: 1; - letter-spacing: -0.03em; + letter-spacing: -0.035em; font-variant-numeric: tabular-nums; - @media (max-width: 32em) { - font-size: 2.25rem; + @media (max-width: 40em) { + font-size: 2.5rem; } } .grant-hero__date { margin: 0; - font-size: 1.5rem; + font-size: 1.625rem; font-weight: 700; - line-height: 1.125; + line-height: 1.1; letter-spacing: -0.02em; } -// A single bar: how much of the grant is still yours to spend. -.grant-progress { - height: 6px; +// How much of the grant is still yours to spend. +.grant-meter { + height: 8px; border-radius: 999px; - background-color: rgba(map-get($palette, smoke), 0.8); + background-color: rgba(map-get($palette, smoke), 0.9); overflow: hidden; html[data-dark='true'] & { - background-color: rgba(255, 255, 255, 0.1); + background-color: rgba(255, 255, 255, 0.15); } } -.grant-progress__fill { +.grant-meter__fill { display: block; height: 100%; border-radius: inherit; background-color: map-get($palette, success); transition: width 0.25s ease-in-out; - &.grant-progress__fill--muted { + &.grant-meter__fill--muted { background-color: map-get($palette, muted); } } -// Numbered marker for each step of the page, echoing `.steps` without the list. -.grant-step { - display: inline-flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - width: 1.75rem; - height: 1.75rem; - border-radius: 50%; - background-color: map-get($palette, black); - color: $white; - font-size: 0.875rem; - font-weight: 700; - font-variant-numeric: tabular-nums; +// Compact label / value pairs stacked in the hero aside. +.grant-meta > div { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + padding: 0.375rem 0; - html[data-dark='true'] & { - background-color: map-get($palette, smoke); - color: map-get($palette, black); + + div { + @include grant-hairline(top); } } -.grant-section__heading { +.grant-section { display: flex; align-items: center; flex-wrap: wrap; - gap: 0.25rem 0.75rem; - margin: 2rem 0 0.75rem; - border-bottom: 0; - padding-bottom: 0; + gap: 0.625rem; + margin: 2.5rem 0 0.875rem; + font-size: 1.375rem; } -// Two panes divided by a hairline; the 1px grid gap is the divider. -.grant-split { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 1px; - padding: 0; - background-color: rgba(200, 200, 200, 0.3); +.grant-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 2rem; + height: 2rem; + border-radius: var(--radius-xl); + background: rgba(map-get($palette, muted), 0.15); + color: map-get($palette, slate); html[data-dark='true'] & { - background-color: rgba(255, 255, 255, 0.125); + color: map-get($palette, smoke); } - // With nothing in the second pane, don't stretch the card across the page. - &.grant-split--single { - max-width: 24rem; + svg { + width: 20px; + height: 20px; } +} + +// Card artwork beside its numbers. +.grant-panel { + display: grid; + grid-template-columns: minmax(0, 1fr); + padding: 0; @media (min-width: 56em) { - grid-template-columns: minmax(0, 21rem) minmax(0, 1fr); + grid-template-columns: 23rem minmax(0, 1fr); - &.grant-split--single { + &.grant-panel--art-only { grid-template-columns: minmax(0, 1fr); + max-width: 23rem; } } } -.grant-split__pane { +.grant-panel__art { padding: 1.5rem; - background-color: var(--card-bg); } -// Label / value / copy rows, e.g. the card & billing details. -.grant-fields { - display: flex; - flex-direction: column; -} - -.grant-field { - display: grid; - grid-template-columns: minmax(6rem, 11rem) minmax(0, 1fr) auto; - align-items: center; - gap: 0.25rem 1rem; - padding: 0.625rem 0; - border-top: 1px solid rgba(200, 200, 200, 0.3); +.grant-panel__fields { + padding: 1.5rem; + @include grant-hairline(top); - &:first-child { + @media (min-width: 56em) { border-top: 0; + @include grant-hairline(left); } +} - html[data-dark='true'] & { - border-color: rgba(255, 255, 255, 0.125); - } - - @media (max-width: 32em) { - grid-template-columns: minmax(0, 1fr) auto; +.grant-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + padding: 0.2rem 0; - > .grant-field__label { - grid-column: 1 / -1; - } + + .grant-row { + @include grant-hairline(top); } } -.grant-field__label { +.grant-row__label { display: inline-flex; align-items: center; gap: 4px; + flex: 0 0 auto; color: map-get($palette, muted); } -.grant-field__value { +.grant-row__value { font-weight: 600; + text-align: right; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; } -.grant-copy { - height: 30px !important; - min-height: 30px !important; - min-width: 4.5rem; - padding: 0 0.75rem !important; - font-size: 0.875rem; - color: map-get($palette, info); -} +// Horizontal strip of facts, hairline-separated. +.grant-strip { + display: flex; + flex-wrap: wrap; -// Hairline-separated grid of facts; the 1px gap doubles as the divider. -.grant-facts { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); - gap: 1px; - padding: 0; - background-color: rgba(200, 200, 200, 0.3); + > div { + flex: 1 1 9rem; + padding: 0.25rem 1.25rem; + @include grant-hairline(left); - html[data-dark='true'] & { - background-color: rgba(255, 255, 255, 0.125); - } -} + &:first-child { + padding-left: 0; + border-left: 0; + } -.grant-fact { - padding: 1.125rem 1.25rem; - background-color: var(--card-bg); + @media (max-width: 40em) { + flex-basis: 45%; + padding: 0.5rem 0.75rem; + + &:nth-child(odd) { + padding-left: 0; + border-left: 0; + } + } + } } -.grant-fact__label { +.grant-strip__label { + display: block; + margin: 0 0 0.125rem; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; color: map-get($palette, muted); - font-size: 0.875rem; } -.grant-fact__value { - margin: 0.125rem 0 0; - font-size: 1.125rem; +.grant-strip__value { + margin: 0; font-weight: 600; - line-height: 1.3; + line-height: 1.35; overflow-wrap: anywhere; } diff --git a/app/views/card_grants/_actions.html.erb b/app/views/card_grants/_actions.html.erb index 985e49059a..b96da3ad91 100644 --- a/app/views/card_grants/_actions.html.erb +++ b/app/views/card_grants/_actions.html.erb @@ -1,22 +1,24 @@ <%# locals: (card_grant:) %> <% if card_grant.active? %> -
+

Controls

- <% if organizer_signed_in?(as: :member) %> - <%= render "stripe_cards/actions/freeze", stripe_card: card_grant.stripe_card if card_grant.stripe_card.present? %> - <%= render "card_grants/actions/convert_to_reimbursement_report", card_grant:, label: card_grant.user == current_user ? "Get reimbursed" : "Create reimbursement report" %> - <% if card_grant.user == current_user %> - <%= render "card_grants/actions/return", card_grant: %> - <% else %> - <%= render "card_grants/actions/cancel", card_grant: %> +
+ <% if organizer_signed_in?(as: :member) %> + <%= render "stripe_cards/actions/freeze", stripe_card: card_grant.stripe_card, label: "Pause spending instantly", subtitle: "You can unfreeze it yourself at any time" if card_grant.stripe_card.present? %> + <%= render "card_grants/actions/convert_to_reimbursement_report", card_grant:, label: card_grant.user == current_user ? "I already paid out of pocket" : "Create reimbursement report", subtitle: "Swap the balance for a reimbursement" %> + <% if card_grant.user == current_user %> + <%= render "card_grants/actions/return", card_grant:, label: "I don’t need this money", subtitle: "Send the remaining balance back to #{card_grant.event.name}" %> + <% else %> + <%= render "card_grants/actions/cancel", card_grant:, label: "Return the balance to #{card_grant.event.name}" %> + <% end %> + <%= render "card_grants/actions/support", card_grant: %> + <% elsif card_grant.user == current_user %> + <%= render "stripe_cards/actions/freeze", stripe_card: card_grant.stripe_card, label: "Pause spending instantly", subtitle: "You can unfreeze it yourself at any time" if card_grant.stripe_card.present? %> + <%= render "card_grants/actions/convert_to_reimbursement_report", card_grant:, label: "Get reimbursed", subtitle: "Swap the balance for a reimbursement" %> + <%= render "card_grants/actions/return", card_grant:, label: "I don’t need this money", subtitle: "Send the remaining balance back to #{card_grant.event.name}" %> + <%= render "card_grants/actions/support", card_grant: %> <% end %> - <%= render "card_grants/actions/support", card_grant: %> - <% elsif card_grant.user == current_user %> - <%= render "stripe_cards/actions/freeze", stripe_card: card_grant.stripe_card if card_grant.stripe_card.present? %> - <%= render "card_grants/actions/convert_to_reimbursement_report", card_grant:, label: "Get reimbursed" %> - <%= render "card_grants/actions/return", card_grant: %> - <%= render "card_grants/actions/support", card_grant: %> - <% end %> +
<% end %> diff --git a/app/views/card_grants/_card_details.html.erb b/app/views/card_grants/_card_details.html.erb index 2f971597a5..3605ff3354 100644 --- a/app/views/card_grants/_card_details.html.erb +++ b/app/views/card_grants/_card_details.html.erb @@ -4,120 +4,120 @@ <% cardholder = stripe_card.cardholder %> <% owner = stripe_card.user == current_user && stripe_card.virtual? %> <% revealed = owner && @show_card_details %> - <%# Everyone with access sees the card itself; the numbers on it are for the - grantee (and auditors looking into a report). %> <% show_fields = card_grant.user == current_user || current_user&.auditor? %> - <% default_address = stripe_card.user == current_user && cardholder.default_billing_address? %> -
"> -
+
"> +
"> <%= render "stripe_cards/stripe_card", stripe_card:, headless: true %>
- <% if owner %> <%= link_to card_grant_path(card_grant, show_details: (!@show_card_details).to_s), class: "btn w-full mt-4", data: { turbo: true } do %> - <%= inline_icon @show_card_details ? "view-fill" : "view" %> - <%= @show_card_details ? "Hide card details" : "Show card details" %> + <%= inline_icon revealed ? "view-fill" : "view" %> + <%= revealed ? "Hide details" : "Show details" %> <% end %> <% end %> - - <% if stripe_card.frozen? && stripe_card.last_frozen_by && stripe_card.last_frozen_by != User.system_user %> -

- Frozen by <%= user_mention stripe_card.last_frozen_by %> -

- <% end %>
- <% details_pane = capture do %> -

Card details

-
- <%= render "card_grants/field", - label: "Card number", - monospace: true, - value: revealed ? stripe_card.formatted_card_number : stripe_card.hidden_card_number_with_last_four, - copy: revealed ? stripe_card.full_card_number : nil, - hint: owner ? nil : "Hidden to protect security", - hint_icon: owner ? "info" : "private", - hint_class: "muted" %> - - <%= render "card_grants/field", - label: "Expiry", - monospace: true, - value: render_exp_date(stripe_card), - copy: render_short_exp_date(stripe_card), - hint: "This is what you enter when asked for expiration date at checkout. It's different from your grant's spend by date.", - hint_icon: "important", - hint_class: "text-yellow" %> + <% fields = capture do %> +
+ Card number + + <% if revealed %> + <%= copy_to_clipboard(stripe_card.full_card_number, tooltip_direction: "w") { stripe_card.formatted_card_number } %> + <% else %> + <%= stripe_card.hidden_card_number_with_last_four %> + <% end %> + +
- <%= render "card_grants/field", - label: "CVC", - monospace: true, - value: revealed ? stripe_card.cvc : stripe_card.hidden_cvc, - copy: revealed ? stripe_card.cvc : nil %> +
+ + Expiry + + <%= inline_icon "important", size: 18, style: "position: unset;" %> + + + + <%= copy_to_clipboard(render_short_exp_date(stripe_card), tooltip_direction: "w") { render_exp_date stripe_card } %> + +
- <% if stripe_card.cash_withdrawal_enabled? %> -
- PIN - •••• - -
- <% end %> +
+ CVC + + <% if revealed %> + <%= copy_to_clipboard(stripe_card.cvc, tooltip_direction: "w") { stripe_card.cvc } %> + <% else %> + <%= stripe_card.hidden_cvc %> + <% end %> +
-

- Billing address - Use this, not your home address. -

-
- <%= render "card_grants/field", - label: cardholder.stripe_billing_address_line2.presence ? "Address line 1" : "Address", - value: cardholder.stripe_billing_address_line1, - copy: cardholder.stripe_billing_address_line1, - hint: default_address ? "You're using HCB's default billing address, not your home address. You can change it in your user settings if you'd like." : nil %> + <% if stripe_card.cash_withdrawal_enabled? %> +
+ PIN + + •••• + <%= inline_icon "view", class: "pointer align-middle", "data-action": "click->pin#view" %> + +
+ <% end %> - <% if cardholder.stripe_billing_address_line2.presence %> - <%= render "card_grants/field", - label: "Address line 2", - value: cardholder.stripe_billing_address_line2, - copy: cardholder.stripe_billing_address_line2 %> - <% end %> +

Billing address

- <%= render "card_grants/field", - label: "City", - value: cardholder.stripe_billing_address_city, - copy: cardholder.stripe_billing_address_city %> +
+ + Street + <% if stripe_card.user == current_user && cardholder.default_billing_address? %> + <%= link_to settings_address_path, class: "info tooltipped tooltipped--e", "aria-label": "This is HCB's billing address, not your home address. You can change it in your settings.", style: "display: flex;", data: { turbo: false } do %> + <%= inline_icon "info", size: 18, style: "position: unset;" %> + <% end %> + <% end %> + + + <%= copy_to_clipboard([cardholder.stripe_billing_address_line1, cardholder.stripe_billing_address_line2.presence].compact.join(" "), tooltip_direction: "w") do %> + <%= cardholder.stripe_billing_address_line1 %> + <%= cardholder.stripe_billing_address_line2.presence %> + <% end %> + +
- <%= render "card_grants/field", - label: "State", - value: cardholder.stripe_billing_address_state, - copy: cardholder.stripe_billing_address_state %> +
+ City + <%= copy_to_clipboard(cardholder.stripe_billing_address_city, tooltip_direction: "w") %> +
- <%= render "card_grants/field", - label: "ZIP/Postal code", - value: stripe_card.stripe_cardholder.address_postal_code, - copy: stripe_card.stripe_cardholder.address_postal_code %> +
+ State + <%= copy_to_clipboard(cardholder.stripe_billing_address_state, tooltip_direction: "w") %> +
- <% if stripe_card.user.phone_number %> - <%= render "card_grants/field", - label: "Phone number", - value: stripe_card.user.pretty_phone_number, - copy: stripe_card.user.pretty_phone_number, - hint: "This is your phone number — use this if asked during checkout." %> - <% end %> +
+ ZIP + <%= copy_to_clipboard(stripe_card.stripe_cardholder.address_postal_code, tooltip_direction: "w") %>
+ + <% if stripe_card.user.pretty_phone_number.presence %> +
+ Phone + <%= copy_to_clipboard(stripe_card.user.pretty_phone_number, tooltip_direction: "w") %> +
+ <% end %> <% end %> <% if card_grant.user == current_user %> -
<%= details_pane %>
+
<%= fields %>
<% elsif current_user&.auditor? %> - <% admin_tool("grant-split__pane") do %><%= details_pane %><% end %> + <% admin_tool("grant-panel__fields") do %><%= fields %><% end %> <% end %>
<% end %> + +<%= render partial: "card_grants/actions", locals: { card_grant: } %> diff --git a/app/views/card_grants/_field.html.erb b/app/views/card_grants/_field.html.erb deleted file mode 100644 index 1c7e06b392..0000000000 --- a/app/views/card_grants/_field.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%# locals: (label:, value:, copy: nil, hint: nil, hint_icon: "info", hint_class: "info", monospace: false, mask: true) %> - -
"> - - <%= label %> - <% if hint.present? %> - - <%= inline_icon hint_icon, size: 18, style: "position: unset;" %> - - <% end %> - - "><%= value %> - <% if copy.present? %> - <%= copy_to_clipboard(copy, tooltip_direction: "w", class: "btn btn-small grant-copy", "data-clipboard-confirm-text-value": "Copied!") { "Copy" } %> - <% else %> - - <% end %> -
diff --git a/app/views/card_grants/_grant_details.html.erb b/app/views/card_grants/_grant_details.html.erb index b26e0b5fc4..a9be080aa1 100644 --- a/app/views/card_grants/_grant_details.html.erb +++ b/app/views/card_grants/_grant_details.html.erb @@ -1,63 +1,46 @@ <%# locals: (card_grant:) %> -
-
-

Merchants

-

- <% if card_grant.allowed_merchants.any? %> - <%= card_grant.allowed_merchant_names.to_sentence %> - <% else %> - Anywhere - <% end %> -

- <% if card_grant.allowed_merchants.any? %> -

- IDs: <%= card_grant.allowed_merchants.join(", ") %> +

+
+
+ Merchants +

+ <%= card_grant.allowed_merchants.any? ? card_grant.allowed_merchant_names.to_sentence : "Anywhere" %>

- <% end %> -
+
-
-

Categories

-

- <%= card_grant.allowed_category_names.to_sentence.presence || "Anything" %> -

-
+
+ Categories +

+ <%= card_grant.allowed_category_names.to_sentence.presence || "Anything" %> +

+
-
-

Deadline

-

<%= format_date card_grant.expiration_at %>

-

Unspent funds return to <%= card_grant.event.name %>.

-
+ <% if card_grant.keyword_lock.present? %> +
+ Keyword +

<%= card_grant.keyword_lock %>

+
+ <% end %> - <% if card_grant.one_time_use %> -
-

One time use

-

Yes

-

The card freezes itself after your first purchase.

+
+ One time use +

<%= card_grant.one_time_use ? "Yes" : "No" %>

- <% end %> - <% if card_grant.keyword_lock.present? %> -
-

Merchant keyword lock

-

<%= card_grant.keyword_lock %>

-

Merchant names must contain this.

+
+ Unspent funds +

Return to <%= card_grant.event.name %>

- <% end %> + - <% if card_grant.purpose.present? %> -
-

Purpose

-

<%= card_grant.purpose %>

-
- <% end %> +
+

+ <%= @card_grant.event.name %> sets these limits, not HCB. +

+

+ To change these restrictions, message the <%= @card_grant.event.name %> team. + HCB cannot change your grant limits. +

+
- -

- - If there are any issues with the grant amount, spend by date, or allowed - merchants, please reach out to the <%= card_grant.event.name %> team. HCB - staff are unable to help with these requests. - -

diff --git a/app/views/card_grants/_hero.html.erb b/app/views/card_grants/_hero.html.erb index fa10406f3b..94c91959d8 100644 --- a/app/views/card_grants/_hero.html.erb +++ b/app/views/card_grants/_hero.html.erb @@ -7,103 +7,75 @@ <% days_left = (card_grant.expiration_at.to_date - Date.current).to_i %> <% spendable = card_grant.active? && !card_grant.pending_invite? %> -
-
-
-

<%= own ? "Your grant" : "Card grant" %>

- - <%= status_badge card_grant.status_badge_type %><%= card_grant.state_text %> - - <% if card_grant.purpose.present? %> - for <%= card_grant.purpose %> - <% end %> -
+<% manage = organizer_signed_in?(as: :member) %> -
-

<%= render_money remaining %>

-

- <% if remaining == total %> - to spend - <% else %> - left to spend of <%= render_money total %> - <% end %> -

-
- -
- " - style="width: <%= number_to_percentage(remaining_fraction * 100, precision: 2) %>"> -
- -

- <% if card_grant.pending_invite? && !own %> - Activate the card below and you can start spending straight away. - <% else %> - <%= card_grant.user.first_name %> can spend this with the card issued to - them - <% end %> -

+<% if manage %> +
+ <%= render "card_grants/actions/edit", card_grant: %> +
+<% end %> - <% if card_grant.stripe_card&.canceled? %> -

- Refunds received after cancelation will be sent to <%= card_grant.event.name %>. - <% if card_grant.reimbursement_report.present? %> - The funds will not automatically be contributed towards the reimbursement report cap. - <% end %> -

- <% elsif card_grant.stripe_card&.frozen? %> -

- Refunds received while frozen will still process and be returned to the card. -

+
" + style="<%= card_grant.event.background_image.attached? ? "--bg-image: url(#{url_for(card_grant.event.background_image).dump})" : "--bg-image: #{GeoPattern.generate(card_grant.stripe_card&.public_id || card_grant.public_id).to_data_uri}" %>"> +
+

<%= own ? "Your grant" : "Card grant" %>

+ + <%= status_badge card_grant.status_badge_type %><%= card_grant.state_text %> + + <% if card_grant.purpose.present? %> + <%= card_grant.purpose %> <% end %>
- +
<% if footer.present? %> -
+
<%= footer %>
<% end %> diff --git a/app/views/card_grants/_section_heading.html.erb b/app/views/card_grants/_section_heading.html.erb index 1817cec5c3..b3ff5cd1e4 100644 --- a/app/views/card_grants/_section_heading.html.erb +++ b/app/views/card_grants/_section_heading.html.erb @@ -1,9 +1,6 @@ -<%# locals: (number:, title:, subtitle: nil) %> +<%# locals: (icon:, title:) %> -

- - <%= title %> - <% if subtitle.present? %> - <%= subtitle %> - <% end %> +

+ + <%= title %>

diff --git a/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb b/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb index 4761b30e6d..aaf0d5375b 100644 --- a/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb +++ b/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb @@ -2,14 +2,14 @@ <% if card_grant.card_grant_setting.reimbursement_conversions_enabled? && card_grant.balance.positive? %> <% reimbursement_tooltip = policy(card_grant).convert_to_reimbursement_report? ? "Cancel this grant and open a reimbursement report with remaining funds" : "You don't have permission to perform this action" %> - <%= link_to convert_to_reimbursement_report_event_card_grant_path(id: card_grant.hashid, event_id: @event.slug), - data: { turbo_method: :post, turbo_confirm: "Are you sure? This will cancel your card grant and convert it into a reimbursement report.", turbo_confirm_danger: true }, - class: "btn btn--list-item", - disabled: !policy(card_grant).convert_to_reimbursement_report? do %> - <%= inline_icon "attachment", size: 20 %> -
+
<%= label %> -

<%= subtitle.presence || reimbursement_tooltip %>

+

<%= subtitle.presence || reimbursement_tooltip %>

+ <%= link_to convert_to_reimbursement_report_event_card_grant_path(id: card_grant.hashid, event_id: @event.slug), + data: { turbo_method: :post, turbo_confirm: "Are you sure? This will cancel your card grant and convert it into a reimbursement report.", turbo_confirm_danger: true }, + class: "btn", + disabled: !policy(card_grant).convert_to_reimbursement_report? do %> + Get reimbursed + <% end %>
- <% end %> <% end %> diff --git a/app/views/card_grants/actions/_return.html.erb b/app/views/card_grants/actions/_return.html.erb index af41d952c5..e58f86fb33 100644 --- a/app/views/card_grants/actions/_return.html.erb +++ b/app/views/card_grants/actions/_return.html.erb @@ -1,13 +1,15 @@ -<%# locals: (card_grant:) %> +<%# locals: (card_grant:, label: "", subtitle: "") %> -<% return_tooltip = "Cancel this grant and return funds to the organization" %> -<%= link_to "#", data: { behavior: "modal_trigger", modal: "return-grant" }, class: "btn btn--list-item" do %> - <%= inline_icon "support", size: 20 %> -
- Return grant -

<%= return_tooltip %>

+<% return_tooltip = subtitle.presence || "Cancel this grant and return funds to the organization" %> +
+ + <%= label.presence || "Return grant" %> + +

<%= return_tooltip %>

+ <%= link_to "#", data: { behavior: "modal_trigger", modal: "return-grant" }, class: "btn" do %> + Return grant + <% end %>
-<% end %>
-
-

- <%= @card_grant.event.name %> sets these limits, not HCB. -

-

- To change these restrictions, message the <%= @card_grant.event.name %> team. - HCB cannot change your grant limits. -

-
+ <% unless organizer_signed_in? %> +
+

+ <%= @card_grant.event.name %> sets these limits, not HCB. +

+

+ To change these restrictions, message the <%= @card_grant.event.name %> team. + HCB cannot change your grant limits. +

+
+ <% end %>
diff --git a/app/views/card_grants/_hero.html.erb b/app/views/card_grants/_hero.html.erb index 4391083101..f6aac91bf2 100644 --- a/app/views/card_grants/_hero.html.erb +++ b/app/views/card_grants/_hero.html.erb @@ -20,7 +20,6 @@
-

<%= own ? "Your grant" : "Card grant" %>

<%= card_grant.state_text %> @@ -41,7 +40,9 @@
-

Spend it by

+

+ <%= own ? "Spend it by" : "Expires on" %> +

<%= format_date card_grant.expiration_at %>

diff --git a/app/views/card_grants/_section_heading.html.erb b/app/views/card_grants/_section_heading.html.erb index b3ff5cd1e4..79afd3f315 100644 --- a/app/views/card_grants/_section_heading.html.erb +++ b/app/views/card_grants/_section_heading.html.erb @@ -1,6 +1,8 @@ <%# locals: (icon:, title:) %>

- + <% unless organizer_signed_in? %> + + <% end %> <%= title %>

diff --git a/app/views/card_grants/show.html.erb b/app/views/card_grants/show.html.erb index 4a35025e5a..fc73fac81e 100644 --- a/app/views/card_grants/show.html.erb +++ b/app/views/card_grants/show.html.erb @@ -46,17 +46,17 @@ <%= render partial: "card_grants/invitation", locals: { card_grant: @card_grant } %> <% end %> <% elsif @card_grant.user.admin? || organizer_signed_in? || own %> - <%= render "card_grants/section_heading", icon: "1", title: "What you can spend it on" %> + <%= render "card_grants/section_heading", icon: "1", title: own ? "Spending controls" : "What you can spend it on" %> <%= render partial: "card_grants/grant_details", locals: { card_grant: @card_grant } %> <% if stripe_card.present? %> - <%= render "card_grants/section_heading", icon: "2", title: "Pay with your card" %> + <%= render "card_grants/section_heading", icon: "2", title: own ? "Virtual card" : "Pay with your card" %> <%= render partial: "card_grants/card_details", locals: { card_grant: @card_grant } %> <% end %> <%= render partial: "card_grant/pre_authorizations/organizer_info", locals: { pre_authorization: @card_grant.pre_authorization, link_to_pre_authorization: true } if @card_grant.pre_authorization.present? && organizer_signed_in? %> - <%= render "card_grants/section_heading", icon: "3", title: "Upload receipts for each purchase (required)" %> + <%= render "card_grants/section_heading", icon: "3", title: own ? "Transactions" : "Upload receipts for each purchase (required)" %> <%= render partial: "card_grants/transactions", locals: { hcb_codes: @hcb_codes, ledger: @ledger, items: @items, heading: false } %> <% end %> From e6d909fa08573f4bee9f23c327fe9528cf97e457 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Mon, 31 Aug 2026 18:43:39 -0700 Subject: [PATCH 11/38] Improve display --- app/views/card_grants/_card_details.html.erb | 5 +++++ app/views/card_grants/_grant_details.html.erb | 4 ++-- app/views/card_grants/_section_heading.html.erb | 4 ++-- app/views/card_grants/show.html.erb | 10 +++++----- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/views/card_grants/_card_details.html.erb b/app/views/card_grants/_card_details.html.erb index 3605ff3354..66924c3d02 100644 --- a/app/views/card_grants/_card_details.html.erb +++ b/app/views/card_grants/_card_details.html.erb @@ -22,6 +22,11 @@
<% fields = capture do %> +
+ Name on card + <%= copy_to_clipboard(stripe_card.user.name, tooltip_direction: "w") %> +
+
Card number diff --git a/app/views/card_grants/_grant_details.html.erb b/app/views/card_grants/_grant_details.html.erb index 00562729cb..37848c80f7 100644 --- a/app/views/card_grants/_grant_details.html.erb +++ b/app/views/card_grants/_grant_details.html.erb @@ -1,4 +1,4 @@ -<%# locals: (card_grant:) %> +<%# locals: (card_grant:, own:) %>
@@ -34,7 +34,7 @@
- <% unless organizer_signed_in? %> + <% if own %>

<%= @card_grant.event.name %> sets these limits, not HCB. diff --git a/app/views/card_grants/_section_heading.html.erb b/app/views/card_grants/_section_heading.html.erb index 79afd3f315..5208bbf672 100644 --- a/app/views/card_grants/_section_heading.html.erb +++ b/app/views/card_grants/_section_heading.html.erb @@ -1,7 +1,7 @@ -<%# locals: (icon:, title:) %> +<%# locals: (icon: "", title:, own:) %>

- <% unless organizer_signed_in? %> + <% if own && !icon.empty? %> <% end %> <%= title %> diff --git a/app/views/card_grants/show.html.erb b/app/views/card_grants/show.html.erb index fc73fac81e..e3c6fcca98 100644 --- a/app/views/card_grants/show.html.erb +++ b/app/views/card_grants/show.html.erb @@ -46,23 +46,23 @@ <%= render partial: "card_grants/invitation", locals: { card_grant: @card_grant } %> <% end %> <% elsif @card_grant.user.admin? || organizer_signed_in? || own %> - <%= render "card_grants/section_heading", icon: "1", title: own ? "Spending controls" : "What you can spend it on" %> - <%= render partial: "card_grants/grant_details", locals: { card_grant: @card_grant } %> + <%= render "card_grants/section_heading", icon: "1", title: own ? "What you can spend it on" : "Spending controls", own: own %> + <%= render partial: "card_grants/grant_details", locals: { card_grant: @card_grant, own: own } %> <% if stripe_card.present? %> - <%= render "card_grants/section_heading", icon: "2", title: own ? "Virtual card" : "Pay with your card" %> + <%= render "card_grants/section_heading", icon: "2", title: own ? "Pay with your card" : "Virtual card", own: own %> <%= render partial: "card_grants/card_details", locals: { card_grant: @card_grant } %> <% end %> <%= render partial: "card_grant/pre_authorizations/organizer_info", locals: { pre_authorization: @card_grant.pre_authorization, link_to_pre_authorization: true } if @card_grant.pre_authorization.present? && organizer_signed_in? %> - <%= render "card_grants/section_heading", icon: "3", title: own ? "Transactions" : "Upload receipts for each purchase (required)" %> + <%= render "card_grants/section_heading", icon: "3", title: own ? "Upload receipts for each purchase (required)" : "Transactions", own: own %> <%= render partial: "card_grants/transactions", locals: { hcb_codes: @hcb_codes, ledger: @ledger, items: @items, heading: false } %> <% end %> <% if policy(@card_grant.comments.build).create? %> - <%= render "card_grants/section_heading", icon: "", title: "Comments" %> + <%= render "card_grants/section_heading", title: "Comments", own: own %> <%= render "comments/list", comments: @card_grant.comments, show_blankslate: true %> <%= render "comments/form", commentable: @card_grant %> <% end %> From 9608d8443cdae3b97e6a2f97173bbbe5df97a275 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Tue, 1 Sep 2026 09:02:17 -0700 Subject: [PATCH 12/38] Add collapsible --- .../stylesheets/components/_card_grants.scss | 53 +++++++++++++++++++ app/views/card_grants/show.html.erb | 16 ++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/components/_card_grants.scss b/app/assets/stylesheets/components/_card_grants.scss index 8d9392c841..a691715891 100644 --- a/app/assets/stylesheets/components/_card_grants.scss +++ b/app/assets/stylesheets/components/_card_grants.scss @@ -420,3 +420,56 @@ $grant-upload-height: 11.5rem; align-self: flex-start; } } + +.grant-comments__summary { + cursor: pointer; + list-style: none; + + &::-webkit-details-marker { + display: none; + } + + &::before { + content: ''; + display: inline-block; + flex: 0 0 auto; + width: 0.45rem; + height: 0.45rem; + margin-right: 0.625rem; + border-right: 2px solid map-get($palette, muted); + border-bottom: 2px solid map-get($palette, muted); + transform: rotate(-45deg); + transition: transform 0.15s ease-in-out; + } + + .grant-section { + display: inline-flex; + vertical-align: middle; + } +} + +.grant-comments[open] > .grant-comments__summary::before { + transform: rotate(45deg); +} + +.grant-comments__count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.5rem; + padding: 0 0.4rem; + font-size: 0.875rem; + font-weight: 700; + line-height: 1.5rem; + border-radius: var(--radius-xl); + background: rgba(map-get($palette, muted), 0.15); + color: map-get($palette, slate); + + html[data-dark='true'] & { + color: map-get($palette, smoke); + } +} + +.grant-comments__body { + margin-top: 0.875rem; +} diff --git a/app/views/card_grants/show.html.erb b/app/views/card_grants/show.html.erb index e3c6fcca98..fec49e5087 100644 --- a/app/views/card_grants/show.html.erb +++ b/app/views/card_grants/show.html.erb @@ -61,8 +61,18 @@ <% end %> <% if policy(@card_grant.comments.build).create? %> + <% comments = policy_scope(@card_grant.comments) %> - <%= render "card_grants/section_heading", title: "Comments", own: own %> - <%= render "comments/list", comments: @card_grant.comments, show_blankslate: true %> - <%= render "comments/form", commentable: @card_grant %> +
> + +

+ Comments + <%= comments.size %> +

+
+
+ <%= render "comments/list", comments: @card_grant.comments, show_blankslate: true %> + <%= render "comments/form", commentable: @card_grant %> +
+
<% end %> From 52cb11640159beff119720912dc5c00ec51d472d Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Tue, 1 Sep 2026 09:24:33 -0700 Subject: [PATCH 13/38] Fix buttons in settings modal --- app/views/card_grants/_actions.html.erb | 8 +-- .../_convert_to_reimbursement_report.html.erb | 21 ++++++- .../stripe_cards/actions/_freeze.html.erb | 61 ++++++++++++++++--- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/app/views/card_grants/_actions.html.erb b/app/views/card_grants/_actions.html.erb index 5d5a8ee94a..da0c0eb257 100644 --- a/app/views/card_grants/_actions.html.erb +++ b/app/views/card_grants/_actions.html.erb @@ -5,8 +5,8 @@

Controls

<% if organizer_signed_in?(as: :member) %> - <%= render "stripe_cards/actions/freeze", stripe_card: card_grant.stripe_card, label: "Pause spending instantly", subtitle: "You can unfreeze it yourself at any time" if card_grant.stripe_card.present? %> - <%= render "card_grants/actions/convert_to_reimbursement_report", card_grant:, label: card_grant.user == current_user ? "I already paid out of pocket" : "Create reimbursement report", subtitle: "Swap the balance for a reimbursement" %> + <%= render "stripe_cards/actions/freeze", stripe_card: card_grant.stripe_card, variant: "control", label: "Pause spending instantly", subtitle: "You can unfreeze it yourself at any time" if card_grant.stripe_card.present? %> + <%= render "card_grants/actions/convert_to_reimbursement_report", card_grant:, variant: "control", label: card_grant.user == current_user ? "I already paid out of pocket" : "Create reimbursement report", subtitle: "Swap the balance for a reimbursement" %> <% if card_grant.user == current_user %> <%= render "card_grants/actions/return", card_grant:, label: "I don’t need this money", subtitle: "Send the remaining balance back to #{card_grant.event.name}" %> <%= render "card_grants/actions/need_more_money", card_grant: %> @@ -15,8 +15,8 @@ <% end %> <%= render "card_grants/actions/support", card_grant: %> <% elsif card_grant.user == current_user %> - <%= render "stripe_cards/actions/freeze", stripe_card: card_grant.stripe_card, label: "Pause spending instantly", subtitle: "You can unfreeze it yourself at any time" if card_grant.stripe_card.present? %> - <%= render "card_grants/actions/convert_to_reimbursement_report", card_grant:, label: "Get reimbursed", subtitle: "Swap the balance for a reimbursement" %> + <%= render "stripe_cards/actions/freeze", stripe_card: card_grant.stripe_card, variant: "control", label: "Pause spending instantly", subtitle: "You can unfreeze it yourself at any time" if card_grant.stripe_card.present? %> + <%= render "card_grants/actions/convert_to_reimbursement_report", card_grant:, variant: "control", label: "Get reimbursed", subtitle: "Swap the balance for a reimbursement" %> <%= render "card_grants/actions/return", card_grant:, label: "I don’t need this money", subtitle: "Send the remaining balance back to #{card_grant.event.name}" %> <%= render "card_grants/actions/need_more_money", card_grant: %> <%= render "card_grants/actions/support", card_grant: %> diff --git a/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb b/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb index 1fca127a71..a9b716ee6e 100644 --- a/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb +++ b/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb @@ -1,15 +1,30 @@ -<%# locals: (card_grant:, label: "Create reimbursement report", subtitle: "") %> +<%# locals: (card_grant:, label: "Create reimbursement report", subtitle: "", variant: "list_item") %> <% if card_grant.card_grant_setting.reimbursement_conversions_enabled? && card_grant.balance.positive? %> <% reimbursement_tooltip = policy(card_grant).convert_to_reimbursement_report? ? "Cancel this grant and open a reimbursement report with remaining funds" : "You don't have permission to perform this action" %> + <% convert_path = convert_to_reimbursement_report_event_card_grant_path(id: card_grant.hashid, event_id: @event.slug) %> + <% convert_data = { turbo_method: :post, turbo_confirm: "Are you sure? This will cancel your card grant and convert it into a reimbursement report.", turbo_confirm_danger: true } %> + <% if variant.to_s == "control" %>
<%= label %>

<%= subtitle.presence || reimbursement_tooltip %>

- <%= link_to convert_to_reimbursement_report_event_card_grant_path(id: card_grant.hashid, event_id: @event.slug), - data: { turbo_method: :post, turbo_confirm: "Are you sure? This will cancel your card grant and convert it into a reimbursement report.", turbo_confirm_danger: true }, + <%= link_to convert_path, + data: convert_data, class: "btn", disabled: !policy(card_grant).convert_to_reimbursement_report? do %> Get reimbursed <% end %>
+ <% else %> + <%= link_to convert_path, + data: convert_data, + class: "btn btn--list-item", + disabled: !policy(card_grant).convert_to_reimbursement_report? do %> + <%= inline_icon "attachment", size: 20 %> +
+ <%= label %> +

<%= subtitle.presence || reimbursement_tooltip %>

+
+ <% end %> + <% end %> <% end %> diff --git a/app/views/stripe_cards/actions/_freeze.html.erb b/app/views/stripe_cards/actions/_freeze.html.erb index 49b15876c8..0865b266f7 100644 --- a/app/views/stripe_cards/actions/_freeze.html.erb +++ b/app/views/stripe_cards/actions/_freeze.html.erb @@ -1,24 +1,40 @@ -<%# locals: (stripe_card:, label: "", subtitle: "") %> +<%# locals: (stripe_card:, label: "", subtitle: "", variant: "list_item") %> + +<% control = variant.to_s == "control" %> <% if stripe_card&.stripe_status == 'inactive' %> - <% defrost_tooltip = "Instantly re-freeze #{stripe_card.user == current_user ? "your" : stripe_card.user.possessive_name} card anytime" %> + <% defrost_tooltip = subtitle.presence || "Instantly re-freeze #{stripe_card.user == current_user ? "your" : stripe_card.user.possessive_name} card anytime" %> + <% defrost_subtitle = policy(stripe_card).defrost? ? defrost_tooltip : "You don't have permission to perform this action" %> + <% if control %>
- Defrost -

<%= policy(stripe_card).defrost? ? defrost_tooltip : "You don't have permission to perform this action" %>

+ <%= label.presence || "Defrost" %> +

<%= defrost_subtitle %>

<%= link_to defrost_stripe_card_path(stripe_card), data: { turbo_method: :post }, class: "btn", disabled: !policy(stripe_card).defrost? do %> - Defrost + Defrost <% end %>
+ <% else %> + <%= link_to defrost_stripe_card_path(stripe_card), + data: { turbo_method: :post }, + class: "btn btn--list-item", + disabled: !policy(stripe_card).defrost? do %> + <%= inline_icon "freeze" %> +
+ Defrost +

<%= defrost_subtitle %>

+
+ <% end %> + <% end %> <% elsif stripe_card&.stripe_status == 'active' %> - <% freeze_tooltip = subtitle.presence || "Instantly freeze #{stripe_card.user == current_user ? "your" : stripe_card.user.possessive_name} card anytime" %> + <% freeze_tooltip = subtitle.presence || label.presence || "Instantly freeze #{stripe_card.user == current_user ? "your" : stripe_card.user.possessive_name} card anytime" %> + <% freeze_subtitle = policy(stripe_card).freeze? ? freeze_tooltip : "You don't have permission to perform this action" %> + <% if control %>
- - <%= label %> - -

<%= policy(stripe_card).freeze? ? freeze_tooltip : "You don't have permission to perform this action" %>

+ <%= label.presence || "Freeze" %> +

<%= freeze_subtitle %>

<%= link_to freeze_stripe_card_path(stripe_card), data: { turbo_method: :post }, class: "btn", @@ -26,9 +42,34 @@ Freeze <% end %>
+ <% else %> + <%= link_to freeze_stripe_card_path(stripe_card), + data: { turbo_method: :post }, + class: "btn btn--list-item", + disabled: !policy(stripe_card).freeze? do %> + <%= inline_icon "freeze" %> +
+ Freeze +

<%= freeze_subtitle %>

+
+ <% end %> + <% end %> <% elsif !stripe_card&.canceled? %> + <% if control %>
Freeze

This card cannot be frozen because it has not been activated

+ <% else %> + <%= link_to "#", + data: { turbo_method: :post }, + class: "btn btn--list-item", + disabled: true do %> + <%= inline_icon "freeze" %> +
+ Freeze +

This card cannot be frozen because it has not been activated

+
+ <% end %> + <% end %> <% end %> From d694cae99da6ea07f40aa121802f3535e3041e52 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Tue, 1 Sep 2026 09:31:33 -0700 Subject: [PATCH 14/38] Improve form UI --- app/controllers/card_grants_controller.rb | 4 --- app/policies/card_grant_policy.rb | 15 +++++---- app/views/card_grants/_edit_nav.html.erb | 3 -- app/views/card_grants/actions/_edit.html.erb | 4 +-- app/views/card_grants/edit_overview.html.erb | 32 -------------------- app/views/card_grants/edit_purpose.html.erb | 10 +++--- config/routes.rb | 1 - 7 files changed, 14 insertions(+), 55 deletions(-) delete mode 100644 app/views/card_grants/edit_overview.html.erb diff --git a/app/controllers/card_grants_controller.rb b/app/controllers/card_grants_controller.rb index 6ac7cfa5bf..2134cdf72a 100644 --- a/app/controllers/card_grants_controller.rb +++ b/app/controllers/card_grants_controller.rb @@ -136,10 +136,6 @@ def bulk_upload_template disposition: "attachment" end - def edit_overview - authorize @card_grant - end - def edit_purpose authorize @card_grant end diff --git a/app/policies/card_grant_policy.rb b/app/policies/card_grant_policy.rb index c355d5ad84..f67b0e9c66 100644 --- a/app/policies/card_grant_policy.rb +++ b/app/policies/card_grant_policy.rb @@ -21,17 +21,16 @@ def spending? record.event.is_public? || user&.auditor? || user_in_event? end - def edit_overview? + def edit_purpose? auditor_or_member? && record.active? end - alias_method :edit_actions?, :edit_overview? - alias_method :edit_usage_restrictions?, :edit_overview? - alias_method :edit_expiration?, :edit_overview? - alias_method :edit_purpose?, :edit_overview? - alias_method :edit_balance?, :edit_overview? - alias_method :edit_topup?, :edit_overview? - alias_method :edit_withdraw?, :edit_overview? + alias_method :edit_actions?, :edit_purpose? + alias_method :edit_usage_restrictions?, :edit_purpose? + alias_method :edit_expiration?, :edit_purpose? + alias_method :edit_balance?, :edit_purpose? + alias_method :edit_topup?, :edit_purpose? + alias_method :edit_withdraw?, :edit_purpose? def activate? (user&.admin? || (cardholder? && authorized_to_activate?)) && record.active? diff --git a/app/views/card_grants/_edit_nav.html.erb b/app/views/card_grants/_edit_nav.html.erb index 437c8bb423..6697b31b58 100644 --- a/app/views/card_grants/_edit_nav.html.erb +++ b/app/views/card_grants/_edit_nav.html.erb @@ -27,9 +27,6 @@ <% end %>
    - <%= settings_tab active: active == "overview" do %> - <%= link_to "Overview", edit_overview_event_card_grant_path(@card_grant), data: { turbo: true } %> - <% end %> <%= settings_tab active: active == "purpose" do %> <%= link_to "Purpose and instructions", edit_purpose_event_card_grant_path(@card_grant), data: { turbo: true } %> <% end %> diff --git a/app/views/card_grants/actions/_edit.html.erb b/app/views/card_grants/actions/_edit.html.erb index c73a5ef5f1..745e45970c 100644 --- a/app/views/card_grants/actions/_edit.html.erb +++ b/app/views/card_grants/actions/_edit.html.erb @@ -8,7 +8,7 @@ <% end %> <% end %> -<% if policy(card_grant).edit_overview? %> +<% if policy(card_grant).edit_purpose? %>
    "> <%= link_to edit_card_grant_path(card_grant), class: "btn whitespace-nowrap", @@ -25,6 +25,6 @@ <% end %> diff --git a/app/views/card_grants/edit_overview.html.erb b/app/views/card_grants/edit_overview.html.erb deleted file mode 100644 index ef5c9d1580..0000000000 --- a/app/views/card_grants/edit_overview.html.erb +++ /dev/null @@ -1,32 +0,0 @@ - - <%= render "card_grants/edit_nav", active: "overview" %> - -
    -

    - Grant sent to - <%= mail_to @card_grant.user.email %> (<%= copy_to_clipboard(@card_grant.user.public_id, class: "w-fit", tooltip_direction: "e") %>) -

    - -

    - Expires on - <%= format_date @card_grant.expiration_at %> -

    - -

    - One time use? - <%= @card_grant.one_time_use ? "Yes" : "No" %> -

    - -

    - Allowed merchants - <% if @card_grant.allowed_merchants.any? %> - - <%= @card_grant.allowed_merchant_names.join(", ") %>
    - (IDs: <%= @card_grant.allowed_merchants.join(", ") %>) -
    - <% else %> - All merchants allowed - <% end %> -

    -
    -
    diff --git a/app/views/card_grants/edit_purpose.html.erb b/app/views/card_grants/edit_purpose.html.erb index db938ac0b9..8aa424d6d5 100644 --- a/app/views/card_grants/edit_purpose.html.erb +++ b/app/views/card_grants/edit_purpose.html.erb @@ -2,18 +2,18 @@ <%= render "card_grants/edit_nav", active: "purpose" %>
    -
    +
    <%= form_with(model: @card_grant, id: "card_grant_edit_form") do |form| %> -
    - <%= form.label :purpose %> +
    + <%= form.label :purpose %>
    <%= form.text_field :purpose, placeholder: "OpenAI Credits", disabled: !policy(@card_grant).update? %>
    -
    +
    <%= form.label :instructions, "Instructions (optional)", class: "bold" %> - <%= form.text_area :instructions, placeholder: "Please only purchase a soldering iron…", class: "min-h-[70px]", disabled: !policy(@card_grant).update? %> + <%= form.text_area :instructions, placeholder: "Please only purchase a soldering iron…", class: "!max-w-full min-h-[70px]", disabled: !policy(@card_grant).update? %>

    <%= link_to "https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#table-of-contents", target: "_blank", class: "flex items-center" do %> <%= inline_icon "markdown", size: 32 %> Styling with Markdown is supported diff --git a/config/routes.rb b/config/routes.rb index 4587bde2d9..7d9ebb81dd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1132,7 +1132,6 @@ post "disable_pre_authorization" post "permit_merchant" - get "edit/overview", to: "card_grants#edit_overview" get "edit/usage_restrictions", to: "card_grants#edit_usage_restrictions" get "edit/expiration", to: "card_grants#edit_expiration" get "edit/purpose", to: "card_grants#edit_purpose" From d6adf3ea41d9ae421322b8f47aabb6cedce03ecb Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Thu, 3 Sep 2026 21:03:51 -0700 Subject: [PATCH 15/38] improve appearance for large merchant and category lists --- .../stylesheets/components/_card_grants.scss | 109 ++++++++++++++++++ .../card_grants/_control_drawer.html.erb | 16 +++ app/views/card_grants/_control_field.html.erb | 16 +++ app/views/card_grants/_grant_details.html.erb | 45 ++++---- 4 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 app/views/card_grants/_control_drawer.html.erb create mode 100644 app/views/card_grants/_control_field.html.erb diff --git a/app/assets/stylesheets/components/_card_grants.scss b/app/assets/stylesheets/components/_card_grants.scss index a691715891..34d8fc21f3 100644 --- a/app/assets/stylesheets/components/_card_grants.scss +++ b/app/assets/stylesheets/components/_card_grants.scss @@ -255,6 +255,115 @@ $grant-hairline: rgba(200, 200, 200, 0.35); overflow-wrap: anywhere; } +// A long merchant or category list would otherwise stretch its strip cell into +// a tall single column, so past a couple of entries the cell shows a count and +// the entries move to a full-width drawer under the strip. CSS-only: each +// drawer has a hidden checkbox ahead of the strip, and the count label toggles +// it — so it works without JS and survives Turbo caching. +.grant-drawer__toggle { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.grant-disclosure { + display: inline-flex; + align-items: center; + gap: 0.125rem; + cursor: pointer; + user-select: none; + color: map-get($palette, info); + + html[data-dark='true'] & { + color: lighten($blue, 12%); + } + + &:hover { + text-decoration: underline; + } +} + +.grant-disclosure__caret { + transition: transform 0.15s ease; +} + +.grant-drawer { + display: none; + margin-top: 1rem; + padding-top: 1rem; + @include grant-hairline(top); +} + +.grant-drawer__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.5rem; +} + +.grant-drawer__close { + display: inline-flex; + align-items: center; + gap: 0.25rem; + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; + color: map-get($palette, muted); + + &:hover { + color: map-get($palette, slate); + } +} + +.grant-chips { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + margin: 0; + padding: 0; + list-style: none; + // Dozens of merchants stay inside one predictable block instead of pushing + // the rest of the page down. + max-height: 13rem; + overflow-y: auto; +} + +.grant-chip { + padding: 0.25rem 0.625rem; + border-radius: 999px; + font-size: 0.8125rem; + font-weight: 600; + line-height: 1.35; + max-width: 100%; + overflow-wrap: anywhere; + background: rgba(140, 150, 165, 0.12); + + html[data-dark='true'] & { + background: rgba(255, 255, 255, 0.08); + } +} + +@each $key in ('merchants', 'categories') { + ##{'grant-' + $key}:checked { + ~ ##{'grant-' + $key}-drawer { + display: block; + } + + ~ .grant-strip .grant-disclosure[for='#{'grant-' + $key}'] .grant-disclosure__caret { + transform: rotate(180deg); + } + } +} + +.grant-limits__note { + margin: 1rem -1rem -1rem; + padding: 0.5rem 1rem; + border-radius: 0 0 0.5rem 0.5rem; + background: rgba(140, 150, 165, 0.1); + @include grant-hairline(top); +} + // Pre-authorization: the cart-screenshot dropzone and its uploads share one // panel, so an empty dropzone reads as part of the step rather than a stray box. $grant-upload-height: 11.5rem; diff --git a/app/views/card_grants/_control_drawer.html.erb b/app/views/card_grants/_control_drawer.html.erb new file mode 100644 index 0000000000..239e805905 --- /dev/null +++ b/app/views/card_grants/_control_drawer.html.erb @@ -0,0 +1,16 @@ +<%# locals: (id:, label:, items:) %> + +

    +
    + <%= label %> + +
    +
      + <% items.each do |item| %> +
    • <%= item %>
    • + <% end %> +
    +
    diff --git a/app/views/card_grants/_control_field.html.erb b/app/views/card_grants/_control_field.html.erb new file mode 100644 index 0000000000..9f3a379675 --- /dev/null +++ b/app/views/card_grants/_control_field.html.erb @@ -0,0 +1,16 @@ +<%# locals: (id:, label:, items:, empty:, inline_limit: 2) %> + +
    + <%= label %> + + <% if items.blank? %> +

    <%= empty %>

    + <% elsif items.size <= inline_limit %> +

    <%= items.to_sentence %>

    + <% else %> + + <% end %> +
    diff --git a/app/views/card_grants/_grant_details.html.erb b/app/views/card_grants/_grant_details.html.erb index 37848c80f7..527793e185 100644 --- a/app/views/card_grants/_grant_details.html.erb +++ b/app/views/card_grants/_grant_details.html.erb @@ -1,46 +1,51 @@ <%# locals: (card_grant:, own:) %> -
    -
    -
    - Merchants -

    - <%= card_grant.allowed_merchants.any? ? card_grant.allowed_merchant_names.to_sentence : "Anywhere" %> -

    -
    +<% + merchants = card_grant.allowed_merchants.any? ? card_grant.allowed_merchant_names : [] + categories = card_grant.allowed_category_names + # Long lists collapse into a full-width drawer below the strip so one big list + # can't stretch the card into a single tall column. + drawers = { "Merchants" => merchants, "Categories" => categories }.select { |_, items| items.size > 2 } +%> -
    - Categories -

    - <%= card_grant.allowed_category_names.to_sentence.presence || "Anything" %> -

    -
    +
    + <% drawers.each_key do |label| %> + + <% end %> + +
    + <%= render "card_grants/control_field", id: "grant-merchants", label: "Merchants", items: merchants, empty: "Anywhere" %> + <%= render "card_grants/control_field", id: "grant-categories", label: "Categories", items: categories, empty: "Anything" %> <% if card_grant.keyword_lock.present? %> -
    +
    Keyword

    <%= card_grant.keyword_lock %>

    <% end %> -
    +
    One time use

    <%= card_grant.one_time_use ? "Yes" : "No" %>

    -
    +
    Unspent funds

    Return to <%= card_grant.event.name %>

    + <% drawers.each do |label, items| %> + <%= render "card_grants/control_drawer", id: "grant-#{label.parameterize}", label:, items: %> + <% end %> + <% if own %> -
    +

    - <%= @card_grant.event.name %> sets these limits, not HCB. + <%= card_grant.event.name %> sets these limits, not HCB.

    - To change these restrictions, message the <%= @card_grant.event.name %> team. + To change these restrictions, message the <%= card_grant.event.name %> team. HCB cannot change your grant limits.

    From 881c37eb263dfe7cd1b5e62254460ef8433f08fd Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Thu, 3 Sep 2026 21:16:42 -0700 Subject: [PATCH 16/38] lint --- app/assets/stylesheets/components/_card_grants.scss | 4 +++- app/views/card_grants/_grant_details.html.erb | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/components/_card_grants.scss b/app/assets/stylesheets/components/_card_grants.scss index 34d8fc21f3..37e5e4ff7a 100644 --- a/app/assets/stylesheets/components/_card_grants.scss +++ b/app/assets/stylesheets/components/_card_grants.scss @@ -350,7 +350,9 @@ $grant-hairline: rgba(200, 200, 200, 0.35); display: block; } - ~ .grant-strip .grant-disclosure[for='#{'grant-' + $key}'] .grant-disclosure__caret { + ~ .grant-strip + .grant-disclosure[for='#{'grant-' + $key}'] + .grant-disclosure__caret { transform: rotate(180deg); } } diff --git a/app/views/card_grants/_grant_details.html.erb b/app/views/card_grants/_grant_details.html.erb index 527793e185..bbf8516061 100644 --- a/app/views/card_grants/_grant_details.html.erb +++ b/app/views/card_grants/_grant_details.html.erb @@ -1,7 +1,6 @@ <%# locals: (card_grant:, own:) %> -<% - merchants = card_grant.allowed_merchants.any? ? card_grant.allowed_merchant_names : [] +merchants = card_grant.allowed_merchants.any? ? card_grant.allowed_merchant_names : [] categories = card_grant.allowed_category_names # Long lists collapse into a full-width drawer below the strip so one big list # can't stretch the card into a single tall column. From 9b5baeff9418e46df3b3202964095a7b39ccf191 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Sat, 12 Sep 2026 15:11:30 -0700 Subject: [PATCH 17/38] Fix card grants UI Restore the missing ERB tag in _grant_details, and make the controls grid's buttons consistent: every cell is now a .grant-control column whose button is bottom-pinned and full-width, matched as a descendant so button_to's display:contents form no longer breaks the selector. Co-Authored-By: Claude Opus 5 (1M context) --- .../stylesheets/components/_card_grants.scss | 15 ++++-- app/views/card_grants/_actions.html.erb | 2 +- app/views/card_grants/_grant_details.html.erb | 12 ++--- .../card_grants/actions/_cancel.html.erb | 36 ++++++++++---- .../_convert_to_reimbursement_report.html.erb | 4 +- .../actions/_need_more_money.html.erb | 4 +- .../card_grants/actions/_return.html.erb | 16 +++---- .../card_grants/actions/_support.html.erb | 47 +++++++------------ .../stripe_cards/actions/_freeze.html.erb | 11 +++-- 9 files changed, 79 insertions(+), 68 deletions(-) diff --git a/app/assets/stylesheets/components/_card_grants.scss b/app/assets/stylesheets/components/_card_grants.scss index 37e5e4ff7a..8fdeec88ab 100644 --- a/app/assets/stylesheets/components/_card_grants.scss +++ b/app/assets/stylesheets/components/_card_grants.scss @@ -521,14 +521,21 @@ $grant-upload-height: 11.5rem; // Controls grid. Headings and descriptions vary in length, so each cell is a // column that pushes its button to the bottom — the buttons line up across the // row instead of floating at whatever height the copy above them ends. -// `> div:not(.modal)` skips the modals some action partials render as siblings. -.grant-controls > div:not(.modal) { +.grant-control { display: flex; flex-direction: column; + padding: 0.5rem; - > .btn:not(.btn--list-item) { + > p { + margin: 0.25rem 0 0.75rem; + } + + // `button_to` wraps its button in a `display: contents` form, so the button + // is sometimes a child and sometimes a grandchild — match it either way, and + // let every cell's button fill its column so they all come out the same size. + .btn { margin-top: auto; - align-self: flex-start; + width: 100%; } } diff --git a/app/views/card_grants/_actions.html.erb b/app/views/card_grants/_actions.html.erb index da0c0eb257..9b561363d6 100644 --- a/app/views/card_grants/_actions.html.erb +++ b/app/views/card_grants/_actions.html.erb @@ -11,7 +11,7 @@ <%= render "card_grants/actions/return", card_grant:, label: "I don’t need this money", subtitle: "Send the remaining balance back to #{card_grant.event.name}" %> <%= render "card_grants/actions/need_more_money", card_grant: %> <% else %> - <%= render "card_grants/actions/cancel", card_grant:, label: "Return the balance to #{card_grant.event.name}" %> + <%= render "card_grants/actions/cancel", card_grant:, variant: "control", label: "Return the balance to #{card_grant.event.name}" %> <% end %> <%= render "card_grants/actions/support", card_grant: %> <% elsif card_grant.user == current_user %> diff --git a/app/views/card_grants/_grant_details.html.erb b/app/views/card_grants/_grant_details.html.erb index bbf8516061..04de842db8 100644 --- a/app/views/card_grants/_grant_details.html.erb +++ b/app/views/card_grants/_grant_details.html.erb @@ -1,11 +1,11 @@ <%# locals: (card_grant:, own:) %> -merchants = card_grant.allowed_merchants.any? ? card_grant.allowed_merchant_names : [] - categories = card_grant.allowed_category_names - # Long lists collapse into a full-width drawer below the strip so one big list - # can't stretch the card into a single tall column. - drawers = { "Merchants" => merchants, "Categories" => categories }.select { |_, items| items.size > 2 } -%> +<% merchants = card_grant.allowed_merchants.any? ? card_grant.allowed_merchant_names : [] %> +<% categories = card_grant.allowed_category_names %> + +<%# Long lists collapse into a full-width drawer below the strip so one big list %> +<%# can't stretch the card into a single tall column. %> +<% drawers = { "Merchants" => merchants, "Categories" => categories }.select { |_, items| items.size > 2 } %>
    <% drawers.each_key do |label| %> diff --git a/app/views/card_grants/actions/_cancel.html.erb b/app/views/card_grants/actions/_cancel.html.erb index e29f4b638a..4f941f715f 100644 --- a/app/views/card_grants/actions/_cancel.html.erb +++ b/app/views/card_grants/actions/_cancel.html.erb @@ -1,15 +1,33 @@ -<%# locals: (card_grant:, label: "") %> +<%# locals: (card_grant:, label: "", variant: "list_item") %> <% cancel_tooltip = policy(card_grant).cancel? ? "Cancel this grant and return funds to the organization" : "You don't have permission to perform this action" %> -<%= button_to cancel_event_card_grant_path(id: card_grant.hashid, event_id: @event.slug), - method: :post, - form_class: "contents", - class: "btn btn--list-item", - disabled: !policy(card_grant).cancel?, - data: { turbo: true, turbo_frame: "_top", turbo_confirm: "Are you sure? This will irreversibly transfer the remaining balance on this grant BACK to your account.", turbo_confirm_danger: true } do %> - <%= inline_icon "reply", size: 20 %> -
    +<% cancel_path = cancel_event_card_grant_path(id: card_grant.hashid, event_id: @event.slug) %> +<% cancel_data = { turbo: true, turbo_frame: "_top", turbo_confirm: "Are you sure? This will irreversibly transfer the remaining balance on this grant BACK to your account.", turbo_confirm_danger: true } %> + +<% if variant.to_s == "control" %> +
    Cancel grant

    <%= label.presence || cancel_tooltip %>

    + <%= button_to cancel_path, + method: :post, + form_class: "contents", + class: "btn", + disabled: !policy(card_grant).cancel?, + data: cancel_data do %> + Cancel grant + <% end %>
    +<% else %> + <%= button_to cancel_path, + method: :post, + form_class: "contents", + class: "btn btn--list-item", + disabled: !policy(card_grant).cancel?, + data: cancel_data do %> + <%= inline_icon "reply", size: 20 %> +
    + Cancel grant +

    <%= label.presence || cancel_tooltip %>

    +
    + <% end %> <% end %> diff --git a/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb b/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb index 5127f62d7d..863fb33f4d 100644 --- a/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb +++ b/app/views/card_grants/actions/_convert_to_reimbursement_report.html.erb @@ -5,9 +5,9 @@ <% convert_path = convert_to_reimbursement_report_event_card_grant_path(id: card_grant.hashid, event_id: @event.slug) %> <% convert_confirm = { turbo: true, turbo_frame: "_top", turbo_confirm: "Are you sure? This will cancel your card grant and convert it into a reimbursement report.", turbo_confirm_danger: true } %> <% if variant.to_s == "control" %> -
    +
    <%= label %> -

    <%= subtitle.presence || reimbursement_tooltip %>

    +

    <%= subtitle.presence || reimbursement_tooltip %>

    <%= button_to convert_path, method: :post, form_class: "contents", diff --git a/app/views/card_grants/actions/_need_more_money.html.erb b/app/views/card_grants/actions/_need_more_money.html.erb index b89419d7e0..efb4c392bd 100644 --- a/app/views/card_grants/actions/_need_more_money.html.erb +++ b/app/views/card_grants/actions/_need_more_money.html.erb @@ -3,9 +3,9 @@ <%# TODO: point this at the real card grant help article. %> <% help_article_url = "https://help.hcb.hackclub.com/card-grants" %> -
    +
    <%= label %> -

    +

    <%= subtitle.presence || "Contact the #{card_grant.event.name} team, or buy the item and get reimbursed" %>

    <%= link_to "Learn more", help_article_url, target: "_blank", rel: "noopener", class: "btn" %> diff --git a/app/views/card_grants/actions/_return.html.erb b/app/views/card_grants/actions/_return.html.erb index f0d53ec192..2c868a51fd 100644 --- a/app/views/card_grants/actions/_return.html.erb +++ b/app/views/card_grants/actions/_return.html.erb @@ -1,15 +1,13 @@ <%# locals: (card_grant:, label: "", subtitle: "") %> <% return_tooltip = subtitle.presence || "Cancel this grant and return funds to the organization" %> -
    - - <%= label.presence || "Return grant" %> - -

    <%= return_tooltip %>

    - <%= link_to "#", data: { behavior: "modal_trigger", modal: "return-grant" }, class: "btn" do %> - Return grant - <% end %> -
    +
    + <%= label.presence || "Return grant" %> +

    <%= return_tooltip %>

    + <%= link_to "#", data: { behavior: "modal_trigger", modal: "return-grant" }, class: "btn" do %> + Return grant + <% end %> +
<%= form.label :slug, "Organization URL" %> -
+
hcb.hackclub.com / - <%= form.text_field :slug, class: "!p-0 !border-none !shadow-none min-h-auto", placeholder: event.persisted? ? event.name.parameterize : nil, data: { action: "input->external-validation#validate" }, disabled: %> + <%= form.text_field :slug, class: "!p-0 !border-none !shadow-none min-h-auto", placeholder: event.persisted? ? event.name.parameterize : nil, data: { action: "input->external-validation#validate", focus_target: "element" }, disabled: %>
diff --git a/app/views/events/settings/_donation_tiers.html.erb b/app/views/events/settings/_donation_tiers.html.erb index 59d687cefd..deb1f7f8ac 100644 --- a/app/views/events/settings/_donation_tiers.html.erb +++ b/app/views/events/settings/_donation_tiers.html.erb @@ -60,7 +60,7 @@
- <%= pop_icon_to "drag-indicator", "javascript:void(0)", class: "draggable-handle cursor-move" %> + <%= inline_icon "drag-indicator", size: 28 %> <%= pop_icon_to "delete", event_donation_tiers_path(event, tier), data: { turbo_method: :delete, turbo_confirm: "Are you sure?", turbo_confirm_danger: true } %>
diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 370dbfb32c..ec575c78ed 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -10,8 +10,10 @@ <% if @event.is_public %> <% auto_discover_feed(@event) %> + <% @skip_layout_og_tags = true # replaced by the organization-specific meta below %> <% content_for :head do %> - <% img = "https://hcb-og.hackclub.com/api/embeds/#{@event.slug}" %> + <%# Slack's link expander crops previews into a square thumbnail, so give it the square image %> + <% img = "https://hcb-og.hackclub.com/api/#{request.user_agent.to_s.include?("Slackbot-LinkExpanding") ? "square-embed" : "embeds"}/#{@event.slug}" %> diff --git a/app/views/events/transactions.html.erb b/app/views/events/transactions.html.erb index 9dd3f8189f..a4de3d78b5 100644 --- a/app/views/events/transactions.html.erb +++ b/app/views/events/transactions.html.erb @@ -6,8 +6,10 @@ <% if @event&.is_public %> <% auto_discover_feed(@event) %> + <% @skip_layout_og_tags = true # replaced by the organization-specific meta below %> <% content_for :head do %> - <% img = "https://hcb-og.hackclub.com/api/embeds/#{@event.slug}" %> + <%# Slack's link expander crops previews into a square thumbnail, so give it the square image %> + <% img = "https://hcb-og.hackclub.com/api/#{request.user_agent.to_s.include?("Slackbot-LinkExpanding") ? "square-embed" : "embeds"}/#{@event.slug}" %> diff --git a/app/views/g_suite_accounts/_panel.html.erb b/app/views/g_suite_accounts/_panel.html.erb index a291a92ed1..6b26c9e483 100644 --- a/app/views/g_suite_accounts/_panel.html.erb +++ b/app/views/g_suite_accounts/_panel.html.erb @@ -76,7 +76,7 @@ <% end %>
- <% if account.g_suite_aliases.count < 30 && policy(account).create? %> + <% if account.g_suite_aliases.size < 30 && policy(account).create? %>
@@ -85,13 +85,13 @@ <% else %>
"> + aria-label="<%= account.g_suite_aliases.size >= 30 ? "You can’t create more than 30 aliases." : "You don't have permission to create aliases." %>">
<%= inline_icon "plus", size: home_action_size %>
<% end %> -

<%= 30 - account.g_suite_aliases.count %> / 30 aliases remaining

+

<%= 30 - account.g_suite_aliases.size %> / 30 aliases remaining

<%= form_with(url: g_suite_account_g_suite_aliases_path(g_suite_account_id: account.id), method: :post, data: { turbo: false }) do |form| %> diff --git a/app/views/g_suite_accounts/index.html.erb b/app/views/g_suite_accounts/index.html.erb index 08d0082b98..860a8e150f 100644 --- a/app/views/g_suite_accounts/index.html.erb +++ b/app/views/g_suite_accounts/index.html.erb @@ -48,7 +48,7 @@ <% @g_suite_accounts.each do |g_suite_account| %> - <%= "shade-red" if g_suite_account.rejected? %>"> + "> <%= g_suite_account.address %> <%= g_suite_account.backup_email %> <%= g_suite_account.status.capitalize %> diff --git a/app/views/hcb_codes/_decline_reason.html.erb b/app/views/hcb_codes/_decline_reason.html.erb index e1f75effb5..3c447e2780 100644 --- a/app/views/hcb_codes/_decline_reason.html.erb +++ b/app/views/hcb_codes/_decline_reason.html.erb @@ -7,7 +7,7 @@ <% stripe_decline_reason = pt&.stripe_decline_reason %> <% this_transaction = local_assigns[:is_email] && pt.present? ? - "A #{humanized_money_with_symbol pt.amount.abs} charge to your card ending in #{hcb_code.stripe_card.last4} at '#{pt.raw_pending_stripe_transaction.stripe_transaction["merchant_data"]["name"]}'" + "A #{humanized_money_with_symbol pt.amount.abs} charge to your card ending in #{card&.last4} at '#{pt.raw_pending_stripe_transaction.stripe_transaction["merchant_data"]["name"]}'" : "This transaction" %> @@ -23,7 +23,7 @@ <% else %> <%= ("#{this_transaction} was declined by our card issuer " + ({ - card_inactive: !hcb_code.stripe_card.initially_activated? ? "because this card hasn't been activated yet" : "because this card is frozen", + card_inactive: card&.initially_activated? ? "because this card is frozen" : "because this card hasn't been activated yet", card_canceled: "because this card is canceled", card_expired: "because this card is expired", verification_failed: "because the card verification failed", @@ -46,7 +46,7 @@ }[hcb_decline_reason] || "Insufficient funds" %> <% else %> <%= { - card_inactive: !hcb_code.stripe_card.initially_activated? ? "Card not activated" : "Card frozen", + card_inactive: card&.initially_activated? ? "Card frozen" : "Card not activated", card_canceled: "Card canceled", card_expired: "Card expired", verification_failed: "Card verification failed", @@ -85,13 +85,15 @@ <% elsif stripe_decline_reason == :webhook_timeout %> HCB experienced a network issue while processing this transaction. Please try again. <% elsif stripe_decline_reason == :card_inactive %> - <% if hcb_code.stripe_card.initially_activated? %> - You can defrost your card at any time <%= link_to "here", stripe_card_url(hcb_code.stripe_card) %>. + <% if card.nil? %> + Try again, or contact the HCB team at <%= mail_to "hcb@hackclub.com" %>. + <% elsif card.initially_activated? %> + You can defrost your card at any time <%= link_to "here", stripe_card_url(card) %>. <% else %> - Activate your new card <%= link_to "here", stripe_card_url(hcb_code.stripe_card) %> before using it. + Activate your new card <%= link_to "here", stripe_card_url(card) %> before using it. <% end %> <% elsif stripe_decline_reason == :card_canceled %> - <% if card.replacement %> + <% if card&.replacement %> Try using its <%= link_to "replacement card", stripe_card_url(card.replacement) %> ending in <%= card.replacement.last4 %>. <% else %> Try using another card. @@ -112,10 +114,10 @@ The card issuer deemed this transaction insecure. Try again using chip, if possible. <% end %> <% elsif stripe_decline_reason.in? %i[verification_failed suspected_fraud] %> - <% verification_data = pt&.raw_pending_stripe_transaction&.stripe_transaction&.[]("verification_data") %> + <% verification_data = pt&.raw_pending_stripe_transaction&.stripe_transaction&.[]("verification_data") || {} %> <% card_details = capture do %> - <% if defined?(current_user) && hcb_code.stripe_card.user == current_user %> - <%= link_to "your card details", stripe_card_path(hcb_code.stripe_card) %> + <% if defined?(current_user) && card && card.user == current_user %> + <%= link_to "your card details", stripe_card_path(card) %> <% else %> the card details <% end %> @@ -153,7 +155,7 @@
- If this transaction was intentional, double check the card details and try again. If you don't recognize this transaction, <%= link_to "freeze your card", stripe_card_url(hcb_code.stripe_card), data: { turbo_method: :post } %>. + If this transaction was intentional, double check the card details and try again.<% if card %> If you don't recognize this transaction, <%= link_to "freeze your card", stripe_card_url(card), data: { turbo_method: :post } %>.<% end %> <% elsif hcb_decline_reason != :inadequate_balance %> Try again, or contact the HCB team at <%= mail_to "hcb@hackclub.com" %>. diff --git a/app/views/hcb_codes/_meatballs.html.erb b/app/views/hcb_codes/_meatballs.html.erb index 8b6ef71666..bdf47d27f0 100644 --- a/app/views/hcb_codes/_meatballs.html.erb +++ b/app/views/hcb_codes/_meatballs.html.erb @@ -4,12 +4,13 @@ <% open_invoice = hcb_code.invoice? && hcb_code.invoice.open_v2? %> <% disputable = can_dispute?(hcb_code:) %> <% reversible_disbursement = hcb_code.incoming_disbursement? && hcb_code.incoming_disbursement.present? && policy(hcb_code.incoming_disbursement.destination_event).create_transfer? %> +<% repayable = hcb_code.ledger_item.present? && hcb_code.event.present? && policy(Disbursement.new(destination_event: hcb_code.event)).repay? && Repayment.unrepaid_ledger_items(hcb_code.event).exists?(id: hcb_code.ledger_item.id) %> <% merchant_not_allowed = hcb_code.stripe_card? && hcb_code.pt.hcb_decline_reason == :merchant_not_allowed && hcb_code.stripe_card&.card_grant.present? %> <% allowable_on_card = merchant_not_allowed && CardGrantPolicy.new(current_user, hcb_code.stripe_card.card_grant).permit_merchant? && !hcb_code.stripe_card.card_grant.merchant_lock.include?(hcb_code.stripe_merchant["network_id"]) %> <% allowable_on_event = merchant_not_allowed && EventPolicy.new(current_user, hcb_code.stripe_card.card_grant.event).permit_merchant? && !hcb_code.stripe_card.card_grant.event.card_grant_setting.merchant_lock.include?(hcb_code.stripe_merchant["network_id"]) %> -<% if (disputable || flaggable || pinnable || open_invoice || reversible_disbursement || allowable_on_card || allowable_on_event) && organizer_signed_in?(as: :member) %> +<% if (disputable || flaggable || pinnable || open_invoice || reversible_disbursement || repayable || allowable_on_card || allowable_on_event) && organizer_signed_in?(as: :member) %>
<%= pop_icon_to "more", "#", @@ -33,6 +34,12 @@ Reverse HCB transfer <% end %> <% end %> + <% if repayable %> + <%= link_to new_disbursement_path(event_id: hcb_code.event.slug, repaid_ledger_item_id: hcb_code.ledger_item.hashid), class: "flex items-center" do %> + <%= inline_icon "payment", size: 24 %> + Repay this + <% end %> + <% end %> <% if flaggable %> <%= link_to ledger_item_invoice_as_personal_transaction_path(hcb_code.ledger_item), class: "flex items-center", data: { turbo_method: :post, turbo_confirm: "Are you sure you'd like to invoice #{hcb_code.stripe_cardholder&.user&.name || "yourself"} for this transaction? They'll be emailed an invoice." } do %> <%= inline_icon "sad", size: 24 %> diff --git a/app/views/hcb_codes/reimbursement/_expense_payout.html.erb b/app/views/hcb_codes/reimbursement/_expense_payout.html.erb index 154e426782..872b751f42 100644 --- a/app/views/hcb_codes/reimbursement/_expense_payout.html.erb +++ b/app/views/hcb_codes/reimbursement/_expense_payout.html.erb @@ -30,6 +30,7 @@ <%= expense.description %>

<% end %> + <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/hcb_codes/show.html.erb b/app/views/hcb_codes/show.html.erb index 4784f98228..9dcdeafc73 100644 --- a/app/views/hcb_codes/show.html.erb +++ b/app/views/hcb_codes/show.html.erb @@ -49,7 +49,7 @@ <% if @hcb_code.shared_commentable? %> <% viewing_disbursement = @hcb_code.outgoing_disbursement || @hcb_code.incoming_disbursement %> <% counterparty_hcb_code = viewing_disbursement.counterparty.local_hcb_code %> - <% if policy(counterparty_hcb_code).show? && counterparty_hcb_code.comments.count > 0 %> + <% if policy(counterparty_hcb_code).show? && counterparty_hcb_code.comments.any? %>

<%= link_to counterparty_hcb_code do %> View <%= pluralize(counterparty_hcb_code.comments.count, "other comment") %> from <%= viewing_disbursement.counterparty_label %> diff --git a/app/views/hcb_codes/transaction_types/_ach_transfer.html.erb b/app/views/hcb_codes/transaction_types/_ach_transfer.html.erb index 03b90a7f78..0fa9737534 100644 --- a/app/views/hcb_codes/transaction_types/_ach_transfer.html.erb +++ b/app/views/hcb_codes/transaction_types/_ach_transfer.html.erb @@ -111,6 +111,7 @@

+ <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/hcb_codes/transaction_types/_bank_fee.html.erb b/app/views/hcb_codes/transaction_types/_bank_fee.html.erb index 1858bc4d15..b06b9beef0 100644 --- a/app/views/hcb_codes/transaction_types/_bank_fee.html.erb +++ b/app/views/hcb_codes/transaction_types/_bank_fee.html.erb @@ -15,6 +15,7 @@
+ <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %>
diff --git a/app/views/hcb_codes/transaction_types/_check.html.erb b/app/views/hcb_codes/transaction_types/_check.html.erb index 0e6b17b9cf..1c69b4c26d 100644 --- a/app/views/hcb_codes/transaction_types/_check.html.erb +++ b/app/views/hcb_codes/transaction_types/_check.html.erb @@ -71,6 +71,7 @@ <%= render_money @hcb_code.check.amount %>

+ <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/hcb_codes/transaction_types/_disbursement.html.erb b/app/views/hcb_codes/transaction_types/_disbursement.html.erb index 24e35062c0..c05f1f3542 100644 --- a/app/views/hcb_codes/transaction_types/_disbursement.html.erb +++ b/app/views/hcb_codes/transaction_types/_disbursement.html.erb @@ -124,6 +124,7 @@

<% end %> + <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/hcb_codes/transaction_types/_donation.html.erb b/app/views/hcb_codes/transaction_types/_donation.html.erb index 25a894193a..39cb99de54 100644 --- a/app/views/hcb_codes/transaction_types/_donation.html.erb +++ b/app/views/hcb_codes/transaction_types/_donation.html.erb @@ -43,9 +43,12 @@ <%= form.text_field :name, value: @donation.name(show_anonymous: true), placeholder: "John Smith", required: true, autofocus: true, autocomplete: "off" %>
- <%= form.check_box :anonymous, style: "margin-left: 0;" %> + <%= form.check_box :anonymous, checked: @donation.anonymous? || @donation.recurring_donation&.anonymous?, style: "margin-left: 0;" %> <%= form.label :anonymous, "Make this donation anonymous.", style: "display: inline; flex-grow: 1;" %>
+ <% if @donation.recurring? %> +

This donation is part of a recurring donation. Updating the donor's name or anonymity will update it for all past and future donations.

+ <% end %> <%= form.submit "Edit donor" %> <% end %> @@ -160,6 +163,7 @@

<% end %> + <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/hcb_codes/transaction_types/_fee_revenue.html.erb b/app/views/hcb_codes/transaction_types/_fee_revenue.html.erb index fa9ed79afe..7ba35c7213 100644 --- a/app/views/hcb_codes/transaction_types/_fee_revenue.html.erb +++ b/app/views/hcb_codes/transaction_types/_fee_revenue.html.erb @@ -14,6 +14,7 @@
+ <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %>
diff --git a/app/views/hcb_codes/transaction_types/_increase_check.html.erb b/app/views/hcb_codes/transaction_types/_increase_check.html.erb index 0323acc708..662043166f 100644 --- a/app/views/hcb_codes/transaction_types/_increase_check.html.erb +++ b/app/views/hcb_codes/transaction_types/_increase_check.html.erb @@ -111,6 +111,7 @@ <% end %> <% end %> + <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %>
diff --git a/app/views/hcb_codes/transaction_types/_invoice.html.erb b/app/views/hcb_codes/transaction_types/_invoice.html.erb index 09c26fed0e..f04930c6e0 100644 --- a/app/views/hcb_codes/transaction_types/_invoice.html.erb +++ b/app/views/hcb_codes/transaction_types/_invoice.html.erb @@ -141,6 +141,7 @@ Memo <%= @invoice.memo %>

+ <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> <% width_css_class = @invoice.deposited? ? @invoice.canonical_transactions.sum { |ct| ct.fee.amount_cents_as_decimal } != 0 : @invoice.event.revenue_fee != 0 ? "w-64" : "w-8" %> diff --git a/app/views/hcb_codes/transaction_types/_paypal_transfer.html.erb b/app/views/hcb_codes/transaction_types/_paypal_transfer.html.erb index 46c6ae15fd..940af917ff 100644 --- a/app/views/hcb_codes/transaction_types/_paypal_transfer.html.erb +++ b/app/views/hcb_codes/transaction_types/_paypal_transfer.html.erb @@ -45,6 +45,7 @@ <%= render_money @hcb_code.paypal_transfer.amount %>

+ <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/hcb_codes/transaction_types/_stripe_card.html.erb b/app/views/hcb_codes/transaction_types/_stripe_card.html.erb index 35927cffa9..e8204408b9 100644 --- a/app/views/hcb_codes/transaction_types/_stripe_card.html.erb +++ b/app/views/hcb_codes/transaction_types/_stripe_card.html.erb @@ -74,6 +74,12 @@ <%= user_mention @hcb_code.stripe_card.user %> <% end %>
+ <% if @hcb_code.stripe_foreign_currency? %> +
+ Charged in <%= @hcb_code.stripe_merchant_amount.currency.iso_code %> + <%= @hcb_code.stripe_merchant_amount.format(with_currency: true) %> +
+ <% end %> <% if @hcb_code.stripe_atm_fee %>
ATM fee @@ -271,6 +277,7 @@

<% end %> <% end %> + <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/hcb_codes/transaction_types/_unknown.html.erb b/app/views/hcb_codes/transaction_types/_unknown.html.erb index d7c487056d..89bb8f2580 100644 --- a/app/views/hcb_codes/transaction_types/_unknown.html.erb +++ b/app/views/hcb_codes/transaction_types/_unknown.html.erb @@ -26,6 +26,7 @@ <% end %> <% end %> <% end %> + <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/hcb_codes/transaction_types/_wire.html.erb b/app/views/hcb_codes/transaction_types/_wire.html.erb index 5303ce89f5..4780f76e19 100644 --- a/app/views/hcb_codes/transaction_types/_wire.html.erb +++ b/app/views/hcb_codes/transaction_types/_wire.html.erb @@ -89,6 +89,7 @@ <%= render_money @hcb_code.amount_cents.abs %>

+ <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/hcb_codes/transaction_types/_wise_transfer.html.erb b/app/views/hcb_codes/transaction_types/_wise_transfer.html.erb index 8f8522b80b..1ca0e7f232 100644 --- a/app/views/hcb_codes/transaction_types/_wise_transfer.html.erb +++ b/app/views/hcb_codes/transaction_types/_wise_transfer.html.erb @@ -88,6 +88,7 @@ <%= render_money @hcb_code.amount_cents.abs %>

+ <%= render "ledger/items/repayments", ledger_item: @hcb_code.ledger_item if @hcb_code.ledger_item %> <%= render "hcb_codes/tags", hcb_code: @hcb_code, event: @event || @hcb_code.event %> diff --git a/app/views/integrations/_discord_integration.html.erb b/app/views/integrations/_discord_integration.html.erb index 010ea088c0..a0023a5ac7 100644 --- a/app/views/integrations/_discord_integration.html.erb +++ b/app/views/integrations/_discord_integration.html.erb @@ -3,7 +3,7 @@
- Discord logo + Discord logo

Discord

<% if active %> diff --git a/app/views/invoices/_form.html.erb b/app/views/invoices/_form.html.erb index d2f210d21f..1813b1a9e2 100644 --- a/app/views/invoices/_form.html.erb +++ b/app/views/invoices/_form.html.erb @@ -1,9 +1,10 @@ +<% invalid_submission = sponsor.errors.any? || invoice.errors.any? %> <% if @event.demo_mode? %>
Once your organization is activated, you'll be able to create and send invoices
<% else %> - <%= form_with(model: invoice, url: event_invoices_path(@event), local: true, data: { controller: "invoice-form" }, html: { "x-data" => "{ amount: null }" }) do |form| %> + <%= form_with(model: invoice, url: event_invoices_path(@event), local: true, data: { controller: "invoice-form", invoice_form_invalid_value: invalid_submission }, html: { "x-data" => "{ amount: null }" }) do |form| %>
> - - - <% @event.sponsors.select(:id, :name, :contact_email, :address_line1, :address_line2, :address_city, :address_state, :address_postal_code, :address_country).each do |sponsor| %> - + + + <% @event.sponsors.select(:id, :name, :contact_email, :address_line1, :address_line2, :address_city, :address_state, :address_postal_code, :address_country).each do |option_sponsor| %> + <% end %>

+<% admin_only_user_comments = @report.user.comments.admin_only %> +<% if admin_only_user_comments.any? %> + <% admin_tool("mt2 p-3") do %> +

+ 📌 <%= pluralize(admin_only_user_comments.size, "admin note") %> about <%= @report.user.first_name %> +

+ <%= render "comments/list", comments: admin_only_user_comments %> + <% end %> +<% end %> + diff --git a/app/views/static_pages/_suggested_pairings.html.erb b/app/views/static_pages/_suggested_pairings.html.erb index 6ddab75881..57561c06f2 100644 --- a/app/views/static_pages/_suggested_pairings.html.erb +++ b/app/views/static_pages/_suggested_pairings.html.erb @@ -1,8 +1,8 @@ <%= turbo_frame_tag "suggested_pairings" do %> <% if local_assigns[:pairings].any? %> -