diff --git a/app/assets/stylesheets/dsfr.scss b/app/assets/stylesheets/dsfr.scss index bd3100ce6..b412f7981 100644 --- a/app/assets/stylesheets/dsfr.scss +++ b/app/assets/stylesheets/dsfr.scss @@ -39,6 +39,13 @@ input[type="radio"] { } // scss-lint:enable DuplicateProperty +// remove additional calendar icon on date input already handle by Firefox navigator +@-moz-document url-prefix() { + .fr-input[type="date"] { + background-image: none; + } +} + // remove pointer cursor on textarea textarea { cursor: auto; @@ -66,3 +73,15 @@ fieldset { color: $light-red; box-shadow: 0px 0px 0px 1px $light-red; } + +// on utilise le dropdown de sélecteur de langue pour un autre usage donc on veut retirer l'icone +.fr-translate .fr-translate__btn.custom-fr-translate-no-icon::before { + display: none; +} + +// on veut ferrer à droite le dropdown de sélecteur de langue +@media (min-width: 62em) { + .fr-nav__item.custom-fr-translate-flex-end { + align-items: flex-end; + } +} diff --git a/app/components/dossiers/filter_component.rb b/app/components/dossiers/instructeur_filter_component.rb similarity index 89% rename from app/components/dossiers/filter_component.rb rename to app/components/dossiers/instructeur_filter_component.rb index 1f4c9a54f..5f55441b1 100644 --- a/app/components/dossiers/filter_component.rb +++ b/app/components/dossiers/instructeur_filter_component.rb @@ -1,4 +1,4 @@ -class Dossiers::FilterComponent < ApplicationComponent +class Dossiers::InstructeurFilterComponent < ApplicationComponent def initialize(procedure:, procedure_presentation:, statut:, field_id: nil) @procedure = procedure @procedure_presentation = procedure_presentation diff --git a/app/components/dossiers/filter_component/filter_component.en.yml b/app/components/dossiers/instructeur_filter_component/instructeur_filter_component.en.yml similarity index 100% rename from app/components/dossiers/filter_component/filter_component.en.yml rename to app/components/dossiers/instructeur_filter_component/instructeur_filter_component.en.yml diff --git a/app/components/dossiers/filter_component/filter_component.fr.yml b/app/components/dossiers/instructeur_filter_component/instructeur_filter_component.fr.yml similarity index 100% rename from app/components/dossiers/filter_component/filter_component.fr.yml rename to app/components/dossiers/instructeur_filter_component/instructeur_filter_component.fr.yml diff --git a/app/components/dossiers/filter_component/filter_component.html.haml b/app/components/dossiers/instructeur_filter_component/instructeur_filter_component.html.haml similarity index 100% rename from app/components/dossiers/filter_component/filter_component.html.haml rename to app/components/dossiers/instructeur_filter_component/instructeur_filter_component.html.haml diff --git a/app/components/dossiers/user_filter_component.rb b/app/components/dossiers/user_filter_component.rb new file mode 100644 index 000000000..8f7d731c5 --- /dev/null +++ b/app/components/dossiers/user_filter_component.rb @@ -0,0 +1,23 @@ +class Dossiers::UserFilterComponent < ApplicationComponent + include DossierHelper + + def initialize(statut:, filter:) + @statut = statut + @filter = filter + end + + attr_reader :statut, :filter + + def render? + ['en-cours', 'traites'].include?(@statut) + end + + def states_collection(statut) + case statut + when 'en-cours' + Dossier.states.values - Dossier::TERMINE + when 'traites' + Dossier::TERMINE + end + end +end diff --git a/app/components/dossiers/user_filter_component/user_filter_component.en.yml b/app/components/dossiers/user_filter_component/user_filter_component.en.yml new file mode 100644 index 000000000..bc72f08e2 --- /dev/null +++ b/app/components/dossiers/user_filter_component/user_filter_component.en.yml @@ -0,0 +1,16 @@ +en: + legend: + states: States + created_at: Creation date + depose_at: Submission date + button: + apply_filters: Apply filters + reset_filters: Reset filters + select_filters: Select a filter + tag: + active_filters: + one: "1 active filter" + other: "%{count} active filters" + active_filters_link_title: + one: Remove the active filter + other: "Remove the %{count} active filters" diff --git a/app/components/dossiers/user_filter_component/user_filter_component.fr.yml b/app/components/dossiers/user_filter_component/user_filter_component.fr.yml new file mode 100644 index 000000000..83348118c --- /dev/null +++ b/app/components/dossiers/user_filter_component/user_filter_component.fr.yml @@ -0,0 +1,16 @@ +fr: + legend: + states: États + created_at: Date de création + depose_at: Date de dépot + button: + apply_filters: Appliquer les filtres + reset_filters: Réinitialiser les filtres + select_filters: Sélectionner un filtre + tag: + active_filters: + one: "1 filtre actif" + other: "%{count} filtres actifs" + active_filters_link_title: + one: Retirer le filtre actif + other: "Retirer les %{count} filtres actifs" diff --git a/app/components/dossiers/user_filter_component/user_filter_component.html.haml b/app/components/dossiers/user_filter_component/user_filter_component.html.haml new file mode 100644 index 000000000..5a6c92e51 --- /dev/null +++ b/app/components/dossiers/user_filter_component/user_filter_component.html.haml @@ -0,0 +1,37 @@ +.fr-grid-row + .fr-col-12 + %nav.fr-translate.fr-nav + .fr-nav__item.custom-fr-translate-flex-end + %button.fr-translate__btn.translate-no-icon.fr-btn.fr-btn--tertiary.custom-fr-translate-no-icon{ "aria-controls" => "filters", "aria-expanded" => "false", title: t('.button.select_filters') } + = t('.button.select_filters') + #filters.fr-collapse.fr-translate__menu.fr-menu + %ul.fr-menu__list.fr-p-3w + = form_with(url: dossiers_path(), method: :get ) do |f| + = f.hidden_field :statut, value: @statut + + %fieldset#checkboxes.fr-fieldset{ "aria-labelledby" => "checkboxes-legend" } + %legend#checkboxes-legend.fr-fieldset__legend--regular.fr-fieldset__legend + = t('.legend.states') + = f.collection_check_boxes :states, states_collection(@statut), :to_s, :to_s, include_hidden: false do |b| + .fr-fieldset__element + .fr-checkbox-group.fr-ml-2w.fr-py-1w + = b.check_box(checked: filter.states_filtered?(b.value)) + = b.label(class: 'fr-label') { dossier_display_state(b.text) } + + + .fr-input-group + = f.label 'from_created_at_date', t('.legend.created_at'), class: 'fr-label' + = f.date_field 'from_created_at_date', value: @filter.from_created_at_date, class: 'fr-input' + + + .fr-input-group + = f.label 'from_depose_at_date', t('.legend.depose_at'), class: 'fr-label' + = f.date_field 'from_depose_at_date', value: @filter.from_depose_at_date, class: 'fr-input' + + .fr-my-2w + = f.submit t('.button.apply_filters'), class: 'fr-btn fr-btn--sm' + = link_to t('.button.reset_filters'), dossiers_path(statut: @statut), class: 'fr-btn fr-btn--sm fr-btn--tertiary-no-outline' + + - if @filter.filter_params.present? + .fr-col-12.text-right + = link_to t('.tag.active_filters', count: @filter.filter_params_count), dossiers_path(statut: @statut), title: t('.tag.active_filters_link_title', count: @filter.filter_params_count), class: 'fr-tag fr-tag--sm fr-tag--dismiss' diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index d4d4d1469..8af994e31 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -49,6 +49,9 @@ module Users end.page(page) @first_brouillon_recently_updated = current_user.dossiers.visible_by_user.brouillons_recently_updated.first + + @filter = DossiersFilter.new(current_user, params) + @dossiers = @filter.filter_procedures(@dossiers).page(page) end def show diff --git a/app/models/dossiers_filter.rb b/app/models/dossiers_filter.rb new file mode 100644 index 000000000..ab1c4deb1 --- /dev/null +++ b/app/models/dossiers_filter.rb @@ -0,0 +1,53 @@ +class DossiersFilter + attr_reader :user, :params + + def initialize(user, params) + @user = user + @params = params.permit(:page, :from_created_at_date, :from_depose_at_date, states: []) + end + + def filter_params + params[:from_created_at_date].presence || params[:from_depose_at_date].presence || params[:states].presence + end + + def filter_params_count + count = 0 + count += 1 if params[:from_created_at_date].presence + count += 1 if params[:from_depose_at_date].presence + count += params[:states].count if params[:states].presence + count + end + + def filter_procedures(dossiers) + return dossiers if filter_params.blank? + dossiers_result = dossiers + dossiers_result = dossiers_result.where(state: states) if states.present? + dossiers_result = dossiers_result.where('created_at >= ?', from_created_at_date) if from_created_at_date.present? + dossiers_result = dossiers_result.where('depose_at >= ?', from_depose_at_date) if from_depose_at_date.present? + dossiers_result + end + + def states + params[:states].compact_blank if params[:states].present? + end + + def states_filtered?(state) + states&.include?(state) + end + + def from_created_at_date + return if params[:from_created_at_date].blank? + + Date.parse(params[:from_created_at_date]) + rescue Date::Error + nil + end + + def from_depose_at_date + return if params[:from_depose_at_date].blank? + + Date.parse(params[:from_depose_at_date]) + rescue Date::Error + nil + end +end diff --git a/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml b/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml index 7106f0d58..4bcafb3ed 100644 --- a/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml +++ b/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml @@ -3,5 +3,4 @@ = t('views.instructeurs.dossiers.filters.title') - menu.with_form do - = render Dossiers::FilterComponent.new(procedure: procedure, procedure_presentation: @procedure_presentation, statut: statut) - + = render Dossiers::InstructeurFilterComponent.new(procedure: procedure, procedure_presentation: @procedure_presentation, statut: statut) diff --git a/app/views/instructeurs/procedures/update_filter.turbo_stream.haml b/app/views/instructeurs/procedures/update_filter.turbo_stream.haml index 05598d95b..a5cd42c91 100644 --- a/app/views/instructeurs/procedures/update_filter.turbo_stream.haml +++ b/app/views/instructeurs/procedures/update_filter.turbo_stream.haml @@ -1,2 +1,2 @@ = turbo_stream.replace 'filter-component' do - = render Dossiers::FilterComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation, statut: @statut, field_id: @field) + = render Dossiers::InstructeurFilterComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation, statut: @statut, field_id: @field) diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index b62773984..7e4bccd89 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -46,9 +46,16 @@ = paginate dossiers, views_prefix: 'shared' - else - .blank-tab - %h2.empty-text Aucun dossier - %p.empty-text-details - Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche. - %br - Celui ci doit ressembler à #{APPLICATION_BASE_URL}/commencer/xxx. + - if filter.filter_params.present? + .blank-tab + %h2.empty-text= t('views.users.dossiers.dossiers_list.no_result_title') + %p.empty-text-details + = t('views.users.dossiers.dossiers_list.no_result_text_with_filter') + %br + = link_to t('views.users.dossiers.dossiers_list.no_result_reset_filter'), dossiers_path(statut: statut), class: 'fr-btn fr-btn--sm fr-mt-2w' + + - else + .blank-tab + %h2.empty-text= t('views.users.dossiers.dossiers_list.no_result_title') + %p.empty-text-details + = t('views.users.dossiers.dossiers_list.no_result_text_html') diff --git a/app/views/users/dossiers/index.html.haml b/app/views/users/dossiers/index.html.haml index d29def972..8fcbe4fba 100644 --- a/app/views/users/dossiers/index.html.haml +++ b/app/views/users/dossiers/index.html.haml @@ -68,6 +68,8 @@ = render partial: "dossiers_list", locals: { dossiers: @dossiers } - else + = render Dossiers::UserFilterComponent.new(statut: @statut, filter: @filter) + - if @statut == "en-cours" - if @first_brouillon_recently_updated.present? = render Dsfr::CalloutComponent.new(title: t('users.dossiers.header.callout.first_brouillon_recently_updated_title'), heading_level: 'h2') do |c| @@ -76,13 +78,13 @@ = t('users.dossiers.header.callout.first_brouillon_recently_updated_text', time_ago: time_ago_in_words(@first_brouillon_recently_updated.created_at), libelle: @first_brouillon_recently_updated.procedure.libelle ) = link_to t('users.dossiers.header.callout.first_brouillon_recently_updated_button'), url_for_dossier(@first_brouillon_recently_updated), class: 'fr-btn' - = render partial: "dossiers_list", locals: { dossiers: @dossiers } + = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } - if @statut == "traites" - = render partial: "dossiers_list", locals: { dossiers: @dossiers } + = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } - if @statut == "dossiers-invites" - = render partial: "dossiers_list", locals: { dossiers: @dossiers } + = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } - if @statut == "dossiers-supprimes-recemment" = render partial: "hidden_dossiers_list", locals: { hidden_dossiers: @dossiers } @@ -95,4 +97,4 @@ = render partial: "transfered_dossiers_list", locals: { dossier_transfers: @dossiers } - if @statut == "dossiers-expirant" - = render partial: "dossiers_list", locals: { dossiers: @dossiers } + = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } diff --git a/config/locales/en.yml b/config/locales/en.yml index d043d4674..5b11d6a75 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -461,6 +461,10 @@ en: status: "Status" updated: "Updated" actions: "Actions" + no_result_title: No files + no_result_text_html: "To fill a procedure, contact your administration asking for the procedure link.
It should look like #{APPLICATION_BASE_URL}/commencer/xxx." + no_result_text_with_filter: found with selected filters + no_result_reset_filter: Reset filters transfers: sender_demande_en_cours: "A transfer request is pending on file Nº %{id} to %{email}" receiver_demande_en_cours: "Transfer request on file Nº %{id} sent by %{email}" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f59b3d6bb..672179445 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -462,6 +462,10 @@ fr: status: "Statut" updated: "Mis à jour" actions: "Actions" + no_result_title: Aucun dossier + no_result_text_html: "Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche.
Celui ci doit ressembler à #{APPLICATION_BASE_URL}/commencer/xxx." + no_result_text_with_filter: ne correspond aux filtres sélectionnés + no_result_reset_filter: Réinitialiser les filtres dossier_action: edit_dossier: "Modifier le dossier" start_other_dossier: "Commencer un autre dossier vide" diff --git a/spec/system/users/list_dossiers_spec.rb b/spec/system/users/list_dossiers_spec.rb index d44eb0711..c692f5279 100644 --- a/spec/system/users/list_dossiers_spec.rb +++ b/spec/system/users/list_dossiers_spec.rb @@ -4,6 +4,7 @@ describe 'user access to the list of their dossiers', js: true do let!(:dossier_en_construction) { create(:dossier, :with_populated_champs, :en_construction, user: user) } let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) } let!(:dossier_traite) { create(:dossier, :accepte, user: user) } + let!(:dossier_refuse) { create(:dossier, :refuse, user: user) } let!(:dossier_archived) { create(:dossier, :en_instruction, :archived, user: user) } let(:dossiers_per_page) { 25 } let(:last_updated_dossier) { dossier_en_construction } @@ -28,7 +29,7 @@ describe 'user access to the list of their dossiers', js: true do expect(page).to have_content(dossier_en_instruction.procedure.libelle) expect(page).to have_content(dossier_archived.procedure.libelle) expect(page).to have_text('4 en cours') - expect(page).to have_text('1 traité') + expect(page).to have_text('2 traités') end it 'the list must be ordered by last updated' do @@ -51,7 +52,82 @@ describe 'user access to the list of their dossiers', js: true do page.click_link("Suivant") expect(page).to have_content(dossier_en_instruction.procedure.libelle) expect(page).to have_text('4 en cours') - expect(page).to have_text('1 traité') + expect(page).to have_text('2 traités') + end + end + + context 'when user uses filter' do + scenario 'user filters state on tab "en-cours"' do + expect(page).to have_text('4 en cours') + expect(page).to have_text('2 traités') + expect(page).to have_text('4 sur 4 dossiers') + click_on('Sélectionner un filtre') + expect(page).to have_text('Brouillon') + expect(page).to have_text('En construction') + expect(page).to have_text('En instruction') + find("label", text: "Brouillon").click + click_on('Appliquer les filtres') + expect(page).to have_text('1 dossier') + expect(page).to have_checked_field('Brouillon') + end + + scenario 'user filters state on tab "traité"' do + visit dossiers_path(statut: 'traites') + expect(page).to have_text('4 en cours') + expect(page).to have_text('2 traités') + expect(page).to have_text('2 sur 2 dossiers') + + click_on('Sélectionner un filtre') + expect(page).to have_text('Accepté') + expect(page).to have_text('Refusé') + expect(page).to have_text('Classé sans suite') + find("label", text: "Refusé").click + click_on('Appliquer les filtres') + expect(page).to have_text('1 dossier') + expect(page).to have_checked_field('Refusé') + + click_on('Sélectionner un filtre') + click_on('Réinitialiser les filtres') + expect(page).to have_text('2 sur 2 dossiers') + expect(page).to have_unchecked_field('Refusé') + end + + scenario 'user filters by created_at' do + dossier_en_construction.update!(created_at: Date.yesterday) + + expect(page).to have_text('4 sur 4 dossiers') + click_on('Sélectionner un filtre') + fill_in 'from_created_at_date', with: Date.today + click_on('Appliquer les filtres') + expect(page).to have_text('3 sur 3 dossiers') + end + + scenario 'user uses multiple filters' do + dossier_en_construction.update!(created_at: Date.yesterday) + dossier_en_instruction.update!(depose_at: Date.yesterday) + + expect(page).to have_text('4 sur 4 dossiers') + click_on('Sélectionner un filtre') + fill_in 'from_created_at_date', with: Date.today + click_on('Appliquer les filtres') + expect(page).to have_text('3 sur 3 dossiers') + expect(page).to have_text('1 filtre actif') + + click_on('Sélectionner un filtre') + find("label", text: "En construction").click + find("label", text: "En instruction").click + click_on('Appliquer les filtres') + expect(page).to have_text('2 sur 2 dossiers') + expect(page).to have_text('3 filtres actifs') + + click_on('Sélectionner un filtre') + fill_in 'from_depose_at_date', with: Date.today + click_on('Appliquer les filtres') + expect(page).to have_text('1 dossier') + expect(page).to have_text('4 filtres actifs') + click_on('4 filtres actifs') + expect(page).to have_text('4 sur 4 dossiers') + expect(page).not_to have_text('4 filtres actifs') end end diff --git a/spec/views/users/dossiers/index.html.haml_spec.rb b/spec/views/users/dossiers/index.html.haml_spec.rb index 8b7678f1c..8cb30522f 100644 --- a/spec/views/users/dossiers/index.html.haml_spec.rb +++ b/spec/views/users/dossiers/index.html.haml_spec.rb @@ -6,6 +6,7 @@ describe 'users/dossiers/index', type: :view do let(:user_dossiers) { [dossier_brouillon, dossier_en_construction, dossier_termine] } let(:dossiers_invites) { [] } let(:statut) { 'en-cours' } + let(:filter) { DossiersFilter.new(user, ActionController::Parameters.new(random_param: 'random_param')) } before do allow(view).to receive(:new_demarche_url).and_return('#') @@ -19,6 +20,7 @@ describe 'users/dossiers/index', type: :view do assign(:dossiers_close_to_expiration, Kaminari.paginate_array([]).page(1)) assign(:dossiers, Kaminari.paginate_array(user_dossiers).page(1)) assign(:statut, statut) + assign(:filter, filter) render end