diff --git a/app/components/dossiers/export_dropdown_component.rb b/app/components/dossiers/export_dropdown_component.rb index b9215ce9911..928cd35fe59 100644 --- a/app/components/dossiers/export_dropdown_component.rb +++ b/app/components/dossiers/export_dropdown_component.rb @@ -6,11 +6,12 @@ class Dossiers::ExportDropdownComponent < ApplicationComponent attr_reader :wrapper attr_reader :export_templates - def initialize(procedure:, export_templates: nil, statut: nil, count: nil, class_btn: nil, export_url: nil, show_export_template_tab: true, wrapper: :div) + def initialize(procedure:, export_templates: nil, statut: nil, count: nil, archived_count: 0, class_btn: nil, export_url: nil, show_export_template_tab: true, wrapper: :div) @procedure = procedure @export_templates = export_templates @statut = statut @count = count + @archived_count = archived_count @class_btn = class_btn @export_url = export_url @show_export_template_tab = show_export_template_tab @@ -29,6 +30,18 @@ def allowed_format?(item) item.fetch(:format) != :json || @procedure.active_revision.carte? end + def can_include_archived? + @statut == 'tous' && @archived_count > 0 + end + + def include_archived_title + if @archived_count > 1 + "Inclure les #{@archived_count} dossiers « archivés »" + else + "Inclure le dossier « archivé »" + end + end + def download_export_path(export_format: nil, export_template_id: nil, no_progress_notification: nil) @export_url.call(@procedure, export_format:, diff --git a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml index b17752c4384..ad1962f281a 100644 --- a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml +++ b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml @@ -15,12 +15,19 @@ .fr-tabs__panel.fr-pb-8w.fr-tabs__panel--selected{ id: "tabpanel-standard#{@count}-panel", role: "tabpanel", "aria-labelledby": "tabpanel-standard#{@count}", tabindex: "0" } = form_with url: download_export_path, namespace: "export#{@count}", data: { turbo_method: :post, turbo: true } do |f| + + - if can_include_archived? + .fr-pb-2w + = render Dsfr::ToggleComponent.new(form: f, + target: :include_archived, + html_title: include_archived_title) + = f.hidden_field :statut, value: @statut %fieldset.fr-fieldset#radio-hint{ "aria-labelledby": "radio-hint-legend" } %legend.fr-fieldset__legend--regular.fr-fieldset__legend#radio-hint-legend Sélectionner le format de l'export .fr-fieldset__element .fr-radio-group - = f.radio_button :export_format, 'xlsx' + = f.radio_button :export_format, 'xlsx', checked: true = f.label :export_format_xlsx, 'Fichier xlsx' .fr-fieldset__element .fr-radio-group @@ -57,6 +64,12 @@ .fr-tabs__panel.fr-pr-3w.fr-pb-8w{ id: "tabpanel-template#{@count}-panel", role: "tabpanel", "aria-labelledby": "tabpanel-template", tabindex: "0" } = form_with url: download_export_path, namespace: "export_template_#{@count}", data: { turbo_method: :post, turbo: true } do |f| = f.hidden_field :statut, value: @statut + - if can_include_archived? + .fr-pb-2w + = render Dsfr::ToggleComponent.new(form: f, + target: :include_archived, + html_title: include_archived_title) + .fr-select-group - if export_templates.present? %label.fr-label{ for: 'select' } diff --git a/app/components/dsfr/toggle_component.rb b/app/components/dsfr/toggle_component.rb index 43084c6f0b9..adb13898fc8 100644 --- a/app/components/dsfr/toggle_component.rb +++ b/app/components/dsfr/toggle_component.rb @@ -3,16 +3,18 @@ class Dsfr::ToggleComponent < ApplicationComponent attr_reader :target attr_reader :title + attr_reader :html_title attr_reader :hint attr_reader :toggle_labels attr_reader :disabled attr_reader :data attr_reader :extra_class_names - def initialize(form:, target:, title:, disabled: nil, hint: nil, toggle_labels: { checked: 'Activé', unchecked: 'Désactivé' }, opt: nil, extra_class_names: nil) + def initialize(form:, target:, title: nil, html_title: nil, disabled: nil, hint: nil, toggle_labels: { checked: 'Activé', unchecked: 'Désactivé' }, opt: nil, extra_class_names: nil) @form = form @target = target @title = title + @html_title = html_title @hint = hint @disabled = disabled @toggle_labels = toggle_labels @@ -22,7 +24,19 @@ def initialize(form:, target:, title:, disabled: nil, hint: nil, toggle_labels: private + def label_for + return input_id if @form.object.present? + + return "#{@form.options[:namespace]}_#{target}" if @form.options[:namespace].present? + + target.to_s + end + def input_id - dom_id(@form.object, target) + if @form.object.present? + dom_id(@form.object, target) + else + target.to_s + end end end diff --git a/app/components/dsfr/toggle_component/toggle_component.html.haml b/app/components/dsfr/toggle_component/toggle_component.html.haml index 90fa244330b..e4582803832 100644 --- a/app/components/dsfr/toggle_component/toggle_component.html.haml +++ b/app/components/dsfr/toggle_component/toggle_component.html.haml @@ -1,8 +1,8 @@ %div{ class: "fr-toggle fr-toggle--label-left #{extra_class_names}" } = @form.check_box target, class: 'fr-toggle__input', disabled:, data:, id: input_id = @form.label target, - title, - for: input_id, + title || html_title&.html_safe, + for: label_for, data: { 'fr-checked-label': toggle_labels[:checked], 'fr-unchecked-label': toggle_labels[:unchecked] }, class: 'fr-toggle__label' diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 06297a87386..1ab08f32016 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -106,6 +106,12 @@ def show page = params[:page].presence || 1 @dossiers_count = @filtered_sorted_ids.size + @archived_dossiers_count = if statut == 'tous' + @counts[:archives] + else + 0 + end + @filtered_sorted_paginated_ids = Kaminari .paginate_array(@filtered_sorted_ids) .page(page) @@ -317,6 +323,7 @@ def export_options time_span_type: params[:time_span_type], statut: params[:statut], export_template:, + include_archived: params[:include_archived], procedure_presentation: params[:statut].present? ? procedure_presentation : nil }.compact end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index cad644dfdc7..8631a72b897 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -244,7 +244,7 @@ def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zon scope :with_type_de_champ, -> (stable_id) { joins(:champs).where(champs: { stream: 'main', stable_id: }) } - scope :all_state, -> { not_archived.state_not_brouillon } + scope :all_state, -> (include_archived: false) { include_archived ? state_not_brouillon : not_archived.state_not_brouillon } scope :en_construction, -> { not_archived.state_en_construction } scope :en_instruction, -> { not_archived.state_en_instruction } scope :termine, -> { not_archived.state_termine } @@ -385,7 +385,7 @@ def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zon .distinct end - scope :by_statut, -> (statut, instructeur = nil) do + scope :by_statut, -> (statut, instructeur: nil, include_archived: false) do case statut when 'a-suivre' visible_by_administration @@ -399,7 +399,7 @@ def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zon when 'traites' visible_by_administration.termine when 'tous' - visible_by_administration.all_state + visible_by_administration.all_state(include_archived:) when 'supprimes' hidden_by_administration.state_termine.or(hidden_by_expired) when 'archives' diff --git a/app/models/export.rb b/app/models/export.rb index 7cce92ca9eb..bcc2c671f54 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -69,7 +69,7 @@ def since time_span_type == Export.time_span_types.fetch(:monthly) ? 30.days.ago : nil end - def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil, export_template: nil) + def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil, export_template: nil, include_archived: false) filtered_columns = Array.wrap(procedure_presentation&.filters_for(statut)) sorted_column = procedure_presentation&.sorted_column @@ -78,6 +78,7 @@ def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, export_template:, time_span_type:, statut:, + include_archived:, key: generate_cache_key(groupe_instructeurs.map(&:id), filtered_columns, sorted_column) } @@ -136,7 +137,7 @@ def dossiers_for_export dossiers.visible_by_administration.where('dossiers.depose_at > ?', since) elsif filtered_columns.present? || sorted_column.present? instructeur = instructeur_from(user_profile) - filtered_sorted_ids = DossierFilterService.filtered_sorted_ids(dossiers, statut, filtered_columns, sorted_column, instructeur) + filtered_sorted_ids = DossierFilterService.filtered_sorted_ids(dossiers, statut, filtered_columns, sorted_column, instructeur, include_archived: include_archived) dossiers.where(id: filtered_sorted_ids) else diff --git a/app/services/dossier_filter_service.rb b/app/services/dossier_filter_service.rb index b8bdb3d8152..5f8679611fd 100644 --- a/app/services/dossier_filter_service.rb +++ b/app/services/dossier_filter_service.rb @@ -3,8 +3,8 @@ class DossierFilterService TYPE_DE_CHAMP = 'type_de_champ' - def self.filtered_sorted_ids(dossiers, statut, filters, sorted_column, instructeur, count: nil) - dossiers_by_statut = dossiers.by_statut(statut, instructeur) + def self.filtered_sorted_ids(dossiers, statut, filters, sorted_column, instructeur, count: nil, include_archived: false) + dossiers_by_statut = dossiers.by_statut(statut, instructeur:, include_archived:) dossiers_sorted_ids = self.sorted_ids(dossiers_by_statut, sorted_column, instructeur, count || dossiers_by_statut.size) if filters.present? diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index 003f8a3cbe0..5e836a06853 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -63,7 +63,7 @@ .fr-ml-auto - if @dossiers_count > 0 %ul.fr-btns-group.fr-btns-group--right.fr-btns-group--sm.fr-btns-group--inline-md.fr-btns-group--icon-left - = render Dossiers::ExportDropdownComponent.new(wrapper: :li, procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), statut: @statut, count: @dossiers_count, + = render Dossiers::ExportDropdownComponent.new(wrapper: :li, procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), statut: @statut, count: @dossiers_count, archived_count: @archived_dossiers_count, class_btn: 'fr-btn--secondary fr-icon-download-line', export_url: method(:download_export_instructeur_procedure_path)) = render Dropdown::MenuComponent.new(wrapper: :li, button_options: { class: ['fr-btn--tertiary', 'fr-icon-settings-5-line'] }, menu_options: { id: 'custom-menu' }) do |menu| diff --git a/db/migrate/20241203154714_add_include_archived_dossiers_in_export.rb b/db/migrate/20241203154714_add_include_archived_dossiers_in_export.rb new file mode 100644 index 00000000000..b5c8395a5a5 --- /dev/null +++ b/db/migrate/20241203154714_add_include_archived_dossiers_in_export.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddIncludeArchivedDossiersInExport < ActiveRecord::Migration[7.0] + def change + add_column :exports, :include_archived, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 8a6de1a8998..9f0c33c87c9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_11_26_145420) do +ActiveRecord::Schema[7.0].define(version: 2024_12_03_154714) do # These are extensions that must be enabled in order to support this database enable_extension "pg_buffercache" enable_extension "pg_stat_statements" @@ -643,6 +643,7 @@ t.bigint "export_template_id" t.jsonb "filtered_columns", default: [], null: false, array: true t.string "format", null: false + t.boolean "include_archived", default: false, null: false t.bigint "instructeur_id" t.string "job_status", default: "pending", null: false t.text "key", null: false diff --git a/spec/components/dossiers/export_dropdown_component_spec.rb b/spec/components/dossiers/export_dropdown_component_spec.rb new file mode 100644 index 00000000000..5192a4aaab9 --- /dev/null +++ b/spec/components/dossiers/export_dropdown_component_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Dossiers::ExportDropdownComponent, type: :component do + subject(:component) { described_class.new(**params) } + + describe '#include_archived_title' do + let(:procedure) { double('Procedure') } + + context 'when archived_count is greater than 1' do + it 'returns the pluralized archived title' do + component = Dossiers::ExportDropdownComponent.new( + procedure: procedure, + archived_count: 3 + ) + expect(component.include_archived_title).to eq("Inclure les 3 dossiers « archivés »") + end + end + + context 'when archived_count is 1 or less' do + it 'returns the singular archived title' do + component = Dossiers::ExportDropdownComponent.new( + procedure: procedure, + archived_count: 1 + ) + expect(component.include_archived_title).to eq("Inclure le dossier « archivé »") + end + end + end +end diff --git a/spec/services/dossier_filter_service_spec.rb b/spec/services/dossier_filter_service_spec.rb index 28d16a283fe..9ae26622ce6 100644 --- a/spec/services/dossier_filter_service_spec.rb +++ b/spec/services/dossier_filter_service_spec.rb @@ -9,20 +9,30 @@ def to_filter((label, filter)) = FilteredColumn.new(column: procedure.find_colum let(:dossiers) { procedure.dossiers } let(:statut) { 'suivis' } let(:filters) { [] } + let(:include_archived) { false } let(:sorted_columns) { procedure.default_sorted_column } - subject { described_class.filtered_sorted_ids(dossiers, statut, filters, sorted_columns, instructeur) } + subject { described_class.filtered_sorted_ids(dossiers, statut, filters, sorted_columns, instructeur, include_archived:) } context 'with no filters' do let(:en_construction_dossier) { create(:dossier, :en_construction, procedure:) } let(:accepte_dossier) { create(:dossier, :accepte, procedure:) } + let(:archived_dossier) { create(:dossier, :accepte, :archived, procedure:) } before do create(:follow, dossier: en_construction_dossier, instructeur:) create(:follow, dossier: accepte_dossier, instructeur:) + create(:follow, dossier: archived_dossier, instructeur:) end it { is_expected.to contain_exactly(en_construction_dossier.id) } + + context 'when include_archived is true' do + let(:include_archived) { true } + let(:statut) { 'tous' } + + it { is_expected.to contain_exactly(en_construction_dossier.id, accepte_dossier.id, archived_dossier.id) } + end end context 'with mocked sorted_ids' do