1import { Controller } from "@hotwired/stimulus"; 2import moment from 'moment-timezone'; 3import { Calendar, dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin, allLocales } from 'fullcalendar-adapter'; 4import { trans } from 'translator'; 5import Swal from 'sweetalert2'; 6 7export default class extends Controller { 8 static values = { 9 localeUser: String, 10 timezoneUser: String, 11 stationSlug: String, 12 } 13 static targets = ['calendar', "checkboxCategorie", "checkboxAll"]; 14 15 #isCreatingEvent = false; 16 17 connect() { 18 document.addEventListener('calendar:refresh', this.refreshCalendar.bind(this)); 19 20 this.calendar = new Calendar(this.calendarTarget, { 21 /*nextDayThreshold: "00:00:00",*/ 22 /*now: getNowWithOffset(offset),*/ 23 plugins: [dayGridPlugin, listPlugin, timeGridPlugin, interactionPlugin], 24 initialView: 'dayGridMonth', 25 locales: allLocales, 26 locale: this.localeUserValue, 27 timeZone: this.timezoneUserValue, 28 editable: true, 29 selectable: true, 30 lazyFetching: true, 31 eventSources: this.getSources(), 32 eventConstraint: this.getCalendarDateConstraints(), 33 selectConstraint: this.getCalendarDateConstraints(), 34 headerToolbar: { 35 left: 'prev,next today', 36 center: 'title', 37 right: 'dayGridMonth,timeGridWeek,timeGridDay,listAllYears' 38 }, 39 views: { 40 listAllYears: { 41 type: 'listYear', 42 duration: { years: 2 }, 43 buttonText: trans('agenda.index.liste_evenement_button'), 44 }, 45 timeGridWeek: { 46 displayEventEnd: true, 47 }, 48 }, 49 select: this.newEvenement.bind(this), 50 eventClick: this.handleEventClick.bind(this), 51 eventDrop: this.updateEvenement.bind(this), 52 eventResize: this.updateEvenement.bind(this), 53 eventContent: this.affichageMonth.bind(this), 54 55 eventDataTransform: (rawEventData) => { 56 // On travaille uniquement sur les événements allDay avec start/end définis 57 if (!rawEventData.allDay || !rawEventData.start || !rawEventData.end) { 58 return rawEventData; 59 } 60 61 const tz = this.timezoneUserValue; 62 const start = moment.tz(rawEventData.start, tz); 63 const end = moment.tz(rawEventData.end, tz); 64 65 // multi-jours si au moins 1 jour d'écart entre start et end (en jours calendaires) 66 const isMultiDay = end.clone().startOf('day').diff(start.clone().startOf('day'), 'days') >= 1; 67 const isEndOfDay = end.hour() === 23 && end.minute() === 59; 68 69 if (isMultiDay && isEndOfDay) { 70 // On décale la fin à minuit du jour suivant pour que FullCalendar 71 // couvre bien tous les jours concernés 72 const displayEnd = end.clone().add(1, 'minute'); 73 74 return { 75 ...rawEventData, 76 end: displayEnd.toISOString(), 77 }; 78 } 79 80 return rawEventData; 81 }, 82 }); 83 84 this.calendar.render(); 85 } 86 87 filterEvents(event) { 88 const categorieCode = event.target.value; 89 const isChecked = event.target.checked; 90 const events = this.calendar.getEvents(); 91 92 events.forEach((calendarEvent) => { 93 if (calendarEvent.extendedProps.categorie.code === categorieCode || categorieCode === 'all') { 94 calendarEvent.setProp('display', isChecked ? 'auto' : 'none'); 95 } 96 }); 97 } 98 99 filterAllEvents(event) { 100 const isChecked = event.target.checked; 101 102 this.checkboxCategorieTargets.forEach((checkbox) => { 103 checkbox.checked = isChecked; 104 }); 105 106 const events = this.calendar.getEvents();
107 events.forEach((calendarEvent) => { 108 calendarEvent.setProp('display', isChecked ? 'auto' : 'none'); 109 }); 110 } 111 112 handleEventClick(info) { 113 if (this.#isCreatingEvent) { 114 return; 115 } 116 this.#isCreatingEvent = true; 117 118 const event = info.event; 119 const url = Routing.generate('app_agenda_edit_flow', { 120 '_locale': this.localeUserValue, 121 slug: event.extendedProps.slug || event.slug, 122 station_slug: this.stationSlugValue 123 }); 124 window.location.href = url; 125 } 126 127 newEvenement(info = null) { 128 129 if (this.#isCreatingEvent) { 130 return; 131 } 132 133 this.#isCreatingEvent = true; 134 135 let params = { 136 '_locale': this.localeUserValue, 137 'station_slug': this.stationSlugValue 138 }; 139 140 if (info) { 141 const tz = this.timezoneUserValue; 142 if (info.view.type === 'dayGridMonth') { 143 const start = moment.tz(info.startStr, tz) 144 .set({ hour: moment().hour(), minute: moment().minute(), second: 0, millisecond: 0 }); 145 const end = start.clone().add(1, 'hour'); 146 params['start'] = start.format(); 147 params['end'] = end.format(); 148 } else { 149 // Pour les autres vues, on garde le comportement natif 150 params['start'] = info.startStr; 151 params['end'] = info.endStr; 152 } 153 params['timezone'] = tz; 154 155 } 156 157 const url = Routing.generate('app_agenda_create_flow', params); 158 window.location.href = url; 159 } 160 161 updateEvenement(info) { 162 const event = info.event; 163 const timezone = this.timezoneUserValue; 164 const start = moment.tz(event.start, timezone).format('YYYY-MM-DDTHH:mm:ss'); 165 const end = moment.tz(event.end, timezone).format('YYYY-MM-DDTHH:mm:ss'); 166 167 const url = Routing.generate('app_agenda_update', { 168 id: event.id, 169 station_slug: this.stationSlugValue 170 }); 171 172 fetch(url, { 173 method: 'POST', 174 body: JSON.stringify({ start, end, timezone }), 175 }) 176 .then(() => Swal.fire('Ãvénement mis à jour avec succès', '', 'success')) 177 .catch(err => Swal.fire('Erreur', 'Impossible de mettre à jour l\'événement', 'error')); 178 } 179 180 getSources() { 181 const evenement_url = Routing.generate('app_agenda_list', { station_slug: this.stationSlugValue }); 182 return [ 183 { 184 url: evenement_url, 185 color: '#a2c617', 186 textColor: 'white', 187 editable: true, 188 }, 189 ]; 190 } 191 192 affichageMonth(info) { 193 const evenement = info.event; 194 let spanAffichage = document.createElement('span'); 195 196 if (info.view.type === "dayGridMonth" && evenement.allDay) { 197 spanAffichage.innerHTML = evenement.title; 198 return { domNodes: [spanAffichage] }; 199 } 200 201 return true; 202 } 203 204 getCalendarDateConstraints() { 205 return { 206 start: moment().tz(this.timezoneUserValue).startOf('day').format('YYYY-MM-DDTHH:mm:ss'), // Début aujourd'hui à minuit 207 end: moment().tz(this.timezoneUserValue).add(100, 'years').startOf('day').format('YYYY-MM-DDTHH:mm:ss') // Fin dans 100 ans 208 }; 209 } 210 211 refreshCalendar() { 212 this.calendar.refetchEvents(); 213 } 214}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.