Merge pull request #9701 from demarches-simplifiees/carte
Afficher une carte de déploiement de DS par département
This commit is contained in:
commit
39bdb5f145
18 changed files with 598 additions and 0 deletions
64
app/assets/stylesheets/map_info.scss
Normal file
64
app/assets/stylesheets/map_info.scss
Normal 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;
|
||||
}
|
||||
}
|
23
app/controllers/carte_controller.rb
Normal file
23
app/controllers/carte_controller.rb
Normal 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
|
19
app/helpers/carte_helper.rb
Normal file
19
app/helpers/carte_helper.rb
Normal 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
|
32
app/javascript/controllers/map_info_controller.ts
Normal file
32
app/javascript/controllers/map_info_controller.ts
Normal 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
84
app/models/map_filter.rb
Normal 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
|
276
app/views/carte/show.html.erb
Normal file
276
app/views/carte/show.html.erb
Normal file
File diff suppressed because one or more lines are too long
|
@ -18,6 +18,8 @@
|
|||
= 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
|
||||
= 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
|
||||
= 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
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
= number_with_delimiter(@procedures_numbers[:total])
|
||||
%span.big-number-card-detail
|
||||
#{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
|
||||
%span.big-number-card-title TOTAL DOSSIERS DÉPOSÉS
|
||||
|
@ -19,6 +21,8 @@
|
|||
= number_with_delimiter(@dossiers_numbers[:total])
|
||||
%span.big-number-card-detail
|
||||
#{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
|
||||
|
|
|
@ -100,6 +100,7 @@ ignore_unused:
|
|||
- 'errors.format'
|
||||
- 'activerecord.models.*'
|
||||
- 'activerecord.attributes.*'
|
||||
- 'activemodel.attributes.map_filter.*'
|
||||
- 'activerecord.errors.*'
|
||||
- 'errors.messages.blank'
|
||||
- 'errors.messages.content_type_invalid'
|
||||
|
|
|
@ -61,6 +61,9 @@ en:
|
|||
label: "Security"
|
||||
title: "Security policy"
|
||||
url: "https://github.com/betagouv/demarches-simplifiees.fr/blob/main/SECURITY.md"
|
||||
carte:
|
||||
label: "Deployment map"
|
||||
title: "Deployment map by department"
|
||||
status_page:
|
||||
label: "Disponibility"
|
||||
title: "Disponibility and availability"
|
||||
|
|
|
@ -69,6 +69,9 @@ fr:
|
|||
stats:
|
||||
label: "Statistiques"
|
||||
title: "Statistiques d’usage"
|
||||
carte:
|
||||
label: "Carte de déploiement"
|
||||
title: "Carte de déploiement par département"
|
||||
status_page:
|
||||
label: "Disponibilité"
|
||||
title: "Disponibilité du site demarches-simplifiees"
|
||||
|
|
15
config/locales/models/map_filter/en.yml
Normal file
15
config/locales/models/map_filter/en.yml
Normal 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}"
|
||||
|
14
config/locales/models/map_filter/fr.yml
Normal file
14
config/locales/models/map_filter/fr.yml
Normal 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}"
|
6
config/locales/views/carte.en.yml
Normal file
6
config/locales/views/carte.en.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
en:
|
||||
carte:
|
||||
show:
|
||||
title: Deployment by department
|
||||
map_choice_title: Which map do you want ?
|
||||
|
5
config/locales/views/carte.fr.yml
Normal file
5
config/locales/views/carte.fr.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
fr:
|
||||
carte:
|
||||
show:
|
||||
title: Déploiement par département
|
||||
map_choice_title: Quelle carte souhaitez vous ?
|
|
@ -217,6 +217,8 @@ Rails.application.routes.draw do
|
|||
get "mentions-legales", to: "static_pages#legal_notice"
|
||||
get "declaration-accessibilite", to: "static_pages#accessibility_statement"
|
||||
|
||||
get "carte", to: "carte#show"
|
||||
|
||||
post "webhooks/sendinblue", to: "webhook#sendinblue"
|
||||
post "webhooks/helpscout", to: "webhook#helpscout"
|
||||
post "webhooks/helpscout_support_dev", to: "webhook#helpscout_support_dev"
|
||||
|
|
22
spec/controllers/carte_controller_spec.rb
Normal file
22
spec/controllers/carte_controller_spec.rb
Normal 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
|
23
spec/models/map_filter_spec.rb
Normal file
23
spec/models/map_filter_spec.rb
Normal 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
|
Loading…
Reference in a new issue