From 6651b36fe42bea0bea9cf812bf848acd3d1de99c Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 10:41:58 +0100 Subject: [PATCH 01/12] db(migration): add closing_reason and closing_details to procedure --- app/models/procedure.rb | 8 ++++++++ ...41520_add_closing_reason_and_closing_details.rb | 6 ++++++ spec/models/procedure_spec.rb | 14 ++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 db/migrate/20240201141520_add_closing_reason_and_closing_details.rb diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 7f95fbbab..9a189f517 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -223,6 +223,11 @@ class Procedure < ApplicationRecord accepte: 'accepte' } + enum closing_reason: { + internal_procedure: 'internal_procedure', + other: 'other' + } + scope :for_api_v2, -> { includes(:draft_revision, :published_revision, administrateurs: :user) } @@ -260,6 +265,9 @@ class Procedure < ApplicationRecord validate :check_juridique, on: [:create, :publication] + # TO DO add validation after data backfill + # validates :replaced_by_id, presence: true, if: -> { closing_reason == self.closing_reasons.fetch(:internal_procedure) } + validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false } validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { diff --git a/db/migrate/20240201141520_add_closing_reason_and_closing_details.rb b/db/migrate/20240201141520_add_closing_reason_and_closing_details.rb new file mode 100644 index 000000000..48bf33f3b --- /dev/null +++ b/db/migrate/20240201141520_add_closing_reason_and_closing_details.rb @@ -0,0 +1,6 @@ +class AddClosingReasonAndClosingDetails < ActiveRecord::Migration[7.0] + def change + add_column :procedures, :closing_reason, :string + add_column :procedures, :closing_details, :string + end +end diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index c1aedbd90..62f553e1e 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -178,6 +178,20 @@ describe Procedure do it { is_expected.to allow_value('Demande de subvention').for(:libelle) } end + context 'closing procedure' do + context 'without replacing procedure in DS' do + let(:procedure) { create(:procedure) } + + context 'valid' do + before do + procedure.update!(closing_details: "Bonjour,\nLa démarche est désormais hébergée sur une autre plateforme\nCordialement", closing_reason: Procedure.closing_reasons.fetch(:other)) + end + + it { expect(procedure).to be_valid } + end + end + end + context 'description' do it { is_expected.not_to allow_value(nil).for(:description) } it { is_expected.not_to allow_value('').for(:description) } From 74903d79bf92807272bbce456583238d4aa61bcb Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 10:54:19 +0100 Subject: [PATCH 02/12] feat(procedure): update archiving to add closing reason and closing details --- .../administrateurs/procedures_controller.rb | 32 ++++++++++---- .../controllers/closing_reason_controller.ts | 44 +++++++++++++++++++ .../procedures/close.html.haml | 20 ++++++--- config/locales/models/procedure/en.yml | 8 ++++ config/locales/models/procedure/fr.yml | 8 ++++ .../views/administrateurs/procedures/en.yml | 3 +- .../views/administrateurs/procedures/fr.yml | 3 +- .../procedures_controller_spec.rb | 39 ++++++++++++++-- 8 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 app/javascript/controllers/closing_reason_controller.ts diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb index ae0ab979b..4c8f851bf 100644 --- a/app/controllers/administrateurs/procedures_controller.rb +++ b/app/controllers/administrateurs/procedures_controller.rb @@ -194,16 +194,19 @@ module Administrateurs def archive procedure = current_administrateur.procedures.find(params[:procedure_id]) - if params[:new_procedure].present? - new_procedure = current_administrateur.procedures.find(params[:new_procedure]) - procedure.update!(replaced_by_procedure_id: new_procedure.id) + if procedure.update(closing_params) + procedure.close! + if (procedure.dossiers.not_archived.state_brouillon.present? || procedure.dossiers.not_archived.state_en_construction_ou_instruction.present?) + redirect_to admin_procedure_closing_notification_path + else + flash.notice = "Démarche close" + redirect_to admin_procedure_path(id: procedure.id) + end + else + flash.alert = procedure.errors.full_messages + redirect_to admin_procedure_close_path end - procedure.close! - - flash.notice = "Démarche close" - redirect_to admin_procedures_path - rescue ActiveRecord::RecordNotFound flash.alert = 'Démarche inexistante' redirect_to admin_procedures_path @@ -327,6 +330,7 @@ module Administrateurs def close @published_procedures = current_administrateur.procedures.publiees.to_h { |p| ["#{p.libelle} (#{p.id})", p.id] } + @closing_reason_options = Procedure.closing_reasons.values.map { |reason| [I18n.t("activerecord.attributes.procedure.closing_reasons.#{reason}"), reason] } end def confirmation @@ -493,6 +497,18 @@ module Administrateurs params.permit(:path, :lien_site_web) end + def closing_params + closing_params = params.require(:procedure).permit(:closing_details, :closing_reason, :replaced_by_procedure_id) + + replaced_by_procedure_id = closing_params[:replaced_by_procedure_id] + if replaced_by_procedure_id.present? + if current_administrateur.procedures.find_by(id: replaced_by_procedure_id).blank? + closing_params.delete(:replaced_by_procedure_id) + end + end + closing_params + end + def allow_decision_access_params params.require(:experts_procedure).permit(:allow_decision_access) end diff --git a/app/javascript/controllers/closing_reason_controller.ts b/app/javascript/controllers/closing_reason_controller.ts new file mode 100644 index 000000000..d7eed1878 --- /dev/null +++ b/app/javascript/controllers/closing_reason_controller.ts @@ -0,0 +1,44 @@ +import { ApplicationController } from './application_controller'; +import { hide, show } from '@utils'; + +export class ClosingReasonController extends ApplicationController { + static targets = ['closingReason', 'replacedByProcedureId', 'closingDetails']; + + declare closingReasonTarget: HTMLSelectElement; + declare replacedByProcedureIdTarget: HTMLInputElement; + declare closingDetailsTarget: HTMLInputElement; + + connect() { + this.displayInput(); + this.on('change', () => this.onChange()); + } + + onChange() { + this.displayInput(); + } + + displayInput() { + const closingReasonSelect = this.closingReasonTarget as HTMLSelectElement; + + Array.from(closingReasonSelect.options).forEach((option) => { + if (option.selected && option.value == 'internal_procedure') { + show(this.replacedByProcedureIdTarget); + hide(this.closingDetailsTarget); + this.emptyValue(this.closingDetailsTarget.querySelector('input')); + } else if (option.selected && option.value == 'other') { + hide(this.replacedByProcedureIdTarget); + this.emptyValue( + this.replacedByProcedureIdTarget.querySelector('select') + ); + show(this.closingDetailsTarget); + this.emptyValue(this.closingDetailsTarget.querySelector('input')); + } + }); + } + + emptyValue(field: HTMLInputElement | HTMLSelectElement | null) { + if (field) { + field.value = ''; + } + } +} diff --git a/app/views/administrateurs/procedures/close.html.haml b/app/views/administrateurs/procedures/close.html.haml index e04e0dbbd..be24a604b 100644 --- a/app/views/administrateurs/procedures/close.html.haml +++ b/app/views/administrateurs/procedures/close.html.haml @@ -8,15 +8,21 @@ .fr-col-12.fr-col-offset-md-2.fr-col-md-8 %h1= t('administrateurs.procedures.close.page_title') - %p= t('administrateurs.procedures.close.replacement_procedure_title') + = render Dsfr::CalloutComponent.new(title: t("administrateurs.procedures.close.replacement_procedure_callout_title"), icon: "fr-fi-information-line") do |c| + - c.with_body do + = t('administrateurs.procedures.close.replacement_procedure_callout_content') - = form_tag admin_procedure_archive_path(@procedure), method: :put do + = form_for @procedure, url: admin_procedure_archive_path(@procedure), method: :put, html: { "data-controller" => "closing-reason" } do |f| + .fr-select-group + = f.label :closing_reason, class: 'fr-label' + = f.select :closing_reason, options_for_select(@closing_reason_options), {}, { class: 'fr-select', "data-closing-reason-target" => "closingReason" } - if @published_procedures.present? - .fr-select-group - = label_tag :new_procedure, class: 'fr-label' do - = t('activerecord.attributes.procedure.new_procedure') - = t('utils.no_mandatory') - = select_tag :new_procedure, options_for_select(@published_procedures), include_blank: true, class: 'fr-select' + .fr-select-group#js_replaced_by_procedure_id{ "data-closing-reason-target" => "replacedByProcedureId" } + = f.label :replaced_by_procedure_id, class: 'fr-label' + = f.select :replaced_by_procedure_id, options_for_select(@published_procedures), { include_blank: "Sélectionnez la nouvelle démarche" }, { class: 'fr-select' } + + .fr-input-group#js_closing_details{ "data-closing-reason-target" => "closingDetails" } + = render Dsfr::InputComponent.new(form: f, attribute: :closing_details, input_type: :text_area, opts: { rows: '10', placeholder: t('activerecord.attributes.procedure.hints.closing_details_placeholder')}, required: false) = submit_tag t('administrateurs.procedures.close.actions.close_procedure'), { class: "fr-btn", id: 'publish', data: { confirm: "Voulez-vous vraiment clore la démarche ? \nLes dossiers en cours pourront être instruits, mais aucun nouveau dossier ne pourra plus être déposé.", disable_with: "Archivage..."} } diff --git a/config/locales/models/procedure/en.yml b/config/locales/models/procedure/en.yml index e999c6afa..01e267453 100644 --- a/config/locales/models/procedure/en.yml +++ b/config/locales/models/procedure/en.yml @@ -14,6 +14,8 @@ en: cadre_juridique: "Exemple: 'https://www.legifrance.gouv.fr/'" old_procedure: This procedure replaces a close? If yes, please indicate the number of the replaced procedure procedure_path: "Personalize if needed the rest of the URL to facilitate access to the procedure. From 3 to 200 characters: lowercase letters, numbers and dashes only" + closing_details: Give as many explanations as possible to users about the reason for closing the process + closing_details_placeholder: "This procedure has been replaced by the page…\n\nThe guide to the new procedure is available here\n\nFor any further information, contact…\n\nSincerely," path: Public link organisation: Service description: What is the purpose of this procedure? @@ -23,6 +25,12 @@ en: lien_site_web: Where will users find the link to the procedure? old_procedure: Replaced procedure number new_procedure: New procedure number + replaced_by_procedure_id: New procedure + closing_details: Information message on the process closing page + closing_reason: Closing reason + closing_reasons: + other: Other + internal_procedure: I replace my procedure with another in Démarches Simplifiées procedure_path: Procedure link to disseminate to users procedure_path_placeholder: procedure-name cadre_juridique: Link to the legal text diff --git a/config/locales/models/procedure/fr.yml b/config/locales/models/procedure/fr.yml index 5fa059494..820b9368b 100644 --- a/config/locales/models/procedure/fr.yml +++ b/config/locales/models/procedure/fr.yml @@ -14,6 +14,8 @@ fr: cadre_juridique: "Exemple: 'https://www.legifrance.gouv.fr/'" old_procedure: Cette démarche remplace une close ? Si oui, veuillez indiquer le n° de la démarche remplacée procedure_path: "Personnalisez si besoin la suite de l’URL, pour faciliter l'accès à la démarche. De 3 à 200 caractères : minuscules, chiffres et tiret seulement" + closing_details: Donnez le plus d'explication possible aux usagers sur la raison de la fermeture de la démarche + closing_details_placeholder: "Cette démarche a été remplacée par la page…\n\nLe guide de la nouvelle démarche est disponible ici\n\nPour toute information complémentaire, contactez…\n\nCordialement," path: Lien public organisation: Organisme duree_conservation_dossiers_dans_ds: Durée de conservation des dossiers sur demarches-simplifiees.fr (choisi par un usager) @@ -27,6 +29,12 @@ fr: lien_site_web: Où les usagers trouveront-ils le lien vers la démarche ? old_procedure: Numéro de la démarche remplacée new_procedure: Numéro de la nouvelle démarche + replaced_by_procedure_id: Nouvelle démarche + closing_details: Message d'information remplaçant la démarche + closing_reason: Raison de la clôture + closing_reasons: + other: Autre + internal_procedure: Je remplace ma démarche par une autre dans Démarches simplifiées procedure_path: Lien de la démarche à diffuser aux usagers procedure_path_placeholder: nom-de-la-demarche cadre_juridique: Lien vers le texte diff --git a/config/locales/views/administrateurs/procedures/en.yml b/config/locales/views/administrateurs/procedures/en.yml index eef827e3b..c9d6ba950 100644 --- a/config/locales/views/administrateurs/procedures/en.yml +++ b/config/locales/views/administrateurs/procedures/en.yml @@ -3,7 +3,8 @@ en: procedures: close: page_title: Close the procedure - replacement_procedure_title: Is this procedure replaced by an existing one? If yes, please indicate the number of the new procedure + replacement_procedure_callout_title: You are about to close a procedure + replacement_procedure_callout_content: Files « in construction » or « instructing » can be instructed, but no new files can be filed. actions: close_procedure: Close the procedure preview_unavailable: Preview is unavailable due to procedure misconfiguration diff --git a/config/locales/views/administrateurs/procedures/fr.yml b/config/locales/views/administrateurs/procedures/fr.yml index 79b9c940e..01e9348ff 100644 --- a/config/locales/views/administrateurs/procedures/fr.yml +++ b/config/locales/views/administrateurs/procedures/fr.yml @@ -3,7 +3,8 @@ fr: procedures: close: page_title: Clore la démarche - replacement_procedure_title: Cette démarche est-elle remplacée par une existante ? Si oui, veuillez indiquer le n° de la nouvelle démarche + replacement_procedure_callout_title: Vous êtes sur le point de clore une démarche + replacement_procedure_callout_content: Les dossiers en cours pourront être instruits, mais aucun nouveau dossier ne pourra plus être déposé. actions: close_procedure: Clore la démarche preview_unavailable: Aperçu non disponible car la démarche est mal configurée diff --git a/spec/controllers/administrateurs/procedures_controller_spec.rb b/spec/controllers/administrateurs/procedures_controller_spec.rb index 3499c679a..407a632a0 100644 --- a/spec/controllers/administrateurs/procedures_controller_spec.rb +++ b/spec/controllers/administrateurs/procedures_controller_spec.rb @@ -690,7 +690,7 @@ describe Administrateurs::ProceduresController, type: :controller do context 'when the admin is an owner of the procedure without procedure replacement' do before do - put :archive, params: { procedure_id: procedure.id } + put :archive, params: { procedure_id: procedure.id, procedure: { closing_reason: 'other' } } procedure.reload end @@ -702,13 +702,23 @@ describe Administrateurs::ProceduresController, type: :controller do it 'does not have any replacement procedure' do expect(procedure.replaced_by_procedure).to be_nil + expect(procedure.closing_reason).to eq('other') + end + + context 'the admin can notify users if there are file in brouillon or en_cours' do + let!(:procedure) { create(:procedure_with_dossiers, :published, dossiers_count: 2, administrateur: admin, lien_site_web: lien_site_web) } + it 'archives the procedure and redirects to page to notify users' do + expect(procedure.close?).to be_truthy + expect(response).to redirect_to :admin_procedure_closing_notification + end end end - context 'when the admin is an owner of the procedure with procedure replacement' do + context 'when the admin is an owner of the procedure with procedure replacement in DS' do + let(:procedure) { create(:procedure_with_dossiers, :published, administrateur: admin, lien_site_web: lien_site_web) } let(:new_procedure) { create(:procedure, :published, administrateur: admin, lien_site_web: lien_site_web) } before do - put :archive, params: { procedure_id: procedure.id, new_procedure: new_procedure } + put :archive, params: { procedure_id: procedure.id, procedure: { closing_reason: 'internal_procedure', replaced_by_procedure_id: new_procedure.id } } procedure.reload end @@ -720,6 +730,27 @@ describe Administrateurs::ProceduresController, type: :controller do it 'does have a replacement procedure' do expect(procedure.replaced_by_procedure).to eq(new_procedure) + expect(procedure.closing_reason).to eq('internal_procedure') + end + end + + context 'when the admin is an owner of the procedure with procedure replacement outside DS' do + let(:new_procedure) { create(:procedure, :published, administrateur: admin, lien_site_web: lien_site_web) } + before do + put :archive, params: { procedure_id: procedure.id, procedure: { closing_reason: 'other', closing_details: "Sorry it's closed" } } + procedure.reload + end + + it 'archives the procedure' do + expect(procedure.close?).to be_truthy + expect(response).to redirect_to :admin_procedures + expect(flash[:notice]).to have_content 'Démarche close' + end + + it 'does have a replacement procedure' do + expect(procedure.replaced_by_procedure).to eq(nil) + expect(procedure.replaced_by_external_url).to eq('new_url.com') + expect(procedure.closing_reason).to eq('external_procedure') end end @@ -730,7 +761,7 @@ describe Administrateurs::ProceduresController, type: :controller do sign_out(admin.user) sign_in(admin_2.user) - put :archive, params: { procedure_id: procedure.id } + put :archive, params: { procedure_id: procedure.id, procedure: { closing_reason: 'other' } } procedure.reload end From 1bbf35a6baaebad1dcc832c023d0e0ec5d0cffa7 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 10:58:05 +0100 Subject: [PATCH 03/12] dsfr(toggle): add attributes to toggle component --- app/components/dsfr/toggle_component.rb | 6 +++++- .../dsfr/toggle_component/toggle_component.html.haml | 12 +++++++----- .../instructeurs_options_component.html.haml | 2 ++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/components/dsfr/toggle_component.rb b/app/components/dsfr/toggle_component.rb index ef310ba57..20c328e9b 100644 --- a/app/components/dsfr/toggle_component.rb +++ b/app/components/dsfr/toggle_component.rb @@ -1,9 +1,13 @@ class Dsfr::ToggleComponent < ApplicationComponent - def initialize(form:, target:, title:, hint:, disabled:) + def initialize(form:, target:, title:, disabled: nil, hint: nil, toggle_labels: { checked: 'Activé', unchecked: 'Désactivé' }, opt: nil) @form = form @target = target @title = title @hint = hint @disabled = disabled + @toggle_labels = toggle_labels + @opt = opt end + + attr_reader :toggle_labels end diff --git a/app/components/dsfr/toggle_component/toggle_component.html.haml b/app/components/dsfr/toggle_component/toggle_component.html.haml index dd0fb2eb7..7769a3724 100644 --- a/app/components/dsfr/toggle_component/toggle_component.html.haml +++ b/app/components/dsfr/toggle_component/toggle_component.html.haml @@ -1,7 +1,9 @@ -.fr-toggle.fr-toggle--border-bottom.fr-toggle--label-left - = @form.check_box @target, class: 'fr-toggle__input', disabled: @disabled +.fr-toggle.fr-toggle--label-left + = @form.check_box @target, class: 'fr-toggle__input', disabled: @disabled, + data: @opt = @form.label @target, @title, - class: 'fr-toggle__label', - data: { 'fr-checked-label': 'Activé', 'fr-unchecked-label': 'Désactivé' } - %p.fr-hint-text= @hint + data: { 'fr-checked-label': toggle_labels[:checked], 'fr-unchecked-label': toggle_labels[:unchecked] }, + class: 'fr-toggle__label' + - if @hint + %p.fr-hint-text= @hint diff --git a/app/components/procedure/instructeurs_options_component/instructeurs_options_component.html.haml b/app/components/procedure/instructeurs_options_component/instructeurs_options_component.html.haml index ac06d1ba5..fd2a07a68 100644 --- a/app/components/procedure/instructeurs_options_component/instructeurs_options_component.html.haml +++ b/app/components/procedure/instructeurs_options_component/instructeurs_options_component.html.haml @@ -14,6 +14,8 @@ title: 'Autogestion des instructeurs', hint: "L’autogestion des instructeurs permet aux instructeurs de gérer eux-mêmes la liste des instructeurs de la démarche.#{ 'Nous recommandons de laisser l’autogestion des instructeurs activée.' if @procedure.routing_enabled? }", disabled: false) + + %hr %p.fr-mt-2w Routage %p.fr-mt-2w= t('.routing_configuration_notice_1') %p.fr-icon-info-line.fr-hint-text{ aria: { hidden: true } } From c147d9b36ce9bbb0f9eb9b579dfc90a30f98e90a Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 10:59:43 +0100 Subject: [PATCH 04/12] db(migration): add closing notifications to procedure --- ...20240202135754_add_closing_notifications_to_procedure.rb | 6 ++++++ db/schema.rb | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 db/migrate/20240202135754_add_closing_notifications_to_procedure.rb diff --git a/db/migrate/20240202135754_add_closing_notifications_to_procedure.rb b/db/migrate/20240202135754_add_closing_notifications_to_procedure.rb new file mode 100644 index 000000000..d1077a6ef --- /dev/null +++ b/db/migrate/20240202135754_add_closing_notifications_to_procedure.rb @@ -0,0 +1,6 @@ +class AddClosingNotificationsToProcedure < ActiveRecord::Migration[7.0] + def change + add_column :procedures, :closing_notification_brouillon, :boolean, default: false, null: false + add_column :procedures, :closing_notification_en_cours, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index dac0c8a28..994f93801 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -856,6 +856,10 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_27_163855) do t.jsonb "chorus", default: {}, null: false t.boolean "cloned_from_library", default: false t.datetime "closed_at", precision: nil + t.string "closing_details" + t.boolean "closing_notification_brouillon", default: false, null: false + t.boolean "closing_notification_en_cours", default: false, null: false + t.string "closing_reason" t.datetime "created_at", precision: nil, null: false t.string "declarative_with_state" t.bigint "defaut_groupe_instructeur_id" From c95f0f1cad27d6b7a294ff56c38c7a738850a5ec Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 11:09:00 +0100 Subject: [PATCH 05/12] feat(procedure): send notifications after closing --- .../administrateurs/procedures_controller.rb | 40 ++++++++++ .../closing_notification_controller.ts | 76 +++++++++++++++++++ app/jobs/send_closing_notification_job.rb | 7 ++ app/mailers/user_mailer.rb | 9 +++ app/models/procedure.rb | 4 + .../procedures/closing_notification.html.haml | 52 +++++++++++++ .../notify_after_closing.html.haml | 4 + .../views/administrateurs/procedures/en.yml | 17 ++++- .../views/administrateurs/procedures/fr.yml | 15 ++++ config/routes.rb | 2 + .../procedures_controller_spec.rb | 70 ++++++++++++++--- spec/mailers/previews/user_mailer_preview.rb | 4 + spec/mailers/user_mailer_spec.rb | 18 +++++ 13 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 app/javascript/controllers/closing_notification_controller.ts create mode 100644 app/jobs/send_closing_notification_job.rb create mode 100644 app/views/administrateurs/procedures/closing_notification.html.haml create mode 100644 app/views/user_mailer/notify_after_closing.html.haml diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb index 4c8f851bf..8a62fdaa7 100644 --- a/app/controllers/administrateurs/procedures_controller.rb +++ b/app/controllers/administrateurs/procedures_controller.rb @@ -212,6 +212,38 @@ module Administrateurs redirect_to admin_procedures_path end + def closing_notification + @procedure = current_administrateur.procedures.find(params[:procedure_id]) + @users_brouillon_count = @procedure.dossiers.not_archived.state_brouillon.count('distinct user_id') + @users_en_cours_count = @procedure.dossiers.not_archived.state_en_construction_ou_instruction.count('distinct user_id') + end + + def notify_after_closing + @procedure = current_administrateur.procedures.find(params[:procedure_id]) + @procedure.update!(notification_closing_params) + + if (@procedure.closing_notification_brouillon? && params[:email_content_brouillon].blank?) || (@procedure.closing_notification_en_cours? && params[:email_content_en_cours].blank?) + flash.alert = "Veuillez renseigner le contenu de l’email afin d’informer les usagers" + redirect_to admin_procedure_closing_notification_path and return + end + + if @procedure.closing_notification_brouillon? + user_ids = @procedure.dossiers.not_archived.state_brouillon.pluck(:user_id).uniq + content = params[:email_content_brouillon] + SendClosingNotificationJob.perform_later(user_ids, content, @procedure) + flash.notice = "Les emails sont en cours d'envoi" + end + + if @procedure.closing_notification_en_cours? + user_ids = @procedure.dossiers.not_archived.state_en_construction_ou_instruction.pluck(:user_id).uniq + content = params[:email_content_en_cours] + SendClosingNotificationJob.perform_later(user_ids, content, @procedure) + flash.notice = "Les emails sont en cours d’envoi" + end + + redirect_to admin_procedures_path + end + def destroy procedure = current_administrateur.procedures.find(params[:id]) @@ -304,6 +336,10 @@ module Administrateurs .update!(replaced_by_procedure: @procedure) end + # TO DO after data backfill add this condition before reset : + # if @procedure.closing_reason.present? + @procedure.reset_closing_params + redirect_to admin_procedure_confirmation_path(@procedure) rescue ActiveRecord::RecordInvalid flash.alert = @procedure.errors.full_messages @@ -509,6 +545,10 @@ module Administrateurs closing_params end + def notification_closing_params + params.require(:procedure).permit(:closing_notification_brouillon, :closing_notification_en_cours) + end + def allow_decision_access_params params.require(:experts_procedure).permit(:allow_decision_access) end diff --git a/app/javascript/controllers/closing_notification_controller.ts b/app/javascript/controllers/closing_notification_controller.ts new file mode 100644 index 000000000..7fcd690ad --- /dev/null +++ b/app/javascript/controllers/closing_notification_controller.ts @@ -0,0 +1,76 @@ +import { ApplicationController } from './application_controller'; +import { hide, show } from '@utils'; + +export class ClosingNotificationController extends ApplicationController { + static targets = [ + 'brouillonToggle', + 'emailContentBrouillon', + 'enCoursToggle', + 'emailContentEnCours', + 'submit' + ]; + + declare readonly brouillonToggleTarget: HTMLInputElement; + declare readonly hasBrouillonToggleTarget: boolean; + declare readonly enCoursToggleTarget: HTMLInputElement; + declare readonly hasEnCoursToggleTarget: boolean; + declare readonly emailContentBrouillonTarget: HTMLElement; + declare readonly emailContentEnCoursTarget: HTMLElement; + declare readonly submitTarget: HTMLButtonElement; + + connect() { + this.displayBrouillonInput(); + this.displayEnCoursInput(); + this.on('change', () => this.onChange()); + } + + onChange() { + this.displayBrouillonInput(); + this.displayEnCoursInput(); + } + + displayBrouillonInput() { + if (this.hasBrouillonToggleTarget) { + const brouillonToggleElement = this + .brouillonToggleTarget as HTMLInputElement; + + const emailContentBrouillonElement = this + .emailContentBrouillonTarget as HTMLElement; + + if (emailContentBrouillonElement) { + if (brouillonToggleElement.checked) { + show(emailContentBrouillonElement); + } else { + hide(emailContentBrouillonElement); + } + } + } + } + + displayEnCoursInput() { + if (this.hasEnCoursToggleTarget) { + const enCoursToggleElement = this.enCoursToggleTarget as HTMLInputElement; + + const emailContentEnCoursElement = this + .emailContentEnCoursTarget as HTMLElement; + + if (emailContentEnCoursElement) { + if (enCoursToggleElement.checked) { + show(this.emailContentEnCoursTarget); + } else { + hide(this.emailContentEnCoursTarget); + } + } + } + } + + enableSubmitOnClick() { + if ( + this.element.querySelectorAll('input[type="checkbox"]:checked').length > 0 + ) { + this.submitTarget.disabled = false; + } else { + this.submitTarget.disabled = true; + } + } +} diff --git a/app/jobs/send_closing_notification_job.rb b/app/jobs/send_closing_notification_job.rb new file mode 100644 index 000000000..919b6baa6 --- /dev/null +++ b/app/jobs/send_closing_notification_job.rb @@ -0,0 +1,7 @@ +class SendClosingNotificationJob < ApplicationJob + def perform(user_ids, content, procedure) + User.where(id: user_ids).find_each do |user| + Expired::MailRateLimiter.new().send_with_delay(UserMailer.notify_after_closing(user, content, @procedure)) + end + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index db28953fe..0989b41c6 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -74,6 +74,15 @@ class UserMailer < ApplicationMailer mail(to: user.email, subject: @subject) end + def notify_after_closing(user, content, procedure = nil) + @user = user + @subject = "Clôture d'une démarche sur Démarches simplifiées" + @procedure = procedure + @content = content + + mail(to: user.email, subject: @subject, content: @content, procedure: @procedure) + end + def self.critical_email?(action_name) [ 'france_connect_merge_confirmation', diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 9a189f517..21b8e8ac5 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -1010,6 +1010,10 @@ class Procedure < ApplicationRecord .first end + def reset_closing_params + update!(closing_reason: nil, closing_details: nil, replaced_by_procedure_id: nil, closing_notification_brouillon: false, closing_notification_en_cours: false) + end + private def pieces_jointes_list diff --git a/app/views/administrateurs/procedures/closing_notification.html.haml b/app/views/administrateurs/procedures/closing_notification.html.haml new file mode 100644 index 000000000..36c9f0e77 --- /dev/null +++ b/app/views/administrateurs/procedures/closing_notification.html.haml @@ -0,0 +1,52 @@ += render partial: 'administrateurs/breadcrumbs', + locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)], + [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],[t('administrateurs.procedures.close.page_title')]], + metadatas: true } + +.fr-container + .fr-grid-row + .fr-col-12.fr-col-offset-md-2.fr-col-md-8 + %h1= t('administrateurs.procedures.closing_notification.page_title') + - if @procedure.closing_reason == Procedure.closing_reasons.fetch(:other) + %h2.fr-h5= I18n.t('administrateurs.procedures.closing_notification.page_subtitle', closing_path: closing_details_path(@procedure.path)).html_safe + - else + %h2.fr-h5= I18n.t('administrateurs.procedures.closing_notification.page_subtitle_with_redirection', redirection_path: commencer_path(@procedure.replaced_by_procedure.path)).html_safe + + = render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c| + - c.with_body do + %p + = t('administrateurs.procedures.closing_notification.callout_content') + + = form_for @procedure, + url: admin_procedure_notify_after_closing_path(@procedure), + method: :post, + html: { "data-controller" => "closing-notification" } do |f| + + %div{ data: { 'action': "click->closing-notification#enableSubmitOnClick" } } + - if @users_brouillon_count != 0 + = render Dsfr::ToggleComponent.new(form: f, + target: :closing_notification_brouillon, + title: t("administrateurs.procedures.closing_notification.email_toggle_brouillon", count: @users_brouillon_count), + toggle_labels: {checked: 'Oui', unchecked: 'Non'}, + opt: {"closing-notification-target" => "brouillonToggle"}) + + .fr-input-group{ "data-closing-notification-target" => "emailContentBrouillon" } + = label_tag :email_content_brouillon, t("administrateurs.procedures.closing_notification.email_content_brouillon"), class: "fr-label" + = text_area_tag :email_content_brouillon, '', class: "fr-input" + + - if @users_en_cours_count != 0 + = render Dsfr::ToggleComponent.new(form: f, + target: :closing_notification_en_cours, + title: t("administrateurs.procedures.closing_notification.email_toggle_en_cours", count: @users_en_cours_count), + toggle_labels: {checked: 'Oui', unchecked: 'Non'}, + opt: {"closing-notification-target" => "enCoursToggle"}) + + .fr-input-group{ "data-closing-notification-target" => "emailContentEnCours" } + = label_tag :email_content_en_cours, t("administrateurs.procedures.closing_notification.email_content_en_cours"), class: "fr-label" + = text_area_tag :email_content_en_cours, '', class: "fr-input" + + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = submit_tag t('administrateurs.procedures.close.actions.notify_after_closing'), { class: "fr-btn", id: 'publish', disabled: true, data: { confirm: "Vous allez informer les usagers de la clôture de la démarche. Souhaitez-vous continuer ?", disable_with: "Envoi des notifications…", 'closing-notification-target': 'submit'} } + %li + = link_to t('administrateurs.procedures.close.actions.cancel'), admin_procedures_path, class: 'fr-btn fr-btn--secondary fr-ml-2w' diff --git a/app/views/user_mailer/notify_after_closing.html.haml b/app/views/user_mailer/notify_after_closing.html.haml new file mode 100644 index 000000000..8b6feb4bc --- /dev/null +++ b/app/views/user_mailer/notify_after_closing.html.haml @@ -0,0 +1,4 @@ +- content_for(:title, @subject) + +%p + = simple_format(@content) diff --git a/config/locales/views/administrateurs/procedures/en.yml b/config/locales/views/administrateurs/procedures/en.yml index c9d6ba950..569747ffa 100644 --- a/config/locales/views/administrateurs/procedures/en.yml +++ b/config/locales/views/administrateurs/procedures/en.yml @@ -6,7 +6,22 @@ en: replacement_procedure_callout_title: You are about to close a procedure replacement_procedure_callout_content: Files « in construction » or « instructing » can be instructed, but no new files can be filed. actions: - close_procedure: Close the procedure + close_procedure: Close procedure + notify_after_closing: Notify users + cancel: Cancel + closing_notification: + page_title: Alert users + page_subtitle: Your procedure has been successfully closed. The link of the procedure now redirects to this closing page. + page_subtitle_with_redirection: Your procedure has been successfully closed. The link of the procedure now redirects to this procedure. + callout_content: You can continue to examine the submitted files. If you do not intend to examine these files, we invite you to inform users + email_toggle_brouillon: + one: You want to send an email to the user with a draft folder + other: You want to send an email to %{count} users with a draft folder + email_content_brouillon: You want to send an email to users with a draft file + email_toggle_en_cours: + one: You want to send an email to the user with a submitted file + other: You want to send an email to %{count} users with a submitted file + email_content_en_cours: You want to send an email to users with a file « in construction » or « instructing » preview_unavailable: Preview is unavailable due to procedure misconfiguration modifications: dossiers_en_construction_and_dossiers_en_instruction: "%{en_construction_count} files « in construction » and %{en_instruction_count} files « instructing » on this procedure version." diff --git a/config/locales/views/administrateurs/procedures/fr.yml b/config/locales/views/administrateurs/procedures/fr.yml index 01e9348ff..e032d889c 100644 --- a/config/locales/views/administrateurs/procedures/fr.yml +++ b/config/locales/views/administrateurs/procedures/fr.yml @@ -7,6 +7,21 @@ fr: replacement_procedure_callout_content: Les dossiers en cours pourront être instruits, mais aucun nouveau dossier ne pourra plus être déposé. actions: close_procedure: Clore la démarche + notify_after_closing: Informer les usagers + cancel: Retour + closing_notification: + page_title: Alerter les usagers + page_subtitle: Votre démarche est close. Le lien de votre démarche redirige désormais vers cette page de fermeture. + page_subtitle_with_redirection: Votre démarche est close. Le lien public redirige désormais vers cette démarche. + callout_content: Vous avez la possibilité de continuer à instruire les dossiers déposés. Si vous n’avez pas l’intention d’instruire ces dossiers, nous vous invitons à en informer les usagers. + email_toggle_brouillon: + one: Souhaitez-vous envoyer un email à l'utilisateur avec un dossier en brouillon ? + other: Souhaitez-vous envoyer un email aux %{count} utilisateurs avec un dossier en brouillon ? + email_content_brouillon: Contenu de l'email + email_toggle_en_cours: + one : Souhaitez-vous envoyer un email à l'utilisateur avec un dossier déposé ? + other: Souhaitez-vous envoyer un email aux %{count} utilisateurs avec un dossier déposé ? + email_content_en_cours: Contenu de l'email preview_unavailable: Aperçu non disponible car la démarche est mal configurée modifications: dossiers_en_construction_and_dossiers_en_instruction: Il y a %{en_construction_count} dossiers « en construction » et %{en_instruction_count} dossiers « en instruction » sur cette version de la démarche. diff --git a/config/routes.rb b/config/routes.rb index d918962d0..56f5cfe30 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -600,6 +600,8 @@ Rails.application.routes.draw do put 'publish_revision' => 'procedures#publish_revision', as: :publish_revision get 'transfert' => 'procedures#transfert', as: :transfert get 'close' => 'procedures#close', as: :close + get 'closing_notification' => 'procedures#closing_notification', as: :closing_notification + post 'notify_after_closing' => 'procedures#notify_after_closing', as: :notify_after_closing get 'confirmation' => 'procedures#confirmation', as: :confirmation post 'transfer' => 'procedures#transfer', as: :transfer resources :mail_templates, only: [:edit, :update, :show] diff --git a/spec/controllers/administrateurs/procedures_controller_spec.rb b/spec/controllers/administrateurs/procedures_controller_spec.rb index 407a632a0..44da987e2 100644 --- a/spec/controllers/administrateurs/procedures_controller_spec.rb +++ b/spec/controllers/administrateurs/procedures_controller_spec.rb @@ -696,7 +696,7 @@ describe Administrateurs::ProceduresController, type: :controller do it 'archives the procedure' do expect(procedure.close?).to be_truthy - expect(response).to redirect_to :admin_procedures + expect(response).to redirect_to admin_procedure_path(procedure.id) expect(flash[:notice]).to have_content 'Démarche close' end @@ -724,8 +724,7 @@ describe Administrateurs::ProceduresController, type: :controller do it 'archives the procedure' do expect(procedure.close?).to be_truthy - expect(response).to redirect_to :admin_procedures - expect(flash[:notice]).to have_content 'Démarche close' + expect(response).to redirect_to admin_procedure_closing_notification_path end it 'does have a replacement procedure' do @@ -743,15 +742,9 @@ describe Administrateurs::ProceduresController, type: :controller do it 'archives the procedure' do expect(procedure.close?).to be_truthy - expect(response).to redirect_to :admin_procedures + expect(response).to redirect_to admin_procedure_path(procedure.id) expect(flash[:notice]).to have_content 'Démarche close' end - - it 'does have a replacement procedure' do - expect(procedure.replaced_by_procedure).to eq(nil) - expect(procedure.replaced_by_external_url).to eq('new_url.com') - expect(procedure.closing_reason).to eq('external_procedure') - end end context 'when the admin is not an owner of the procedure' do @@ -770,6 +763,42 @@ describe Administrateurs::ProceduresController, type: :controller do expect(flash[:alert]).to have_content 'Démarche inexistante' end end + + context 'when the admin is not an owner of the new procedure in DS' do + let(:admin_2) { create(:administrateur) } + let(:other_admin_procedure) { create(:procedure, :with_all_champs, administrateurs: [admin_2]) } + + before do + put :archive, params: { procedure_id: procedure.id, procedure: { closing_reason: 'internal_procedure', replaced_by_procedure_id: other_admin_procedure.id } } + procedure.reload + end + + it 'closes the procedure without redirection to the new procedure in DS' do + expect(response).to redirect_to admin_procedure_path(procedure.id) + expect(flash[:notice]).to have_content 'Démarche close' + expect(procedure.replaced_by_procedure).to eq(nil) + end + end + end + + describe 'POST #notify_after_closing' do + let(:procedure_closed) { create(:procedure_with_dossiers, :closed, administrateurs: [admin]) } + let(:user_ids) { [procedure_closed.dossiers.first.user.id] } + let(:email_content) { "La démarche a fermé" } + + subject do + post :notify_after_closing, params: { procedure_id: procedure_closed.id, procedure: { closing_notification_brouillon: true }, email_content_brouillon: email_content } + end + + before do + sign_in(admin.user) + end + + it 'redirects to admin procedures' do + expect { subject }.to have_enqueued_job(SendClosingNotificationJob).with(user_ids, email_content, procedure_closed) + expect(flash.notice).to eq("Les emails sont en cours d'envoi") + expect(response).to redirect_to :admin_procedures + end end describe 'DELETE #destroy' do @@ -1056,6 +1085,27 @@ describe Administrateurs::ProceduresController, type: :controller do end end + context 'procedure was closed and is re opened' do + before do + procedure.publish! + procedure.update!(closing_reason: 'internal_procedure', replaced_by_procedure_id: procedure2.id) + procedure.close! + procedure.update!(closing_notification_brouillon: true, closing_notification_en_cours: true) + perform_request + procedure.reload + procedure2.reload + end + + it 'publish the given procedure and reset closing params' do + expect(procedure.publiee?).to be_truthy + expect(procedure.path).to eq(path) + expect(procedure.closing_reason).to be_nil + expect(procedure.replaced_by_procedure_id).to be_nil + expect(procedure.closing_notification_brouillon).to be_falsy + expect(procedure.closing_notification_en_cours).to be_falsy + end + end + context 'procedure path exists and is not owned by current administrator' do let(:path) { procedure3.path } let(:lien_site_web) { 'http://mon-site.gouv.fr' } diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 1114af695..6fd487fe2 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -33,6 +33,10 @@ class UserMailerPreview < ActionMailer::Preview UserMailer.notify_inactive_close_to_deletion(user) end + def notify_after_closing + UserMailer.notify_after_closing([user]) + end + private def user diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 7382504f1..3bd017852 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -127,4 +127,22 @@ RSpec.describe UserMailer, type: :mailer do end end end + + describe '.notify_after_closing' do + let(:procedure) { create(:procedure) } + let(:content) { "Bonjour,\r\nsaut de ligne" } + subject { described_class.notify_after_closing(user, content, procedure) } + + it { expect(subject.to).to eq([user.email]) } + it { expect(subject.body).to include("Clôture d'une démarche sur Démarches simplifiées") } + it { expect(subject.body).to include("Bonjour,\r\n
saut de ligne") } + + context 'when perform_later is called' do + let(:custom_queue) { 'low_priority' } + before { ENV['BULK_EMAIL_QUEUE'] = custom_queue } + it 'enqueues email is custom queue for low priority delivery' do + expect { subject.deliver_later }.to have_enqueued_job.on_queue(custom_queue) + end + end + end end From 8c322353c76048e9576303fbe444aa21d8da1616 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 11:10:23 +0100 Subject: [PATCH 06/12] feat(procedure): add alert on admin show when procedure closed --- .../administrateurs/procedures/show.html.haml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/views/administrateurs/procedures/show.html.haml b/app/views/administrateurs/procedures/show.html.haml index 44652fc3f..4856331ab 100644 --- a/app/views/administrateurs/procedures/show.html.haml +++ b/app/views/administrateurs/procedures/show.html.haml @@ -47,6 +47,24 @@ - if !@procedure.procedure_expires_when_termine_enabled? = render partial: 'administrateurs/procedures/suggest_expires_when_termine', locals: { procedure: @procedure } + +- if @procedure.close? + .fr-container + = render Dsfr::AlertComponent.new(title: 'cette démarche est close', state: (:warning), heading_level: 'h2', extra_class_names: 'fr-my-2w') do |c| + - c.with_body do + %p + Les dossiers en cours peuvent être instruits, mais aucun nouveau dossier ne peut plus être déposé. + - if @procedure.closing_reason == 'internal_procedure' + - new_procedure = Procedure.find_by(id: @procedure.replaced_by_procedure_id) + %p + = "Cette démarche est remplacée par une autre démarche dans Démarches simplifiées :" + = link_to(new_procedure&.libelle, admin_procedure_path(new_procedure)) + - if @procedure.closing_reason == 'other' + %p + = "Plus d'informations dans la #{link_to('page de fermeture', closing_details_path(@procedure.path))}, visible par les usagers." + - if @procedure.closing_notification_brouillon? || @procedure.closing_notification_en_cours? + = "Un email a été envoyé pour informer les usagers le #{ l(@procedure.closed_at.to_date) }" + .fr-container %h2= "Gestion de la démarche № #{@procedure.id}" %h3.fr-h6 Indispensable avant publication From facb38b539170dcd0aad67c264545b63ac190a69 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 11:11:35 +0100 Subject: [PATCH 07/12] db(procedure): add a maintenance task to backfill closed procedures with closing reason --- ...losing_reason_in_closed_procedures_task.rb | 23 ++++++++++++++ ...g_reason_in_closed_procedures_task_spec.rb | 30 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 app/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task.rb create mode 100644 spec/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task_spec.rb diff --git a/app/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task.rb b/app/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task.rb new file mode 100644 index 000000000..49981e6ed --- /dev/null +++ b/app/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Maintenance + class BackfillClosingReasonInClosedProceduresTask < MaintenanceTasks::Task + def collection + Procedure + .with_discarded + .where(aasm_state: :close) + end + + def process(procedure) + if procedure.replaced_by_procedure_id.present? + procedure.update!(closing_reason: Procedure.closing_reasons.fetch(:internal_procedure)) + else + procedure.update!(closing_reason: Procedure.closing_reasons.fetch(:other)) + end + end + + def count + collection.count + end + end +end diff --git a/spec/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task_spec.rb b/spec/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task_spec.rb new file mode 100644 index 000000000..fa3e34c07 --- /dev/null +++ b/spec/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "rails_helper" + +module Maintenance + RSpec.describe BackfillClosingReasonInClosedProceduresTask do + describe "#process" do + subject(:process) { described_class.process(procedure) } + + context 'with a closed and replaced procedure' do + let(:published_procedure) { create(:procedure, :published) } + let(:procedure) { create(:procedure, :closed, replaced_by_procedure_id: published_procedure.id) } + + it 'fills closing_reason with internal_procedure' do + subject + expect(procedure.closing_reason).to eq Procedure.closing_reasons.fetch(:internal_procedure) + end + end + + context 'with a closed and not replaced procedure' do + let(:procedure) { create(:procedure, :closed) } + + it 'fills closing_reason with other' do + subject + expect(procedure.closing_reason).to eq Procedure.closing_reasons.fetch(:other) + end + end + end + end +end From d80021392090c7e8a265b5d9f1cb2a63d284180b Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 11:21:31 +0100 Subject: [PATCH 08/12] feat(procedure): add closing details page --- app/controllers/users/commencer_controller.rb | 10 +++++++--- app/views/layouts/closing_details.html.haml | 16 ++++++++++++++++ .../users/commencer/closing_details.html.haml | 8 ++++++++ config/locales/views/users/commencer/en.yml | 2 ++ config/locales/views/users/commencer/fr.yml | 2 ++ config/routes.rb | 1 + .../users/commencer_controller_spec.rb | 4 ++-- 7 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 app/views/layouts/closing_details.html.haml create mode 100644 app/views/users/commencer/closing_details.html.haml diff --git a/app/controllers/users/commencer_controller.rb b/app/controllers/users/commencer_controller.rb index bdbced324..3e6a958fb 100644 --- a/app/controllers/users/commencer_controller.rb +++ b/app/controllers/users/commencer_controller.rb @@ -78,6 +78,11 @@ module Users current_user ? :user : :guest end + def closing_details + @procedure = Procedure.find_by(path: params[:path]) + render 'closing_details', layout: 'closing_details' + end + private def extra_query_params @@ -136,9 +141,8 @@ module Users redirect_to commencer_path(procedure.replaced_by_procedure.path, **extra_query_params) return elsif procedure&.close? - flash.alert = procedure.service.presence ? - t('errors.messages.procedure_archived.with_service_and_phone_email', service_name: procedure.service.nom, service_phone_number: procedure.service.telephone, service_email: procedure.service.email) : - t('errors.messages.procedure_archived.with_organisation_only', organisation_name: procedure.organisation) + redirect_to closing_details_path(procedure.path) + return else flash.alert = t('errors.messages.procedure_not_found') end diff --git a/app/views/layouts/closing_details.html.haml b/app/views/layouts/closing_details.html.haml new file mode 100644 index 000000000..9faa2c075 --- /dev/null +++ b/app/views/layouts/closing_details.html.haml @@ -0,0 +1,16 @@ +- procedure = @procedure || @dossier&.procedure || nil + +- content_for :content do + .fr-container.fr-mt-5w + .fr-grid-row + .fr-col-12.fr-col-md-8.fr-col-offset-md-2 + .procedure-preview.fr-mb-5w + = yield + +- content_for :footer do + - if procedure + = render partial: 'users/procedure_footer', locals: { procedure: procedure, dossier: @dossier } + - else + = render partial: 'application/footer' + += render template: 'layouts/application' diff --git a/app/views/users/commencer/closing_details.html.haml b/app/views/users/commencer/closing_details.html.haml new file mode 100644 index 000000000..afbb80cdf --- /dev/null +++ b/app/views/users/commencer/closing_details.html.haml @@ -0,0 +1,8 @@ +- content_for(:title, @procedure.libelle) + +.fr-container + .fr-grid-row + .fr-col-12 + %h1= t('commencer.closing_details.page_title', libelle: @procedure.libelle, closed_at: @procedure.closed_at.strftime('%d/%m/%Y')) + %p + = format_text_value(@procedure.closing_details) diff --git a/config/locales/views/users/commencer/en.yml b/config/locales/views/users/commencer/en.yml index ab6d1db27..06ab8ce61 100644 --- a/config/locales/views/users/commencer/en.yml +++ b/config/locales/views/users/commencer/en.yml @@ -21,3 +21,5 @@ en: other: "Your last %{count} created files :" already_created_details_html: "N° %{id}, created %{created_at} ago and %{state}" + closing_details: + page_title: "Procedure %{libelle} is closed since %{closed_at}" diff --git a/config/locales/views/users/commencer/fr.yml b/config/locales/views/users/commencer/fr.yml index 9f3a8d2e0..f0b7308ce 100644 --- a/config/locales/views/users/commencer/fr.yml +++ b/config/locales/views/users/commencer/fr.yml @@ -21,3 +21,5 @@ fr: other: "Vos %{count} derniers dossiers créés :" already_created_details_html: "N° %{id}, créé il y a %{created_at} et %{state}" + closing_details: + page_title: "La démarche %{libelle} est close depuis le %{closed_at}" diff --git a/config/routes.rb b/config/routes.rb index 56f5cfe30..09c38d410 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -382,6 +382,7 @@ Rails.application.routes.draw do post 'accept_merge' => 'profil#accept_merge' post 'refuse_merge' => 'profil#refuse_merge' delete 'france_connect_information' => 'profil#destroy_fci' + get 'fermeture/:path', to: 'commencer#closing_details', as: :closing_details end get 'procedures/:id/logo', to: 'procedures#logo', as: :procedure_logo diff --git a/spec/controllers/users/commencer_controller_spec.rb b/spec/controllers/users/commencer_controller_spec.rb index 70c24bbb6..77801e36c 100644 --- a/spec/controllers/users/commencer_controller_spec.rb +++ b/spec/controllers/users/commencer_controller_spec.rb @@ -42,7 +42,7 @@ describe Users::CommencerController, type: :controller do published_procedure.organisation = "hello" published_procedure.close! get :commencer, params: { path: published_procedure.path } - expect(response).to redirect_to(root_path) + expect(response).to redirect_to(closing_details_path(published_procedure.path)) end end @@ -51,7 +51,7 @@ describe Users::CommencerController, type: :controller do published_procedure.service = create(:service) published_procedure.close! get :commencer, params: { path: published_procedure.path } - expect(response).to redirect_to(root_path) + expect(response).to redirect_to(closing_details_path(published_procedure.path)) end end From d3c26591c81caafae2638803f9d4c4791c27e9cb Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 11:22:55 +0100 Subject: [PATCH 09/12] feat(user dossiers): update messages when procedure is closed --- app/views/users/dossiers/_dossiers_list.html.haml | 9 ++++++++- config/locales/en.yml | 10 +++++++--- config/locales/fr.yml | 11 +++++++---- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index b0e17a303..4588785e6 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -54,7 +54,14 @@ = render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: "fr-mb-2w") do |c| - c.with_body do %p - = t('views.users.dossiers.dossiers_list.procedure_closed') + - if dossier.brouillon? && dossier.procedure.closing_reason == Procedure.closing_reasons.fetch(:internal_procedure) + = I18n.t('views.users.dossiers.dossiers_list.procedure_closed.brouillon.internal_procedure', link: commencer_path(dossier.procedure.replaced_by_procedure.path)).html_safe + - elsif dossier.brouillon? && dossier.procedure.closing_reason == Procedure.closing_reasons.fetch(:other) + = I18n.t('views.users.dossiers.dossiers_list.procedure_closed.brouillon.other', link: closing_details_path(dossier.procedure.path)).html_safe + - elsif (dossier.en_construction? || dossier.en_instruction?) && dossier.procedure.closing_reason == Procedure.closing_reasons.fetch(:internal_procedure) + = I18n.t('views.users.dossiers.dossiers_list.procedure_closed.en_cours.internal_procedure') + - elsif (dossier.en_construction? || dossier.en_instruction?) && dossier.procedure.closing_reason == Procedure.closing_reasons.fetch(:other) + = I18n.t('views.users.dossiers.dossiers_list.procedure_closed.en_cours.other', link: closing_details_path(dossier.procedure.path)).html_safe - if dossier.pending_correction? = render Dsfr::AlertComponent.new(state: :warning, size: :sm, extra_class_names: "fr-mb-2w") do |c| diff --git a/config/locales/en.yml b/config/locales/en.yml index 4c3201e89..0a4b4e46a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -507,6 +507,13 @@ en: shared_with: File shared with deleted: Deleted at %{date} deleted_badge: Deleted + procedure_closed: + brouillon: + internal_procedure: This procedure is closed, you cannot submit this file. We invite you to submit a new one on the procedure which replaces it + other: This process is closed, you cannot submit this file. More information here + en_cours: + internal_procedure: This procedure is closed. Your file has been submitted and can be investigated by the administration + other: "This procedure is closed. Your file has been submitted and can be processed by the administration. More information here" transfers: sender_from_support: Technical support sender_demande_en_cours: "A transfer request is pending on file Nº %{id} to %{email}" @@ -716,9 +723,6 @@ en: # # etablissement_fail: 'Désolé, nous n’avons pas réussi à enregistrer l’établissement correspondant à ce numéro SIRET' france_connect: connexion: "Error trying to connect to France Connect." - procedure_archived: - with_service_and_phone_email: This procedure has been closed, it is no longer possible to submit a file. For more information, please contact the service %{service_name}, available at %{service_phone_number} or by email %{service_email} - with_organisation_only: This procedure has been closed, it is no longer possible to submit a file. For more information, please contact the organisation %{organisation_name} evil_regexp: The regular expression you have entered is potentially dangerous and could lead to performance issues. mismatch_regexp: The provided example must match the regular expression syntax_error_regexp: The syntax of the regular expression is invalid diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 528d589a2..a40e17e03 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -502,7 +502,13 @@ fr: no_result_reset_search: Réinitialiser la recherche no_result_text_with_filter: ne correspond aux filtres sélectionnés no_result_reset_filter: Réinitialiser les filtres - procedure_closed: Cette démarche a été clôturée, vous ne pourrez pas redéposer de dossier à partir du lien de la démarche, contactez votre administration pour plus d’information. + procedure_closed: + brouillon: + internal_procedure: Cette démarche est close, vous ne pouvez pas déposer ce dossier. Nous vous invitons à en déposer un nouveau sur la démarche qui la remplace + other: Cette démarche est close, vous ne pouvez pas déposer ce dossier. Plus d'informations ici + en_cours: + internal_procedure: Cette démarche est close. Votre dossier est bien déposé et peut être instruit par l'administration + other: "Cette démarche est close. Votre dossier est bien déposé et peut être instruit par l'administration. Plus d'informations ici" pending_correction: Ce dossier attend vos corrections. Consultez les modifications à apporter dans la messagerie. depose_at: Déposé le %{date} created_at: Créé le %{date} @@ -722,9 +728,6 @@ fr: france_connect: connexion: "Erreur lors de la connexion à France Connect." forbidden_html: "Seul-e-s les usagers peuvent se connecter via France Connect. En tant qu’instructeur ou administrateur, nous vous invitons à réininitialiser votre mot de passe." - procedure_archived: - with_service_and_phone_email: Cette démarche en ligne a été close, il n’est plus possible de déposer de dossier. Pour plus d’informations veuillez contacter le service %{service_name} au %{service_phone_number} ou par email à %{service_email} - with_organisation_only: Cette démarche en ligne a été close, il n’est plus possible de déposer de dossier. Pour plus d’informations veuillez contacter le service %{organisation_name} evil_regexp: L'expression régulière que vous avez entrée est potentiellement dangereuse et pourrait entraîner des problèmes de performance mismatch_regexp: L'exemple doit correspondre à l'expression régulière fournie syntax_error_regexp: La syntaxe de l'expression régulière n'est pas valide From 5a098df525b4b9e4067e40cf2737c5f284a6e1e6 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 15 Feb 2024 11:24:16 +0100 Subject: [PATCH 10/12] test(procedure): add a system spec to test closing --- .../administrateurs/procedure_closing_spec.rb | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 spec/system/administrateurs/procedure_closing_spec.rb diff --git a/spec/system/administrateurs/procedure_closing_spec.rb b/spec/system/administrateurs/procedure_closing_spec.rb new file mode 100644 index 000000000..4a34f00dd --- /dev/null +++ b/spec/system/administrateurs/procedure_closing_spec.rb @@ -0,0 +1,96 @@ +require 'system/administrateurs/procedure_spec_helper' + +describe 'Closing a procedure', js: true do + include ProcedureSpecHelper + + let(:administrateur) { create(:administrateur) } + let!(:procedure) do + create(:procedure_with_dossiers, + :published, + :with_path, + :with_type_de_champ, + :with_service, + :with_zone, + administrateur: administrateur, + dossiers_count: 2) + end + + let!(:other_procedure) do + create(:procedure, + :published, + :with_path, + administrateur: administrateur) + end + + before do + login_as administrateur.user, scope: :user + end + + context 'when procedure is replaced in DS' do + scenario 'the link of the new procedure is added in show page' do + visit admin_procedure_close_path(procedure) + + expect(page).to have_current_path(admin_procedure_close_path(procedure)) + + expect(page).to have_text('Clore la démarche') + + select('Je remplace ma démarche par une autre dans Démarches simplifiées') + + select("#{other_procedure.libelle} (#{other_procedure.id})") + + accept_alert do + within('form') { click_on 'Clore la démarche' } + end + + procedure.reload + + expect(page).to have_current_path(admin_procedure_closing_notification_path(procedure)) + + expect(page).to have_text('Votre démarche est close') + end + end + + context 'when procedure is not replaced in DS' do + scenario 'the admin can notify users' do + visit admin_procedure_close_path(procedure) + + expect(page).to have_current_path(admin_procedure_close_path(procedure)) + + expect(page).to have_text('Clore la démarche') + + select('Autre') + + fill_in("Message d'information remplaçant la démarche", with: "Bonjour,\nLa démarche est maintenant sur www.autre-site.fr\nCordialement") + + accept_alert do + within('form') { click_on 'Clore la démarche' } + end + + procedure.reload + + expect(page).to have_current_path(admin_procedure_closing_notification_path(procedure)) + + expect(page).to have_text('Votre démarche est close') + + expect(page).to have_text("Souhaitez-vous envoyer un email à l'utilisateur avec un dossier en brouillon ?") + + check("Souhaitez-vous envoyer un email à l'utilisateur avec un dossier en brouillon ?") + + expect(page).to have_text ("Contenu de l'email") + + fill_in('email_content_brouillon', with: "La démarche a fermé.") + + accept_alert do + click_on 'Informer les usagers' + end + + expect(page).to have_current_path(admin_procedures_path) + + visit admin_procedure_path(procedure) + + procedure.reload + + expect(page).to have_text("Un email a été envoyé pour informer les usagers le #{procedure.closed_at.strftime('%d/%m/%Y')}") + end + end +end From c96d86a64b0b989aa46cba617201cf953825c2d2 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Thu, 7 Mar 2024 18:58:39 +0100 Subject: [PATCH 11/12] style(procedure removed banner): add links for email and telephone --- app/views/users/dossiers/_procedure_removed_banner.html.haml | 2 +- config/locales/views/users/header/en.yml | 2 +- config/locales/views/users/header/fr.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/users/dossiers/_procedure_removed_banner.html.haml b/app/views/users/dossiers/_procedure_removed_banner.html.haml index efa9b17ac..cffffab84 100644 --- a/app/views/users/dossiers/_procedure_removed_banner.html.haml +++ b/app/views/users/dossiers/_procedure_removed_banner.html.haml @@ -14,7 +14,7 @@ = render partial: "users/dossiers/replacement_procedure", locals: { replacement_procedure: dossier.procedure.replaced_by_procedure } %p - = t('users.dossiers.header.banner.contact_service', service_name: dossier.procedure.service.nom, service_phone_number: dossier.procedure.service.telephone, service_email: dossier.procedure.service.email) + = t('users.dossiers.header.banner.contact_service_html', service_name: dossier.procedure.service.nom, service_phone_number: Phonelib.parse(dossier.procedure.service.telephone_url).full_national, service_email: dossier.procedure.service.email) - if !dossier.brouillon? = render(partial: 'users/dossiers/show/print_dossier', locals: { dossier: dossier }) diff --git a/config/locales/views/users/header/en.yml b/config/locales/views/users/header/en.yml index 249c915cb..947d59046 100644 --- a/config/locales/views/users/header/en.yml +++ b/config/locales/views/users/header/en.yml @@ -11,7 +11,7 @@ en: procedure_close_content: You can still consult your file, but it will not be processed by the administration new_procedure_link: see the procedure new_procedure_content: "A new procedure is available, consult it here:" - contact_service: For more information, please contact the service %{service_name}, available at %{service_phone_number} or by email %{service_email} + contact_service_html: For more information, please contact the service %{service_name}, available at %{service_phone_number} or by email to %{service_email} states: brouillon: Your file is still in draft and will soon expire. So it will be deleted soon without being instructed. If you want to pursue your procedure you can submit it now. Otherwise you are able to delay its expiration by clicking on the underneath button. en_construction: Your file is pending for instruction. The maximum delay is %{nominal_duration_months} months, but you can extend the duration by clicking on the underneath button. diff --git a/config/locales/views/users/header/fr.yml b/config/locales/views/users/header/fr.yml index 8246d82a5..07de1e7a8 100644 --- a/config/locales/views/users/header/fr.yml +++ b/config/locales/views/users/header/fr.yml @@ -10,7 +10,7 @@ fr: procedure_close_content: "Vous pouvez toujours consulter votre dossier, mais il ne sera pas traité par l’administration" new_procedure_link: voir la démarche new_procedure_content: "Une nouvelle démarche est disponible, consultez-la ici :" - contact_service: Pour plus d’informations, veuillez vous rapprocher du service %{service_name}, disponible au %{service_phone_number} ou par email %{service_email} + contact_service_html: Pour plus d’informations, veuillez vous rapprocher du service %{service_name}, disponible au %{service_phone_number} ou par email à %{service_email} title: Votre dossier va expirer states: brouillon: Votre dossier est en brouillon, mais va bientôt expirer. Cela signifie qu’il va bientôt être supprimé sans avoir été déposé. Si vous souhaitez le conserver afin de poursuivre la démarche, vous pouvez étendre la durée de conversation en cliquant sur le bouton ci-dessous. From 2d186a72921b27dc80ecd675a209ee4925e57fa8 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Mon, 11 Mar 2024 17:29:47 +0100 Subject: [PATCH 12/12] add svg in view --- app/views/users/commencer/closing_details.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/users/commencer/closing_details.html.haml b/app/views/users/commencer/closing_details.html.haml index afbb80cdf..9b3a34c1a 100644 --- a/app/views/users/commencer/closing_details.html.haml +++ b/app/views/users/commencer/closing_details.html.haml @@ -6,3 +6,6 @@ %h1= t('commencer.closing_details.page_title', libelle: @procedure.libelle, closed_at: @procedure.closed_at.strftime('%d/%m/%Y')) %p = format_text_value(@procedure.closing_details) + + .text-center + = image_tag('landing/hero/dematerialiser.svg', "aria-hidden": true)