diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index a76c690a5..b96c83dc5 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -209,6 +209,7 @@ module Instructeurs @usual_traitement_time = @procedure.stats_usual_traitement_time @dossiers_funnel = @procedure.stats_dossiers_funnel @termines_states = @procedure.stats_termines_states + @termines_by_week = @procedure.stats_termines_by_week end private diff --git a/app/controllers/users/statistiques_controller.rb b/app/controllers/users/statistiques_controller.rb new file mode 100644 index 000000000..456a93572 --- /dev/null +++ b/app/controllers/users/statistiques_controller.rb @@ -0,0 +1,21 @@ +module Users + class StatistiquesController < ApplicationController + def statistiques + @procedure = procedure + return procedure_not_found if @procedure.blank? || @procedure.brouillon? + + @usual_traitement_time = @procedure.stats_usual_traitement_time + @dossiers_funnel = @procedure.stats_dossiers_funnel + @termines_states = @procedure.stats_termines_states + @termines_by_week = @procedure.stats_termines_by_week + + render :show + end + + private + + def procedure + Procedure.publiees.or(Procedure.brouillons).find_by(path: params[:path]) + end + end +end diff --git a/app/models/concerns/procedure_stats_concern.rb b/app/models/concerns/procedure_stats_concern.rb index 7e487c6a4..a98fe4e23 100644 --- a/app/models/concerns/procedure_stats_concern.rb +++ b/app/models/concerns/procedure_stats_concern.rb @@ -27,4 +27,22 @@ module ProcedureStatsConcern ] end end + + def stats_termines_by_week + Rails.cache.fetch("#{cache_key_with_version}/stats_termines_by_week", expires_in: 12.hours) do + now = Time.zone.now + chart_data = dossiers.joins(:traitements) + .state_termine + .where(traitements: { processed_at: (now.beginning_of_week - 6.months)..now.end_of_week }) + + dossier_state_values = chart_data.pluck(:state).sort.uniq + + # rubocop:disable Style/HashTransformValues + dossier_state_values + .map do |state| + { name: state, data: chart_data.where(state: state).group_by_week { |dossier| dossier.traitements.first.processed_at }.map { |k, v| [k, v.count] }.to_h } + # rubocop:enable Style/HashTransformValues + end + end + end end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index d457f2cf7..b81517ff3 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -8,6 +8,7 @@ # autorisation_donnees :boolean # brouillon_close_to_expiration_notice_sent_at :datetime # conservation_extension :interval default(0 seconds) +# declarative_triggered_at :datetime # deleted_user_email_never_send :string # en_construction_at :datetime # en_construction_close_to_expiration_notice_sent_at :datetime @@ -655,7 +656,9 @@ class Dossier < ApplicationRecord end def after_passer_automatiquement_en_instruction - update!(en_instruction_at: Time.zone.now) if self.en_instruction_at.nil? + self.en_instruction_at ||= Time.zone.now + self.declarative_triggered_at = Time.zone.now + save! log_automatic_dossier_operation(:passer_en_instruction) end @@ -695,6 +698,7 @@ class Dossier < ApplicationRecord def after_accepter_automatiquement self.traitements.build(state: Dossier.states.fetch(:accepte), instructeur_email: nil, motivation: nil, processed_at: Time.zone.now) self.en_instruction_at ||= Time.zone.now + self.declarative_triggered_at = Time.zone.now if attestation.nil? self.attestation = build_attestation diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 0aa534ad3..9f7325044 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -606,10 +606,12 @@ class Procedure < ApplicationRecord when Procedure.declarative_with_states.fetch(:en_instruction) dossiers .state_en_construction + .where(declarative_triggered_at: nil) .find_each(&:passer_automatiquement_en_instruction!) when Procedure.declarative_with_states.fetch(:accepte) dossiers .state_en_construction + .where(declarative_triggered_at: nil) .find_each(&:accepter_automatiquement!) end end diff --git a/app/views/instructeurs/procedures/stats.html.haml b/app/views/instructeurs/procedures/stats.html.haml index 28ea42acd..4d83e6d16 100644 --- a/app/views/instructeurs/procedures/stats.html.haml +++ b/app/views/instructeurs/procedures/stats.html.haml @@ -5,31 +5,4 @@ locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)), 'Statistiques'] } -.statistiques - -# Load Chartkick lazily, by using our React lazy-loader. - -# (Chartkick itself doesn't use React though) - = react_component('Chartkick') - - %h1.new-h1= title - .stat-cards - - if @usual_traitement_time.present? - .stat-card.big-number-card - %span.big-number-card-title TEMPS DE TRAITEMENT USUEL - %span.big-number-card-number - = distance_of_time_in_words(@usual_traitement_time) - %span.big-number-card-detail - 90% des demandes du mois dernier ont été traitées en moins de #{distance_of_time_in_words(@usual_traitement_time)}. - - .stat-cards - .stat-card.stat-card-half.pull-left - %span.stat-card-title AVANCÉE DES DOSSIERS - .chart-container - .chart - = area_chart @dossiers_funnel - - .stat-card.stat-card-half.pull-left - %span.stat-card-title TAUX D’ACCEPTATION - .chart-container - .chart - - colors = %w(#C3D9FF #0069CC #1C7EC9) # from _colors.scss - = pie_chart @termines_states, colors: colors += render partial: 'shared/procedures/stats', locals: { title: title } diff --git a/app/views/shared/procedures/_stats.html.haml b/app/views/shared/procedures/_stats.html.haml new file mode 100644 index 000000000..fb6925914 --- /dev/null +++ b/app/views/shared/procedures/_stats.html.haml @@ -0,0 +1,35 @@ +.statistiques + -# Load Chartkick lazily, by using our React lazy-loader. + -# (Chartkick itself doesn't use React though) + = react_component('Chartkick') + + %h1.new-h1= title + .stat-cards + - if @usual_traitement_time.present? + .stat-card.big-number-card + %span.big-number-card-title TEMPS DE TRAITEMENT USUEL + %span.big-number-card-number + = distance_of_time_in_words(@usual_traitement_time) + %span.big-number-card-detail + 90% des demandes du mois dernier ont été traitées en moins de #{distance_of_time_in_words(@usual_traitement_time)}. + + .stat-cards + .stat-card.stat-card-half.pull-left + %span.stat-card-title AVANCÉE DES DOSSIERS + .chart-container + .chart + = area_chart @dossiers_funnel + + .stat-card.stat-card-half.pull-left + %span.stat-card-title TAUX D’ACCEPTATION + .chart-container + .chart + - colors = %w(#C3D9FF #0069CC #1C7EC9) # from _colors.scss + = pie_chart @termines_states, colors: colors + + .stat-cards + .stat-card.stat-card-half.pull-left + %span.stat-card-title RÉPARTITION PAR SEMAINE + .chart-container + .chart + = line_chart @termines_by_week, colors: ["#387EC3", "#AE2C2B", "#FAD859"] diff --git a/app/views/users/_procedure_footer.html.haml b/app/views/users/_procedure_footer.html.haml index b225927c6..65c5f2f30 100644 --- a/app/views/users/_procedure_footer.html.haml +++ b/app/views/users/_procedure_footer.html.haml @@ -33,6 +33,10 @@ - horaires = "Horaires : #{formatted_horaires(service.horaires)}" = simple_format(horaires, {}, wrapper_tag: 'span') + %li + Statistiques : + = link_to "voir les statistiques de la démarche", statistiques_path(procedure.path) + - politiques = politiques_conservation_de_donnees(procedure) - if politiques.present? diff --git a/app/views/users/statistiques/show.html.haml b/app/views/users/statistiques/show.html.haml new file mode 100644 index 000000000..0de25c09c --- /dev/null +++ b/app/views/users/statistiques/show.html.haml @@ -0,0 +1,4 @@ +- title = "Statistiques · #{@procedure.libelle}" +- content_for(:title, title) + += render partial: 'shared/procedures/stats', locals: { title: title } diff --git a/config/routes.rb b/config/routes.rb index a876587b1..214001c4f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -248,6 +248,10 @@ Rails.application.routes.draw do # scope module: 'users' do + namespace :statistiques do + get '/:path', action: 'statistiques' + end + namespace :commencer do get '/test/:path/dossier_vide', action: 'dossier_vide_pdf_test', as: :dossier_vide_test get '/test/:path', action: 'commencer_test', as: :test diff --git a/db/migrate/20210604095054_add_declarative_triggered_at_to_dossiers.rb b/db/migrate/20210604095054_add_declarative_triggered_at_to_dossiers.rb new file mode 100644 index 000000000..25eb5a50b --- /dev/null +++ b/db/migrate/20210604095054_add_declarative_triggered_at_to_dossiers.rb @@ -0,0 +1,5 @@ +class AddDeclarativeTriggeredAtToDossiers < ActiveRecord::Migration[6.1] + def change + add_column :dossiers, :declarative_triggered_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 1f272774e..adf34108a 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.define(version: 2021_05_07_135603) do +ActiveRecord::Schema.define(version: 2021_06_04_095054) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -278,6 +278,7 @@ ActiveRecord::Schema.define(version: 2021_05_07_135603) do t.string "deleted_user_email_never_send" t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin + t.datetime "declarative_triggered_at" t.index ["archived"], name: "index_dossiers_on_archived" t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id" t.index ["hidden_at"], name: "index_dossiers_on_hidden_at" diff --git a/spec/jobs/cron/declarative_procedures_job_spec.rb b/spec/jobs/cron/declarative_procedures_job_spec.rb index f2ad8c486..f64210d7a 100644 --- a/spec/jobs/cron/declarative_procedures_job_spec.rb +++ b/spec/jobs/cron/declarative_procedures_job_spec.rb @@ -9,14 +9,17 @@ RSpec.describe Cron::DeclarativeProceduresJob, type: :job do let(:nouveau_dossier2) { create(:dossier, :en_construction, :with_individual, procedure: procedure) } let(:dossier_recu) { create(:dossier, :en_instruction, :with_individual, procedure: procedure) } let(:dossier_brouillon) { create(:dossier, procedure: procedure) } + let(:dossier_repasse_en_construction) { create(:dossier, :en_construction, :with_individual, procedure: procedure) } before do Timecop.freeze(date) + dossier_repasse_en_construction.touch(:declarative_triggered_at) dossiers = [ nouveau_dossier1, nouveau_dossier2, dossier_recu, - dossier_brouillon + dossier_brouillon, + dossier_repasse_en_construction ] create(:attestation_template, procedure: procedure) @@ -33,19 +36,21 @@ RSpec.describe Cron::DeclarativeProceduresJob, type: :job do let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last } it { - expect(nouveau_dossier1.en_instruction?).to be true + expect(nouveau_dossier1.en_instruction?).to be_truthy expect(nouveau_dossier1.en_instruction_at).to eq(date) expect(last_operation.operation).to eq('passer_en_instruction') expect(last_operation.automatic_operation?).to be_truthy - expect(nouveau_dossier2.en_instruction?).to be true + expect(nouveau_dossier2.en_instruction?).to be_truthy expect(nouveau_dossier2.en_instruction_at).to eq(date) - expect(dossier_recu.en_instruction?).to be true + expect(dossier_recu.en_instruction?).to be_truthy expect(dossier_recu.en_instruction_at).to eq(instruction_date) - expect(dossier_brouillon.brouillon?).to be true + expect(dossier_brouillon.brouillon?).to be_truthy expect(dossier_brouillon.en_instruction_at).to eq(nil) + + expect(dossier_repasse_en_construction.en_construction?).to be_truthy } end diff --git a/spec/views/users/statistiques/show.html.haml_spec.rb b/spec/views/users/statistiques/show.html.haml_spec.rb new file mode 100644 index 000000000..17ed52a7b --- /dev/null +++ b/spec/views/users/statistiques/show.html.haml_spec.rb @@ -0,0 +1,16 @@ +describe 'users/statistiques/show.html.haml', type: :view do + let(:procedure) { create(:procedure) } + + before do + assign(:procedure, procedure) + end + + subject { render } + + it "display stats" do + expect(subject).to have_text("RÉPARTITION PAR SEMAINE") + expect(subject).to have_text("AVANCÉE DES DOSSIERS") + expect(subject).to have_text("TAUX D’ACCEPTATION") + expect(subject).to have_text(procedure.libelle) + end +end