diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 8ca6d5bb8..b01ea2b0f 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -6,9 +6,9 @@ class OrganisationsController < ApplicationController before_action :find_resource, except: %i[index new create] before_action :authenticate_scope!, except: [:index] before_action :session_filters, if: -> { current_user.support? || current_user.organisation.has_managing_agents? }, only: %i[lettings_logs sales_logs email_lettings_csv download_lettings_csv email_sales_csv download_sales_csv] - before_action :session_filters, only: %i[users] + before_action :session_filters, only: %i[users schemes] before_action -> { filter_manager.serialize_filters_to_session }, if: -> { current_user.support? || current_user.organisation.has_managing_agents? }, only: %i[lettings_logs sales_logs email_lettings_csv download_lettings_csv email_sales_csv download_sales_csv] - before_action -> { filter_manager.serialize_filters_to_session }, only: %i[users] + before_action -> { filter_manager.serialize_filters_to_session }, only: %i[users schemes] def index redirect_to organisation_path(current_user.organisation) unless current_user.support? @@ -22,9 +22,10 @@ class OrganisationsController < ApplicationController def schemes all_schemes = Scheme.where(owning_organisation: [@organisation] + @organisation.parent_organisations).order_by_completion.order_by_service_name - @pagy, @schemes = pagy(filtered_collection(all_schemes, search_term)) + @pagy, @schemes = pagy(filter_manager.filtered_schemes(all_schemes, search_term, session_filters)) @searched = search_term.presence @total_count = all_schemes.size + @filter_type = "schemes" end def show @@ -209,6 +210,8 @@ private "sales_logs" elsif params[:action].include?("users") "users" + elsif params[:action].include?("schemes") + "schemes" end end diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index 6dc24ddcd..a433832b5 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -6,6 +6,8 @@ class SchemesController < ApplicationController before_action :find_resource, except: %i[index create new] before_action :redirect_if_scheme_confirmed, only: %i[primary_client_group confirm_secondary_client_group secondary_client_group support details] before_action :authorize_user + before_action :session_filters, if: :current_user, only: %i[index] + before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index] rescue_from ActiveRecord::RecordNotFound, with: :render_not_found @@ -13,9 +15,10 @@ class SchemesController < ApplicationController redirect_to schemes_organisation_path(current_user.organisation) unless current_user.support? all_schemes = Scheme.order_by_completion.order_by_service_name - @pagy, @schemes = pagy(filtered_collection(all_schemes, search_term)) + @pagy, @schemes = pagy(filter_manager.filtered_schemes(all_schemes, search_term, session_filters)) @searched = search_term.presence @total_count = all_schemes.size + @filter_type = "schemes" end def show @@ -336,4 +339,12 @@ private logs.update!(location: nil, scheme: nil, unresolved: true) logs end + + def filter_manager + FilterManager.new(current_user:, session:, params:, filter_type: "schemes") + end + + def session_filters + filter_manager.session_filters + end end diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 5b88655ad..be2f7ced5 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -46,6 +46,16 @@ module FiltersHelper }.freeze end + def scheme_status_filters + { + "incomplete" => "Incomplete", + "active" => "Active", + "deactivating_soon" => "Deactivating soon", + "reactivating_soon" => "Reactivating soon", + "deactivated" => "Deactivated", + }.freeze + end + def selected_option(filter, filter_type) return false unless session[session_name_for(filter_type)] diff --git a/app/models/scheme.rb b/app/models/scheme.rb index fdb5cf394..ff24abdad 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -17,8 +17,58 @@ class Scheme < ApplicationRecord .or(filter_by_id(param)).distinct } - scope :order_by_completion, -> { order("confirmed ASC NULLS FIRST") } + scope :order_by_completion, -> { order("schemes.confirmed ASC NULLS FIRST") } scope :order_by_service_name, -> { order(service_name: :asc) } + scope :filter_by_status, lambda { |statuses, _user = nil| + filtered_records = all + scopes = [] + + statuses.each do |status| + status = status == "active" ? "active_status" : status + if respond_to?(status, true) + scopes << send(status) + end + end + + if scopes.any? + filtered_records = filtered_records + .left_outer_joins(:scheme_deactivation_periods) + .order("scheme_deactivation_periods.created_at DESC") + .merge(scopes.reduce(&:or)) + end + + filtered_records + } + + scope :incomplete, lambda { + where.not(confirmed: true) + .or(where.not(id: Location.select(:scheme_id).where(confirmed: true).distinct)) + .where.not(id: joins(:scheme_deactivation_periods).reactivating_soon.pluck(:id)) + .where.not(id: joins(:scheme_deactivation_periods).deactivated.pluck(:id)) + .where.not(id: joins(:scheme_deactivation_periods).deactivating_soon.pluck(:id)) + } + + scope :deactivated, lambda { + merge(SchemeDeactivationPeriod.deactivations_without_reactivation) + .where("scheme_deactivation_periods.deactivation_date <= ?", Time.zone.now) + } + + scope :deactivating_soon, lambda { + merge(SchemeDeactivationPeriod.deactivations_without_reactivation) + .where("scheme_deactivation_periods.deactivation_date > ? AND scheme_deactivation_periods.deactivation_date < ? ", Time.zone.now, 6.months.from_now) + } + + scope :reactivating_soon, lambda { + where.not("scheme_deactivation_periods.reactivation_date IS NULL") + .where("scheme_deactivation_periods.reactivation_date > ?", Time.zone.now) + } + + scope :active_status, lambda { + where.not(id: joins(:scheme_deactivation_periods).reactivating_soon.pluck(:id)) + .where.not(id: joins(:scheme_deactivation_periods).deactivated.pluck(:id)) + .where.not(id: incomplete.pluck(:id)) + .where.not(id: joins(:scheme_deactivation_periods).deactivating_soon.pluck(:id)) + } validate :validate_confirmed validate :validate_owning_organisation diff --git a/app/services/filter_manager.rb b/app/services/filter_manager.rb index 9de3fcdbb..a473cf271 100644 --- a/app/services/filter_manager.rb +++ b/app/services/filter_manager.rb @@ -50,6 +50,17 @@ class FilterManager users end + def self.filter_schemes(schemes, search_term, filters, user) + schemes = filter_by_search(schemes, search_term) + + filters.each do |category, values| + next if Array(values).reject(&:empty?).blank? + + schemes = schemes.public_send("filter_by_#{category}", values, user) + end + schemes + end + def serialize_filters_to_session(specific_org: false) session[session_name_for(filter_type)] = session_filters(specific_org:).to_json end @@ -73,7 +84,7 @@ class FilterManager new_filters["user"] = current_user.id.to_s if params["assigned_to"] == "you" end - if @filter_type.include?("users") && params["status"].present? + if (@filter_type.include?("schemes") || @filter_type.include?("users")) && params["status"].present? new_filters["status"] = params["status"] end @@ -90,6 +101,10 @@ class FilterManager FilterManager.filter_users(users, search_term, filters, current_user) end + def filtered_schemes(schemes, search_term, filters) + FilterManager.filter_schemes(schemes, search_term, filters, current_user) + end + def bulk_upload id = (logs_filters["bulk_upload_id"] || []).reject(&:blank?)[0] @bulk_upload ||= current_user.bulk_uploads.find_by(id:) diff --git a/app/views/organisations/schemes.html.erb b/app/views/organisations/schemes.html.erb index 6dac0f3a7..20fc40886 100644 --- a/app/views/organisations/schemes.html.erb +++ b/app/views/organisations/schemes.html.erb @@ -11,21 +11,24 @@ ) %>

Supported housing schemes

<% end %> +
+ <% if SchemePolicy.new(current_user, nil).create? %> + <%= govuk_button_link_to "Create a new supported housing scheme", new_scheme_path, html: { method: :post } %> + <% end %> + + <%= govuk_details( + classes: "govuk-!-width-two-thirds", + summary_text: "What is a supported housing scheme?", + text: "A supported housing scheme (also known as a ‘supported housing service’) provides shared or self-contained housing for a particular client group, for example younger or vulnerable people. A single scheme can contain multiple units, for example bedrooms in shared houses or a bungalow with 3 bedrooms.", + ) %> + <%= render partial: "schemes/scheme_filters" %> +
+ <%= render SearchComponent.new(current_user:, search_label: "Search by scheme name, code, postcode or location name", value: @searched) %> -<% if SchemePolicy.new(current_user, nil).create? %> - <%= govuk_button_link_to "Create a new supported housing scheme", new_scheme_path, html: { method: :post } %> -<% end %> - -<%= govuk_details( - classes: "govuk-!-width-two-thirds", - summary_text: "What is a supported housing scheme?", - text: "A supported housing scheme (also known as a ‘supported housing service’) provides shared or self-contained housing for a particular client group, for example younger or vulnerable people. A single scheme can contain multiple units, for example bedrooms in shared houses or a bungalow with 3 bedrooms.", -) %> - -<%= render SearchComponent.new(current_user:, search_label: "Search by scheme name, code, postcode or location name", value: @searched) %> - -
+
-<%= render partial: "schemes/scheme_list", locals: { schemes: @schemes, title:, pagy: @pagy, searched: @searched, item_label:, total_count: @total_count } %> + <%= render partial: "schemes/scheme_list", locals: { schemes: @schemes, title:, pagy: @pagy, searched: @searched, item_label:, total_count: @total_count } %> -<%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "schemes" } %> + <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "schemes" } %> +
+
diff --git a/app/views/schemes/_scheme_filters.html.erb b/app/views/schemes/_scheme_filters.html.erb new file mode 100644 index 000000000..086d6fd84 --- /dev/null +++ b/app/views/schemes/_scheme_filters.html.erb @@ -0,0 +1,29 @@ +
+
+
+

Filters

+
+ +
+ <%= form_with url: schemes_path, html: { method: :get } do |f| %> +
+

+ <%= filters_applied_text(@filter_type) %> +

+

+ <%= reset_filters_link(@filter_type) %> +

+
+ + <%= render partial: "filters/checkbox_filter", + locals: { + f:, + options: scheme_status_filters, + label: "Status", + category: "status", + } %> + <%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %> + <% end %> +
+
+
diff --git a/app/views/schemes/index.html.erb b/app/views/schemes/index.html.erb index fa900ca8d..a1c2041ff 100644 --- a/app/views/schemes/index.html.erb +++ b/app/views/schemes/index.html.erb @@ -8,11 +8,15 @@ <% if SchemePolicy.new(current_user, nil).create? %> <%= govuk_button_link_to "Create a new supported housing scheme", new_scheme_path, html: { method: :post } %> <% end %> +
+ <%= render partial: "schemes/scheme_filters" %> +
+ <%= render SearchComponent.new(current_user:, search_label: "Search by scheme name, code, postcode or location name", value: @searched) %> -<%= render SearchComponent.new(current_user:, search_label: "Search by scheme name, code, postcode or location name", value: @searched) %> +
-
+ <%= render partial: "schemes/scheme_list", locals: { schemes: @schemes, title:, pagy: @pagy, searched: @searched, item_label:, total_count: @total_count } %> -<%= render partial: "schemes/scheme_list", locals: { schemes: @schemes, title:, pagy: @pagy, searched: @searched, item_label:, total_count: @total_count } %> - -<%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "schemes" } %> + <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "schemes" } %> +
+
diff --git a/spec/factories/scheme.rb b/spec/factories/scheme.rb index 155dea11a..5441df32e 100644 --- a/spec/factories/scheme.rb +++ b/spec/factories/scheme.rb @@ -25,5 +25,9 @@ FactoryBot.define do trait :with_old_visible_id do old_visible_id { rand(9_999_999) } end + trait :incomplete do + confirmed { false } + support_type { nil } + end end end diff --git a/spec/features/schemes_spec.rb b/spec/features/schemes_spec.rb index 886a71176..c88b831ec 100644 --- a/spec/features/schemes_spec.rb +++ b/spec/features/schemes_spec.rb @@ -63,6 +63,39 @@ RSpec.describe "Schemes scheme Features" do end end end + + context "when filtering schemes" do + context "when no filters are selected" do + it "displays the filters component with no clear button" do + expect(page).to have_content("No filters applied") + expect(page).not_to have_content("Clear") + end + end + + context "when I have selected filters" do + before do + check("Active") + check("Incomplete") + click_button("Apply filters") + end + + it "displays the filters component with a correct count and clear button" do + expect(page).to have_content("2 filters applied") + expect(page).to have_content("Clear") + end + + context "when clearing the filters" do + before do + click_link("Clear") + end + + it "clears the filters and displays the filter component as before" do + expect(page).to have_content("No filters applied") + expect(page).not_to have_content("Clear") + end + end + end + end end end end diff --git a/spec/models/scheme_spec.rb b/spec/models/scheme_spec.rb index e7b50e5c8..0bdcb940a 100644 --- a/spec/models/scheme_spec.rb +++ b/spec/models/scheme_spec.rb @@ -89,6 +89,75 @@ RSpec.describe Scheme, type: :model do expect(described_class.search_by(location.name.downcase).first.locations.first.name).to eq(location.name) end end + + context "when filtering by status" do + let!(:incomplete_scheme) { FactoryBot.create(:scheme, :incomplete) } + let(:active_scheme) { FactoryBot.create(:scheme) } + let(:deactivating_soon_scheme) { FactoryBot.create(:scheme) } + let(:deactivated_scheme) { FactoryBot.create(:scheme) } + let(:reactivating_soon_scheme) { FactoryBot.create(:scheme) } + + before do + scheme.destroy! + scheme_1.destroy! + scheme_2.destroy! + Timecop.freeze(2022, 6, 7) + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 8, 8), scheme: deactivating_soon_scheme) + deactivating_soon_scheme.save! + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 6), scheme: deactivated_scheme) + deactivated_scheme.save! + FactoryBot.create(:scheme_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 7), reactivation_date: Time.zone.local(2022, 6, 8), scheme: reactivating_soon_scheme) + reactivating_soon_scheme.save! + FactoryBot.create(:location, scheme: active_scheme, confirmed: true) + end + + after do + Timecop.unfreeze + end + + context "when filtering by incomplete status" do + it "returns only incomplete schemes" do + expect(described_class.filter_by_status(%w[incomplete]).count).to eq(1) + expect(described_class.filter_by_status(%w[incomplete]).first).to eq(incomplete_scheme) + end + end + + context "when filtering by active status" do + it "returns only active schemes" do + expect(described_class.filter_by_status(%w[active]).count).to eq(1) + expect(described_class.filter_by_status(%w[active]).first).to eq(active_scheme) + end + end + + context "when filtering by deactivating_soon status" do + it "returns only deactivating_soon schemes" do + expect(described_class.filter_by_status(%w[deactivating_soon]).count).to eq(1) + expect(described_class.filter_by_status(%w[deactivating_soon]).first).to eq(deactivating_soon_scheme) + end + end + + context "when filtering by deactivated status" do + it "returns only deactivated schemes" do + expect(described_class.filter_by_status(%w[deactivated]).count).to eq(1) + expect(described_class.filter_by_status(%w[deactivated]).first).to eq(deactivated_scheme) + end + end + + context "when filtering by reactivating_soon status" do + it "returns only reactivating_soon schemes" do + expect(described_class.filter_by_status(%w[reactivating_soon]).count).to eq(1) + expect(described_class.filter_by_status(%w[reactivating_soon]).first).to eq(reactivating_soon_scheme) + end + end + + context "when filtering by multiple statuses" do + it "returns relevant schemes" do + expect(described_class.filter_by_status(%w[deactivating_soon reactivating_soon]).count).to eq(2) + expect(described_class.filter_by_status(%w[deactivating_soon reactivating_soon])).to include(reactivating_soon_scheme) + expect(described_class.filter_by_status(%w[deactivating_soon reactivating_soon])).to include(deactivating_soon_scheme) + end + end + end end end diff --git a/spec/requests/schemes_controller_spec.rb b/spec/requests/schemes_controller_spec.rb index 3fe859010..b145d7288 100644 --- a/spec/requests/schemes_controller_spec.rb +++ b/spec/requests/schemes_controller_spec.rb @@ -75,6 +75,65 @@ RSpec.describe SchemesController, type: :request do end end end + + context "when filtering" do + context "with status filter" do + let!(:incomplete_scheme) { create(:scheme, :incomplete, owning_organisation: user.organisation) } + let(:active_scheme) { create(:scheme, owning_organisation: user.organisation) } + let!(:deactivated_scheme) { create(:scheme, owning_organisation: user.organisation) } + + before do + create(:location, scheme: active_scheme) + create(:scheme_deactivation_period, scheme: deactivated_scheme, deactivation_date: Time.zone.local(2022, 4, 1)) + end + + it "shows schemes for multiple selected statuses" do + get "/schemes?status[]=incomplete&status[]=active", headers:, params: {} + follow_redirect! + expect(page).to have_link(incomplete_scheme.service_name) + expect(page).to have_link(active_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + end + + it "shows filtered incomplete schemes" do + get "/schemes?status[]=incomplete", headers:, params: {} + follow_redirect! + expect(page).to have_link(incomplete_scheme.service_name) + expect(page).not_to have_link(active_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + end + + it "shows filtered active schemes" do + get "/schemes?status[]=active", headers:, params: {} + follow_redirect! + expect(page).to have_link(active_scheme.service_name) + expect(page).not_to have_link(incomplete_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + end + + it "shows filtered deactivated schemes" do + get "/schemes?status[]=deactivated", headers:, params: {} + follow_redirect! + expect(page).to have_link(deactivated_scheme.service_name) + expect(page).not_to have_link(active_scheme.service_name) + expect(page).not_to have_link(incomplete_scheme.service_name) + end + + it "does not reset the filters" do + get "/schemes?status[]=incomplete", headers:, params: {} + follow_redirect! + expect(page).to have_link(incomplete_scheme.service_name) + expect(page).not_to have_link(active_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + + get "/schemes", headers:, params: {} + follow_redirect! + expect(page).to have_link(incomplete_scheme.service_name) + expect(page).not_to have_link(active_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + end + end + end end context "when signed in as a support user" do @@ -221,6 +280,59 @@ RSpec.describe SchemesController, type: :request do expect(page).to have_title("Supported housing schemes (1 scheme matching ‘#{search_param}’) - Submit social housing lettings and sales data (CORE) - GOV.UK") end end + + context "when filtering" do + context "with status filter" do + let!(:incomplete_scheme) { create(:scheme, :incomplete) } + let(:active_scheme) { create(:scheme) } + let!(:deactivated_scheme) { create(:scheme) } + + before do + create(:location, scheme: active_scheme) + create(:scheme_deactivation_period, scheme: deactivated_scheme, deactivation_date: Time.zone.local(2022, 4, 1)) + end + + it "shows schemes for multiple selected statuses" do + get "/schemes?status[]=incomplete&status[]=active", headers:, params: {} + expect(page).to have_link(incomplete_scheme.service_name) + expect(page).to have_link(active_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + end + + it "shows filtered incomplete schemes" do + get "/schemes?status[]=incomplete", headers:, params: {} + expect(page).to have_link(incomplete_scheme.service_name) + expect(page).not_to have_link(active_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + end + + it "shows filtered active schemes" do + get "/schemes?status[]=active", headers:, params: {} + expect(page).to have_link(active_scheme.service_name) + expect(page).not_to have_link(incomplete_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + end + + it "shows filtered deactivated schemes" do + get "/schemes?status[]=deactivated", headers:, params: {} + expect(page).to have_link(deactivated_scheme.service_name) + expect(page).not_to have_link(active_scheme.service_name) + expect(page).not_to have_link(incomplete_scheme.service_name) + end + + it "does not reset the filters" do + get "/schemes?status[]=incomplete", headers:, params: {} + expect(page).to have_link(incomplete_scheme.service_name) + expect(page).not_to have_link(active_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + + get "/schemes", headers:, params: {} + expect(page).to have_link(incomplete_scheme.service_name) + expect(page).not_to have_link(active_scheme.service_name) + expect(page).not_to have_link(deactivated_scheme.service_name) + end + end + end end end diff --git a/spec/views/organisations/schemes.html.erb_spec.rb b/spec/views/organisations/schemes.html.erb_spec.rb index 5c4bff422..484c197cd 100644 --- a/spec/views/organisations/schemes.html.erb_spec.rb +++ b/spec/views/organisations/schemes.html.erb_spec.rb @@ -4,10 +4,15 @@ RSpec.describe "organisations/schemes.html.erb" do context "when data provider" do let(:user) { build(:user) } + before do + session[:schemes_filters] = {}.to_json + end + it "does not render button to create schemes" do assign(:organisation, user.organisation) assign(:pagy, Pagy.new(count: 0, page: 1)) assign(:schemes, []) + assign(:filter_type, "schemes") allow(view).to receive(:current_user).and_return(user) diff --git a/spec/views/schemes/index.html.erb_spec.rb b/spec/views/schemes/index.html.erb_spec.rb index 86a8610c0..ce06177e3 100644 --- a/spec/views/schemes/index.html.erb_spec.rb +++ b/spec/views/schemes/index.html.erb_spec.rb @@ -4,9 +4,14 @@ RSpec.describe "schemes/index.html.erb" do context "when data provider" do let(:user) { build(:user) } + before do + session[:schemes_filters] = {}.to_json + end + it "does not render button to create schemes" do assign(:pagy, Pagy.new(count: 0, page: 1)) assign(:schemes, []) + assign(:filter_type, "schemes") allow(view).to receive(:current_user).and_return(user)