diff --git a/app/assets/stylesheets/buttons.scss b/app/assets/stylesheets/buttons.scss index a7f680601..57a09165b 100644 --- a/app/assets/stylesheets/buttons.scss +++ b/app/assets/stylesheets/buttons.scss @@ -218,6 +218,8 @@ ul.dropdown-items { padding-inline-start: 0; list-style: none; + margin-top: 0; + margin-bottom: 0; } .dropdown-items { diff --git a/app/components/dossiers/batch_alert_component/batch_alert_component.en.yml b/app/components/dossiers/batch_alert_component/batch_alert_component.en.yml index 4895eaa8c..764c2f611 100644 --- a/app/components/dossiers/batch_alert_component/batch_alert_component.en.yml +++ b/app/components/dossiers/batch_alert_component/batch_alert_component.en.yml @@ -2,56 +2,74 @@ en: archiver: finish: text_success: - one: 1/1 file has been archived + one: "%{success_count}/1 file has been archived" other: "%{success_count}/%{count} files have been archived" in_progress: text_success: - one: 1/1 is being archived + one: "1 is being archived" other: "%{success_count}/%{count} files have been archived" passer_en_instruction: finish: text_success: - one: 1/1 file has been changed to instructing + one: "%{success_count}/1 file has been changed to instructing" other: "%{success_count}/%{count} files have been changed to instructing" in_progress: text_success: - one: 1/1 is being changed to instructing + one: "1 is being changed to instructing" other: "%{success_count}/%{count} files have been changed to instructing" accepter: finish: text_success: - one: 1/1 file has been accepted + one: "%{success_count}/1 file has been accepted" other: "%{success_count}/%{count} files have been accepted" in_progress: text_success: - one: 1/1 is being accepted + one: "1 is being accepted" other: "%{success_count}/%{count} files have been accepted" + refuser: + finish: + text_success: + one: "%{success_count}/1 file has been refused" + other: "%{success_count}/%{count} files have been refused" + in_progress: + text_success: + one: "1 is being refused" + other: "%{success_count}/%{count} files have been refused" + classer_sans_suite: + finish: + text_success: + one: "%{success_count}/1 file has been closed without continuation" + other: "%{success_count}/%{count} files have been closed without continuation" + in_progress: + text_success: + one: "1 is being closed without continuation" + other: "%{success_count}/%{count} files have been closed without continuation" follow: finish: text_success: - one: 1/1 file has been followed + one: "%{success_count}/1 file has been followed" other: "%{success_count}/%{count} files have been followed" in_progress: text_success: - one: 1/1 is being followed + one: "1 is being followed" other: "%{success_count}/%{count} files have been followed" unfollow: finish: text_success: - one: 1/1 file has been unfollowed + one: "%{success_count}/1 file has been unfollowed" other: "%{success_count}/%{count} files have been unfollowed" in_progress: text_success: - one: 1/1 is being unfollowed + one: "1 is being unfollowed" other: "%{success_count}/%{count} files have been unfollowed" repasser_en_construction: finish: text_success: - one: 1/1 file has been changed to in progress + one: "%{success_count}/1 file has been changed to in progress" other: "%{success_count}/%{count} files have been changed to in progress" in_progress: text_success: - one: 1/1 is being changed to in progress + one: "1 is being changed to in progress" other: "%{success_count}/%{count} files have been changed to in progress" title: finish: The bulk action is finished diff --git a/app/components/dossiers/batch_alert_component/batch_alert_component.fr.yml b/app/components/dossiers/batch_alert_component/batch_alert_component.fr.yml index 88bab8187..ed386a227 100644 --- a/app/components/dossiers/batch_alert_component/batch_alert_component.fr.yml +++ b/app/components/dossiers/batch_alert_component/batch_alert_component.fr.yml @@ -2,56 +2,74 @@ fr: archiver: finish: text_success: - one: 1 dossier a été archivé + one: "%{success_count}/1 dossier a été archivé" other: "%{success_count}/%{count} dossiers ont été archivés" in_progress: text_success: - one: 1 dossier sera archivé + one: "1 dossier est en cours d'archivage" other: "%{success_count}/%{count} dossiers ont été archivés" passer_en_instruction: finish: text_success: - one: 1 dossier a été passé en instruction + one: "%{success_count}/1 dossier a été passé en instruction" other: "%{success_count}/%{count} dossiers ont été passés en instruction" in_progress: text_success: - one: 1 dossier sera passé en instruction + one: "1 dossier est en cours d'instruction" other: "%{success_count}/%{count} dossiers ont été passés en instruction" accepter: finish: text_success: - one: 1 dossier a été accepté + one: "%{success_count}/1 dossier a été accepté" other: "%{success_count}/%{count} dossiers ont été acceptés" in_progress: text_success: - one: 1 dossier sera accepté + one: "1 dossier est en cours d'acceptation" other: "%{success_count}/%{count} dossiers ont été acceptés" + refuser: + finish: + text_success: + one: "%{success_count}/1 dossier a été refusé" + other: "%{success_count}/%{count} dossiers ont été refusés" + in_progress: + text_success: + one: "1 dossier est en cours de refus" + other: "%{success_count}/%{count} dossiers ont été refusés" + classer_sans_suite: + finish: + text_success: + one: "%{success_count}/1 dossier a été classé sans suite" + other: "%{success_count}/%{count} dossiers ont été classés sans suite" + in_progress: + text_success: + one: "1 dossier est en cours de classement sans suite" + other: "%{success_count}/%{count} dossiers ont été classés sans suite" follow: finish: text_success: - one: 1 dossier a été suivi + one: "%{success_count}/1 dossier a été suivi" other: "%{success_count}/%{count} dossiers ont été suivis" in_progress: text_success: - one: 1 dossier sera suivi + one: "1 dossier est en cours d'être suivi" other: "%{success_count}/%{count} dossiers ont été suivis" unfollow: finish: text_success: - one: 1 dossier n'est plus suivi + one: "%{success_count}/1 dossier n'est plus suivi" other: "%{success_count}/%{count} dossiers ne sont plus suivis" in_progress: text_success: - one: 1 dossier ne sera plus suivi + one: "1 dossier est en cours de ne plus être suivi" other: "%{success_count}/%{count} dossiers ne sont plus suivis" repasser_en_construction: finish: text_success: - one: 1 dossier a été repassé en construction + one: "%{success_count}/1 dossier a été repassé en construction" other: "%{success_count}/%{count} dossiers ont été repassés en construction" in_progress: text_success: - one: 1 dossier sera repassé en construction + one: "1 dossier est en cours d'être repassé en construction" other: "%{success_count}/%{count} dossiers ont été repassés en construction" title: finish: L’action de masse est terminée diff --git a/app/components/dossiers/batch_operation_component.rb b/app/components/dossiers/batch_operation_component.rb index baa8b3499..3164f91f7 100644 --- a/app/components/dossiers/batch_operation_component.rb +++ b/app/components/dossiers/batch_operation_component.rb @@ -15,7 +15,7 @@ class Dossiers::BatchOperationComponent < ApplicationComponent when Dossier.states.fetch(:en_construction) [BatchOperation.operations.fetch(:passer_en_instruction)] when Dossier.states.fetch(:en_instruction) - [BatchOperation.operations.fetch(:accepter), BatchOperation.operations.fetch(:repasser_en_construction)] + [BatchOperation.operations.fetch(:accepter), BatchOperation.operations.fetch(:refuser), BatchOperation.operations.fetch(:classer_sans_suite), BatchOperation.operations.fetch(:repasser_en_construction)] when Dossier.states.fetch(:accepte), Dossier.states.fetch(:refuse), Dossier.states.fetch(:sans_suite) [BatchOperation.operations.fetch(:archiver)] else @@ -58,10 +58,33 @@ class Dossiers::BatchOperationComponent < ApplicationComponent }, { - label: t(".operations.accepter"), - operation: BatchOperation.operations.fetch(:accepter) - }, + instruction: + [ + { + label: t(".operations.accepter"), + operation_description: t(".operations.accepter_description"), + operation: BatchOperation.operations.fetch(:accepter), + operation_class_name: 'accept', + placeholder: t(".placeholders.accepter") + }, + { + label: t(".operations.refuser"), + operation_description: t(".operations.refuser_description"), + operation: BatchOperation.operations.fetch(:refuser), + operation_class_name: 'refuse', + placeholder: t(".placeholders.refuser") + }, + + { + label: t(".operations.classer_sans_suite"), + operation_description: t(".operations.classer_sans_suite_description"), + operation: BatchOperation.operations.fetch(:classer_sans_suite), + operation_class_name: 'without-continuation', + placeholder: t(".placeholders.classer_sans_suite") + } + ] + }, { label: t(".operations.unfollow"), operation: BatchOperation.operations.fetch(:unfollow) diff --git a/app/components/dossiers/batch_operation_component/batch_operation_component.en.yml b/app/components/dossiers/batch_operation_component/batch_operation_component.en.yml index ba3dd26a5..fb10bc430 100644 --- a/app/components/dossiers/batch_operation_component/batch_operation_component.en.yml +++ b/app/components/dossiers/batch_operation_component/batch_operation_component.en.yml @@ -2,7 +2,19 @@ fr: operations: archiver: 'Archive selected files' passer_en_instruction: 'Change selected files to instructing' + instruction: Instructing files accepter: 'Accept seleted files' + accepter_description: Users will be notified that their file has been accepted + refuser: 'Refuse seleted files' + refuser_description: Users will be notified that their file has been refused + without_continuation: 'Close without continuation seleted files' + without_continuation_description: Users will be notified that their file has been closed without continuation follow: 'Follow seleted files' unfollow: 'Unfollow seleted files' repasser_en_construction: 'Change selected files to in progress' + other: Other batch operations + confirm: Do you confirm the batch operation for selected files ? + placeholders: + accepter: "Explain to users why their file is accepted (optional)" + refuser: "Explain to users why their file is refused (mandatory)" + classer_sans_suite: "Explain to users why their file is closed without continuation (mandatory)" diff --git a/app/components/dossiers/batch_operation_component/batch_operation_component.fr.yml b/app/components/dossiers/batch_operation_component/batch_operation_component.fr.yml index ac1c759ad..ef7980d39 100644 --- a/app/components/dossiers/batch_operation_component/batch_operation_component.fr.yml +++ b/app/components/dossiers/batch_operation_component/batch_operation_component.fr.yml @@ -2,7 +2,19 @@ fr: operations: archiver: 'Archiver les dossiers' passer_en_instruction: 'Passer les dossiers en instruction' + instruction: Instruire les dossiers accepter: 'Accepter les dossiers' + accepter_description: Les usagers seront informés que leur dossier a été accepté + refuser: 'Refuser les dossiers' + refuser_description: Les usagers seront informés que leur dossier a été refusé + classer_sans_suite: 'Classer sans suite les dossiers' + classer_sans_suite_description: Les usagers seront informés que leur dossier a été classé sans suite follow: 'Suivre les dossiers' unfollow: 'Ne plus suivre les dossiers' repasser_en_construction: 'Repasser les dossiers en construction' + other: Autres actions multiples + confirm: Confirmez-vous le traitement multiple des dossiers sélectionnés ? + placeholders: + accepter: "Expliquez aux demandeurs pourquoi leur dossier est accepté (facultatif)" + refuser: "Expliquez aux demandeurs pourquoi leur dossier est accepté (obligatoire)" + classer_sans_suite: "Expliquez aux demandeurs pourquoi leur dossier est accepté (obligatoire)" diff --git a/app/components/dossiers/batch_operation_component/batch_operation_component.html.haml b/app/components/dossiers/batch_operation_component/batch_operation_component.html.haml index 945440874..e3de57caf 100644 --- a/app/components/dossiers/batch_operation_component/batch_operation_component.html.haml +++ b/app/components/dossiers/batch_operation_component/batch_operation_component.html.haml @@ -1,20 +1,20 @@ .batch-operation.fr-ml-auto.flex - if available_operations[:options].count.between?(1,3) - = form_for(BatchOperation.new, url: instructeur_batch_operations_path(procedure_id: procedure.id), method: :post, html: { class: 'form', id: dom_id(BatchOperation.new) }) do |form| + = form_for(BatchOperation.new, url: instructeur_batch_operations_path(procedure_id: procedure.id), method: :post, html: { class: 'form', id: dom_id(BatchOperation.new) }, data: { turbo: true, turbo_confirm: t('.operations.confirm') }) do |form| - available_operations[:options].each do |opt| = render Dossiers::BatchOperationInlineButtonsComponent.new(opt:, icons:, form:) - else - = form_for(BatchOperation.new, url: instructeur_batch_operations_path(procedure_id: procedure.id), method: :post, html: { class: 'form flex', id: dom_id(BatchOperation.new) }) do |form| + = form_for(BatchOperation.new, url: instructeur_batch_operations_path(procedure_id: procedure.id), method: :post, html: { class: 'form flex', id: dom_id(BatchOperation.new) }, data: { turbo: true, turbo_confirm: t('.operations.confirm') }) do |form| - available_operations[:options][0,2].each do |opt| = render Dossiers::BatchOperationInlineButtonsComponent.new(opt:, icons:, form:) .dropdown{ data: { controller: 'menu-button', popover: 'true' } } -# Dropdown button title - %button.fr-btn.fr-btn--sm.fr-btn--secondary.fr-ml-1w.dropdown-button{ disabled: true, data: { menu_button_target: 'button', batch_operation_target: 'menu' } } - Autres actions multiples + %button#batch_operation_others.fr-btn.fr-btn--sm.fr-btn--secondary.fr-ml-1w.dropdown-button{ disabled: true, data: { menu_button_target: 'button', batch_operation_target: 'dropdown' } } + = t('.operations.other') - #state-menu.dropdown-content.fade-in-down{ data: { menu_button_target: 'menu' } } + #state-menu.dropdown-content.fade-in-down{ data: { menu_button_target: 'menu' }, "aria-labelledby" => "batch_operation_others" } %ul.dropdown-items - available_operations[:options][2, available_operations[:options].count].each do |opt| %li{ 'data-turbo': 'true' } diff --git a/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.en.yml b/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.en.yml new file mode 100644 index 000000000..d1854186d --- /dev/null +++ b/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.en.yml @@ -0,0 +1,3 @@ +en: + labels: + instruction: Instruct files diff --git a/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.fr.yml b/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.fr.yml new file mode 100644 index 000000000..d9a2ed553 --- /dev/null +++ b/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.fr.yml @@ -0,0 +1,3 @@ +fr: + labels: + instruction: Instruire les dossiers diff --git a/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.html.haml b/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.html.haml index b40976b61..f3f1e6ca5 100644 --- a/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.html.haml +++ b/app/components/dossiers/batch_operation_inline_buttons_component/batch_operation_inline_buttons_component.html.haml @@ -1,31 +1,17 @@ -- if opt[:operation] == 'accepter' - .dropdown{ data: { controller: 'menu-button', popover: 'true', operation: opt[:operation] }, id: 'dropdown_batch' } - -# Dropdown button title - %button{ disabled: true, class: ['fr-btn fr-btn--sm fr-btn--icon-left fr-ml-1w', icons[opt[:operation].to_sym]], disabled: true, name: "#{form.object_name}[operation]" , data: { menu_button_target: 'button' } } - = opt[:label] +- if opt.keys.include?(:instruction) + = render Dropdown::MenuComponent.new(wrapper: :div, wrapper_options: { data: {controller: 'menu-button', popover: 'true', operation: opt[:operation]} }, menu_options: { id: "dropdown_batch" }, button_options: { disabled: true, data: { batch_operation_target: "dropdown" }, class: "fr-btn fr-btn--sm fr-ml-1w"}, role: :region ) do |menu| + - menu.with_button_inner_html do + = t(".labels.instruction") - #state-menu.dropdown-content.fade-in-down{ data: { menu_button_target: 'menu' } } - %ul.dropdown-items - %li.inactive{ 'data-turbo': 'true' } - - if opt[:operation] == 'accepter' - .wrapper - .dropdown-items-link - %span{ class: icons[opt[:operation].to_sym] } - .dropdown-description - %h4= opt[:label] - - .motivation.accept - = form.text_area :motivation, class: 'fr-input' - #justificatif_motivation_suggest_accept.optional-justificatif - %button.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline.fr-btn--icon-left.fr-icon-attachment-line.fr-ml-0{ type: 'button', onclick: "DS.showImportJustificatif('accept');" } Ajouter un justificatif (optionnel) - #justificatif_motivation_import_accept.hidden - = form.file_field :justificatif_motivation, direct_upload: true, id: "dossier_justificatif_motivation_accept", onchange: "DS.showDeleteJustificatif('accept');" - - .hidden.js_delete_motivation{ id: "delete_motivation_import_accept" } - %button.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline.fr-btn--icon-left.fr-icon-delete-line.fr-ml-0.fr-mt-1w{ type: 'button', onclick: "DS.deleteJustificatif('accept');" } Supprimer le justificatif - - = button_tag "Annuler", type: :reset, class: 'fr-btn fr-btn--sm fr-btn--secondary', onclick: 'DS.motivationCancelBatch();' - = form.button "Valider la décision", class: ['fr-btn fr-btn--sm fr-mt-2w'], disabled: true, name: "#{form.object_name}[operation]", value: opt[:operation] + - opt[:instruction].each do |opt| + - menu.with_item do + = link_to('#', onclick: "DS.showMotivation(event, '#{opt[:operation_class_name]}');", role: 'menuitem') do + %span{ class: "icon #{opt[:operation_class_name]}" } + .dropdown-description + %h4= opt[:label] + = opt[:operation_description] + - menu.with_item(class: "hidden inactive form-inside fr-pt-1v") do + = render partial: 'instructeurs/dossiers/instruction_button_motivation_batch', locals: { instruction_operation: opt[:operation_class_name], form:, opt: } - else = form.button opt[:label], class: ['fr-btn fr-btn--sm fr-btn--icon-left fr-ml-1w', icons[opt[:operation].to_sym]], disabled: true, name: "#{form.object_name}[operation]", value: opt[:operation], data: { operation: opt[:operation] } diff --git a/app/components/dropdown/menu_component.rb b/app/components/dropdown/menu_component.rb index 1bbbbd1cf..964478d34 100644 --- a/app/components/dropdown/menu_component.rb +++ b/app/components/dropdown/menu_component.rb @@ -51,4 +51,12 @@ class Dropdown::MenuComponent < ApplicationComponent def button_class_names ['fr-btn', 'dropdown-button'] + Array(@button_options[:class]) end + + def disabled? + @button_options[:disabled] == true + end + + def data + { menu_button_target: 'button' }.deep_merge(@button_options[:data].to_h) + end end diff --git a/app/components/dropdown/menu_component/menu_component.html.haml b/app/components/dropdown/menu_component/menu_component.html.haml index 441ba1db7..b1a98d8bc 100644 --- a/app/components/dropdown/menu_component/menu_component.html.haml +++ b/app/components/dropdown/menu_component/menu_component.html.haml @@ -1,5 +1,5 @@ = content_tag(@wrapper, wrapper_options) do - %button{ class: button_class_names, id: button_id, data: { menu_button_target: 'button' }, "aria-expanded": "false", 'aria-haspopup': 'true', 'aria-controls': menu_id } + %button{ class: button_class_names, id: button_id, disabled: disabled?, data: data, "aria-expanded": "false", 'aria-haspopup': 'true', 'aria-controls': menu_id } = button_inner_html %div{ data: { menu_button_target: 'menu' }, id: menu_id, 'aria-labelledby': button_id, role: menu_role, 'tab-index': -1, class: menu_class_names } @@ -7,7 +7,7 @@ -# the dropdown can be a menu with a list of item - if items? - %ul.dropdown-items.fr-pl-0{ role: 'none' } + %ul.dropdown-items{ role: 'none' } - items.each do |dropdown_item| = dropdown_item -# the dropdown can be a menu with forms diff --git a/app/components/instructeurs/instruction_menu_component/instruction_menu_component.en.yml b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.en.yml index 165ddbeaf..34a90708d 100644 --- a/app/components/instructeurs/instruction_menu_component/instruction_menu_component.en.yml +++ b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.en.yml @@ -1,3 +1,7 @@ --- en: instruct: Instruct the file + placeholders: + accepter: "Explain to the user why this file is accepted (optional)" + refuser: "Explain to the user why this file is refused (mandatory)" + classer_sans_suite: "Explain to the user why this file is closed without continuation (mandatory)" diff --git a/app/components/instructeurs/instruction_menu_component/instruction_menu_component.fr.yml b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.fr.yml index e82e45e9e..76d3c259a 100644 --- a/app/components/instructeurs/instruction_menu_component/instruction_menu_component.fr.yml +++ b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.fr.yml @@ -1,3 +1,7 @@ --- fr: instruct: Instruire le dossier + placeholders: + accepter: "Expliquez au demandeur pourquoi ce dossier est accepté (facultatif)" + refuser: "Expliquez au demandeur pourquoi ce dossier est refusé (obligatoire)" + classer_sans_suite: "Expliquez au demandeur pourquoi ce dossier est classé sans suite (obligatoire)" diff --git a/app/javascript/controllers/batch_operation_controller.ts b/app/javascript/controllers/batch_operation_controller.ts index dcb719dc5..972552ad8 100644 --- a/app/javascript/controllers/batch_operation_controller.ts +++ b/app/javascript/controllers/batch_operation_controller.ts @@ -3,11 +3,11 @@ import { disable, enable, show, hide } from '@utils'; import invariant from 'tiny-invariant'; export class BatchOperationController extends ApplicationController { - static targets = ['menu', 'input']; + static targets = ['menu', 'input', 'dropdown']; - declare readonly menuTarget: HTMLButtonElement; - declare readonly hasMenuTarget: boolean; + declare readonly menuTargets: HTMLButtonElement[]; declare readonly inputTargets: HTMLInputElement[]; + declare readonly dropdownTargets: HTMLButtonElement[]; onCheckOne() { this.toggleSubmitButtonWhenNeeded(); @@ -58,6 +58,37 @@ export class BatchOperationController extends ApplicationController { } } + onSubmitInstruction(event: { srcElement: HTMLInputElement }) { + const field_refuse = document.querySelector( + '.js_batch_operation_motivation_refuse' + ); + + const field_without_continuation = document.querySelector( + '.js_batch_operation_motivation_without-continuation' + ); + + if (field_refuse != null) { + if (event.srcElement.value == 'refuser' && field_refuse.value == '') { + field_refuse.setCustomValidity('La motivation doit être remplie'); + } else { + field_refuse.setCustomValidity(''); + } + } + + if (field_without_continuation != null) { + if ( + event.srcElement.value == 'classer_sans_suite' && + field_without_continuation.value == '' + ) { + field_without_continuation.setCustomValidity( + 'La motivation doit être remplie' + ); + } else { + field_without_continuation.setCustomValidity(''); + } + } + } + onDeleteSelection(event: { preventDefault: () => void }) { event.preventDefault(); emptyCheckboxes(); @@ -78,18 +109,38 @@ export class BatchOperationController extends ApplicationController { switchButton(button, available); return available; }); - if (this.hasMenuTarget) { + + if (this.menuTargets.length) { if (available.length) { - enable(this.menuTarget); + this.menuTargets.forEach((e) => enable(e)); } else { - disable(this.menuTarget); + this.menuTargets.forEach((e) => disable(e)); } } + + this.dropdownTargets.forEach((dropdown) => { + const buttons = Array.from( + document.querySelectorAll( + `[aria-labelledby='${dropdown.id}'] button[data-operation]` + ) + ); + + const disabled = buttons.every((button) => button.disabled); + + if (disabled) { + disable(dropdown); + } else { + enable(dropdown); + } + }); + + // pour chaque chaque dropdown, on va chercher tous les boutons + // si tous les boutons sont disabled, on disable le dropdown } else { - if (this.hasMenuTarget) { - disable(this.menuTarget); - } + this.menuTargets.forEach((e) => disable(e)); buttons.forEach((button) => switchButton(button, false)); + + this.dropdownTargets.forEach((e) => disable(e)); } } } diff --git a/app/models/batch_operation.rb b/app/models/batch_operation.rb index 0fe2d43e0..4c3e4e345 100644 --- a/app/models/batch_operation.rb +++ b/app/models/batch_operation.rb @@ -18,6 +18,8 @@ class BatchOperation < ApplicationRecord enum operation: { accepter: 'accepter', + refuser: 'refuser', + classer_sans_suite: 'classer_sans_suite', archiver: 'archiver', follow: 'follow', passer_en_instruction: 'passer_en_instruction', @@ -61,6 +63,10 @@ class BatchOperation < ApplicationRecord query.state_en_construction when BatchOperation.operations.fetch(:accepter) then query.state_en_instruction + when BatchOperation.operations.fetch(:refuser) then + query.state_en_instruction + when BatchOperation.operations.fetch(:classer_sans_suite) then + query.state_en_instruction when BatchOperation.operations.fetch(:follow) then query.without_followers.en_cours when BatchOperation.operations.fetch(:repasser_en_construction) then @@ -83,6 +89,10 @@ class BatchOperation < ApplicationRecord dossier.passer_en_instruction(instructeur: instructeur) when BatchOperation.operations.fetch(:accepter) dossier.accepter(instructeur: instructeur, motivation: motivation, justificatif: justificatif_motivation) + when BatchOperation.operations.fetch(:refuser) + dossier.refuser(instructeur: instructeur, motivation: motivation, justificatif: justificatif_motivation) + when BatchOperation.operations.fetch(:classer_sans_suite) + dossier.classer_sans_suite(instructeur: instructeur, motivation: motivation, justificatif: justificatif_motivation) when BatchOperation.operations.fetch(:follow) instructeur.follow(dossier) when BatchOperation.operations.fetch(:repasser_en_construction) diff --git a/app/views/instructeurs/dossiers/_instruction_button.html.haml b/app/views/instructeurs/dossiers/_instruction_button.html.haml index 85a5b10ca..5c528bcf9 100644 --- a/app/views/instructeurs/dossiers/_instruction_button.html.haml +++ b/app/views/instructeurs/dossiers/_instruction_button.html.haml @@ -12,7 +12,7 @@ L’usager sera informé que son dossier a été accepté - menu.with_item(class: "hidden inactive form-inside") do - = render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: 'Expliquez au demandeur pourquoi ce dossier est accepté (facultatif)', popup_class: 'accept', process_action: 'accepter', title: 'Accepter', confirm: "Confirmez-vous l'acceptation ce dossier ?" } + = render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: placeholder, popup_class: 'accept', process_action: 'accepter', title: 'Accepter', confirm: "Confirmez-vous l'acceptation ce dossier ?" } - menu.with_item do @@ -23,7 +23,7 @@ L’usager sera informé que son dossier a été classé sans suite - menu.with_item(class: "hidden inactive form-inside") do - = render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: 'Expliquez au demandeur pourquoi ce dossier est classé sans suite (obligatoire)', popup_class: 'without-continuation', process_action: 'classer_sans_suite', title: 'Classer sans suite', confirm: 'Confirmez-vous le classement sans suite de ce dossier ?' } + = render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: placeholder, popup_class: 'without-continuation', process_action: 'classer_sans_suite', title: 'Classer sans suite', confirm: 'Confirmez-vous le classement sans suite de ce dossier ?' } - menu.with_item do = link_to('#', onclick: "DS.showMotivation(event, 'refuse');", role: 'menuitem') do @@ -33,7 +33,7 @@ L’usager sera informé que son dossier a été refusé - menu.with_item(class: "hidden inactive form-inside") do - = render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: 'Expliquez au demandeur pourquoi ce dossier est refusé (obligatoire)', popup_class: 'refuse', process_action: 'refuser', title: 'Refuser', confirm: 'Confirmez-vous le refus de ce dossier ?' } + = render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: placeholder, popup_class: 'refuse', process_action: 'refuser', title: 'Refuser', confirm: 'Confirmez-vous le refus de ce dossier ?' } - if dossier.may_flag_as_pending_correction? - menu.with_item do diff --git a/app/views/instructeurs/dossiers/_instruction_button_motivation_batch.html.haml b/app/views/instructeurs/dossiers/_instruction_button_motivation_batch.html.haml new file mode 100644 index 000000000..654babf26 --- /dev/null +++ b/app/views/instructeurs/dossiers/_instruction_button_motivation_batch.html.haml @@ -0,0 +1,13 @@ +%div{ class: "motivation #{instruction_operation}" } + = form.text_area :motivation, class: "fr-input js_batch_operation_motivation_#{instruction_operation}", placeholder: opt[:placeholder] + .optional-justificatif{ id: "justificatif_motivation_suggest_#{instruction_operation}" } + %button.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline.fr-btn--icon-left.fr-icon-attachment-line.fr-ml-0{ type: 'button', onclick: "DS.showImportJustificatif('#{instruction_operation}');" } Ajouter un justificatif (optionnel) + .hidden{ id: "justificatif_motivation_import_#{instruction_operation}" } + = form.file_field :justificatif_motivation, direct_upload: true, id: "dossier_justificatif_motivation_#{instruction_operation}", onchange: "DS.showDeleteJustificatif('#{instruction_operation}');" + + .hidden.js_delete_motivation{ id: "delete_motivation_import_#{instruction_operation}" } + %button.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline.fr-btn--icon-left.fr-icon-delete-line.fr-ml-0.fr-mt-1w{ type: 'button', onclick: "DS.deleteJustificatif('#{instruction_operation}');" } Supprimer le justificatif + + = button_tag "Annuler", type: :reset, class: 'fr-btn fr-btn--sm fr-btn--secondary', onclick: 'DS.motivationCancelBatch();' + + = form.button "Valider la décision", class: ['fr-btn fr-btn--sm fr-mt-2w'], disabled: true, name: "#{form.object_name}[operation]", value: opt[:operation], data: { operation: opt[:operation], action: "batch-operation#onSubmitInstruction" } diff --git a/spec/components/dossiers/batch_alert_component_spec.rb b/spec/components/dossiers/batch_alert_component_spec.rb index 303e004cd..d43c3bf40 100644 --- a/spec/components/dossiers/batch_alert_component_spec.rb +++ b/spec/components/dossiers/batch_alert_component_spec.rb @@ -166,6 +166,78 @@ RSpec.describe Dossiers::BatchAlertComponent, type: :component do end end + describe 'refuser' do + let(:component) do + described_class.new( + batch: batch_operation, + procedure: procedure + ) + end + let!(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } + let!(:dossier_2) { create(:dossier, :en_instruction, procedure: procedure) } + let!(:batch_operation) { create(:batch_operation, operation: :refuser, dossiers: [dossier, dossier_2], instructeur: instructeur) } + + context 'in_progress' do + before { + batch_operation.track_processed_dossier(true, dossier) + batch_operation.reload + } + + it { is_expected.to have_selector('.fr-alert--info') } + it { is_expected.to have_text("Une action de masse est en cours") } + it { is_expected.to have_text("1/2 dossiers ont été refusés") } + end + + context 'finished and success' do + before { + batch_operation.track_processed_dossier(true, dossier) + batch_operation.track_processed_dossier(true, dossier_2) + batch_operation.reload + } + + it { is_expected.to have_selector('.fr-alert--success') } + it { is_expected.to have_text("L’action de masse est terminée") } + it { is_expected.to have_text("2 dossiers ont été refusés") } + it { expect(batch_operation.seen_at).to eq(nil) } + end + end + + describe 'classer_sans_suite' do + let(:component) do + described_class.new( + batch: batch_operation, + procedure: procedure + ) + end + let!(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } + let!(:dossier_2) { create(:dossier, :en_instruction, procedure: procedure) } + let!(:batch_operation) { create(:batch_operation, operation: :classer_sans_suite, dossiers: [dossier, dossier_2], instructeur: instructeur) } + + context 'in_progress' do + before { + batch_operation.track_processed_dossier(true, dossier) + batch_operation.reload + } + + it { is_expected.to have_selector('.fr-alert--info') } + it { is_expected.to have_text("Une action de masse est en cours") } + it { is_expected.to have_text("1/2 dossiers ont été classés sans suite") } + end + + context 'finished and success' do + before { + batch_operation.track_processed_dossier(true, dossier) + batch_operation.track_processed_dossier(true, dossier_2) + batch_operation.reload + } + + it { is_expected.to have_selector('.fr-alert--success') } + it { is_expected.to have_text("L’action de masse est terminée") } + it { is_expected.to have_text("2 dossiers ont été classés sans suite") } + it { expect(batch_operation.seen_at).to eq(nil) } + end + end + describe 'follow' do let(:component) do described_class.new( diff --git a/spec/components/dossiers/batch_operation_component_spec.rb b/spec/components/dossiers/batch_operation_component_spec.rb index 7494b6e46..b6a51e33d 100644 --- a/spec/components/dossiers/batch_operation_component_spec.rb +++ b/spec/components/dossiers/batch_operation_component_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Dossiers::BatchOperationComponent, type: :component do context 'statut suivis' do let(:statut) { 'suivis' } it { is_expected.to have_button('Passer les dossiers en instruction', disabled: true) } - it { is_expected.to have_button('Accepter les dossiers', disabled: true) } + it { is_expected.to have_button('Instruire les dossiers', disabled: true) } it { is_expected.to have_button('Autres actions multiples', disabled: true) } it { is_expected.to have_button('Repasser les dossiers en construction', disabled: true) } it { is_expected.to have_button('Ne plus suivre les dossiers', disabled: true) } diff --git a/spec/factories/batch_operation.rb b/spec/factories/batch_operation.rb index e82e396b7..9d91cfb05 100644 --- a/spec/factories/batch_operation.rb +++ b/spec/factories/batch_operation.rb @@ -40,6 +40,28 @@ FactoryBot.define do end end + trait :refuser do + operation { BatchOperation.operations.fetch(:refuser) } + after(:build) do |batch_operation, evaluator| + procedure = create(:simple_procedure, :published, instructeurs: [evaluator.invalid_instructeur.presence || batch_operation.instructeur], administrateurs: [create(:administrateur)]) + batch_operation.dossiers = [ + create(:dossier, :with_individual, :en_instruction, procedure: procedure), + create(:dossier, :with_individual, :en_instruction, procedure: procedure) + ] + end + end + + trait :classer_sans_suite do + operation { BatchOperation.operations.fetch(:classer_sans_suite) } + after(:build) do |batch_operation, evaluator| + procedure = create(:simple_procedure, :published, instructeurs: [evaluator.invalid_instructeur.presence || batch_operation.instructeur], administrateurs: [create(:administrateur)]) + batch_operation.dossiers = [ + create(:dossier, :with_individual, :en_instruction, procedure: procedure), + create(:dossier, :with_individual, :en_instruction, procedure: procedure) + ] + end + end + trait :follow do operation { BatchOperation.operations.fetch(:follow) } after(:build) do |batch_operation, evaluator| diff --git a/spec/jobs/batch_operation_process_one_job_spec.rb b/spec/jobs/batch_operation_process_one_job_spec.rb index 24946cd09..e808ac59f 100644 --- a/spec/jobs/batch_operation_process_one_job_spec.rb +++ b/spec/jobs/batch_operation_process_one_job_spec.rb @@ -154,6 +154,48 @@ describe BatchOperationProcessOneJob, type: :job do end end + context 'when operation is "refuser"' do + let(:batch_operation) do + create(:batch_operation, :refuser, + options.merge(instructeur: create(:instructeur), motivation: 'motivation')) + end + + it 'refuses the dossier in the batch' do + expect { subject.perform_now } + .to change { dossier_job.reload.refuse? } + .from(false) + .to(true) + end + + it 'refuses the dossier in the batch with a motivation' do + expect { subject.perform_now } + .to change { dossier_job.reload.motivation } + .from(nil) + .to('motivation') + end + end + + context 'when operation is "classer_sans_suite"' do + let(:batch_operation) do + create(:batch_operation, :classer_sans_suite, + options.merge(instructeur: create(:instructeur), motivation: 'motivation')) + end + + it 'closes without continuation the dossier in the batch' do + expect { subject.perform_now } + .to change { dossier_job.reload.sans_suite? } + .from(false) + .to(true) + end + + it 'closes without continuation the dossier in the batch with a motivation' do + expect { subject.perform_now } + .to change { dossier_job.reload.motivation } + .from(nil) + .to('motivation') + end + end + context 'when the dossier is out of sync (ie: someone applied a transition somewhere we do not know)' do let(:instructeur) { create(:instructeur) } let(:procedure) { create(:simple_procedure, instructeurs: [instructeur]) } diff --git a/spec/system/instructeurs/batch_operation_spec.rb b/spec/system/instructeurs/batch_operation_spec.rb index 9be184ffd..acd0e79e2 100644 --- a/spec/system/instructeurs/batch_operation_spec.rb +++ b/spec/system/instructeurs/batch_operation_spec.rb @@ -31,18 +31,21 @@ describe 'BatchOperation a dossier:', js: true do expect(page).to have_button("Archiver les dossiers") # ensure batch is created - expect { click_on "Archiver les dossiers" } - .to change { BatchOperation.count } - .from(0).to(1) + + page.accept_alert do + click_on "Archiver les dossiers" + end # ensure batched dossier is disabled expect(page).to have_selector("##{checkbox_id}[disabled]") + # ensure Batch is created + expect(BatchOperation.count).to eq(1) # check a11y with disabled checkbox expect(page).to be_axe_clean # ensure alert is present expect(page).to have_content("Information : Une action de masse est en cours") - expect(page).to have_content("1 dossier sera archivé") + expect(page).to have_content("1 dossier est en cours d'archivage") # ensure jobs are queued perform_enqueued_jobs(only: [BatchOperationEnqueueAllJob]) @@ -71,10 +74,14 @@ describe 'BatchOperation a dossier:', js: true do end # submit checkall - expect { click_on "Archiver les dossiers" } - .to change { BatchOperation.count } - .from(1).to(2) + page.accept_alert do + click_on "Archiver les dossiers" + end + # reload + visit instructeur_procedure_path(procedure, statut: 'traites') + + expect(BatchOperation.count).to eq(2) expect(BatchOperation.last.dossiers).to match_array([dossier_2, dossier_3]) end @@ -109,10 +116,14 @@ describe 'BatchOperation a dossier:', js: true do find("##{dom_id(BatchOperation.new, :checkbox_all)}").check click_on("Sélectionner tous les 3 dossiers") - expect { click_on "Suivre les dossiers" } - .to change { BatchOperation.count } - .from(0).to(1) + accept_alert do + click_on "Suivre les dossiers" + end + # reload + visit instructeur_procedure_path(procedure, statut: 'a-suivre') + + expect(BatchOperation.count).to eq(1) expect(BatchOperation.last.dossiers).to match_array([dossier_1, dossier_2, dossier_3]) end @@ -138,10 +149,14 @@ describe 'BatchOperation a dossier:', js: true do expect(find_field("batch_operation[dossier_ids][]", type: :hidden).value).to eq "#{dossier_4.id},#{dossier_3.id},#{dossier_2.id}" # create batch - expect { click_on "Suivre les dossiers" } - .to change { BatchOperation.count } - .from(0).to(1) + accept_alert do + click_on "Suivre les dossiers" + end + # reload + visit instructeur_procedure_path(procedure, statut: 'a-suivre') + + expect(BatchOperation.count).to eq(1) expect(BatchOperation.last.dossiers).to match_array([dossier_2, dossier_3, dossier_4]) end end