Merge pull request #9274 from demarches-simplifiees/bach-operation-all-instruction-actions
[Actions multiples] Ajouter la possibilité pour les instructeurs de classer sans suite et refuser
This commit is contained in:
commit
3e30834644
23 changed files with 406 additions and 88 deletions
|
@ -218,6 +218,8 @@
|
|||
ul.dropdown-items {
|
||||
padding-inline-start: 0;
|
||||
list-style: none;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dropdown-items {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
en:
|
||||
labels:
|
||||
instruction: Instruct files
|
|
@ -0,0 +1,3 @@
|
|||
fr:
|
||||
labels:
|
||||
instruction: Instruire les dossiers
|
|
@ -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] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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<HTMLInputElement>(
|
||||
'.js_batch_operation_motivation_refuse'
|
||||
);
|
||||
|
||||
const field_without_continuation = document.querySelector<HTMLInputElement>(
|
||||
'.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<HTMLButtonElement>(
|
||||
`[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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" }
|
|
@ -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(
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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]) }
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue