// js/calendar_admin.js document.addEventListener('DOMContentLoaded', function () { const calendarEl = getElementOrThrow('calendar'); const modal = getElementOrThrow('event_modal'); let selected_start_string = null; let event_selected = false; // pour event.remove() const calendar = new FullCalendar.Calendar(calendarEl, { editable: true, locale: 'fr', //timeZone: 'local', // à modifier pour être à l'heure d'un autre pays initialView: 'dayGridMonth', headerToolbar: { left: 'prev,next today', center: 'title', //right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' right: 'dayGridMonth,timeGridWeek,listWeek' }, slotMinWidth: 70, defaultAllDay: false, // numéros de semaine //weekNumbers: true, //weekText: 's', // vue mois contentHeight: 600, // après initialisation: calendar.setOption('contentHeight', 650); //aspectRatio: 1.5, // après initialisation: calendar.setOption('aspectRatio', 1.8); // pour recalculer la taille au redimensionnement du parent, exécuter: calendar.updateSize() stickyHeaderDates: true, // garder les en-tête de colonnes lors du scroll fixedWeekCount: false, // avec false, affiche 4, 5 ou 6 semaines selon le mois selectable: true, // sélection de jours en cliquant dessus longPressDelay: 300, // 1000ms par défaut navLinks: true, // numéros de jour et de semaines clicables // vue semaine slotEventOverlap: true, // superposition (limitée) de deux évènements simultanés allDayContent: 'Journée', // texte dans la case "toute la journée" nowIndicator: true, // barre rouge pour maintenant // params en plus: https://fullcalendar.io/docs/events-json-feed events: 'index.php?action=get_events', // fichier PHP qui retourne les événements select: function (info) { selected_start_string = info.startStr; // variable "globale" event_selected = false; let all_day = false; let input_type = 'datetime-local'; // input datetime-local = date + heure const start = new Date(info.startStr); const end = new Date(info.endStr); if (calendar.view.type == 'dayGridMonth') { end.setDate(end.getDate() - 1); // jour de fin modifié pour ne pas faire bizarre pour l'utilisateur end.setHours(start.getTime() === end.getTime() ? 11 : 10); start.setHours(10); } else if (calendar.view.type == 'timeGridWeek' && info.startStr.split("T").length === 1) { // clic sur la ligne "Journée" => évènement allDay all_day = true; input_type = 'date'; } //else if(calendar.view.type == 'timeGridDay'){} const modal_view = new CalendarModalView({ mode: 'new', all_day: all_day, input_type: input_type, start: start, end: end }); modal.innerHTML = modal_view.getView(); calendar.updateSize(); }, // sélection d'une date simple sur mobile, évite des problèmes de conflit avec eventClick dateClick: function (info) { if (window.matchMedia('(pointer: coarse)').matches) { const end = new Date(info.date); calendar.view.type === 'dayGridMonth' ? end.setDate(end.getDate() + 1) : end.setMinutes(end.getMinutes() + 30); // vue date: la fin est une date exclue // vue semaine: durée de 30min par défaut const local_end_date_array = end.toLocaleDateString().split('/'); let local_end_str = local_end_date_array[2] + '-' + local_end_date_array[1] + '-' + local_end_date_array[0]; if (calendar.view.type != 'dayGridMonth') { local_end_str += 'T' + end.toLocaleTimeString(); } // on a besoin de deux chaînes représentant l'heure locale pour obtenir le même objet "info" qu'avec l'évènement select // la vue "dayGridMonth" nécessite deux paramètres, par sécurité on en enverra toujours deux calendar.select(info.dateStr, local_end_str); } }, //unselect: function(event, view){}, eventClick: function (info) { event_selected = true; // variable "globale" if (!info.event.start || !info.event.end) { throw new Error("info.event.start ou info.event.end est null"); } const modal_view = new CalendarModalView({ mode: 'edit', input_type: info.event.allDay ? 'date' : 'datetime-local', id: info.event.id, title: info.event.title, description: info.event.extendedProps.description, color: info.event.backgroundColor, all_day: info.event.allDay, start: info.event.start, end: info.event.end }); modal.innerHTML = modal_view.getView(); calendar.updateSize(); }, viewDidMount: function (_info) { hideModal(); if (selected_start_string) { calendar.gotoDate(new Date(selected_start_string)); } }, //datesSet: function(info){}, // déclenché lorsque des dates affichées sont chargées (= comme viewDidMount + changement de date) }); function hideModal() { event_selected = false; modal.innerHTML = ''; calendar.updateSize(); } function submitEvent(new_event) { const event_title_input = getElementOrThrow('event_title'); const event_description_input = getElementOrThrow('event_description'); const event_all_day_input = getElementOrThrow('event_all_day'); const event_start_input = getElementOrThrow('event_start'); const event_end_input = getElementOrThrow('event_end'); const event_color_input = getElementOrThrow('event_color'); assertElementType(event_title_input, HTMLInputElement); assertElementType(event_description_input, HTMLTextAreaElement); assertElementType(event_all_day_input, HTMLInputElement); assertElementType(event_start_input, HTMLInputElement); assertElementType(event_end_input, HTMLInputElement); assertElementType(event_color_input, HTMLInputElement); const event_title = event_title_input.value; const event_description = event_description_input.value; const event_all_day = event_all_day_input.checked; let event_start = new Date(event_start_input.value); let event_end = new Date(event_end_input.value); const event_color = event_color_input.value; // #3788d8 par défaut let event_id = ''; if (!new_event) { //const event_id_input: HTMLElement|null = document.getElementById('event_id'); const event_id_input = getElementOrThrow('event_id'); assertElementType(event_id_input, HTMLInputElement); event_id = event_id_input.value; } // contrôle de saisie if (event_title.length !== 0 && event_start_input.value.length !== 0 && event_end_input.value.length !== 0 && event_color.length !== 0 && (new_event || event_id.length !== 0)) { if (event_all_day) { // on remet le jour de fin exclu event_end.setDate(event_end.getDate() + 1); } else { event_start = new Date(event_start); event_end = new Date(event_end); } // contrôle date/heure de fin après le début if (event_start > event_end || (!event_all_day && event_start == event_end)) { console.log("Erreur: la fin de l'évènement doit se situer après son début."); return; } // création if (new_event) { const event = { id: '', title: event_title, description: event_description, allDay: event_all_day, start: event_start, end: event_end, color: event_color }; fetch('index.php?action=new_event', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(event), }) .then(response => response.json()) .then(data => { if (data.success) { event.id = data.id; calendar.addEvent(event); hideModal(); } }) .catch((error) => { console.error('Error:', error); }); } // modification else { const event = calendar.getEventById(event_id); if (!event) { throw new Error("Événement non trouvé !"); } const event_copy = { id: parseInt(event.id), description: event_description, title: event_title, allDay: event_all_day, start: event_start, end: event_end, color: event_color }; fetch('index.php?action=update_event', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(event_copy), }) .then(response => response.json()) .then(data => { if (data.success) { event.setProp('title', event_title); event.setExtendedProp('description', event_description); event.setAllDay(event_all_day); event.setStart(event_start); event.setEnd(event_end); event.setProp('color', event_color); hideModal(); } }) .catch((error) => { console.error('Error:', error); }); } } else { // notif input vide console.log('erreur: input vide'); } } function checkAllDay() { const event_start_input = getElementOrThrow('event_start'); const event_end_input = getElementOrThrow('event_end'); const event_all_day = getElementOrThrow('event_all_day'); assertElementType(event_start_input, HTMLInputElement); assertElementType(event_end_input, HTMLInputElement); assertElementType(event_all_day, HTMLInputElement); const start = event_start_input.value; const end = event_end_input.value; if (event_all_day.checked) { event_start_input.type = 'date'; event_end_input.type = 'date'; event_start_input.value = start.split('T')[0]; event_end_input.value = end.split('T')[0]; } else { event_start_input.type = 'datetime-local'; event_end_input.type = 'datetime-local'; event_start_input.value = start + 'T10:00'; event_end_input.value = end + 'T11:00'; } } function removeEvent() { if (!confirm("Voulez-vous vraiment supprimer cet évènement du calendrier?")) { return; } const event_tag = getElementOrThrow('event_id'); // cible input hidden assertElementType(event_tag, HTMLInputElement); const event_id = event_tag.value; const event = calendar.getEventById(event_id); if (!event) { throw new Error("Événement non trouvé !"); } fetch('index.php?action=remove_event', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ 'id': event_id }), }) .then(response => response.json()) .then(data => { if (data.success) { event.remove(); hideModal(); } }) .catch((error) => { console.error('Error:', error); }); event_selected = false; } // touches de clavier document.addEventListener('keydown', function (event) { if (event.key === 'Escape') { hideModal(); } else if (event.key === 'Delete' && event_selected === true) { removeEvent(); } }); // boutons dans la "modale" // technique de la délégation d'événements pour utiliser un bouton ajouté dynamiquement document.addEventListener('click', function (event) { if (!event.target) { throw new Error('évènement click non conforme'); } assertElementType(event.target, HTMLElement); if (event.target.classList.contains('event_close_button')) { hideModal(); } else if (event.target.classList.contains('event_all_day')) { checkAllDay(); } else if (event.target.classList.contains('submit_new_event')) { submitEvent(true); } else if (event.target.classList.contains('submit_update_event')) { submitEvent(false); } else if (event.target.classList.contains('delete_event')) { removeEvent(); } }); calendar.render(); });