diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index 88ad9da1b8..cb8e9e059e 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -167,6 +167,8 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $ if destination? $window.location = destination else + if ($window.adminOrderCycleUpdateCallback) + adminOrderCycleUpdateCallback(data.order_cycle); StatusMessage.display 'success', t('js.order_cycles.update_success') , (response) -> if response.data.errors? diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 35333e7af7..a52c40f7a4 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -70,7 +70,12 @@ module Admin respond_to do |format| flash[:success] = t('.success') if params[:reloading] == '1' format.html { redirect_to_after_update_path } - format.json { render json: { success: true } } + format.json { + render json: { success: true, order_cycle: { + orders_open_at: @order_cycle.orders_open_at&.strftime('%Y-%m-%d %H:%M'), + orders_close_at: @order_cycle.orders_close_at&.strftime('%Y-%m-%d %H:%M') + } } + } end elsif request.format.html? render :checkout_options diff --git a/app/views/admin/order_cycles/_name_and_timing_form.html.haml b/app/views/admin/order_cycles/_name_and_timing_form.html.haml index e3bc8df191..008b5e6178 100644 --- a/app/views/admin/order_cycles/_name_and_timing_form.html.haml +++ b/app/views/admin/order_cycles/_name_and_timing_form.html.haml @@ -11,7 +11,7 @@ = f.label :orders_open_at, t('.orders_open') .omega.six.columns.fullwidth_inputs - if viewing_as_coordinator_of?(@order_cycle) - = f.text_field :orders_open_at, data: { controller: "flatpickr", "flatpickr-enable-time-value": true }, 'ng-model' => 'order_cycle.orders_open_at', 'ng-if' => 'loaded()', 'change-warning' => 'order_cycle', class: "datetimepicker" + = f.text_field :orders_open_at, data: { controller: "flatpickr", "flatpickr-enable-time-value": true, action: 'order-cycle#toggleSaveBtns', 'order-cycle-target': 'input' }, 'ng-model' => 'order_cycle.orders_open_at', 'ng-if' => 'loaded()', 'change-warning' => 'order_cycle', class: "datetimepicker" - else {{ order_cycle.orders_open_at }} @@ -24,7 +24,7 @@ = f.label :orders_close, t('.orders_close') .six.columns.omega.fullwidth_inputs - if viewing_as_coordinator_of?(@order_cycle) - = f.text_field :orders_close_at, data: { controller: "flatpickr", "flatpickr-enable-time-value": true }, 'ng-model' => 'order_cycle.orders_close_at', 'ng-if' => 'loaded()', 'change-warning' => 'order_cycle', class: "datetimepicker" + = f.text_field :orders_close_at, data: { controller: "flatpickr", "flatpickr-enable-time-value": true, action: 'order-cycle#toggleSaveBtns', 'order-cycle-target': 'input' }, 'ng-model' => 'order_cycle.orders_close_at', 'ng-if' => 'loaded()', 'change-warning' => 'order_cycle', class: "datetimepicker" - else {{ order_cycle.orders_close_at }} diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index f6f3bc6f49..44fe2afd42 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -15,19 +15,46 @@ = t :edit_order_cycle - ng_controller = @order_cycle.simple? ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' +- has_scheduled_order = @order_cycle.schedules.exists? = admin_inject_order_cycle_instance(@order_cycle) -= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form'} do |f| - += form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form', data: { controller: 'modal modal-link order-cycle', "modal-link-target-value": "linked-schedule-warning-modal", 'order-cycle-has-schedule-value': has_scheduled_order, 'order-cycle-init-vals-value': { 'order_cycle[orders_open_at]': @order_cycle.orders_open_at&.strftime('%Y-%m-%d %H:%M'), 'order_cycle[orders_close_at]': @order_cycle.orders_close_at&.strftime('%Y-%m-%d %H:%M') } } } do |f| %save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" } - %input.red{ type: "button", value: t('.save'), "ng-click": "submit($event, null)", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" } - - if @order_cycle.simple? - %input.red{ type: "button", value: t('.save_and_back_to_list'), "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" } - - else - %input.red{ type: "button", value: t('.save_and_next'), "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" } - %input{ type: "button", value: t('.next'), "ng-click": "cancel('#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "order_cycle_form.$dirty" } - %input{ type: "button", "ng-value": "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", "ng-click": "cancel('#{main_app.admin_order_cycles_path}')" } + %div#form-actions + %input.red{ type: "button", value: t('.save'), "ng-click": "submit($event, null)", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" } + - if @order_cycle.simple? + %input.red{ type: "button", value: t('.save_and_back_to_list'), "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" } + - else + %input.red{ type: "button", value: t('.save_and_next'), "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" } + %input{ type: "button", value: t('.next'), "ng-click": "cancel('#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "order_cycle_form.$dirty" } + %input{ type: "button", "ng-value": "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", "ng-click": "cancel('#{main_app.admin_order_cycles_path}')" } + %div#modal-actions{style: "display: none;"} + %input.red{ type: "button", value: t('.save'), "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { 'action': 'click->modal-link#open click->order-cycle#updateModalConfirmButton', 'target': 'save'} } + - if @order_cycle.simple? + %input.red{ type: "button", value: t('.save_and_back_to_list'), "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { 'action': 'click->modal-link#open click->order-cycle#updateModalConfirmButton', 'target': 'saveAndBack'} } + - else + %input.red{ type: "button", value: t('.save_and_next'), "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { 'action': 'click->modal-link#open click->order-cycle#updateModalConfirmButton', 'target': 'saveAndNext'} } + %input{ type: "button", value: t('.next'), "ng-click": "cancel('#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "order_cycle_form.$dirty" } + %input{ type: "button", "ng-value": "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", "ng-click": "cancel('#{main_app.admin_order_cycles_path}')" } - if @order_cycle.simple? = render 'simple_form', f: f - else = render 'form', f: f + + - if has_scheduled_order + = render ModalComponent.new(id: "linked-schedule-warning-modal", close_button: false) do + .content + .modal-body + %h6 + = t('admin.order_cycles.edit.linked_schedule_warning_modal.title') + %div{ style: 'font-size: 1rem;' } + = t('admin.order_cycles.edit.linked_schedule_warning_modal.content') + %p.modal-actions.justify-end + %button.button.secondary#modal-confirm{ type: "button", 'data-action': 'click->modal#close', style: 'display: none;', data: { 'order-cycle-target': 'modalConfirm', request: 'save' }, "ng-click": "submit($event, null)" } + = t('admin.order_cycles.edit.linked_schedule_warning_modal.proceed') + %button.button.secondary#modal-confirm{ type: "button", 'data-action': 'click->modal#close', style: 'display: none;', data: { 'order-cycle-target': 'modalConfirm', request: 'saveAndNext' }, "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')" } + = t('admin.order_cycles.edit.linked_schedule_warning_modal.proceed') + %button.button.secondary#modal-confirm{ type: "button", 'data-action': 'click->modal#close', style: 'display: none;', data: { 'order-cycle-target': 'modalConfirm', request: 'saveAndBack' }, "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')" } + = t('admin.order_cycles.edit.linked_schedule_warning_modal.proceed') + %button.button.primary{ type: "button", 'data-action': 'click->modal#close' } + = t('admin.order_cycles.edit.linked_schedule_warning_modal.cancel') \ No newline at end of file diff --git a/app/webpacker/controllers/order_cycle_controller.js b/app/webpacker/controllers/order_cycle_controller.js new file mode 100644 index 0000000000..5bb7ecdae0 --- /dev/null +++ b/app/webpacker/controllers/order_cycle_controller.js @@ -0,0 +1,52 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + static targets = ['input', 'modalConfirm']; + static values = { initVals: { type: Object, default: {} }, hasSchedule: { type: Boolean, default: false } }; + + connect() { + if(!this.hasScheduleValue) return; + // Attach update callback method + window.adminOrderCycleUpdateCallback = this.updateCallback.bind(this); + } + + toggleSaveBtns() { + if(!this.hasScheduleValue) return; + + // Check that datetime input value has a change + const dirty = this.inputTargets.some(ele => + new Date(this.initValsValue[`${ele.name}`]).getTime() !== new Date(ele.value).getTime()); + + // Toggle save bar action button + if (dirty) { + this.element.querySelector('#form-actions').style.display = 'none'; + this.element.querySelector('#modal-actions').style.display = 'unset'; + } else { + this.element.querySelector('#form-actions').style.display = 'unset'; + this.element.querySelector('#modal-actions').style.display = 'none'; + } + } + + updateModalConfirmButton(e) { + if(!this.hasScheduleValue) return; + // Display modal confirm button coresponding to save bar button clicked + this.modalConfirmTargets.forEach(ele => { + if (e.target.getAttribute('data-target') === ele.getAttribute('data-request')) { + ele.style.display = 'unset'; + } else { + ele.style.display = 'none'; + } + }); + } + + updateCallback(data) { + // Reset order values and update save bar buttons + this.initValsValue = { 'order_cycle[orders_open_at]': data.orders_open_at, 'order_cycle[orders_close_at]': data.orders_close_at }; + this.toggleSaveBtns(); + } + + disconnect() { + // remove attached update callback method + delete window.adminOrderCycleUpdateCallback; + } +} \ No newline at end of file diff --git a/app/webpacker/css/admin/order_cycles.scss b/app/webpacker/css/admin/order_cycles.scss index ea73266fb8..cad5d3afb6 100644 --- a/app/webpacker/css/admin/order_cycles.scss +++ b/app/webpacker/css/admin/order_cycles.scss @@ -62,3 +62,25 @@ form.order_cycle { } } } + +#linked-schedule-warning-modal { + .reveal-modal { + width: 28rem; + + .content { + display: flex; + flex-direction: column; + gap: 2rem; + + .modal-body { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .modal-actions { + gap: 1rem; + } + } + } +} diff --git a/config/locales/en.yml b/config/locales/en.yml index 74e5b24b68..8e1e1ad995 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1506,6 +1506,11 @@ en: choose_products_from: "Choose Products From:" re_notify_producers: Re notify producers notify_producers_tip: This will send an email to each producer with the list of their orders. + linked_schedule_warning_modal: + title: 'Orders are linked to this order cycle.' + content: 'If you wish to create a new order cycle, it is recommended to duplicate the order cycle first and then change the dates.' + proceed: 'Proceed anyway' + cancel: 'Cancel' incoming: incoming: "Incoming" supplier: "Supplier" diff --git a/spec/system/admin/order_cycles/edit_spec.rb b/spec/system/admin/order_cycles/edit_spec.rb new file mode 100644 index 0000000000..ff157e5539 --- /dev/null +++ b/spec/system/admin/order_cycles/edit_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'system_helper' + +RSpec.describe ' + As an administrator + I want to edit a specific order cycle +' do + include AdminHelper + include AuthenticationHelper + include WebHelper + + let(:oc0) { + create(:simple_order_cycle, name: 'oc0', + orders_open_at: nil, orders_close_at: nil) + } + let(:oc1) { create(:order_cycle, name: 'oc1') } + + context 'when cycle has attached schedule(s)' do + it "properly toggles order cycle save bar buttons to show warning modal" do + create(:schedule, name: 'Schedule1', order_cycles: [oc0]) + + # When I go to the admin order cycle edit page + login_as_admin + visit edit_admin_order_cycle_path(oc0) + + expect(page).to have_selector("#linked-schedule-warning-modal") + expect(page).not_to have_selector("#modal-actions") + expect(page).to have_selector("#form-actions") + + # change non-date range field + fill_in 'order_cycle_name', with: "OC0 name updated" + expect(page).to have_content('You have unsaved changes') + click_button('Save') + expect(page).not_to have_selector('#linked-schedule-warning-modal .reveal-modal.in') + expect(page).to have_content('Your order cycle has been updated.') + + # change date range field value + time = DateTime.current + find('#order_cycle_orders_close_at').click + select_datetime_from_datepicker Time.zone.at(time) + + # Enable savebar save buttons to open warning modal + expect(page.find('#order_cycle_orders_close_at').value).to eq time.strftime('%Y-%m-%d %H:%M') + expect(page).not_to have_selector("#form-actions") + expect(page).to have_selector("#modal-actions") + expect(page).to have_content('You have unsaved changes') + expect(page).not_to have_selector('#linked-schedule-warning-modal .reveal-modal.in') + + # click save to open warning modal + click_button('Save') + expect(page).to have_selector('#linked-schedule-warning-modal .reveal-modal.in') + + # confirm to close modal and update order cycle changed fields + click_button('Proceed anyway') + expect(page).not_to have_selector('#linked-schedule-warning-modal .reveal-modal.in') + expect(page.find('#order_cycle_orders_close_at').value).to eq time.strftime('%Y-%m-%d %H:%M') + end + end + + context 'when cycle does not have attached schedule' do + it "does not render warning modal" do + # When I go to the admin order cycle edit page + login_as_admin + visit edit_admin_order_cycle_path(oc1) + + expect(page).not_to have_selector("#linked-schedule-warning-modal") + expect(page).not_to have_selector("#modal-actions") + expect(page).to have_selector("#form-actions") + + # change non-date range field value + fill_in 'order_cycle_name', with: "OC1 name updated" + expect(page).to have_content('You have unsaved changes') + + # click save + click_button('Save') + expect(page).not_to have_selector('#linked-schedule-warning-modal .reveal-modal.in') + expect(page).to have_content('Your order cycle has been updated.') + + # change date range field value + time = DateTime.current + find('#order_cycle_orders_close_at').click + select_datetime_from_datepicker Time.zone.at(time) + expect(page).to have_content('You have unsaved changes') + + click_button('Save') + expect(page).not_to have_selector("#modal-actions") + expect(page).to have_content('Your order cycle has been updated.') + end + end +end