diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index dfe50fa22..9d3e63b33 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -4,8 +4,8 @@ class OrganisationsController < ApplicationController include DuplicateLogsHelper before_action :authenticate_user! - before_action :find_resource, except: %i[index new create] - before_action :authenticate_scope!, except: [:index] + before_action :find_resource, except: %i[index new create search] + before_action :authenticate_scope!, except: %i[index search] 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 schemes email_schemes_csv download_schemes_csv] 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] @@ -280,6 +280,17 @@ class OrganisationsController < ApplicationController render "schemes/changes" end + def search + org_options = current_user.support? ? Organisation.all : Organisation.affiliated_organisations(current_user.organisation) + organisations = org_options.search_by(params["query"]).limit(20) + + org_data = organisations.each_with_object({}) do |org, hash| + hash[org.id] = { value: org.name } + end + + render json: org_data.to_json + end + private def filter_type diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 3fe3b1813..2f7bb2bd6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -32,6 +32,17 @@ class UsersController < ApplicationController end end + def search + user_options = current_user.support? ? User.all : User.own_and_managing_org_users(current_user.organisation) + users = user_options.search_by(params["query"]).limit(20) + + user_data = users.each_with_object({}) do |user, hash| + hash[user.id] = { value: user.name, hint: user.email } + end + + render json: user_data.to_json + end + def resend_invite @user.send_confirmation_instructions flash[:notice] = "Invitation sent to #{@user.email}" diff --git a/app/frontend/controllers/index.js b/app/frontend/controllers/index.js index ece539d15..ef29b99ca 100644 --- a/app/frontend/controllers/index.js +++ b/app/frontend/controllers/index.js @@ -13,6 +13,8 @@ import GovukfrontendController from './govukfrontend_controller.js' import NumericQuestionController from './numeric_question_controller.js' +import SearchController from './search_controller.js' + import FilterLayoutController from './filter_layout_controller.js' application.register('accessible-autocomplete', AccessibleAutocompleteController) application.register('conditional-filter', ConditionalFilterController) @@ -20,3 +22,4 @@ application.register('conditional-question', ConditionalQuestionController) application.register('govukfrontend', GovukfrontendController) application.register('numeric-question', NumericQuestionController) application.register('filter-layout', FilterLayoutController) +application.register('search', SearchController) diff --git a/app/frontend/controllers/search_controller.js b/app/frontend/controllers/search_controller.js new file mode 100644 index 000000000..565b9d0d1 --- /dev/null +++ b/app/frontend/controllers/search_controller.js @@ -0,0 +1,46 @@ +import { Controller } from '@hotwired/stimulus' +import accessibleAutocomplete from 'accessible-autocomplete' +import 'accessible-autocomplete/dist/accessible-autocomplete.min.css' +import { searchSuggestion, fetchAndPopulateSearchResults, confirmSelectedOption, searchableName } from '../modules/search' + +const options = [] +const populateOptions = (results, selectEl) => { + selectEl.innerHTML = '' + + Object.keys(results).forEach((key) => { + const option = document.createElement('option') + option.value = key + option.innerHTML = results[key].value + if (results[key].hint) { option.setAttribute('data-hint', results[key].hint) } + option.setAttribute('text', searchableName(results[key])) + selectEl.appendChild(option) + options.push(option) + }) +} + +export default class extends Controller { + connect () { + const selectEl = this.element + const matches = /^(\w+)\[(\w+)\]$/.exec(selectEl.name) + const rawFieldName = matches ? `${matches[1]}[${matches[2]}_raw]` : '' + const searchUrl = JSON.parse(this.element.dataset.info).search_url + + document.querySelectorAll('.non-js-text-search-input-field').forEach((el) => { + el.style.display = 'none' + }) + + accessibleAutocomplete.enhanceSelectElement({ + defaultValue: '', + selectElement: selectEl, + minLength: 1, + source: (query, populateResults) => { + fetchAndPopulateSearchResults(query, populateResults, searchUrl, populateOptions, selectEl) + }, + autoselect: true, + placeholder: 'Start typing to search', + templates: { suggestion: (value) => searchSuggestion(value, options) }, + name: rawFieldName, + onConfirm: (val) => confirmSelectedOption(selectEl, val) + }) + } +} diff --git a/app/frontend/modules/search.js b/app/frontend/modules/search.js index 71944746e..efdf7b9d0 100644 --- a/app/frontend/modules/search.js +++ b/app/frontend/modules/search.js @@ -117,6 +117,22 @@ export const suggestion = (value, options) => { } } +export const searchSuggestion = (value, options) => { + try { + const option = options.find((o) => o.getAttribute('text') === value) + if (option) { + const result = enhanceOption(option) + const html = result.append ? `${result.text} ${result.append}` : `${result.text}` + return result.hint ? `${html}
${result.hint}
` : html + } else { + return 'No results found' + } + } catch (error) { + console.error('Error fetching user option:', error) + return value + } +} + export const enhanceOption = (option) => { return { text: option.text, @@ -128,6 +144,39 @@ export const enhanceOption = (option) => { } } +export const fetchAndPopulateSearchResults = async (query, populateResults, relativeUrlRoute, populateOptions, selectEl) => { + if (/\S/.test(query)) { + const results = await fetchUserOptions(query, relativeUrlRoute) + populateOptions(results, selectEl) + populateResults(Object.values(results).map((o) => searchableName(o))) + } +} + +export const fetchUserOptions = async (query, searchUrl) => { + try { + const response = await fetch(`${searchUrl}?query=${encodeURIComponent(query)}`) + const results = await response.json() + return results + } catch (error) { + console.error('Error fetching user options:', error) + return [] + } +} + export const getSearchableName = (option) => { return option.getAttribute('data-hint') ? option.text + ' ' + option.getAttribute('data-hint') : option.text } + +export const searchableName = (option) => { + return option.hint ? option.value + ' ' + option.hint : option.value +} + +export const confirmSelectedOption = (selectEl, val) => { + const arrayOfOptions = Array.from(selectEl.options).filter(function (option, index, arr) { return option.value !== '' }) + + const selectedOption = [].filter.call( + arrayOfOptions, + (option) => option.getAttribute('text') === val + )[0] + if (selectedOption) selectedOption.selected = true +} diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 38c15b82b..5f8488bc9 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -11,8 +11,8 @@ module FiltersHelper return true if !selected_filters.key?("owning_organisation") && filter == "owning_organisation_select" && value == :all return true if !selected_filters.key?("managing_organisation") && filter == "managing_organisation_select" && value == :all - return true if selected_filters["owning_organisation"].present? && filter == "owning_organisation_select" && value == :specific_org - return true if selected_filters["managing_organisation"].present? && filter == "managing_organisation_select" && value == :specific_org + return true if (selected_filters["owning_organisation"].present? || selected_filters["owning_organisation_text_search"].present?) && filter == "owning_organisation_select" && value == :specific_org + return true if (selected_filters["managing_organisation"].present? || selected_filters["managing_organisation_text_search"].present?) && filter == "managing_organisation_select" && value == :specific_org return false if selected_filters[filter].blank? @@ -84,16 +84,54 @@ module FiltersHelper JSON.parse(session[session_name_for(filter_type)])[filter] || "" end - def owning_organisation_filter_options(user) + def all_owning_organisation_filter_options(user) organisation_options = user.support? ? Organisation.all : ([user.organisation] + user.organisation.stock_owners + user.organisation.absorbed_organisations).uniq [OpenStruct.new(id: "", name: "Select an option")] + organisation_options.map { |org| OpenStruct.new(id: org.id, name: org.name) } end - def assigned_to_filter_options(user) + def owning_organisation_filter_options(user, filter_type) + if applied_filters(filter_type)["owning_organisation"].present? + organisation_id = applied_filters(filter_type)["owning_organisation"] + + org = if user.support? + Organisation.where(id: organisation_id)&.first + else + Organisation.affiliated_organisations(user.organisation).where(id: organisation_id)&.first + end + return [OpenStruct.new(id: org.id, name: org.name)] if org.present? + end + + [OpenStruct.new(id: "", name: "Select an option")] + end + + def assigned_to_csv_filter_options(user) user_options = user.support? ? User.all : (user.organisation.users + user.organisation.managing_agents.flat_map(&:users) + user.organisation.stock_owners.flat_map(&:users)).uniq [OpenStruct.new(id: "", name: "Select an option", hint: "")] + user_options.map { |user_option| OpenStruct.new(id: user_option.id, name: user_option.name, hint: user_option.email) } end + def assigned_to_filter_options(filter_type) + if applied_filters(filter_type)["assigned_to"] == "specific_user" && applied_filters(filter_type)["user"].present? + user_id = applied_filters(filter_type)["user"] + selected_user = if current_user.support? + User.where(id: user_id)&.first + else + User.own_and_managing_org_users(current_user.organisation).where(id: user_id)&.first + end + + return [OpenStruct.new(id: selected_user.id, name: selected_user.name, hint: selected_user.email)] if selected_user.present? + end + [OpenStruct.new(id: "", name: "Select an option", hint: "")] + end + + def filter_search_url(category) + case category + when :user + search_users_path + when :owning_organisation, :managing_organisation + search_organisations_path + end + end + def collection_year_options years = { current_collection_start_year.to_s => year_combo(current_collection_start_year), @@ -125,11 +163,26 @@ module FiltersHelper end end - def managing_organisation_filter_options(user) + def managing_organisation_csv_filter_options(user) organisation_options = user.support? ? Organisation.all : ([user.organisation] + user.organisation.managing_agents + user.organisation.absorbed_organisations).uniq [OpenStruct.new(id: "", name: "Select an option")] + organisation_options.map { |org| OpenStruct.new(id: org.id, name: org.name) } end + def managing_organisation_filter_options(user, filter_type) + if applied_filters(filter_type)["managing_organisation"].present? + organisation_id = applied_filters(filter_type)["managing_organisation"] + + org = if user.support? + Organisation.where(id: organisation_id)&.first + else + Organisation.affiliated_organisations(user.organisation).where(id: organisation_id)&.first + end + return [OpenStruct.new(id: org.id, name: org.name)] if org.present? + end + + [OpenStruct.new(id: "", name: "Select an option")] + end + def show_scheme_managing_org_filter?(user) org = user.organisation @@ -176,8 +229,8 @@ module FiltersHelper { id: "status", label: "Status", value: formatted_status_filter(session_filters) }, filter_type == "lettings_logs" ? { id: "needstype", label: "Needs type", value: formatted_needstype_filter(session_filters) } : nil, { id: "assigned_to", label: "Assigned to", value: formatted_assigned_to_filter(session_filters) }, - { id: "owned_by", label: "Owned by", value: formatted_owned_by_filter(session_filters) }, - { id: "managed_by", label: "Managed by", value: formatted_managed_by_filter(session_filters) }, + { id: "owned_by", label: "Owned by", value: formatted_owned_by_filter(session_filters, filter_type) }, + { id: "managed_by", label: "Managed by", value: formatted_managed_by_filter(session_filters, filter_type) }, ].compact end @@ -221,7 +274,7 @@ private filters.each.sum do |category, category_filters| if %w[years status needstypes bulk_upload_id].include?(category) category_filters.count(&:present?) - elsif %w[user owning_organisation managing_organisation].include?(category) + elsif %w[user owning_organisation managing_organisation user_text_search owning_organisation_text_search managing_organisation_text_search].include?(category) 1 else 0 @@ -256,26 +309,27 @@ private return "All" if session_filters["assigned_to"].include?("all") return "You" if session_filters["assigned_to"].include?("you") - selected_user_option = assigned_to_filter_options(current_user).find { |x| x.id == session_filters["user"].to_i } + User.own_and_managing_org_users(current_user.organisation).find(session_filters["user"].to_i).name + selected_user_option = User.own_and_managing_org_users(current_user.organisation).find(session_filters["user"].to_i) return unless selected_user_option - "#{selected_user_option.name} (#{selected_user_option.hint})" + "#{selected_user_option.name} (#{selected_user_option.email})" end - def formatted_owned_by_filter(session_filters) + def formatted_owned_by_filter(session_filters, filter_type) return "All" if params["id"].blank? && (session_filters["owning_organisation"].blank? || session_filters["owning_organisation"]&.include?("all")) session_org_id = session_filters["owning_organisation"] || params["id"] - selected_owning_organisation_option = owning_organisation_filter_options(current_user).find { |org| org.id == session_org_id.to_i } + selected_owning_organisation_option = owning_organisation_filter_options(current_user, filter_type).find { |org| org.id == session_org_id.to_i } return unless selected_owning_organisation_option selected_owning_organisation_option&.name end - def formatted_managed_by_filter(session_filters) + def formatted_managed_by_filter(session_filters, filter_type) return "All" if session_filters["managing_organisation"].blank? || session_filters["managing_organisation"].include?("all") - selected_managing_organisation_option = managing_organisation_filter_options(current_user).find { |org| org.id == session_filters["managing_organisation"].to_i } + selected_managing_organisation_option = managing_organisation_filter_options(current_user, filter_type).find { |org| org.id == session_filters["managing_organisation"].to_i } return unless selected_managing_organisation_option selected_managing_organisation_option&.name diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index e7a72350d..10ab612cd 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -132,6 +132,10 @@ class LettingsLog < Log illness_type_10: false) } + scope :filter_by_user_text_search, ->(param, user) { where(assigned_to: user.support? ? User.search_by(param) : User.own_and_managing_org_users(user.organisation).search_by(param)) } + scope :filter_by_owning_organisation_text_search, ->(param, _user) { where(owning_organisation: Organisation.search_by(param)) } + scope :filter_by_managing_organisation_text_search, ->(param, _user) { where(managing_organisation: Organisation.search_by(param)) } + AUTOGENERATED_FIELDS = %w[id status created_at updated_at discarded_at].freeze OPTIONAL_FIELDS = %w[tenancycode propcode chcharge].freeze RENT_TYPE_MAPPING_LABELS = { 1 => "Social Rent", 2 => "Affordable Rent", 3 => "Intermediate Rent" }.freeze diff --git a/app/models/log.rb b/app/models/log.rb index c095a8276..3a6c1e982 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -53,6 +53,9 @@ class Log < ApplicationRecord scope :filter_by_organisation, ->(org, _user = nil) { where(owning_organisation: org).or(where(managing_organisation: org)) } scope :filter_by_owning_organisation, ->(owning_organisation, _user = nil) { where(owning_organisation:) } scope :filter_by_managing_organisation, ->(managing_organisation, _user = nil) { where(managing_organisation:) } + scope :filter_by_user_text_search, ->(param, user) { where(assigned_to: user.support? ? User.search_by(param) : User.own_and_managing_org_users(user.organisation).search_by(param)) } + scope :filter_by_owning_organisation_text_search, ->(param, _user) { where(owning_organisation: Organisation.search_by(param)) } + scope :filter_by_managing_organisation_text_search, ->(param, _user) { where(managing_organisation: Organisation.search_by(param)) } attr_accessor :skip_update_status, :skip_update_uprn_confirmed, :select_best_address_match, :skip_dpo_validation diff --git a/app/models/organisation.rb b/app/models/organisation.rb index 8f77df166..65b35c24e 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -18,6 +18,7 @@ class Organisation < ApplicationRecord belongs_to :absorbing_organisation, class_name: "Organisation", optional: true has_many :absorbed_organisations, class_name: "Organisation", foreign_key: "absorbing_organisation_id" scope :visible, -> { where(discarded_at: nil) } + scope :affiliated_organisations, ->(organisation) { where(id: (organisation.child_organisations + [organisation] + organisation.parent_organisations + organisation.absorbed_organisations).map(&:id)) } def affiliated_stock_owners ids = [] diff --git a/app/models/user.rb b/app/models/user.rb index d25faaa53..c79ceb0d9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -77,6 +77,7 @@ class User < ApplicationRecord scope :deactivated, -> { where(active: false) } scope :active_status, -> { where(active: true).where.not(last_sign_in_at: nil) } scope :visible, -> { where(discarded_at: nil) } + scope :own_and_managing_org_users, ->(organisation) { where(organisation: organisation.child_organisations + [organisation]) } def lettings_logs if support? @@ -209,9 +210,9 @@ class User < ApplicationRecord def logs_filters(specific_org: false) if (support? && !specific_org) || organisation.has_managing_agents? || organisation.has_stock_owners? - %w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id] + %w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id user_text_search owning_organisation_text_search managing_organisation_text_search] else - %w[years status needstypes assigned_to user bulk_upload_id] + %w[years status needstypes assigned_to user bulk_upload_id user_text_search] end end diff --git a/app/services/filter_manager.rb b/app/services/filter_manager.rb index 8d661b3fa..9f68a097c 100644 --- a/app/services/filter_manager.rb +++ b/app/services/filter_manager.rb @@ -24,6 +24,9 @@ class FilterManager next if category == "owning_organisation" && all_orgs next if category == "managing_organisation" && all_orgs next if category == "assigned_to" + next if category == "user_text_search" && filters["assigned_to"] != "specific_user" + next if category == "owning_organisation_text_search" && all_orgs + next if category == "managing_organisation_text_search" && all_orgs logs = logs.public_send("filter_by_#{category}", values, user) end @@ -94,11 +97,19 @@ class FilterManager new_filters[filter] = params[filter] if params[filter].present? end + if params["action"] == "download_csv" + new_filters["assigned_to"] = "all" if new_filters["assigned_to"] == "specific_user" && new_filters["user_text_search"].present? + new_filters["owning_organisation_select"] = "all" if new_filters["owning_organisation_select"] == "specific_organisation" && new_filters["owning_organisation_text_search"].present? + new_filters["managing_organisation_select"] = "all" if new_filters["managing_organisation_select"] == "specific_organisation" && new_filters["managing_organisation_text_search"].present? + end new_filters = new_filters.except("owning_organisation") if params["owning_organisation_select"] == "all" new_filters = new_filters.except("managing_organisation") if params["managing_organisation_select"] == "all" new_filters = new_filters.except("user") if params["assigned_to"] == "all" new_filters["user"] = current_user.id.to_s if params["assigned_to"] == "you" + new_filters = new_filters.except("user_text_search") if params["assigned_to"] == "all" || params["assigned_to"] == "you" + new_filters = new_filters.except("owning_organisation_text_search") if params["owning_organisation_select"] == "all" + new_filters = new_filters.except("managing_organisation_text_search") if params["managing_organisation_select"] == "all" end if (filter_type.include?("schemes") || filter_type.include?("users") || filter_type.include?("scheme_locations")) && params["status"].present? diff --git a/app/views/filters/_radio_filter.html.erb b/app/views/filters/_radio_filter.html.erb index 6eb902dba..e4d23573c 100644 --- a/app/views/filters/_radio_filter.html.erb +++ b/app/views/filters/_radio_filter.html.erb @@ -10,6 +10,7 @@ collection: option[:conditional_filter][:options], category: option[:conditional_filter][:category], label: option[:conditional_filter][:label], + caption_text: option[:conditional_filter][:caption_text], secondary: true, hint_text: option[:conditional_filter][:hint_text], } %> diff --git a/app/views/filters/_text_select_filter.html.erb b/app/views/filters/_text_select_filter.html.erb new file mode 100644 index 000000000..ecc997bba --- /dev/null +++ b/app/views/filters/_text_select_filter.html.erb @@ -0,0 +1,20 @@ + +<%= f.govuk_text_field "#{category}_text_search".to_sym, + label: { text: label, hidden: secondary }, + "data-controller": "search conditional-filter", + caption: { text: caption_text }, + "data-info": { search_url: filter_search_url(category.to_sym) }.to_json, + value: selected_option("#{category}_text_search", @filter_type) %> + +<%= f.govuk_select(category.to_sym, + label: { text: label, hidden: secondary }, + "data-controller": "search conditional-filter", + "hidden": true, + "data-info": { search_url: filter_search_url(category.to_sym) }.to_json) do %> + <% collection.each do |answer| %> + + <% end %> + <% end %> diff --git a/app/views/filters/assigned_to.html.erb b/app/views/filters/assigned_to.html.erb index 9f0582fbb..778d63c8a 100644 --- a/app/views/filters/assigned_to.html.erb +++ b/app/views/filters/assigned_to.html.erb @@ -11,7 +11,8 @@ type: "select", label: "User", category: "user", - options: assigned_to_filter_options(current_user), + caption_text: "User's name or email", + options: assigned_to_csv_filter_options(current_user), }, }, }, diff --git a/app/views/filters/managed_by.html.erb b/app/views/filters/managed_by.html.erb index e3d849c9b..5d4b684f3 100644 --- a/app/views/filters/managed_by.html.erb +++ b/app/views/filters/managed_by.html.erb @@ -9,7 +9,8 @@ type: "select", label: "Managed by", category: "managing_organisation", - options: managing_organisation_filter_options(current_user), + options: managing_organisation_csv_filter_options(current_user), + caption_text: "Organisation name", }, }, }, diff --git a/app/views/filters/owned_by.html.erb b/app/views/filters/owned_by.html.erb index 7acfd459c..271b68de9 100644 --- a/app/views/filters/owned_by.html.erb +++ b/app/views/filters/owned_by.html.erb @@ -9,7 +9,8 @@ type: "select", label: "Owning Organisation", category: "owning_organisation", - options: owning_organisation_filter_options(current_user), + options: all_owning_organisation_filter_options(current_user), + caption_text: "Organisation name", }, }, }, diff --git a/app/views/logs/_log_filters.html.erb b/app/views/logs/_log_filters.html.erb index aaef70377..3beab4b6b 100644 --- a/app/views/logs/_log_filters.html.erb +++ b/app/views/logs/_log_filters.html.erb @@ -66,10 +66,11 @@ "specific_user": { label: "Specific user", conditional_filter: { - type: "select", + type: "text_select", label: "User", category: "user", - options: assigned_to_filter_options(current_user), + options: assigned_to_filter_options(@filter_type), + caption_text: "User's name or email", }, }, }, @@ -86,10 +87,11 @@ "specific_org": { label: "Specific owning organisation", conditional_filter: { - type: "select", + type: "text_select", label: "Owning Organisation", category: "owning_organisation", - options: owning_organisation_filter_options(current_user), + options: owning_organisation_filter_options(current_user, @filter_type), + caption_text: "Organisation name", }, }, }, @@ -107,10 +109,11 @@ "specific_org": { label: "Specific managing organisation", conditional_filter: { - type: "select", + type: "text_select", label: user_or_org_lettings_path? ? "Managed by" : "Reported by", category: "managing_organisation", - options: managing_organisation_filter_options(current_user), + options: managing_organisation_filter_options(current_user, @filter_type), + caption_text: "Organisation name", }, }, }, diff --git a/app/views/schemes/_scheme_filters.html.erb b/app/views/schemes/_scheme_filters.html.erb index ca0538463..51687a096 100644 --- a/app/views/schemes/_scheme_filters.html.erb +++ b/app/views/schemes/_scheme_filters.html.erb @@ -35,7 +35,7 @@ type: "select", label: "Owning Organisation", category: "owning_organisation", - options: owning_organisation_filter_options(current_user), + options: all_owning_organisation_filter_options(current_user), }, }, }, diff --git a/config/routes.rb b/config/routes.rb index e0d9631e9..faea457fe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -125,6 +125,10 @@ Rails.application.routes.draw do get "edit-dpo", to: "users#dpo" get "edit-key-contact", to: "users#key_contact" + collection do + get :search + end + member do get "deactivate", to: "users#deactivate" get "reactivate", to: "users#reactivate" @@ -191,6 +195,10 @@ Rails.application.routes.draw do get "delete-confirmation", to: "organisations#delete_confirmation" delete "delete", to: "organisations#delete" end + + collection do + get :search + end end resources :merge_requests, path: "/merge-request" do diff --git a/spec/features/lettings_log_spec.rb b/spec/features/lettings_log_spec.rb index 2b977fdd7..ac9a1e4a8 100644 --- a/spec/features/lettings_log_spec.rb +++ b/spec/features/lettings_log_spec.rb @@ -89,9 +89,9 @@ RSpec.describe "Lettings Log Features" do check("In progress") choose("You") choose("Specific owning organisation") - select(stock_owner_1.name, from: "owning_organisation") + fill_in("owning-organisation-text-search-field", with: "stock") choose("Specific managing organisation") - select(managing_agent_1.name, from: "managing_organisation") + fill_in("managing-organisation-text-search-field", with: "managing") click_button("Apply filters") end diff --git a/spec/features/organisation_spec.rb b/spec/features/organisation_spec.rb index 65f787c2a..1867285eb 100644 --- a/spec/features/organisation_spec.rb +++ b/spec/features/organisation_spec.rb @@ -199,14 +199,14 @@ RSpec.describe "User Features" do it "can filter lettings logs by year" do check("years-2022-field") click_button("Apply filters") - expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&years[]=2022&status[]=&needstypes[]=&assigned_to=all&user=&owning_organisation_select=all&owning_organisation=&managing_organisation_select=all&managing_organisation=") + expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&years[]=2022&status[]=&needstypes[]=&assigned_to=all&user_text_search=&user=&owning_organisation_select=all&owning_organisation_text_search=&owning_organisation=&managing_organisation_select=all&managing_organisation_text_search=&managing_organisation=") expect(page).not_to have_link first_log.id.to_s, href: "/lettings-logs/#{first_log.id}" end it "can filter lettings logs by needstype" do check("needstypes-1-field") click_button("Apply filters") - expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&status[]=&needstypes[]=&needstypes[]=1&assigned_to=all&user=&owning_organisation_select=all&owning_organisation=&managing_organisation_select=all&managing_organisation=") + expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&status[]=&needstypes[]=&needstypes[]=1&assigned_to=all&user_text_search=&user=&owning_organisation_select=all&owning_organisation_text_search=&owning_organisation=&managing_organisation_select=all&managing_organisation_text_search=&managing_organisation=") other_general_needs_logs.each do |general_needs_log| expect(page).to have_link general_needs_log.id.to_s, href: "/lettings-logs/#{general_needs_log.id}" end @@ -245,7 +245,7 @@ RSpec.describe "User Features" do end check("years-2022-field") click_button("Apply filters") - expect(page).to have_current_path("/organisations/#{org_id}/sales-logs?years[]=&years[]=2022&status[]=&assigned_to=all&user=&owning_organisation_select=all&owning_organisation=&managing_organisation_select=all&managing_organisation=") + expect(page).to have_current_path("/organisations/#{org_id}/sales-logs?years[]=&years[]=2022&status[]=&assigned_to=all&user_text_search=&user=&owning_organisation_select=all&owning_organisation_text_search=&owning_organisation=&managing_organisation_select=all&managing_organisation_text_search=&managing_organisation=") expect(page).not_to have_link first_log.id.to_s, href: "/sales-logs/#{first_log.id}" end end diff --git a/spec/features/user_spec.rb b/spec/features/user_spec.rb index 169465cb1..c30abe1e9 100644 --- a/spec/features/user_spec.rb +++ b/spec/features/user_spec.rb @@ -796,12 +796,13 @@ RSpec.describe "User Features" do visit("/lettings-logs") choose("owning-organisation-select-specific-org-field", allow_label_click: true) expect(page).to have_field("owning-organisation-field", with: "") - find("#owning-organisation-field").click.native.send_keys("F", "i", "l", "t", :down, :enter) + find("#owning-organisation-field").click.native.send_keys("F", "i", "l", "t") + select(parent_organisation.name, from: "owning-organisation-field-select", visible: false) click_button("Apply filters") - expect(page).to have_current_path("/lettings-logs?%5Byears%5D%5B%5D=&%5Bstatus%5D%5B%5D=&%5Bneedstypes%5D%5B%5D=&assigned_to=all&owning_organisation_select=specific_org&owning_organisation=#{parent_organisation.id}&managing_organisation_select=all") + expect(page).to have_current_path("/lettings-logs?%5Byears%5D%5B%5D=&%5Bstatus%5D%5B%5D=&%5Bneedstypes%5D%5B%5D=&assigned_to=all&user_text_search=&owning_organisation_select=specific_org&owning_organisation_text_search=&owning_organisation=#{parent_organisation.id}&managing_organisation_select=all&managing_organisation_text_search=") choose("owning-organisation-select-all-field", allow_label_click: true) click_button("Apply filters") - expect(page).to have_current_path("/lettings-logs?%5Byears%5D%5B%5D=&%5Bstatus%5D%5B%5D=&%5Bneedstypes%5D%5B%5D=&assigned_to=all&owning_organisation_select=all&managing_organisation_select=all") + expect(page).to have_current_path("/lettings-logs?%5Byears%5D%5B%5D=&%5Bstatus%5D%5B%5D=&%5Bneedstypes%5D%5B%5D=&assigned_to=all&user_text_search=&owning_organisation_select=all&owning_organisation_text_search=&managing_organisation_select=all&managing_organisation_text_search=") end end end diff --git a/spec/helpers/filters_helper_spec.rb b/spec/helpers/filters_helper_spec.rb index f04157521..c57f92311 100644 --- a/spec/helpers/filters_helper_spec.rb +++ b/spec/helpers/filters_helper_spec.rb @@ -175,27 +175,146 @@ RSpec.describe FiltersHelper do context "with a support user" do let(:user) { FactoryBot.create(:user, :support, organisation: child_organisation) } - it "returns a list of all organisations" do - expect(owning_organisation_filter_options(user)).to match_array([ - OpenStruct.new(id: "", name: "Select an option"), - OpenStruct.new(id: child_organisation.id, name: "Child organisation"), - OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"), - OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"), - OpenStruct.new(id: 99, name: "Other organisation"), - ]) + context "when no organisation is selected in the filters" do + it "returns an empty list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end + end + + context "when a specific child organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "owning_organisation": child_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: child_organisation.id, name: "Child organisation"), + ]) + end + end + + context "when a specific parent organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "owning_organisation": parent_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"), + ]) + end + end + + context "when a specific absorbed organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "owning_organisation": absorbed_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"), + ]) + end + end + + context "when a specific non related organisation is selected in the filters" do + let(:unrelated_organisation) { create(:organisation, name: "Unrelated organisation") } + + before do + session[:lettings_logs_filters] = { "owning_organisation": unrelated_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: unrelated_organisation.id, name: "Unrelated organisation"), + ]) + end + end + + context "when a non existing organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "owning_organisation": 143_542_542 }.to_json + end + + it "returns an empty list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end end end context "with a data coordinator user" do let(:user) { FactoryBot.create(:user, :data_coordinator, organisation: child_organisation) } - it "returns a list of parent orgs and your own organisation" do - expect(owning_organisation_filter_options(user.reload)).to eq([ - OpenStruct.new(id: "", name: "Select an option"), - OpenStruct.new(id: child_organisation.id, name: "Child organisation"), - OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"), - OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"), - ]) + context "when no organisation is selected in the filters" do + it "returns an empty list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end + end + + context "when a specific child organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "owning_organisation": child_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: child_organisation.id, name: "Child organisation"), + ]) + end + end + + context "when a specific parent organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "owning_organisation": parent_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"), + ]) + end + end + + context "when a specific absorbed organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "owning_organisation": absorbed_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"), + ]) + end + end + + context "when a specific non related organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "owning_organisation": create(:organisation).id }.to_json + end + + it "returns an empty list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end + end + + context "when a non existing organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "owning_organisation": 143_542_542 }.to_json + end + + it "returns an empty list" do + expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end end end end @@ -214,27 +333,146 @@ RSpec.describe FiltersHelper do context "with a support user" do let(:user) { FactoryBot.create(:user, :support, organisation: parent_organisation) } - it "returns a list of all organisations" do - expect(managing_organisation_filter_options(user)).to eq([ - OpenStruct.new(id: "", name: "Select an option"), - OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"), - OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"), - OpenStruct.new(id: child_organisation.id, name: "Child organisation"), - OpenStruct.new(id: 99, name: "Other organisation"), - ]) + context "when no organisation is selected in the filters" do + it "returns an empty list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end + end + + context "when a specific child organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "managing_organisation": child_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: child_organisation.id, name: "Child organisation"), + ]) + end + end + + context "when a specific parent organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "managing_organisation": parent_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"), + ]) + end + end + + context "when a specific absorbed organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "managing_organisation": absorbed_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"), + ]) + end + end + + context "when a specific non related organisation is selected in the filters" do + let(:unrelated_organisation) { create(:organisation, name: "Unrelated organisation") } + + before do + session[:lettings_logs_filters] = { "managing_organisation": unrelated_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: unrelated_organisation.id, name: "Unrelated organisation"), + ]) + end + end + + context "when a non existing organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "managing_organisation": 143_542_542 }.to_json + end + + it "returns an empty list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end end end context "with a data coordinator user" do let(:user) { FactoryBot.create(:user, :data_coordinator, organisation: parent_organisation) } - it "returns a list of child orgs and your own organisation" do - expect(managing_organisation_filter_options(user.reload)).to eq([ - OpenStruct.new(id: "", name: "Select an option"), - OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"), - OpenStruct.new(id: child_organisation.id, name: "Child organisation"), - OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"), - ]) + context "when no organisation is selected in the filters" do + it "returns an empty list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end + end + + context "when a specific child organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "managing_organisation": child_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: child_organisation.id, name: "Child organisation"), + ]) + end + end + + context "when a specific parent organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "managing_organisation": parent_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"), + ]) + end + end + + context "when a specific absorbed organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "managing_organisation": absorbed_organisation.id }.to_json + end + + it "returns the selected organisation in the list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"), + ]) + end + end + + context "when a specific non related organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "managing_organisation": create(:organisation).id }.to_json + end + + it "returns an empty list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end + end + + context "when a non existing organisation is selected in the filters" do + before do + session[:lettings_logs_filters] = { "managing_organisation": 143_542_542 }.to_json + end + + it "returns an empty list" do + expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([ + OpenStruct.new(id: "", name: "Select an option"), + ]) + end end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index edb998ac3..6a04e9a0b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -164,7 +164,7 @@ RSpec.describe User, type: :model do end it "can filter lettings logs by user, year and status" do - expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user bulk_upload_id]) + expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user bulk_upload_id user_text_search]) end end @@ -174,7 +174,7 @@ RSpec.describe User, type: :model do end it "can filter lettings logs by user, year, status, managing_organisation and owning_organisation" do - expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id]) + expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id managing_organisation_text_search owning_organisation_text_search user_text_search]) end end end @@ -215,7 +215,7 @@ RSpec.describe User, type: :model do end it "can filter lettings logs by user, year, status, managing_organisation and owning_organisation" do - expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user owning_organisation managing_organisation bulk_upload_id]) + expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user owning_organisation managing_organisation bulk_upload_id managing_organisation_text_search owning_organisation_text_search user_text_search]) end end diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb index 13879a38c..d3e8a8155 100644 --- a/spec/requests/organisations_controller_spec.rb +++ b/spec/requests/organisations_controller_spec.rb @@ -75,6 +75,13 @@ RSpec.describe OrganisationsController, type: :request do end end end + + describe "#search" do + it "redirects to the sign in page" do + get "/organisations/search" + expect(response).to redirect_to("/account/sign-in") + end + end end context "when user is signed in" do @@ -807,6 +814,25 @@ RSpec.describe OrganisationsController, type: :request do end end end + + describe "#search" do + let(:parent_organisation) { create(:organisation, name: "parent test organisation") } + let(:child_organisation) { create(:organisation, name: "child test organisation") } + + before do + user.organisation.update!(name: "test organisation") + create(:organisation_relationship, parent_organisation: user.organisation, child_organisation:) + create(:organisation_relationship, child_organisation: user.organisation, parent_organisation:) + create(:organisation, name: "other organisation test organisation") + end + + it "only searches within the current user's organisation, managing agents and stock owners" do + get "/organisations/search", headers:, params: { query: "test organisation" } + result = JSON.parse(response.body) + expect(result.count).to eq(3) + expect(result.keys).to match_array([user.organisation.id.to_s, parent_organisation.id.to_s, child_organisation.id.to_s]) + end + end end context "with a data provider user" do @@ -2077,6 +2103,25 @@ RSpec.describe OrganisationsController, type: :request do end end end + + describe "#search" do + let(:parent_organisation) { create(:organisation, name: "parent test organisation") } + let(:child_organisation) { create(:organisation, name: "child test organisation") } + let!(:other_organisation) { create(:organisation, name: "other organisation test organisation") } + + before do + user.organisation.update!(name: "test organisation") + create(:organisation_relationship, parent_organisation: user.organisation, child_organisation:) + create(:organisation_relationship, child_organisation: user.organisation, parent_organisation:) + end + + it "searches within all the organisations" do + get "/organisations/search", headers:, params: { query: "test organisation" } + result = JSON.parse(response.body) + expect(result.count).to eq(4) + expect(result.keys).to match_array([user.organisation.id.to_s, parent_organisation.id.to_s, child_organisation.id.to_s, other_organisation.id.to_s]) + end + end end end diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index bb0a1cca3..8e87f7f28 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -117,6 +117,13 @@ RSpec.describe UsersController, type: :request do expect(response).to redirect_to("/account/sign-in") end end + + describe "#search" do + it "redirects to the sign in page" do + get "/users/search" + expect(response).to redirect_to("/account/sign-in") + end + end end context "when user is signed in as a data provider" do @@ -404,6 +411,25 @@ RSpec.describe UsersController, type: :request do expect(response).to have_http_status(:unauthorized) end end + + describe "#search" do + let(:parent_relationship) { create(:organisation_relationship, parent_organisation: user.organisation) } + let(:child_relationship) { create(:organisation_relationship, child_organisation: user.organisation) } + let!(:org_user) { create(:user, organisation: user.organisation, name: "test_name") } + let!(:managing_user) { create(:user, organisation: parent_relationship.child_organisation, name: "managing_agent_test_name") } + + before do + create(:user, organisation: child_relationship.parent_organisation, name: "stock_owner_test_name") + create(:user, name: "other_organisation_test_name") + end + + it "only searches within the current user's organisation and managing agents" do + get "/users/search", headers:, params: { query: "test_name" } + result = JSON.parse(response.body) + expect(result.count).to eq(2) + expect(result.keys).to match_array([org_user.id.to_s, managing_user.id.to_s]) + end + end end context "when user is signed in as a data coordinator" do @@ -1174,6 +1200,25 @@ RSpec.describe UsersController, type: :request do expect(response).to have_http_status(:unauthorized) end end + + describe "#search" do + let(:parent_relationship) { create(:organisation_relationship, parent_organisation: user.organisation) } + let(:child_relationship) { create(:organisation_relationship, child_organisation: user.organisation) } + let!(:org_user) { create(:user, organisation: user.organisation, email: "test_name@example.com") } + let!(:managing_user) { create(:user, organisation: parent_relationship.child_organisation, email: "managing_agent_test_name@example.com") } + + before do + create(:user, email: "other_organisation_test_name@example.com") + create(:user, organisation: child_relationship.parent_organisation, email: "stock_owner_test_name@example.com") + end + + it "only searches within the current user's organisation and managing agents" do + get "/users/search", headers:, params: { query: "test_name" } + result = JSON.parse(response.body) + expect(result.count).to eq(2) + expect(result.keys).to match_array([org_user.id.to_s, managing_user.id.to_s]) + end + end end context "when user is signed in as a support user" do @@ -2111,6 +2156,22 @@ RSpec.describe UsersController, type: :request do expect(page).not_to have_link("User to be deleted") end end + + describe "#search" do + let(:parent_relationship) { create(:organisation_relationship, parent_organisation: user.organisation) } + let(:child_relationship) { create(:organisation_relationship, child_organisation: user.organisation) } + let!(:org_user) { create(:user, organisation: user.organisation, name: "test_name") } + let!(:managing_user) { create(:user, organisation: child_relationship.parent_organisation, name: "stock_owner_test_name") } + let!(:owner_user) { create(:user, organisation: parent_relationship.child_organisation, name: "managing_agent_test_name") } + let!(:other_user) { create(:user, name: "other_organisation_test_name") } + + it "searches all users" do + get "/users/search", headers:, params: { query: "test_name" } + result = JSON.parse(response.body) + expect(result.count).to eq(4) + expect(result.keys).to match_array([org_user.id.to_s, managing_user.id.to_s, owner_user.id.to_s, other_user.id.to_s]) + end + end end describe "title link" do