Skip to content

Commit

Permalink
Première version de la prise de rdv par intégration (#5006)
Browse files Browse the repository at this point in the history
* wip: trying to add radio buttons

* wip

* fix

* nicer radio buttons, still need proper field name

* wip

* Add location type

* wip

* wip

* wip

* wip

* fix

* Extend integration spec for rdv_plan

* fix user update

* wip

* anonymizer fix

* update brakeman ignore

* fix

* remove unused css

* Add policy call for lieux

* fix

* wip

* Vérifie dans les specs qu'on envoie les bons mails

---------

Co-authored-by: François Ferrandis <[email protected]>
  • Loading branch information
victormours and francois-ferrandis authored Jan 29, 2025
1 parent b4751b8 commit b978726
Show file tree
Hide file tree
Showing 23 changed files with 458 additions and 17 deletions.
73 changes: 71 additions & 2 deletions app/controllers/agents/rdv_plans_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class Agents::RdvPlansController < AgentAuthController
layout "application"
before_action :find_rdv_plan
before_action :redirect_to_rdv, if: -> { @rdv_plan.rdv.present? }, except: [:rdv]

def show
redirect_to edit_starts_at_agents_rdv_plan_path(@rdv_plan)
Expand All @@ -12,10 +13,73 @@ def edit_starts_at

def update_starts_at
@rdv_plan.update!(params.require(:rdv_plan).permit(:starts_at).merge(rdv_agent: current_agent))
redirect_to show_starts_at_agents_rdv_plan_path(@rdv_plan)
redirect_to edit_modalites_agents_rdv_plan_path(@rdv_plan)
end

def show_starts_at; end
def edit_modalites; end

def update_modalites
rdv_plan_params = params.require(:rdv_plan).permit(:starts_at, :modalite)

if @rdv_plan.update(rdv_plan_params)
redirect_to edit_motif_agents_rdv_plan_path(@rdv_plan)
else
render "edit_modalites"
end
end

def edit_motif
@motifs = current_agent.motifs.individuel.where(
organisation_id: current_agent.roles.select(:organisation_id),
location_type: @rdv_plan.location_type,
service: @rdv_plan.rdv_agent.services
)
if @motifs.count == 1
@rdv_plan.motif_id ||= @motifs.first.id
end
@rdv_plan.duration_in_minutes ||= @motifs.first.default_duration_in_min
end

def update_motif
rdv_plan_params = params.require(:rdv_plan).permit(:motif_id, :duration_in_minutes)

if @rdv_plan.update(rdv_plan_params)
redirect_to edit_user_agents_rdv_plan_path(@rdv_plan)
else
render "edit_motif_from_calendar"
end
end

def edit_user; end

def create_rdv
rdv_plan_params = params.require(:rdv_plan)

user_attributes = rdv_plan_params.require(:user).permit(:email, :phone_number)

# TODO: possible à mettre en commun ?
participation_attributes = if @rdv_plan.motif.visible_and_notified?
rdv_plan_params.require(:participation).permit(
:send_lifecycle_notifications, :send_reminder_notification
)
else
{ send_lifecycle_notifications: false, send_reminder_notification: false }
end

rdv = @rdv_plan.create_rdv(user_attributes:, participation_attributes:)

if rdv.valid?
flash[:success] = "Le rendez-vous a été créé."
redirect_to rdv_agents_rdv_plan_path(@rdv_plan)
else
flash[:error] = rdv.errors.full_messages.to_sentence
redirect_to edit_user_agents_rdv_plan_path(@rdv_plan)
end
end

def rdv
@rdv = @rdv_plan.rdv
end

private

Expand All @@ -24,6 +88,11 @@ def find_rdv_plan
authorize @rdv_plan, :edit?, policy_class: Agent::RdvPlanPolicy
end

def redirect_to_rdv
# TODO: ajouter un flash ici?
redirect_to rdv_agents_rdv_plan_path(@rdv_plan)
end

def pundit_user
current_agent
end
Expand Down
16 changes: 9 additions & 7 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,17 @@ def alert_dsfr_class_for(alert)
end
end

def datetime_input(form, field, input_html: {})
def datetime_input(form, field, input_html: {}, options: {})
form.input(
field,
as: :string,
input_html: {
value: form.object.send(field)&.strftime("%d/%m/%Y %H:%M"),
data: { behaviour: "datetimepicker" },
autocomplete: "off",
}.deep_merge(input_html)
{
as: :string,
input_html: {
value: form.object.send(field)&.strftime("%d/%m/%Y %H:%M"),
data: { behaviour: "datetimepicker" },
autocomplete: "off",
}.deep_merge(input_html),
}.deep_merge(options)
)
end

Expand Down
41 changes: 41 additions & 0 deletions app/models/rdv_plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,47 @@ class RdvPlan < ApplicationRecord

validate :return_url_is_authorized

# TODO: mettre en commun avec les motifs et ajouter une validation de synchro
enum :location_type, { public_office: "public_office", phone: "phone", home: "home", visio: "visio" }

def modalite
if location_type == "public_office"
"#{location_type}-#{lieu&.id}"
else
location_type
end
end

def modalite=(modalite)
self.location_type, self.lieu_id = modalite.split("-")
end

def create_rdv(user_attributes:, participation_attributes:)
if user.encrypted_password.blank? # Pour mettre à jour l'email sans renvoyer de mail de confirmation
user.skip_confirmation_notification!
user.skip_reconfirmation!
end
user.update!(user_attributes)

rdv = Rdv.create(
agents: [rdv_agent],
participations: [Participation.new(participation_attributes.merge(user_id: user.id))],
motif: motif,
organisation: organisation,
lieu: lieu,
starts_at: starts_at,
created_by: planning_agent,
ends_at: starts_at + (duration_in_minutes || motif.default_duration_in_min).minutes
)

if rdv.persisted?
update(rdv: rdv)
Notifiers::RdvCreated.perform_with(rdv, planning_agent)
end

rdv
end

private

def return_url_is_authorized
Expand Down
2 changes: 1 addition & 1 deletion app/models/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Service < ApplicationRecord
validates :name, :short_name, presence: true, uniqueness: { case_sensitive: false }

# Scopes
default_scope { order(Arel.sql("unaccent(LOWER(name))")) }
default_scope { order(Arel.sql("unaccent(LOWER(services.name))")) }

## -

Expand Down
11 changes: 10 additions & 1 deletion app/policies/agent/rdv_plan_policy.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Agent::RdvPlanPolicy < ApplicationPolicy
def create?
pundit_user == record.planning_agent
authorized_lieu && pundit_user == record.planning_agent
end
alias edit? create?

Expand All @@ -9,4 +9,13 @@ def resolve
scope.where(planning_agent: pundit_user)
end
end

private

# TODO: ajouter une spec pour ce cas
def authorized_lieu
return true unless record.lieu_id

Agent::LieuPolicy::Scope.new(pundit_user, Lieu.enabled).resolve.find_by(id: record.lieu_id).present?
end
end
48 changes: 48 additions & 0 deletions app/views/agents/rdv_plans/_modalites_field.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
- available_location_types = rdv_plan.rdv_agent.motifs.individuel.active.where(organisation_id: rdv_plan.rdv_agent.roles.select(:organisation_id)).pluck(:location_type)

fieldset[class="fr-fieldset" id="rdv_plan[modalite]" aria-labelledby="radio-rich-no-pictogram-inline-legend radio-rich-no-pictogram-inline-messages"]
legend[class="fr-fieldset__legend--regular fr-fieldset__legend" id="radio-rich-no-pictogram-inline-legend"]
| Comment souhaitez-vous faire le rendez-vous ?

- if available_location_types.include?("public_office")
- lieux = policy_scope(Lieu.enabled, policy_scope_class: Agent::LieuPolicy::Scope)
- lieux.each do |lieu|
.fr-fieldset__element
.fr-radio-group.fr-radio-rich
input[value="public_office-#{lieu.id}" type="radio" id="public_office-#{lieu.id}" name="rdv_plan[modalite]" required]
label.fr-label[for="public_office-#{lieu.id}"]
span
= lieu_icon
= " Sur place à #{lieu.name}"
span.fr-hint-text = lieu.address

- if available_location_types.include?("phone")
.fr-fieldset__element
.fr-radio-group.fr-radio-rich
input[value="phone" type="radio" id="rdv_plan_modalite_phone" name="rdv_plan[modalite]" required]
label.fr-label[for="rdv_plan_modalite_phone"]
span
= phone_icon
= " Par téléphone"

- if available_location_types.include?("visio")
.fr-fieldset__element
.fr-radio-group.fr-radio-rich
input[value="visio" type="radio" id="rdv_plan_modalite_visio" name="rdv_plan[modalite]" required]
label.fr-label[for="rdv_plan_modalite_visio"]
span
= visio_icon
= " Par visioconférence"
span.fr-hint-text
| Nous vous fournirons un lien de visio

- if available_location_types.include?("home")
.fr-fieldset__element
.fr-radio-group.fr-radio-rich
input[value="home" type="radio" id="rdv_plan_modalite_home" name="rdv_plan[modalite]" required]
label.fr-label[for="rdv_plan_modalite_home"]
span
= home_icon
| Au domicile de l'usager

div[class="fr-messages-group" id="radio-rich-no-pictogram-inline-messages" aria-live="polite"]
18 changes: 18 additions & 0 deletions app/views/agents/rdv_plans/edit_modalites.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.fr-row
.fr-col-12
= render "agents/rdv_plans/header", rdv_plan: @rdv_plan, current_step: 1, step_title: "Créneau", next_step_title: "Motif"
.row.fr-mb-16w
.col-12.col-md-4
= render "agents/rdv_plans/calendar", rdv_plan: @rdv_plan, single_day: true
= link_to edit_starts_at_agents_rdv_plan_path(@rdv_plan) do
= "retour"

.col-12.col-md-4
= simple_form_for @rdv_plan, url: update_modalites_agents_rdv_plan_path(@rdv_plan) do |f|
.fr-mb-4w
.input-group
= datetime_input(f, :starts_at, options: { label_html: { style: "flex-shrink: 0;", class: "fr-mr-2w" }, wrapper_html: { class: "d-flex", style: "align-items: baseline;" }})

= render "agents/rdv_plans/modalites_field", rdv_plan: @rdv_plan

= f.submit "Continuer", class: "fr-btn float-right"
23 changes: 23 additions & 0 deletions app/views/agents/rdv_plans/edit_motif.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.fr-row
.fr-col-12
= render "agents/rdv_plans/header", rdv_plan: @rdv_plan, current_step: 2, step_title: "Motif", next_step_title: "Usager"
.row.fr-mb-16w
.col-4
= render "agents/rdv_plans/calendar", rdv_plan: @rdv_plan, single_day: true
= link_to edit_modalites_agents_rdv_plan_path(@rdv_plan) do
= back_icon(class: "fr-mr-1w")
= "retour"

.col-12.col-md-6
= render "agents/rdv_plans/summary/starts_at", rdv_plan: @rdv_plan
= render "agents/rdv_plans/summary/location_type", rdv_plan: @rdv_plan
hr.fr-mt-2w.fr-mb-4w

= simple_form_for @rdv_plan, url: update_motif_agents_rdv_plan_path(@rdv_plan) do |f|
= render "model_errors", model: @rdv_plan

.input-group.fr-mb-3w
= f.input :motif_id, label: "Motif du rendez-vous", required: true, collection: @motifs, label_method: :name, label_html: { class: "fr-mr-2w" }, include_blank: true

= f.input :duration_in_minutes, label: "Durée en minutes"
= f.submit "Continuer", class: "fr-btn float-right"
31 changes: 31 additions & 0 deletions app/views/agents/rdv_plans/edit_user.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.row.justify-content-md-center
.col-12
= render "agents/rdv_plans/header", rdv_plan: @rdv_plan, current_step: 3, step_title: "Usager", next_step_title: ""
.row.fr-mb-16w
.col-12.col-md-4
= render "agents/rdv_plans/calendar", rdv_plan: @rdv_plan, single_day: true
= link_to edit_motif_agents_rdv_plan_path(@rdv_plan) do
= back_icon(class: "fr-mr-1w")
= "retour"
.col-12.col-md-8
= render "agents/rdv_plans/summary/starts_at", rdv_plan: @rdv_plan
= render "agents/rdv_plans/summary/location_type", rdv_plan: @rdv_plan
= render "agents/rdv_plans/summary/motif", rdv_plan: @rdv_plan

hr.fr-mt-2w
h3= @rdv_plan.user.full_name
= simple_form_for @rdv_plan, url: create_rdv_agents_rdv_plan_path(@rdv_plan), method: :post do |f|
= f.fields_for :user, @rdv_plan.user do |ff|
.row
.col-6
/ TODO: gérer le cas des users confirmed (et gérer ce cas dans l'interface principale aussi) : leur nouvelle adresse mail ne sera pas prise en compte tant qu'ils ne la confirment pas
= ff.input :email
.col-6
= ff.input :phone_number
- if @rdv_plan.motif.visible_and_notified?
= f.fields_for :participation, (Participation.new(send_lifecycle_notifications: true, send_reminder_notification: true)) do |ff|
= ff.input :send_lifecycle_notifications, label: "Envoyer une notification de confirmation"
= ff.input :send_reminder_notification, label: "Envoyer une notification de rappel 48h avant le rendez-vous"
- else
| Les notifications ne sont pas envoyées pour ce motif de rendez-vous.
= f.submit "Confirmer le rendez-vous", class: "fr-btn float-right fr-btn--lg"
33 changes: 33 additions & 0 deletions app/views/agents/rdv_plans/rdv.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.fr-grid-row.justify-content-md-center.fr-mb-16w
.fr-col-8
h1.fr-h2 Rendez-vous confirmé
.card
.card-body
div
span.fr-icon-calendar-fill.fr-mr-1w[aria-hidden="true"]
= "#{l(@rdv.starts_at, format: :human)} (#{@rdv.duration_in_min} minutes)"
= " "
= dsfr_link_to "voir dans l'agenda", admin_organisation_agent_agenda_path(@rdv.organisation, @rdv.agents.first, selected_event_id: @rdv.id, date: @rdv.starts_at.to_date), target: :blank

.fr-mt-2w
- if @rdv.lieu
= lieu_icon(class: "fr-mr-1w")
= @rdv.lieu.name
br
.fr-ml-4w= @rdv.lieu.address
/ TODO: gérer les autres location types
.fr-mt-2w
= motif_icon(class: "fr-mr-1w")
= "Motif : #{@rdv.motif.name}"

.fr-mt-2w
span.fr-icon-user-fill.fr-mr-1w[aria-hidden="true"]
= @rdv_plan.user.full_name
.fr-ml-4w= @rdv_plan.user.email
.fr-ml-4w= @rdv_plan.user.phone_number

.fr-mt-4w
= link_to "Modifier", edit_admin_organisation_rdv_path(@rdv.organisation, @rdv), class: "fr-btn fr-btn--secondary", target: :blank
- if @rdv_plan.return_url
= link_to "Retour sur Mon Suivi Social", @rdv_plan.return_url, class: "float-right"
12 changes: 12 additions & 0 deletions app/views/agents/rdv_plans/summary/_location_type.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/ locals(rdv_plan:)
br
- if rdv_plan.public_office?
= lieu_icon(class: "fr-mr-1w")
= rdv_plan.lieu.name
- elsif rdv_plan.phone?
= phone_icon(class: "fr-mr-1w")
= "Par téléphone"
- elsif rdv_plan.visio?
= visio_icon(class: "fr-mr-1w")
= "Par visio"
= link_to "modifier", edit_modalites_agents_rdv_plan_path(rdv_plan), class: "fr-ml-2w"
5 changes: 5 additions & 0 deletions app/views/agents/rdv_plans/summary/_motif.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/ locals(rdv_plan:)
br
= motif_icon(class: "fr-mr-1w")
= rdv_plan.motif.name
= link_to "modifier", edit_motif_agents_rdv_plan_path(rdv_plan), class: "fr-ml-2w"
4 changes: 4 additions & 0 deletions app/views/agents/rdv_plans/summary/_starts_at.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/ locals(rdv_plan:)
span.fr-icon-calendar-fill.fr-mr-1w[aria-hidden="true"]
= "#{l(rdv_plan.starts_at, format: :human)} (#{rdv_plan.duration_in_minutes} minutes)"
= link_to "modifier", edit_starts_at_agents_rdv_plan_path(rdv_plan), class: "fr-ml-2w"
1 change: 1 addition & 0 deletions config/anonymizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,4 @@ tables:
- starts_at
- created_at
- updated_at
- location_type
Loading

0 comments on commit b978726

Please sign in to comment.