diff --git a/app/controllers/session_dates_controller.rb b/app/controllers/session_dates_controller.rb index 1c05a0f1ff..2e82cb895f 100644 --- a/app/controllers/session_dates_controller.rb +++ b/app/controllers/session_dates_controller.rb @@ -2,41 +2,33 @@ class SessionDatesController < ApplicationController before_action :set_session + before_action :set_draft_session_dates def show - @session.session_dates.build if @session.session_dates.empty? - end - - def update - @session.assign_attributes(remove_invalid_dates(session_params)) - @session.set_notification_dates + initialize_draft_from_session - render :show, status: :unprocessable_content and return if @session.invalid? - - @session.save! - - # If deleting dates, they don't disappear from `session.dates` until - # the model has been saved due to how `accepts_nested_attributes_for` - # works. - if any_destroyed? - @session.session_dates.reload - @session.set_notification_dates - @session.save! + if params[:add_another] || + @draft_session_dates.session_dates_attributes.empty? + add_blank_session_date end + end - StatusUpdaterJob.perform_later(session: @session) + def update + @draft_session_dates.session_dates_attributes = + fetch_draft_session_dates_from_form - if params.include?(:add_another) - @session.session_dates.build + if params[:delete_date] + delete_session_date(form_index_to_attr_index(params[:delete_date])) + return render_with_error if @draft_session_dates.errors.any? + render :show + elsif params[:add_another] + add_blank_session_date render :show else - redirect_to( - if any_destroyed? - session_dates_path(@session) - else - edit_session_path(@session) - end - ) + unless @draft_session_dates.save(context: :continue) + return render_with_error + end + apply_changes_and_redirect end end @@ -46,41 +38,121 @@ def set_session @session = policy_scope(Session).find_by!(slug: params[:session_slug]) end - def session_params - params.expect( - session: { - session_dates_attributes: [%i[id value _destroy]] - } + def set_draft_session_dates + @draft_session_dates = + DraftSessionDates.new( + request_session: session, + current_user: current_user, + session: @session, + wizard_step: :dates + ) + end + + def draft_session_dates_params + params + .fetch(:draft_session_dates, {}) + .permit( + session_dates_attributes: [ + :id, + :value, + :_destroy, + "value(1i)", + "value(2i)", + "value(3i)" + ] + ) + .merge(wizard_step: :dates) + end + + def initialize_draft_from_session + attributes = {} + + @session + .session_dates + .order(:value) + .each_with_index do |session_date, index| + attributes[index] = { + "id" => session_date.id.to_s, + "value" => session_date.value + } + end + + @draft_session_dates.session_dates_attributes = attributes + @draft_session_dates.save!(context: :initialize) + end + + def add_blank_session_date + current_attrs = @draft_session_dates.session_dates_attributes + next_index = (current_attrs.keys.map(&:to_i).max || -1) + 1 + current_attrs[next_index] = { "value" => nil } + @draft_session_dates.session_dates_attributes = current_attrs + end + + def render_with_error + render :show, status: :unprocessable_content + end + + def apply_changes_and_redirect + @draft_session_dates.write_to!(@session) + session.delete(@draft_session_dates.request_session_key) + StatusUpdaterJob.perform_later(session: @session) + redirect_to edit_session_path(@session) + rescue ActiveRecord::RecordInvalid => e + @draft_session_dates.errors.add( + :base, + "Failed to save session dates: #{e.message}" ) + render_with_error end - def any_destroyed? - session_params[:session_dates_attributes].values.any? do - _1[:_destroy].present? + def form_index_to_attr_index(form_index) + current_attrs = @draft_session_dates.session_dates_attributes + corresponding_form_index = -1 + current_attrs.each do |real_index, attrs| + corresponding_form_index += 1 unless attrs["_destroy"] == "true" + + return real_index if corresponding_form_index.to_s == form_index end + + ( + form_index.to_i - corresponding_form_index.to_i + current_attrs.length + ).to_s end - def remove_invalid_dates(obj, key: "session_dates_attributes") - return obj if obj[key].blank? - - obj[key] = obj[key].transform_values do |value| - if value.key?("value(1i)") && value.key?("value(2i)") && - value.key?("value(3i)") - begin - Date.new( - value["value(1i)"].to_i, - value["value(2i)"].to_i, - value["value(3i)"].to_i - ) - value - rescue StandardError - {} - end - else - value - end + def fetch_draft_session_dates_from_form + current_attrs = @draft_session_dates.session_dates_attributes + + form_params = draft_session_dates_params[:session_dates_attributes] || {} + + merged_attrs = {} + + current_attrs.each do |index, attrs| + merged_attrs[index] = attrs if attrs["_destroy"] == "true" end - obj + form_params.each do |form_index, form_attrs| + attr_index = form_index_to_attr_index(form_index) + merged_attrs[attr_index] = form_attrs + end + + merged_attrs + end + + def delete_session_date(index) + current_attrs = @draft_session_dates.session_dates_attributes + + if current_attrs[index] + if @draft_session_dates.non_destroyed_session_dates_count <= 1 + @draft_session_dates.errors.add( + :base, + "You cannot delete the last session date. A session must have at least one date." + ) + return + end + + current_attrs[index]["_destroy"] = "true" + @draft_session_dates.session_dates_attributes = current_attrs + @draft_session_dates.save!(context: :update) + end end end diff --git a/app/models/draft_session_dates.rb b/app/models/draft_session_dates.rb new file mode 100644 index 0000000000..3f5391a6ed --- /dev/null +++ b/app/models/draft_session_dates.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +class DraftSessionDates + include RequestSessionPersistable + include WizardStepConcern + include ActiveRecord::AttributeMethods::Serialization + + def request_session_key + "draft_session_dates" + end + + attribute :session_id, :integer + attribute :current_user_id, :integer + + def initialize(current_user:, **attributes) + @current_user = current_user + super(**attributes) + end + + serialize :session_dates_attributes_json, coder: JSON + + def wizard_steps + %i[dates] + end + + on_wizard_step :dates, exact: true do + validate :validate_session_dates, on: %i[update continue] + end + + def session + return nil if session_id.nil? + + SessionPolicy::Scope.new(@current_user, Session).resolve.find(session_id) + end + + def session=(value) + self.session_id = value.id + end + + def session_dates + @session_dates ||= build_session_dates_from_attributes + end + + def non_destroyed_session_dates_count + session_dates_attributes.count { |_, attrs| attrs["_destroy"] != "true" } + end + + def session_dates_attributes + @session_dates_attributes ||= + JSON.parse(session_dates_attributes_json || "{}") + end + + def session_dates_attributes=(attributes) + @session_dates_attributes = attributes + self.session_dates_attributes_json = attributes + @session_dates = nil # Reset cached session dates + end + + def write_to!(session) + return unless session_id == session.id + + ActiveRecord::Base.transaction do + delete_marked_session_dates(session) + create_or_update_session_dates(session) + update_session_notifications(session) + end + end + + private + + def delete_marked_session_dates(session) + session_dates_attributes.each_value do |attrs| + next unless attrs["_destroy"] == "true" && attrs["id"].present? + + session_date = session.session_dates.find_by(id: attrs["id"]) + session_date&.destroy! unless session_date&.session_attendances&.any? + end + end + + def create_or_update_session_dates(session) + session_dates_attributes.each_value do |attrs| + next if attrs["_destroy"] == "true" + + date_value = parse_date_from_attributes(attrs) + next unless date_value + + if attrs["id"].present? + update_existing_session_date(session, attrs["id"], date_value) + else + session.session_dates.create!(value: date_value) + end + end + end + + def update_existing_session_date(session, id, date_value) + session_date = session.session_dates.find_by(id: id) + session_date&.update!(value: date_value) + end + + def update_session_notifications(session) + session.set_notification_dates + session.save! + end + + def reset_unused_fields + # No unused fields to reset for session dates + end + + def build_session_dates_from_attributes + dates = [] + + if session_dates_attributes.present? + session_dates_attributes.each_value do |attrs| + next if attrs["_destroy"] == "true" + + if attrs["id"].present? + existing_date = session&.session_dates&.find_by(id: attrs["id"]) + parsed_date = parse_date_from_attributes(attrs) + dates << DraftSessionDate.new( + id: attrs["id"], + value: parsed_date || existing_date.value, + session: session, + persisted: true + ) + else + parsed_date = parse_date_from_attributes(attrs) + dates << DraftSessionDate.new( + value: parsed_date, + session: session, + persisted: false + ) + end + end + end + + dates + end + + class DraftSessionDate + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :id, :integer + attribute :value, :date + + attr_accessor :session, :persisted_state + + def initialize(session:, persisted: false, **attributes) + @session = session + @persisted_state = persisted + super(attributes) + end + + def persisted? + @persisted_state + end + + def session_attendances + return [] unless persisted? && id + + session&.session_dates&.find_by(id: id)&.session_attendances || [] + end + end + + def parse_date_from_attributes(attrs) + return attrs["value"] if attrs["value"].is_a?(Date) + + if attrs["value(1i)"] && attrs["value(2i)"] && attrs["value(3i)"] + begin + Date.new( + attrs["value(1i)"].to_i, + attrs["value(2i)"].to_i, + attrs["value(3i)"].to_i + ) + rescue StandardError + nil + end + end + end + + def validate_session_dates + attrs_hash = session_dates_attributes + + accepted_dates = [] + has_valid_date = false + + attrs_hash.each_value do |attrs| + next if attrs["_destroy"] == "true" + + date_value = parse_date_from_attributes(attrs) + + if date_value.nil? + if attrs["value(1i)"].present? || attrs["value(2i)"].present? || + attrs["value(3i)"].present? || attrs["value"].present? + errors.add(:base, "Enter a valid date") + end + next + end + + has_valid_date = true + + if accepted_dates.include?(date_value) + errors.add(:base, "Session dates must be unique") + next + end + + accepted_dates << date_value + end + + errors.add(:base, "Enter a date") unless has_valid_date + end +end diff --git a/app/views/session_dates/show.html.erb b/app/views/session_dates/show.html.erb index 5093f3e9f8..bf91c4793e 100644 --- a/app/views/session_dates/show.html.erb +++ b/app/views/session_dates/show.html.erb @@ -4,7 +4,7 @@ <%= h1 "When will sessions be held?" %> -<%= form_with model: @session, url: session_dates_path(@session), method: :put do |f| %> +<%= form_with model: @draft_session_dates, url: session_dates_path(@session), method: :put do |f| %> <% content_for(:before_content) { f.govuk_error_summary } %>
@@ -23,14 +23,13 @@ legend: { size: "m", text: "Session date" }, hint: { text: "For example, 27 3 2024" } %> - <% if date_f.object.persisted? || @session.session_dates.length > 1 %> - <% button_class = "nhsuk-button app-add-another__delete app-button--secondary-warning app-button--small" %> - - <% if date_f.object.new_record? %> - <%= link_to "Delete", session_dates_path(@session), class: button_class %> - <% else %> - <%= f.govuk_submit "Delete", name: "session[session_dates_attributes][#{date_f.index}][_destroy]", value: "true", class: button_class %> - <% end %> + + <% if date_f.object.persisted? || @draft_session_dates.session_dates.length > 1 %> + <%= f.govuk_submit "Delete", + name: "delete_date", + value: date_f.index, + warning: true, + class: "app-add-another__delete app-button--small" %> <% end %> <% end %> @@ -42,6 +41,5 @@
<%= f.govuk_submit "Continue" %> - <%= govuk_link_to "Back", edit_session_path(@session) %>
<% end %> diff --git a/spec/features/draft_session_dates_spec.rb b/spec/features/draft_session_dates_spec.rb new file mode 100644 index 0000000000..04e6cded92 --- /dev/null +++ b/spec/features/draft_session_dates_spec.rb @@ -0,0 +1,474 @@ +# frozen_string_literal: true + +describe "Edit session dates" do + around { |example| travel_to(Time.zone.local(2024, 2, 18)) { example.run } } + + scenario "User can modify session dates without immediate saving" do + given_i_have_a_session_with_existing_dates + when_i_visit_the_session_edit_page + then_i_see_the_existing_session_dates + + when_i_click_change_session_dates + then_i_see_the_session_dates_page_with_existing_dates + + when_i_modify_the_first_date + and_i_click_back + then_i_should_be_back_on_session_edit_page + and_the_original_dates_should_be_unchanged + + when_i_click_change_session_dates_again + then_i_see_the_session_dates_page_with_original_dates + + when_i_modify_the_first_date_to_a_different_value + and_i_click_continue + then_i_should_be_back_on_session_edit_page + and_the_dates_should_be_updated + end + + scenario "User can add new session dates" do + given_i_have_a_session_without_dates + when_i_visit_the_session_edit_page + then_i_see_add_session_dates_link + + when_i_click_add_session_dates + then_i_see_the_session_dates_page_with_empty_form + + when_i_add_a_new_date + and_i_click_add_another_date + when_i_add_a_second_date + and_i_click_continue + then_i_should_be_back_on_session_edit_page + and_i_should_see_both_new_dates + end + + scenario "User gets validation error for invalid date" do + given_i_have_a_session_without_dates + when_i_visit_the_session_edit_page + and_i_click_add_session_dates + + when_i_add_an_invalid_date + and_i_click_continue + then_i_should_see_invalid_date_error + end + + scenario "User sees read-only display for session dates with attendances" do + given_i_have_a_session_with_dates_and_attendances + when_i_visit_the_session_edit_page + then_i_see_the_existing_session_dates + + when_i_click_change_session_dates + then_i_see_the_session_dates_page_with_attendance_restrictions + end + + scenario "User can delete session dates" do + given_i_have_a_session_with_existing_dates + when_i_visit_the_session_edit_page + then_i_see_the_existing_session_dates + + when_i_click_change_session_dates + then_i_see_the_session_dates_page_with_existing_dates + + when_i_delete_the_first_date + and_i_delete_the_new_first_date_too + then_i_should_only_see_the_third_date + + and_i_click_continue + then_i_should_be_back_on_session_edit_page + and_only_the_third_date_should_remain + end + + scenario "User can delete a newly added session date in the same session" do + given_i_have_a_session_with_existing_dates + when_i_visit_the_session_edit_page + then_i_see_the_existing_session_dates + + when_i_click_change_session_dates + then_i_see_the_session_dates_page_with_existing_dates + + and_i_click_add_another_date + when_i_add_a_fourth_date + then_i_should_see_four_dates + + when_i_delete_the_newly_added_fourth_date + then_i_should_see_original_three_dates_only + + and_i_click_continue + then_i_should_be_back_on_session_edit_page + and_the_original_dates_should_be_unchanged + end + + scenario "User cannot delete the last remaining session date" do + given_i_have_a_session_with_one_date + when_i_visit_the_session_edit_page + then_i_see_the_single_session_date + + when_i_click_change_session_dates + then_i_see_the_session_dates_page_with_one_date + + when_i_try_to_delete_the_only_date + then_i_should_see_cannot_delete_last_date_error + and_the_date_should_still_be_visible + end + + def given_i_have_a_session_with_existing_dates + @team = create(:team, :with_one_nurse) + @session = create(:session, :unscheduled, team: @team) + @original_date1 = Date.new(2024, 3, 15) + @original_date2 = Date.new(2024, 3, 16) + @original_date3 = Date.new(2024, 3, 17) + @session.session_dates.create!(value: @original_date1) + @session.session_dates.create!(value: @original_date2) + @session.session_dates.create!(value: @original_date3) + + sign_in @team.users.first + end + + def given_i_have_a_session_without_dates + @team = create(:team, :with_one_nurse) + @session = create(:session, :unscheduled, team: @team) + + sign_in @team.users.first + end + + def given_i_have_a_session_with_one_date + @team = create(:team, :with_one_nurse) + @session = create(:session, :unscheduled, team: @team) + @single_date = Date.new(2024, 3, 15) + @session.session_dates.create!(value: @single_date) + + sign_in @team.users.first + end + + def when_i_visit_the_session_edit_page + visit edit_session_path(@session) + end + + def then_i_see_the_existing_session_dates + expect(page).to have_content("15 March 2024") + expect(page).to have_content("16 March 2024") + expect(page).to have_content("17 March 2024") + end + + def then_i_see_add_session_dates_link + expect(page).to have_link("Add session dates") + end + + def when_i_click_change_session_dates + click_link "Change session dates" + end + + def when_i_click_change_session_dates_again + when_i_click_change_session_dates + end + + def when_i_click_add_session_dates + click_link "Add session dates" + end + + alias_method :and_i_click_add_session_dates, :when_i_click_add_session_dates + + def then_i_see_the_session_dates_page_with_existing_dates + expect(page).to have_content("When will sessions be held?") + + # Check that existing dates are populated + within page.all(".app-add-another__list-item")[0] do + expect(page).to have_field("Day", with: "15") + expect(page).to have_field("Month", with: "3") + expect(page).to have_field("Year", with: "2024") + end + + within page.all(".app-add-another__list-item")[1] do + expect(page).to have_field("Day", with: "16") + expect(page).to have_field("Month", with: "3") + expect(page).to have_field("Year", with: "2024") + end + + within page.all(".app-add-another__list-item")[2] do + expect(page).to have_field("Day", with: "17") + expect(page).to have_field("Month", with: "3") + expect(page).to have_field("Year", with: "2024") + end + end + + def then_i_see_the_session_dates_page_with_original_dates + then_i_see_the_session_dates_page_with_existing_dates + end + + def then_i_see_the_session_dates_page_with_empty_form + expect(page).to have_content("When will sessions be held?") + # Check that there are empty form fields (they might have nil values) + expect(page).to have_field("Day") + expect(page).to have_field("Month") + expect(page).to have_field("Year") + end + + def when_i_modify_the_first_date + within page.all(".app-add-another__list-item")[0] do + fill_in "Day", with: "20" + end + end + + def when_i_modify_the_first_date_to_a_different_value + within page.all(".app-add-another__list-item")[0] do + fill_in "Day", with: "25" + end + end + + def when_i_add_a_new_date + fill_in "Day", with: "15" + fill_in "Month", with: "3" + fill_in "Year", with: "2024" + end + + def when_i_add_a_second_date + # Find the second list item (after clicking "Add another date") + within page.all(".app-add-another__list-item")[1] do + fill_in "Day", with: "16" + fill_in "Month", with: "3" + fill_in "Year", with: "2024" + end + end + + def and_i_click_back + click_link "Back" + end + + def and_i_click_continue + click_button "Continue" + end + + def and_i_click_add_another_date + click_button "Add another date" + end + + def then_i_should_be_back_on_session_edit_page + expect(page).to have_content("Edit session") + expect(page).to have_content(@session.location.name) + end + + def and_the_original_dates_should_be_unchanged + @session.reload + expect(@session.session_dates.map(&:value)).to contain_exactly( + @original_date1, + @original_date2, + @original_date3 + ) + expect(page).to have_content("15 March 2024") + expect(page).to have_content("16 March 2024") + expect(page).to have_content("17 March 2024") + end + + def and_the_dates_should_be_updated + @session.reload + expect(@session.session_dates.map(&:value)).to contain_exactly( + Date.new(2024, 3, 25), + @original_date2, + @original_date3 + ) + expect(page).to have_content("25 March 2024") + expect(page).to have_content("16 March 2024") + expect(page).to have_content("17 March 2024") + end + + def and_i_should_see_both_new_dates + @session.reload + expect(@session.session_dates.count).to eq(2) + expect(@session.session_dates.map(&:value)).to contain_exactly( + Date.new(2024, 3, 15), + Date.new(2024, 3, 16) + ) + expect(page).to have_content("15 March 2024") + expect(page).to have_content("16 March 2024") + end + + def then_i_should_see_duplicate_date_error + expect(page).to have_content("There is a problem") + expect(page).to have_content("Session dates must be unique") + end + + def when_i_delete_the_first_date + within page.all(".app-add-another__list-item")[0] do + click_button "Delete" + end + end + + alias_method :and_i_delete_the_new_first_date_too, + :when_i_delete_the_first_date + + def then_i_should_only_see_the_third_date + expect(page).to have_content("When will sessions be held?") + + expect(page).to have_field(with: "17") + expect(page).not_to have_field(with: "16") + expect(page).not_to have_field(with: "15") + + visible_date_groups = page.all(".app-add-another__list-item") + expect(visible_date_groups.count).to eq(1) + end + + def and_only_the_third_date_should_remain + @session.reload + expect(@session.session_dates.count).to eq(1) + expect(@session.session_dates.first.value).to eq(@original_date3) + expect(page).to have_content("17 March 2024") + expect(page).not_to have_content("16 March 2024") + expect(page).not_to have_content("15 March 2024") + end + + def when_i_add_a_fourth_date + # Find the fourth list item (after clicking "Add another date") + within page.all(".app-add-another__list-item")[3] do + fill_in "Day", with: "18" + fill_in "Month", with: "3" + fill_in "Year", with: "2024" + end + end + + def then_i_should_see_four_dates + expect(page).to have_content("When will sessions be held?") + + visible_date_groups = page.all(".app-add-another__list-item") + expect(visible_date_groups.count).to eq(4) + + # Check all four dates are visible + expect(page).to have_field(with: "15") + expect(page).to have_field(with: "16") + expect(page).to have_field(with: "17") + expect(page).to have_field(with: "18") + end + + def when_i_delete_the_newly_added_fourth_date + within page.all(".app-add-another__list-item")[3] do + click_button "Delete" + end + end + + def then_i_should_see_original_three_dates_only + expect(page).to have_content("When will sessions be held?") + + visible_date_groups = page.all(".app-add-another__list-item") + expect(visible_date_groups.count).to eq(3) + + # Check only original three dates are visible + expect(page).to have_field(with: "15") + expect(page).to have_field(with: "16") + expect(page).to have_field(with: "17") + expect(page).not_to have_field(with: "18") + end + + def when_i_add_an_invalid_date + fill_in "Day", with: "29" + fill_in "Month", with: "2" + fill_in "Year", with: "2023" # Not a leap year + end + + def when_i_add_an_incomplete_date + fill_in "Day", with: "15" + fill_in "Month", with: "" + fill_in "Year", with: "2024" + end + + def then_i_should_see_invalid_date_error + expect(page).to have_content("There is a problem") + expect(page).to have_content("Enter a valid date") + end + + def given_i_have_a_session_with_dates_and_attendances + @team = create(:team, :with_one_nurse) + @session = create(:session, :unscheduled, team: @team) + @original_date1 = Date.new(2024, 3, 15) + @original_date2 = Date.new(2024, 3, 16) + @original_date3 = Date.new(2024, 3, 17) + + # Create session dates + @session_date1 = @session.session_dates.create!(value: @original_date1) + @session_date2 = @session.session_dates.create!(value: @original_date2) + @session_date3 = @session.session_dates.create!(value: @original_date3) + + # Create a patient and patient session + @patient = create(:patient, team: @team) + @patient_session = + create(:patient_session, patient: @patient, session: @session) + + # Create session attendance for the first date (this will prevent changing that date) + create( + :session_attendance, + :present, + patient_session: @patient_session, + session_date: @session_date1 + ) + + sign_in @team.users.first + end + + def then_i_see_the_session_dates_page_with_attendance_restrictions + expect(page).to have_content("When will sessions be held?") + + # First date should be read-only (has attendances) + within page.all(".app-add-another__list-item")[0] do + expect(page).to have_content("15 March 2024") + expect(page).to have_content( + "Children have attended this session. It cannot be changed." + ) + expect(page).not_to have_field("Day") + end + + # Second and third dates should be editable (no attendances) + within page.all(".app-add-another__list-item")[1] do + expect(page).to have_field("Day", with: "16") + expect(page).to have_field("Month", with: "3") + expect(page).to have_field("Year", with: "2024") + end + + within page.all(".app-add-another__list-item")[2] do + expect(page).to have_field("Day", with: "17") + expect(page).to have_field("Month", with: "3") + expect(page).to have_field("Year", with: "2024") + end + end + + def then_i_should_see_no_dates_error + expect(page).to have_content("There is a problem") + expect(page).to have_content("Enter a date") + end + + def then_i_see_the_single_session_date + expect(page).to have_content("15 March 2024") + end + + def then_i_see_the_session_dates_page_with_one_date + expect(page).to have_content("When will sessions be held?") + + visible_date_groups = page.all(".app-add-another__list-item") + expect(visible_date_groups.count).to eq(1) + + within visible_date_groups[0] do + expect(page).to have_field("Day", with: "15") + expect(page).to have_field("Month", with: "3") + expect(page).to have_field("Year", with: "2024") + end + end + + def when_i_try_to_delete_the_only_date + within page.all(".app-add-another__list-item")[0] do + click_button "Delete" + end + end + + def then_i_should_see_cannot_delete_last_date_error + expect(page).to have_content("There is a problem") + expect(page).to have_content("You cannot delete the last session date") + expect(page).to have_content("A session must have at least one date") + end + + def and_the_date_should_still_be_visible + visible_date_groups = page.all(".app-add-another__list-item") + expect(visible_date_groups.count).to eq(1) + + within visible_date_groups[0] do + expect(page).to have_field("Day", with: "15") + expect(page).to have_field("Month", with: "3") + expect(page).to have_field("Year", with: "2024") + end + end +end diff --git a/spec/models/draft_session_dates_spec.rb b/spec/models/draft_session_dates_spec.rb new file mode 100644 index 0000000000..f9055c30f8 --- /dev/null +++ b/spec/models/draft_session_dates_spec.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +describe DraftSessionDates do + subject(:draft_session_dates) do + described_class.new( + request_session: request_session, + current_user: current_user, + session: session, + wizard_step: :dates + ) + end + + let(:programme) { create(:programme, :hpv) } + let(:team) { create(:team, :with_one_nurse, programmes: [programme]) } + let(:session) { create(:session, team: team, programmes: [programme]) } + let(:current_user) { team.users.first } + let(:request_session) { {} } + + describe "validations" do + context "with valid session dates" do + before do + draft_session_dates.session_dates_attributes = { + "0" => { + "value(1i)" => "2024", + "value(2i)" => "10", + "value(3i)" => "15" + } + } + end + + it "is valid" do + expect(draft_session_dates.valid?(:update)).to be true + end + end + + context "with invalid date" do + before do + draft_session_dates.session_dates_attributes = { + "0" => { + "value(1i)" => "2024", + "value(2i)" => "13", # Invalid month + "value(3i)" => "15" + } + } + end + + it "is invalid" do + expect(draft_session_dates.valid?(:update)).to be false + expect(draft_session_dates.errors[:base]).to include( + "Enter a valid date" + ) + end + end + + context "with duplicate dates" do + before do + draft_session_dates.session_dates_attributes = { + "0" => { + "value(1i)" => "2024", + "value(2i)" => "10", + "value(3i)" => "15" + }, + "1" => { + "value(1i)" => "2024", + "value(2i)" => "10", + "value(3i)" => "15" + } + } + end + + it "is invalid" do + expect(draft_session_dates.valid?(:update)).to be false + expect(draft_session_dates.errors[:base]).to include( + "Session dates must be unique" + ) + end + + it "validates uniqueness of dates" do + # First validate to trigger the validation + draft_session_dates.valid?(:update) + + # Check that the validation was triggered + expect(draft_session_dates.errors[:base]).to include( + "Session dates must be unique" + ) + + # Now change one of the dates to make them unique + draft_session_dates.session_dates_attributes = { + "0" => { + "value(1i)" => "2024", + "value(2i)" => "10", + "value(3i)" => "15" + }, + "1" => { + "value(1i)" => "2024", + "value(2i)" => "10", + "value(3i)" => "16" # Changed day from 15 to 16 + } + } + + # Validate again + expect(draft_session_dates.valid?(:update)).to be true + end + end + + context "with no dates" do + before { draft_session_dates.session_dates_attributes = {} } + + it "is invalid on update" do + expect(draft_session_dates.valid?(:update)).to be false + expect(draft_session_dates.errors[:base]).to include("Enter a date") + end + + it "is valid on create (allows saving empty draft)" do + expect(draft_session_dates.valid?(:create)).to be true + end + end + end + + describe "#write_to!" do + let(:target_session) do + create(:session, team: team, programmes: [programme]) + end + + before do + draft_session_dates.session = target_session + draft_session_dates.session_dates_attributes = { + "0" => { + "value(1i)" => "2024", + "value(2i)" => "10", + "value(3i)" => "15" + } + } + end + + it "creates new session dates" do + expect { draft_session_dates.write_to!(target_session) }.to change { + target_session.session_dates.count + }.by(1) + + expect(target_session.session_dates.last.value).to eq( + Date.new(2024, 10, 15) + ) + end + + it "calls set_notification_dates on the session" do + expect(target_session).to receive(:set_notification_dates) + expect(target_session).to receive(:save!) + + draft_session_dates.write_to!(target_session) + end + end + + describe "#parse_date_from_attributes" do + it "parses date from multi-parameter attributes" do + attrs = { + "value(1i)" => "2024", + "value(2i)" => "10", + "value(3i)" => "15" + } + + result = draft_session_dates.send(:parse_date_from_attributes, attrs) + expect(result).to eq(Date.new(2024, 10, 15)) + end + + it "returns nil for invalid date" do + attrs = { + "value(1i)" => "2024", + "value(2i)" => "13", # Invalid month + "value(3i)" => "15" + } + + result = draft_session_dates.send(:parse_date_from_attributes, attrs) + expect(result).to be_nil + end + + it "returns date object as-is" do + date = Date.new(2024, 10, 15) + attrs = { "value" => date } + + result = draft_session_dates.send(:parse_date_from_attributes, attrs) + expect(result).to eq(date) + end + end +end