diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index f92cdf31dc..ef354de6fb 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -319,6 +319,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..d1f38ec2ca --- /dev/null +++ b/app/assets/stylesheets/components/_card_grants.scss @@ -0,0 +1,982 @@ +// 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); + +// Bordered tile shared by the help articles and the controls. An inset shadow +// rather than a border, so the outline doesn't add to the tile's size. +@mixin grant-tile { + position: relative; + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + padding: 0.875rem; + box-shadow: inset 0 0 0 1px rgba(140, 150, 165, 0.3); + + html[data-dark='true'] & { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12); + } +} + +@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; + + // The shared wash only fades to near-white at the very bottom, but the + // grant's numbers sit higher up, so wash the whole hero evenly instead. + &.card.card--background-image { + background-image: + linear-gradient(rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.96)), + var(--bg-image) !important; + + html[data-dark='true'] & { + background-image: + linear-gradient(rgba(37, 36, 41, 0.85), rgba(37, 36, 41, 0.94)), + var(--bg-image) !important; + } + } + + @media (max-width: 40em) { + padding: 1.25rem; + } +} + +// Status on the left, the organizer's actions on the right. +.grant-hero__top { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.75rem 1rem; + margin-bottom: 1.25rem; +} + +// Secondary text over the background image: `.muted` is too faint to read there. +.grant-hero__soft { + color: mix($slate, $muted, 60%); + + html[data-dark='true'] & { + color: $smoke-dark; + } +} + +.grant-hero__grid { + display: grid; + gap: 1.5rem; + grid-template-columns: minmax(0, 1fr); + + @media (min-width: 56em) { + grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); + gap: 2.5rem; + } +} + +.grant-hero__aside { + @include grant-hairline(top); + padding-top: 1.25rem; + + @media (min-width: 56em) { + border-top: 0; + padding-top: 0; + padding-left: 2.5rem; + @include grant-hairline(left); + } +} + +.grant-hero__amount { + display: block; + margin: 0; + font-size: 3.25rem; + font-weight: 700; + line-height: 1; + letter-spacing: -0.035em; + font-variant-numeric: tabular-nums; + + @media (max-width: 40em) { + font-size: 2.5rem; + } +} + +.grant-hero__date { + margin: 0; + font-size: 1.625rem; + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.02em; +} + +// How much of the grant is still yours to spend. +.grant-meter { + height: 8px; + border-radius: 999px; + background-color: rgba(map-get($palette, muted), 0.25); + overflow: hidden; + + html[data-dark='true'] & { + background-color: rgba(255, 255, 255, 0.2); + } +} + +.grant-meter__fill { + display: block; + height: 100%; + border-radius: inherit; + background-color: map-get($palette, success); + transition: width 0.25s ease-in-out; + + &.grant-meter__fill--muted { + background-color: map-get($palette, muted); + } +} + +// Compact label / value pairs stacked in the hero aside. Rows grow when a long +// value wraps rather than colliding with the row beneath them. +.grant-meta { + display: grid; + gap: 0.375rem; +} + +.grant-meta > div { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + min-height: 1.625rem; +} + +.grant-meta__label { + flex: 0 0 auto; + white-space: nowrap; +} + +.grant-meta__value { + min-width: 0; + text-align: right; + overflow-wrap: anywhere; + + // `.mention` carries a trailing margin meant for inline prose; drop it so + // avatars line up flush with the values above and below. + &.mention, + .mention { + margin-right: 0; + } +} + +// State notices (canceled, frozen, instructions, fraud check…) stack under the +// hero with one gap between them, whatever margins the callout partial brings. +.grant-notices { + display: grid; + gap: 0.75rem; + margin-top: 1rem; + + > .card { + margin: 0; + } +} + +.grant-section { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.625rem; + margin: 2.5rem 0 0.875rem; + font-size: 1.375rem; +} + +.grant-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 2rem; + height: 2rem; + font-size: 17px; + font-weight: 900; + 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); + } + + svg { + width: 20px; + height: 20px; + } +} + +// Card artwork beside its numbers, then the controls. Laid out against the +// section's own width (not the viewport) since the sidebar and admin tools +// change how much room it gets at any given screen size. +.grant-pay { + container: grant-pay / inline-size; + display: grid; + gap: 0.75rem; +} + +.grant-panel { + display: grid; + grid-template-columns: minmax(0, 1fr); + margin: 0; + padding: 0; + + // Any narrower than 21rem and the card artwork wraps its own number. + @container grant-pay (min-width: 46rem) { + grid-template-columns: 21rem minmax(0, 1fr); + } +} + +.grant-panel__art { + display: flex; + flex-direction: column; + justify-content: center; + padding: 1.5rem; + + .btn { + max-width: 20rem; + margin-inline: auto; + } +} + +.grant-panel__fields { + display: grid; + align-content: center; + gap: 1.5rem; + padding: 1.25rem 1.5rem; + @include grant-hairline(top); + + @container grant-pay (min-width: 46rem) { + border-top: 0; + @include grant-hairline(left); + } + + // Wide enough for card and billing details side by side. + @container grant-pay (min-width: 72rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + column-gap: 2rem; + } +} + +// Heading inside a card, one step below the page's `.grant-section`. +.grant-subheading { + margin: 0 0 0.5rem; + font-size: 1.125rem; +} + +.grant-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.125rem 1.5rem; + min-height: 2.5rem; + padding: 0.375rem 0; + + + .grant-row { + @include grant-hairline(top); + } +} + +.grant-row__label { + display: inline-flex; + align-items: center; + gap: 4px; + flex: 0 0 auto; + color: map-get($palette, muted); +} + +// On a narrow screen a long value drops below its label but stays right-aligned. +.grant-row__value { + margin-left: auto; + min-width: 0; + font-weight: 600; + text-align: right; + font-variant-numeric: tabular-nums; + overflow-wrap: anywhere; +} + +.grant-controls { + margin: 0; + padding: 1.25rem 1.5rem 1.5rem; +} + +// Tiles like the help articles below. Stacked full-width rows on phones, with +// the icon beside the copy; once there's room, one row of icon-on-top tiles. +.grant-controls__grid { + display: grid; + gap: 0.625rem; + margin-top: 0.875rem; + + .grant-action { + @include grant-tile; + flex-direction: row; + align-items: center; + height: 100%; + } + + @container grant-pay (min-width: 40rem) { + grid-auto-flow: column; + grid-auto-columns: minmax(0, 1fr); + + .grant-action { + flex-direction: column; + align-items: flex-start; + } + + .grant-action__arrow { + position: absolute; + top: 0.5rem; + right: 0.5rem; + transform: translate(-3px, 3px); + } + } +} + +// Help card: the help center articles as tiles, then the org's own support +// as a slim tinted strip along the bottom. +.grant-help { + display: grid; + margin: 0; + padding: 0; + overflow: hidden; + + > .grant-action { + padding: 0.875rem 1.5rem; + border-radius: 0; + } + + // Under the tiles it's a footer: tinted, and "Still stuck?" runs into its + // description on one line instead of stacking. + > .grant-answers + .grant-action { + background: rgba(140, 150, 165, 0.08); + @include grant-hairline(top); + + &:hover:not(:disabled), + &:focus-visible { + background: rgba(140, 150, 165, 0.14); + } + + .grant-action__text { + flex-direction: row; + flex-wrap: wrap; + align-items: baseline; + column-gap: 0.375rem; + } + } +} + +.grant-answers { + padding: 1.25rem 1.5rem 1.5rem; +} + +// As many columns as fit (all five on a full-width page), wrapping on phones. +.grant-answers__row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); + gap: 0.625rem; + margin-top: 0.875rem; +} + +// A help tile's or control's colour shows in its icon, and washes over the +// whole thing on hover. +.grant-answer, +.grant-action { + --tone: #{map-get($palette, muted)}; + --tone-wash: #{rgba(map-get($palette, muted), 0.08)}; + --tone-soft: #{rgba(map-get($palette, muted), 0.14)}; + + @each $tone in (primary, warning, success, info, purple, accent) { + &.grant-tone--#{'' + $tone} { + --tone: #{map-get($palette, $tone)}; + --tone-wash: #{rgba(map-get($palette, $tone), 0.08)}; + --tone-soft: #{rgba(map-get($palette, $tone), 0.14)}; + } + } +} + +.grant-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 2.25rem; + height: 2.25rem; + border-radius: 0.625rem; + color: var(--tone); + background: var(--tone-soft); +} + +// An arrow slides in on hover to say the tile opens the help center. +.grant-answer { + @include grant-tile; + display: flex; + border-radius: var(--radius-lg); + background: transparent; + color: inherit; + text-decoration: none; + transition: background-color 0.15s ease; + + &:hover, + &:focus { + color: inherit; + } + + &:hover, + &:focus-visible { + background: var(--tone-wash); + + html[data-dark='true'] & { + background: var(--tone-wash); + } + + .grant-answer__arrow { + opacity: 1; + transform: none; + } + } + + &:focus-visible { + outline: 2px solid var(--tone); + outline-offset: 2px; + } + + @media (prefers-reduced-motion: reduce) { + transition: none; + } +} + +.grant-answer__label { + font-size: 0.875rem; + font-weight: 600; + line-height: 1.25; +} + +// The arrow glyph only fills the middle third of its icon, so the icon is +// drawn large and tucked into the corner. +.grant-answer__arrow { + position: absolute; + top: 0.25rem; + right: 0.25rem; + display: inline-flex; + color: var(--tone); + opacity: 0; + transform: translate(-3px, 3px); + transition: + opacity 0.15s ease, + transform 0.15s ease; +} + +.grant-section-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.5rem 1rem; + margin: 2.5rem 0 0.875rem; + + > .grant-section { + margin: 0; + } +} + +// Horizontal strip of facts, hairline-separated. +.grant-strip { + display: flex; + flex-wrap: wrap; + + > div { + flex: 1 1 9rem; + padding: 0.25rem 1.25rem; + @include grant-hairline(left); + + &:first-child { + padding-left: 0; + border-left: 0; + } + + @media (max-width: 40em) { + flex-basis: 45%; + padding: 0.5rem 0.75rem; + + &:nth-child(odd) { + padding-left: 0; + border-left: 0; + } + } + } +} + +.grant-strip__label { + display: block; + margin: 0 0 0.125rem; + font-size: 0.875rem; + color: map-get($palette, muted); +} + +.grant-strip__value { + margin: 0; + font-weight: 600; + line-height: 1.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; + +.grant-upload { + padding: 0; +} + +.grant-upload__grid { + display: flex; + flex-direction: column; + + @media (min-width: 56em) { + flex-direction: row; + align-items: stretch; + } +} + +.grant-upload__drop { + flex: 0 0 auto; + padding: 1.25rem; + + @media (min-width: 56em) { + flex-basis: 19rem; + } +} + +.grant-upload__dropzone { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.625rem; + height: 100%; + min-height: $grant-upload-height; + padding: 1.25rem; + text-align: center; + border: 2px dashed $grant-hairline; + border-radius: var(--radius-lg); + background-color: rgba(map-get($palette, smoke), 0.35); + transition: 0.125s ease-in-out; + transition-property: border-color, background-color; + + html[data-dark='true'] & { + border-color: rgba(255, 255, 255, 0.15); + background-color: rgba(255, 255, 255, 0.03); + } + + &:hover { + border-color: rgba(map-get($palette, info), 0.5); + } +} + +.grant-upload__dropzone-icon { + color: map-get($palette, muted); +} + +.grant-upload__list { + display: flex; + flex: 1 1 auto; + align-items: center; + gap: 0.75rem; + min-width: 0; + min-height: $grant-upload-height; + padding: 1.25rem; + overflow-x: auto; +} + +// Only hairline the two apart when the dropzone is actually there — once the +// pre-authorization is submitted the list stands alone. +.grant-upload__drop + .grant-upload__list { + @include grant-hairline(top); + + @media (min-width: 56em) { + border-top: 0; + @include grant-hairline(left); + } +} + +// Nothing uploaded yet: let the dropzone have the whole panel rather than +// pairing it with an empty gallery. Browsers without `:has()` keep the split. +.grant-upload__grid:has(.grant-upload__empty) { + .grant-upload__drop { + flex: 1 1 auto; + } + + .grant-upload__drop + .grant-upload__list { + display: none; + } +} + +.grant-upload__thumb { + display: block; + flex: 0 0 auto; + height: $grant-upload-height; + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: var(--shadow-border-sm); + transition: transform 0.125s ease-in-out; + + &:hover { + transform: translateY(-2px); + } + + img { + display: block; + height: 100%; + width: auto; + max-width: none; + } +} + +.grant-upload__empty { + flex: 1 1 auto; + align-self: center; + text-align: center; +} + +.grant-upload__footer { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.5rem 1rem; + padding: 0.75rem 1.25rem; + font-size: 0.875rem; + @include grant-hairline(top); + + a { + display: inline-flex; + align-items: center; + gap: 0.25rem; + } +} + +// Step 3: the explainer and its call to action sit on one line where there's room. +.grant-submit { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + + p { + flex: 1 1 22rem; + } + + .btn { + flex: 0 0 auto; + } +} + +// One control. The whole row is the button (or link): an icon in the +// control's colour, its copy, and an arrow that slides in on hover. +.grant-action { + display: flex; + align-items: center; + gap: 0.875rem; + width: 100%; + margin: 0; + padding: 0.75rem; + border: 0; + border-radius: var(--radius-lg); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + text-decoration: none; + cursor: pointer; + transition: background-color 0.125s ease-in-out; + + // Some rows are links: beat the global `a:hover, a:focus` slate, which is + // near-invisible in dark mode and sticks after a modal trigger is clicked. + &:hover, + &:focus { + color: inherit; + } + + &:hover:not(:disabled), + &:focus-visible { + background-color: var(--tone-wash); + + .grant-action__arrow { + opacity: 1; + transform: none; + } + } + + &:focus-visible { + outline: 2px solid var(--tone); + outline-offset: -2px; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } +} + +.grant-action__text { + display: flex; + flex: 1 1 auto; + flex-direction: column; + gap: 0.125rem; + min-width: 0; +} + +.grant-action__title { + font-weight: 600; + line-height: 1.3; +} + +.grant-action__desc { + font-size: 0.875rem; + line-height: 1.35; + color: map-get($palette, muted); +} + +.grant-action__arrow { + flex: 0 0 auto; + color: var(--tone); + opacity: 0; + transform: translateX(-3px); + transition: + opacity 0.15s ease, + transform 0.15s ease; + + @media (prefers-reduced-motion: reduce) { + transition: none; + } +} + +.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; +} + +// Activation pledges. Each statement is a full-width clickable row rather than +// a bare checkbox, and lights up once it's been agreed to. +.grant-pledge { + padding: 0; + overflow: hidden; +} + +.grant-pledge__row { + display: flex; + align-items: flex-start; + gap: 0.875rem; + margin: 0; + padding: 1rem 1.25rem; + font-weight: 400; + line-height: 1.45; + cursor: pointer; + transition: background-color 0.125s ease-in-out; + + + .grant-pledge__row { + @include grant-hairline(top); + } + + &:hover { + background-color: rgba(140, 150, 165, 0.06); + } + + &:has(input:checked) { + background-color: rgba(map-get($palette, success), 0.06); + } + + input[type='checkbox'] { + flex: 0 0 auto; + width: 1.125rem; + height: 1.125rem; + margin: 0.125rem 0 0; + accent-color: map-get($palette, success); + cursor: pointer; + } +} + +.grant-pledge__footer { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.75rem 1rem; + padding: 1rem 1.25rem; + font-size: 0.875rem; + background: rgba(140, 150, 165, 0.06); + @include grant-hairline(top); + + .btn { + flex: 0 0 auto; + } +} diff --git a/app/controllers/card_grants_controller.rb b/app/controllers/card_grants_controller.rb index 6fac2a34de..9719faa5c5 100644 --- a/app/controllers/card_grants_controller.rb +++ b/app/controllers/card_grants_controller.rb @@ -154,10 +154,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_grant/pre_authorizations/_screenshot_count.html.erb b/app/views/card_grant/pre_authorizations/_screenshot_count.html.erb index 31a05ac5d0..18b8fdc634 100644 --- a/app/views/card_grant/pre_authorizations/_screenshot_count.html.erb +++ b/app/views/card_grant/pre_authorizations/_screenshot_count.html.erb @@ -1,10 +1,14 @@ +<%# locals: (count:, card_grant:, disabled:) %> + <% url = clear_screenshots_card_grant_pre_authorizations_path(card_grant) %> -
- - <%= pluralize(count, "screenshot") %> uploaded - <% if count >= 1 && !disabled %> - • <%= link_to "Remove all", url, class: "remove-screenshots", data: { turbo_method: :post } %> + diff --git a/app/views/card_grant/pre_authorizations/_screenshot_form.html.erb b/app/views/card_grant/pre_authorizations/_screenshot_form.html.erb index 6307de74cf..d7280f45d3 100644 --- a/app/views/card_grant/pre_authorizations/_screenshot_form.html.erb +++ b/app/views/card_grant/pre_authorizations/_screenshot_form.html.erb @@ -1,78 +1,63 @@ -
-
h-100" style="position: relative;"> - <%= form_with model: false, url: card_grant_pre_authorizations_path, method: :patch, remote: true, class: "flex flex-col justify-center h-100", data: { - turbo: defined?(turbo) ? turbo : "false", - controller: "file-drop form", - action: defined?(disabled) && disabled ? "" : " - dragover@window->file-drop#dragover - drop@window->file-drop#drop - dragenter@window->file-drop#dragenter - dragleave@window->file-drop#dragleave - ", +<%# Cart screenshot dropzone. Rendered inline on the pre-authorization page and + swapped wholesale by the controller's turbo stream after an upload, so the + layout classes live on the outermost element. %> - "file-drop-title-value" => "Drop to add a screenshot.", - "file-drop-global-paste-value" => "true" - }.compact, html: { - onsubmit: " - const button = $('#upload-screenshot-button', this); - $('span', button).text('Uploading...'); - button.attr('disabled', 'disabled'); - " - } do |form| %> +
+ <%= form_with model: false, url: card_grant_pre_authorizations_path, method: :patch, remote: true, class: "h-100", data: { + turbo: defined?(turbo) ? turbo : "false", + controller: "file-drop form", + action: local_assigns[:disabled] ? "" : " + dragover@window->file-drop#dragover + drop@window->file-drop#drop + dragenter@window->file-drop#dragenter + dragleave@window->file-drop#dragleave + ", -
justify-center px-8" id="upload_screenshot_form"> - <% if defined?(show_banner) %> - <% if defined?(uploaded_files) && uploaded_files.any? %> -

- <%= inline_icon "checkmark", size: 24, class: "mr2" %> - <%= pluralize uploaded_files.count, "screenshot" %> - added -

- <% end %> - <% end %> -
+ "file-drop-title-value" => "Drop to add a screenshot.", + "file-drop-global-paste-value" => "true" + }.compact, html: { + onsubmit: " + const button = $('#upload-screenshot-button', this); + $('span', button).text('Uploading...'); + button.attr('disabled', 'disabled'); + " + } do |form| %> + +
+ <%= form.file_field :screenshot, + multiple: true, + include_hidden: false, + required: true, + class: "display-none", + accept: "image/*,image/heic,.pdf", + direct_upload: true, + data: { + "file-drop-target" => "fileInput", + "action" => "change->form#submit" + } %> - <% if local_assigns[:redirect_url] %> - <%= form.hidden_field :redirect_url, value: local_assigns[:redirect_url] %> - <% end %> - <%= (form.hidden_field :pre_authorization_id, value: pre_authorization.id) if defined?(pre_authorization) %> - <%= (form.hidden_field :show_link, value: true) if defined?(inline_linking) %> + <%= inline_icon "photo", size: 28, class: "grant-upload__dropzone-icon" %> - <%= form.file_field :screenshot, - multiple: true, - include_hidden: false, - required: true, - class: "display-none", - accept: "image/*,image/heic,.pdf", - direct_upload: true, - data: { - "file-drop-target" => "fileInput", - "action" => "change->form#submit" - } %> - - <%= form.label :screenshot, class: "btn m1 #{"bg-success" if local_assigns[:success]} #{"bg-error" if local_assigns[:error]}", id: "upload-screenshot-button", disabled: false do %> - <%= inline_icon "cloud-upload" %> - <% if local_assigns[:success] || local_assigns[:error] %> - <%= local_assigns[:success] || local_assigns[:error] %> - - <% else %> - Upload screenshot - <% end %> - <% end %> - + <%= form.label :screenshot, class: "btn #{"bg-success" if local_assigns[:success]} #{"bg-error" if local_assigns[:error]}", id: "upload-screenshot-button" do %> + <%= inline_icon "cloud-upload" %> + <% if local_assigns[:success] || local_assigns[:error] %> + <%= local_assigns[:success] || local_assigns[:error] %> + + <% else %> + Upload screenshot + <% end %> + <% end %> -

Or drag & drop your screenshot here

-
-
+

or drag, drop, or paste it here

+
- <% end %> -
+ <% end %>
diff --git a/app/views/card_grant/pre_authorizations/_screenshot_list.html.erb b/app/views/card_grant/pre_authorizations/_screenshot_list.html.erb index 65bd38a4bd..52aa661be4 100644 --- a/app/views/card_grant/pre_authorizations/_screenshot_list.html.erb +++ b/app/views/card_grant/pre_authorizations/_screenshot_list.html.erb @@ -1,34 +1,40 @@ -
- <% screenshots.each do |screenshot| %> - <% file_path = rails_blob_path(screenshot, only_path: true, disposition: "inline") %> - <% if screenshot.image? %> - <% modal_id = "preview_screenshot_#{screenshot.id}" %> - <%= image_tag screenshot, - class: "h-100 !w-min-content card min-w-0 !flex-shrink-0 pointer", - alt: screenshot.filename, - title: "Click to enlarge", - data: { behavior: "modal_trigger", modal: modal_id } %> +<%# locals: (screenshots:) %> - - <% else %> - <%= link_to file_path, target: "_blank", rel: "noopener", class: "card flex flex-col items-center justify-center h-100 px-4 !flex-shrink-0 min-w-0 pointer", title: "Open #{screenshot.filename}" do %> - <%= inline_icon "docs", size: 32, class: "muted" %> - <%= screenshot.filename %> + + + <% else %> + <%= link_to rails_blob_path(screenshot, only_path: true, disposition: "inline"), + target: "_blank", + rel: "noopener", + class: "grant-upload__thumb flex flex-col items-center justify-center px-4", + title: "Open #{screenshot.filename}" do %> + <%= inline_icon "docs", size: 32, class: "muted" %> + <%= screenshot.filename %> + <% end %> <% end %> <% end %> - <% end %> - - <% if screenshots.empty? %> - <%= blankslate "No screenshots uploaded", class: "w-full flex items-center justify-center" %> + <% else %> +

No screenshots uploaded yet

<% end %>
diff --git a/app/views/card_grant/pre_authorizations/show.html.erb b/app/views/card_grant/pre_authorizations/show.html.erb index 613117969d..485ce56eaf 100644 --- a/app/views/card_grant/pre_authorizations/show.html.erb +++ b/app/views/card_grant/pre_authorizations/show.html.erb @@ -1,53 +1,39 @@ <% title "Grant to #{@card_grant.user.name}" %> <%= render "events/nav", selected: :transfers if organizer_signed_in? %> +<%= render "users/nav", selected: :home unless organizer_signed_in? %> <%= turbo_stream_from @pre_authorization %> -<% shimmer = @pre_authorization.submitted? %> - -
-
" style="<%= "--bg-image: url(#{url_for(@event.background_image).dump})" if @event.background_image.attached? %>"> -

- <%= @event.name %> - has granted you - <%= render_money @card_grant.amount %> - <% if @card_grant.purpose.present? %> - for <%= @card_grant.purpose %> - <% end %> -

-
-
-
-
-

Pre-authorization

- <%= inline_icon "view-forward", class: "m-0 lg:self-center", size: 20 %> -
-

Before you can get spending, we need to know what you're planning to buy.

-
-
-
- <%= render partial: "card_grant/pre_authorizations/status_badge", locals: { pre_authorization: @pre_authorization, organizer: false } %> +<% draft = @pre_authorization.draft? %> + +<%# The hero doubles as the page header: same grant summary the grantee sees on + the grant itself, with the pre-authorization step called out underneath. %> +<% hero_footer = capture do %> +
+
+

Pre-authorization

+

Before you can start spending, tell us what you're planning to buy.

+ <%= render partial: "card_grant/pre_authorizations/status_badge", locals: { pre_authorization: @pre_authorization, organizer: false } %>
-
+<% end %> + +<%= render partial: "card_grants/hero", locals: { card_grant: @card_grant, footer: hero_footer, show_actions: false } %> <% if @pre_authorization.authorized? %> -
-

- <%= inline_icon "checkmark", size: 24, class: "mr2 text-green" %> - Your card grant has been approved! -

- <%= link_to "View card details", card_grant_path(@card_grant), class: "btn bg-success" %> -
+ <%= render "application/callout", type: "success", title: "Your card grant has been approved!" do %> +

You're all set — your card is ready to spend.

+ <%= link_to "View card details", card_grant_path(@card_grant), class: "btn bg-success mt-3" %> + <% end %> <% end %> <% if @pre_authorization.fraudulent? && @pre_authorization.unauthorized? %> -
-

- <%= inline_icon "important", size: 24, class: "mr2 text-orange" %> - Your card grant has been flagged as fraudulent because it doesn't meet the given requirements. Please reach out to <%= @card_grant.event.name %> for support. + <%= render "application/callout", type: "error", title: "This grant has been flagged" do %> +

+ Your card grant was flagged because it doesn't meet the given requirements. + Please reach out to <%= @card_grant.event.name %> for support.

-
+ <% end %> <% end %> <% if @card_grant.instructions.present? %> @@ -60,46 +46,67 @@ <%= render partial: "card_grant/pre_authorizations/organizer_info", locals: { pre_authorization: @pre_authorization, link_to_pre_authorization: false } %> -<%= form_with model: @pre_authorization, id: "pre_authorization_form", url: card_grant_pre_authorizations_path(@card_grant), method: :patch, class: "flex flex-col", data: { turbo: true } do |form| %> - -

1. Provide a link to the product page

-

Submit a link to the product you're looking to purchase. We'll use this to verify that the item is in line with the purpose of the grant.

- -
- <%= form.label :product_url, "Product page URL" %> - <%= form.text_field :product_url, required: true, placeholder: "https://example.com/product", disabled: !@pre_authorization.draft? %> +<%= render "card_grants/section_heading", icon: "1", title: "Link to the product page" %> +

+ Submit a link to the product you're looking to purchase. We'll use this to verify that the item is in line with the purpose of the grant. +

+ +<%= form_with model: @pre_authorization, id: "pre_authorization_form", url: card_grant_pre_authorizations_path(@card_grant), method: :patch, data: { turbo: true } do |form| %> +
+ <% if draft %> +
+ <%= form.label :product_url, "Product page URL" %> + <%= form.text_field :product_url, required: true, placeholder: "https://example.com/product" %> +
+ <% else %> +
+ + <%= inline_icon "link", size: 20 %> + Product page + + <% if @pre_authorization.product_url.present? %> + <%= ugc_link_to @pre_authorization.product_url, @pre_authorization.product_url, target: :_blank, rel: "noopener", class: "grant-row__value" %> + <% else %> + Not provided + <% end %> +
+ <% end %>
- <% end %> -

2. Upload a screenshot of your cart

-

Upload a screenshot of your cart showing the item you want to purchase. Make sure it includes the total cost, as well as any taxes, fees, and shipping costs. This helps us verify the total amount you need to spend.

+<%= render "card_grants/section_heading", icon: "2", title: "Screenshot of your cart" %> +

+ Upload a screenshot of your cart showing the item you want to purchase. Make sure it includes the total cost, as well as any taxes, fees, and shipping costs. This helps us verify the total amount you need to spend. +

+ +
+
+ <% if draft %> + <%= render partial: "card_grant/pre_authorizations/screenshot_form", locals: { turbo: true } %> + <% end %> -
- <% if @pre_authorization.draft? %> - <%= render partial: "card_grant/pre_authorizations/screenshot_form", locals: { - upload_method: "attach_receipt_page", - turbo: true + <%= render partial: "card_grant/pre_authorizations/screenshot_list", locals: { + screenshots: @pre_authorization.screenshots.attachments } %> - <% end %> +
- <%= render partial: "card_grant/pre_authorizations/screenshot_list", locals: { - screenshots: @pre_authorization.screenshots.attachments, - shimmer: + <%= render partial: "card_grant/pre_authorizations/screenshot_count", locals: { + count: @pre_authorization.screenshots.count, + card_grant: @card_grant, + disabled: !draft } %> -
+
-<%= render partial: "card_grant/pre_authorizations/screenshot_count", locals: { - count: @pre_authorization.screenshots.count, - card_grant: @card_grant, - disabled: !@pre_authorization.draft? - } %> +<%= render "card_grants/section_heading", icon: "3", title: "Submit for review" %> -

3. Submit for review

-

Once you've provided the product link and uploaded the screenshot, our review process will screen your submission and activate your grant.

+
+

+ Once you've provided the product link and uploaded the screenshot, our review process will screen your submission and activate your grant. +

-<% if @pre_authorization.draft? %> - -<% elsif @pre_authorization.submitted? %> - -<% end %> + <% if draft %> + + <% elsif @pre_authorization.submitted? %> + + <% end %> +
diff --git a/app/views/card_grants/_actions.html.erb b/app/views/card_grants/_actions.html.erb index d912d8005a..ea184f8877 100644 --- a/app/views/card_grants/_actions.html.erb +++ b/app/views/card_grants/_actions.html.erb @@ -1,22 +1,38 @@ <%# 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: %> - <% 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: %> + <% own = card_grant.user == current_user %> + <% stripe_card = card_grant.stripe_card %> + + <%# Rendered up front since several hide themselves, and an empty card shouldn't show. %> + <% controls = [] %> + <% if organizer_signed_in?(as: :member) || own %> + <% controls << render("stripe_cards/actions/freeze", stripe_card:, variant: "control", label: "Pause spending instantly", subtitle: "You can unfreeze it yourself at any time") if stripe_card.present? %> + <% controls << render("card_grants/actions/convert_to_reimbursement_report", card_grant:, variant: "control", label: own ? "I paid out of pocket" : "Create reimbursement report", subtitle: "Swap the balance for a reimbursement") %> + <% if own %> + <% controls << 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 %> + <% controls << render("card_grants/actions/cancel", card_grant:, variant: "control", label: "Return the balance to #{card_grant.event.name}") %> <% end %> -
+ <% end %> + <% controls.reject!(&:blank?) %> + <% support = render("card_grants/actions/support", card_grant:, title: own ? "Still stuck?" : "Need help?") if organizer_signed_in?(as: :member) || own %> + <% answers = render("card_grants/quick_answers") if own %> + + <% if controls.any? %> +
+

Controls

+
+ <%= safe_join controls %> +
+
+ <% end %> + + <%# Help gets its own card so it doesn't read as one more control. %> + <% if answers.present? || support.present? %> +
+ <%= answers %> + <%= support %> +
+ <% end %> <% end %> diff --git a/app/views/card_grants/_canceled_warning.html.erb b/app/views/card_grants/_canceled_warning.html.erb deleted file mode 100644 index 657b41a9cf..0000000000 --- a/app/views/card_grants/_canceled_warning.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -<%# locals: (card_grant:) %> - -<% if card_grant.canceled? %> -
-
- <%= inline_icon "info", size: 32, class: "mr1" %> -

- <% if card_grant.converted_to_reimbursement_report? %> - This grant has been canceled and converted into a reimbursement report - <% else %> - This grant has been canceled - <% end %> - <% if canceled_by = card_grant.last_user_change_to(status: :canceled) %> - by <%= user_mention canceled_by, disable_tooltip: true %> - <% end %> - <% if canceled_at = card_grant.last_time_change_to(status: :canceled) %> - - <%= time_ago_in_words canceled_at %> ago - - <% end %> - <% if card_grant.converted_to_reimbursement_report? && policy(card_grant.reimbursement_report).show? %> - • <%= link_to "View reimbursement report", card_grant.reimbursement_report %> - <% end %> -

-
-
-<% end %> diff --git a/app/views/card_grants/_card_details.html.erb b/app/views/card_grants/_card_details.html.erb index 59e0d2a406..1c422803b4 100644 --- a/app/views/card_grants/_card_details.html.erb +++ b/app/views/card_grants/_card_details.html.erb @@ -1,147 +1,139 @@ <%# 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:

- -
-
- Status - - <%= status_badge stripe_card.status_badge_type %> - <%= stripe_card.status_text.capitalize %> - +
+ <% if (stripe_card = card_grant.stripe_card).present? %> + <% cardholder = stripe_card.cardholder %> + <% owner = stripe_card.user == current_user && stripe_card.virtual? %> + <% revealed = owner && @show_card_details %> + +
+
+
"> + <%= 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 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 %> +
+
+

Card

+ +
+ Name on card + <%= copy_to_clipboard(stripe_card.user.name, tooltip_direction: "w") %>
- <% 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 %> + <% 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 %> + +
+ 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 %> - <%= 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 %> +
-
-
- - Expiration date - - <%= inline_icon "important", size: 18, style: "position: unset;" %> +
+ + Expiry + + <%= 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 } %> -
+ + <%= copy_to_clipboard(render_short_exp_date(stripe_card), tooltip_direction: "w") { render_exp_date stripe_card } %> + +
-
- 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 %> +
+ CVC + + <% if revealed %> + <%= copy_to_clipboard(stripe_card.cvc, tooltip_direction: "w") { stripe_card.cvc } %> <% else %> <%= stripe_card.hidden_cvc %> <% end %> - <% else %> - <%= stripe_card.hidden_cvc %> -
- <%= inline_icon "private", size: 32, class: "muted" %> -
- <% end %> +
-
- <% if stripe_card.cash_withdrawal_enabled? %> -
- PIN -
- •••• - <%= inline_icon @show_card_details ? "view-fill" : "view", class: "align-middle", "data-action": "click->pin#view" %> + <% if stripe_card.cash_withdrawal_enabled? && stripe_card.user == current_user %> +
+ PIN + + •••• + <%= inline_icon "view", class: "pointer align-middle", "data-action": "click->pin#view" %> +
-
- <% 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") %> + <% end %>
- <% 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") %> -
- <% end %> + <%# The billing address can be the grantee's home, so organizers only get the card itself. %> + <% admin_tool_if(card_grant.user != current_user) do %> +
+

Billing address

-
- City - <%= copy_to_clipboard(stripe_card.cardholder.stripe_billing_address_city, class: "w-fit", tooltip_direction: "e") %> -
+
+ + 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 %> + +
-
- State - <%= copy_to_clipboard(stripe_card.cardholder.stripe_billing_address_state, class: "w-fit", tooltip_direction: "e") %> -
+
+ City + <%= copy_to_clipboard(cardholder.stripe_billing_address_city, tooltip_direction: "w") %> +
-
- ZIP/Postal code - <%= copy_to_clipboard(stripe_card.stripe_cardholder.address_postal_code, class: "w-fit", tooltip_direction: "e") %> -
+
+ State + <%= copy_to_clipboard(cardholder.stripe_billing_address_state, tooltip_direction: "w") %> +
- <% 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") %> +
+ 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 %> - -
- Type - <%= stripe_card.virtual? ? "Virtual" : "Physical" %> -
- -
- Network - Visa -
-
+
-
-<% end %> + <% end %> + + <%= render partial: "card_grants/actions", locals: { card_grant: } %> +
diff --git a/app/views/card_grants/_control.html.erb b/app/views/card_grants/_control.html.erb new file mode 100644 index 0000000000..0db89d3f6e --- /dev/null +++ b/app/views/card_grants/_control.html.erb @@ -0,0 +1,9 @@ +<%# locals: (title:, description:, icon:) %> + +<%# Body of one row in the grant controls list. The row itself is the button or link, so everything in here is inline. %> +<%= inline_icon icon, size: 20 %> + + <%= title %> + <%= description %> + +<%= inline_icon "view-forward", size: 24, class: "grant-action__arrow" %> 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/_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/_grant_details.html.erb b/app/views/card_grants/_grant_details.html.erb index c99c69fedd..3f77dd6957 100644 --- a/app/views/card_grants/_grant_details.html.erb +++ b/app/views/card_grants/_grant_details.html.erb @@ -1,73 +1,57 @@ -<%# locals: (card_grant:) %> +<%# locals: (card_grant:, own:) %> -<% 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 = 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. %> +<% inline_limit = 2 %> +<% drawers = { "Merchants" => merchants, "Categories" => categories }.select { |_, items| items.size > inline_limit } %> + +
    + <% drawers.each_key do |label| %> + + <% end %> + +
    + <%= render "card_grants/control_field", id: "grant-merchants", label: "Merchants", items: merchants, empty: "Anywhere", inline_limit: %> + <%= render "card_grants/control_field", id: "grant-categories", label: "Categories", items: categories, empty: "Anything", inline_limit: %> -
    -

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

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

    <%= card_grant.keyword_lock %>

    +
    + <% end %> - <% 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 %> +
    + One time use +

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

    +
    -

    - Allowed merchants - <% if card_grant.allowed_merchants.any? %> - <%= card_grant.allowed_merchant_names.to_sentence %> - <% else %> - All merchants allowed - <% end %> -

    +
    + Unspent funds +

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

    +
    +
    - <% if card_grant.allowed_merchants.any? && organizer_signed_in? %> -

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

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

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

    + <% if !own && card_grant.allowed_merchants.any? && organizer_signed_in? %> +

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

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

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

    - <% end %> -
    + <% if own %> +
    +

    + <%= 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 %> + <% end %> +
    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..1fc2ee4bac --- /dev/null +++ b/app/views/card_grants/_hero.html.erb @@ -0,0 +1,93 @@ +<%# locals: (card_grant:, footer: nil, show_actions: true) %> + +<% 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 %> +<%# `state` is the one place that knows whether the grant can be spent right + now, so the badge, the meter and the date all key off it. %> +<% state = card_grant.state %> +<% spendable = state == "success" %> +<% ended = card_grant.canceled? || card_grant.expired? %> +<% if ended %> + <% ended_at = card_grant.last_time_change_to(status: card_grant.status.to_sym) || card_grant.expiration_at %> +<% end %> +<% days_left = ((ended ? ended_at.to_date : card_grant.expiration_at.to_date) - Date.current).to_i %> + +
    "> +
    +

    + <%= status_badge state %> + <%= card_grant.state_text %> + <% if card_grant.purpose.present? %> +   • <%= card_grant.purpose %> + <% end %> +

    + <% if show_actions && organizer_signed_in?(as: :member) %> +
    + <%= render "card_grants/actions/edit", card_grant: %> +
    + <% end %> +
    + +
    +
    + <%= render_money remaining %> +

    + <%= remaining == total ? "to spend" : "left of #{render_money total}" %> +

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

    + <% if card_grant.canceled? %> + Canceled on + <% elsif card_grant.expired? %> + Expired on + <% else %> + <%= own ? "Spend it by" : "Expires on" %> + <% end %> +

    +
    +

    <%= format_date(ended ? ended_at : card_grant.expiration_at) %>

    + + <% if days_left.positive? %> + <%= pluralize days_left, "day" %> left + <% elsif days_left.zero? %> + today + <% else %> + <%= pluralize days_left.abs, "day" %> ago + <% end %> + +
    + +
    +
    + Granted by + <%= link_to card_grant.event.name, card_grant.event, class: "grant-meta__value" %> +
    +
    + Granted to + <%= user_mention card_grant.user, class: "grant-meta__value" %> +
    +
    + Sent + + <%= time_ago_in_words card_grant.created_at %> ago + +
    +
    +
    +
    +
    +<% if footer.present? %> +
    + <%= footer %> +
    +<% end %> diff --git a/app/views/card_grants/_invitation.html.erb b/app/views/card_grants/_invitation.html.erb index 9b299c0b95..3e0e0b97c8 100644 --- a/app/views/card_grants/_invitation.html.erb +++ b/app/views/card_grants/_invitation.html.erb @@ -2,47 +2,46 @@ <% if card_grant.pending_invite? %> <%# Prompt user to create a Stripe Card %> -
    -
    -
    -

    Activate your grant card

    -

    - Before you can start spending, you need to agree to the Card Issuing Terms and pledge to use the card responsibly. -

    - <%= form_with url: activate_card_grant_path(card_grant) do |form| %> -
    - <%= form.check_box :purpose, required: true %> - <%= form.label :purpose do %> - I will only use this card for the intended purpose - <% if card_grant.purpose.present? %> - (<%= card_grant.purpose %>) - <% end %> - and adhere to the instructions provided by <%= card_grant.event.name + (card_grant.instructions.present? ? ":" : "") %> - <% end %> - <% if card_grant.instructions.present? %> -
    <%# this empty
    exists for layout purposes to handle checkbox alignment %> -
    - <%= MarkdownService.instance.render(card_grant.instructions) %> -
    - <% end %> -
    -
    - <%= form.check_box :policy, required: true %> - <%= form.label :policy do %> - I will abide by the <%= link_to "Code of Conduct", "https://hackclub.com/conduct", target: :_blank %> and understand that fraudulent activity will result in a ban - <% end %> -
    -
    - <%= form.check_box :terms, required: true %> - <%= form.label :terms do %> - I agree to the <%= link_to "Card Issuing Terms", "https://stripe.com/legal/issuing/celtic-authorized-user-terms", target: :_blank %> - <% end %> -
    -
    - <%= form.submit "Start spending" %> -
    +
    +

    Activate your grant card

    + <%= link_to "https://grants.help.hcb.hackclub.com/en/articles/15410293-i-got-a-card-grant-how-do-i-use-it", target: "_blank", rel: "noopener", class: "flex items-center shrink-0 no-underline" do %> + How card grants work + <%= inline_icon "view-forward" %> + <% end %> +
    +

    + One more step before you can start spending: agree to the following and we'll issue your card. +

    + + <%= form_with url: activate_card_grant_path(card_grant), class: "card grant-pledge" do |form| %> + <%= form.label :purpose, class: "grant-pledge__row" do %> + <%= form.check_box :purpose, required: true %> + + I will only use this card for the intended purpose + <% if card_grant.purpose.present? %> + (<%= card_grant.purpose %>) <% end %> -
    + and adhere to the instructions provided by <%= card_grant.event.name %><%= " above" if card_grant.instructions.present? %> + + <% end %> + + <%= form.label :policy, class: "grant-pledge__row" do %> + <%= form.check_box :policy, required: true %> + + I will abide by the <%= link_to "Code of Conduct", "https://hackclub.com/conduct", target: :_blank %> and understand that fraudulent activity will result in a ban + + <% end %> + + <%= form.label :terms, class: "grant-pledge__row" do %> + <%= form.check_box :terms, required: true %> + + I agree to the <%= link_to "Card Issuing Terms", "https://stripe.com/legal/issuing/celtic-authorized-user-terms", target: :_blank %> + + <% end %> + + -
    + <% end %> <% end %> diff --git a/app/views/card_grants/_quick_answers.html.erb b/app/views/card_grants/_quick_answers.html.erb new file mode 100644 index 0000000000..1773b635b4 --- /dev/null +++ b/app/views/card_grants/_quick_answers.html.erb @@ -0,0 +1,28 @@ +<%# locals: () %> + +<%# Help center articles as a row of tiles rather than a FAQ list, so one glance shows what's covered. %> +
    +
    +

    Need help?

    + <%= link_to "https://grants.help.hcb.hackclub.com/en/collections/19675936-card-grants", target: "_blank", rel: "noopener", class: "flex items-center shrink-0 no-underline" do %> + All articles + <%= inline_icon "view-forward" %> + <% end %> +
    + +
    + <% [ + ["I'm out of funds", "plus", "purple", "https://grants.help.hcb.hackclub.com/en/articles/15582009-can-i-add-more-money-to-my-grant-card"], + ["My card was declined", "forbidden", "primary", "https://grants.help.hcb.hackclub.com/en/articles/15414377-my-grant-card-was-declined-what-can-i-do"], + ["I made a personal purchase", "flag", "warning", "https://grants.help.hcb.hackclub.com/en/articles/15582008-i-used-my-grant-card-for-a-personal-purchase-by-mistake-what-do-i-do"], + ["I got a refund", "view-reload", "success", "https://grants.help.hcb.hackclub.com/en/articles/15582011-i-got-a-refund-on-a-grant-purchase-where-does-the-money-go"], + ["I have multiple grants", "card-list", "info", "https://grants.help.hcb.hackclub.com/en/articles/15582012-can-i-combine-multiple-grant-cards"], + ].each do |label, icon, tone, url| %> + <%= link_to url, target: "_blank", rel: "noopener", class: "grant-answer grant-tone--#{tone}" do %> + <%= inline_icon icon, size: 20 %> + <%= label %> + + <% end %> + <% 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..549b8883be --- /dev/null +++ b/app/views/card_grants/_section_heading.html.erb @@ -0,0 +1,11 @@ +<%# locals: (icon: "", title:, own: true, badge: nil) %> + +

    + <% if own && !icon.empty? %> + + <% end %> + <%= title %> + <% if badge %> + <%= badge %> + <% end %> +

    diff --git a/app/views/card_grants/_status_callout.html.erb b/app/views/card_grants/_status_callout.html.erb new file mode 100644 index 0000000000..eaa55c0e28 --- /dev/null +++ b/app/views/card_grants/_status_callout.html.erb @@ -0,0 +1,106 @@ +<%# locals: (card_grant:) %> + +<%# One callout per grant state, so the hero badge, this notice and the sections + underneath always agree. Only states that need explaining get one: an + active, spendable grant shows nothing here. Suspected fraud is left to the + pre-authorization callout, which already carries the organizer's actions. %> + +<% own = card_grant.user == current_user %> +<% stripe_card = card_grant.stripe_card %> +<% event_name = card_grant.event.name %> + +<% canceled_by = card_grant.last_user_change_to(status: :canceled) if card_grant.canceled? %> +<% canceled_at = card_grant.last_time_change_to(status: :canceled) if card_grant.canceled? %> +<% canceled_when_by = capture do %> + <% if canceled_by %>by <%= user_mention canceled_by, disable_tooltip: true %><% end %> + <% if canceled_at %><%= time_ago_in_words canceled_at %> ago<% end %> +<% end %> + +<% if card_grant.converted_to_reimbursement_report? %> + <%= render "application/callout", type: "info", icon: "attachment", title: "This grant was converted into a reimbursement report" do %> +

    + Converted <%= canceled_when_by %>. The card no longer works; the money is paid out through the report instead. +

    + <% if policy(card_grant.reimbursement_report).show? %> + <%= link_to "View reimbursement report", card_grant.reimbursement_report, class: "btn mt-3" %> + <% end %> + <% end %> + +<% elsif card_grant.canceled? %> + <%= render "application/callout", type: "info", color: "muted", icon: "forbidden", title: "This grant has been canceled" do %> +

    + Canceled <%= canceled_when_by %>. + <% if stripe_card.nil? %> + It was canceled before the card was activated, so nothing was ever charged to it. + <% else %> + The card no longer works, and the unspent balance went back to <%= event_name %>. + <% if own %>Any purchases you already made still need receipts, which you can upload below.<% end %> + <% end %> +

    + <% end %> + +<% elsif card_grant.expired? %> + <%= render "application/callout", type: "info", color: "muted", icon: "clock", title: "This grant expired on #{format_date card_grant.expiration_at}" do %> +

    + <% if stripe_card.nil? %> + It expired before the card was activated, so nothing was ever charged to it. + <% else %> + The card no longer works, and the unspent balance went back to <%= event_name %>. + <% if own %>Any purchases you already made still need receipts, which you can upload below.<% end %> + <% end %> +

    + <% end %> + +<% elsif card_grant.pending_invite? && !own %> + <%= render "application/callout", type: "info", icon: "email", title: "Waiting for #{card_grant.user.name} to activate this grant" do %> +

    + We emailed an invitation to <%= card_grant.email %>. The virtual card is issued as soon as they accept it, and nothing can be spent until then. +

    + <% end %> + +<% elsif stripe_card&.frozen? %> + <% frozen_by = stripe_card.last_frozen_by if stripe_card.last_frozen_by && stripe_card.last_frozen_by != User.system_user %> + <%= render "application/callout", type: "warning", icon: "freeze", title: "This card is frozen" do %> +

    + <% if frozen_by %> + Frozen by <%= user_mention frozen_by, disable_tooltip: true %>. + <% end %> + Nothing can be charged to it until it's defrosted. + <% if policy(stripe_card).defrost? %> + You can defrost it from the controls below. + <% elsif card_grant.event.financially_frozen? || card_grant.user.locked? %> + Only the HCB team can defrost it. <%= help_message %> + <% elsif frozen_by && frozen_by != current_user %> + To use it again, ask <%= frozen_by.name %> or a manager of <%= event_name %> to defrost it. + <% else %> + To use it again, ask a manager of <%= event_name %> to defrost it. + <% end %> +

    + <%# Grantees can't defrost a card someone else froze, so point them straight at whoever can. + The support modal lives in the controls, which only render for these viewers. %> + <% if (own || organizer_signed_in?(as: :member)) && !policy(stripe_card).defrost? && !card_grant.event.financially_frozen? && !card_grant.user.locked? %> + <% if card_grant.setting.support_message.present? %> + + <% elsif card_grant.setting.support_url.present? %> + <%= link_to card_grant.setting.support_url, target: "_blank", rel: "noopener", class: "btn mt-3" do %> + <%= inline_icon card_grant.setting.slack_support? ? "slack" : (card_grant.setting.email_support? ? "email" : "support") %> + <% if card_grant.setting.slack_support? %> + Contact <%= event_name %> on Slack + <% elsif card_grant.setting.email_support? %> + Email <%= event_name %> + <% else %> + Contact <%= event_name %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + +<% elsif stripe_card&.inactive? && card_grant.pre_authorization.nil? %> + <%= render "application/callout", type: "warning", icon: "important", title: "This card isn't active yet" do %> +

    Nothing can be charged to it until it's activated.

    + <% end %> +<% end %> diff --git a/app/views/card_grants/_transactions.html.erb b/app/views/card_grants/_transactions.html.erb index d67a357836..20e7d0c57a 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, receipt_upload_button: !is_public %> <% else %> diff --git a/app/views/card_grants/actions/_cancel.html.erb b/app/views/card_grants/actions/_cancel.html.erb index e29f4b638a..e9b4ba6485 100644 --- a/app/views/card_grants/actions/_cancel.html.erb +++ b/app/views/card_grants/actions/_cancel.html.erb @@ -1,15 +1,29 @@ -<%# 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 grant -

    <%= label.presence || cancel_tooltip %>

    -
    +<% 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" %> + <%= button_to cancel_path, + method: :post, + form_class: "contents", + class: "grant-action grant-tone--primary", + disabled: !policy(card_grant).cancel?, + data: cancel_data do %> + <%= render "card_grants/control", title: "Cancel grant", description: policy(card_grant).cancel? ? label.presence || cancel_tooltip : cancel_tooltip, icon: "reply" %> + <% 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 6763ba2772..46d7c99cfd 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,17 +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" %> - <%= button_to convert_to_reimbursement_report_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).convert_to_reimbursement_report?, - data: { 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 } do %> - <%= inline_icon "attachment", size: 20 %> -
    - <%= label %> -

    <%= subtitle.presence || reimbursement_tooltip %>

    -
    + <% 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" %> + <%= button_to convert_path, + method: :post, + form_class: "contents", + class: "grant-action grant-tone--purple", + disabled: !policy(card_grant).convert_to_reimbursement_report?, + data: convert_confirm do %> + <%= render "card_grants/control", title: label, description: policy(card_grant).convert_to_reimbursement_report? ? subtitle.presence || reimbursement_tooltip : reimbursement_tooltip, icon: "reimbursement" %> + <% end %> + <% else %> + <%= button_to convert_path, + method: :post, + form_class: "contents", + class: "btn btn--list-item", + disabled: !policy(card_grant).convert_to_reimbursement_report?, + data: convert_confirm do %> + <%= inline_icon "attachment", size: 20 %> +
    + <%= label %> +

    <%= subtitle.presence || reimbursement_tooltip %>

    +
    + <% end %> <% end %> <% end %> diff --git a/app/views/card_grants/actions/_edit.html.erb b/app/views/card_grants/actions/_edit.html.erb index 166fdfa38b..c09c269614 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 "#", class: "btn whitespace-nowrap", @@ -25,6 +25,6 @@ <% end %> diff --git a/app/views/card_grants/actions/_return.html.erb b/app/views/card_grants/actions/_return.html.erb index d2177a6bb7..6a1a9c3cb2 100644 --- a/app/views/card_grants/actions/_return.html.erb +++ b/app/views/card_grants/actions/_return.html.erb @@ -1,12 +1,8 @@ -<%# 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" %> +<%= link_to "#", data: { behavior: "modal_trigger", modal: "return-grant" }, class: "grant-action grant-tone--primary" do %> + <%= render "card_grants/control", title: label.presence || "Return grant", description: return_tooltip, icon: "reply" %> <% end %>