feat(instructeur): can flag a dossier as "pending corrections"

This commit is contained in:
Colin Darie 2023-03-14 17:23:17 +01:00
parent 5d61c6fa35
commit ca3b127942
No known key found for this signature in database
GPG key ID: 4FB865FDBCA4BCC4
13 changed files with 254 additions and 32 deletions

View file

@ -7,3 +7,4 @@ en:
automatic_email: Automatic email automatic_email: Automatic email
you: You you: You
deleted_body: Message deleted deleted_body: Message deleted
flagged_pending_corrections: Modification requested

View file

@ -7,3 +7,4 @@ fr:
automatic_email: Email automatique automatic_email: Email automatique
you: Vous you: Vous
deleted_body: Message supprimé deleted_body: Message supprimé
flagged_pending_corrections: Modification demandée

View file

@ -6,6 +6,9 @@
= commentaire_issuer = commentaire_issuer
- if commentaire_from_guest? - if commentaire_from_guest?
%span.fr-text--xs.fr-text-mention--grey.font-weight-normal= t('.guest') %span.fr-text--xs.fr-text-mention--grey.font-weight-normal= t('.guest')
- if commentaire.flagged_pending_corrections?
%span.fr-badge.fr-badge--sm.fr-badge--info
= t('.flagged_pending_corrections')
%span.date{ class: ["fr-text--xs", "fr-text-mention--grey", "font-weight-normal", highlight_if_unseen_class], data: scroll_to_target } %span.date{ class: ["fr-text--xs", "fr-text-mention--grey", "font-weight-normal", highlight_if_unseen_class], data: scroll_to_target }
= commentaire_date = commentaire_date
.rich-text .rich-text

View file

@ -223,6 +223,34 @@ module Instructeurs
render :change_state render :change_state
end end
def pending_corrections
message, piece_jointe = params.require(:dossier).permit(:motivation, :justificatif_motivation).values
if message.empty?
flash.alert = "Vous devez préciser quelle modification est attendue."
elsif !dossier.may_flag_as_pending_correction?
flash.alert = dossier.termine? ? "Impossible de demander de corriger un dossier terminé." : "Le dossier est déjà en attente de correction."
else
commentaire = CommentaireService.create(current_instructeur, dossier, { body: message, piece_jointe: })
dossier.flag_as_pending_correction!(commentaire)
dossier.update!(last_commentaire_updated_at: Time.zone.now)
current_instructeur.follow(dossier)
flash.notice = "Dossier marqué comme en attente de correction."
end
respond_to do |format|
format.turbo_stream do
@dossier = dossier
render :change_state
end
format.html do
redirect_back(fallback_location: instructeur_procedure_path(procedure))
end
end
end
def create_commentaire def create_commentaire
@commentaire = CommentaireService.create(current_instructeur, dossier, commentaire_params) @commentaire = CommentaireService.create(current_instructeur, dossier, commentaire_params)

View file

@ -95,6 +95,10 @@ class Commentaire < ApplicationRecord
update! body: '' update! body: ''
end end
def flagged_pending_corrections?
DossierResolution.exists?(commentaire: self)
end
private private
def notify def notify

View file

@ -4,6 +4,22 @@ module DossierResolvableConcern
included do included do
has_many :resolutions, class_name: 'DossierResolution', dependent: :destroy has_many :resolutions, class_name: 'DossierResolution', dependent: :destroy
def flag_as_pending_correction!(commentaire)
return unless may_flag_as_pending_correction?
resolutions.create(commentaire:)
return if en_construction?
repasser_en_construction!(instructeur: commentaire.instructeur)
end
def may_flag_as_pending_correction?
return false if resolutions.pending.exists?
en_construction? || may_repasser_en_construction?
end
def pending_resolution? def pending_resolution?
# We don't want to show any alert if user is not allowed to modify the dossier # We don't want to show any alert if user is not allowed to modify the dossier
return false unless en_construction? return false unless en_construction?

View file

@ -1,8 +1,9 @@
- if dossier.en_instruction? - if dossier.en_instruction? || (dossier.en_construction? && dossier.may_flag_as_pending_correction?)
= render Dropdown::MenuComponent.new(wrapper: :div, wrapper_options: { data: { turbo_force: :server } }, button_options: { class: [button_or_label_class(dossier)]}, role: dossier.en_instruction? ? :region : :menu) do |menu| = render Dropdown::MenuComponent.new(wrapper: :div, wrapper_options: { data: { turbo_force: :server } }, button_options: { class: [button_or_label_class(dossier)]}, role: dossier.en_instruction? ? :region : :menu) do |menu|
- menu.with_button_inner_html do - menu.with_button_inner_html do
Instruire le dossier = dossier.en_instruction? ? "Instruire le dossier" : "Demander une modification"
- if dossier.en_instruction?
- menu.with_item do - menu.with_item do
= link_to('#', onclick: "DS.showMotivation(event, 'accept');", role: 'menuitem') do = link_to('#', onclick: "DS.showMotivation(event, 'accept');", role: 'menuitem') do
%span.icon.accept %span.icon.accept
@ -33,3 +34,23 @@
- menu.with_item(class: "hidden inactive form-inside") do - 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: '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 ?' }
- if dossier.may_flag_as_pending_correction?
- menu.with_item do
= link_to('#', onclick: "DS.showMotivation(event, 'pending_corrections');", role: 'menuitem') do
%span.fr-icon.fr-icon-error-warning-line.fr-text-default--info{ "aria-hidden": "true" }
.dropdown-description
%h4 Demander une modification
Lusager sera informé que des modifications sont attendues
- menu.with_item(class: class_names("inactive form-inside": true, hidden: dossier.en_instruction?)) do
= render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier,
visible: !dossier.en_instruction?,
form_path: pending_corrections_instructeur_dossier_path(dossier.procedure, dossier),
placeholder: 'Expliquez au demandeur quelles modifications sont attendues',
popup_class: 'pending_corrections',
button_justificatif_label: "Ajouter une pièce jointe (facultatif)",
process_button: dossier.en_construction? ? 'Valider' : 'Valider et repasser en construction',
process_action: nil,
title: 'Marquer en attente de corrections',
confirm: 'Envoyer la demande de corrections ?'}

View file

@ -1,5 +1,5 @@
.motivation.hidden{ class: popup_class } .motivation{ class: class_names(popup_class => true, hidden: !defined?(visible) || !visible) }
= form_tag(terminer_instructeur_dossier_path(dossier.procedure, dossier), data: { turbo: true, turbo_confirm: confirm }, method: :post, multipart: true) do = form_tag(defined?(form_path) ? form_path : terminer_instructeur_dossier_path(dossier.procedure, dossier), data: { turbo: true, turbo_confirm: confirm }, method: :post, multipart: true) do
- if title == 'Accepter' - if title == 'Accepter'
= text_area :dossier, :motivation, class: 'fr-input', placeholder: placeholder, required: false = text_area :dossier, :motivation, class: 'fr-input', placeholder: placeholder, required: false
- if dossier.attestation_template&.activated? - if dossier.attestation_template&.activated?
@ -28,11 +28,11 @@
- else - else
= text_area :dossier, :motivation, class: 'fr-input', placeholder: placeholder, required: true = text_area :dossier, :motivation, class: 'fr-input', placeholder: placeholder, required: true
.optional-justificatif{ id: "justificatif_motivation_suggest_#{popup_class}", onclick: "DS.showImportJustificatif('#{popup_class}');" } .optional-justificatif{ id: "justificatif_motivation_suggest_#{popup_class}", onclick: "DS.showImportJustificatif('#{popup_class}');" }
%button.fr-btn.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) %button.fr-btn.fr-btn--tertiary-no-outline.fr-btn--icon-left.fr-icon-attachment-line.fr-ml-0{ type: 'button', onclick: "DS.showImportJustificatif('accept');" }= defined?(button_justificatif_label) ? button_justificatif_label : "Ajouter un justificatif (optionnel)"
.hidden{ id: "justificatif_motivation_import_#{popup_class}" } .hidden{ id: "justificatif_motivation_import_#{popup_class}" }
= file_field :dossier, :justificatif_motivation, direct_upload: true, id: "dossier_justificatif_motivation_#{popup_class}",onchange: "DS.showDeleteJustificatif('#{popup_class}');" = file_field :dossier, :justificatif_motivation, direct_upload: true, id: "dossier_justificatif_motivation_#{popup_class}",onchange: "DS.showDeleteJustificatif('#{popup_class}');"
.hidden.js_delete_motivation{ id: "delete_motivation_import_#{popup_class}" } .hidden.js_delete_motivation{ id: "delete_motivation_import_#{popup_class}" }
%button.fr-btn.fr-btn--tertiary-no-outline.fr-btn--icon-left.fr-icon-delete-line.fr-ml-0.fr-mt-1w{ type: 'button', onclick: "DS.deleteJustificatif('#{popup_class}');" } Supprimer le justificatif %button.fr-btn.fr-btn--tertiary-no-outline.fr-btn--icon-left.fr-icon-delete-line.fr-ml-0.fr-mt-1w{ type: 'button', onclick: "DS.deleteJustificatif('#{popup_class}');" } Supprimer le justificatif
.fr-mt-2w .fr-mt-2w
= button_tag "Annuler", type: :reset, class: 'fr-btn fr-btn--secondary', onclick: 'DS.motivationCancel();' = button_tag "Annuler", type: :reset, class: 'fr-btn fr-btn--secondary', onclick: 'DS.motivationCancel();'
= button_tag 'Valider la décision', name: :process_action, value: process_action, class: 'fr-btn fr-mr-0', title: title = button_tag defined?(process_button) ? process_button : 'Valider la décision', name: :process_action, value: process_action, class: 'fr-btn fr-mr-0', title: title

View file

@ -445,6 +445,7 @@ Rails.application.routes.draw do
post 'repasser-en-construction' => 'dossiers#repasser_en_construction' post 'repasser-en-construction' => 'dossiers#repasser_en_construction'
post 'repasser-en-instruction' => 'dossiers#repasser_en_instruction' post 'repasser-en-instruction' => 'dossiers#repasser_en_instruction'
post 'terminer' post 'terminer'
post 'pending_corrections'
post 'send-to-instructeurs' => 'dossiers#send_to_instructeurs' post 'send-to-instructeurs' => 'dossiers#send_to_instructeurs'
post 'avis' => 'dossiers#create_avis' post 'avis' => 'dossiers#create_avis'
get 'print' => 'dossiers#print' get 'print' => 'dossiers#print'

View file

@ -496,6 +496,90 @@ describe Instructeurs::DossiersController, type: :controller do
end end
end end
describe '#pending_corrections' do
let(:message) { 'do that' }
let(:justificatif) { nil }
subject do
post :pending_corrections, params: {
procedure_id: procedure.id, dossier_id: dossier.id,
dossier: { motivation: message, justificatif_motivation: justificatif }
}, format: :turbo_stream
end
before { sign_in(instructeur.user) }
context "dossier en instruction" do
let(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure: procedure) }
before { subject }
it 'pass en_construction and create a pending resolution' do
expect(response).to have_http_status(:ok)
expect(response.body).to include('en attente de modifications')
expect(dossier.reload).to be_en_construction
expect(dossier).to be_pending_resolution
end
it 'create a comment with text body' do
expect(dossier.commentaires.last.body).to eq("do that")
expect(dossier.commentaires.last).to be_flagged_pending_corrections
end
context 'with an attachment' do
let(:justificatif) { fake_justificatif }
it 'attach file to comment' do
expect(dossier.commentaires.last.piece_jointe).to be_attached
end
end
context 'with an empty message' do
let(:message) { '' }
it 'requires a message' do
expect(dossier.reload).not_to be_pending_resolution
expect(dossier.commentaires.count).to eq(0)
expect(response.body).to include('Vous devez préciser')
end
end
context 'dossier already having pending corrections' do
before do
create(:dossier_resolution, dossier:)
end
it 'does not create an new pending resolution' do
expect { subject }.not_to change { DossierResolution.count }
end
it 'shows a flash alert' do
subject
expect(response.body).to include('')
end
end
end
context 'dossier en_construction' do
it 'can create a pending resolution' do
subject
expect(dossier.reload).to be_pending_resolution
expect(dossier.commentaires.last).to be_flagged_pending_corrections
end
end
context 'dossier is termine' do
let(:dossier) { create(:dossier, :accepte, :with_individual, procedure: procedure) }
it 'does not create a pending resolution' do
expect { subject }.not_to change { DossierResolution.count }
expect(response.body).to include('Impossible')
end
end
end
describe '#messagerie' do describe '#messagerie' do
before { expect(controller.current_instructeur).to receive(:mark_tab_as_seen).with(dossier, :messagerie) } before { expect(controller.current_instructeur).to receive(:mark_tab_as_seen).with(dossier, :messagerie) }
subject { get :messagerie, params: { procedure_id: procedure.id, dossier_id: dossier.id } } subject { get :messagerie, params: { procedure_id: procedure.id, dossier_id: dossier.id } }

View file

@ -25,4 +25,56 @@ describe DossierResolvableConcern do
it { expect(dossier.pending_resolution?).to be_falsey } it { expect(dossier.pending_resolution?).to be_falsey }
end end
end end
describe '#flag_as_pending_correction!' do
let(:dossier) { create(:dossier, :en_construction) }
let(:instructeur) { create(:instructeur) }
let(:commentaire) { create(:commentaire, dossier:, instructeur:) }
context 'when dossier is en_construction' do
it 'creates a resolution' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.resolutions.pending.count }.by(1)
end
it 'does not change dossier state' do
expect { dossier.flag_as_pending_correction!(commentaire) }.not_to change { dossier.state }
end
end
context 'when dossier is not en_instruction' do
let(:dossier) { create(:dossier, :en_instruction) }
it 'creates a resolution' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.resolutions.pending.count }.by(1)
end
it 'repasse dossier en_construction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.state }.to('en_construction')
end
end
context 'when dossier has already a pending resolution' do
before { create(:dossier_resolution, dossier:) }
it 'does not create a resolution' do
expect { dossier.flag_as_pending_correction!(commentaire) }.not_to change { dossier.resolutions.pending.count }
end
end
context 'when dossier has already a resolved resolution' do
before { create(:dossier_resolution, :resolved, dossier:) }
it 'creates a resolution' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.resolutions.pending.count }.by(1)
end
end
context 'when dossier is not en_construction and may not be repassed en_construction' do
let(:dossier) { create(:dossier, :accepte) }
it 'does not create a resolution' do
expect { dossier.flag_as_pending_correction!(commentaire) }.not_to change { dossier.resolutions.pending.count }
end
end
end
end end

View file

@ -29,15 +29,25 @@ describe 'instructeurs/dossiers/instruction_button', type: :view do
end end
end end
context 'en_construction' do
let(:dossier) { create(:dossier, :en_construction) }
it 'renders a dropdown' do
expect(rendered).to have_dropdown_title('Demander une modification')
expect(rendered).to have_dropdown_items(count: 2) # form is already expanded so we have 2 visible items
end
end
context 'en_instruction' do context 'en_instruction' do
let(:dossier) { create(:dossier, :en_instruction) } let(:dossier) { create(:dossier, :en_instruction) }
it 'renders a dropdown' do it 'renders a dropdown' do
expect(rendered).to have_dropdown_title('Instruire le dossier') expect(rendered).to have_dropdown_title('Instruire le dossier')
expect(rendered).to have_dropdown_items(count: 3) expect(rendered).to have_dropdown_items(count: 4)
expect(rendered).to have_dropdown_item('Accepter') expect(rendered).to have_dropdown_item('Accepter')
expect(rendered).to have_dropdown_item('Classer sans suite') expect(rendered).to have_dropdown_item('Classer sans suite')
expect(rendered).to have_dropdown_item('Refuser') expect(rendered).to have_dropdown_item('Refuser')
expect(rendered).to have_dropdown_item('Demander une modification')
end end
end end
end end

View file

@ -61,7 +61,8 @@ describe 'instructeurs/dossiers/show', type: :view do
within("form[action=\"#{follow_instructeur_dossier_path(dossier.procedure, dossier)}\"]") do within("form[action=\"#{follow_instructeur_dossier_path(dossier.procedure, dossier)}\"]") do
expect(subject).to have_button('Suivre le dossier') expect(subject).to have_button('Suivre le dossier')
end end
expect(subject).to have_selector('.header-actions ul:first-child .fr-btn', count: 2) expect(subject).to have_button('Demander une modification')
expect(subject).to have_selector('.header-actions ul:first-child > li.instruction-button', count: 1)
end end
end end