diff --git a/README.md b/README.md index 50665adff..500dc1c49 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian FindDubiousProceduresJob.set(cron: "0 0 * * *").perform_later Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").perform_later WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later + GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later ### Voir les emails envoyés en local diff --git a/app/assets/stylesheets/new_design/_constants.scss b/app/assets/stylesheets/new_design/_constants.scss index fca67e46a..0b479f2de 100644 --- a/app/assets/stylesheets/new_design/_constants.scss +++ b/app/assets/stylesheets/new_design/_constants.scss @@ -6,7 +6,7 @@ $default-padding: 2 * $default-spacer; // layouts $two-columns-padding: 60px; -$two-columns-breakpoint: $page-width + (2 * $two-columns-padding); +$two-columns-breakpoint: 980px; // z-order $alert-z-index: 100; diff --git a/app/assets/stylesheets/new_design/contact.scss b/app/assets/stylesheets/new_design/contact.scss index 102ae3a6f..b85f64123 100644 --- a/app/assets/stylesheets/new_design/contact.scss +++ b/app/assets/stylesheets/new_design/contact.scss @@ -2,7 +2,6 @@ $default-space: 15px; $contact-padding: $default-space * 2; #contact-form { - width: 1040px; margin: 0 auto; padding-top: $contact-padding; diff --git a/app/assets/stylesheets/new_design/helpers.scss b/app/assets/stylesheets/new_design/helpers.scss index 976e4c0d1..189bd8252 100644 --- a/app/assets/stylesheets/new_design/helpers.scss +++ b/app/assets/stylesheets/new_design/helpers.scss @@ -3,3 +3,7 @@ .mb-1 { margin-bottom: $default-spacer; } + +.mr-1 { + margin-right: $default-spacer; +} diff --git a/app/assets/stylesheets/new_design/layouts.scss b/app/assets/stylesheets/new_design/layouts.scss index fda1ff4ad..a5eee6808 100644 --- a/app/assets/stylesheets/new_design/layouts.scss +++ b/app/assets/stylesheets/new_design/layouts.scss @@ -13,29 +13,32 @@ @extend .container; display: flex; flex-direction: column; + justify-content: center; @media (min-width: $two-columns-breakpoint) { flex-direction: row; align-items: stretch; - justify-content: center; + justify-content: space-between; } } .column { - padding: $two-columns-padding 0 0; width: 100%; max-width: 500px; + margin: 0 auto; + padding: ($default-padding * 2) 0 0 0; + + &:last-of-type { + padding-bottom: $default-padding * 2; + } @media (min-width: $two-columns-breakpoint) { - padding: $two-columns-padding; - width: 50%; + width: 45%; + margin: 0; + padding: $two-columns-padding 0 $two-columns-padding 0; - &:first-child { - padding-left: 0; - } - - &:last-child { - padding-right: 0; + &:last-of-type { + padding-bottom: $two-columns-padding; } } } diff --git a/app/assets/stylesheets/new_design/procedure_context.scss b/app/assets/stylesheets/new_design/procedure_context.scss index 714097652..dccd52c9b 100644 --- a/app/assets/stylesheets/new_design/procedure_context.scss +++ b/app/assets/stylesheets/new_design/procedure_context.scss @@ -26,17 +26,19 @@ $procedure-context-breakpoint: $two-columns-breakpoint; .procedure-title { font-size: 30px; - margin: 20px 0 0; + margin: 0 0 20px 0; + text-align: center; @media (min-width: $procedure-context-breakpoint) { margin: 50px 0 32px; + text-align: left; } } .procedure-description { font-size: 16px; - p { + p:not(:last-of-type) { margin-bottom: 2 * $default-spacer; } @@ -52,7 +54,7 @@ $procedure-context-breakpoint: $two-columns-breakpoint; img { max-height: 50px; max-width: 100%; - margin: 0 10px; + margin: 0 10px 20px 10px; @media (min-width: $procedure-context-breakpoint) { max-height: 130px; diff --git a/app/assets/stylesheets/new_design/procedure_show.scss b/app/assets/stylesheets/new_design/procedure_show.scss index 15a89047e..272cd985d 100644 --- a/app/assets/stylesheets/new_design/procedure_show.scss +++ b/app/assets/stylesheets/new_design/procedure_show.scss @@ -6,7 +6,12 @@ h1 { color: $black; font-size: 22px; - margin-bottom: 2 * $default-padding; + margin-bottom: 1 * $default-padding; + } + + a.notifications { + display: inline-block; + margin-bottom: 1 * $default-padding; } .dossiers-table { diff --git a/app/controllers/new_gestionnaire/procedures_controller.rb b/app/controllers/new_gestionnaire/procedures_controller.rb index 428543ef6..46de9ce85 100644 --- a/app/controllers/new_gestionnaire/procedures_controller.rb +++ b/app/controllers/new_gestionnaire/procedures_controller.rb @@ -186,6 +186,18 @@ module NewGestionnaire end end + def email_notifications + @procedure = procedure + @assign_to = assign_to + end + + def update_email_notifications + assign_to.update!(email_notifications_enabled: params[:assign_to][:email_notifications_enabled]) + + flash.notice = 'Vos notifications sont enregistrées.' + redirect_to gestionnaire_procedure_path(procedure) + end + private def find_field(table, column) @@ -196,6 +208,10 @@ module NewGestionnaire field.values_at('table', 'column').join('/') end + def assign_to + current_gestionnaire.assign_to.find_by(procedure: procedure) + end + def statut @statut ||= (params[:statut].presence || 'a-suivre') end diff --git a/app/jobs/gestionnaire_email_notification_job.rb b/app/jobs/gestionnaire_email_notification_job.rb new file mode 100644 index 000000000..1920457df --- /dev/null +++ b/app/jobs/gestionnaire_email_notification_job.rb @@ -0,0 +1,7 @@ +class GestionnaireEmailNotificationJob < ApplicationJob + queue_as :cron + + def perform(*args) + NotificationService.send_gestionnaire_email_notification + end +end diff --git a/app/mailers/gestionnaire_mailer.rb b/app/mailers/gestionnaire_mailer.rb index 09f7af238..472f09dae 100644 --- a/app/mailers/gestionnaire_mailer.rb +++ b/app/mailers/gestionnaire_mailer.rb @@ -43,4 +43,11 @@ class GestionnaireMailer < ApplicationMailer mail(to: gestionnaire.email, subject: subject) end + + def send_notifications(gestionnaire, data) + @data = data + subject = "Vous avez du nouveau sur vos démarches" + + mail(to: gestionnaire.email, subject: subject) + end end diff --git a/app/models/assign_to.rb b/app/models/assign_to.rb index 1e5621fe3..43da53ea3 100644 --- a/app/models/assign_to.rb +++ b/app/models/assign_to.rb @@ -3,6 +3,8 @@ class AssignTo < ApplicationRecord belongs_to :gestionnaire has_one :procedure_presentation, dependent: :destroy + scope :with_email_notifications, -> { where(email_notifications_enabled: true) } + def procedure_presentation_or_default_and_errors errors = reset_procedure_presentation_if_invalid [procedure_presentation || build_procedure_presentation, errors] diff --git a/app/models/gestionnaire.rb b/app/models/gestionnaire.rb index 82516d4de..9bfa8e211 100644 --- a/app/models/gestionnaire.rb +++ b/app/models/gestionnaire.rb @@ -11,6 +11,10 @@ class Gestionnaire < ApplicationRecord has_many :assign_to, dependent: :destroy has_many :procedures, through: :assign_to + + has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo' + has_many :procedures_with_email_notifications, through: :assign_to_with_email_notifications, source: :procedure + has_many :dossiers, -> { state_not_brouillon }, through: :procedures has_many :follows has_many :followed_dossiers, through: :follows, source: :dossier @@ -205,6 +209,25 @@ class Gestionnaire < ApplicationRecord trusted_device_token&.token_young? end + def email_notification_data + procedures_with_email_notifications + .reduce([]) do |acc, procedure| + + h = { + nb_en_construction: procedure.dossiers.en_construction.count, + nb_notification: notifications_per_procedure(procedure).count + } + + if h[:nb_en_construction] > 0 || h[:nb_notification] > 0 + h[:procedure_id] = procedure.id + h[:procedure_libelle] = procedure.libelle + acc << h + end + + acc + end + end + private def annotations_hash(demande, annotations_privees, avis, messagerie) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb new file mode 100644 index 000000000..da8a1bb44 --- /dev/null +++ b/app/services/notification_service.rb @@ -0,0 +1,19 @@ +class NotificationService + class << self + def send_gestionnaire_email_notification + Gestionnaire + .includes(assign_to: { procedure: :dossiers }) + .where(assign_tos: { email_notifications_enabled: true }) + .find_in_batches { |gestionnaires| send_batch_of_gestionnaires_email_notification(gestionnaires) } + end + + private + + def send_batch_of_gestionnaires_email_notification(gestionnaires) + gestionnaires + .map { |gestionnaire| [gestionnaire, gestionnaire.email_notification_data] } + .reject { |(_gestionnaire, data)| data.empty? } + .each { |(gestionnaire, data)| GestionnaireMailer.send_notifications(gestionnaire, data).deliver_later } + end + end +end diff --git a/app/views/admin/procedures/_informations.html.haml b/app/views/admin/procedures/_informations.html.haml index 9f0e0815e..cc2566d3a 100644 --- a/app/views/admin/procedures/_informations.html.haml +++ b/app/views/admin/procedures/_informations.html.haml @@ -107,11 +107,6 @@ qui doivent donc s'identifier en tant que personne physique. %ul#individual-with-siret - %li - .checkbox - %label - = f.check_box :individual_with_siret - Donner la possibilité de renseigner un SIRET au cours de la construction du dossier. %li .checkbox %label diff --git a/app/views/gestionnaire_mailer/send_notifications.html.haml b/app/views/gestionnaire_mailer/send_notifications.html.haml new file mode 100644 index 000000000..50f76c55f --- /dev/null +++ b/app/views/gestionnaire_mailer/send_notifications.html.haml @@ -0,0 +1,20 @@ +- content_for(:title, 'Du nouveau sur vos démarches') + +%p + Bonjour, + +%p + Vous avez du nouveau sur demarches-simplifiees.fr. + +%ul + - @data.each do |datum| + %li + = link_to(datum[:procedure_libelle], procedure_url(datum[:procedure_id])) + - if datum[:nb_en_construction] > 0 + %br + #{datum[:nb_en_construction]} #{'dossier'.pluralize(datum[:nb_en_construction])} en construction + - if datum[:nb_notification] > 0 + %br + #{datum[:nb_notification]} #{'notification'.pluralize(datum[:nb_notification])} + += render partial: "layouts/mailers/signature" diff --git a/app/views/new_gestionnaire/procedures/email_notifications.html.haml b/app/views/new_gestionnaire/procedures/email_notifications.html.haml new file mode 100644 index 000000000..751be27bf --- /dev/null +++ b/app/views/new_gestionnaire/procedures/email_notifications.html.haml @@ -0,0 +1,24 @@ +- content_for(:title, "Notifications pour #{@procedure.libelle}") + += render partial: 'new_administrateur/breadcrumbs', + locals: { steps: [link_to(@procedure.libelle, procedure_path(@procedure)), + 'Notifications'] } + +.container + %h1 Notifications par email + + = form_for @assign_to, url: update_email_notifications_gestionnaire_procedure_path(@procedure), html: { class: 'form' } do |form| + = form.label :email_notification, "Recevoir une fois par jour, du lundi au samedi vers 10 h, un email de notification me signalant le dépôt de nouveaux dossiers ou des changements sur mes dossiers en cours" + + .radios + %label + = form.radio_button :email_notifications_enabled, true + Oui + + %label + = form.radio_button :email_notifications_enabled, false + Non + + .send-wrapper + = link_to "Revenir à la procédure", gestionnaire_procedure_path(@procedure), class: 'button mr-1' + = form.submit "Enregistrer", class: "button primary" diff --git a/app/views/new_gestionnaire/procedures/show.html.haml b/app/views/new_gestionnaire/procedures/show.html.haml index 003929fd6..c10fa3b8a 100644 --- a/app/views/new_gestionnaire/procedures/show.html.haml +++ b/app/views/new_gestionnaire/procedures/show.html.haml @@ -9,6 +9,8 @@ .procedure-header %h1= procedure_libelle @procedure + = link_to 'configurez vos notifications', email_notifications_gestionnaire_procedure_path(@procedure), class: 'notifications' + %ul.tabs = tab_item('à suivre', @@ -64,7 +66,7 @@ - if i > 0 ou %span.filter - = link_to remove_filter_gestionnaire_procedure_path(@procedure, statut: @statut, table: filter['table'], column: filter['column'], value: filter['value']) do + = link_to remove_filter_gestionnaire_procedure_path(@procedure, { statut: @statut, table: filter['table'], column: filter['column'], value: filter['value'] }) do %img.close-icon{ src: image_url("close.svg") } = "#{filter['label'].truncate(50)} : #{filter['value']}" %table.table.dossiers-table.hoverable diff --git a/config/routes.rb b/config/routes.rb index ddb0e18d5..3a9bc9966 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -313,8 +313,10 @@ Rails.application.routes.draw do patch 'update_displayed_fields' get 'update_sort/:table/:column' => 'procedures#update_sort', as: 'update_sort' post 'add_filter' - get 'remove_filter/:statut/:table/:column/:value' => 'procedures#remove_filter', as: 'remove_filter' + get 'remove_filter' => 'procedures#remove_filter', as: 'remove_filter' get 'download_dossiers' + get 'email_notifications' + patch 'update_email_notifications' resources :dossiers, only: [:show], param: :dossier_id do member do diff --git a/db/migrate/20190318154812_add_email_notifications_column_to_assign_to.rb b/db/migrate/20190318154812_add_email_notifications_column_to_assign_to.rb new file mode 100644 index 000000000..9443922fc --- /dev/null +++ b/db/migrate/20190318154812_add_email_notifications_column_to_assign_to.rb @@ -0,0 +1,5 @@ +class AddEmailNotificationsColumnToAssignTo < ActiveRecord::Migration[5.2] + def change + add_column :assign_tos, :email_notifications_enabled, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 9cb1ebfe5..6b3b6e255 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_03_11_140926) do +ActiveRecord::Schema.define(version: 2019_03_18_154812) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -99,6 +99,7 @@ ActiveRecord::Schema.define(version: 2019_03_11_140926) do t.integer "procedure_id" t.datetime "created_at" t.datetime "updated_at" + t.boolean "email_notifications_enabled", default: false, null: false t.index ["gestionnaire_id", "procedure_id"], name: "index_assign_tos_on_gestionnaire_id_and_procedure_id", unique: true t.index ["gestionnaire_id"], name: "index_assign_tos_on_gestionnaire_id" t.index ["procedure_id"], name: "index_assign_tos_on_procedure_id" diff --git a/spec/controllers/new_gestionnaire/procedures_controller_spec.rb b/spec/controllers/new_gestionnaire/procedures_controller_spec.rb index 772a9bcff..91d6f8886 100644 --- a/spec/controllers/new_gestionnaire/procedures_controller_spec.rb +++ b/spec/controllers/new_gestionnaire/procedures_controller_spec.rb @@ -336,4 +336,25 @@ describe NewGestionnaire::ProceduresController, type: :controller do end end end + + describe '#update_email_notifications' do + let(:gestionnaire) { create(:gestionnaire) } + let!(:procedure) { create(:procedure, gestionnaires: [gestionnaire]) } + + context "when logged in" do + before { sign_in(gestionnaire) } + + it { expect(gestionnaire.procedures_with_email_notifications).to be_empty } + + context 'when the gestionnaire update its preferences' do + let(:assign_to) { gestionnaire.assign_to.find_by(procedure: procedure) } + + before do + patch :update_email_notifications, params: { procedure_id: procedure.id, assign_to: { id: assign_to.id, email_notifications_enabled: true } } + end + + it { expect(gestionnaire.procedures_with_email_notifications).to eq([procedure]) } + end + end + end end diff --git a/spec/features/new_gestionnaire/procedure_filters_spec.rb b/spec/features/new_gestionnaire/procedure_filters_spec.rb index 2868855c8..8ac88e567 100644 --- a/spec/features/new_gestionnaire/procedure_filters_spec.rb +++ b/spec/features/new_gestionnaire/procedure_filters_spec.rb @@ -115,7 +115,7 @@ feature "procedure filters" do end def remove_filter(filter_value) - find(:xpath, "(//span[contains(@class, 'filter')]/a[contains(@href, '#{URI.encode(filter_value)}')])[1]").click + find(:xpath, "(//span[contains(@class, 'filter')]/a[contains(@href, '#{CGI.escape(filter_value)}')])[1]").click end def add_filter(column_name, filter_value) diff --git a/spec/mailers/previews/gestionnaire_mailer_preview.rb b/spec/mailers/previews/gestionnaire_mailer_preview.rb index ddafd66b4..eda8b78aa 100644 --- a/spec/mailers/previews/gestionnaire_mailer_preview.rb +++ b/spec/mailers/previews/gestionnaire_mailer_preview.rb @@ -19,6 +19,24 @@ class GestionnaireMailerPreview < ActionMailer::Preview GestionnaireMailer.user_to_gestionnaire(gestionnaire.email) end + def send_notifications + data = [ + { + procedure_libelle: 'une superbe démarche', + procedure_id: 213, + nb_en_construction: 2, + nb_notification: 2 + }, + { + procedure_libelle: 'une démarche incroyable', + procedure_id: 213, + nb_en_construction: 1, + nb_notification: 1 + } + ] + GestionnaireMailer.send_notifications(gestionnaire, data) + end + private def gestionnaire @@ -30,6 +48,10 @@ class GestionnaireMailerPreview < ActionMailer::Preview end def procedure - Procedure.new(id: 15) + Procedure.new(id: 15, libelle: 'libelle') + end + + def dossier + Dossier.new(id: 15, procedure: procedure) end end diff --git a/spec/models/gestionnaire_spec.rb b/spec/models/gestionnaire_spec.rb index 6b7a6febb..ee3cfbafa 100644 --- a/spec/models/gestionnaire_spec.rb +++ b/spec/models/gestionnaire_spec.rb @@ -413,6 +413,53 @@ describe Gestionnaire, type: :model do end end + describe '#email_notification_data' do + let(:gestionnaire) { create(:gestionnaire) } + let(:procedure_to_assign) { create(:procedure) } + + before do + create(:assign_to, gestionnaire: gestionnaire, procedure: procedure_to_assign, email_notifications_enabled: true) + end + + context 'when a dossier in construction exists' do + let!(:dossier) { create(:dossier, procedure: procedure_to_assign, state: Dossier.states.fetch(:en_construction)) } + + it do + expect(gestionnaire.email_notification_data).to eq([ + { + nb_en_construction: 1, + nb_notification: 0, + procedure_id: procedure_to_assign.id, + procedure_libelle: procedure_to_assign.libelle + } + ]) + end + end + + context 'when a notification exists' do + before do + allow(gestionnaire).to receive(:notifications_per_procedure) + .with(procedure_to_assign) + .and_return([1, 2, 3]) + end + + it do + expect(gestionnaire.email_notification_data).to eq([ + { + nb_en_construction: 0, + nb_notification: 3, + procedure_id: procedure_to_assign.id, + procedure_libelle: procedure_to_assign.libelle + } + ]) + end + end + + context 'otherwise' do + it { expect(gestionnaire.email_notification_data).to eq([]) } + end + end + private def assign(procedure_to_assign) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb new file mode 100644 index 000000000..7c9a9e116 --- /dev/null +++ b/spec/services/notification_service_spec.rb @@ -0,0 +1,63 @@ +describe NotificationService do + describe '.send_gestionnaire_email_notification' do + let(:procedure) { create(:procedure) } + + before do + allow(GestionnaireMailer).to receive(:send_notifications) + .and_return(double(deliver_later: true)) + end + + subject { NotificationService.send_gestionnaire_email_notification } + + context 'when a gestionnaire does not enable its email notification' do + let!(:dossier) { create(:dossier, :en_construction, procedure: procedure) } + let(:gestionnaire) { create(:gestionnaire) } + + before { create(:assign_to, gestionnaire: gestionnaire, procedure: procedure) } + + it do + subject + expect(GestionnaireMailer).not_to have_received(:send_notifications) + end + end + + context 'when a gestionnaire enables its email_notification on one procedure' do + let(:gestionnaire_with_email_notifications) { create(:gestionnaire) } + + before do + create(:assign_to, + gestionnaire: gestionnaire_with_email_notifications, + procedure: procedure, + email_notifications_enabled: true) + end + + context "when there is no activity on the gestionnaire's procedures" do + it do + subject + expect(GestionnaireMailer).not_to have_received(:send_notifications) + end + end + + context 'when a dossier en construction exists on this procedure' do + let!(:dossier) { create(:dossier, :en_construction, procedure: procedure) } + + it do + subject + expect(GestionnaireMailer).to have_received(:send_notifications) + end + end + + context 'when there is a notification on this procedure' do + before do + allow_any_instance_of(Gestionnaire).to receive(:notifications_per_procedure) + .and_return([12]) + end + + it do + subject + expect(GestionnaireMailer).to have_received(:send_notifications) + end + end + end + end +end diff --git a/spec/views/gestionnaire_mailer/send_notifications.html.haml_spec.rb b/spec/views/gestionnaire_mailer/send_notifications.html.haml_spec.rb new file mode 100644 index 000000000..df3fbbecc --- /dev/null +++ b/spec/views/gestionnaire_mailer/send_notifications.html.haml_spec.rb @@ -0,0 +1,45 @@ +require 'rails_helper' + +describe 'gestionnaire_mailer/send_notifications.html.haml', type: :view do + let(:gestionnaire) { create(:gestionnaire) } + + before do + assign(:data, data) + + render + end + + context 'when there is one dossier in contruction' do + let(:data) do + [ + { + procedure_libelle: 'une superbe démarche', + procedure_id: 213, + nb_en_construction: 1, + nb_notification: 0 + } + ] + end + + it { expect(rendered).to have_link('une superbe démarche', href: procedure_url(213)) } + it { expect(rendered).to have_text('une superbe démarche') } + it { expect(rendered).to have_text('1 dossier en construction') } + it { expect(rendered).not_to have_text('notification') } + end + + context 'when there is one notification' do + let(:data) do + [ + { + procedure_libelle: 'une superbe démarche', + procedure_id: 213, + nb_en_construction: 0, + nb_notification: 1 + } + ] + end + + it { expect(rendered).not_to have_text('en construction') } + it { expect(rendered).to have_text('1 notification') } + end +end