diff --git a/app/controllers/locations_controller.rb b/app/controllers/locations_controller.rb index 797e79544..6cd40161f 100644 --- a/app/controllers/locations_controller.rb +++ b/app/controllers/locations_controller.rb @@ -21,7 +21,23 @@ class LocationsController < ApplicationController def show; end def deactivate - render "toggle_active", locals: { action: "deactivate" } + if params[:location].blank? + render "toggle_active", locals: { action: "deactivate" } + elsif params[:location][:confirm].present? && params[:location][:deactivation_date].present? + confirm_deactivation + else + deactivation_date_errors + if @location.errors.present? + @location.deactivation_date_type = params[:location][:deactivation_date_type] + render "toggle_active", locals: { action: "deactivate" }, status: :unprocessable_entity + else + render "toggle_active_confirm", locals: { action: "deactivate", deactivation_date: } + end + end + end + + def reactivate + render "toggle_active", locals: { action: "reactivate" } end def create @@ -128,7 +144,7 @@ private end def authenticate_action! - if %w[new edit update create index edit_name edit_local_authority].include?(action_name) && !((current_user.organisation == @scheme&.owning_organisation) || current_user.support?) + if %w[new edit update create index edit_name edit_local_authority deactivate].include?(action_name) && !((current_user.organisation == @scheme&.owning_organisation) || current_user.support?) render_not_found and return end end @@ -146,4 +162,49 @@ private def valid_location_admin_district?(location_params) location_params["location_admin_district"] != "Select an option" end + + def confirm_deactivation + if @location.update(deactivation_date: params[:location][:deactivation_date]) + flash[:notice] = "#{@location.name || @location.postcode} has been deactivated" + end + redirect_to scheme_location_path(@scheme, @location) + end + + def deactivation_date_errors + if params[:location][:deactivation_date].blank? && params[:location][:deactivation_date_type].blank? + @location.errors.add(:deactivation_date_type, message: I18n.t("validations.location.deactivation_date.not_selected")) + end + + if params[:location][:deactivation_date_type] == "other" + day = params[:location]["deactivation_date(3i)"] + month = params[:location]["deactivation_date(2i)"] + year = params[:location]["deactivation_date(1i)"] + + collection_start_date = FormHandler.instance.current_collection_start_date + + if [day, month, year].any?(&:blank?) + { day:, month:, year: }.each do |period, value| + @location.errors.add(:deactivation_date, message: I18n.t("validations.location.deactivation_date.not_entered", period: period.to_s)) if value.blank? + end + elsif !Date.valid_date?(year.to_i, month.to_i, day.to_i) + @location.errors.add(:deactivation_date, message: I18n.t("validations.location.deactivation_date.invalid")) + elsif !Date.new(year.to_i, month.to_i, day.to_i).between?(collection_start_date, Date.new(2200, 1, 1)) + @location.errors.add(:deactivation_date, message: I18n.t("validations.location.deactivation_date.out_of_range", date: collection_start_date.to_formatted_s(:govuk_date))) + end + end + end + + def deactivation_date + return if params[:location].blank? + + collection_start_date = FormHandler.instance.current_collection_start_date + return collection_start_date if params[:location][:deactivation_date_type] == "default" + return params[:location][:deactivation_date] if params[:location][:deactivation_date_type].blank? + + day = params[:location]["deactivation_date(3i)"] + month = params[:location]["deactivation_date(2i)"] + year = params[:location]["deactivation_date(1i)"] + + Date.new(year.to_i, month.to_i, day.to_i) + end end diff --git a/app/frontend/controllers/conditional_question_controller.js b/app/frontend/controllers/conditional_question_controller.js index fb52d07c1..d24d5c6c3 100644 --- a/app/frontend/controllers/conditional_question_controller.js +++ b/app/frontend/controllers/conditional_question_controller.js @@ -10,14 +10,14 @@ export default class extends Controller { const selectedValue = this.element.value const dataInfo = JSON.parse(this.element.dataset.info) const conditionalFor = dataInfo.conditional_questions - const logType = dataInfo.log_type + const type = dataInfo.type Object.entries(conditionalFor).forEach(([targetQuestion, conditions]) => { if (!conditions.map(String).includes(String(selectedValue))) { - const textNumericInput = document.getElementById(`${logType}-log-${targetQuestion.replaceAll('_', '-')}-field`) + const textNumericInput = document.getElementById(`${type}-${targetQuestion.replaceAll('_', '-')}-field`) if (textNumericInput == null) { const dateInputs = [1, 2, 3].map((idx) => { - return document.getElementById(`${logType}_log_${targetQuestion}_${idx}i`) + return document.getElementById(`${type.replaceAll('-', '_')}_${targetQuestion}_${idx}i`) }) this.clearDateInputs(dateInputs) } else { diff --git a/app/helpers/question_attribute_helper.rb b/app/helpers/question_attribute_helper.rb index 857ce5eb1..020ea1909 100644 --- a/app/helpers/question_attribute_helper.rb +++ b/app/helpers/question_attribute_helper.rb @@ -7,6 +7,14 @@ module QuestionAttributeHelper merge_controller_attributes(*attribs) end + def basic_conditional_html_attributes(conditional_for, type) + { + "data-controller": "conditional-question", + "data-action": "click->conditional-question#displayConditional", + "data-info": { conditional_questions: conditional_for, type: type }.to_json, + } + end + private def numeric_question_html_attributes(question) @@ -27,7 +35,7 @@ private { "data-controller": "conditional-question", "data-action": "click->conditional-question#displayConditional", - "data-info": { conditional_questions: question.conditional_for, log_type: question.form.type }.to_json, + "data-info": { conditional_questions: question.conditional_for, type: "#{question.form.type}-log" }.to_json, } end end diff --git a/app/helpers/tag_helper.rb b/app/helpers/tag_helper.rb index 08b6180b3..2ea23a86a 100644 --- a/app/helpers/tag_helper.rb +++ b/app/helpers/tag_helper.rb @@ -7,6 +7,8 @@ module TagHelper in_progress: "In progress", completed: "Completed", active: "Active", + deactivating_soon: "Deactivating soon", + deactivated: "Deactivated", }.freeze COLOUR = { @@ -15,6 +17,8 @@ module TagHelper in_progress: "blue", completed: "green", active: "green", + deactivating_soon: "yellow", + deactivated: "grey", }.freeze def status_tag(status, classes = []) diff --git a/app/models/form_handler.rb b/app/models/form_handler.rb index c6dde13ab..c080e6e9b 100644 --- a/app/models/form_handler.rb +++ b/app/models/form_handler.rb @@ -49,6 +49,10 @@ class FormHandler today < window_end_date ? today.year - 1 : today.year end + def current_collection_start_date + Time.utc(current_collection_start_year, 4, 1) + end + def form_name_from_start_year(year, type) form_mappings = { 0 => "current_#{type}", 1 => "previous_#{type}", -1 => "next_#{type}" } form_mappings[current_collection_start_year - year] diff --git a/app/models/location.rb b/app/models/location.rb index 0fdc24a8e..ab1c3620a 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -2,6 +2,7 @@ class Location < ApplicationRecord validate :validate_postcode validates :units, :type_of_unit, :mobility_type, presence: true belongs_to :scheme + has_many :lettings_logs, class_name: "LettingsLog" has_paper_trail @@ -9,7 +10,7 @@ class Location < ApplicationRecord auto_strip_attributes :name - attr_accessor :add_another_location + attr_accessor :add_another_location, :deactivation_date_type scope :search_by_postcode, ->(postcode) { where("REPLACE(postcode, ' ', '') ILIKE ?", "%#{postcode.delete(' ')}%") } scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") } @@ -372,7 +373,9 @@ class Location < ApplicationRecord end def status - "active" + return :active if deactivation_date.blank? + return :deactivating_soon if Time.zone.now < deactivation_date + return :deactivated if Time.zone.now >= deactivation_date end private diff --git a/app/views/locations/show.html.erb b/app/views/locations/show.html.erb index fdceb77ad..ccc0163c2 100644 --- a/app/views/locations/show.html.erb +++ b/app/views/locations/show.html.erb @@ -24,5 +24,9 @@ <% if FeatureToggle.location_toggle_enabled? %> - <%= govuk_button_link_to "Deactivate this location", scheme_location_deactivate_path(scheme_id: @scheme.id, location_id: @location.id), warning: true %> + <% if @location.status == :active %> + <%= govuk_button_link_to "Deactivate this location", scheme_location_deactivate_path(scheme_id: @scheme.id, location_id: @location.id), warning: true %> + <% else %> + <%= govuk_button_link_to "Reactivate this location", scheme_location_reactivate_path(scheme_id: @scheme.id, location_id: @location.id) %> + <% end %> <% end %> diff --git a/app/views/locations/toggle_active.html.erb b/app/views/locations/toggle_active.html.erb index e69de29bb..96cda6daa 100644 --- a/app/views/locations/toggle_active.html.erb +++ b/app/views/locations/toggle_active.html.erb @@ -0,0 +1,39 @@ +<% title = "#{action.humanize} #{@location.postcode}" %> +<% content_for :title, title %> + +<% content_for :before_content do %> + <%= govuk_back_link( + text: "Back", + href: scheme_location_path(scheme_id: @location.scheme.id, id: @location.id), + ) %> +<% end %> + +<%= form_with model: @location, url: scheme_location_deactivate_path(scheme_id: @location.scheme.id, location_id: @location.id), method: "patch", local: true do |f| %> +
+
+ <% collection_start_date = FormHandler.instance.current_collection_start_date %> + <%= f.govuk_error_summary %> + <%= f.govuk_radio_buttons_fieldset :deactivation_date_type, + legend: { text: I18n.t("questions.location.deactivation.apply_from") }, + caption: { text: "Deactivate #{@location.postcode}" }, + hint: { text: I18n.t("hints.location.deactivation", date: collection_start_date.to_formatted_s(:govuk_date)) } do %> + <%= govuk_warning_text text: I18n.t("warnings.location.deactivation.existing_logs") %> + <%= f.govuk_radio_button :deactivation_date_type, + "default", + label: { text: "From the start of the current collection period (#{collection_start_date.to_formatted_s(:govuk_date)})" } %> + + <%= f.govuk_radio_button :deactivation_date_type, + "other", + label: { text: "For tenancies starting after a certain date" }, + **basic_conditional_html_attributes({ "deactivation_date" => %w[other] }, "location") do %> + <%= f.govuk_date_field :deactivation_date, + legend: { text: "Date", size: "m" }, + hint: { text: "For example, 27 3 2008" }, + width: 20 %> + <% end %> + <% end %> + + <%= f.govuk_submit "Continue" %> +
+
+<% end %> diff --git a/app/views/locations/toggle_active_confirm.html.erb b/app/views/locations/toggle_active_confirm.html.erb new file mode 100644 index 000000000..452d66e48 --- /dev/null +++ b/app/views/locations/toggle_active_confirm.html.erb @@ -0,0 +1,16 @@ +<%= form_with model: @location, url: scheme_location_deactivate_path(@location), method: "patch", local: true do |f| %> + <% content_for :before_content do %> + <%= govuk_back_link(href: :back) %> + <% end %> +

+ <%= @location.postcode %> + <%= "This change will affect #{@location.lettings_logs.count} logs" %> +

+ <%= govuk_warning_text text: I18n.t("warnings.location.deactivation.review_logs") %> + <%= f.hidden_field :confirm, value: true %> + <%= f.hidden_field :deactivation_date, value: deactivation_date %> +
+ <%= f.govuk_submit "Deactivate this location" %> + <%= govuk_button_link_to "Cancel", scheme_location_path(scheme_id: @scheme, id: @location.id), html: { method: :get }, secondary: true %> +
+ <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 9a45d96dd..3c4808696 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -312,6 +312,13 @@ en: declaration: missing: "You must show the DLUHC privacy notice to the tenant before you can submit this log." + location: + deactivation_date: + not_selected: "Select one of the options" + not_entered: "Enter a %{period}" + invalid: "Enter a valid date" + out_of_range: "The date must be on or after the %{date}" + soft_validations: net_income: title_text: "Net income is outside the expected range based on the lead tenant’s working situation" @@ -362,6 +369,8 @@ en: startdate: "When did the first property in this location become available under this scheme? (optional)" add_another_location: "Do you want to add another location?" mobility_type: "What are the mobility standards for the majority of units in this location?" + deactivation: + apply_from: "When should this change apply?" descriptions: location: mobility_type: @@ -374,6 +383,13 @@ en: postcode: "For example, SW1P 4DF." name: "This is how you refer to this location within your organisation" units: "A unit can be a bedroom in a shared house or flat, or a house with 4 bedrooms. Do not include bedrooms used for wardens, managers, volunteers or sleep-in staff." + deactivation: "If the date is before %{date}, select ‘From the start of the current collection period’ because the previous period has now closed." + + warnings: + location: + deactivation: + existing_logs: "It will not be possible to add logs with this location if their tenancy start date is on or after the date you enter. Any existing logs may be affected." + review_logs: "Your data providers will need to review these logs and answer a few questions again. We’ll email each log creator with a list of logs that need updating." test: one_argument: "This is based on the tenant’s work situation: %{ecstat1}" diff --git a/config/routes.rb b/config/routes.rb index ad088fe11..8bd78d6e6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,6 +54,8 @@ Rails.application.routes.draw do get "edit-name", to: "locations#edit_name" get "edit-local-authority", to: "locations#edit_local_authority" get "deactivate", to: "locations#deactivate" + get "reactivate", to: "locations#reactivate" + patch "deactivate", to: "locations#deactivate" end end diff --git a/db/migrate/20221109122033_add_deactivation_date_to_locations.rb b/db/migrate/20221109122033_add_deactivation_date_to_locations.rb new file mode 100644 index 000000000..3360bfc7c --- /dev/null +++ b/db/migrate/20221109122033_add_deactivation_date_to_locations.rb @@ -0,0 +1,5 @@ +class AddDeactivationDateToLocations < ActiveRecord::Migration[7.0] + def change + add_column :locations, :deactivation_date, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 43fc94775..ea4b59670 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_10_19_082625) do +ActiveRecord::Schema[7.0].define(version: 2022_11_09_122033) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -260,6 +260,7 @@ ActiveRecord::Schema[7.0].define(version: 2022_10_19_082625) do t.datetime "startdate", precision: nil t.string "location_admin_district" t.boolean "confirmed" + t.datetime "deactivation_date" t.index ["old_id"], name: "index_locations_on_old_id", unique: true t.index ["scheme_id"], name: "index_locations_on_scheme_id" end diff --git a/spec/helpers/locations_helper_spec.rb b/spec/helpers/locations_helper_spec.rb index 9a4550912..f2a5a67a5 100644 --- a/spec/helpers/locations_helper_spec.rb +++ b/spec/helpers/locations_helper_spec.rb @@ -60,7 +60,7 @@ RSpec.describe LocationsHelper do { name: "Mobility type", value: location.mobility_type }, { name: "Code", value: location.location_code }, { name: "Availability", value: "Available from 8 August 2022" }, - { name: "Status", value: "active" }, + { name: "Status", value: :active }, ] expect(display_attributes(location)).to eq(attributes) diff --git a/spec/helpers/question_attribute_helper_spec.rb b/spec/helpers/question_attribute_helper_spec.rb index 2be903535..9d769cdf6 100644 --- a/spec/helpers/question_attribute_helper_spec.rb +++ b/spec/helpers/question_attribute_helper_spec.rb @@ -48,7 +48,7 @@ RSpec.describe QuestionAttributeHelper do "data-action": "input->numeric-question#calculateFields click->conditional-question#displayConditional", "data-target": "lettings-log-#{question.result_field.to_s.dasherize}-field", "data-calculated": question.fields_to_add.to_json, - "data-info": { conditional_questions: question.conditional_for, log_type: "lettings" }.to_json, + "data-info": { conditional_questions: question.conditional_for, type: "lettings-log" }.to_json, } end diff --git a/spec/models/form_handler_spec.rb b/spec/models/form_handler_spec.rb index 81a9b1c45..c6a47c107 100644 --- a/spec/models/form_handler_spec.rb +++ b/spec/models/form_handler_spec.rb @@ -118,6 +118,10 @@ RSpec.describe FormHandler do it "returns the correct next sales form name" do expect(form_handler.form_name_from_start_year(2023, "sales")).to eq("next_sales") end + + it "returns the correct current start date" do + expect(form_handler.current_collection_start_date).to eq(Time.utc(2022, 4, 1)) + end end context "with the date before 1st of April" do diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb index 856575932..8f9f452bc 100644 --- a/spec/models/location_spec.rb +++ b/spec/models/location_spec.rb @@ -111,4 +111,36 @@ RSpec.describe Location, type: :model do end end end + + describe "status" do + let(:location) { FactoryBot.build(:location) } + + before do + Timecop.freeze(2022, 6, 7) + end + + it "returns active if the location is not deactivated" do + location.deactivation_date = nil + location.save! + expect(location.status).to eq(:active) + end + + it "returns deactivating soon if deactivation_date is in the future" do + location.deactivation_date = Time.zone.local(2022, 8, 8) + location.save! + expect(location.status).to eq(:deactivating_soon) + end + + it "returns deactivated if deactivation_date is in the past" do + location.deactivation_date = Time.zone.local(2022, 4, 8) + location.save! + expect(location.status).to eq(:deactivated) + end + + it "returns deactivated if deactivation_date is today" do + location.deactivation_date = Time.zone.local(2022, 6, 7) + location.save! + expect(location.status).to eq(:deactivated) + end + end end diff --git a/spec/requests/locations_controller_spec.rb b/spec/requests/locations_controller_spec.rb index 96b3d1b21..feeae2b99 100644 --- a/spec/requests/locations_controller_spec.rb +++ b/spec/requests/locations_controller_spec.rb @@ -1212,4 +1212,189 @@ RSpec.describe LocationsController, type: :request do end end end + + describe "#deactivate" do + context "when not signed in" do + it "redirects to the sign in page" do + patch "/schemes/1/locations/1/deactivate" + expect(response).to redirect_to("/account/sign-in") + end + end + + context "when signed in as a data provider" do + let(:user) { FactoryBot.create(:user) } + + before do + sign_in user + patch "/schemes/1/locations/1/deactivate" + end + + it "returns 401 unauthorized" do + request + expect(response).to have_http_status(:unauthorized) + end + end + + context "when signed in as a data coordinator" do + let(:user) { FactoryBot.create(:user, :data_coordinator) } + let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) } + let!(:location) { FactoryBot.create(:location, scheme:) } + let(:startdate) { Time.utc(2021, 1, 2) } + let(:deactivation_date) { Time.utc(2022, 10, 10) } + + before do + Timecop.freeze(Time.utc(2022, 10, 10)) + sign_in user + patch "/schemes/#{scheme.id}/locations/#{location.id}/deactivate", params: + end + + context "with default date" do + let(:params) { { location: { deactivation_date_type: "default" } } } + + it "renders the confirmation page" do + expect(response).to have_http_status(:ok) + expect(page).to have_content("This change will affect #{location.lettings_logs.count} logs") + end + end + + context "with other date" do + let(:params) { { location: { deactivation_date: "other", "deactivation_date(3i)": "10", "deactivation_date(2i)": "10", "deactivation_date(1i)": "2022" } } } + + it "renders the confirmation page" do + expect(response).to have_http_status(:ok) + expect(page).to have_content("This change will affect #{location.lettings_logs.count} logs") + end + end + + context "when confirming deactivation" do + let(:params) { { location: { deactivation_date:, confirm: true } } } + + it "updates existing location with valid deactivation date and renders location page" do + follow_redirect! + expect(response).to have_http_status(:ok) + expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success") + location.reload + expect(location.deactivation_date).to eq(deactivation_date) + end + end + + context "when the date is not selected" do + let(:params) { { location: { "deactivation_date": "" } } } + + it "displays the new page with an error message" do + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content(I18n.t("validations.location.deactivation_date.not_selected")) + end + end + + context "when invalid date is entered" do + let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "10", "deactivation_date(2i)": "44", "deactivation_date(1i)": "2022" } } } + + it "displays the new page with an error message" do + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content(I18n.t("validations.location.deactivation_date.invalid")) + end + end + + context "when the date is entered is before the beginning of current collection window" do + let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "10", "deactivation_date(2i)": "4", "deactivation_date(1i)": "2020" } } } + + it "displays the new page with an error message" do + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content(I18n.t("validations.location.deactivation_date.out_of_range", date: "1 April 2022")) + end + end + + context "when the day is not entered" do + let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "", "deactivation_date(2i)": "2", "deactivation_date(1i)": "2022" } } } + + it "displays page with an error message" do + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content(I18n.t("validations.location.deactivation_date.not_entered", period: "day")) + end + end + + context "when the month is not entered" do + let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "2", "deactivation_date(2i)": "", "deactivation_date(1i)": "2022" } } } + + it "displays page with an error message" do + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content(I18n.t("validations.location.deactivation_date.not_entered", period: "month")) + end + end + + context "when the year is not entered" do + let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "2", "deactivation_date(2i)": "2", "deactivation_date(1i)": "" } } } + + it "displays page with an error message" do + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content(I18n.t("validations.location.deactivation_date.not_entered", period: "year")) + end + end + end + end + + describe "#show" do + context "when not signed in" do + it "redirects to the sign in page" do + get "/schemes/1/locations/1" + expect(response).to redirect_to("/account/sign-in") + end + end + + context "when signed in as a data provider" do + let(:user) { FactoryBot.create(:user) } + + before do + sign_in user + get "/schemes/1/locations/1" + end + + it "returns 401 unauthorized" do + request + expect(response).to have_http_status(:unauthorized) + end + end + + context "when signed in as a data coordinator" do + let(:user) { FactoryBot.create(:user, :data_coordinator) } + let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) } + let!(:location) { FactoryBot.create(:location, scheme:) } + + before do + Timecop.freeze(Time.utc(2022, 10, 10)) + sign_in user + location.deactivation_date = deactivation_date + location.save! + get "/schemes/#{scheme.id}/locations/#{location.id}" + end + + context "with active location" do + let(:deactivation_date) { nil } + + it "renders deactivate this location" do + expect(response).to have_http_status(:ok) + expect(page).to have_link("Deactivate this location", href: "/schemes/#{scheme.id}/locations/#{location.id}/deactivate") + end + end + + context "with deactivated location" do + let(:deactivation_date) { Time.utc(2022, 10, 9) } + + it "renders reactivate this location" do + expect(response).to have_http_status(:ok) + expect(page).to have_link("Reactivate this location", href: "/schemes/#{scheme.id}/locations/#{location.id}/reactivate") + end + end + + context "with location that's deactivating soon" do + let(:deactivation_date) { Time.utc(2022, 10, 12) } + + it "renders reactivate this location" do + expect(response).to have_http_status(:ok) + expect(page).to have_link("Reactivate this location", href: "/schemes/#{scheme.id}/locations/#{location.id}/reactivate") + end + end + end + end end