diff --git a/app/controllers/grants_controller.rb b/app/controllers/grants_controller.rb
index 38be97e29..92ad1f63a 100644
--- a/app/controllers/grants_controller.rb
+++ b/app/controllers/grants_controller.rb
@@ -100,6 +100,12 @@ def filter_grants(scope)
scope = scope.where(funder_type: params[:funder_type]) if Grant::FUNDER_TYPES.include?(params[:funder_type])
scope = scope.where(funder_id: params[:funder_id]) if params[:funder_id].present?
+ scope = case params[:planned_giving]
+ when "yes" then scope.where(planned_giving: true)
+ when "no" then scope.where(planned_giving: false)
+ else scope
+ end
+
# Sector/category filters back the "View all grants" deep link from the
# taggings browse page (see TaggingsHelper#tagged_index_path).
scope = scope.sector_names_all(params[:sector_names_all]) if params[:sector_names_all].present?
@@ -155,6 +161,7 @@ def grant_params
params.require(:grant).permit(
:name, :description, :amount_dollars, :amount_cents, :funder_sgid,
:funds_allocation_deadline, :funds_received_on, :eligibility_criteria, :tasks,
+ :planned_giving,
sector_ids: [], category_ids: [],
primary_asset_attributes: [ :id, :file, :_destroy ],
gallery_assets_attributes: [ :id, :file, :_destroy ]
diff --git a/app/decorators/grant_decorator.rb b/app/decorators/grant_decorator.rb
index ac78b1278..2bcbf3516 100644
--- a/app/decorators/grant_decorator.rb
+++ b/app/decorators/grant_decorator.rb
@@ -33,6 +33,16 @@ def fully_allocated?
object.remaining_cents <= 0
end
+ # Amber pill flagging a grant as a Legacy scholarship (funded through planned
+ # giving). Renders nothing for ordinary grants, so callers can inline it.
+ def legacy_scholarship_badge
+ return unless object.planned_giving?
+
+ h.tag.span("Legacy scholarship",
+ class: "inline-flex items-center rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-800",
+ title: "Funded through planned giving")
+ end
+
# Whole-number percentage of the grant awarded in scholarships, clamped to
# 0–100, for rendering the allocation progress bar on the index.
def allocation_percentage
diff --git a/app/views/grants/_filters.html.erb b/app/views/grants/_filters.html.erb
index 0fab8f3d7..b58e8356f 100644
--- a/app/views/grants/_filters.html.erb
+++ b/app/views/grants/_filters.html.erb
@@ -32,6 +32,13 @@
include_blank: "All",
class: field_class %>
+
+ <%= label_tag :planned_giving, "Legacy scholarship", class: label_class %>
+ <%= select_tag :planned_giving,
+ options_for_select([ [ "Legacy scholarships", "yes" ], [ "Other grants", "no" ] ], params[:planned_giving]),
+ include_blank: "All",
+ class: field_class %>
+
<%= label_tag :tasks, "Tasks completed", class: label_class %>
<%= select_tag :tasks,
diff --git a/app/views/grants/_form.html.erb b/app/views/grants/_form.html.erb
index 11b24531c..da2a4f6c2 100644
--- a/app/views/grants/_form.html.erb
+++ b/app/views/grants/_form.html.erb
@@ -3,10 +3,21 @@
<%= render "shared/errors", resource: @grant if @grant.errors.any? %>
-
- <%= f.input :name,
- label: "Grant name",
- input_html: { class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm" } %>
+
+
+ <%= f.input :name,
+ label: "Grant name",
+ input_html: { class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm" } %>
+
+
+ <%= f.label :amount_dollars, "Funding amount", class: "block text-sm font-medium text-gray-700 mb-1" %>
+
+ $
+ <%= f.number_field :amount_dollars, step: 0.01, value: @grant.amount_dollars,
+ class: "pl-7 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm" %>
+
+
Total scholarship amounts awarded from this grant cannot exceed this.
+
@@ -27,15 +38,13 @@
label: "Funder",
hint: "The organization or person funding the grant." %>
-
- <%= f.label :amount_dollars, "Funding amount", class: "block text-sm font-medium text-gray-700 mb-1" %>
-
- $
- <%= f.number_field :amount_dollars, step: 0.01, value: @grant.amount_dollars,
- class: "pl-7 w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm" %>
-
-
Total scholarship amounts awarded from this grant cannot exceed this.
-
+
diff --git a/app/views/grants/_grant.html.erb b/app/views/grants/_grant.html.erb
index 1292559e1..255227842 100644
--- a/app/views/grants/_grant.html.erb
+++ b/app/views/grants/_grant.html.erb
@@ -4,7 +4,10 @@
rather than their formatted text. %>
|
- <%= link_to grant.name, grant_path(grant), class: "inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-900 shadow-sm transition-colors hover:border-gray-400 hover:bg-gray-50", data: { turbo_frame: "_top" } %>
+
+ <%= link_to grant.name, grant_path(grant), class: "inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-900 shadow-sm transition-colors hover:border-gray-400 hover:bg-gray-50", data: { turbo_frame: "_top" } %>
+ <%= grant.legacy_scholarship_badge %>
+
|
<%= grant_funder_badge(grant) %> |
<%= grant.amount %> |
diff --git a/app/views/grants/show.html.erb b/app/views/grants/show.html.erb
index 8cfed35ad..ffd44a364 100644
--- a/app/views/grants/show.html.erb
+++ b/app/views/grants/show.html.erb
@@ -16,7 +16,10 @@
<% end %>
-
<%= grant.name %>
+
+
<%= grant.name %>
+ <%= grant.legacy_scholarship_badge %>
+
<%= grant_funder_badge(grant) %>
<% if allowed_to?(:edit?, @grant) %>
diff --git a/db/migrate/20260814230559_add_planned_giving_to_grants.rb b/db/migrate/20260814230559_add_planned_giving_to_grants.rb
new file mode 100644
index 000000000..eb97a63ad
--- /dev/null
+++ b/db/migrate/20260814230559_add_planned_giving_to_grants.rb
@@ -0,0 +1,9 @@
+class AddPlannedGivingToGrants < ActiveRecord::Migration[8.1]
+ def up
+ add_column :grants, :planned_giving, :boolean, default: false, null: false
+ end
+
+ def down
+ remove_column :grants, :planned_giving, if_exists: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8da6a717f..964b86fdf 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -715,6 +715,7 @@
t.date "funds_allocation_deadline"
t.date "funds_received_on"
t.string "name", null: false
+ t.boolean "planned_giving", default: false, null: false
t.text "tasks"
t.datetime "updated_at", null: false
t.bigint "updated_by_id"
diff --git a/spec/decorators/grant_decorator_spec.rb b/spec/decorators/grant_decorator_spec.rb
index 39c889e20..1e52ae714 100644
--- a/spec/decorators/grant_decorator_spec.rb
+++ b/spec/decorators/grant_decorator_spec.rb
@@ -132,4 +132,15 @@
expect(grant.decorate).not_to be_fully_allocated
end
end
+
+ describe "#legacy_scholarship_badge" do
+ it "renders a Legacy scholarship pill for planned-giving grants" do
+ badge = create(:grant, :planned_giving).decorate.legacy_scholarship_badge
+ expect(badge).to include("Legacy scholarship")
+ end
+
+ it "renders nothing for ordinary grants" do
+ expect(create(:grant).decorate.legacy_scholarship_badge).to be_nil
+ end
+ end
end
diff --git a/spec/factories/grants.rb b/spec/factories/grants.rb
index c36fb65b1..66634f763 100644
--- a/spec/factories/grants.rb
+++ b/spec/factories/grants.rb
@@ -21,5 +21,9 @@
trait :donated_by_person do
association :funder, factory: :person
end
+
+ trait :planned_giving do
+ planned_giving { true }
+ end
end
end
diff --git a/spec/models/grant_spec.rb b/spec/models/grant_spec.rb
index c594b5154..5846e59c8 100644
--- a/spec/models/grant_spec.rb
+++ b/spec/models/grant_spec.rb
@@ -49,6 +49,12 @@
end
end
+ describe "planned_giving" do
+ it "defaults to false" do
+ expect(Grant.new.planned_giving).to be(false)
+ end
+ end
+
describe "validations" do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_numericality_of(:amount_cents).is_greater_than_or_equal_to(0) }
diff --git a/spec/requests/grants_spec.rb b/spec/requests/grants_spec.rb
index c373a697e..9d9a1e540 100644
--- a/spec/requests/grants_spec.rb
+++ b/spec/requests/grants_spec.rb
@@ -81,6 +81,19 @@
expect(response.body).not_to include("Org grant")
end
+ it "filters by legacy scholarship (planned giving)" do
+ legacy = create(:grant, :planned_giving, name: "Legacy fund")
+ ordinary = create(:grant, name: "Ordinary fund")
+
+ get grants_url(planned_giving: "yes"), headers: frame_headers
+ expect(response.body).to include("Legacy fund")
+ expect(response.body).not_to include("Ordinary fund")
+
+ get grants_url(planned_giving: "no"), headers: frame_headers
+ expect(response.body).to include("Ordinary fund")
+ expect(response.body).not_to include("Legacy fund")
+ end
+
it "filters by grant name" do
create(:grant, name: "Healing Arts Fund")
create(:grant, name: "Music Therapy Grant")
@@ -227,6 +240,12 @@
expect(Grant.last.primary_asset.file).to be_attached
end
+ it "flags the grant as planned giving when checked" do
+ post grants_url, params: { grant: valid_attributes.merge(planned_giving: "1") }
+
+ expect(Grant.last).to be_planned_giving
+ end
+
it "attaches the selected sectors and categories" do
sector = create(:sector, :published)
category = create(:category, :published)