class StatsController < ApplicationController before_action :authenticate_administration!, only: [:download] MEAN_NUMBER_OF_CHAMPS_IN_A_FORM = 24.0 def index procedures = Procedure.publiees_ou_archivees dossiers = Dossier.state_not_brouillon @procedures_numbers = procedures_numbers(procedures) @dossiers_numbers = dossiers_numbers(dossiers) @satisfaction_usagers = satisfaction_usagers @contact_percentage = contact_percentage @contact_percentage_excluded_tags = Helpscout::UserConversationsAdapter::EXCLUDED_TAGS @dossiers_states = dossiers_states @procedures_cumulative = cumulative_hash(procedures, :published_at) @procedures_in_the_last_4_months = last_four_months_hash(procedures, :published_at) @dossiers_cumulative = cumulative_hash(dossiers, :en_construction_at) @dossiers_in_the_last_4_months = last_four_months_hash(dossiers, :en_construction_at) @procedures_count_per_administrateur = procedures_count_per_administrateur(procedures) if administration_signed_in? @dossier_instruction_mean_time = Rails.cache.fetch("dossier_instruction_mean_time", expires_in: 1.day) do dossier_instruction_mean_time(dossiers) end @dossier_filling_mean_time = Rails.cache.fetch("dossier_filling_mean_time", expires_in: 1.day) do dossier_filling_mean_time(dossiers) end @avis_usage = avis_usage @avis_average_answer_time = avis_average_answer_time @avis_answer_percentages = avis_answer_percentages @motivation_usage_dossier = motivation_usage_dossier @motivation_usage_procedure = motivation_usage_procedure @cloned_from_library_procedures_ratio = cloned_from_library_procedures_ratio end end def download headers = [ 'ID du dossier', 'ID de la démarche', 'Nom de la démarche', 'ID utilisateur', 'Etat du fichier', 'Durée en brouillon', 'Durée en construction', 'Durée en instruction' ] data = Dossier .includes(:procedure, :user) .in_batches .flat_map do |dossiers| dossiers .pluck( "dossiers.id", "procedures.id", "procedures.libelle", "users.id", "dossiers.state", "dossiers.en_construction_at - dossiers.created_at", "dossiers.en_instruction_at - dossiers.en_construction_at", "dossiers.processed_at - dossiers.en_instruction_at" ) end respond_to do |format| format.csv { send_data(SpreadsheetArchitect.to_xlsx(headers: headers, data: data), filename: "statistiques.csv") } end end private def procedures_numbers(procedures) total = procedures.count last_30_days_count = procedures.where(published_at: 1.month.ago..Time.zone.now).count previous_count = procedures.where(published_at: 2.months.ago..1.month.ago).count if previous_count != 0 evolution = (((last_30_days_count.to_f / previous_count) - 1) * 100).round(0) else evolution = 0 end formatted_evolution = format("%+d", evolution) { total: total.to_s, last_30_days_count: last_30_days_count.to_s, evolution: formatted_evolution } end def dossiers_numbers(dossiers) total = dossiers.count last_30_days_count = dossiers.where(en_construction_at: 1.month.ago..Time.zone.now).count previous_count = dossiers.where(en_construction_at: 2.months.ago..1.month.ago).count if previous_count != 0 evolution = (((last_30_days_count.to_f / previous_count) - 1) * 100).round(0) else evolution = 0 end formatted_evolution = format("%+d", evolution) { total: total.to_s, last_30_days_count: last_30_days_count.to_s, evolution: formatted_evolution } end def dossiers_states { 'Brouilllon' => Dossier.state_brouillon.count, 'En construction' => Dossier.state_en_construction.count, 'En instruction' => Dossier.state_en_instruction.count, 'Terminé' => Dossier.state_termine.count } end def satisfaction_usagers legend = { Feedback.ratings.fetch(:unhappy) => "Mécontents", Feedback.ratings.fetch(:neutral) => "Neutres", Feedback.ratings.fetch(:happy) => "Satisfaits" } number_of_weeks = 6 totals = Feedback .group_by_week(:created_at, last: number_of_weeks, current: false) .count legend.keys.map do |rating| data = Feedback .where(rating: rating) .group_by_week(:created_at, last: number_of_weeks, current: false) .count .map do |week, count| total = totals[week] # By default a week is displayed by the first day of the week – but we'd rather display the last day label = week.next_week if total > 0 [label, (count.to_f / total * 100).round(2)] else [label, 0] end end.to_h { name: legend[rating], data: data } end end def contact_percentage from = Date.new(2018, 1) to = Date.today.prev_month adapter = Helpscout::UserConversationsAdapter.new(from, to) if !adapter.can_fetch_reports? return nil end adapter .reports .map do |monthly_report| start_date = monthly_report[:start_date].to_time.localtime end_date = monthly_report[:end_date].to_time.localtime replies_count = monthly_report[:conversations_count] dossiers_count = Dossier.where(en_construction_at: start_date..end_date).count monthly_contact_percentage = replies_count.fdiv(dossiers_count || 1) * 100 [I18n.l(start_date, format: '%b %y'), monthly_contact_percentage.round(1)] end end def cloned_from_library_procedures_ratio [3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |date| min_date = date.beginning_of_week max_date = min_date.end_of_week all_procedures = Procedure.created_during(min_date..max_date) cloned_from_library_procedures = all_procedures.cloned_from_library denominator = [1, all_procedures.count].max ratio = percentage(cloned_from_library_procedures.count, denominator) [l(max_date, format: '%d/%m/%Y'), ratio] end end def max_date if administration_signed_in? Time.zone.now.to_date else Time.zone.now.beginning_of_month - 1.second end end def last_four_months_hash(association, date_attribute) min_date = 3.months.ago.beginning_of_month.to_date association .where(date_attribute => min_date..max_date) .group("DATE_TRUNC('month', #{date_attribute})") .count .to_a .sort_by { |a| a[0] } .map { |e| [I18n.l(e.first, format: "%B %Y"), e.last] } end def cumulative_hash(association, date_attribute) sum = 0 association .where("#{date_attribute} < ?", max_date) .group("DATE_TRUNC('month', #{date_attribute})") .count .to_a .sort_by { |a| a[0] } .map { |x, y| { x => (sum += y) } } .reduce({}, :merge) end def procedures_count_per_administrateur(procedures) count_per_administrateur = procedures.group(:administrateur_id).count.values { 'Une démarche' => count_per_administrateur.select { |count| count == 1 }.count, 'Entre deux et cinq démarches' => count_per_administrateur.select { |count| count.in?(2..5) }.count, 'Plus de cinq démarches' => count_per_administrateur.select { |count| count > 5 }.count } end def mean(collection) (collection.sum.to_f / collection.size).round(2) end def percentage(numerator, denominator) ((numerator.to_f / denominator) * 100).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.zone.now.to_date processed_dossiers = dossiers .where(:processed_at => min_date..max_date) .pluck(:procedure_id, :en_construction_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.zone.now.to_date processed_dossiers = dossiers .where(:processed_at => min_date..max_date) .pluck( :procedure_id, Arel.sql('EXTRACT(EPOCH FROM (en_construction_at - created_at)) / 60 AS processing_time'), :processed_at ) # Group dossiers by month processed_dossiers_by_month = processed_dossiers .group_by do |(*_, processed_at)| processed_at.beginning_of_month.to_s end procedure_id_type_de_champs_count = TypeDeChamp .where(private: false) .group(:procedure_id) .count processed_dossiers_by_month.map do |month, dossier_plucks| # Group the dossiers for this month by procedure dossiers_grouped_by_procedure = dossier_plucks.group_by { |(procedure_id, *_)| procedure_id } # Compute the mean time for this procedure procedure_processing_times = dossiers_grouped_by_procedure.map do |procedure_id, procedure_dossiers| procedure_fields_count = procedure_id_type_de_champs_count[procedure_id] if (procedure_fields_count == 0 || procedure_fields_count.nil?) next end procedure_dossiers_processing_time = procedure_dossiers.map { |_, processing_time, _| processing_time } procedure_mean = mean(procedure_dossiers_processing_time) # We normalize the data for 24 fields procedure_mean * (MEAN_NUMBER_OF_CHAMPS_IN_A_FORM / procedure_fields_count) end .compact # 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 avis_usage [3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |min_date| max_date = min_date + 1.week weekly_dossiers = Dossier.includes(:avis).where(created_at: min_date..max_date).to_a weekly_dossiers_count = weekly_dossiers.count if weekly_dossiers_count == 0 result = 0 else weekly_dossier_with_avis_count = weekly_dossiers.select { |dossier| dossier.avis.present? }.count result = percentage(weekly_dossier_with_avis_count, weekly_dossiers_count) end [min_date.to_i, result] end end def avis_average_answer_time [3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |min_date| max_date = min_date + 1.week average = Avis.with_answer .where(created_at: min_date..max_date) .average("EXTRACT(EPOCH FROM avis.updated_at - avis.created_at) / 86400") result = average ? average.to_f.round(2) : 0 [min_date.to_i, result] end end def avis_answer_percentages [3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |min_date| max_date = min_date + 1.week weekly_avis = Avis.where(created_at: min_date..max_date) weekly_avis_count = weekly_avis.count if weekly_avis_count == 0 [min_date.to_i, 0] else answered_weekly_avis_count = weekly_avis.with_answer.count result = percentage(answered_weekly_avis_count, weekly_avis_count) [min_date.to_i, result] end end end def motivation_usage_dossier [3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |date| min_date = date.beginning_of_week max_date = date.end_of_week weekly_termine_dossiers = Dossier.where(processed_at: min_date..max_date) weekly_termine_dossiers_count = weekly_termine_dossiers.count weekly_termine_dossiers_with_motivation_count = weekly_termine_dossiers.where.not(motivation: nil).count if weekly_termine_dossiers_count == 0 result = 0 else result = percentage(weekly_termine_dossiers_with_motivation_count, weekly_termine_dossiers_count) end [l(max_date, format: '%d/%m/%Y'), result] end end def motivation_usage_procedure [3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |date| min_date = date.beginning_of_week max_date = date.end_of_week procedures_with_dossier_processed_this_week = Procedure .joins(:dossiers) .where(dossiers: { processed_at: min_date..max_date }) procedures_with_dossier_processed_this_week_count = procedures_with_dossier_processed_this_week .uniq .count procedures_with_dossier_processed_this_week_and_with_motivation_count = procedures_with_dossier_processed_this_week .where .not(dossiers: { motivation: nil }) .uniq .count if procedures_with_dossier_processed_this_week_count == 0 result = 0 else result = percentage(procedures_with_dossier_processed_this_week_and_with_motivation_count, procedures_with_dossier_processed_this_week_count) end [l(max_date, format: '%d/%m/%Y'), result] end end end