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:
commit
8cd5f31488
62 changed files with 899 additions and 128 deletions
|
@ -45,7 +45,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.number-col {
|
||||
.number-col,
|
||||
.fr-badge {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
color: $dark-red;
|
||||
}
|
||||
|
||||
label,
|
||||
label:not(.fr-label),
|
||||
legend.form-label {
|
||||
font-size: 18px;
|
||||
margin-bottom: $default-padding;
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
@import "constants";
|
||||
|
||||
.motivation {
|
||||
padding: $default-padding;
|
||||
color: $black;
|
||||
width: 450px;
|
||||
|
||||
|
|
|
@ -27,7 +27,8 @@ class Dossiers::EditFooterComponent < ApplicationComponent
|
|||
{
|
||||
class: 'fr-btn fr-btn--sm',
|
||||
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
|
||||
|
||||
|
|
|
@ -8,6 +8,13 @@ class Dossiers::MessageComponent < ApplicationComponent
|
|||
|
||||
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
|
||||
|
||||
def show_reply_button?
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
= commentaire_issuer
|
||||
- if commentaire_from_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 }
|
||||
= commentaire_date
|
||||
.rich-text
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
en:
|
||||
revert_en_construction: Revert to in progress
|
||||
request_correction: Request a correction
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
fr:
|
||||
revert_en_construction: Repasser en construction
|
||||
request_correction: Demander une correction
|
|
@ -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')
|
||||
L’usager sera notifié qu’il 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')
|
||||
L’usager 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 ?'}
|
17
app/components/instructeurs/instruction_menu_component.rb
Normal file
17
app/components/instructeurs/instruction_menu_component.rb
Normal 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
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
en:
|
||||
instruct: Instruct the file
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
fr:
|
||||
instruct: Instruire le dossier
|
|
@ -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
|
||||
L’usager 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
|
||||
L’usager 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
|
||||
L’usager 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 ?' }
|
||||
|
|
@ -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]
|
||||
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_annotations_privees_as_read, only: [:annotations_privees, :update_annotations]
|
||||
|
||||
|
@ -223,6 +223,39 @@ module Instructeurs
|
|||
render :change_state
|
||||
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
|
||||
@commentaire = CommentaireService.create(current_instructeur, dossier, commentaire_params)
|
||||
|
||||
|
|
|
@ -227,6 +227,10 @@ module Users
|
|||
editing_fork_origin.merge_fork(@dossier)
|
||||
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)
|
||||
else
|
||||
flash.now.alert = errors
|
||||
|
|
|
@ -79,7 +79,7 @@ module DossierHelper
|
|||
|
||||
def status_badge(state, alignment_class = '')
|
||||
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
|
||||
|
||||
def deletion_reason_badge(reason)
|
||||
|
@ -94,6 +94,14 @@ module DossierHelper
|
|||
tag.span(status_text, class: "label #{status_class} ")
|
||||
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)
|
||||
if dossier.procedure.for_individual?
|
||||
"#{dossier&.individual&.nom} #{dossier&.individual&.prenom}"
|
||||
|
|
|
@ -46,6 +46,19 @@ class DossierMailer < ApplicationMailer
|
|||
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)
|
||||
I18n.with_locale(avis.dossier.user_locale) do
|
||||
@avis = avis
|
||||
|
|
|
@ -50,6 +50,10 @@ class NotificationMailer < ApplicationMailer
|
|||
with(dossier: dossier, state: Dossier.states.fetch(:sans_suite)).send_notification
|
||||
end
|
||||
|
||||
def self.send_pending_correction(dossier)
|
||||
with(dossier: dossier).send_notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_services_publics_plus
|
||||
|
|
|
@ -19,6 +19,7 @@ class Commentaire < ApplicationRecord
|
|||
|
||||
belongs_to :instructeur, 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? }
|
||||
|
||||
|
@ -94,6 +95,10 @@ class Commentaire < ApplicationRecord
|
|||
update! body: ''
|
||||
end
|
||||
|
||||
def flagged_pending_correction?
|
||||
DossierCorrection.exists?(commentaire: self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify
|
||||
|
@ -108,8 +113,12 @@ class Commentaire < ApplicationRecord
|
|||
end
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
def messagerie_available?
|
||||
return if sent_by_system?
|
||||
|
|
38
app/models/concerns/dossier_correctable_concern.rb
Normal file
38
app/models/concerns/dossier_correctable_concern.rb
Normal 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
|
|
@ -47,12 +47,13 @@
|
|||
# user_id :integer
|
||||
#
|
||||
class Dossier < ApplicationRecord
|
||||
include DossierCloneConcern
|
||||
include DossierCorrectableConcern
|
||||
include DossierFilteringConcern
|
||||
include DossierPrefillableConcern
|
||||
include DossierRebaseConcern
|
||||
include DossierSearchableConcern
|
||||
include DossierSectionsConcern
|
||||
include DossierCloneConcern
|
||||
|
||||
enum state: {
|
||||
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 :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 :follows, -> { active }, inverse_of: :dossier
|
||||
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
|
||||
|
@ -890,6 +893,8 @@ class Dossier < ApplicationRecord
|
|||
.processed_at
|
||||
save!
|
||||
|
||||
resolve_pending_correction!
|
||||
|
||||
if !disable_notification
|
||||
NotificationMailer.send_en_instruction_notification(self).deliver_later
|
||||
end
|
||||
|
|
23
app/models/dossier_correction.rb
Normal file
23
app/models/dossier_correction.rb
Normal 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
|
|
@ -183,6 +183,8 @@ class ProcedurePresentation < ApplicationRecord
|
|||
.filter_map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
|
||||
|
||||
dossiers.filter_by_datetimes(column, dates)
|
||||
elsif field['column'] == "state" && values.include?("pending_correction")
|
||||
dossiers.joins(:corrections).where(corrections: DossierCorrection.pending)
|
||||
else
|
||||
dossiers.where("dossiers.#{column} IN (?)", values)
|
||||
end
|
||||
|
@ -245,7 +247,11 @@ class ProcedurePresentation < ApplicationRecord
|
|||
if [TYPE_DE_CHAMP, TYPE_DE_CHAMP_PRIVATE].include?(filter[TABLE])
|
||||
find_type_de_champ(filter[COLUMN]).dynamic_type.filter_to_human(filter['value'])
|
||||
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']}")
|
||||
end
|
||||
elsif filter['table'] == 'groupe_instructeur' && filter['column'] == 'id'
|
||||
instructeur.groupe_instructeurs
|
||||
.find { _1.id == filter['value'].to_i }&.label || filter['value']
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
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
|
||||
|
||||
TABLE = 'table'
|
||||
|
@ -23,7 +29,8 @@ class DossierProjectionService
|
|||
batch_operation_field = { TABLE => 'self', COLUMN => 'batch_operation_id' }
|
||||
hidden_by_user_at_field = { TABLE => 'self', COLUMN => 'hidden_by_user_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] = {} }
|
||||
.group_by { |f| f[TABLE] } # one query per table
|
||||
.each do |table, fields|
|
||||
|
@ -76,6 +83,18 @@ class DossierProjectionService
|
|||
.where(id: dossiers_ids)
|
||||
.pluck('dossiers.id, groupe_instructeurs.label')
|
||||
.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'
|
||||
Dossier
|
||||
.joins(:procedure)
|
||||
|
@ -111,6 +130,7 @@ class DossierProjectionService
|
|||
hidden_by_user_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],
|
||||
dossier_corrections[:id_value_h][dossier_id],
|
||||
fields.map { |f| f[:id_value_h][dossier_id] }
|
||||
)
|
||||
end
|
||||
|
|
13
app/views/dossier_mailer/notify_pending_correction.html.haml
Normal file
13
app/views/dossier_mailer/notify_pending_correction.html.haml
Normal 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
|
|
@ -2,12 +2,14 @@
|
|||
= render partial: "instructeurs/procedures/dossier_actions",
|
||||
locals: { procedure_id: dossier.procedure.id,
|
||||
dossier_id: dossier.id,
|
||||
dossier: dossier,
|
||||
state: dossier.state,
|
||||
archived: dossier.archived,
|
||||
dossier_is_followed: current_instructeur&.follow?(dossier),
|
||||
close_to_expiration: dossier.close_to_expiration?,
|
||||
hidden_by_administration: dossier.hidden_by_administration?,
|
||||
turbo: true }
|
||||
turbo: true,
|
||||
with_menu: true }
|
||||
|
||||
%li.instruction-button
|
||||
= render partial: "instruction_button", locals: { dossier: dossier }
|
||||
= render Instructeurs::InstructionMenuComponent.new(dossier:)
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
= "Dossier nº #{dossier.id}"
|
||||
|
||||
= 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"
|
||||
= procedure_badge(dossier.procedure)
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
- if dossier.en_instruction?
|
||||
= 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|
|
||||
- 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: :region) do |menu|
|
||||
- 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
|
||||
= link_to('#', onclick: "DS.showMotivation(event, 'accept');", role: 'menuitem') do
|
||||
%span.icon.accept
|
||||
|
@ -33,3 +34,23 @@
|
|||
|
||||
- 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 ?' }
|
||||
|
||||
- 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
|
||||
L’usager 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 ?'}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.motivation.hidden{ class: popup_class }
|
||||
= form_tag(terminer_instructeur_dossier_path(dossier.procedure, dossier), data: { turbo: true, turbo_confirm: confirm }, method: :post, multipart: true) do
|
||||
.motivation{ class: class_names(popup_class => true, hidden: !defined?(visible) || !visible, "fr-pb-2w fr-px-2w": true) }
|
||||
= 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'
|
||||
= text_area :dossier, :motivation, class: 'fr-input', placeholder: placeholder, required: false
|
||||
- if dossier.attestation_template&.activated?
|
||||
|
@ -28,11 +28,11 @@
|
|||
- else
|
||||
= text_area :dossier, :motivation, class: 'fr-input', placeholder: placeholder, required: true
|
||||
.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}" }
|
||||
= 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}" }
|
||||
%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
|
||||
= 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
|
||||
|
|
|
@ -29,21 +29,24 @@
|
|||
= ""
|
||||
|
||||
- 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
|
||||
%li
|
||||
= 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')
|
||||
- else
|
||||
%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')
|
||||
|
||||
- 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
|
||||
|
|
|
@ -170,10 +170,9 @@
|
|||
= "- #{t('views.instructeurs.dossiers.deleted_by_user')}" if p.hidden_by_user_at.present?
|
||||
|
||||
%td.status-col
|
||||
- if p.hidden_by_administration_at.present?
|
||||
%span.cell-link= status_badge(p.state)
|
||||
- else
|
||||
%a.cell-link{ href: path }= status_badge(p.state)
|
||||
- status = [status_badge(p.state)]
|
||||
- status << pending_correction_badge(:for_instructeur, html_class: "fr-mt-1v") if p.pending_correction?
|
||||
= link_to_if(p.hidden_by_administration_at.blank?, safe_join(status), path, class: class_names("cell-link": true, "fr-py-0": status.many?))
|
||||
|
||||
%td.action-col.follow-col
|
||||
%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),
|
||||
close_to_expiration: @statut == 'expirant',
|
||||
hidden_by_administration: @statut == 'supprimes_recemment',
|
||||
turbo: false }
|
||||
turbo: false,
|
||||
with_menu: false }
|
||||
%tfoot
|
||||
%tr
|
||||
%td.force-table-100{ colspan: @procedure_presentation.displayed_fields_for_headers.size + 2 }
|
||||
|
|
|
@ -101,7 +101,8 @@
|
|||
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
|
||||
close_to_expiration: nil,
|
||||
hidden_by_administration: nil,
|
||||
turbo: false }
|
||||
turbo: false,
|
||||
with_menu: false }
|
||||
|
||||
- else
|
||||
%td
|
||||
|
|
|
@ -39,6 +39,12 @@
|
|||
dossier.procedure.groupe_instructeurs.active.map { |gi| [gi.label, gi.id] },
|
||||
{ include_blank: dossier.brouillon? }
|
||||
|
||||
|
||||
= 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)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.messagerie.container
|
||||
%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) }
|
||||
= 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))
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
= 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
|
||||
- placeholder = t('views.shared.dossiers.messages.form.write_message_to_administration_placeholder')
|
||||
- if instructeur_signed_in? || administrateur_signed_in? || expert_signed_in?
|
||||
|
@ -10,11 +10,11 @@
|
|||
= t('message', scope: [:utils])
|
||||
%span.mandatory *
|
||||
= 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
|
||||
%div
|
||||
- if !disable_piece_jointe
|
||||
.fr-mt-3w
|
||||
= render Attachment::EditComponent.new(attached_file: commentaire.piece_jointe)
|
||||
|
||||
.send-wrapper.fr-my-3w
|
||||
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn send', data: { disable: true }
|
||||
.fr-mt-3w
|
||||
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true }
|
||||
|
|
|
@ -37,7 +37,11 @@
|
|||
%td
|
||||
%span.cell-link= demandeur_dossier(dossier)
|
||||
%td.status-col
|
||||
- if dossier.pending_correction?
|
||||
= pending_correction_badge(:for_user)
|
||||
- else
|
||||
= status_badge(dossier.state)
|
||||
|
||||
%td.updated-at-col.cell-link
|
||||
= try_format_date(dossier.updated_at)
|
||||
%td.action-col.follow-col
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
%h1
|
||||
= dossier.procedure.libelle
|
||||
= status_badge(dossier.state, 'super')
|
||||
= pending_correction_badge(:for_user) if dossier.pending_correction?
|
||||
%h2
|
||||
= t('views.users.dossiers.show.header.dossier_number', dossier_id: dossier.id)
|
||||
- if dossier.depose_at.present?
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
= t('views.users.dossiers.show.status_overview.status_draft')
|
||||
%li.en-construction{ class: dossier.en_construction? ? 'active' : nil }
|
||||
= 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 }
|
||||
= t('views.users.dossiers.show.status_overview.status_review')
|
||||
%li.termine{ class: dossier.termine? ? 'active' : nil }
|
||||
|
@ -23,6 +26,10 @@
|
|||
-# brouillon does not occure
|
||||
- if dossier.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' }
|
||||
= t('views.users.dossiers.show.status_overview.en_construction_html')
|
||||
|
||||
|
|
|
@ -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.
|
||||
notice: "Download the notice of the 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:
|
||||
form:
|
||||
send_message: "Send message"
|
||||
|
|
|
@ -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.
|
||||
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."
|
||||
pending_correction:
|
||||
confirm_label: Je certifie avoir effectué toutes les corrections demandées par l’administration.
|
||||
messages:
|
||||
form:
|
||||
send_message: "Envoyer le message"
|
||||
|
@ -824,11 +826,6 @@ fr:
|
|||
explication_html: "<p>API Particulier facilite l’accès des administrations aux données familiales (CAF), aux données fiscales (DGFiP), au statut pôle-emploi et au statut étudiant d’un citoyen pour simplifier les démarches administratives mises en œuvre par les collectivités et les administrations.<br> Cela permet aux administrations d’accéder à des informations certifiées à la source et ainsi : </p> <ul> <li>de s’affranchir des pièces justificatives lors des démarches en ligne,</li> <li>de réduire le nombre d’erreurs de saisie,</li> <li>d’écarter le risque de fraude documentaire.</li> </ul> <p> <strong>Important :</strong> les disposition de l’article <a href='https://www.legifrance.gouv.fr/affichCodeArticle.do?cidTexte=LEGITEXT000031366350&idArticle=LEGIARTI000031367412&dateTexte=&categorieLien=cid'>L144-8</a> n’autorisent 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 d’un point de vue légal.</p>"
|
||||
update:
|
||||
sources_ok: 'Mise à jour effectuée'
|
||||
procedures:
|
||||
show:
|
||||
ready: "Validé"
|
||||
needs_configuration: "À configurer"
|
||||
configure_api_particulier_token: "Configurer le jeton API particulier"
|
||||
zones:
|
||||
ministeres: Ministères
|
||||
france_connect:
|
||||
|
|
|
@ -15,6 +15,10 @@ en:
|
|||
accepte: "Accepted"
|
||||
refuse: "Refused"
|
||||
sans_suite: "No further action"
|
||||
pending_correction:
|
||||
for_instructeur: "pending"
|
||||
for_user: "to be corrected"
|
||||
resolved: corrected
|
||||
traitement:
|
||||
state: "State"
|
||||
traitement/state:
|
||||
|
|
|
@ -19,6 +19,10 @@ fr:
|
|||
accepte: "Accepté"
|
||||
refuse: "Refusé"
|
||||
sans_suite: "Classé sans suite"
|
||||
pending_correction:
|
||||
for_instructeur: "en attente"
|
||||
for_user: "à corriger"
|
||||
resolved: corrigé
|
||||
traitement:
|
||||
state: "État"
|
||||
traitement/state:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
@ -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 d’apporter 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
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ fr:
|
|||
classe_sans_suite: Le %{processed_at}, %{email} a classé ce dossier sans suite
|
||||
filterable_state:
|
||||
en_construction: "En construction"
|
||||
pending_correction: "En attente"
|
||||
en_instruction: "En instruction"
|
||||
accepte: "Accepté"
|
||||
refuse: "Refusé"
|
||||
|
|
|
@ -445,6 +445,7 @@ Rails.application.routes.draw do
|
|||
post 'repasser-en-construction' => 'dossiers#repasser_en_construction'
|
||||
post 'repasser-en-instruction' => 'dossiers#repasser_en_instruction'
|
||||
post 'terminer'
|
||||
post 'pending_correction'
|
||||
post 'send-to-instructeurs' => 'dossiers#send_to_instructeurs'
|
||||
post 'avis' => 'dossiers#create_avis'
|
||||
get 'print' => 'dossiers#print'
|
||||
|
|
15
db/migrate/20230228134859_create_dossier_corrections.rb
Normal file
15
db/migrate/20230228134859_create_dossier_corrections.rb
Normal 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
|
|
@ -1,5 +1,5 @@
|
|||
class DropTableDropDownLists < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
drop_table :drop_down_lists
|
||||
drop_table :drop_down_lists if table_exists?(:drop_down_lists)
|
||||
end
|
||||
end
|
||||
|
|
13
db/schema.rb
13
db/schema.rb
|
@ -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"
|
||||
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|
|
||||
t.boolean "automatic_operation", default: false, null: false
|
||||
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 "dossier_batch_operations", "batch_operations"
|
||||
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_transfer_logs", "dossiers"
|
||||
add_foreign_key "dossiers", "batch_operations"
|
||||
|
|
|
@ -144,4 +144,32 @@ RSpec.describe Dossiers::MessageComponent, type: :component do
|
|||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -496,6 +496,112 @@ describe Instructeurs::DossiersController, type: :controller do
|
|||
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 d’un 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
|
||||
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 } }
|
||||
|
|
|
@ -513,6 +513,16 @@ describe Users::DossiersController, type: :controller do
|
|||
expect(flash.alert).to eq("Les modifications ont déjà été déposées")
|
||||
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
|
||||
|
||||
describe '#update brouillon' do
|
||||
|
|
11
spec/factories/dossier_corrections.rb
Normal file
11
spec/factories/dossier_corrections.rb
Normal 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
|
|
@ -8,6 +8,10 @@ class DossierMailerPreview < ActionMailer::Preview
|
|||
DossierMailer.with(commentaire: commentaire(on: draft)).notify_new_answer
|
||||
end
|
||||
|
||||
def notify_pending_correction
|
||||
DossierMailer.with(dossier: dossier_en_construction).notify_pending_correction
|
||||
end
|
||||
|
||||
def notify_revert_to_instruction
|
||||
DossierMailer.notify_revert_to_instruction(dossier)
|
||||
end
|
||||
|
|
103
spec/models/concern/dossier_correctable_concern_spec.rb
Normal file
103
spec/models/concern/dossier_correctable_concern_spec.rb
Normal 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
|
|
@ -1044,6 +1044,7 @@ describe Dossier do
|
|||
let(:last_operation) { dossier.dossier_operation_logs.last }
|
||||
let(:operation_serialized) { last_operation.data }
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let!(:correction) { create(:dossier_correction, dossier:) }
|
||||
|
||||
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['dossier_id']).to eq(dossier.id) }
|
||||
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
|
||||
|
||||
describe '#passer_automatiquement_en_instruction!' do
|
||||
|
|
|
@ -248,6 +248,29 @@ describe DossierProjectionService do
|
|||
it { is_expected.to eq("") }
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -52,7 +52,7 @@ describe 'instructeurs/dossiers/show', type: :view do
|
|||
end
|
||||
end
|
||||
|
||||
context 'en_contruction' do
|
||||
context 'en_construction' do
|
||||
let(:dossier) { create(:dossier, :en_construction) }
|
||||
it 'displays the correct actions' 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
|
||||
expect(subject).to have_button('Suivre le dossier')
|
||||
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
|
||||
|
||||
|
@ -74,15 +75,15 @@ describe 'instructeurs/dossiers/show', type: :view do
|
|||
end
|
||||
|
||||
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
|
||||
expect(subject).to have_button('Ne plus suivre')
|
||||
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_selector('.header-actions ul:first-child > li .fr-btn', count: 15)
|
||||
expect(subject).to have_selector('.header-actions ul:first-child > li.instruction-button', count: 1)
|
||||
expect(subject).to have_selector('.instruction-button .fr-btn', count: 13)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue