diff --git a/app/assets/stylesheets/dossiers_table.scss b/app/assets/stylesheets/dossiers_table.scss index dfcacfecd..aa1aaf791 100644 --- a/app/assets/stylesheets/dossiers_table.scss +++ b/app/assets/stylesheets/dossiers_table.scss @@ -5,7 +5,6 @@ font-size: 14px; th { - vertical-align: top; padding: (2 * $default-spacer) $default-spacer; } @@ -54,7 +53,7 @@ white-space: nowrap; } - .folder-col { + .text-center { text-align: center; } diff --git a/app/assets/stylesheets/dsfr.scss b/app/assets/stylesheets/dsfr.scss index 25b9a2150..e060d196a 100644 --- a/app/assets/stylesheets/dsfr.scss +++ b/app/assets/stylesheets/dsfr.scss @@ -49,3 +49,4 @@ fieldset { display: none; } } + diff --git a/app/assets/stylesheets/sub_header.scss b/app/assets/stylesheets/sub_header.scss index 2bc9abb9d..ef3a6e02b 100644 --- a/app/assets/stylesheets/sub_header.scss +++ b/app/assets/stylesheets/sub_header.scss @@ -19,4 +19,8 @@ background-image: none; // remove DSFR underline } } + + .fr-btns-group .fr-btn { + margin-bottom: 0; + } } diff --git a/app/assets/stylesheets/table.scss b/app/assets/stylesheets/table.scss index 6c955a29e..59ac50d80 100644 --- a/app/assets/stylesheets/table.scss +++ b/app/assets/stylesheets/table.scss @@ -51,3 +51,17 @@ } } } + +.force-table-100 { + width: calc(100vw); +} + +.fr-table--bordered { + .table { + &.hoverable { + tbody tr:hover { + background: $white; + } + } + } +} diff --git a/app/components/dossiers/batch_alert_component.rb b/app/components/dossiers/batch_alert_component.rb new file mode 100644 index 000000000..67d5b9f3f --- /dev/null +++ b/app/components/dossiers/batch_alert_component.rb @@ -0,0 +1,14 @@ +class Dossiers::BatchAlertComponent < ApplicationComponent + attr_reader :batch + + def initialize(batch:, procedure:) + @batch = batch + @procedure = procedure + set_seen_at! if batch.finished_at.present? + end + + def set_seen_at! + @batch.seen_at = Time.zone.now + @batch.save + end +end diff --git a/app/components/dossiers/batch_alert_component/batch_alert_component.en.yml b/app/components/dossiers/batch_alert_component/batch_alert_component.en.yml new file mode 100644 index 000000000..78cee8dfb --- /dev/null +++ b/app/components/dossiers/batch_alert_component/batch_alert_component.en.yml @@ -0,0 +1,13 @@ +en: + finish: + title: The bulk action is finished + text: + one: 1/1 file has been archived + other: "%{success_count}/%{count} files have been archived" + in_progress: + title: A bulk action is processing + text_success: + one: 1/1 is being archived + other: "%{progress_count}/%{count} files have been archived" + link_text: Refresh this webpage + after_link_text: to check if the process is over. diff --git a/app/components/dossiers/batch_alert_component/batch_alert_component.fr.yml b/app/components/dossiers/batch_alert_component/batch_alert_component.fr.yml new file mode 100644 index 000000000..eb356bafa --- /dev/null +++ b/app/components/dossiers/batch_alert_component/batch_alert_component.fr.yml @@ -0,0 +1,13 @@ +fr: + finish: + title: L'action de masse est terminée + text_success: + one: 1 dossier a été archivé + other: "%{success_count}/%{count} dossiers ont été archivés" + in_progress: + title: Une action de masse est en cours + text_success: + one: 1 dossier sera archivé + other: "%{progress_count}/%{count} dossiers ont été archivés" + link_text: Recharger la page + after_link_text: pour voir si l'opération est finie. diff --git a/app/components/dossiers/batch_alert_component/batch_alert_component.html.haml b/app/components/dossiers/batch_alert_component/batch_alert_component.html.haml new file mode 100644 index 000000000..3e4f47a76 --- /dev/null +++ b/app/components/dossiers/batch_alert_component/batch_alert_component.html.haml @@ -0,0 +1,16 @@ +.fr-mb-5v + - if @batch.finished_at.present? + = render Dsfr::AlertComponent.new(title: t('.finish.title'), state: (@batch.failed_dossier_ids.size.positive? ? :warning : :success), heading_level: 'h2') do |c| + - c.body do + %p + = t('.finish.text_success', count: @batch.total_count, success_count: @batch.success_dossier_ids.size) + + + - else + = render Dsfr::AlertComponent.new(title: t('.in_progress.title'), state: :info, heading_level: 'h2') do |c| + - c.body do + %p= t('.in_progress.text_success', count: @batch.total_count, progress_count: @batch.progress_count) + + %p + = link_to t('.link_text'), instructeur_procedure_path(@procedure, statut: params["statut"]), data: { action: 'turbo-poll#refresh' } + = t('.after_link_text') diff --git a/app/components/dossiers/batch_operation_component.rb b/app/components/dossiers/batch_operation_component.rb new file mode 100644 index 000000000..37944a08d --- /dev/null +++ b/app/components/dossiers/batch_operation_component.rb @@ -0,0 +1,23 @@ +class Dossiers::BatchOperationComponent < ApplicationComponent + attr_reader :statut, :procedure + + def initialize(statut:, procedure:) + @statut = statut + @procedure = procedure + end + + def render? + @statut == 'traites' + end + + def available_operations + options = [] + case @statut + when 'traites' then + options.push [t(".operations.archiver"), BatchOperation.operations.fetch(:archiver)] + else + end + + options + end +end diff --git a/app/components/dossiers/batch_operation_component/batch_operation_component.en.yml b/app/components/dossiers/batch_operation_component/batch_operation_component.en.yml new file mode 100644 index 000000000..5f716d115 --- /dev/null +++ b/app/components/dossiers/batch_operation_component/batch_operation_component.en.yml @@ -0,0 +1,3 @@ +fr: + operations: + archiver: 'Archive selected files' diff --git a/app/components/dossiers/batch_operation_component/batch_operation_component.fr.yml b/app/components/dossiers/batch_operation_component/batch_operation_component.fr.yml new file mode 100644 index 000000000..6d785cd1b --- /dev/null +++ b/app/components/dossiers/batch_operation_component/batch_operation_component.fr.yml @@ -0,0 +1,3 @@ +fr: + operations: + archiver: 'Archiver les dossiers sélectionnés' diff --git a/app/components/dossiers/batch_operation_component/batch_operation_component.html.haml b/app/components/dossiers/batch_operation_component/batch_operation_component.html.haml new file mode 100644 index 000000000..98c4d445a --- /dev/null +++ b/app/components/dossiers/batch_operation_component/batch_operation_component.html.haml @@ -0,0 +1,4 @@ += form_for(BatchOperation.new, url: instructeur_batch_operations_path(procedure_id: procedure.id), method: :post, id: dom_id(BatchOperation.new), html: { class: 'flex justify-end' }, data: { "batch-operation-target" => "form"}) do |form| + .flex.align-center + - available_operations.each do |opt| + = form.button opt[0], class: "fr-btn fr-btn--icon-left fr-icon-folder-2-line", disabled: :disabled, name: "#{form.object_name}[operation]", data: { "batch-operation-target" => "submit", "submitter-operation" => opt[1]} diff --git a/app/components/dsfr/alert_component.rb b/app/components/dsfr/alert_component.rb index f33b15d89..6323578a4 100644 --- a/app/components/dsfr/alert_component.rb +++ b/app/components/dsfr/alert_component.rb @@ -14,11 +14,12 @@ class Dsfr::AlertComponent < ApplicationComponent private - def initialize(state:, title:) + def initialize(state:, title:, heading_level: 'h3') @state = state @title = title @block = block + @heading_level = heading_level end - attr_reader :state, :title, :block + attr_reader :state, :title, :block, :heading_level end diff --git a/app/components/dsfr/alert_component/alert_component.html.haml b/app/components/dsfr/alert_component/alert_component.html.haml index 869823e51..c8b934124 100644 --- a/app/components/dsfr/alert_component/alert_component.html.haml +++ b/app/components/dsfr/alert_component/alert_component.html.haml @@ -1,3 +1,4 @@ %div{ class: "fr-alert fr-alert--#{state}" } - %h3.fr-alert__title= "#{prefix_for_state}#{title}" + = content_tag(heading_level, class: 'fr-alert__title') do + = "#{prefix_for_state}#{title}" = body diff --git a/app/controllers/instructeurs/batch_operations_controller.rb b/app/controllers/instructeurs/batch_operations_controller.rb new file mode 100644 index 000000000..f82e1a7d3 --- /dev/null +++ b/app/controllers/instructeurs/batch_operations_controller.rb @@ -0,0 +1,31 @@ +module Instructeurs + class BatchOperationsController < ApplicationController + before_action :set_procedure + before_action :ensure_ownership! + + def create + BatchOperation.safe_create!(batch_operation_params) + redirect_back(fallback_location: instructeur_procedure_url(@procedure.id)) + end + + private + + def batch_operation_params + params.require(:batch_operation) + .permit(:operation, dossier_ids: []) + .merge(instructeur: current_instructeur) + .merge(groupe_instructeurs: current_instructeur.groupe_instructeurs.where(procedure_id: @procedure.id)) + end + + def set_procedure + @procedure = Procedure.find(params[:procedure_id]) + end + + def ensure_ownership! + if !current_instructeur.procedures.exists?(@procedure.id) + flash[:alert] = "Vous n’avez pas accès à cette démarche" + redirect_to root_path + end + end + end +end diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 697f20286..8f0570f29 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -9,7 +9,9 @@ module Instructeurs include Zipline before_action :redirect_on_dossier_not_found, only: :show + 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_avis_as_read, only: [:avis, :create_avis] after_action :mark_annotations_privees_as_read, only: [:annotations_privees, :update_annotations] @@ -45,6 +47,7 @@ module Instructeurs def show @demande_seen_at = current_instructeur.follows.find_by(dossier: dossier_with_champs)&.demande_seen_at + @is_dossier_in_batch_operation = dossier.batch_operation.present? respond_to do |format| format.pdf do @@ -320,5 +323,17 @@ module Instructeurs redirect_to instructeur_procedure_path(procedure) end end + + def redirect_on_dossier_in_batch_operation + dossier_in_batch = begin + dossier + rescue ActiveRecord::RecordNotFound + current_instructeur.dossiers.find(params[:dossier_id]) + end + if dossier_in_batch.batch_operation.present? + flash.alert = "Votre action n'a pas été effectuée, ce dossier fait parti d'un traitement de masse." + redirect_back(fallback_location: instructeur_dossier_path(procedure, dossier_in_batch)) + end + end end end diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 90fd54944..4fc045d93 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -90,6 +90,11 @@ module Instructeurs @projected_dossiers = DossierProjectionService.project(@filtered_sorted_paginated_ids, procedure_presentation.displayed_fields) assign_exports + + @batch_operations = BatchOperation.joins(:groupe_instructeurs) + .where(groupe_instructeurs: current_instructeur.groupe_instructeurs.where(procedure_id: @procedure.id)) + .where(seen_at: nil) + .distinct end def deleted_dossiers diff --git a/app/javascript/controllers/batch_operation_controller.ts b/app/javascript/controllers/batch_operation_controller.ts new file mode 100644 index 000000000..66720d06d --- /dev/null +++ b/app/javascript/controllers/batch_operation_controller.ts @@ -0,0 +1,49 @@ +import { ApplicationController } from './application_controller'; + +export class BatchOperationController extends ApplicationController { + static targets = ['form', 'input', 'submit']; + + declare readonly formTarget: HTMLFormElement; + declare readonly submitTarget: HTMLInputElement; + declare readonly inputTargets: HTMLInputElement[]; + + connect() { + this.formTarget.addEventListener( + 'submit', + this.interceptFormSubmit.bind(this) + ); + } + + // DSFR recommends a or