diff --git a/AGENTS.md b/AGENTS.md index 0443af703b..b306c5a5d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,8 @@ This codebase (Rails 8.1) | `Story` | Editorial content with facilitators, primary/gallery assets | | `Resource` | Handouts, toolkits, templates with downloadable assets | | `Person` | Organization affiliates with contacts, addresses, sectors | -| `Organization` | Groups with affiliations, addresses, logos via ActiveStorage | +| `Organization` | Groups with affiliations, addresses, logos via ActiveStorage; `belongs_to :organization_type` | +| `OrganizationType` | Admin-managed "base data" classification (Publishable, e.g. "501c3/nonprofit"); supplies the organization form and public registration dropdown options | | `Grant` | Donated funds (polymorphic `donor`: Organization or Person) with eligibility criteria, tasks, deadlines; parent of `Scholarship`. Scholarship totals cannot exceed the grant amount | | `Scholarship` | Award to a `Person`; optionally drawn from a `Grant`, syncs to event registration `Allocation` | | `Report` | STI base class for MonthlyReport | diff --git a/app/controllers/organization_types_controller.rb b/app/controllers/organization_types_controller.rb new file mode 100644 index 0000000000..b3cc273ece --- /dev/null +++ b/app/controllers/organization_types_controller.rb @@ -0,0 +1,65 @@ +class OrganizationTypesController < ApplicationController + include AhoyTracking + before_action :set_organization_type, only: [ :show, :edit, :update, :destroy ] + + def index + authorize! + per_page = params[:number_of_items_per_page].presence || 25 + base_scope = authorized_scope(OrganizationType.all) + filtered = base_scope.filter_scope(params) + @organization_types = filtered.ordered.paginate(page: params[:page], per_page: per_page) + @organization_counts = Organization.where(organization_type_id: @organization_types.map(&:id)) + .group(:organization_type_id).count + + @count_display = filtered.count == base_scope.count ? base_scope.count : "#{filtered.count}/#{base_scope.count}" + end + + def show + authorize! @organization_type + end + + def new + @organization_type = OrganizationType.new + authorize! @organization_type + end + + def edit + authorize! @organization_type + end + + def create + @organization_type = OrganizationType.new(organization_type_params) + authorize! @organization_type + + if @organization_type.save + redirect_to @organization_type, notice: "Organization type was successfully created." + else + render :new, status: :unprocessable_content + end + end + + def update + authorize! @organization_type + if @organization_type.update(organization_type_params) + redirect_to @organization_type, notice: "Organization type was successfully updated.", status: :see_other + else + render :edit, status: :unprocessable_content + end + end + + def destroy + authorize! @organization_type + @organization_type.destroy! + redirect_to organization_types_path, notice: "Organization type was successfully destroyed." + end + + private + + def set_organization_type + @organization_type = OrganizationType.find(params[:id]) + end + + def organization_type_params + params.require(:organization_type).permit(:name, :published, :description) + end +end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index d2a397df76..03d31204fe 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -8,7 +8,7 @@ def index if turbo_frame_request? per_page = params[:number_of_items_per_page].presence || 25 base_scope = authorized_scope(Organization.includes( - :windows_type, :organization_status, :sectors, :addresses, + :windows_type, :organization_status, :organization_type, :sectors, :addresses, { categorizable_items: { category: :category_type } }, logo_attachment: :blob )) @@ -208,7 +208,7 @@ def populations_served def set_organization @organization = Organization.includes( - :organization_status, :windows_type, :addresses, + :organization_status, :organization_type, :windows_type, :addresses, :categorizable_items, { comments: [ :created_by, :updated_by ] }, { sectorable_items: :sector }, @@ -220,7 +220,7 @@ def set_organization def organization_params params.require(:organization).permit( :name, :description, :start_date, :end_date, :mission_vision_values, - :agency_type, :agency_type_other, :filemaker_code, :logo, :notes, :email, :website_url, + :organization_type_id, :agency_type_other, :filemaker_code, :logo, :notes, :email, :website_url, :organization_status_id, :location_id, :windows_type_id, :profile_show_sectors, :profile_show_email, :profile_show_phone, :profile_show_website, :profile_show_description, :profile_show_workshops, diff --git a/app/decorators/organization_type_decorator.rb b/app/decorators/organization_type_decorator.rb new file mode 100644 index 0000000000..a05041f755 --- /dev/null +++ b/app/decorators/organization_type_decorator.rb @@ -0,0 +1,9 @@ +class OrganizationTypeDecorator < ApplicationDecorator + def title + name + end + + def detail(length: nil) + "Organization type: #{name}" + end +end diff --git a/app/helpers/admin_cards_helper.rb b/app/helpers/admin_cards_helper.rb index 2f0dc068c7..3cdec5ab7d 100644 --- a/app/helpers/admin_cards_helper.rb +++ b/app/helpers/admin_cards_helper.rb @@ -50,6 +50,10 @@ def reference_cards intensity: 100, params: { published: true }), custom_card("Organization statuses", organization_statuses_path, icon: "🧮", color: :emerald, intensity: 100), + model_card(:organization_types, icon: "🏢", + intensity: 100, + title: "Organization types", + params: { published: true }), model_card(:sectors, icon: "🏭", intensity: 100, title: "Sectors", diff --git a/app/models/organization.rb b/app/models/organization.rb index b1ed44097d..ac9dccea2d 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -1,6 +1,7 @@ class Organization < ApplicationRecord include RemoteSearchable, TagFilterable, Trendable, WindowsTypeFilterable, SectorsTaggable, AgeGroupTaggable # Publishable belongs_to :organization_status + belongs_to :organization_type, optional: true belongs_to :organization_obligation, optional: true belongs_to :location, optional: true # TODO - remove Location if unused belongs_to :windows_type, optional: true diff --git a/app/models/organization_type.rb b/app/models/organization_type.rb new file mode 100644 index 0000000000..48ea4c3600 --- /dev/null +++ b/app/models/organization_type.rb @@ -0,0 +1,37 @@ +class OrganizationType < ApplicationRecord + include NameFilterable, Publishable + + # Canonical classifications offered on the organization profile and public + # registration forms. Seeded as published records (see db/seeds.rb) and used + # as the fallback list when no published types exist yet (e.g. a fresh test + # database without seeds), so the forms always have sensible options. + DEFAULT_NAMES = [ + "501c3/nonprofit", + "For-profit", + "Government agency", + "Other" + ].freeze + + has_many :organizations, dependent: :nullify + + validates :name, presence: true, uniqueness: { case_sensitive: false } + + # Names sort cleanly: digits precede letters, so "501c3/nonprofit" leads and + # "Other" trails — exactly the intended display order. + scope :ordered, -> { order(:name) } + scope :name_contains, ->(term) { term.present? ? where("name LIKE ?", "%#{sanitize_sql_like(term)}%") : all } + + # Published names for the forms, falling back to the canonical defaults when + # nothing is seeded yet. + def self.published_names + published.ordered.pluck(:name).presence || DEFAULT_NAMES + end + + scope :filter_scope, ->(params) do + filtered = all + filtered = filtered.name_contains(params[:name]) + filtered = filtered.published if params[:published] == "true" + filtered = filtered.where(published: false) if params[:published] == "false" + filtered + end +end diff --git a/app/policies/organization_type_policy.rb b/app/policies/organization_type_policy.rb new file mode 100644 index 0000000000..44adbccff1 --- /dev/null +++ b/app/policies/organization_type_policy.rb @@ -0,0 +1,12 @@ +class OrganizationTypePolicy < ApplicationPolicy + def index? = admin? + def show? = admin? + def create? = admin? + def update? = admin? + def destroy? = record.persisted? && admin? + + relation_scope do |relation| + next relation if admin? + relation.none + end +end diff --git a/app/services/form_builder_service.rb b/app/services/form_builder_service.rb index daa33f5721..b65d41f8f0 100644 --- a/app/services/form_builder_service.rb +++ b/app/services/form_builder_service.rb @@ -406,10 +406,7 @@ def build_person_contact_info_fields(form, position) key: "agency_website", group: "person_contact_info", required: false) position = add_field(form, position, "Organization Type", :single_select_radio, key: "agency_type", group: "person_contact_info", required: false, - options: [ - "501c3/nonprofit", "For-profit", "Government agency", - "Other (please specify below)" - ]) + options: OrganizationType.published_names) position = add_field(form, position, "Organization Street Address", :free_form_input_one_line, key: "agency_street", group: "person_contact_info", required: false) position = add_field(form, position, "Organization City", :free_form_input_one_line, diff --git a/app/views/organization_types/_form.html.erb b/app/views/organization_types/_form.html.erb new file mode 100644 index 0000000000..bbec66f4d5 --- /dev/null +++ b/app/views/organization_types/_form.html.erb @@ -0,0 +1,45 @@ +<%= simple_form_for(@organization_type) do |f| %> +
| + Name + | + ++ Published + | + ++ Organizations + | + ++ Actions + | +
|---|---|---|---|
| + <%= organization_type.name %> + <% if organization_type.description.present? %> + + <% end %> + | + ++ <% if organization_type.published? %> + + Yes + + <% else %> + + No + + <% end %> + | + ++ <%= @organization_counts[organization_type.id].to_i %> + | + + ++ <%= link_to "Edit", + edit_organization_type_path(organization_type), + class: "btn btn-secondary-outline whitespace-nowrap" %> + | + +
+ No <%= OrganizationType.model_name.human.pluralize.downcase %> found. +
+ <% end %> +Name:
+<%= @organization_type.name %>
+Published:
+<%= @organization_type.published? %>
+Description:
+<%= @organization_type.description %>
++ <%= @organization.organization_type.name %> +
+ <% end %> <% org_earliest_affiliation = @organization.affiliations.minimum(:start_date) %> <% org_affiliated_since = org_earliest_affiliation || @organization.start_date %> <% org_show_aff_ended = @organization.affiliations.any? && !@organization.affiliations.active.exists? %> diff --git a/config/routes.rb b/config/routes.rb index 529c0cc97e..269cc45da0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -204,6 +204,7 @@ resources :refunds, only: [ :new, :create, :show ] resources :organization_statuses + resources :organization_types resources :affiliations, only: :destroy resources :quotes diff --git a/db/migrate/20260623103130_create_organization_types.rb b/db/migrate/20260623103130_create_organization_types.rb new file mode 100644 index 0000000000..9a1341c0b8 --- /dev/null +++ b/db/migrate/20260623103130_create_organization_types.rb @@ -0,0 +1,18 @@ +class CreateOrganizationTypes < ActiveRecord::Migration[8.0] + def up + return if table_exists?(:organization_types) + + create_table :organization_types do |t| + t.string :name + t.text :description + t.boolean :published, default: false, null: false + + t.timestamps + end + add_index :organization_types, :published + end + + def down + drop_table :organization_types, if_exists: true + end +end diff --git a/db/migrate/20260623103132_add_organization_type_to_organizations.rb b/db/migrate/20260623103132_add_organization_type_to_organizations.rb new file mode 100644 index 0000000000..7571e4db2e --- /dev/null +++ b/db/migrate/20260623103132_add_organization_type_to_organizations.rb @@ -0,0 +1,11 @@ +class AddOrganizationTypeToOrganizations < ActiveRecord::Migration[8.0] + def up + return if column_exists?(:organizations, :organization_type_id) + + add_reference :organizations, :organization_type, foreign_key: true, null: true, index: true + end + + def down + remove_reference :organizations, :organization_type, foreign_key: true, if_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 9b89cc1013..e1accb7905 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -782,6 +782,15 @@ t.index ["published"], name: "index_organization_statuses_on_published" end + create_table "organization_types", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.text "description" + t.string "name" + t.boolean "published", default: false, null: false + t.datetime "updated_at", null: false + t.index ["published"], name: "index_organization_types_on_published" + end + create_table "organizations", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.string "agency_type" t.string "agency_type_other" @@ -798,6 +807,7 @@ t.string "name" t.text "notes", size: :long t.integer "organization_status_id" + t.bigint "organization_type_id" t.boolean "profile_show_description", default: true, null: false t.boolean "profile_show_email", default: true, null: false t.boolean "profile_show_events_registered", default: true, null: false @@ -813,6 +823,7 @@ t.integer "windows_type_id" t.index ["location_id"], name: "index_organizations_on_location_id" t.index ["organization_status_id"], name: "index_organizations_on_organization_status_id" + t.index ["organization_type_id"], name: "index_organizations_on_organization_type_id" t.index ["windows_type_id"], name: "index_organizations_on_windows_type_id" end @@ -1681,6 +1692,7 @@ add_foreign_key "notifications", "users", column: "sender_id" add_foreign_key "organizations", "locations" add_foreign_key "organizations", "organization_statuses" + add_foreign_key "organizations", "organization_types" add_foreign_key "organizations", "windows_types" add_foreign_key "pay_charges", "pay_customers", column: "customer_id" add_foreign_key "pay_charges", "pay_subscriptions", column: "subscription_id" diff --git a/db/seeds.rb b/db/seeds.rb index 54b50151d4..d0354af9e5 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -148,6 +148,11 @@ def find_or_create_by_name!(klass, name, **attrs, &block) OrganizationObligation.where(name: obligation_type).first_or_create! end +puts "Creating OrganizationTypes…" +OrganizationType::DEFAULT_NAMES.each do |type_name| + find_or_create_by_name!(OrganizationType, type_name).update!(published: true) +end + puts "Creating Sectors…" # Optional descriptions clarify a sector on the public registration form: shown as # subtext under the checkbox in the additional sectors list, and folded into diff --git a/lib/domain_theme.rb b/lib/domain_theme.rb index e291f9d101..9fc35b233f 100644 --- a/lib/domain_theme.rb +++ b/lib/domain_theme.rb @@ -11,6 +11,7 @@ module DomainTheme events: :blue, people: :sky, organizations: :emerald, + organization_types: :emerald, quotes: :slate, grants: :green, diff --git a/spec/factories/organization_types.rb b/spec/factories/organization_types.rb new file mode 100644 index 0000000000..379be8434a --- /dev/null +++ b/spec/factories/organization_types.rb @@ -0,0 +1,14 @@ +FactoryBot.define do + factory :organization_type do + sequence(:name) { |n| "Organization Type #{n}" } + published { false } + + trait :published do + published { true } + end + + trait :unpublished do + published { false } + end + end +end diff --git a/spec/models/organization_type_spec.rb b/spec/models/organization_type_spec.rb new file mode 100644 index 0000000000..7672ee0d57 --- /dev/null +++ b/spec/models/organization_type_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +RSpec.describe OrganizationType, type: :model do + describe "validations" do + it "requires a name" do + expect(build(:organization_type, name: "")).not_to be_valid + end + + it "enforces case-insensitive name uniqueness" do + create(:organization_type, name: "Nonprofit") + expect(build(:organization_type, name: "nonprofit")).not_to be_valid + end + end + + describe "scopes" do + it ".published returns only published types" do + published = create(:organization_type, :published) + create(:organization_type, :unpublished) + expect(described_class.published).to contain_exactly(published) + end + + it ".name_contains filters by partial, case-insensitive match" do + match = create(:organization_type, name: "Government agency") + create(:organization_type, name: "For-profit") + expect(described_class.name_contains("govern")).to contain_exactly(match) + end + + it ".ordered sorts digits before letters so defaults read in intended order" do + OrganizationType::DEFAULT_NAMES.shuffle.each { |name| create(:organization_type, name: name) } + expect(described_class.ordered.pluck(:name)).to eq(OrganizationType::DEFAULT_NAMES) + end + end + + describe ".published_names" do + it "returns published names ordered" do + create(:organization_type, :published, name: "For-profit") + create(:organization_type, :unpublished, name: "Hidden") + expect(described_class.published_names).to eq([ "For-profit" ]) + end + + it "falls back to the canonical defaults when none are published" do + expect(described_class.published_names).to eq(OrganizationType::DEFAULT_NAMES) + end + end + + describe "associations" do + it "nullifies the type on dependent organizations when destroyed" do + type = create(:organization_type, :published) + organization = create(:organization, organization_type: type) + type.destroy + expect(organization.reload.organization_type_id).to be_nil + end + end +end diff --git a/spec/requests/organization_types_spec.rb b/spec/requests/organization_types_spec.rb new file mode 100644 index 0000000000..b2a53ce1fd --- /dev/null +++ b/spec/requests/organization_types_spec.rb @@ -0,0 +1,173 @@ +require 'rails_helper' + +RSpec.describe "/organization_types", type: :request do + let(:valid_attributes) do + { + name: "Test Organization Type", + published: true + } + end + + let(:invalid_attributes) do + { + name: "", + published: nil + } + end + + let(:admin) { create(:user, :admin) } + + before do + sign_in admin + end + + describe "GET /index" do + it "renders a successful response" do + OrganizationType.create! valid_attributes + get organization_types_url + expect(response).to be_successful + end + + context "filtering" do + let!(:organization_types) do + [ + create(:organization_type, name: "Type1", published: true), + create(:organization_type, name: "Type2", published: true), + create(:organization_type, name: "Type3", published: false), + create(:organization_type, name: "Type4", published: false) + ] + end + + it "returns all organization types without filters" do + get organization_types_url + organization_types.each { |type| expect(response.body).to include(type.name) } + end + + it "returns only published organization types when published=true" do + get organization_types_url, params: { published: "true" } + expect(response.body).to include("Type1", "Type2") + expect(response.body).not_to include("Type3") + expect(response.body).not_to include("Type4") + end + + it "returns only unpublished organization types when published=false" do + get organization_types_url, params: { published: "false" } + expect(response.body).to include("Type3", "Type4") + expect(response.body).not_to include("Type1") + expect(response.body).not_to include("Type2") + end + + it "filters by name" do + get organization_types_url, params: { name: "Type1" } + expect(response.body).to include("Type1") + expect(response.body).not_to include("Type2") + end + end + end + + describe "GET /show" do + it "renders a successful response" do + organization_type = OrganizationType.create! valid_attributes + get organization_type_url(organization_type) + expect(response).to be_successful + end + end + + describe "GET /new" do + it "renders a successful response" do + get new_organization_type_url + expect(response).to be_successful + end + end + + describe "GET /edit" do + it "renders a successful response" do + organization_type = OrganizationType.create! valid_attributes + get edit_organization_type_url(organization_type) + expect(response).to be_successful + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new OrganizationType" do + expect { + post organization_types_url, params: { organization_type: valid_attributes } + }.to change(OrganizationType, :count).by(1) + end + + it "redirects to the created organization type" do + post organization_types_url, params: { organization_type: valid_attributes } + expect(response).to redirect_to(organization_type_url(OrganizationType.last)) + end + end + + context "with invalid parameters" do + it "does not create a new OrganizationType" do + expect { + post organization_types_url, params: { organization_type: invalid_attributes } + }.to change(OrganizationType, :count).by(0) + end + + it "renders a response with 422 status" do + post organization_types_url, params: { organization_type: invalid_attributes } + expect(response).to have_http_status(:unprocessable_content) + end + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:new_attributes) do + valid_attributes.merge( + name: "Updated Type Name", + description: "Clarifying subtext for this type" + ) + end + + it "updates the requested organization type" do + organization_type = OrganizationType.create! valid_attributes + patch organization_type_url(organization_type), params: { organization_type: new_attributes } + organization_type.reload + expect(organization_type.name).to eq("Updated Type Name") + expect(organization_type.description).to eq("Clarifying subtext for this type") + end + + it "redirects to the organization type" do + organization_type = OrganizationType.create! valid_attributes + patch organization_type_url(organization_type), params: { organization_type: new_attributes } + expect(response).to redirect_to(organization_type_url(organization_type)) + end + end + + context "with invalid parameters" do + it "renders a response with 422 status" do + organization_type = OrganizationType.create! valid_attributes + patch organization_type_url(organization_type), params: { organization_type: invalid_attributes } + expect(response).to have_http_status(:unprocessable_content) + end + end + end + + describe "DELETE /destroy" do + it "destroys the requested organization type" do + organization_type = OrganizationType.create! valid_attributes + expect { + delete organization_type_url(organization_type) + }.to change(OrganizationType, :count).by(-1) + end + + it "redirects to the organization types list" do + organization_type = OrganizationType.create! valid_attributes + delete organization_type_url(organization_type) + expect(response).to redirect_to(organization_types_url) + end + + it "nullifies the type on organizations that referenced it" do + organization_type = OrganizationType.create! valid_attributes + organization = create(:organization, organization_type: organization_type) + delete organization_type_url(organization_type) + expect(organization.reload.organization_type_id).to be_nil + end + end +end diff --git a/spec/services/form_builder_service_spec.rb b/spec/services/form_builder_service_spec.rb index b6717e268c..173ab66950 100644 --- a/spec/services/form_builder_service_spec.rb +++ b/spec/services/form_builder_service_spec.rb @@ -75,9 +75,22 @@ it "offers the agency type as the four organization classifications" do field = form.form_fields.find_by(field_identifier: "agency_type") expect(field.answer_options.pluck(:name)).to contain_exactly( - "501c3/nonprofit", "For-profit", "Government agency", "Other (please specify below)" + "501c3/nonprofit", "For-profit", "Government agency", "Other" ) end + + context "when organization types are published" do + before do + create(:organization_type, :published, name: "Co-op") + create(:organization_type, :published, name: "Faith-based") + create(:organization_type, :unpublished, name: "Hidden") + end + + it "offers the published organization type names" do + field = form.form_fields.find_by(field_identifier: "agency_type") + expect(field.answer_options.pluck(:name)).to contain_exactly("Co-op", "Faith-based") + end + end end context "scholarship section" do diff --git a/spec/views/page_bg_class_alignment_spec.rb b/spec/views/page_bg_class_alignment_spec.rb index 97efbf1652..b8ba607d70 100644 --- a/spec/views/page_bg_class_alignment_spec.rb +++ b/spec/views/page_bg_class_alignment_spec.rb @@ -119,6 +119,7 @@ "app/views/forms/show.html.erb" => "admin-only bg-blue-100", "app/views/notifications/index.html.erb" => "admin-only bg-blue-100", "app/views/organization_statuses/index.html.erb" => "admin-only bg-blue-100", + "app/views/organization_types/index.html.erb" => "admin-only bg-blue-100", "app/views/quotes/index.html.erb" => "admin-only bg-blue-100", "app/views/sectors/index.html.erb" => "admin-only bg-blue-100", "app/views/story_ideas/index.html.erb" => "admin-only bg-blue-100", @@ -133,6 +134,7 @@ "app/views/categories/show.html.erb" => "admin-only bg-blue-100", "app/views/category_types/show.html.erb" => "admin-only bg-blue-100", "app/views/organization_statuses/show.html.erb" => "admin-only bg-blue-100", + "app/views/organization_types/show.html.erb" => "admin-only bg-blue-100", "app/views/sectors/show.html.erb" => "admin-only bg-blue-100", "app/views/users/show.html.erb" => "admin-only bg-blue-100", "app/views/windows_types/show.html.erb" => "admin-only bg-blue-100", @@ -149,6 +151,7 @@ "app/views/forms/new.html.erb" => "admin-only bg-blue-100", "app/views/organizations/new.html.erb" => "admin-only bg-blue-100", "app/views/organization_statuses/new.html.erb" => "admin-only bg-blue-100", + "app/views/organization_types/new.html.erb" => "admin-only bg-blue-100", "app/views/people/new.html.erb" => "admin-only bg-blue-100", "app/views/quotes/new.html.erb" => "admin-only bg-blue-100", "app/views/resources/new.html.erb" => "admin-only bg-blue-100", @@ -175,6 +178,7 @@ "app/views/forms/edit_sections.html.erb" => "admin-only bg-blue-100", "app/views/faqs/edit.html.erb" => "admin-only bg-blue-100", "app/views/organization_statuses/edit.html.erb" => "admin-only bg-blue-100", + "app/views/organization_types/edit.html.erb" => "admin-only bg-blue-100", "app/views/quotes/edit.html.erb" => "admin-only bg-blue-100", "app/views/resources/edit.html.erb" => "admin-only bg-blue-100", "app/views/sectors/edit.html.erb" => "admin-only bg-blue-100",