Merge pull request #9701 from demarches-simplifiees/carte

Afficher une carte de déploiement de DS par département
This commit is contained in:
krichtof 2023-11-16 13:18:16 +00:00 committed by GitHub
commit 39bdb5f145
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 598 additions and 0 deletions

View file

@ -0,0 +1,64 @@
@import "colors";
$dep-nothing: #E3E3FD; // blue-france-925
$dep-small: #CACAFB; // blue-france-850
$dep-medium: #8585F6; // blue-france-625
$dep-large: #313178; // blue-france-200
$dep-xlarge: #272747; // blue-france-125
#map-svg {
max-width: 100%;
height: auto;
}
#map-infos {
min-width: 328px;
}
.departement.nothing {
fill: $dep-nothing;
}
.departement.small {
fill: $dep-small;
}
.departement.medium {
fill: $dep-medium;
}
.departement.large {
fill: $dep-large;
}
.departement.xlarge {
fill: $dep-xlarge;
}
.legends {
.legend {
width: 60px;
height: 10px;
display: inline-block;
}
.nothing {
background-color: $dep-nothing;
}
.small {
background-color: $dep-small;
}
.medium {
background-color: $dep-medium;
}
.large {
background-color: $dep-large;
}
.xlarge {
background-color: $dep-xlarge;
}
}

View file

@ -0,0 +1,23 @@
class CarteController < ApplicationController
def show
@map_filter = MapFilter.new(params)
@map_filter.stats = stats
end
private
def stats
departements_sql = "select departement, count(procedures.id) as nb_demarches, sum(procedures.estimated_dossiers_count) as nb_dossiers from services inner join procedures on services.id = procedures.service_id where procedures.hidden_at is null and procedures.aasm_state in ('publiee', 'close', 'depubliee')"
departements_sql += " and procedures.published_at >= '#{@map_filter.year}-01-01' and procedures.published_at <= '#{@map_filter.year}-12-31'" if @map_filter.year.present?
departements_sql += " group by services.departement"
departements = ActiveRecord::Base.connection.execute(ActiveRecord::Base.sanitize_sql(departements_sql))
departements.to_a.reduce(Hash.new({ nb_demarches: 0, nb_dossiers: 0 })) do |h, v|
h.merge(
v["departement"] => {
'nb_demarches': v["nb_demarches"].presence || 0,
'nb_dossiers': v['nb_dossiers'].presence || 0
}
)
end
end
end

View file

@ -0,0 +1,19 @@
module CarteHelper
def svg_path(map_filter, departement, d)
tag.path(
class: "land departement departement#{departement} #{map_filter.css_class_for_departement(departement)}",
'stroke-width': ".5",
d: d,
data: {
departement: name_for_departement(departement),
demarches: map_filter.nb_demarches_for_departement(departement),
dossiers: map_filter.nb_dossiers_for_departement(departement),
action: "mouseenter->map-info#showInfo mouseout->map-info#hideInfo"
}
)
end
def name_for_departement(departement)
"#{departement.upcase} - #{APIGeoService.departement_name(departement.upcase)}"
end
end

View file

@ -0,0 +1,32 @@
import { Controller } from '@hotwired/stimulus';
import { hide, show } from '@utils';
export class MapInfoController extends Controller {
static targets = ['infos', 'departement', 'demarches', 'dossiers'];
declare readonly infosTarget: HTMLDivElement;
declare readonly departementTarget: HTMLDivElement;
declare readonly demarchesTarget: HTMLDivElement;
declare readonly dossiersTarget: HTMLDivElement;
showInfo(event: Event) {
const target = event.target as HTMLElement;
if (target && target.dataset && target.dataset.departement) {
target.setAttribute('stroke-width', '2.5');
this.departementTarget.innerHTML = target.dataset.departement;
this.demarchesTarget.innerHTML = Number(
target.dataset.demarches
).toLocaleString();
this.dossiersTarget.innerHTML = Number(
target.dataset.dossiers
).toLocaleString();
}
show(this.infosTarget);
}
hideInfo(event: Event) {
hide(this.infosTarget);
const target = event.target as HTMLElement;
target.removeAttribute('stroke-width');
}
}

84
app/models/map_filter.rb Normal file
View file

@ -0,0 +1,84 @@
class MapFilter
# https://api.rubyonrails.org/v7.1.1/classes/ActiveModel/Errors.html
include ActiveModel::Conversion
extend ActiveModel::Translation
extend ActiveModel::Naming
LEGEND = {
nb_demarches: { 'nothing': -1, 'small': 20, 'medium': 50, 'large': 100, 'xlarge': 500 },
nb_dossiers: { 'nothing': -1, 'small': 500, 'medium': 2000, 'large': 10000, 'xlarge': 50000 }
}
attr_accessor :stats
attr_reader :errors
def initialize(params)
@params = params[:map_filter]&.permit(:kind, :year) || {}
@errors = ActiveModel::Errors.new(self)
end
def persisted?
false
end
def kind
@params[:kind]&.to_sym || :nb_demarches
end
def year
@params[:year].presence
end
def kind_buttons
LEGEND.keys.map do
{ label: I18n.t("kind.#{_1}", scope:), value: _1 }
end
end
def kind_legend_keys
LEGEND[kind].keys
end
def css_class_for_departement(departement)
if kind == :nb_demarches
kind_legend_keys.reverse.find do
nb_demarches_for_departement(departement) > LEGEND[kind][_1]
end
else
kind_legend_keys.reverse.find do
nb_dossiers_for_departement(departement) > LEGEND[kind][_1]
end
end
end
def nb_demarches_for_departement(departement)
stats[departement.upcase] ? stats[departement.upcase][:nb_demarches] : 0
end
def nb_dossiers_for_departement(departement)
stats[departement.upcase] ? stats[departement.upcase][:nb_dossiers] : 0
end
def legende_for(legende)
limit = LEGEND[kind][legende]
index = LEGEND[kind].keys.index(legende.to_sym)
next_limit = LEGEND[kind].to_a[index + 1]
if next_limit
I18n.t(:legend, min_thresold: limit + 1, max_thresold: next_limit[1], scope:)
else
"> #{limit}"
end
end
def detailed_title
add_on = I18n.t(:specific_year_add_on, year:, scope:) if year
I18n.t("detailed_title_for_#{kind}", add_on:, scope:)
end
private
def scope
'activemodel.attributes.map_filter'
end
end

File diff suppressed because one or more lines are too long

View file

@ -18,6 +18,8 @@
= link_to t("links.footer.suivi.label"), suivi_path, title: t("links.footer.suivi.title"), class: "fr-footer__top-link" = link_to t("links.footer.suivi.label"), suivi_path, title: t("links.footer.suivi.title"), class: "fr-footer__top-link"
%li.fr-footer__top-link %li.fr-footer__top-link
= link_to t("links.footer.stats.label"), stats_path, title: t("links.footer.stats.title"), class: "fr-footer__top-link" = link_to t("links.footer.stats.label"), stats_path, title: t("links.footer.stats.title"), class: "fr-footer__top-link"
%li.fr-footer__top_link
= link_to t("links.footer.carte.label"), carte_path, title: t("links.footer.carte.title"), class: "fr-footer__top-link"
%li.fr-footer__top-link %li.fr-footer__top-link
= link_to t("links.footer.cgu.label"), t("links.footer.cgu.url"), title: t("links.footer.cgu.title"), class: "fr-footer__top-link", rel: "noopener noreferrer" = link_to t("links.footer.cgu.label"), t("links.footer.cgu.url"), title: t("links.footer.cgu.title"), class: "fr-footer__top-link", rel: "noopener noreferrer"
.fr-col-12.fr-col-sm-3.fr-col-md-3 .fr-col-12.fr-col-sm-3.fr-col-md-3

View file

@ -12,6 +12,8 @@
= number_with_delimiter(@procedures_numbers[:total]) = number_with_delimiter(@procedures_numbers[:total])
%span.big-number-card-detail %span.big-number-card-detail
#{number_with_delimiter(@procedures_numbers[:last_30_days_count])} (#{@procedures_numbers[:evolution]} %) sur les 30 derniers jours #{number_with_delimiter(@procedures_numbers[:last_30_days_count])} (#{@procedures_numbers[:evolution]} %) sur les 30 derniers jours
%span.big-number-card-detail
= link_to "Voir carte de déploiement", carte_path
.stat-card.stat-card-half.big-number-card.pull-left .stat-card.stat-card-half.big-number-card.pull-left
%span.big-number-card-title TOTAL DOSSIERS DÉPOSÉS %span.big-number-card-title TOTAL DOSSIERS DÉPOSÉS
@ -19,6 +21,8 @@
= number_with_delimiter(@dossiers_numbers[:total]) = number_with_delimiter(@dossiers_numbers[:total])
%span.big-number-card-detail %span.big-number-card-detail
#{number_with_delimiter(@dossiers_numbers[:last_30_days_count])} (#{@dossiers_numbers[:evolution]} %) sur les 30 derniers jours #{number_with_delimiter(@dossiers_numbers[:last_30_days_count])} (#{@dossiers_numbers[:evolution]} %) sur les 30 derniers jours
%span.big-number-card-detail
= link_to "Voir carte de déploiement", carte_path(map_filter: { kind: :nb_dossiers })
.stat-card.stat-card-half.pull-left .stat-card.stat-card-half.pull-left

View file

@ -100,6 +100,7 @@ ignore_unused:
- 'errors.format' - 'errors.format'
- 'activerecord.models.*' - 'activerecord.models.*'
- 'activerecord.attributes.*' - 'activerecord.attributes.*'
- 'activemodel.attributes.map_filter.*'
- 'activerecord.errors.*' - 'activerecord.errors.*'
- 'errors.messages.blank' - 'errors.messages.blank'
- 'errors.messages.content_type_invalid' - 'errors.messages.content_type_invalid'

View file

@ -61,6 +61,9 @@ en:
label: "Security" label: "Security"
title: "Security policy" title: "Security policy"
url: "https://github.com/betagouv/demarches-simplifiees.fr/blob/main/SECURITY.md" url: "https://github.com/betagouv/demarches-simplifiees.fr/blob/main/SECURITY.md"
carte:
label: "Deployment map"
title: "Deployment map by department"
status_page: status_page:
label: "Disponibility" label: "Disponibility"
title: "Disponibility and availability" title: "Disponibility and availability"

View file

@ -69,6 +69,9 @@ fr:
stats: stats:
label: "Statistiques" label: "Statistiques"
title: "Statistiques dusage" title: "Statistiques dusage"
carte:
label: "Carte de déploiement"
title: "Carte de déploiement par département"
status_page: status_page:
label: "Disponibilité" label: "Disponibilité"
title: "Disponibilité du site demarches-simplifiees" title: "Disponibilité du site demarches-simplifiees"

View file

@ -0,0 +1,15 @@
en:
activemodel:
attributes:
map_filter:
year: Year
from_beginning: From the beginning
specific_year_add_on: "(in %{year})"
kind:
nb_demarches: Number of procedures
nb_dossiers: Number of files
detailed_title_for_nb_demarches: "Number of published or closed procedures %{add_on}"
detailed_title_for_nb_dossiers: "Number of submitted files %{add_on}"
legend: "Between %{min_thresold} and %{max_thresold}"

View file

@ -0,0 +1,14 @@
fr:
activemodel:
attributes:
map_filter:
year: Année
from_beginning: Depuis le début
specific_year_add_on: "(en %{year})"
kind:
nb_demarches: Nombre de démarches
nb_dossiers: Nombre de dossiers
detailed_title_for_nb_demarches: "Nombre de démarches publiées ou closes %{add_on}"
detailed_title_for_nb_dossiers: "Nombre de dossiers déposés %{add_on}"
legend: "Entre %{min_thresold} et %{max_thresold}"

View file

@ -0,0 +1,6 @@
en:
carte:
show:
title: Deployment by department
map_choice_title: Which map do you want ?

View file

@ -0,0 +1,5 @@
fr:
carte:
show:
title: Déploiement par département
map_choice_title: Quelle carte souhaitez vous ?

View file

@ -217,6 +217,8 @@ Rails.application.routes.draw do
get "mentions-legales", to: "static_pages#legal_notice" get "mentions-legales", to: "static_pages#legal_notice"
get "declaration-accessibilite", to: "static_pages#accessibility_statement" get "declaration-accessibilite", to: "static_pages#accessibility_statement"
get "carte", to: "carte#show"
post "webhooks/sendinblue", to: "webhook#sendinblue" post "webhooks/sendinblue", to: "webhook#sendinblue"
post "webhooks/helpscout", to: "webhook#helpscout" post "webhooks/helpscout", to: "webhook#helpscout"
post "webhooks/helpscout_support_dev", to: "webhook#helpscout_support_dev" post "webhooks/helpscout_support_dev", to: "webhook#helpscout_support_dev"

View file

@ -0,0 +1,22 @@
describe CarteController do
describe '#show' do
let(:service) { create(:service, departement: '63') }
let(:service2) { create(:service, departement: '75') }
let(:service3) { create(:service, departement: '75') }
let!(:procedure) { create(:procedure, :published, service:, estimated_dossiers_count: 4) }
let!(:procedure2) { create(:procedure, :published, service: service2, estimated_dossiers_count: 20, published_at: Date.parse('2020-07-14')) }
let!(:procedure3) { create(:procedure, :published, service: service3, estimated_dossiers_count: 30, published_at: Date.parse('2021-07-14')) }
let(:subject) { assigns(:map_filter) }
it 'give stats for each departement' do
get :show
expect(subject.stats['63']).to eq({ nb_demarches: 1, nb_dossiers: 4 })
expect(subject.stats['75']).to eq({ nb_demarches: 2, nb_dossiers: 50 })
end
it 'give stats for each departement for a specific year' do
get :show, params: { map_filter: { year: 2020 } }
expect(subject.stats['75']).to eq({ nb_demarches: 1, nb_dossiers: 20 })
end
end
end

View file

@ -0,0 +1,23 @@
describe MapFilter do
let(:map_filter) do
mf = MapFilter.new(params)
mf.stats = { '63' => { nb_demarches: 51, nb_dossiers: 2001 } }
mf
end
describe 'css_class_for_departement' do
let(:params) { { kind: :nb_demarches } }
context 'for nb_demarches' do
it 'return class css' do
expect(map_filter.css_class_for_departement('63')).to eq :medium
end
end
context 'fr nb_dossiers' do
let(:params) { { kind: :nb_dossiers } }
it 'return class css' do
expect(map_filter.css_class_for_departement('63')).to eq :medium
end
end
end
end