diff --git a/app/javascript/components/rdv-plan-calendar.js b/app/javascript/components/rdv-plan-calendar.js new file mode 100644 index 0000000000..07586e809c --- /dev/null +++ b/app/javascript/components/rdv-plan-calendar.js @@ -0,0 +1,165 @@ +import { Calendar } from '@fullcalendar/core'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import frLocale from '@fullcalendar/core/locales/fr'; +import interactionPlugin from '@fullcalendar/interaction'; + +class RdvPlanCalendar { + + constructor() { + this.calendarEl = document.getElementById('rdvPlanCalendar'); + if (this.calendarEl == null || this.calendarEl.innerHTML !== "") + return + + this.data = this.calendarEl.dataset + this.fullCalendarInstance = this.initFullCalendar(this.calendarEl) + this.fullCalendarInstance.render(); + } + + initFullCalendar = () => { + var hiddenDays = [] + if (this.data.displaySaturdays !== "true") { + hiddenDays.push(6); + } + if (this.data.displaySundays !== "true") { + hiddenDays.push(0); + } + + const options = { + plugins: [timeGridPlugin, interactionPlugin], + locale: frLocale, + eventSources: JSON.parse(this.data.eventSourcesJson), + defaultDate: JSON.parse(this.data.defaultDateJson), + allDaySlot: false, + nowIndicator: true, + defaultView: this.data.singleDay === "true" ? 'timeGridDay' : 'timeGridWeek', + hiddenDays: hiddenDays, + height: "auto", + selectable: true, + select: this.selectEvent, + businessHours: { + // days of week. an array of zero-based day of week integers (0=Sunday) + daysOfWeek: [1, 2, 3, 4, 5, 6, 0], + startTime: '07:00', + endTime: '19:00', + }, + minTime: this.data.minTime || '07:00:00', + maxTime: this.data.maxTime || '20:00:00', + eventRender: this.eventRender, + eventMouseLeave: (info) => $(info.el).tooltip('hide'), // extra security + timeZone: "Europe/Paris" // This is a hack to make sure that the events will be shown at the proper time in the calendar. + // If this is removed, there is a bug that causes the events in the calendar to be show at the wrong + // time for agents that are not in the Paris timezone. + // The proper fix for this would be to make sure we store all rdvs with the right timezone, but that's a much bigger project. + // The timezone is forced to paris on the server side, so if we make sure that we also force it to the same timezone here, + // we always have a consistent result. + // We're always assuming that people are interested in their local time. + // + // There is one case for which this fix would fail: if the local time of the user and the agent is not the same (for example the agent is + // in the métropole and the user is at la réunion), they will not see the same time + // see the same time for the rdv. This seems unlikely for now. + } + + if (this.data.singleDay === "true") { + options.header = { left: '', center: '', right: '' }; + } else { + options.header = { left: '', center: '', right: 'prev,next' }; + } + + return new Calendar(this.calendarEl, options); + } + + selectEvent = (info) => { + const urlSearchParams = new URLSearchParams({ + user_id: this.data.userId, + starts_at: info.startStr, + }); + const field = document.getElementById('rdvPlanCalendarField') + field.value = info.startStr + const form = document.getElementById('rdvPlanCalendarForm') + form.submit() + } + + // TODO: extraire cette fonction pour la partager avec l'autre calendrier + eventRender = (info) => { + let $el = $(info.el); + let extendedProps = info.event.extendedProps; + + if (extendedProps.past == true) { + $el.addClass("fc-event-past"); + }; + if (extendedProps.duration <= 30) { + $el.addClass("fc-event-small"); + }; + if (extendedProps.unauthorizedRdvExplanation) { + $el.addClass("fc-unauthorized-rdv"); + }; + + if (this.data.selectedEventId && info.event.id == this.data.selectedEventId) + $el.addClass("selected"); + + $el.addClass("fc-event-" + extendedProps.status); + + if (extendedProps.userInWaitingRoom == true) { + $el.addClass("fc-event-waiting"); + } + + if (extendedProps.jour_feries == true) { + return + } + + let title = ``; + const start = Intl.DateTimeFormat("fr", { timeZone: 'UTC', hour: 'numeric', minute: 'numeric' }).format(info.event.start); + const end = Intl.DateTimeFormat("fr", { timeZone: 'UTC', hour: 'numeric', minute: 'numeric' }).format(info.event.end); + + if (info.isStart && info.isEnd) { + title += `${start} - ${end}`; + } else if (info.isStart) { + title += `À partir de ${start}`; + } else if (info.isEnd) { + title += `Jusqu'à ${end}`; + } else { + title += `Toute la journée`; + } + + if (info.event.rendering == 'background') { + $el.append("
" + info.event.title + "
"); + + if (extendedProps.organisationName) { + title += `
${extendedProps.organisationName}`; + } + title += `
${info.event.title}`; + if (extendedProps.lieu) { + title += `
Lieu : ${extendedProps.lieu}`; + } + } else { + if (extendedProps.duration) { + title += ` (${extendedProps.duration} min)`; + title += `
${extendedProps.motif}`; + } + + title += `
${info.event.title}`; + + if (extendedProps.organisationName) { + title += `
${extendedProps.organisationName}`; + } + if (extendedProps.lieu) { + title += `
Lieu: ${extendedProps.lieu}`; + } + if (extendedProps.readableStatus) { + title += `
Statut: ${extendedProps.readableStatus}`; + } + if (extendedProps.unauthorizedRdvExplanation) { + title += `
${extendedProps.unauthorizedRdvExplanation}`; + } + } + + $el.attr("title", title); + $el.attr("data-toggle", "tooltip"); + $el.attr("data-html", "true"); + $el.tooltip() + } +} + +document.addEventListener('turbolinks:load', function () { + new RdvPlanCalendar() +}); diff --git a/app/javascript/rdv_plan.js b/app/javascript/rdv_plan.js new file mode 100644 index 0000000000..994e469950 --- /dev/null +++ b/app/javascript/rdv_plan.js @@ -0,0 +1,3 @@ +import './stylesheets/rdv_plan'; + +import './components/rdv-plan-calendar'; diff --git a/app/javascript/stylesheets/rdv_plan.scss b/app/javascript/stylesheets/rdv_plan.scss new file mode 100644 index 0000000000..f54af5ff47 --- /dev/null +++ b/app/javascript/stylesheets/rdv_plan.scss @@ -0,0 +1,19 @@ +@import "@fullcalendar/core/main"; +@import "@fullcalendar/daygrid/main"; +@import "@fullcalendar/timegrid/main"; + +@import "~bootstrap/scss/functions"; +@import "~bootstrap/scss/variables"; +@import "~bootstrap/scss/mixins"; +@import "./variables"; +@import "./components/calendar"; + +.fc-widget-content { + // On suit l'exemple de Google Calendar : dans le calendrier principal même si on peut cliquer n'importe où dans le calendrier + // pour créer un evènement, on ne passe habituellement le cursor en point que sur les évènements, + // parce qu'on considère que par défaut on est en train de consulter le calendrier plutôt que de vouloir + // ajouter un évènement + // Cependant, dans le cadre du rdv_plan, on sait qu'on et en train de chercher à créer un nouvel évènement + // donc on explicite que le calendrier est cliquable avec un cursor pointer. + cursor: pointer; +} diff --git a/app/views/agents/rdv_plans/_header.html.slim b/app/views/agents/rdv_plans/_header.html.slim index a4f109ded1..b7ba705d2c 100644 --- a/app/views/agents/rdv_plans/_header.html.slim +++ b/app/views/agents/rdv_plans/_header.html.slim @@ -1,4 +1,6 @@ / locals(rdv_plan:, current_step:, step_title:, next_step_title:) +- content_for(:additional_scripts) { javascript_include_tag "rdv_plan", "data-turbolinks-track": "reload" } +- content_for(:additional_stylesheets) { stylesheet_link_tag "rdv_plan", media: "all", "data-turbolinks-track": "reload" } h1.fr-h2 | Nouveau rendez-vous avec #{rdv_plan.user.full_name} = render "common/stepper", step_count: 3, current_step:, step_title:, next_step_title: diff --git a/app/views/layouts/application_base.html.slim b/app/views/layouts/application_base.html.slim index 89e6916f7f..d0d798557a 100644 --- a/app/views/layouts/application_base.html.slim +++ b/app/views/layouts/application_base.html.slim @@ -5,6 +5,8 @@ html lang="fr" = stylesheet_link_tag "#{dsfr_path}/dsfr.min.css", "data-turbo-track": "reload" = stylesheet_link_tag "#{dsfr_path}/utility/icons/icons.min.css", "data-turbo-track": "reload" = stylesheet_link_tag "application", media: "all", "data-turbolinks-track": "reload" + = content_for(:additional_stylesheets) + = javascript_include_tag "application", "data-turbolinks-track": "reload" = javascript_include_tag "#{dsfr_path}/dsfr.module.min.js", "data-turbo-track": "reload", type: "module", defer: true = javascript_include_tag "#{dsfr_path}/dsfr.nomodule.min.js", "data-turbo-track": "reload", nomodule: true, defer: true diff --git a/webpack.config.js b/webpack.config.js index deb52a4791..e4af57fbde 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,6 +10,7 @@ module.exports = { application_agent: "./app/javascript/application_agent", application_agent_config: "./app/javascript/application_agent_config", charts: "./app/javascript/charts", + rdv_plan: "./app/javascript/rdv_plan", mail: "./app/javascript/mail", instance_name: "./app/javascript/instance_name", },