commit
547f165b24
14 changed files with 128 additions and 35 deletions
|
@ -209,6 +209,7 @@ module Instructeurs
|
||||||
@usual_traitement_time = @procedure.stats_usual_traitement_time
|
@usual_traitement_time = @procedure.stats_usual_traitement_time
|
||||||
@dossiers_funnel = @procedure.stats_dossiers_funnel
|
@dossiers_funnel = @procedure.stats_dossiers_funnel
|
||||||
@termines_states = @procedure.stats_termines_states
|
@termines_states = @procedure.stats_termines_states
|
||||||
|
@termines_by_week = @procedure.stats_termines_by_week
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
21
app/controllers/users/statistiques_controller.rb
Normal file
21
app/controllers/users/statistiques_controller.rb
Normal file
|
@ -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
|
|
@ -27,4 +27,22 @@ module ProcedureStatsConcern
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
# autorisation_donnees :boolean
|
# autorisation_donnees :boolean
|
||||||
# brouillon_close_to_expiration_notice_sent_at :datetime
|
# brouillon_close_to_expiration_notice_sent_at :datetime
|
||||||
# conservation_extension :interval default(0 seconds)
|
# conservation_extension :interval default(0 seconds)
|
||||||
|
# declarative_triggered_at :datetime
|
||||||
# deleted_user_email_never_send :string
|
# deleted_user_email_never_send :string
|
||||||
# en_construction_at :datetime
|
# en_construction_at :datetime
|
||||||
# en_construction_close_to_expiration_notice_sent_at :datetime
|
# en_construction_close_to_expiration_notice_sent_at :datetime
|
||||||
|
@ -655,7 +656,9 @@ class Dossier < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_passer_automatiquement_en_instruction
|
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)
|
log_automatic_dossier_operation(:passer_en_instruction)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -695,6 +698,7 @@ class Dossier < ApplicationRecord
|
||||||
def after_accepter_automatiquement
|
def after_accepter_automatiquement
|
||||||
self.traitements.build(state: Dossier.states.fetch(:accepte), instructeur_email: nil, motivation: nil, processed_at: Time.zone.now)
|
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.en_instruction_at ||= Time.zone.now
|
||||||
|
self.declarative_triggered_at = Time.zone.now
|
||||||
|
|
||||||
if attestation.nil?
|
if attestation.nil?
|
||||||
self.attestation = build_attestation
|
self.attestation = build_attestation
|
||||||
|
|
|
@ -606,10 +606,12 @@ class Procedure < ApplicationRecord
|
||||||
when Procedure.declarative_with_states.fetch(:en_instruction)
|
when Procedure.declarative_with_states.fetch(:en_instruction)
|
||||||
dossiers
|
dossiers
|
||||||
.state_en_construction
|
.state_en_construction
|
||||||
|
.where(declarative_triggered_at: nil)
|
||||||
.find_each(&:passer_automatiquement_en_instruction!)
|
.find_each(&:passer_automatiquement_en_instruction!)
|
||||||
when Procedure.declarative_with_states.fetch(:accepte)
|
when Procedure.declarative_with_states.fetch(:accepte)
|
||||||
dossiers
|
dossiers
|
||||||
.state_en_construction
|
.state_en_construction
|
||||||
|
.where(declarative_triggered_at: nil)
|
||||||
.find_each(&:accepter_automatiquement!)
|
.find_each(&:accepter_automatiquement!)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,31 +5,4 @@
|
||||||
locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)),
|
locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)),
|
||||||
'Statistiques'] }
|
'Statistiques'] }
|
||||||
|
|
||||||
.statistiques
|
= render partial: 'shared/procedures/stats', locals: { title: title }
|
||||||
-# 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
|
|
||||||
|
|
35
app/views/shared/procedures/_stats.html.haml
Normal file
35
app/views/shared/procedures/_stats.html.haml
Normal file
|
@ -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"]
|
|
@ -33,6 +33,10 @@
|
||||||
- horaires = "Horaires : #{formatted_horaires(service.horaires)}"
|
- horaires = "Horaires : #{formatted_horaires(service.horaires)}"
|
||||||
= simple_format(horaires, {}, wrapper_tag: 'span')
|
= 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)
|
- politiques = politiques_conservation_de_donnees(procedure)
|
||||||
- if politiques.present?
|
- if politiques.present?
|
||||||
|
|
4
app/views/users/statistiques/show.html.haml
Normal file
4
app/views/users/statistiques/show.html.haml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
- title = "Statistiques · #{@procedure.libelle}"
|
||||||
|
- content_for(:title, title)
|
||||||
|
|
||||||
|
= render partial: 'shared/procedures/stats', locals: { title: title }
|
|
@ -248,6 +248,10 @@ Rails.application.routes.draw do
|
||||||
#
|
#
|
||||||
|
|
||||||
scope module: 'users' do
|
scope module: 'users' do
|
||||||
|
namespace :statistiques do
|
||||||
|
get '/:path', action: 'statistiques'
|
||||||
|
end
|
||||||
|
|
||||||
namespace :commencer do
|
namespace :commencer do
|
||||||
get '/test/:path/dossier_vide', action: 'dossier_vide_pdf_test', as: :dossier_vide_test
|
get '/test/:path/dossier_vide', action: 'dossier_vide_pdf_test', as: :dossier_vide_test
|
||||||
get '/test/:path', action: 'commencer_test', as: :test
|
get '/test/:path', action: 'commencer_test', as: :test
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddDeclarativeTriggeredAtToDossiers < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :dossiers, :declarative_triggered_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -278,6 +278,7 @@ ActiveRecord::Schema.define(version: 2021_05_07_135603) do
|
||||||
t.string "deleted_user_email_never_send"
|
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 || 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.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 ["archived"], name: "index_dossiers_on_archived"
|
||||||
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
|
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
|
||||||
t.index ["hidden_at"], name: "index_dossiers_on_hidden_at"
|
t.index ["hidden_at"], name: "index_dossiers_on_hidden_at"
|
||||||
|
|
|
@ -9,14 +9,17 @@ RSpec.describe Cron::DeclarativeProceduresJob, type: :job do
|
||||||
let(:nouveau_dossier2) { create(:dossier, :en_construction, :with_individual, procedure: procedure) }
|
let(:nouveau_dossier2) { create(:dossier, :en_construction, :with_individual, procedure: procedure) }
|
||||||
let(:dossier_recu) { create(:dossier, :en_instruction, :with_individual, procedure: procedure) }
|
let(:dossier_recu) { create(:dossier, :en_instruction, :with_individual, procedure: procedure) }
|
||||||
let(:dossier_brouillon) { create(:dossier, procedure: procedure) }
|
let(:dossier_brouillon) { create(:dossier, procedure: procedure) }
|
||||||
|
let(:dossier_repasse_en_construction) { create(:dossier, :en_construction, :with_individual, procedure: procedure) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Timecop.freeze(date)
|
Timecop.freeze(date)
|
||||||
|
dossier_repasse_en_construction.touch(:declarative_triggered_at)
|
||||||
dossiers = [
|
dossiers = [
|
||||||
nouveau_dossier1,
|
nouveau_dossier1,
|
||||||
nouveau_dossier2,
|
nouveau_dossier2,
|
||||||
dossier_recu,
|
dossier_recu,
|
||||||
dossier_brouillon
|
dossier_brouillon,
|
||||||
|
dossier_repasse_en_construction
|
||||||
]
|
]
|
||||||
|
|
||||||
create(:attestation_template, procedure: procedure)
|
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 }
|
let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last }
|
||||||
|
|
||||||
it {
|
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(nouveau_dossier1.en_instruction_at).to eq(date)
|
||||||
expect(last_operation.operation).to eq('passer_en_instruction')
|
expect(last_operation.operation).to eq('passer_en_instruction')
|
||||||
expect(last_operation.automatic_operation?).to be_truthy
|
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(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_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_brouillon.en_instruction_at).to eq(nil)
|
||||||
|
|
||||||
|
expect(dossier_repasse_en_construction.en_construction?).to be_truthy
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
16
spec/views/users/statistiques/show.html.haml_spec.rb
Normal file
16
spec/views/users/statistiques/show.html.haml_spec.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue