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;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
@import "constants";
|
@import "constants";
|
||||||
|
|
||||||
.motivation {
|
.motivation {
|
||||||
padding: $default-padding;
|
|
||||||
color: $black;
|
color: $black;
|
||||||
width: 450px;
|
width: 450px;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
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
|
# 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
|
||||||
|
|
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 }
|
.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']
|
||||||
|
|
|
@ -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
|
||||||
|
|
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",
|
= 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:)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
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 }
|
.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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 l’administration.
|
||||||
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 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>"
|
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:
|
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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
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é"
|
||||||
|
|
|
@ -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'
|
||||||
|
|
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]
|
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
|
||||||
|
|
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"
|
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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
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
|
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 } }
|
||||||
|
|
|
@ -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
|
||||||
|
|
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
|
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
|
||||||
|
|
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(: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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue