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 a form (not a