commit
f783d9e6f0
4 changed files with 298 additions and 104 deletions
|
@ -1,46 +1,38 @@
|
|||
class StatsController < ApplicationController
|
||||
layout "new_application"
|
||||
|
||||
MEAN_NUMBER_OF_CHAMPS_IN_A_FORM = 24.0
|
||||
|
||||
def index
|
||||
procedures = Procedure.where(:published => true)
|
||||
dossiers = Dossier.where.not(:state => :draft)
|
||||
|
||||
@procedures_30_days_flow = thirty_days_flow_hash(procedures)
|
||||
@dossiers_30_days_flow = thirty_days_flow_hash(dossiers, :initiated_at)
|
||||
|
||||
@procedures_cumulative = cumulative_hash(procedures)
|
||||
@dossiers_cumulative = cumulative_hash(dossiers, :initiated_at)
|
||||
|
||||
@procedures_count = procedures.count
|
||||
@dossiers_count = dossiers.count
|
||||
|
||||
@procedures_cumulative = cumulative_hash(procedures)
|
||||
@procedures_in_the_last_4_months = last_four_months_hash(procedures)
|
||||
|
||||
@dossiers_cumulative = cumulative_hash(dossiers, :initiated_at)
|
||||
@dossiers_in_the_last_4_months = last_four_months_hash(dossiers, :initiated_at)
|
||||
|
||||
@dossier_instruction_mean_time = dossier_instruction_mean_time(dossiers)
|
||||
@dossier_filling_mean_time = dossier_filling_mean_time(dossiers)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def thirty_days_flow_hash(association, date_attribute = :created_at)
|
||||
min_date = 30.days.ago.to_date
|
||||
def last_four_months_hash(association, date_attribute = :created_at)
|
||||
min_date = 3.months.ago.beginning_of_month.to_date
|
||||
max_date = Time.now.to_date
|
||||
|
||||
thirty_days_flow_hash = association
|
||||
association
|
||||
.where(date_attribute => min_date..max_date)
|
||||
.group("date_trunc('day', #{date_attribute.to_s})")
|
||||
.group("DATE_TRUNC('month', #{date_attribute.to_s})")
|
||||
.count
|
||||
|
||||
clean_hash(thirty_days_flow_hash, min_date, max_date)
|
||||
end
|
||||
|
||||
def clean_hash(h, min_date, max_date)
|
||||
# Convert keys to date
|
||||
h = Hash[h.map { |(k, v)| [k.to_date, v] }]
|
||||
|
||||
# Add missing vales where count is 0
|
||||
(min_date..max_date).each do |date|
|
||||
if h[date].nil?
|
||||
h[date] = 0
|
||||
end
|
||||
end
|
||||
|
||||
h
|
||||
.to_a
|
||||
.sort{ |x, y| x[0] <=> y[0] }
|
||||
.map { |e| [I18n.l(e.first, format: "%B %Y"), e.last] }
|
||||
end
|
||||
|
||||
def cumulative_hash(association, date_attribute = :created_at)
|
||||
|
@ -54,4 +46,91 @@ class StatsController < ApplicationController
|
|||
.reduce({}, :merge)
|
||||
end
|
||||
|
||||
def mean(collection)
|
||||
(collection.sum.to_f / collection.size).round(2)
|
||||
end
|
||||
|
||||
def dossier_instruction_mean_time(dossiers)
|
||||
# In the 12 last months, we compute for each month
|
||||
# the average time it took to instruct a dossier
|
||||
# We compute monthly averages by first making an average per procedure
|
||||
# and then computing the average for all the procedures
|
||||
|
||||
min_date = 11.months.ago
|
||||
max_date = Time.now.to_date
|
||||
|
||||
processed_dossiers = dossiers
|
||||
.where(:processed_at => min_date..max_date)
|
||||
.pluck(:procedure_id, :initiated_at, :processed_at)
|
||||
|
||||
# Group dossiers by month
|
||||
processed_dossiers_by_month = processed_dossiers
|
||||
.group_by do |dossier|
|
||||
dossier[2].beginning_of_month.to_s
|
||||
end
|
||||
|
||||
processed_dossiers_by_month.map do |month, value|
|
||||
# Group the dossiers for this month by procedure
|
||||
dossiers_grouped_by_procedure = value.group_by { |dossier| dossier[0] }
|
||||
|
||||
# Compute the mean time for this procedure
|
||||
procedure_processing_times = dossiers_grouped_by_procedure.map do |procedure_id, procedure_dossiers|
|
||||
procedure_dossiers_processing_time = procedure_dossiers.map do |dossier|
|
||||
(dossier[2] - dossier[1]).to_f / (3600 * 24)
|
||||
end
|
||||
|
||||
mean(procedure_dossiers_processing_time)
|
||||
end
|
||||
|
||||
# Compute the average mean time for all the procedures of this month
|
||||
month_average = mean(procedure_processing_times)
|
||||
|
||||
[month, month_average]
|
||||
end.to_h
|
||||
end
|
||||
|
||||
def dossier_filling_mean_time(dossiers)
|
||||
# In the 12 last months, we compute for each month
|
||||
# the average time it took to fill a dossier
|
||||
# We compute monthly averages by first making an average per procedure
|
||||
# and then computing the average for all the procedures
|
||||
# For each procedure, we normalize the data: the time is calculated
|
||||
# for a 24 champs form (the current form mean length)
|
||||
|
||||
min_date = 11.months.ago
|
||||
max_date = Time.now.to_date
|
||||
|
||||
processed_dossiers = dossiers
|
||||
.where(:processed_at => min_date..max_date)
|
||||
.pluck(:procedure_id, :created_at, :initiated_at, :processed_at)
|
||||
|
||||
# Group dossiers by month
|
||||
processed_dossiers_by_month = processed_dossiers
|
||||
.group_by do |e|
|
||||
e[3].beginning_of_month.to_s
|
||||
end
|
||||
|
||||
processed_dossiers_by_month.map do |month, value|
|
||||
# Group the dossiers for this month by procedure
|
||||
dossiers_grouped_by_procedure = value.group_by { |dossier| dossier[0] }
|
||||
|
||||
# Compute the mean time for this procedure
|
||||
procedure_processing_times = dossiers_grouped_by_procedure.map do |procedure_id, procedure_dossiers|
|
||||
procedure_dossiers_processing_time = procedure_dossiers.map do |dossier|
|
||||
(dossier[2] - dossier[1]).to_f / 60
|
||||
end
|
||||
|
||||
procedure_mean = mean(procedure_dossiers_processing_time)
|
||||
|
||||
# We normalize the data for 24 fields
|
||||
procedure_fields_count = Procedure.find(procedure_id).types_de_champ.count
|
||||
procedure_mean * (MEAN_NUMBER_OF_CHAMPS_IN_A_FORM / procedure_fields_count)
|
||||
end
|
||||
|
||||
# Compute the average mean time for all the procedures of this month
|
||||
month_average = mean(procedure_processing_times)
|
||||
|
||||
[month, month_average]
|
||||
end.to_h
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,41 +3,6 @@
|
|||
%h1.new-h1 Statistiques
|
||||
|
||||
.stat-cards
|
||||
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%ul.segmented-control.pull-right
|
||||
%li.segmented-control-item.segmented-control-item-active{ :onclick => "TPS.toggleChart(event, '.cumulative-procedures-chart');" }
|
||||
Cumul
|
||||
%li.segmented-control-item{ :onclick => "TPS.toggleChart(event, '.flux-procedures-chart');" }
|
||||
Flux (30 jours)
|
||||
%span.stat-card-title.pull-left Procédures dématérialisées
|
||||
.clearfix
|
||||
|
||||
.chart-container
|
||||
.chart.cumulative-procedures-chart
|
||||
= area_chart @procedures_cumulative,
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
.chart.flux-procedures-chart.hidden
|
||||
= line_chart @procedures_30_days_flow,
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%ul.segmented-control.pull-right
|
||||
%li.segmented-control-item.segmented-control-item-active{ :onclick => "TPS.toggleChart(event, '.cumulative-dossiers-chart');" }
|
||||
Cumul
|
||||
%li.segmented-control-item{ :onclick => "TPS.toggleChart(event, '.flux-dossiers-chart');" }
|
||||
Flux (30 jours)
|
||||
%span.stat-card-title.pull-left Dossiers déposés
|
||||
.clearfix
|
||||
|
||||
.chart-container
|
||||
.chart.cumulative-dossiers-chart
|
||||
= area_chart @dossiers_cumulative,
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
.chart.flux-dossiers-chart.hidden
|
||||
= line_chart @dossiers_30_days_flow,
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
|
||||
.stat-card.stat-card-half.big-number-card.pull-left
|
||||
%span.big-number-card-title TOTAL PROCÉDURES DÉMATÉRIALISÉES
|
||||
%span.big-number-card-number
|
||||
|
@ -48,4 +13,58 @@
|
|||
%span.big-number-card-number
|
||||
= number_with_delimiter(@dossiers_count)
|
||||
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%ul.segmented-control.pull-right
|
||||
%li.segmented-control-item.segmented-control-item-active{ :onclick => "TPS.toggleChart(event, '.monthly-procedures-chart');" }
|
||||
Par mois
|
||||
%li.segmented-control-item{ :onclick => "TPS.toggleChart(event, '.cumulative-procedures-chart');" }
|
||||
Cumul
|
||||
%span.stat-card-title.pull-left Procédures dématérialisées
|
||||
.clearfix
|
||||
|
||||
.chart-container
|
||||
.chart.monthly-procedures-chart
|
||||
= column_chart @procedures_in_the_last_4_months,
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
.chart.cumulative-procedures-chart.hidden
|
||||
= area_chart @procedures_cumulative,
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%ul.segmented-control.pull-right
|
||||
%li.segmented-control-item.segmented-control-item-active{ :onclick => "TPS.toggleChart(event, '.monthly-dossiers-chart');" }
|
||||
Par mois
|
||||
%li.segmented-control-item{ :onclick => "TPS.toggleChart(event, '.cumulative-dossiers-chart');" }
|
||||
Cumul
|
||||
%span.stat-card-title.pull-left Dossiers déposés
|
||||
.clearfix
|
||||
|
||||
.chart-container
|
||||
.chart.monthly-dossiers-chart
|
||||
= column_chart @dossiers_in_the_last_4_months,
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
.chart.cumulative-dossiers-chart.hidden
|
||||
= area_chart @dossiers_cumulative,
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
|
||||
- if administration_signed_in?
|
||||
- cache "computation-heavy-stats", :expires_in => 1.day do
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title Temps de traitement moyen d'un dossier
|
||||
|
||||
.chart-container
|
||||
.chart
|
||||
= line_chart @dossier_instruction_mean_time,
|
||||
:ytitle => "Jours",
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title Temps de remplissage moyen d'un dossier
|
||||
|
||||
.chart-container
|
||||
.chart
|
||||
= line_chart @dossier_filling_mean_time,
|
||||
:ytitle => "Minutes",
|
||||
:colors => ["rgba(61, 149, 236, 1)"]
|
||||
|
||||
.clearfix
|
||||
|
|
|
@ -1,51 +1,41 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe StatsController, type: :controller do
|
||||
describe '#thirty_days_flow_hash' do
|
||||
context "without a date_attribut" do
|
||||
describe "#last_four_months_hash" do
|
||||
context "without a date attribute" do
|
||||
before do
|
||||
FactoryGirl.create(:procedure, :created_at => 6.months.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 45.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 15.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 1.day.ago)
|
||||
|
||||
@expected_hash = {}
|
||||
(30.days.ago.to_date..Time.now.to_date).each do |day|
|
||||
if [15.days.ago.to_date, 1.day.ago.to_date].include?(day)
|
||||
@expected_hash[day] = 1
|
||||
else
|
||||
@expected_hash[day] = 0
|
||||
end
|
||||
end
|
||||
FactoryGirl.create(:procedure, :created_at => 1.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 1.days.ago)
|
||||
end
|
||||
|
||||
let (:association) { Procedure.all }
|
||||
|
||||
subject { StatsController.new.send(:thirty_days_flow_hash, association) }
|
||||
subject { StatsController.new.send(:last_four_months_hash, association) }
|
||||
|
||||
it { expect(subject).to eq(@expected_hash) }
|
||||
it { expect(subject).to eq([
|
||||
[I18n.l(45.days.ago.beginning_of_month, format: "%B %Y"), 1],
|
||||
[I18n.l(1.days.ago.beginning_of_month, format: "%B %Y"), 2]
|
||||
] ) }
|
||||
end
|
||||
|
||||
context "with a date_attribut" do
|
||||
context "with a date attribute" do
|
||||
before do
|
||||
FactoryGirl.create(:procedure, :created_at => 45.days.ago, :updated_at => 50.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 15.days.ago, :updated_at => 10.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 1.day.ago, :updated_at => 3.days.ago)
|
||||
|
||||
@expected_hash = {}
|
||||
(30.days.ago.to_date..Time.now.to_date).each do |day|
|
||||
if [10.days.ago.to_date, 3.day.ago.to_date].include?(day)
|
||||
@expected_hash[day] = 1
|
||||
else
|
||||
@expected_hash[day] = 0
|
||||
end
|
||||
end
|
||||
FactoryGirl.create(:procedure, :created_at => 6.months.ago, :updated_at => 6.months.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 2.months.ago, :updated_at => 45.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 2.months.ago, :updated_at => 45.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 2.months.ago, :updated_at => 1.days.ago)
|
||||
end
|
||||
|
||||
let (:association) { Procedure.all }
|
||||
|
||||
subject { StatsController.new.send(:thirty_days_flow_hash, association, :updated_at) }
|
||||
subject { StatsController.new.send(:last_four_months_hash, association, :updated_at) }
|
||||
|
||||
it { expect(subject).to eq(@expected_hash) }
|
||||
it { expect(subject).to eq([
|
||||
[I18n.l(45.days.ago.beginning_of_month, format: "%B %Y"), 2],
|
||||
[I18n.l(1.days.ago.beginning_of_month, format: "%B %Y"), 1]
|
||||
] ) }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -66,22 +56,122 @@ describe StatsController, type: :controller do
|
|||
15.days.ago.beginning_of_month => 3
|
||||
}) }
|
||||
end
|
||||
|
||||
context "with a date attribute" do
|
||||
before do
|
||||
FactoryGirl.create(:procedure, :created_at => 45.days.ago, :updated_at => 20.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 15.days.ago, :updated_at => 20.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 15.days.ago, :updated_at => 10.days.ago)
|
||||
end
|
||||
|
||||
let (:association) { Procedure.all }
|
||||
|
||||
subject { StatsController.new.send(:cumulative_hash, association, :updated_at) }
|
||||
|
||||
it { expect(subject).to eq({
|
||||
20.days.ago.beginning_of_month => 2,
|
||||
10.days.ago.beginning_of_month => 3
|
||||
}) }
|
||||
end
|
||||
end
|
||||
|
||||
context "with a date attribute" do
|
||||
describe "#dossier_instruction_mean_time" do
|
||||
# Month-2: mean 3 days
|
||||
# procedure_1: mean 2 days
|
||||
# dossier_p1_a: 3 days
|
||||
# dossier_p1_b: 1 days
|
||||
# procedure_2: mean 4 days
|
||||
# dossier_p2_a: 4 days
|
||||
#
|
||||
# Month-1: mean 5 days
|
||||
# procedure_1: mean 5 days
|
||||
# dossier_p1_c: 5 days
|
||||
|
||||
before do
|
||||
FactoryGirl.create(:procedure, :created_at => 45.days.ago, :updated_at => 20.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 15.days.ago, :updated_at => 20.days.ago)
|
||||
FactoryGirl.create(:procedure, :created_at => 15.days.ago, :updated_at => 10.days.ago)
|
||||
procedure_1 = FactoryGirl.create(:procedure)
|
||||
procedure_2 = FactoryGirl.create(:procedure)
|
||||
dossier_p1_a = FactoryGirl.create(:dossier,
|
||||
:procedure => procedure_1,
|
||||
:initiated_at => 2.months.ago.beginning_of_month,
|
||||
:processed_at => 2.months.ago.beginning_of_month + 3.days)
|
||||
dossier_p1_b = FactoryGirl.create(:dossier,
|
||||
:procedure => procedure_1,
|
||||
:initiated_at => 2.months.ago.beginning_of_month,
|
||||
:processed_at => 2.months.ago.beginning_of_month + 1.days)
|
||||
dossier_p1_c = FactoryGirl.create(:dossier,
|
||||
:procedure => procedure_1,
|
||||
:initiated_at => 1.months.ago.beginning_of_month,
|
||||
:processed_at => 1.months.ago.beginning_of_month + 5.days)
|
||||
dossier_p2_a = FactoryGirl.create(:dossier,
|
||||
:procedure => procedure_2,
|
||||
:initiated_at => 2.month.ago.beginning_of_month,
|
||||
:processed_at => 2.month.ago.beginning_of_month + 4.days)
|
||||
|
||||
# Write directly in the DB to avoid the before_validation hook
|
||||
Dossier.update_all(state: "closed")
|
||||
|
||||
@expected_hash = {
|
||||
"#{2.months.ago.beginning_of_month}" => 3.0,
|
||||
"#{1.months.ago.beginning_of_month}" => 5.0
|
||||
}
|
||||
end
|
||||
|
||||
let (:association) { Procedure.all }
|
||||
let (:association) { Dossier.where.not(:state => :draft) }
|
||||
|
||||
subject { StatsController.new.send(:cumulative_hash, association, :updated_at) }
|
||||
subject { StatsController.new.send(:dossier_instruction_mean_time, association) }
|
||||
|
||||
it { expect(subject).to eq({
|
||||
20.days.ago.beginning_of_month => 2,
|
||||
10.days.ago.beginning_of_month => 3
|
||||
}) }
|
||||
it { expect(subject).to eq(@expected_hash) }
|
||||
end
|
||||
|
||||
describe "#dossier_filling_mean_time" do
|
||||
# Month-2: mean 30 minutes
|
||||
# procedure_1: mean 20 minutes
|
||||
# dossier_p1_a: 30 minutes
|
||||
# dossier_p1_b: 10 minutes
|
||||
# procedure_2: mean 40 minutes
|
||||
# dossier_p2_a: 80 minutes, for twice the fields
|
||||
#
|
||||
# Month-1: mean 50 minutes
|
||||
# procedure_1: mean 50 minutes
|
||||
# dossier_p1_c: 50 minutes
|
||||
|
||||
before do
|
||||
procedure_1 = FactoryGirl.create(:procedure, :with_type_de_champ, :types_de_champ_count => 24)
|
||||
procedure_2 = FactoryGirl.create(:procedure, :with_type_de_champ, :types_de_champ_count => 48)
|
||||
dossier_p1_a = FactoryGirl.create(:dossier,
|
||||
:procedure => procedure_1,
|
||||
:created_at => 2.months.ago.beginning_of_month,
|
||||
:initiated_at => 2.months.ago.beginning_of_month + 30.minutes,
|
||||
:processed_at => 2.months.ago.beginning_of_month + 1.day)
|
||||
dossier_p1_b = FactoryGirl.create(:dossier,
|
||||
:procedure => procedure_1,
|
||||
:created_at => 2.months.ago.beginning_of_month,
|
||||
:initiated_at => 2.months.ago.beginning_of_month + 10.minutes,
|
||||
:processed_at => 2.months.ago.beginning_of_month + 1.day)
|
||||
dossier_p1_c = FactoryGirl.create(:dossier,
|
||||
:procedure => procedure_1,
|
||||
:created_at => 1.months.ago.beginning_of_month,
|
||||
:initiated_at => 1.months.ago.beginning_of_month + 50.minutes,
|
||||
:processed_at => 1.months.ago.beginning_of_month + 1.day)
|
||||
dossier_p2_a = FactoryGirl.create(:dossier,
|
||||
:procedure => procedure_2,
|
||||
:created_at => 2.month.ago.beginning_of_month,
|
||||
:initiated_at => 2.month.ago.beginning_of_month + 80.minutes,
|
||||
:processed_at => 2.month.ago.beginning_of_month + 1.day)
|
||||
|
||||
# Write directly in the DB to avoid the before_validation hook
|
||||
Dossier.update_all(state: "closed")
|
||||
|
||||
@expected_hash = {
|
||||
"#{2.months.ago.beginning_of_month}" => 30.0,
|
||||
"#{1.months.ago.beginning_of_month}" => 50.0
|
||||
}
|
||||
end
|
||||
|
||||
let (:association) { Dossier.where.not(:state => :draft) }
|
||||
|
||||
subject { StatsController.new.send(:dossier_filling_mean_time, association) }
|
||||
|
||||
it { expect(subject).to eq(@expected_hash) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,10 +30,16 @@ FactoryGirl.define do
|
|||
end
|
||||
|
||||
trait :with_type_de_champ do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
type_de_champ = create(:type_de_champ_public)
|
||||
transient do
|
||||
types_de_champ_count 1
|
||||
end
|
||||
|
||||
procedure.types_de_champ << type_de_champ
|
||||
after(:build) do |procedure, evaluator|
|
||||
evaluator.types_de_champ_count.times do
|
||||
type_de_champ = create(:type_de_champ_public)
|
||||
|
||||
procedure.types_de_champ << type_de_champ
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue