Merge pull request #9930 from demarches-simplifiees/add-external-link-to-procedure

ETQ admin lorsque je clos une démarche je peux alerter les usagers et je crée une page de fermeture si la démarche n'est pas redirigée dans DS
This commit is contained in:
Colin Darie 2024-03-12 15:03:30 +00:00 committed by GitHub
commit ee92668611
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 721 additions and 47 deletions

View file

@ -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

View file

@ -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

View file

@ -14,6 +14,8 @@
title: 'Autogestion des instructeurs',
hint: "Lautogestion des instructeurs permet aux instructeurs de gérer eux-mêmes la liste des instructeurs de la démarche.#{ 'Nous recommandons de laisser lautogestion 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 } }

View file

@ -194,21 +194,56 @@ 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
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 lemail afin dinformer 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 denvoi"
end
redirect_to admin_procedures_path
end
def destroy
procedure = current_administrateur.procedures.find(params[:id])
@ -301,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
@ -327,6 +366,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
@ -494,6 +534,22 @@ 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 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

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -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 = '';
}
}
}

View file

@ -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

View file

@ -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',

View file

@ -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: {
@ -1002,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

View file

@ -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

View file

@ -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..."} }

View file

@ -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'

View file

@ -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

View file

@ -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'

View file

@ -0,0 +1,4 @@
- content_for(:title, @subject)
%p
= simple_format(@content)

View file

@ -0,0 +1,11 @@
- 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)
.text-center
= image_tag('landing/hero/dematerialiser.svg', "aria-hidden": true)

View file

@ -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|

View file

@ -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 })

View file

@ -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 <a href="%{link}" target="_blank" rel="noopener noreferrer">procedure</a> which replaces it
other: This process is closed, you cannot submit this file. More information <a href="%{link}" target="_blank" rel="noopener noreferrer">here</a>
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 <a href=%{link} target=_blank rel=noopener noreferrer>here</a>"
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 navons 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

View file

@ -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 dinformation.
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 <a href="%{link}" target="_blank" rel="noopener noreferrer">démarche</a> qui la remplace
other: Cette démarche est close, vous ne pouvez pas déposer ce dossier. Plus d'informations <a href="%{link}" target="_blank" rel="noopener noreferrer">ici</a>
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 <a href=%{link} target=_blank rel=noopener noreferrer>ici</a>"
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 quinstructeur ou administrateur, nous vous invitons à <a href='%{reset_link}'>réininitialiser votre mot de passe</a>."
procedure_archived:
with_service_and_phone_email: Cette démarche en ligne a été close, il nest plus possible de déposer de dossier. Pour plus dinformations 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 nest plus possible de déposer de dossier. Pour plus dinformations 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

View file

@ -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

View file

@ -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 lURL, 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

View file

@ -3,9 +3,25 @@ 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
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 <a href="%{closing_path}" target="_blank" rel="noopener noreferrer">closing page</a>.
page_subtitle_with_redirection: Your procedure has been successfully closed. The link of the procedure now redirects to this <a href="%{redirection_path}" target="_blank" rel="noopener noreferrer">procedure</a>.
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."

View file

@ -3,9 +3,25 @@ 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
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 <a href="%{closing_path}" target="_blank" rel="noopener noreferrer">page de fermeture</a>.
page_subtitle_with_redirection: Votre démarche est close. Le lien public redirige désormais vers cette <a href="%{redirection_path}" target="_blank" rel="noopener noreferrer">démarche</a>.
callout_content: Vous avez la possibilité de continuer à instruire les dossiers déposés. Si vous navez pas lintention dinstruire 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.

View file

@ -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}"

View file

@ -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}"

View file

@ -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 <a href="mailto:%{service_email}">%{service_email}</a>
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.

View file

@ -10,7 +10,7 @@ fr:
procedure_close_content: "Vous pouvez toujours consulter votre dossier, mais il ne sera pas traité par ladministration"
new_procedure_link: voir la démarche
new_procedure_content: "Une nouvelle démarche est disponible, consultez-la ici :"
contact_service: Pour plus dinformations, veuillez vous rapprocher du service %{service_name}, disponible au %{service_phone_number} ou par email %{service_email}
contact_service_html: Pour plus dinformations, veuillez vous rapprocher du service %{service_name}, disponible au %{service_phone_number} ou par email à <a href="mailto:%{service_email}">%{service_email}</a>
title: Votre dossier va expirer
states:
brouillon: Votre dossier est en brouillon, mais va bientôt expirer. Cela signifie quil 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.

View file

@ -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
@ -600,6 +601,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]

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -720,36 +720,60 @@ 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
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 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
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
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_procedure_path(procedure.id)
expect(flash[:notice]).to have_content 'Démarche close'
end
end
@ -760,7 +784,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
@ -769,6 +793,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
@ -1055,6 +1115,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' }

View file

@ -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

View file

@ -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

View file

@ -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&#39;une démarche sur Démarches simplifiées") }
it { expect(subject.body).to include("Bonjour,\r\n<br />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

View file

@ -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) }

View file

@ -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

View file

@ -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