diff --git a/app/assets/stylesheets/buttons.scss b/app/assets/stylesheets/buttons.scss index 53fe0130c..4a3e7fcac 100644 --- a/app/assets/stylesheets/buttons.scss +++ b/app/assets/stylesheets/buttons.scss @@ -397,3 +397,7 @@ ul.dropdown-items { content: none !important; } } + +.back-btn { + line-height: 1.75rem; +} diff --git a/app/components/instructeurs/back_button_component.rb b/app/components/instructeurs/back_button_component.rb index 316a05d3d..ae373aaf8 100644 --- a/app/components/instructeurs/back_button_component.rb +++ b/app/components/instructeurs/back_button_component.rb @@ -6,6 +6,8 @@ class Instructeurs::BackButtonComponent < ApplicationComponent end def call - link_to "", @to, class: 'back-btn fr-btn fr-btn--secondary fr-btn--sm fr-mr-2w fr-icon-arrow-left-line', title: t('.back') + link_to @to, class: 'back-btn fr-btn fr-btn--secondary fr-btn--sm fr-mr-2w fr-px-1v', title: t('.back') do + dsfr_icon("fr-icon-arrow-left-line") + end end end diff --git a/app/components/instructeurs/dossiers_navigation_component.rb b/app/components/instructeurs/dossiers_navigation_component.rb new file mode 100644 index 000000000..71d83b6bc --- /dev/null +++ b/app/components/instructeurs/dossiers_navigation_component.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Instructeurs::DossiersNavigationComponent < ApplicationComponent + attr_reader :dossier, :statut + + def initialize(dossier:, procedure_presentation:, statut:) + @dossier = dossier + @cache = Cache::ProcedureDossierPagination.new(procedure_presentation: procedure_presentation, statut:) + @statut = statut + end + + def back_url_options + options = { statut: } + options = options.merge(page: @cache.incoming_page) if @cache.incoming_page + options + end + + def link_next + if has_next? + html_tag = :a + options = { class: "fr-link no-wrap fr-ml-3w", href: next_instructeur_dossier_path(dossier:, statut:) } + else + html_tag = :span + options = { class: "fr-link no-wrap fr-ml-3w fr-text-mention--grey" } + end + + tag.send(html_tag, t('.next').html_safe + tag.span(class: 'fr-icon-arrow-right-line fr-ml-1w'), **options) + end + + def link_previous + if has_previous? + html_tag = :a + options = { class: "fr-link no-wrap", href: previous_instructeur_dossier_path(dossier:, statut:) } + else + html_tag = :span + options = { class: "fr-link no-wrap fr-text-mention--grey" } + end + + tag.send(html_tag, tag.span(class: 'fr-icon-arrow-left-line fr-mr-1w') + t('.previous'), **options) + end + + def has_next? = @has_next ||= @cache.next_dossier_id(from_id: dossier.id).present? + + def has_previous? = @has_previous ||= @cache.previous_dossier_id(from_id: dossier.id).present? +end diff --git a/app/components/instructeurs/dossiers_navigation_component/dossiers_navigation_component.en.yml b/app/components/instructeurs/dossiers_navigation_component/dossiers_navigation_component.en.yml new file mode 100644 index 000000000..697df0c32 --- /dev/null +++ b/app/components/instructeurs/dossiers_navigation_component/dossiers_navigation_component.en.yml @@ -0,0 +1,4 @@ +--- +en: + next: Next file + previous: Previous file \ No newline at end of file diff --git a/app/components/instructeurs/dossiers_navigation_component/dossiers_navigation_component.fr.yml b/app/components/instructeurs/dossiers_navigation_component/dossiers_navigation_component.fr.yml new file mode 100644 index 000000000..05758d6ff --- /dev/null +++ b/app/components/instructeurs/dossiers_navigation_component/dossiers_navigation_component.fr.yml @@ -0,0 +1,4 @@ +--- +fr: + next: Dossier suivant + previous: Dossier précédent \ No newline at end of file diff --git a/app/components/instructeurs/dossiers_navigation_component/dossiers_navigation_component.html.haml b/app/components/instructeurs/dossiers_navigation_component/dossiers_navigation_component.html.haml new file mode 100644 index 000000000..e95fe3b13 --- /dev/null +++ b/app/components/instructeurs/dossiers_navigation_component/dossiers_navigation_component.html.haml @@ -0,0 +1,11 @@ +.flex.fr-mb-1w.align-start + = render Instructeurs::BackButtonComponent.new(to: instructeur_procedure_path(dossier.procedure, **back_url_options)) + + %h1.fr-h3.fr-mb-0 + = t('show_dossier', scope: [:layouts, :breadcrumb], dossier_id: dossier.id, owner_name: dossier.owner_name) + + .fr.ml-auto.align-center.flex.fr-mt-1v + = link_previous + = link_next + += link_to dossier.procedure.libelle.truncate_words(10), instructeur_procedure_path(dossier.procedure), title: dossier.procedure.libelle, class: "fr-link" diff --git a/app/controllers/concerns/instructeur_concern.rb b/app/controllers/concerns/instructeur_concern.rb new file mode 100644 index 000000000..6ccc57c4e --- /dev/null +++ b/app/controllers/concerns/instructeur_concern.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module InstructeurConcern + extend ActiveSupport::Concern + + included do + def retrieve_procedure_presentation + @procedure_presentation ||= current_instructeur.procedure_presentation_for_procedure_id(params[:procedure_id]) + end + end +end diff --git a/app/controllers/instructeurs/commentaires_controller.rb b/app/controllers/instructeurs/commentaires_controller.rb index 38855904e..f8d71d166 100644 --- a/app/controllers/instructeurs/commentaires_controller.rb +++ b/app/controllers/instructeurs/commentaires_controller.rb @@ -2,10 +2,12 @@ module Instructeurs class CommentairesController < ApplicationController + include InstructeurConcern before_action :authenticate_instructeur_or_expert! after_action :mark_messagerie_as_read def destroy + retrieve_procedure_presentation if current_instructeur if commentaire.sent_by?(current_instructeur) || commentaire.sent_by?(current_expert) commentaire.soft_delete! diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 0d1bf2600..89c34d2fb 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -7,15 +7,16 @@ module Instructeurs include CreateAvisConcern include DossierHelper include TurboChampsConcern - + include InstructeurConcern include ActionController::Streaming 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] before_action :set_gallery_attachments, only: [:show, :pieces_jointes, :annotations_privees, :avis, :messagerie, :personnes_impliquees, :reaffectation] - after_action :mark_demande_as_read, only: :show + before_action :retrieve_procedure_presentation, only: [:annotations_privees, :avis_new, :avis, :messagerie, :personnes_impliquees, :pieces_jointes, :reaffectation, :show, :dossier_labels, :passer_en_instruction, :repasser_en_construction, :repasser_en_instruction, :terminer, :pending_correction, :create_avis, :create_commentaire] + after_action :mark_demande_as_read, only: :show 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] @@ -390,8 +391,40 @@ module Instructeurs @pieces_jointes_seen_at = current_instructeur.follows.find_by(dossier: dossier)&.pieces_jointes_seen_at end + def next + navigate_through_dossiers_list do |cache| + cache.next_dossier_id(from_id: params[:dossier_id]) + end + end + + def previous + navigate_through_dossiers_list do |cache| + cache.previous_dossier_id(from_id: params[:dossier_id]) + end + end + private + def navigate_through_dossiers_list + dossier = dossier_scope.find(params[:dossier_id]) + procedure_presentation = current_instructeur.procedure_presentation_for_procedure_id(dossier.procedure.id) + cache = Cache::ProcedureDossierPagination.new(procedure_presentation:, statut: params[:statut]) + + next_or_previous_dossier_id = yield(cache) + + if next_or_previous_dossier_id + redirect_to instructeur_dossier_path(procedure_id: procedure.id, dossier_id: next_or_previous_dossier_id, statut: params[:statut]) + else + redirect_back fallback_location: instructeur_dossier_path(procedure_id: procedure.id, dossier_id: dossier.id, statut: params[:statut]), alert: "Une erreur est survenue" + end + rescue ActiveRecord::RecordNotFound + Sentry.capture_message( + "Navigation through dossier failed => ActiveRecord::RecordNotFound", + extra: { dossier_id: params[:dossier_id] } + ) + redirect_to instructeur_procedure_path(procedure_id: procedure.id), alert: "Une erreur est survenue" + end + def dossier_scope if action_name == 'update_annotations' Dossier diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index df6e11684..d06a3b4fb 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -122,6 +122,8 @@ module Instructeurs .where(groupe_instructeurs: current_instructeur.groupe_instructeurs.where(procedure_id: @procedure.id)) .where(seen_at: nil) .distinct + + cache_show_procedure_state # don't move in callback, inherited by Instructeurs::DossiersController end def deleted_dossiers @@ -392,5 +394,11 @@ module Instructeurs def ordered_procedure_ids_params params.require(:ordered_procedure_ids) end + + def cache_show_procedure_state + cache = Cache::ProcedureDossierPagination.new(procedure_presentation:, statut:) + + cache.save_context(ids: @filtered_sorted_paginated_ids, incoming_page: params[:page]) + end end end diff --git a/app/models/cache/procedure_dossier_pagination.rb b/app/models/cache/procedure_dossier_pagination.rb new file mode 100644 index 000000000..cccd0e6da --- /dev/null +++ b/app/models/cache/procedure_dossier_pagination.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +class Cache::ProcedureDossierPagination + HALF_WINDOW = 50 + TRESHOLD_BEFORE_REFRESH = 1 + CACHE_EXPIRACY = 8.hours + + attr_reader :procedure_presentation, :statut, :cache + delegate :procedure, :instructeur, to: :procedure_presentation + + def initialize(procedure_presentation:, statut:) + @procedure_presentation = procedure_presentation + @statut = statut + @cache = Kredis.json(cache_key, expires_in: CACHE_EXPIRACY) + end + + def save_context(ids:, incoming_page:) + value = { ids: } + value[:incoming_page] = incoming_page if incoming_page + write_cache(value) + end + + def next_dossier_id(from_id:) + index = ids&.index(from_id.to_i) + + return nil if index.nil? # not found + + if refresh_cache_after?(from_id:) + renew_ids(from_id:) + index = ids.index(from_id.to_i) + end + return nil if index.blank? + return nil if index + 1 > ids.size # out of bound end + + ids[index + 1] + end + + def previous_dossier_id(from_id:) + index = ids&.index(from_id.to_i) + + return nil if index.nil? # not found + + if refresh_cache_before?(from_id:) + renew_ids(from_id:) + index = ids.index(from_id.to_i) + end + return nil if index.blank? + return nil if index - 1 < 0 # out of bound start + + ids[index - 1] + end + + def incoming_page + read_cache[:incoming_page] + end + + private + + def cache_key + [procedure.id, instructeur.id, statut].join(":") + end + + def write_cache(value) + cache.value = value + @read_cache = nil + end + + def read_cache + @read_cache ||= Hash(cache.value).with_indifferent_access + end + + def ids = read_cache[:ids] + + def refresh_cache_after?(from_id:) = from_id.in?(ids.last(TRESHOLD_BEFORE_REFRESH)) + + def refresh_cache_before?(from_id:) = from_id.in?(ids.first(TRESHOLD_BEFORE_REFRESH)) + + def renew_ids(from_id:) + value = read_cache + value[:ids] = fetch_ids_around(from_id:) + + write_cache(value) + end + + def fetch_all_ids + dossiers = Dossier.where(groupe_instructeur_id: GroupeInstructeur.joins(:instructeurs, :procedure).where(procedure: procedure, instructeurs: [instructeur]).select(:id)) + DossierFilterService.filtered_sorted_ids(dossiers, statut, procedure_presentation.filters_for(statut), procedure_presentation.sorted_column, instructeur, count: 0) + end + + def fetch_ids_around(from_id:) + all_ids = fetch_all_ids + from_id_at = all_ids.index(from_id) + + if from_id_at.present? + new_page_starts_at = [0, from_id_at - HALF_WINDOW].max # avoid index below 0 + new_page_ends_at = [from_id_at + HALF_WINDOW, all_ids.size].min # avoid index above all_ids.size + all_ids.slice(new_page_starts_at, new_page_ends_at) + else + [] + end + end +end diff --git a/app/models/instructeur.rb b/app/models/instructeur.rb index 83f035d44..8efe12970 100644 --- a/app/models/instructeur.rb +++ b/app/models/instructeur.rb @@ -127,12 +127,14 @@ class Instructeur < ApplicationRecord end end + def procedure_presentation_for_procedure_id(procedure_id) + assign_to = assign_to_for_procedure_id(procedure_id) + assign_to.procedure_presentation || assign_to.create_procedure_presentation! + end + def procedure_presentation_and_errors_for_procedure_id(procedure_id) - assign_to - .joins(:groupe_instructeur) - .includes(:instructeur, :procedure) - .find_by(groupe_instructeurs: { procedure_id: procedure_id }) - .procedure_presentation_or_default_and_errors + assign_to = assign_to_for_procedure_id(procedure_id) + assign_to.procedure_presentation_or_default_and_errors end def notifications_for_dossier(dossier) @@ -355,4 +357,11 @@ class Instructeur < ApplicationRecord .merge(followed_dossiers) .with_notifications end + + def assign_to_for_procedure_id(procedure_id) + assign_to + .joins(:groupe_instructeur) + .includes(:instructeur, :procedure) + .find_by(groupe_instructeurs: { procedure_id: procedure_id }) + end end diff --git a/app/views/instructeurs/commentaires/destroy.turbo_stream.haml b/app/views/instructeurs/commentaires/destroy.turbo_stream.haml index afe148257..610ded463 100644 --- a/app/views/instructeurs/commentaires/destroy.turbo_stream.haml +++ b/app/views/instructeurs/commentaires/destroy.turbo_stream.haml @@ -3,4 +3,4 @@ = render Dossiers::MessageComponent.new(commentaire: @commentaire, connected_user: @commentaire.instructeur || @commentaire.expert) - if current_user.instructeur? && @commentaire.dossier_correction.present? - = turbo_stream.replace 'header-top', partial: 'instructeurs/dossiers/header_top', locals: { dossier: @commentaire.dossier } + = turbo_stream.replace 'header-top', partial: 'instructeurs/dossiers/header_top', locals: { dossier: @commentaire.dossier, procedure_presentation: @procedure_presentation } diff --git a/app/views/instructeurs/dossiers/_header.html.haml b/app/views/instructeurs/dossiers/_header.html.haml index 0a0a1607a..d449e3033 100644 --- a/app/views/instructeurs/dossiers/_header.html.haml +++ b/app/views/instructeurs/dossiers/_header.html.haml @@ -10,7 +10,7 @@ locals: { steps: [[t('show_procedure', scope: [:layouts, :breadcrumb], libelle: dossier.procedure.libelle.truncate(22)), instructeur_procedure_path(dossier.procedure)], [t('show_dossier', scope: [:layouts, :breadcrumb], dossier_id: dossier.id, owner_name: dossier.owner_name)]] } - = render partial: 'instructeurs/dossiers/header_top', locals: { dossier: } + = render partial: 'instructeurs/dossiers/header_top', locals: { dossier:, procedure_presentation: } = render partial: 'instructeurs/dossiers/header_bottom', locals: { dossier:, gallery_attachments: } .fr-container diff --git a/app/views/instructeurs/dossiers/_header_top.html.haml b/app/views/instructeurs/dossiers/_header_top.html.haml index 45805fbb6..366cb4498 100644 --- a/app/views/instructeurs/dossiers/_header_top.html.haml +++ b/app/views/instructeurs/dossiers/_header_top.html.haml @@ -1,14 +1,9 @@ #header-top.fr-container - .flex + = render Instructeurs::DossiersNavigationComponent.new(dossier:, procedure_presentation:, statut: params[:statut]) + + .flex.fr-mb-3w %div - .flex.fr-mb-1w - = render Instructeurs::BackButtonComponent.new(to: instructeur_procedure_path(dossier.procedure, statut: params[:statut])) - %h1.fr-h3.fr-mb-1w - = t('show_dossier', scope: [:layouts, :breadcrumb], dossier_id: dossier.id, owner_name: dossier.owner_name) - - - = link_to dossier.procedure.libelle.truncate_words(10), instructeur_procedure_path(dossier.procedure), title: dossier.procedure.libelle, class: "fr-link" - .fr-mt-2w.fr-badge-group + .fr-mt-2w.badge-group = procedure_badge(dossier.procedure) = status_badge(dossier.state) diff --git a/app/views/instructeurs/dossiers/annotations_privees.html.haml b/app/views/instructeurs/dossiers/annotations_privees.html.haml index f603d753a..61708ae5e 100644 --- a/app/views/instructeurs/dossiers/annotations_privees.html.haml +++ b/app/views/instructeurs/dossiers/annotations_privees.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Annotations privées · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments, procedure_presentation: @procedure_presentation } #dossier-annotations-privees .fr-container diff --git a/app/views/instructeurs/dossiers/avis.html.haml b/app/views/instructeurs/dossiers/avis.html.haml index 172816591..4e81a8110 100644 --- a/app/views/instructeurs/dossiers/avis.html.haml +++ b/app/views/instructeurs/dossiers/avis.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Avis · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments, procedure_presentation: @procedure_presentation } .container .fr-grid-row diff --git a/app/views/instructeurs/dossiers/avis_new.html.haml b/app/views/instructeurs/dossiers/avis_new.html.haml index 5b9125a98..5d6a78956 100644 --- a/app/views/instructeurs/dossiers/avis_new.html.haml +++ b/app/views/instructeurs/dossiers/avis_new.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Avis · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments, procedure_presentation: @procedure_presentation } .container .fr-grid-row diff --git a/app/views/instructeurs/dossiers/change_state.turbo_stream.haml b/app/views/instructeurs/dossiers/change_state.turbo_stream.haml index d7dd8ded2..5dd2e0d9f 100644 --- a/app/views/instructeurs/dossiers/change_state.turbo_stream.haml +++ b/app/views/instructeurs/dossiers/change_state.turbo_stream.haml @@ -1 +1 @@ -= turbo_stream.replace 'header-top', partial: 'header_top', locals: { dossier: @dossier } += turbo_stream.replace 'header-top', partial: 'header_top', locals: { dossier: @dossier, procedure_presentation: @procedure_presentation } diff --git a/app/views/instructeurs/dossiers/messagerie.html.haml b/app/views/instructeurs/dossiers/messagerie.html.haml index 33395d7f1..28adc6020 100644 --- a/app/views/instructeurs/dossiers/messagerie.html.haml +++ b/app/views/instructeurs/dossiers/messagerie.html.haml @@ -1,5 +1,5 @@ - content_for(:title, "Messagerie · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments, procedure_presentation: @procedure_presentation } = render partial: "shared/dossiers/messagerie", locals: { dossier: @dossier, connected_user: current_instructeur, messagerie_seen_at: @messagerie_seen_at , new_commentaire: @commentaire, form_url: commentaire_instructeur_dossier_path(@dossier.procedure, @dossier, statut: params[:statut]) } diff --git a/app/views/instructeurs/dossiers/personnes_impliquees.html.haml b/app/views/instructeurs/dossiers/personnes_impliquees.html.haml index 30d3224ec..4472d0999 100644 --- a/app/views/instructeurs/dossiers/personnes_impliquees.html.haml +++ b/app/views/instructeurs/dossiers/personnes_impliquees.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Personnes impliquées · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments, procedure_presentation: @procedure_presentation } .personnes-impliquees.container = render partial: 'instructeurs/dossiers/envoyer_dossier_block', locals: { dossier: @dossier, potential_recipients: @potential_recipients } diff --git a/app/views/instructeurs/dossiers/pieces_jointes.html.haml b/app/views/instructeurs/dossiers/pieces_jointes.html.haml index 527b2c65c..94ef1015a 100644 --- a/app/views/instructeurs/dossiers/pieces_jointes.html.haml +++ b/app/views/instructeurs/dossiers/pieces_jointes.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Pièces jointes") -= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments, procedure_presentation: @procedure_presentation } .fr-container .gallery.gallery-pieces-jointes{ "data-controller": "lightbox" } diff --git a/app/views/instructeurs/dossiers/reaffectation.html.haml b/app/views/instructeurs/dossiers/reaffectation.html.haml index 364b5415a..3c0038f11 100644 --- a/app/views/instructeurs/dossiers/reaffectation.html.haml +++ b/app/views/instructeurs/dossiers/reaffectation.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Réaffectation · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments, procedure_presentation: @procedure_presentation } .container.groupe-instructeur diff --git a/app/views/instructeurs/dossiers/show.html.haml b/app/views/instructeurs/dossiers/show.html.haml index 8c9ad55e4..0bd246e42 100644 --- a/app/views/instructeurs/dossiers/show.html.haml +++ b/app/views/instructeurs/dossiers/show.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Demande · Dossier nº #{@dossier.id} (#{@dossier.owner_name})") -= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments } += render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments, procedure_presentation: @procedure_presentation } - if @dossier.etablissement&.as_degraded_mode? diff --git a/app/views/instructeurs/procedures/_header.html.haml b/app/views/instructeurs/procedures/_header.html.haml index 3f8f5e199..1209e15a7 100644 --- a/app/views/instructeurs/procedures/_header.html.haml +++ b/app/views/instructeurs/procedures/_header.html.haml @@ -1,5 +1,5 @@ .procedure-header - .clipboard-container.flex + .align-start.flex = render Instructeurs::BackButtonComponent.new(to: instructeur_procedures_path) %h1.fr-h3.fr-mb-0 = "#{procedure_libelle procedure} (n°#{procedure.id})" diff --git a/config/routes.rb b/config/routes.rb index 73014cab8..aba3a0e80 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -485,6 +485,8 @@ Rails.application.routes.draw do resources :dossiers, only: [:show, :destroy], param: :dossier_id, path: "(:statut)/dossiers", defaults: { statut: 'a-suivre' } do member do resources :commentaires, only: [:destroy] + get 'next' + get 'previous' post 'repousser-expiration' => 'dossiers#extend_conservation' post 'repousser-expiration-and-restore' => 'dossiers#extend_conservation_and_restore' post 'dossier_labels' => 'dossiers#dossier_labels' diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index aa0f53c4a..7ca846868 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -984,6 +984,68 @@ describe Instructeurs::DossiersController, type: :controller do end end + describe 'navigation accross next/prev dossiers' do + let(:dossier_id) { dossier.id } + let(:statut) { 'a-suivre' } + let(:previous_dossier) { create(:dossier, :en_construction, procedure:) } + let(:next_dossier) { create(:dossier, :en_construction, procedure:) } + let(:cached_ids) { [previous_dossier, dossier, next_dossier].map(&:id) } + before do + cache = Cache::ProcedureDossierPagination.new(procedure_presentation: double(procedure:, instructeur:), statut:) + cache.save_context(incoming_page: 1, ids: cached_ids) + end + + context 'when nexting' do + subject { get :next, params: { procedure_id: procedure.id, dossier_id: from_id, statut: } } + + context 'when their is a next id' do + let(:from_id) { dossier.id } + it { is_expected.to redirect_to(instructeur_dossier_path(procedure_id: procedure.id, dossier_id: next_dossier.id)) } + end + + context 'when their is not next id (en of list)' do + let(:from_id) { cached_ids.last } + it 'redirect on fallback location being current dossier and flashes an error' do + expect(subject).to redirect_to(instructeur_dossier_path(procedure_id: procedure.id, dossier_id: from_id)) + expect(flash.alert).to eq("Une erreur est survenue") + end + end + + context 'when id does not exists' do + let(:from_id) { 'kthxbye' } + it 'redirect on fallback location being current dossier and flashes an error' do + expect(subject).to redirect_to(instructeur_procedure_path(procedure_id: procedure.id)) + expect(flash.alert).to eq("Une erreur est survenue") + end + end + end + + context 'when previousing' do + subject { get :previous, params: { procedure_id: procedure.id, dossier_id: from_id, statut: } } + + context 'when their is a previous id' do + let(:from_id) { dossier.id } + it { is_expected.to redirect_to(instructeur_dossier_path(procedure_id: procedure.id, dossier_id: previous_dossier.id)) } + end + + context 'when their is not previous id (before list)' do + let(:from_id) { cached_ids.first } + it 'redirect on fallback location being current dossier and flashes an error' do + expect(subject).to redirect_to(instructeur_dossier_path(procedure_id: procedure.id, dossier_id: from_id)) + expect(flash.alert).to eq("Une erreur est survenue") + end + end + + context 'when id does not exists' do + let(:from_id) { 'kthxbye' } + it 'redirect on fallback location being current dossier and flashes an error' do + expect(subject).to redirect_to(instructeur_procedure_path(procedure_id: procedure.id)) + expect(flash.alert).to eq("Une erreur est survenue") + end + end + end + end + describe "#update_annotations" do let(:procedure) do create(:procedure, :published, types_de_champ_public:, types_de_champ_private:, instructeurs: instructeurs) diff --git a/spec/controllers/instructeurs/procedures_controller_spec.rb b/spec/controllers/instructeurs/procedures_controller_spec.rb index 389e849bb..85a0ea9e2 100644 --- a/spec/controllers/instructeurs/procedures_controller_spec.rb +++ b/spec/controllers/instructeurs/procedures_controller_spec.rb @@ -670,6 +670,18 @@ describe Instructeurs::ProceduresController, type: :controller do end end end + + describe 'caches statut and page query param' do + let(:statut) { 'tous' } + let(:page) { '1' } + let!(:dossier) { create(:dossier, :accepte, procedure:) } + before { sign_in(instructeur.user) } + subject { get :show, params: { procedure_id: procedure.id, statut:, page: } } + it 'changes cached value' do + expect { subject }.to change { Cache::ProcedureDossierPagination.new(statut:, procedure_presentation: double(procedure:, instructeur:)).send(:read_cache) } + .from({}).to(ids: [dossier.id], incoming_page: page) + end + end end describe '#deleted_dossiers' do diff --git a/spec/models/cache/procedure_dossier_pagination_spec.rb b/spec/models/cache/procedure_dossier_pagination_spec.rb new file mode 100644 index 000000000..a179d0372 --- /dev/null +++ b/spec/models/cache/procedure_dossier_pagination_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +describe Cache::ProcedureDossierPagination do + let(:instructeur) { double(id: 1) } + let(:procedure) { double(id: 1) } + let(:procedure_presentation) { double(instructeur:, procedure:) } + let(:instance) { described_class.new(procedure_presentation:, statut: 'a-suivre') } + + before do + instance.save_context(ids: cached_ids, incoming_page: nil) + end + + describe 'next_dossier_id' do + context 'when procedure.dossiers.by_statut has only one page' do + let(:cached_ids) { [3, 4] } + before do + allow(instance).to receive(:fetch_all_ids).and_return(cached_ids) + end + + it 'find next until the end' do + expect(instance.next_dossier_id(from_id: cached_ids.last)).to eq(nil) + expect(instance.next_dossier_id(from_id: cached_ids.first)).to eq(cached_ids.last) + end + end + + context 'when procedure.dossiers.by_statut has more than one page' do + let(:cached_ids) { [2, 3, 4] } + let(:next_page_ids) { (0..10).to_a } + + subject { instance.next_dossier_id(from_id: cached_ids.last) } + before do + allow(instance).to receive(:fetch_all_ids).and_return(next_page_ids) + end + + it 'refreshes paginated_ids' do + expect { subject }.to change { instance.send(:ids) }.from(cached_ids).to(next_page_ids) + end + end + + context 'when procedure.dossiers.by_statut does not include searched dossiers anymore' do + let(:cached_ids) { [] } + let(:next_page_ids) { [] } + before { allow(instance).to receive(:fetch_all_ids).and_return(next_page_ids) } + + it 'works' do + expect(instance.next_dossier_id(from_id: 50)).to eq(nil) + end + end + end + + describe 'previous_dossier_id' do + context 'when procedure.dossiers.by_statut has only one page' do + let(:cached_ids) { [3, 4] } + before do + allow(instance).to receive(:fetch_all_ids).and_return(cached_ids) + end + + it 'find next until the end' do + expect(instance.previous_dossier_id(from_id: cached_ids.last)).to eq(cached_ids.first) + expect(instance.previous_dossier_id(from_id: cached_ids.first)).to eq(nil) + end + end + + context 'when procedure.dossiers.by_statut has more than one page' do + let(:cached_ids) { [11, 12, 13] } + subject { instance.previous_dossier_id(from_id: cached_ids.first) } + let(:next_page_ids) { (11..20).to_a } + before do + allow(instance).to receive(:fetch_all_ids).and_return(next_page_ids) + end + + it 'works' do + expect { subject }.to change { instance.send(:ids) }.from(cached_ids).to(next_page_ids) + end + end + + context 'when procedure.dossiers.by_statut does not include searched dossiers anymore' do + let(:cached_ids) { [] } + before { allow(instance).to receive(:fetch_all_ids).and_return([]) } + + it 'works' do + expect(instance.previous_dossier_id(from_id: 50)).to eq(nil) + end + end + end +end diff --git a/spec/views/instructeur/dossiers/annotations_privee.html.haml_spec.rb b/spec/views/instructeur/dossiers/annotations_privee.html.haml_spec.rb index c5a01535e..4b9690cba 100644 --- a/spec/views/instructeur/dossiers/annotations_privee.html.haml_spec.rb +++ b/spec/views/instructeur/dossiers/annotations_privee.html.haml_spec.rb @@ -3,12 +3,15 @@ describe 'instructeurs/dossiers/annotations_privees', type: :view do let(:current_instructeur) { create(:instructeur) } let(:dossier) { create(:dossier, :en_construction) } + let(:procedure_presentation) { double(instructeur: current_instructeur, procedure: dossier.procedure) } before do sign_in(current_instructeur.user) allow(view).to receive(:current_instructeur).and_return(current_instructeur) + allow(controller).to receive(:params).and_return({ statut: 'a-suivre' }) assign(:dossier, dossier) + assign(:procedure_presentation, procedure_presentation) end subject { render } diff --git a/spec/views/instructeur/dossiers/show.html.haml_spec.rb b/spec/views/instructeur/dossiers/show.html.haml_spec.rb index 6414fbc77..0eea3dc15 100644 --- a/spec/views/instructeur/dossiers/show.html.haml_spec.rb +++ b/spec/views/instructeur/dossiers/show.html.haml_spec.rb @@ -3,12 +3,15 @@ describe 'instructeurs/dossiers/show', type: :view do let(:current_instructeur) { create(:instructeur) } let(:dossier) { create(:dossier, :en_construction) } + let(:statut) { { statut: 'tous' } } + let(:procedure_presentation) { double(instructeur: current_instructeur, procedure: dossier.procedure) } before do sign_in(current_instructeur.user) allow(view).to receive(:current_instructeur).and_return(current_instructeur) - allow(controller).to receive(:params).and_return(statut: 'a-suivre') + allow(controller).to receive(:params).and_return(statut:) assign(:dossier, dossier) + assign(:procedure_presentation, procedure_presentation) end subject { render } @@ -17,6 +20,12 @@ describe 'instructeurs/dossiers/show', type: :view do expect(subject).to have_text("Dossier nº #{dossier.id}") end + context 'when procedure statut / page was saved in session' do + it 'renders back button with saved state' do + expect(subject).to have_selector("a[href=\"#{instructeur_procedure_path(dossier.procedure, statut: statut)}\"]") + end + end + it 'renders the dossier infos' do expect(subject).to have_text('Identité') expect(subject).to have_text('Demande')