diff --git a/app/assets/stylesheets/dossiers_table.scss b/app/assets/stylesheets/dossiers_table.scss index 602e9c616..4f903da50 100644 --- a/app/assets/stylesheets/dossiers_table.scss +++ b/app/assets/stylesheets/dossiers_table.scss @@ -45,7 +45,8 @@ } } - .number-col { + .number-col, + .fr-badge { white-space: nowrap; } diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 2974a2fae..d6e7d944f 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -30,7 +30,7 @@ color: $dark-red; } - label, + label:not(.fr-label), legend.form-label { font-size: 18px; margin-bottom: $default-padding; diff --git a/app/assets/stylesheets/motivation.scss b/app/assets/stylesheets/motivation.scss index 32e076dff..fff6c80f0 100644 --- a/app/assets/stylesheets/motivation.scss +++ b/app/assets/stylesheets/motivation.scss @@ -2,7 +2,6 @@ @import "constants"; .motivation { - padding: $default-padding; color: $black; width: 450px; diff --git a/app/components/dossiers/edit_footer_component.rb b/app/components/dossiers/edit_footer_component.rb index 0e7fc0128..fca7fab45 100644 --- a/app/components/dossiers/edit_footer_component.rb +++ b/app/components/dossiers/edit_footer_component.rb @@ -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 diff --git a/app/components/dossiers/message_component.rb b/app/components/dossiers/message_component.rb index 16244c2a6..af19bc2f8 100644 --- a/app/components/dossiers/message_component.rb +++ b/app/components/dossiers/message_component.rb @@ -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? diff --git a/app/components/dossiers/message_component/message_component.html.haml b/app/components/dossiers/message_component/message_component.html.haml index 2785815a2..1839981a0 100644 --- a/app/components/dossiers/message_component/message_component.html.haml +++ b/app/components/dossiers/message_component/message_component.html.haml @@ -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 diff --git a/app/components/instructeurs/en_construction_menu_component.rb b/app/components/instructeurs/en_construction_menu_component.rb new file mode 100644 index 000000000..a2c930f80 --- /dev/null +++ b/app/components/instructeurs/en_construction_menu_component.rb @@ -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 diff --git a/app/components/instructeurs/en_construction_menu_component/en_construction_menu_component.en.yml b/app/components/instructeurs/en_construction_menu_component/en_construction_menu_component.en.yml new file mode 100644 index 000000000..d9d6426bb --- /dev/null +++ b/app/components/instructeurs/en_construction_menu_component/en_construction_menu_component.en.yml @@ -0,0 +1,4 @@ +--- +en: + revert_en_construction: Revert to in progress + request_correction: Request a correction diff --git a/app/components/instructeurs/en_construction_menu_component/en_construction_menu_component.fr.yml b/app/components/instructeurs/en_construction_menu_component/en_construction_menu_component.fr.yml new file mode 100644 index 000000000..4430f7654 --- /dev/null +++ b/app/components/instructeurs/en_construction_menu_component/en_construction_menu_component.fr.yml @@ -0,0 +1,4 @@ +--- +fr: + revert_en_construction: Repasser en construction + request_correction: Demander une correction diff --git a/app/components/instructeurs/en_construction_menu_component/en_construction_menu_component.html.haml b/app/components/instructeurs/en_construction_menu_component/en_construction_menu_component.html.haml new file mode 100644 index 000000000..0c691ca45 --- /dev/null +++ b/app/components/instructeurs/en_construction_menu_component/en_construction_menu_component.html.haml @@ -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 ?'} diff --git a/app/components/instructeurs/instruction_menu_component.rb b/app/components/instructeurs/instruction_menu_component.rb new file mode 100644 index 000000000..01dc27f85 --- /dev/null +++ b/app/components/instructeurs/instruction_menu_component.rb @@ -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 diff --git a/app/components/instructeurs/instruction_menu_component/instruction_menu_component.en.yml b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.en.yml new file mode 100644 index 000000000..165ddbeaf --- /dev/null +++ b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.en.yml @@ -0,0 +1,3 @@ +--- +en: + instruct: Instruct the file diff --git a/app/components/instructeurs/instruction_menu_component/instruction_menu_component.fr.yml b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.fr.yml new file mode 100644 index 000000000..e82e45e9e --- /dev/null +++ b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.fr.yml @@ -0,0 +1,3 @@ +--- +fr: + instruct: Instruire le dossier diff --git a/app/components/instructeurs/instruction_menu_component/instruction_menu_component.html.haml b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.html.haml new file mode 100644 index 000000000..51a08855a --- /dev/null +++ b/app/components/instructeurs/instruction_menu_component/instruction_menu_component.html.haml @@ -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 ?' } + diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 220fd83a5..fc4b82b19 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -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) diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 36bb97fc1..3f979c4c2 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -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 diff --git a/app/helpers/dossier_helper.rb b/app/helpers/dossier_helper.rb index b94ac46e5..81099a161 100644 --- a/app/helpers/dossier_helper.rb +++ b/app/helpers/dossier_helper.rb @@ -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}" diff --git a/app/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb index 81d42cf4f..24ebeab43 100644 --- a/app/mailers/dossier_mailer.rb +++ b/app/mailers/dossier_mailer.rb @@ -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 diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index d7293e290..c4bfdf593 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -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 diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb index f5e3561e7..a34c477c2 100644 --- a/app/models/commentaire.rb +++ b/app/models/commentaire.rb @@ -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,7 +113,11 @@ class Commentaire < ApplicationRecord end def notify_user(job_options = {}) - DossierMailer.with(commentaire: self).notify_new_answer.deliver_later(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? diff --git a/app/models/concerns/dossier_correctable_concern.rb b/app/models/concerns/dossier_correctable_concern.rb new file mode 100644 index 000000000..ee9dc09aa --- /dev/null +++ b/app/models/concerns/dossier_correctable_concern.rb @@ -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 diff --git a/app/models/dossier.rb b/app/models/dossier.rb index ac717eba4..fb8ac746f 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -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 diff --git a/app/models/dossier_correction.rb b/app/models/dossier_correction.rb new file mode 100644 index 000000000..eda8dfa40 --- /dev/null +++ b/app/models/dossier_correction.rb @@ -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 diff --git a/app/models/procedure_presentation.rb b/app/models/procedure_presentation.rb index 194ef1519..42e47398d 100644 --- a/app/models/procedure_presentation.rb +++ b/app/models/procedure_presentation.rb @@ -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' - Dossier.human_attribute_name("state.#{filter['value']}") + 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'] diff --git a/app/services/dossier_projection_service.rb b/app/services/dossier_projection_service.rb index 4a32aae39..a5e348ad6 100644 --- a/app/services/dossier_projection_service.rb +++ b/app/services/dossier_projection_service.rb @@ -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 diff --git a/app/views/dossier_mailer/notify_pending_correction.html.haml b/app/views/dossier_mailer/notify_pending_correction.html.haml new file mode 100644 index 000000000..6a576b6dd --- /dev/null +++ b/app/views/dossier_mailer/notify_pending_correction.html.haml @@ -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 diff --git a/app/views/instructeurs/dossiers/_header_actions.html.haml b/app/views/instructeurs/dossiers/_header_actions.html.haml index 38b8856e9..d5d28a0b1 100644 --- a/app/views/instructeurs/dossiers/_header_actions.html.haml +++ b/app/views/instructeurs/dossiers/_header_actions.html.haml @@ -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:) diff --git a/app/views/instructeurs/dossiers/_header_top.html.haml b/app/views/instructeurs/dossiers/_header_top.html.haml index 27b8ac424..4537515f2 100644 --- a/app/views/instructeurs/dossiers/_header_top.html.haml +++ b/app/views/instructeurs/dossiers/_header_top.html.haml @@ -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) diff --git a/app/views/instructeurs/dossiers/_instruction_button.html.haml b/app/views/instructeurs/dossiers/_instruction_button.html.haml index fbf1b1b3b..85a5b10ca 100644 --- a/app/views/instructeurs/dossiers/_instruction_button.html.haml +++ b/app/views/instructeurs/dossiers/_instruction_button.html.haml @@ -1,35 +1,56 @@ -- 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" - - 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é + - if dossier.en_instruction? + - 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") 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(class: "hidden inactive form-inside") 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 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") 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(class: "hidden inactive form-inside") 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 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") 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 ?' } + - 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 ?'} diff --git a/app/views/instructeurs/dossiers/_instruction_button_motivation.html.haml b/app/views/instructeurs/dossiers/_instruction_button_motivation.html.haml index 4d089c6e4..76b4d576e 100644 --- a/app/views/instructeurs/dossiers/_instruction_button_motivation.html.haml +++ b/app/views/instructeurs/dossiers/_instruction_button_motivation.html.haml @@ -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 diff --git a/app/views/instructeurs/procedures/_dossier_actions.html.haml b/app/views/instructeurs/procedures/_dossier_actions.html.haml index 91de51c96..6dda325aa 100644 --- a/app/views/instructeurs/procedures/_dossier_actions.html.haml +++ b/app/views/instructeurs/procedures/_dossier_actions.html.haml @@ -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 diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index 4678472e3..a9b7fc008 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -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 } diff --git a/app/views/recherche/index.html.haml b/app/views/recherche/index.html.haml index d98aeef72..71814bd8c 100644 --- a/app/views/recherche/index.html.haml +++ b/app/views/recherche/index.html.haml @@ -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 diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index 083c9b78e..1f1dbefcd 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -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) diff --git a/app/views/shared/dossiers/_messagerie.html.haml b/app/views/shared/dossiers/_messagerie.html.haml index 56eba7d7e..854316665 100644 --- a/app/views/shared/dossiers/_messagerie.html.haml +++ b/app/views/shared/dossiers/_messagerie.html.haml @@ -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)) diff --git a/app/views/shared/dossiers/messages/_form.html.haml b/app/views/shared/dossiers/messages/_form.html.haml index 69bca30cc..5e616301c 100644 --- a/app/views/shared/dossiers/messages/_form.html.haml +++ b/app/views/shared/dossiers/messages/_form.html.haml @@ -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 - = 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 } + - disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false + - if !disable_piece_jointe + .fr-mt-3w + = render Attachment::EditComponent.new(attached_file: commentaire.piece_jointe) + + .fr-mt-3w + = f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true } diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index 2a5c2951b..4c5a7361a 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -37,7 +37,11 @@ %td %span.cell-link= demandeur_dossier(dossier) %td.status-col - = status_badge(dossier.state) + - 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 diff --git a/app/views/users/dossiers/show/_header.html.haml b/app/views/users/dossiers/show/_header.html.haml index 62836d088..e5544c01a 100644 --- a/app/views/users/dossiers/show/_header.html.haml +++ b/app/views/users/dossiers/show/_header.html.haml @@ -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? diff --git a/app/views/users/dossiers/show/_status_overview.html.haml b/app/views/users/dossiers/show/_status_overview.html.haml index c258b3b6f..3fef287dc 100644 --- a/app/views/users/dossiers/show/_status_overview.html.haml +++ b/app/views/users/dossiers/show/_status_overview.html.haml @@ -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') diff --git a/config/locales/en.yml b/config/locales/en.yml index a60b14563..36cf3ee11 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index e66005122..210631de9 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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: "
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.
Cela permet aux administrations d’accéder à des informations certifiées à la source et ainsi :
Important : les disposition de l’article L144-8 n’autorisent que l’échange des informations strictement nécessaires pour traiter une démarche.
En conséquence, ne sélectionnez ici que les données auxquelles vous aurez accès d’un point de vue légal.