Merge pull request #8714 from colinux/dossier-pending-resolution

ETQ Instructeur je peux marquer un dossier "à corriger" par l'usager
This commit is contained in:
Colin Darie 2023-06-05 08:04:44 +00:00 committed by GitHub
commit 8cd5f31488
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 899 additions and 128 deletions

View file

@ -45,7 +45,8 @@
} }
} }
.number-col { .number-col,
.fr-badge {
white-space: nowrap; white-space: nowrap;
} }

View file

@ -30,7 +30,7 @@
color: $dark-red; color: $dark-red;
} }
label, label:not(.fr-label),
legend.form-label { legend.form-label {
font-size: 18px; font-size: 18px;
margin-bottom: $default-padding; margin-bottom: $default-padding;

View file

@ -2,7 +2,6 @@
@import "constants"; @import "constants";
.motivation { .motivation {
padding: $default-padding;
color: $black; color: $black;
width: 450px; width: 450px;

View file

@ -27,7 +27,8 @@ class Dossiers::EditFooterComponent < ApplicationComponent
{ {
class: 'fr-btn fr-btn--sm', class: 'fr-btn fr-btn--sm',
method: :post, method: :post,
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' } data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' },
form: { id: "form-submit-en-construction" }
} }
end end

View file

@ -8,6 +8,13 @@ class Dossiers::MessageComponent < ApplicationComponent
attr_reader :commentaire, :connected_user, :messagerie_seen_at attr_reader :commentaire, :connected_user, :messagerie_seen_at
def correction_badge
return if commentaire.dossier_correction.nil?
return helpers.correction_resolved_badge if commentaire.dossier_correction.resolved?
helpers.pending_correction_badge(connected_user.is_a?(Instructeur) ? :for_instructeur : :for_user)
end
private private
def show_reply_button? def show_reply_button?

View file

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

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
class Instructeurs::EnConstructionMenuComponent < ApplicationComponent
attr_reader :dossier
def initialize(dossier:)
@dossier = dossier
end
def render?
return true if dossier.may_repasser_en_construction?
return true if dossier.may_flag_as_pending_correction?
false
end
def menu_label
if dossier.en_construction?
t('.request_correction')
else
t(".revert_en_construction")
end
end
end

View file

@ -0,0 +1,4 @@
---
en:
revert_en_construction: Revert to in progress
request_correction: Request a correction

View file

@ -0,0 +1,4 @@
---
fr:
revert_en_construction: Repasser en construction
request_correction: Demander une correction

View file

@ -0,0 +1,31 @@
= render Dropdown::MenuComponent.new(wrapper: :div, menu_options: { id: "menu-en-construction" }, button_options: { class: "fr-btn--secondary" }, role: :region) do |menu|
- menu.with_button_inner_html do
= menu_label
- if dossier.may_repasser_en_construction?
= menu.with_item do
= link_to(repasser_en_construction_instructeur_dossier_path(dossier.procedure.id, dossier.id), method: :post, role: 'menuitem') do
%span.fr-icon.fr-icon-draft-line.fr-text-default--info.fr-mt-1v{ "aria-hidden": "true" }
.dropdown-description
%h4= t('.revert_en_construction')
Lusager sera notifié quil peut modifier son dossier
- menu.with_item do
= link_to('#', onclick: "DS.showMotivation(event, 'pending_correction');", role: 'menuitem') do
%span.fr-icon.fr-icon-error-warning-line.fr-text-default--info.fr-mt-1v{ "aria-hidden": "true" }
.dropdown-description
%h4= t('.request_correction')
Lusager sera notifié que des modifications sont attendues
- menu.with_item(class: "inactive form-inside fr-pt-1v") do
= render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier:,
visible: true,
form_path: pending_correction_instructeur_dossier_path(dossier.procedure, dossier),
placeholder: 'Expliquez au demandeur quelle(s) correction(s) sont attendues',
popup_class: 'pending_correction',
button_justificatif_label: "Ajouter une pièce jointe (facultatif)",
process_button: dossier.en_construction? ? 'Valider' : 'Valider et repasser en construction',
process_action: nil,
title: 'Marquer en attente de corrections',
confirm: 'Envoyer la demande de corrections ?'}

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Instructeurs::InstructionMenuComponent < ApplicationComponent
attr_reader :dossier
def initialize(dossier:)
@dossier = dossier
end
def render?
dossier.en_instruction?
end
def menu_label
t(".instruct")
end
end

View file

@ -0,0 +1,3 @@
---
en:
instruct: Instruct the file

View file

@ -0,0 +1,3 @@
---
fr:
instruct: Instruire le dossier

View file

@ -0,0 +1,34 @@
= render Dropdown::MenuComponent.new(wrapper: :div, wrapper_options: { data: { turbo_force: :server } }, role: :region) do |menu|
- menu.with_button_inner_html do
= menu_label
- menu.with_item do
= link_to('#', onclick: "DS.showMotivation(event, 'accept');", role: 'menuitem') do
%span.icon.accept
.dropdown-description
%h4 Accepter
Lusager sera informé que son dossier a été accepté
- menu.with_item(class: "hidden inactive form-inside fr-pt-1v") do
= render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: 'Expliquez au demandeur pourquoi ce dossier est accepté (facultatif)', popup_class: 'accept', process_action: 'accepter', title: 'Accepter', confirm: "Confirmez-vous l'acceptation ce dossier ?" }
- menu.with_item do
= link_to('#', onclick: "DS.showMotivation(event, 'without-continuation');", role: 'menuitem') do
%span.icon.without-continuation
.dropdown-description
%h4 Classer sans suite
Lusager sera informé que son dossier a été classé sans suite
- menu.with_item(class: "hidden inactive form-inside fr-pt-1v") do
= render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: 'Expliquez au demandeur pourquoi ce dossier est classé sans suite (obligatoire)', popup_class: 'without-continuation', process_action: 'classer_sans_suite', title: 'Classer sans suite', confirm: 'Confirmez-vous le classement sans suite de ce dossier ?' }
- menu.with_item do
= link_to('#', onclick: "DS.showMotivation(event, 'refuse');", role: 'menuitem') do
%span.icon.refuse
.dropdown-description
%h4 Refuser
Lusager sera informé que son dossier a été refusé
- menu.with_item(class: "hidden inactive form-inside fr-pt-1v") do
= render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: 'Expliquez au demandeur pourquoi ce dossier est refusé (obligatoire)', popup_class: 'refuse', process_action: 'refuser', title: 'Refuser', confirm: 'Confirmez-vous le refus de ce dossier ?' }

View file

@ -13,7 +13,7 @@ module Instructeurs
before_action :redirect_on_dossier_in_batch_operation, only: [:archive, :unarchive, :follow, :unfollow, :passer_en_instruction, :repasser_en_construction, :repasser_en_instruction, :terminer, :restore, :destroy, :extend_conservation] before_action :redirect_on_dossier_in_batch_operation, only: [:archive, :unarchive, :follow, :unfollow, :passer_en_instruction, :repasser_en_construction, :repasser_en_instruction, :terminer, :restore, :destroy, :extend_conservation]
after_action :mark_demande_as_read, only: :show after_action :mark_demande_as_read, only: :show
after_action :mark_messagerie_as_read, only: [:messagerie, :create_commentaire] after_action :mark_messagerie_as_read, only: [:messagerie, :create_commentaire, :pending_correction]
after_action :mark_avis_as_read, only: [:avis, :create_avis] after_action :mark_avis_as_read, only: [:avis, :create_avis]
after_action :mark_annotations_privees_as_read, only: [:annotations_privees, :update_annotations] after_action :mark_annotations_privees_as_read, only: [:annotations_privees, :update_annotations]
@ -223,6 +223,39 @@ module Instructeurs
render :change_state render :change_state
end end
def pending_correction
message, piece_jointe = params.require(:dossier).permit(:motivation, :justificatif_motivation).values
if message.empty?
flash.alert = "Vous devez préciser quelle correction est attendue."
elsif !dossier.may_flag_as_pending_correction?
flash.alert = dossier.termine? ? "Impossible de demander de corriger un dossier terminé." : "Le dossier est déjà en attente de correction."
else
commentaire = CommentaireService.build(current_instructeur, dossier, { body: message, piece_jointe: })
if commentaire.valid?
dossier.flag_as_pending_correction!(commentaire)
dossier.update!(last_commentaire_updated_at: Time.zone.now)
current_instructeur.follow(dossier)
flash.notice = "Dossier marqué comme en attente de correction."
else
flash.alert = commentaire.errors.full_messages.map { "Commentaire : #{_1}" }
end
end
respond_to do |format|
format.turbo_stream do
@dossier = dossier
render :change_state
end
format.html do
redirect_back(fallback_location: instructeur_procedure_path(procedure))
end
end
end
def create_commentaire def create_commentaire
@commentaire = CommentaireService.create(current_instructeur, dossier, commentaire_params) @commentaire = CommentaireService.create(current_instructeur, dossier, commentaire_params)

View file

@ -227,6 +227,10 @@ module Users
editing_fork_origin.merge_fork(@dossier) editing_fork_origin.merge_fork(@dossier)
RoutingEngine.compute(editing_fork_origin) RoutingEngine.compute(editing_fork_origin)
if cast_bool(params.dig(:dossier, :pending_correction_confirm))
editing_fork_origin.resolve_pending_correction!
end
redirect_to dossier_path(editing_fork_origin) redirect_to dossier_path(editing_fork_origin)
else else
flash.now.alert = errors flash.now.alert = errors

View file

@ -79,7 +79,7 @@ module DossierHelper
def status_badge(state, alignment_class = '') def status_badge(state, alignment_class = '')
status_text = dossier_display_state(state, lower: true) status_text = dossier_display_state(state, lower: true)
tag.span(status_text, class: "fr-badge #{class_badge_state(state)} fr-badge--no-icon #{alignment_class}", role: 'status') tag.span(status_text, class: "fr-badge fr-badge--sm #{class_badge_state(state)} fr-badge--no-icon #{alignment_class}", role: 'status')
end end
def deletion_reason_badge(reason) def deletion_reason_badge(reason)
@ -94,6 +94,14 @@ module DossierHelper
tag.span(status_text, class: "label #{status_class} ") tag.span(status_text, class: "label #{status_class} ")
end end
def pending_correction_badge(for_profile, html_class: nil)
tag.span(Dossier.human_attribute_name("pending_correction.#{for_profile}"), class: ['fr-badge fr-badge--sm fr-badge--warning super', html_class], role: 'status')
end
def correction_resolved_badge
tag.span(Dossier.human_attribute_name("pending_correction.resolved"), class: ['fr-badge fr-badge--sm fr-badge--success super'], role: 'status')
end
def demandeur_dossier(dossier) def demandeur_dossier(dossier)
if dossier.procedure.for_individual? if dossier.procedure.for_individual?
"#{dossier&.individual&.nom} #{dossier&.individual&.prenom}" "#{dossier&.individual&.nom} #{dossier&.individual&.prenom}"

View file

@ -46,6 +46,19 @@ class DossierMailer < ApplicationMailer
end end
end end
def notify_pending_correction(dossier)
I18n.with_locale(dossier.user_locale) do
@dossier = dossier
@service = dossier.procedure.service
@logo_url = attach_logo(dossier.procedure)
@subject = default_i18n_subject(dossier_id: dossier.id, libelle_demarche: dossier.procedure.libelle)
mail(to: dossier.user_email_for(:notification), subject: @subject) do |format|
format.html { render layout: 'mailers/notifications_layout' }
end
end
end
def notify_new_avis_to_instructeur(avis, instructeur_email) def notify_new_avis_to_instructeur(avis, instructeur_email)
I18n.with_locale(avis.dossier.user_locale) do I18n.with_locale(avis.dossier.user_locale) do
@avis = avis @avis = avis

View file

@ -50,6 +50,10 @@ class NotificationMailer < ApplicationMailer
with(dossier: dossier, state: Dossier.states.fetch(:sans_suite)).send_notification with(dossier: dossier, state: Dossier.states.fetch(:sans_suite)).send_notification
end end
def self.send_pending_correction(dossier)
with(dossier: dossier).send_notification
end
private private
def set_services_publics_plus def set_services_publics_plus

View file

@ -19,6 +19,7 @@ class Commentaire < ApplicationRecord
belongs_to :instructeur, inverse_of: :commentaires, optional: true belongs_to :instructeur, inverse_of: :commentaires, optional: true
belongs_to :expert, inverse_of: :commentaires, optional: true belongs_to :expert, inverse_of: :commentaires, optional: true
has_one :dossier_correction, inverse_of: :commentaire, dependent: :nullify
validate :messagerie_available?, on: :create, unless: -> { dossier.brouillon? } validate :messagerie_available?, on: :create, unless: -> { dossier.brouillon? }
@ -94,6 +95,10 @@ class Commentaire < ApplicationRecord
update! body: '' update! body: ''
end end
def flagged_pending_correction?
DossierCorrection.exists?(commentaire: self)
end
private private
def notify def notify
@ -108,8 +113,12 @@ class Commentaire < ApplicationRecord
end end
def notify_user(job_options = {}) def notify_user(job_options = {})
if flagged_pending_correction?
DossierMailer.notify_pending_correction(dossier).deliver_later(job_options)
else
DossierMailer.with(commentaire: self).notify_new_answer.deliver_later(job_options) DossierMailer.with(commentaire: self).notify_new_answer.deliver_later(job_options)
end end
end
def messagerie_available? def messagerie_available?
return if sent_by_system? return if sent_by_system?

View file

@ -0,0 +1,38 @@
module DossierCorrectableConcern
extend ActiveSupport::Concern
included do
has_many :corrections, class_name: 'DossierCorrection', dependent: :destroy
def flag_as_pending_correction!(commentaire)
return unless may_flag_as_pending_correction?
corrections.create!(commentaire:)
return if en_construction?
repasser_en_construction!(instructeur: commentaire.instructeur)
end
def may_flag_as_pending_correction?
return false if corrections.pending.exists?
en_construction? || may_repasser_en_construction?
end
def pending_correction?
# We don't want to show any alert if user is not allowed to modify the dossier
return false unless en_construction?
corrections.pending.exists?
end
def pending_correction
corrections.pending.first
end
def resolve_pending_correction!
corrections.pending.update!(resolved_at: Time.current)
end
end
end

View file

@ -47,12 +47,13 @@
# user_id :integer # user_id :integer
# #
class Dossier < ApplicationRecord class Dossier < ApplicationRecord
include DossierCloneConcern
include DossierCorrectableConcern
include DossierFilteringConcern include DossierFilteringConcern
include DossierPrefillableConcern include DossierPrefillableConcern
include DossierRebaseConcern include DossierRebaseConcern
include DossierSearchableConcern include DossierSearchableConcern
include DossierSectionsConcern include DossierSectionsConcern
include DossierCloneConcern
enum state: { enum state: {
brouillon: 'brouillon', brouillon: 'brouillon',
@ -98,6 +99,8 @@ class Dossier < ApplicationRecord
has_many :prefilled_champs_public, -> { root.public_only.prefilled }, class_name: 'Champ', inverse_of: false has_many :prefilled_champs_public, -> { root.public_only.prefilled }, class_name: 'Champ', inverse_of: false
has_many :commentaires, inverse_of: :dossier, dependent: :destroy has_many :commentaires, inverse_of: :dossier, dependent: :destroy
has_many :preloaded_commentaires, -> { includes(:dossier_correction, piece_jointe_attachment: :blob) }, class_name: 'Commentaire', inverse_of: :dossier
has_many :invites, dependent: :destroy has_many :invites, dependent: :destroy
has_many :follows, -> { active }, inverse_of: :dossier has_many :follows, -> { active }, inverse_of: :dossier
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
@ -890,6 +893,8 @@ class Dossier < ApplicationRecord
.processed_at .processed_at
save! save!
resolve_pending_correction!
if !disable_notification if !disable_notification
NotificationMailer.send_en_instruction_notification(self).deliver_later NotificationMailer.send_en_instruction_notification(self).deliver_later
end end

View file

@ -0,0 +1,23 @@
# == Schema Information
#
# Table name: dossier_corrections
#
# id :bigint not null, primary key
# resolved_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# commentaire_id :bigint
# dossier_id :bigint not null
#
class DossierCorrection < ApplicationRecord
belongs_to :dossier
belongs_to :commentaire
validates_associated :commentaire
scope :pending, -> { where(resolved_at: nil) }
def resolved?
resolved_at.present?
end
end

View file

@ -183,6 +183,8 @@ class ProcedurePresentation < ApplicationRecord
.filter_map { |v| Time.zone.parse(v).beginning_of_day rescue nil } .filter_map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
dossiers.filter_by_datetimes(column, dates) dossiers.filter_by_datetimes(column, dates)
elsif field['column'] == "state" && values.include?("pending_correction")
dossiers.joins(:corrections).where(corrections: DossierCorrection.pending)
else else
dossiers.where("dossiers.#{column} IN (?)", values) dossiers.where("dossiers.#{column} IN (?)", values)
end end
@ -245,7 +247,11 @@ class ProcedurePresentation < ApplicationRecord
if [TYPE_DE_CHAMP, TYPE_DE_CHAMP_PRIVATE].include?(filter[TABLE]) if [TYPE_DE_CHAMP, TYPE_DE_CHAMP_PRIVATE].include?(filter[TABLE])
find_type_de_champ(filter[COLUMN]).dynamic_type.filter_to_human(filter['value']) find_type_de_champ(filter[COLUMN]).dynamic_type.filter_to_human(filter['value'])
elsif filter['column'] == 'state' elsif filter['column'] == 'state'
if filter['value'] == 'pending_correction'
Dossier.human_attribute_name("pending_correction.for_instructeur")
else
Dossier.human_attribute_name("state.#{filter['value']}") Dossier.human_attribute_name("state.#{filter['value']}")
end
elsif filter['table'] == 'groupe_instructeur' && filter['column'] == 'id' elsif filter['table'] == 'groupe_instructeur' && filter['column'] == 'id'
instructeur.groupe_instructeurs instructeur.groupe_instructeurs
.find { _1.id == filter['value'].to_i }&.label || filter['value'] .find { _1.id == filter['value'].to_i }&.label || filter['value']

View file

@ -1,5 +1,11 @@
class DossierProjectionService class DossierProjectionService
class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :hidden_by_administration_at, :batch_operation_id, :columns) class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :hidden_by_administration_at, :batch_operation_id, :corrections, :columns) do
def pending_correction?
return false if corrections.blank?
corrections.any? { _1[:resolved_at].nil? }
end
end
end end
TABLE = 'table' TABLE = 'table'
@ -23,7 +29,8 @@ class DossierProjectionService
batch_operation_field = { TABLE => 'self', COLUMN => 'batch_operation_id' } batch_operation_field = { TABLE => 'self', COLUMN => 'batch_operation_id' }
hidden_by_user_at_field = { TABLE => 'self', COLUMN => 'hidden_by_user_at' } hidden_by_user_at_field = { TABLE => 'self', COLUMN => 'hidden_by_user_at' }
hidden_by_administration_at_field = { TABLE => 'self', COLUMN => 'hidden_by_administration_at' } hidden_by_administration_at_field = { TABLE => 'self', COLUMN => 'hidden_by_administration_at' }
([state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field, batch_operation_field] + fields) # the view needs state and archived dossier attributes dossier_corrections = { TABLE => 'dossier_corrections', COLUMN => 'resolved_at' }
([state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field, batch_operation_field, dossier_corrections] + fields) # the view needs state and archived dossier attributes
.each { |f| f[:id_value_h] = {} } .each { |f| f[:id_value_h] = {} }
.group_by { |f| f[TABLE] } # one query per table .group_by { |f| f[TABLE] } # one query per table
.each do |table, fields| .each do |table, fields|
@ -76,6 +83,18 @@ class DossierProjectionService
.where(id: dossiers_ids) .where(id: dossiers_ids)
.pluck('dossiers.id, groupe_instructeurs.label') .pluck('dossiers.id, groupe_instructeurs.label')
.to_h .to_h
when 'dossier_corrections'
columns = fields.map { _1[COLUMN].to_sym }
id_value_h = DossierCorrection.where(dossier_id: dossiers_ids)
.pluck(:dossier_id, *columns)
.group_by(&:first) # group corrections by dossier_id
.transform_values do |values| # build each correction has an hash column => value
values.map { Hash[columns.zip(_1[1..-1])] }
end
fields[0][:id_value_h] = id_value_h
when 'procedure' when 'procedure'
Dossier Dossier
.joins(:procedure) .joins(:procedure)
@ -111,6 +130,7 @@ class DossierProjectionService
hidden_by_user_at_field[:id_value_h][dossier_id], hidden_by_user_at_field[:id_value_h][dossier_id],
hidden_by_administration_at_field[:id_value_h][dossier_id], hidden_by_administration_at_field[:id_value_h][dossier_id],
batch_operation_field[:id_value_h][dossier_id], batch_operation_field[:id_value_h][dossier_id],
dossier_corrections[:id_value_h][dossier_id],
fields.map { |f| f[:id_value_h][dossier_id] } fields.map { |f| f[:id_value_h][dossier_id] }
) )
end end

View file

@ -0,0 +1,13 @@
- content_for :procedure_logo do
= render 'layouts/mailers/logo', url: @logo_url
%p= t(:hello, scope: [:views, :shared, :greetings])
%p= t('.explanation_html', dossier_id: @dossier.id, libelle_demarche: @dossier.procedure.libelle)
%p= t('.link')
= round_button(t('.access_message'), messagerie_dossier_url(@dossier), :primary)
= render 'layouts/mailers/signature', service: @service
- content_for :footer do
= render 'layouts/mailers/service_footer', service: @service, dossier: @dossier

View file

@ -2,12 +2,14 @@
= render partial: "instructeurs/procedures/dossier_actions", = render partial: "instructeurs/procedures/dossier_actions",
locals: { procedure_id: dossier.procedure.id, locals: { procedure_id: dossier.procedure.id,
dossier_id: dossier.id, dossier_id: dossier.id,
dossier: dossier,
state: dossier.state, state: dossier.state,
archived: dossier.archived, archived: dossier.archived,
dossier_is_followed: current_instructeur&.follow?(dossier), dossier_is_followed: current_instructeur&.follow?(dossier),
close_to_expiration: dossier.close_to_expiration?, close_to_expiration: dossier.close_to_expiration?,
hidden_by_administration: dossier.hidden_by_administration?, hidden_by_administration: dossier.hidden_by_administration?,
turbo: true } turbo: true,
with_menu: true }
%li.instruction-button %li.instruction-button
= render partial: "instruction_button", locals: { dossier: dossier } = render Instructeurs::InstructionMenuComponent.new(dossier:)

View file

@ -5,6 +5,8 @@
= "Dossier nº #{dossier.id}" = "Dossier nº #{dossier.id}"
= status_badge(dossier.state, 'super') = status_badge(dossier.state, 'super')
= pending_correction_badge(:for_instructeur) if dossier.pending_correction?
= link_to dossier.procedure.libelle.truncate_words(10), instructeur_procedure_path(dossier.procedure), title: dossier.procedure.libelle, class: "fr-link" = link_to dossier.procedure.libelle.truncate_words(10), instructeur_procedure_path(dossier.procedure), title: dossier.procedure.libelle, class: "fr-link"
= procedure_badge(dossier.procedure) = procedure_badge(dossier.procedure)

View file

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

View file

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

View file

@ -29,21 +29,24 @@
= "" = ""
- elsif Dossier::EN_CONSTRUCTION_OU_INSTRUCTION.include?(state) - elsif Dossier::EN_CONSTRUCTION_OU_INSTRUCTION.include?(state)
- if Dossier.states[:en_construction] == state
%li{ 'data-turbo': turbo ? 'true' : 'false' }
= button_to passer_en_instruction_instructeur_dossier_path(procedure_id, dossier_id), method: :post, class: 'fr-btn fr-btn--secondary fr-icon-edit-line' do
Passer en instruction
- elsif Dossier.states[:en_instruction] == state
%li{ 'data-turbo': turbo ? 'true' : 'false' }
= button_to repasser_en_construction_instructeur_dossier_path(procedure_id, dossier_id), method: :post, class: 'fr-btn fr-btn--secondary fr-icon-draft-line' do
Repasser en construction
- if dossier_is_followed - if dossier_is_followed
%li %li
= button_to unfollow_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, class: 'fr-btn fr-btn--secondary fr-icon-star-fill' do = button_to unfollow_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, class: 'fr-btn fr-btn--secondary fr-icon-star-fill' do
= t('views.instructeurs.dossiers.stop_follow') = t('views.instructeurs.dossiers.stop_follow')
- else - else
%li %li
= button_to follow_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, class: 'fr-btn fr-icon-star-line' do = button_to follow_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, class: 'fr-btn fr-btn--secondary fr-icon-star-line' do
= t('views.instructeurs.dossiers.follow_file') = t('views.instructeurs.dossiers.follow_file')
- if with_menu
%li.en-construction-menu{ 'data-turbo': turbo ? 'true' : 'false' }
= render Instructeurs::EnConstructionMenuComponent.new(dossier:)
- if Dossier.states[:en_construction] == state
%li{ 'data-turbo': turbo ? 'true' : 'false' }
= button_to passer_en_instruction_instructeur_dossier_path(procedure_id, dossier_id), method: :post, class: 'fr-btn fr-icon-edit-line' do
Passer en instruction
- elsif Dossier.states[:en_instruction] == state && !with_menu
%li{ 'data-turbo': turbo ? 'true' : 'false' }
= button_to repasser_en_construction_instructeur_dossier_path(procedure_id, dossier_id), method: :post, class: 'fr-btn fr-btn--secondary fr-icon-draft-line' do
Repasser en construction

View file

@ -170,10 +170,9 @@
= "- #{t('views.instructeurs.dossiers.deleted_by_user')}" if p.hidden_by_user_at.present? = "- #{t('views.instructeurs.dossiers.deleted_by_user')}" if p.hidden_by_user_at.present?
%td.status-col %td.status-col
- if p.hidden_by_administration_at.present? - status = [status_badge(p.state)]
%span.cell-link= status_badge(p.state) - status << pending_correction_badge(:for_instructeur, html_class: "fr-mt-1v") if p.pending_correction?
- else = link_to_if(p.hidden_by_administration_at.blank?, safe_join(status), path, class: class_names("cell-link": true, "fr-py-0": status.many?))
%a.cell-link{ href: path }= status_badge(p.state)
%td.action-col.follow-col %td.action-col.follow-col
%ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right %ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right
@ -184,7 +183,8 @@
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id), dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
close_to_expiration: @statut == 'expirant', close_to_expiration: @statut == 'expirant',
hidden_by_administration: @statut == 'supprimes_recemment', hidden_by_administration: @statut == 'supprimes_recemment',
turbo: false } turbo: false,
with_menu: false }
%tfoot %tfoot
%tr %tr
%td.force-table-100{ colspan: @procedure_presentation.displayed_fields_for_headers.size + 2 } %td.force-table-100{ colspan: @procedure_presentation.displayed_fields_for_headers.size + 2 }

View file

@ -101,7 +101,8 @@
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id), dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
close_to_expiration: nil, close_to_expiration: nil,
hidden_by_administration: nil, hidden_by_administration: nil,
turbo: false } turbo: false,
with_menu: false }
- else - else
%td %td

View file

@ -39,6 +39,12 @@
dossier.procedure.groupe_instructeurs.active.map { |gi| [gi.label, gi.id] }, dossier.procedure.groupe_instructeurs.active.map { |gi| [gi.label, gi.id] },
{ include_blank: dossier.brouillon? } { include_blank: dossier.brouillon? }
= render EditableChamp::SectionComponent.new(champs: dossier_for_editing.champs_public) = render EditableChamp::SectionComponent.new(champs: dossier_for_editing.champs_public)
- if dossier.pending_correction?
.fr-checkbox-group.fr-my-3w
= check_box_tag field_name(:dossier, :pending_correction_confirm), "1", false, form: "form-submit-en-construction"
%label.fr-label{ for: :dossier_pending_correction_confirm }= t('views.shared.dossiers.edit.pending_correction.confirm_label')
= render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false) = render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false)

View file

@ -1,6 +1,6 @@
.messagerie.container .messagerie.container
%ul.messages-list{ data: { controller: 'scroll-to' } } %ul.messages-list{ data: { controller: 'scroll-to' } }
- dossier.commentaires.with_attached_piece_jointe.each do |commentaire| - dossier.preloaded_commentaires.each do |commentaire|
%li.message{ class: commentaire_is_from_me_class(commentaire, connected_user), id: dom_id(commentaire) } %li.message{ class: commentaire_is_from_me_class(commentaire, connected_user), id: dom_id(commentaire) }
= render Dossiers::MessageComponent.new(commentaire: commentaire, connected_user: connected_user, messagerie_seen_at: messagerie_seen_at, show_reply_button: show_reply_button(commentaire, connected_user)) = render Dossiers::MessageComponent.new(commentaire: commentaire, connected_user: connected_user, messagerie_seen_at: messagerie_seen_at, show_reply_button: show_reply_button(commentaire, connected_user))

View file

@ -1,5 +1,5 @@
= render NestedForms::FormOwnerComponent.new = render NestedForms::FormOwnerComponent.new
= form_for(commentaire, url: form_url, html: { class: 'form', multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: @dossier.present? ? dom_id(@dossier) : dom_id(@procedure, :bulk_message) } }) do |f| = form_for(commentaire, url: form_url, html: { multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: @dossier.present? ? dom_id(@dossier) : dom_id(@procedure, :bulk_message) } }) do |f|
- dossier = commentaire.dossier - dossier = commentaire.dossier
- placeholder = t('views.shared.dossiers.messages.form.write_message_to_administration_placeholder') - placeholder = t('views.shared.dossiers.messages.form.write_message_to_administration_placeholder')
- if instructeur_signed_in? || administrateur_signed_in? || expert_signed_in? - if instructeur_signed_in? || administrateur_signed_in? || expert_signed_in?
@ -10,11 +10,11 @@
= t('message', scope: [:utils]) = t('message', scope: [:utils])
%span.mandatory * %span.mandatory *
= f.text_area :body, rows: 5, placeholder: placeholder, title: placeholder, required: true, class: 'fr-input message-textarea' = f.text_area :body, rows: 5, placeholder: placeholder, title: placeholder, required: true, class: 'fr-input message-textarea'
.flex.justify-between.wrap
- disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false - disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false
%div
- if !disable_piece_jointe - if !disable_piece_jointe
.fr-mt-3w
= render Attachment::EditComponent.new(attached_file: commentaire.piece_jointe) = render Attachment::EditComponent.new(attached_file: commentaire.piece_jointe)
.send-wrapper.fr-my-3w .fr-mt-3w
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn send', data: { disable: true } = f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true }

View file

@ -37,7 +37,11 @@
%td %td
%span.cell-link= demandeur_dossier(dossier) %span.cell-link= demandeur_dossier(dossier)
%td.status-col %td.status-col
- if dossier.pending_correction?
= pending_correction_badge(:for_user)
- else
= status_badge(dossier.state) = status_badge(dossier.state)
%td.updated-at-col.cell-link %td.updated-at-col.cell-link
= try_format_date(dossier.updated_at) = try_format_date(dossier.updated_at)
%td.action-col.follow-col %td.action-col.follow-col

View file

@ -3,6 +3,7 @@
%h1 %h1
= dossier.procedure.libelle = dossier.procedure.libelle
= status_badge(dossier.state, 'super') = status_badge(dossier.state, 'super')
= pending_correction_badge(:for_user) if dossier.pending_correction?
%h2 %h2
= t('views.users.dossiers.show.header.dossier_number', dossier_id: dossier.id) = t('views.users.dossiers.show.header.dossier_number', dossier_id: dossier.id)
- if dossier.depose_at.present? - if dossier.depose_at.present?

View file

@ -8,6 +8,9 @@
= t('views.users.dossiers.show.status_overview.status_draft') = t('views.users.dossiers.show.status_overview.status_draft')
%li.en-construction{ class: dossier.en_construction? ? 'active' : nil } %li.en-construction{ class: dossier.en_construction? ? 'active' : nil }
= t('views.users.dossiers.show.status_overview.status_in_progress') = t('views.users.dossiers.show.status_overview.status_in_progress')
- if dossier.pending_correction.present?
= "(#{Dossier.human_attribute_name("pending_correction.for_user")})"
%li.en-instruction{ class: dossier.en_instruction? ? 'active' : nil } %li.en-instruction{ class: dossier.en_instruction? ? 'active' : nil }
= t('views.users.dossiers.show.status_overview.status_review') = t('views.users.dossiers.show.status_overview.status_review')
%li.termine{ class: dossier.termine? ? 'active' : nil } %li.termine{ class: dossier.termine? ? 'active' : nil }
@ -23,6 +26,10 @@
-# brouillon does not occure -# brouillon does not occure
- if dossier.en_construction? - if dossier.en_construction?
.en-construction .en-construction
- if dossier.pending_correction.present?
.message.inverted-background
= render Dossiers::MessageComponent.new(commentaire: dossier.pending_correction.commentaire, connected_user: current_user)
%p{ role: 'status' } %p{ role: 'status' }
= t('views.users.dossiers.show.status_overview.en_construction_html') = t('views.users.dossiers.show.status_overview.en_construction_html')

View file

@ -332,6 +332,8 @@ en:
autosave: Your file is automatically saved after each modification. You can close the window at any time and pick up where you left off later. autosave: Your file is automatically saved after each modification. You can close the window at any time and pick up where you left off later.
notice: "Download the notice of the procedure" notice: "Download the notice of the procedure"
notice_title: "To help you complete your file, you can consult the notice to this procedure." notice_title: "To help you complete your file, you can consult the notice to this procedure."
pending_correction:
confirm_label: I certify that I have made all corrections requested by the administration.
messages: messages:
form: form:
send_message: "Send message" send_message: "Send message"

View file

@ -332,6 +332,8 @@ fr:
autosave: Votre dossier est enregistré automatiquement après chaque modification. Vous pouvez à tout moment fermer la fenêtre et reprendre plus tard là où vous en étiez. autosave: Votre dossier est enregistré automatiquement après chaque modification. Vous pouvez à tout moment fermer la fenêtre et reprendre plus tard là où vous en étiez.
notice: Télécharger le guide de la démarche notice: Télécharger le guide de la démarche
notice_title: "Pour vous aider à remplir votre dossier, vous pouvez consulter le guide de cette démarche." notice_title: "Pour vous aider à remplir votre dossier, vous pouvez consulter le guide de cette démarche."
pending_correction:
confirm_label: Je certifie avoir effectué toutes les corrections demandées par ladministration.
messages: messages:
form: form:
send_message: "Envoyer le message" send_message: "Envoyer le message"
@ -824,11 +826,6 @@ fr:
explication_html: "<p>API Particulier facilite laccès des administrations aux données familiales (CAF), aux données fiscales (DGFiP), au statut pôle-emploi et au statut étudiant dun citoyen pour simplifier les démarches administratives mises en œuvre par les collectivités et les administrations.<br> Cela permet aux administrations daccéder à des informations certifiées à la source et ainsi : </p> <ul> <li>de saffranchir des pièces justificatives lors des démarches en ligne,</li> <li>de réduire le nombre derreurs de saisie,</li> <li>décarter le risque de fraude documentaire.</li> </ul> <p> <strong>Important&nbsp;:</strong> les disposition de larticle <a href='https://www.legifrance.gouv.fr/affichCodeArticle.do?cidTexte=LEGITEXT000031366350&amp;idArticle=LEGIARTI000031367412&amp;dateTexte=&amp;categorieLien=cid'>L144-8</a> nautorisent que léchange des informations strictement nécessaires pour traiter une démarche.<br /><br />En conséquence, ne sélectionnez ici que les données auxquelles vous aurez accès dun point de vue légal.</p>" explication_html: "<p>API Particulier facilite laccès des administrations aux données familiales (CAF), aux données fiscales (DGFiP), au statut pôle-emploi et au statut étudiant dun citoyen pour simplifier les démarches administratives mises en œuvre par les collectivités et les administrations.<br> Cela permet aux administrations daccéder à des informations certifiées à la source et ainsi : </p> <ul> <li>de saffranchir des pièces justificatives lors des démarches en ligne,</li> <li>de réduire le nombre derreurs de saisie,</li> <li>décarter le risque de fraude documentaire.</li> </ul> <p> <strong>Important&nbsp;:</strong> les disposition de larticle <a href='https://www.legifrance.gouv.fr/affichCodeArticle.do?cidTexte=LEGITEXT000031366350&amp;idArticle=LEGIARTI000031367412&amp;dateTexte=&amp;categorieLien=cid'>L144-8</a> nautorisent que léchange des informations strictement nécessaires pour traiter une démarche.<br /><br />En conséquence, ne sélectionnez ici que les données auxquelles vous aurez accès dun point de vue légal.</p>"
update: update:
sources_ok: 'Mise à jour effectuée' sources_ok: 'Mise à jour effectuée'
procedures:
show:
ready: "Validé"
needs_configuration: "À configurer"
configure_api_particulier_token: "Configurer le jeton API particulier"
zones: zones:
ministeres: Ministères ministeres: Ministères
france_connect: france_connect:

View file

@ -15,6 +15,10 @@ en:
accepte: "Accepted" accepte: "Accepted"
refuse: "Refused" refuse: "Refused"
sans_suite: "No further action" sans_suite: "No further action"
pending_correction:
for_instructeur: "pending"
for_user: "to be corrected"
resolved: corrected
traitement: traitement:
state: "State" state: "State"
traitement/state: traitement/state:

View file

@ -19,6 +19,10 @@ fr:
accepte: "Accepté" accepte: "Accepté"
refuse: "Refusé" refuse: "Refusé"
sans_suite: "Classé sans suite" sans_suite: "Classé sans suite"
pending_correction:
for_instructeur: "en attente"
for_user:  corriger"
resolved: corrigé
traitement: traitement:
state: "État" state: "État"
traitement/state: traitement/state:

View file

@ -0,0 +1,12 @@
---
en:
dossier_mailer:
notify_pending_correction:
subject: You need to modify your file no. %{dossier_id} « %{libelle_demarche} »
explanation_html:
In order to continue its instruction, <strong>an instructor requested you to edit information</strong> to your file no. %{dossier_id} of the « %{libelle_demarche} » procedure.
link:
Check your file's mailbox to see what changes need to be made, then edit the file directly on the website.
access_message: Open the mailbox

View file

@ -0,0 +1,13 @@
---
fr:
dossier_mailer:
notify_pending_correction:
subject: Vous devez corriger votre dossier nº %{dossier_id} « %{libelle_demarche} »
explanation_html:
Afin de poursuivre son instruction, <strong>un instructeur vous demande dapporter des corrections</strong> à votre dossier nº %{dossier_id} de la démarche « %{libelle_demarche} ».
link:
Consultez la messagerie de votre dossier pour prendre connaissance des modifications à effectuer,
puis modifiez le dossier directement sur le site.
access_message: Ouvrir la messagerie

View file

@ -19,6 +19,7 @@ fr:
classe_sans_suite: Le %{processed_at}, %{email} a classé ce dossier sans suite classe_sans_suite: Le %{processed_at}, %{email} a classé ce dossier sans suite
filterable_state: filterable_state:
en_construction: "En construction" en_construction: "En construction"
pending_correction: "En attente"
en_instruction: "En instruction" en_instruction: "En instruction"
accepte: "Accepté" accepte: "Accepté"
refuse: "Refusé" refuse: "Refusé"

View file

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

View file

@ -0,0 +1,15 @@
class CreateDossierCorrections < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
create_table :dossier_corrections do |t|
t.references :dossier, null: false, foreign_key: true
t.references :commentaire, foreign_key: true
t.datetime :resolved_at, precision: 6
t.timestamps
end
add_index :dossier_corrections, :resolved_at, where: "(resolved_at IS NULL OR resolved_at IS NOT NULL)", algorithm: :concurrently
end
end

View file

@ -1,5 +1,5 @@
class DropTableDropDownLists < ActiveRecord::Migration[6.1] class DropTableDropDownLists < ActiveRecord::Migration[6.1]
def up def up
drop_table :drop_down_lists drop_table :drop_down_lists if table_exists?(:drop_down_lists)
end end
end end

View file

@ -318,6 +318,17 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_08_160551) do
t.index ["dossier_id"], name: "index_dossier_batch_operations_on_dossier_id" t.index ["dossier_id"], name: "index_dossier_batch_operations_on_dossier_id"
end end
create_table "dossier_corrections", force: :cascade do |t|
t.bigint "commentaire_id"
t.datetime "created_at", precision: 6, null: false
t.bigint "dossier_id", null: false
t.datetime "resolved_at", precision: 6
t.datetime "updated_at", precision: 6, null: false
t.index ["commentaire_id"], name: "index_dossier_corrections_on_commentaire_id"
t.index ["dossier_id"], name: "index_dossier_corrections_on_dossier_id"
t.index ["resolved_at"], name: "index_dossier_corrections_on_resolved_at", where: "((resolved_at IS NULL) OR (resolved_at IS NOT NULL))"
end
create_table "dossier_operation_logs", force: :cascade do |t| create_table "dossier_operation_logs", force: :cascade do |t|
t.boolean "automatic_operation", default: false, null: false t.boolean "automatic_operation", default: false, null: false
t.bigint "bill_signature_id" t.bigint "bill_signature_id"
@ -1010,6 +1021,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_08_160551) do
add_foreign_key "commentaires", "instructeurs" add_foreign_key "commentaires", "instructeurs"
add_foreign_key "dossier_batch_operations", "batch_operations" add_foreign_key "dossier_batch_operations", "batch_operations"
add_foreign_key "dossier_batch_operations", "dossiers" add_foreign_key "dossier_batch_operations", "dossiers"
add_foreign_key "dossier_corrections", "commentaires"
add_foreign_key "dossier_corrections", "dossiers"
add_foreign_key "dossier_operation_logs", "bill_signatures" add_foreign_key "dossier_operation_logs", "bill_signatures"
add_foreign_key "dossier_transfer_logs", "dossiers" add_foreign_key "dossier_transfer_logs", "dossiers"
add_foreign_key "dossiers", "batch_operations" add_foreign_key "dossiers", "batch_operations"

View file

@ -144,4 +144,32 @@ RSpec.describe Dossiers::MessageComponent, type: :component do
end end
end end
end end
describe '#correction_badge' do
let(:resolved_at) { nil }
before do
create(:dossier_correction, commentaire:, dossier:, resolved_at:)
end
it 'returns a badge à corriger' do
expect(subject).to have_text( corriger')
end
context 'connected as instructeur' do
let(:connected_user) { create(:instructeur) }
it 'returns a badge en attente' do
expect(subject).to have_text('en attente')
end
end
context 'when the correction is resolved' do
let(:resolved_at) { 1.minute.ago }
it 'returns a badge corrigé' do
expect(subject).to have_text("corrigé")
end
end
end
end end

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
RSpec.describe Instructeurs::EnConstructionMenuComponent, type: :component do
include DossierHelper
subject do
render_inline(described_class.new(dossier:))
end
matcher :have_dropdown_title do |expected_title|
match do |subject|
expect(subject).to have_selector('.dropdown .dropdown-button', text: expected_title)
end
end
matcher :have_dropdown_items do |options|
match do |subject|
expected_count = options[:count] || 1
expect(subject).to have_selector('ul.dropdown-items li:not(.hidden)', count: expected_count)
end
end
matcher :have_dropdown_item do |expected_title, options = {}|
match do |subject|
expected_href = options[:href]
if (expected_href.present?)
expect(subject).to have_selector("ul.dropdown-items li a[href='#{expected_href}']", text: expected_title)
else
expect(subject).to have_selector('ul.dropdown-items li', text: expected_title)
end
end
end
context 'en_construction' do
let(:dossier) { create(:dossier, :en_construction) }
it 'renders a dropdown' do
expect(subject).to have_dropdown_title('Demander une correction')
expect(subject).to have_dropdown_items(count: 2) # form is already expanded so we have 2 visible items
end
end
context 'en_instruction' do
let(:dossier) { create(:dossier, :en_instruction) }
it 'renders a dropdown' do
expect(subject).to have_dropdown_title('Repasser en construction')
expect(subject).to have_dropdown_item('Demander une correction')
expect(subject).to have_dropdown_item('Repasser en construction')
expect(subject).to have_dropdown_items(count: 3)
end
end
end

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
RSpec.describe Instructeurs::InstructionMenuComponent, type: :component do
include DossierHelper
subject do
render_inline(described_class.new(dossier:))
end
matcher :have_dropdown_title do |expected_title|
match do |subject|
expect(subject).to have_selector('.dropdown .dropdown-button', text: expected_title)
end
end
matcher :have_dropdown_items do |options|
match do |subject|
expected_count = options[:count] || 1
expect(subject).to have_selector('ul.dropdown-items li:not(.hidden)', count: expected_count)
end
end
matcher :have_dropdown_item do |expected_title, options = {}|
match do |subject|
expected_href = options[:href]
if (expected_href.present?)
expect(subject).to have_selector("ul.dropdown-items li a[href='#{expected_href}']", text: expected_title)
else
expect(subject).to have_selector('ul.dropdown-items li', text: expected_title)
end
end
end
context 'en_construction' do
let(:dossier) { create(:dossier, :en_construction) }
it 'does not render' do
expect(subject.to_s).to be_empty
end
end
context 'en_instruction' do
let(:dossier) { create(:dossier, :en_instruction) }
it 'renders a dropdown' do
expect(subject).to have_dropdown_title('Instruire le dossier')
expect(subject).to have_dropdown_items(count: 3)
expect(subject).to have_dropdown_item('Accepter')
expect(subject).to have_dropdown_item('Classer sans suite')
expect(subject).to have_dropdown_item('Refuser')
end
end
end

View file

@ -496,6 +496,112 @@ describe Instructeurs::DossiersController, type: :controller do
end end
end end
describe '#pending_correction' do
let(:message) { 'do that' }
let(:justificatif) { nil }
subject do
post :pending_correction, params: {
procedure_id: procedure.id, dossier_id: dossier.id,
dossier: { motivation: message, justificatif_motivation: justificatif }
}, format: :turbo_stream
end
before do
sign_in(instructeur.user)
allow(DossierMailer).to receive(:notify_pending_correction)
.and_return(double(deliver_later: nil))
expect(controller.current_instructeur).to receive(:mark_tab_as_seen).with(dossier, :messagerie)
end
context "dossier en instruction" do
let(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure: procedure) }
before { subject }
it 'sends an email to user' do
expect(DossierMailer).to have_received(:notify_pending_correction).once
expect(DossierMailer).to have_received(:notify_pending_correction).with(dossier)
end
it 'pass en_construction and create a pending correction' do
expect(response).to have_http_status(:ok)
expect(response.body).to include('en attente de correction')
expect(dossier.reload).to be_en_construction
expect(dossier).to be_pending_correction
end
it 'create a comment with text body' do
expect(dossier.commentaires.last.body).to eq("do that")
expect(dossier.commentaires.last).to be_flagged_pending_correction
end
context 'with an attachment' do
let(:justificatif) { fake_justificatif }
it 'attach file to comment' do
expect(dossier.commentaires.last.piece_jointe).to be_attached
end
end
context 'with an invalid comment / attachment' do
let(:justificatif) { Rack::Test::UploadedFile.new(Rails.root.join('Gemfile.lock'), 'text/lock') }
it 'does not save anything' do
expect(dossier.reload).not_to be_pending_correction
expect(dossier.commentaires.count).to eq(0)
expect(response.body).to include('pas dun type accepté')
end
end
context 'with an empty message' do
let(:message) { '' }
it 'requires a message' do
expect(dossier.reload).not_to be_pending_correction
expect(dossier.commentaires.count).to eq(0)
expect(response.body).to include('Vous devez préciser')
end
end
context 'dossier already having pending corrections' do
before do
create(:dossier_correction, dossier:)
end
it 'does not create an new pending correction' do
expect { subject }.not_to change { DossierCorrection.count }
end
it 'shows a flash alert' do
subject
expect(response.body).to include('')
end
end
end
context 'dossier en_construction' do
it 'can create a pending correction' do
subject
expect(dossier.reload).to be_pending_correction
expect(dossier.commentaires.last).to be_flagged_pending_correction
end
end
context 'dossier is termine' do
let(:dossier) { create(:dossier, :accepte, :with_individual, procedure: procedure) }
it 'does not create a pending correction' do
expect { subject }.not_to change { DossierCorrection.count }
expect(response.body).to include('Impossible')
end
end
end
describe '#messagerie' do describe '#messagerie' do
before { expect(controller.current_instructeur).to receive(:mark_tab_as_seen).with(dossier, :messagerie) } before { expect(controller.current_instructeur).to receive(:mark_tab_as_seen).with(dossier, :messagerie) }
subject { get :messagerie, params: { procedure_id: procedure.id, dossier_id: dossier.id } } subject { get :messagerie, params: { procedure_id: procedure.id, dossier_id: dossier.id } }

View file

@ -513,6 +513,16 @@ describe Users::DossiersController, type: :controller do
expect(flash.alert).to eq("Les modifications ont déjà été déposées") expect(flash.alert).to eq("Les modifications ont déjà été déposées")
end end
end end
context "when there are pending correction" do
let!(:correction) { create(:dossier_correction, dossier: dossier) }
subject { post :submit_en_construction, params: { id: dossier.id, dossier: { pending_correction_confirm: "1" } } }
it "resolve correction" do
expect { subject }.to change { correction.reload.resolved_at }.to be_truthy
end
end
end end
describe '#update brouillon' do describe '#update brouillon' do

View file

@ -0,0 +1,11 @@
FactoryBot.define do
factory :dossier_correction do
dossier
commentaire
resolved_at { nil }
trait :resolved do
resolved_at { Time.zone.now }
end
end
end

View file

@ -8,6 +8,10 @@ class DossierMailerPreview < ActionMailer::Preview
DossierMailer.with(commentaire: commentaire(on: draft)).notify_new_answer DossierMailer.with(commentaire: commentaire(on: draft)).notify_new_answer
end end
def notify_pending_correction
DossierMailer.with(dossier: dossier_en_construction).notify_pending_correction
end
def notify_revert_to_instruction def notify_revert_to_instruction
DossierMailer.notify_revert_to_instruction(dossier) DossierMailer.notify_revert_to_instruction(dossier)
end end

View file

@ -0,0 +1,103 @@
describe DossierCorrectableConcern do
describe "#pending_correction?" do
let(:dossier) { create(:dossier, :en_construction) }
context "when dossier has no correction" do
it { expect(dossier.pending_correction?).to be_falsey }
end
context "when dossier has a pending correction" do
before { create(:dossier_correction, dossier:) }
it { expect(dossier.pending_correction?).to be_truthy }
end
context "when dossier has a resolved correction" do
before { create(:dossier_correction, :resolved, dossier:) }
it { expect(dossier.pending_correction?).to be_falsey }
end
context "when dossier is not en_construction" do
let(:dossier) { create(:dossier, :en_instruction) }
before { create(:dossier_correction, dossier:) }
it { expect(dossier.pending_correction?).to be_falsey }
end
end
describe '#flag_as_pending_correction!' do
let(:dossier) { create(:dossier, :en_construction) }
let(:instructeur) { create(:instructeur) }
let(:commentaire) { create(:commentaire, dossier:, instructeur:) }
context 'when dossier is en_construction' do
it 'creates a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.corrections.pending.count }.by(1)
end
it 'does not change dossier state' do
expect { dossier.flag_as_pending_correction!(commentaire) }.not_to change { dossier.state }
end
end
context 'when dossier is not en_instruction' do
let(:dossier) { create(:dossier, :en_instruction) }
it 'creates a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.corrections.pending.count }.by(1)
end
it 'repasse dossier en_construction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.state }.to('en_construction')
end
end
context 'when dossier has already a pending correction' do
before { create(:dossier_correction, dossier:) }
it 'does not create a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.not_to change { dossier.corrections.pending.count }
end
end
context 'when dossier has already a resolved correction' do
before { create(:dossier_correction, :resolved, dossier:) }
it 'creates a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.to change { dossier.corrections.pending.count }.by(1)
end
end
context 'when dossier is not en_construction and may not be repassed en_construction' do
let(:dossier) { create(:dossier, :accepte) }
it 'does not create a correction' do
expect { dossier.flag_as_pending_correction!(commentaire) }.not_to change { dossier.corrections.pending.count }
end
end
end
describe "#resolve_pending_correction!" do
let(:dossier) { create(:dossier, :en_construction) }
subject(:resolve) { dossier.resolve_pending_correction! }
context "when dossier has no correction" do
it { expect { resolve }.not_to change { dossier.corrections.pending.count } }
end
context "when dossier has a pending correction" do
let!(:correction) { create(:dossier_correction, dossier:) }
it {
expect { resolve }.to change { correction.reload.resolved_at }.from(nil)
}
end
context "when dossier has a already resolved correction" do
before { create(:dossier_correction, :resolved, dossier:) }
it { expect { resolve }.not_to change { dossier.corrections.pending.count } }
end
end
end

View file

@ -1044,6 +1044,7 @@ describe Dossier do
let(:last_operation) { dossier.dossier_operation_logs.last } let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { last_operation.data } let(:operation_serialized) { last_operation.data }
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
let!(:correction) { create(:dossier_correction, dossier:) }
before { dossier.passer_en_instruction!(instructeur: instructeur) } before { dossier.passer_en_instruction!(instructeur: instructeur) }
@ -1055,6 +1056,11 @@ describe Dossier do
it { expect(operation_serialized['operation']).to eq('passer_en_instruction') } it { expect(operation_serialized['operation']).to eq('passer_en_instruction') }
it { expect(operation_serialized['dossier_id']).to eq(dossier.id) } it { expect(operation_serialized['dossier_id']).to eq(dossier.id) }
it { expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601) } it { expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601) }
it "resolve pending correction" do
expect(dossier.pending_correction?).to be_falsey
expect(correction.reload.resolved_at).to be_present
end
end end
describe '#passer_automatiquement_en_instruction!' do describe '#passer_automatiquement_en_instruction!' do

View file

@ -248,6 +248,29 @@ describe DossierProjectionService do
it { is_expected.to eq("") } it { is_expected.to eq("") }
end end
end end
context 'for dossier corrections table' do
let(:table) { 'dossier_corrections' }
let(:column) { 'resolved_at' }
let(:dossier) { create(:dossier, :en_construction) }
subject { described_class.project(dossiers_ids, fields)[0] }
context "when dossier has pending correction" do
before { create(:dossier_correction, dossier:) }
it { expect(subject.pending_correction?).to be(true) }
end
context "when dossier has a resolved correction" do
before { create(:dossier_correction, :resolved, dossier:) }
it { expect(subject.pending_correction?).to eq(false) }
end
context "when dossier has no correction at all" do
it { expect(subject.pending_correction?).to eq(false) }
end
end
end end
end end
end end

View file

@ -1,43 +0,0 @@
describe 'instructeurs/dossiers/instruction_button', type: :view do
include DossierHelper
subject! do
render('instructeurs/dossiers/instruction_button', dossier: dossier)
end
matcher :have_dropdown_title do |expected_title|
match do |rendered|
expect(rendered).to have_selector('.dropdown .dropdown-button', text: expected_title)
end
end
matcher :have_dropdown_items do |options|
match do |rendered|
expected_count = options[:count] || 1
expect(rendered).to have_selector('ul.dropdown-items li:not(.hidden)', count: expected_count)
end
end
matcher :have_dropdown_item do |expected_title, options = {}|
match do |rendered|
expected_href = options[:href]
if (expected_href.present?)
expect(rendered).to have_selector("ul.dropdown-items li a[href='#{expected_href}']", text: expected_title)
else
expect(rendered).to have_selector('ul.dropdown-items li', text: expected_title)
end
end
end
context 'en_instruction' do
let(:dossier) { create(:dossier, :en_instruction) }
it 'renders a dropdown' do
expect(rendered).to have_dropdown_title('Instruire le dossier')
expect(rendered).to have_dropdown_items(count: 3)
expect(rendered).to have_dropdown_item('Accepter')
expect(rendered).to have_dropdown_item('Classer sans suite')
expect(rendered).to have_dropdown_item('Refuser')
end
end
end

View file

@ -52,7 +52,7 @@ describe 'instructeurs/dossiers/show', type: :view do
end end
end end
context 'en_contruction' do context 'en_construction' do
let(:dossier) { create(:dossier, :en_construction) } let(:dossier) { create(:dossier, :en_construction) }
it 'displays the correct actions' do it 'displays the correct actions' do
within("form[action=\"#{passer_en_instruction_instructeur_dossier_path(dossier.procedure, dossier)}\"]") do within("form[action=\"#{passer_en_instruction_instructeur_dossier_path(dossier.procedure, dossier)}\"]") do
@ -61,7 +61,8 @@ describe 'instructeurs/dossiers/show', type: :view do
within("form[action=\"#{follow_instructeur_dossier_path(dossier.procedure, dossier)}\"]") do within("form[action=\"#{follow_instructeur_dossier_path(dossier.procedure, dossier)}\"]") do
expect(subject).to have_button('Suivre le dossier') expect(subject).to have_button('Suivre le dossier')
end end
expect(subject).to have_selector('.header-actions ul:first-child .fr-btn', count: 2) expect(subject).to have_button('Demander une correction')
expect(subject).to have_selector('.header-actions ul:first-child > li.instruction-button', count: 1)
end end
end end
@ -74,15 +75,15 @@ describe 'instructeurs/dossiers/show', type: :view do
end end
it 'displays the correct actions' do it 'displays the correct actions' do
within("form[action=\"#{repasser_en_construction_instructeur_dossier_path(dossier.procedure, dossier)}\"]") do
expect(subject).to have_button('Repasser en construction')
end
within("form[action=\"#{unfollow_instructeur_dossier_path(dossier.procedure, dossier)}\"]") do within("form[action=\"#{unfollow_instructeur_dossier_path(dossier.procedure, dossier)}\"]") do
expect(subject).to have_button('Ne plus suivre') expect(subject).to have_button('Ne plus suivre')
end end
expect(subject).to have_button('Repasser en construction')
expect(subject).to have_selector('.en-construction-menu .fr-btn', count: 5)
expect(subject).to have_button('Instruire le dossier') expect(subject).to have_button('Instruire le dossier')
expect(subject).to have_selector('.header-actions ul:first-child > li .fr-btn', count: 15) expect(subject).to have_selector('.instruction-button .fr-btn', count: 13)
expect(subject).to have_selector('.header-actions ul:first-child > li.instruction-button', count: 1)
end end
end end