// Module ES6 pour l'initialisation et la gestion de FullCalendar import { openModal } from './agenda-modal.js'; import { getEvents, updateEvent, getEvent } from './agenda-api.js'; import { notifyError } from './agenda-notifications.js'; import { initializeFilters } from './agenda-filters.js'; import { mapEventToFullCalendar } from './agenda-event-mapper.js'; import toastr from 'toastr'; import { Calendar } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; import listPlugin from '@fullcalendar/list'; import interactionPlugin from '@fullcalendar/interaction'; import { apiFetch } from './agenda-api.js'; // Ajoutez d'autres plugins si besoin export function initializeCalendar() { console.log('🚀 Initialisation de FullCalendar...'); const calendarEl = document.getElementById('agenda-calendar'); if (!calendarEl) { console.error('❌ ÉlĂ©ment agenda-calendar non trouvĂ©'); return null; } console.log('✅ ÉlĂ©ment agenda-calendar trouvĂ©, crĂ©ation du calendrier...'); console.log('🔧 Configuration FullCalendar - Options d\'Ă©dition:'); console.log(' - editable: true'); console.log(' - eventStartEditable: true'); console.log(' - eventResizableFromStart: true'); console.log(' - eventDurationEditable: true'); console.log(' - selectable: true'); console.log(' - dateClick: implĂ©mentĂ©'); const calendar = new Calendar(calendarEl, { plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin], // Ajoutez ici les autres plugins si besoin initialView: 'timeGridWeek', eventDisplay: 'block', // Afficher les Ă©vĂ©nements comme des blocs pleins slotEventOverlap: false, // EmpĂȘche le chevauchement visuel des Ă©vĂ©nements eventMaxStack: 1, // Limite l'empilement des Ă©vĂ©nements Ă  1 eventMinHeight: 20, // Hauteur minimale des Ă©vĂ©nements en pixels eventShortHeight: 20, // Hauteur des Ă©vĂ©nements courts en pixels expandRows: true, // Permet d'Ă©tendre les lignes pour afficher tous les Ă©vĂ©nements locale: 'fr', firstDay: 1, // La semaine commence le lundi (0 = dimanche, 1 = lundi) headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' }, buttonText: { today: 'Aujourd\'hui', month: 'Mois', week: 'Semaine', day: 'Jour', list: 'Liste' }, height: 'auto', editable: true, eventStartEditable: true, // Permet le drag & drop des Ă©vĂ©nements eventDurationEditable: true, // Permet le redimensionnement des Ă©vĂ©nements eventResizableFromStart: true, // Permet le redimensionnement depuis le dĂ©but selectable: true, selectMirror: true, dayMaxEventRows: true, // Active la limite de lignes d'Ă©vĂ©nements par jour dayMaxEvents: 3, // Afficher maximum 3 Ă©vĂ©nements par jour, puis "+x plus" slotDuration: '00:30:00', // DurĂ©e de chaque crĂ©neau horaire : 30 minutes slotLabelInterval: '01:00', // Afficher les Ă©tiquettes d'heures toutes les heures weekends: true, moreLinkClick: 'popover', // Afficher les Ă©vĂ©nements cachĂ©s dans un popover moreLinkContent: function(arg) { // Personnaliser le texte du lien "+x plus" const count = arg.num; return `+${count} plus`; }, events: async function(fetchInfo, successCallback, failureCallback) { try { const params = { start: fetchInfo.startStr.split('T')[0], // Date de dĂ©but du mois end: fetchInfo.endStr.split('T')[0] // Date de fin du mois }; const apiEvents = await getEvents(params); // Mapping des objets API vers le format FullCalendar const events = apiEvents.map(ev => mapEventToFullCalendar(ev)); successCallback(events); } catch (e) { console.error('Erreur lors du chargement des Ă©vĂ©nements:', e); notifyError('Erreur lors du chargement des Ă©vĂ©nements'); failureCallback(e); } }, select: function(arg) { // VĂ©rifier que la date de dĂ©but n'est pas dans le passĂ© if (arg.startStr) { const startDateStr = arg.startStr.split('T')[0]; const today = new Date(); today.setHours(0, 0, 0, 0); const selectedDate = new Date(startDateStr + 'T00:00:00'); selectedDate.setHours(0, 0, 0, 0); if (selectedDate < today) { notifyError('Impossible de crĂ©er un Ă©vĂ©nement Ă  une date passĂ©e'); console.warn('⚠ Tentative de crĂ©ation d\'Ă©vĂ©nement Ă  une date passĂ©e:', startDateStr); return; // EmpĂȘcher l'ouverture du modal } } // Corriger les donnĂ©es pour que la date de fin soit le mĂȘme jour que le dĂ©but // (FullCalendar ajoute +1 jour pour les sĂ©lections allDay) const dateDebut = arg.startStr?.split('T')[0]; const heureDebut = arg.startStr?.split('T')[1] || '09:00:00'; const heureFin = arg.endStr?.split('T')[1] || '09:15:00'; // CrĂ©er de nouveaux objets Date avec les bonnes heures en timezone local const startDate = new Date(`${dateDebut}T${heureDebut}`); const endDate = new Date(`${dateDebut}T${heureFin}`); const correctedArg = { ...arg, start: startDate, end: endDate, startStr: `${dateDebut}T${heureDebut}`, endStr: `${dateDebut}T${heureFin}`, allDay: false }; openModal('create', correctedArg); }, // dateClick dĂ©sactivĂ© car select gĂšre dĂ©jĂ  la crĂ©ation d'Ă©vĂ©nements // et Ă©vite les doubles appels /*dateClick: function(info) { // VĂ©rifier les permissions de crĂ©ation const userCanCreate = window.crviPermissions && window.crviPermissions.can_create; if (!userCanCreate) { console.warn('⚠ Utilisateur non autorisĂ© Ă  crĂ©er des Ă©vĂ©nements'); return; } // CrĂ©er un objet avec les informations de la date cliquĂ©e const clickedDate = info.date; // CORRECTION: Utiliser dateStr si disponible, sinon extraire correctement la date locale // Le problĂšme de dĂ©calage vient du fait que info.date peut ĂȘtre en UTC // et que getDate()/getMonth() utilisent le fuseau horaire local, causant un dĂ©calage let dateStr; if (info.dateStr) { // FullCalendar fournit dateStr directement (format YYYY-MM-DD) - c'est la mĂ©thode la plus fiable dateStr = info.dateStr; } else { // Fallback: utiliser toLocaleDateString avec des options pour obtenir YYYY-MM-DD // Cela garantit qu'on obtient la date locale rĂ©elle, pas une date UTC convertie const dateParts = clickedDate.toLocaleDateString('fr-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }); // toLocaleDateString avec 'fr-CA' retourne YYYY-MM-DD directement dateStr = dateParts; } // VĂ©rifier que la date n'est pas dans le passĂ© const today = new Date(); today.setHours(0, 0, 0, 0); const clickedDateOnly = new Date(dateStr + 'T00:00:00'); clickedDateOnly.setHours(0, 0, 0, 0); if (clickedDateOnly < today) { notifyError('Impossible de crĂ©er un Ă©vĂ©nement Ă  une date passĂ©e'); return; } const eventData = { startStr: dateStr + 'T09:00:00', endStr: dateStr + 'T10:00:00', start: clickedDate, end: new Date(clickedDate.getTime() + 60 * 60 * 1000), // +1 heure allDay: false }; openModal('create', eventData); },*/ eventDrop: async function(info) { console.log('🔄 EVENT DROP DÉTECTÉ'); console.log('📋 Informations de l\'Ă©vĂ©nement:', { id: info.event.id, title: info.event.title, start: info.event.start, end: info.event.end, oldStart: info.oldEvent.start, oldEnd: info.oldEvent.end }); // VĂ©rifier les permissions const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit; if (!userCanEdit) { console.warn('❌ Utilisateur non autorisĂ© Ă  dĂ©placer des Ă©vĂ©nements'); info.revert(); // Annuler le dĂ©placement notifyError('Vous n\'ĂȘtes pas autorisĂ© Ă  dĂ©placer des Ă©vĂ©nements'); return; } // VĂ©rifier si l'ancien Ă©vĂ©nement Ă©tait passĂ© (on ne peut pas dĂ©placer un Ă©vĂ©nement passĂ©) const oldStartDate = new Date(info.oldEvent.start); const now = new Date(); if (oldStartDate < now) { console.warn('❌ Impossible de dĂ©placer un Ă©vĂ©nement passĂ©'); info.revert(); // Annuler le dĂ©placement notifyError('Impossible de dĂ©placer un Ă©vĂ©nement passĂ©'); return; } // VĂ©rifier les disponibilitĂ©s avant de permettre le drop const isAvailable = await checkEventAvailability(info.event); if (!isAvailable.available) { console.warn('❌ ÉvĂ©nement non dĂ©plaçable - Conflit de disponibilitĂ©:', isAvailable.reason); info.revert(); // Annuler le dĂ©placement notifyError(`Impossible de dĂ©placer l'Ă©vĂ©nement : ${isAvailable.reason}`); return; } try { console.log('✅ DisponibilitĂ© vĂ©rifiĂ©e - DĂ©placement autorisĂ©'); // PrĂ©parer les donnĂ©es de mise Ă  jour const newStart = info.event.start; const newEnd = info.event.end || new Date(newStart.getTime() + 60 * 60 * 1000); // +1h par dĂ©faut const updateData = { date_rdv: newStart.toISOString().split('T')[0], heure_rdv: newStart.toTimeString().substring(0, 5), date_fin: newEnd.toISOString().split('T')[0], heure_fin: newEnd.toTimeString().substring(0, 5) }; // Appeler l'API pour mettre Ă  jour l'Ă©vĂ©nement await updateEvent(info.event.id, updateData); // SuccĂšs toastr.success('ÉvĂ©nement dĂ©placĂ© avec succĂšs'); } catch (error) { console.error('Erreur lors du dĂ©placement de l\'Ă©vĂ©nement:', error); info.revert(); // Annuler le dĂ©placement en cas d'erreur notifyError('Erreur lors du dĂ©placement de l\'Ă©vĂ©nement'); } }, eventResize: async function(info) { // console.log('📏 EVENT RESIZE DÉTECTÉ'); // console.log('📋 Informations de l\'Ă©vĂ©nement:', { // id: info.event.id, // title: info.event.title, // start: info.event.start, // end: info.event.end, // oldStart: info.oldEvent.start, // oldEnd: info.oldEvent.end // }); // VĂ©rifier les permissions // console.log('🔐 VĂ©rification des permissions...'); // console.log('window.crviPermissions:', window.crviPermissions); // console.log('window.crviPermissions.can_edit:', window.crviPermissions?.can_edit); const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit; if (!userCanEdit) { console.warn('❌ Utilisateur non autorisĂ© Ă  redimensionner des Ă©vĂ©nements'); // console.log('🔒 Permissions insuffisantes - Annulation du redimensionnement'); info.revert(); // Annuler le redimensionnement notifyError('Vous n\'ĂȘtes pas autorisĂ© Ă  redimensionner des Ă©vĂ©nements'); return; } // console.log('✅ Permissions OK - Redimensionnement autorisĂ©'); try { // console.log('Redimensionnement d\'Ă©vĂ©nement:', info.event.id, 'nouvelle fin:', info.event.end); // PrĂ©parer les donnĂ©es de mise Ă  jour const newEnd = info.event.end; const updateData = { date_fin: newEnd.toISOString().split('T')[0], heure_fin: newEnd.toTimeString().substring(0, 5) }; // Appeler l'API pour mettre Ă  jour l'Ă©vĂ©nement await updateEvent(info.event.id, updateData); // SuccĂšs toastr.success('ÉvĂ©nement redimensionnĂ© avec succĂšs'); // console.log('ÉvĂ©nement redimensionnĂ© avec succĂšs'); } catch (error) { console.error('Erreur lors du redimensionnement de l\'Ă©vĂ©nement:', error); info.revert(); // Annuler le redimensionnement en cas d'erreur notifyError('Erreur lors du redimensionnement de l\'Ă©vĂ©nement'); } }, eventClick: async function(arg) { // VĂ©rifier les permissions avant d'ouvrir le modal const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit; const userCanView = window.crviPermissions && window.crviPermissions.can_view; if (!userCanView) { console.warn('Utilisateur non autorisĂ© Ă  voir les Ă©vĂ©nements'); return; } try { // Overlay ciblĂ© sur le calendrier pendant le chargement des dĂ©tails const calendarEl = document.getElementById('agenda-calendar'); if (window.CRVI_OVERLAY && calendarEl) { window.CRVI_OVERLAY.show(calendarEl); } // Charger les dĂ©tails complets de l'Ă©vĂ©nement depuis l'API const eventId = arg.event.id; // console.log('Chargement des dĂ©tails de l\'Ă©vĂ©nement:', eventId); const eventDetails = await getEvent(eventId); // console.log('eventClick - DĂ©tails de l\'Ă©vĂ©nement reçus:', eventDetails); // console.log('eventClick - date_rdv:', eventDetails.date); // console.log('eventClick - heure_rdv:', eventDetails.heure); // console.log('eventClick - date_fin:', eventDetails.date_fin); // console.log('eventClick - heure_fin:', eventDetails.heure_fin); // VĂ©rifier et crĂ©er les dates de dĂ©but et fin let startDate, endDate; try { // CrĂ©er la date de dĂ©but if (eventDetails.date_rdv && eventDetails.heure_rdv) { startDate = new Date(eventDetails.date_rdv + 'T' + eventDetails.heure_rdv); if (isNaN(startDate.getTime())) { // console.warn('Date de dĂ©but invalide:', eventDetails.date_rdv, eventDetails.heure_rdv); startDate = new Date(); } } else { // console.warn('DonnĂ©es de date/heure de dĂ©but manquantes'); startDate = new Date(); } // CrĂ©er la date de fin if (eventDetails.date_fin && eventDetails.heure_fin) { endDate = new Date(eventDetails.date_fin + 'T' + eventDetails.heure_fin); if (isNaN(endDate.getTime())) { // console.warn('Date de fin invalide:', eventDetails.date_fin, eventDetails.heure_fin); endDate = startDate; } } else { // console.warn('DonnĂ©es de date/heure de fin manquantes, utilisation de la date de dĂ©but'); endDate = startDate; } } catch (error) { console.error('Erreur lors de la crĂ©ation des dates:', error); startDate = new Date(); endDate = new Date(); } // CrĂ©er un objet Ă©vĂ©nement avec les dĂ©tails complets const fullEvent = { id: eventDetails.id, start: startDate, end: endDate, extendedProps: { // DonnĂ©es de base type: eventDetails.type, commentaire: eventDetails.commentaire, date: eventDetails.date, heure: eventDetails.heure, date_fin: eventDetails.date_fin, heure_fin: eventDetails.heure_fin, date_rdv: eventDetails.date_rdv, heure_rdv: eventDetails.heure_rdv, // Relations avec les entitĂ©s id_beneficiaire: eventDetails.id_beneficiaire, id_intervenant: eventDetails.id_intervenant, id_traducteur: eventDetails.id_traducteur, id_local: eventDetails.id_local, id_departement: eventDetails.id_departement, id_type_intervention: eventDetails.id_type_intervention || (eventDetails.type_intervention && eventDetails.type_intervention.id ? eventDetails.type_intervention.id : null), langue: eventDetails.langue, langues_disponibles: eventDetails.langues_disponibles || null, nom_traducteur: eventDetails.nom_traducteur, // DonnĂ©es des entitĂ©s beneficiaire: eventDetails.beneficiaire, intervenant: eventDetails.intervenant, traducteur: eventDetails.traducteur, local: eventDetails.local, departement: eventDetails.departement, type_intervention: eventDetails.type_intervention, // DonnĂ©es spĂ©cifiques aux groupes nb_participants: eventDetails.nb_participants, nb_hommes: eventDetails.nb_hommes, nb_femmes: eventDetails.nb_femmes, // Autres donnĂ©es statut: eventDetails.statut, assign: eventDetails.assign || 0, created_at: eventDetails.created_at, updated_at: eventDetails.updated_at } }; // DĂ©terminer le mode selon les permissions const mode = 'view'; // Toujours ouvrir en mode view par dĂ©faut openModal(mode, fullEvent); } catch (error) { console.error('Erreur lors du chargement des dĂ©tails de l\'Ă©vĂ©nement:', error); notifyError('Erreur lors du chargement des dĂ©tails de l\'Ă©vĂ©nement'); } finally { const calendarEl = document.getElementById('agenda-calendar'); if (window.CRVI_OVERLAY && calendarEl) { window.CRVI_OVERLAY.hide(calendarEl); } } }, eventDidMount: function(arg) { // Debug: Afficher les donnĂ©es de l'Ă©vĂ©nement const event = arg.event; const eventProps = event.extendedProps; // Appliquer l'attribut data-disabled si la permanence est dĂ©sactivĂ©e if (eventProps && eventProps.isDisabled) { arg.el.setAttribute('data-disabled', 'true'); arg.el.style.opacity = '0.6'; arg.el.style.cursor = 'not-allowed'; } // console.log('=== DEBUG ÉVÉNEMENT ==='); // console.log('ÉvĂ©nement:', event.title); // console.log('Intervenant:', eventProps.intervenant); // console.log('Statut:', eventProps.statut); // console.log('Background dĂ©fini:', event.backgroundColor); // console.log('Text color dĂ©fini:', event.textColor); // console.log('========================'); // Fonction utilitaire pour trouver la configuration d'un intervenant function findIntervenantConfig(intervenant) { if (!intervenant || !crviACFData || !crviACFData.intervenants) { // console.log('❌ DonnĂ©es manquantes dans eventDidMount - intervenant:', !!intervenant, 'crviACFData:', !!crviACFData, 'intervenants:', !!crviACFData?.intervenants); return null; } // VĂ©rifier que l'intervenant a un ID if (!intervenant.id) { // console.log('❌ ID d\'intervenant manquant dans eventDidMount:', intervenant); return null; } const intervenantId = parseInt(intervenant.id); if (isNaN(intervenantId)) { // console.log('❌ ID d\'intervenant invalide dans eventDidMount:', intervenant.id); return null; } // Chercher directement par ID let intervenantConfig = null; for (const key in crviACFData.intervenants) { const config = crviACFData.intervenants[key]; // VĂ©rifier que la config a un ID if (!config || !config.id) { // console.log('⚠ Configuration invalide dans eventDidMount pour la clĂ©:', key, 'config:', config); continue; } if (config.id == intervenantId) { intervenantConfig = config; // console.log('✅ Configuration trouvĂ©e dans eventDidMount pour ID:', intervenantId); break; } } if (!intervenantConfig) { // console.log('❌ Aucune configuration trouvĂ©e dans eventDidMount pour l\'ID:', intervenantId); // console.log('IDs disponibles:', Object.values(crviACFData.intervenants).map(c => c.id)); } return intervenantConfig; } // Appliquer les couleurs selon la logique simplifiĂ©e const eventEl = arg.el; const intervenantConfig = findIntervenantConfig(eventProps.intervenant); // Fonction pour dĂ©terminer la couleur selon la logique simplifiĂ©e function determineEventColor() { let bgColor = null; let txtColor = null; // Pour les permanences : logique simplifiĂ©e if (eventProps.type === 'permanence') { const isPermanenceAssigned = eventProps.assign === 1 || (eventProps.id_beneficiaire && eventProps.id_local); const isPermanenceNonAttribuee = eventProps.assign === 0 && !eventProps.id_beneficiaire && !eventProps.id_local; // VĂ©rifier si indisponible (via isDisabled ou statut) let isPermanenceNonDisponible = eventProps.isDisabled || (eventProps.statut && ['annule', 'non_tenu', 'absence'].includes(eventProps.statut)); // VĂ©rifier aussi si l'intervenant est indisponible if (!isPermanenceNonDisponible && eventProps.id_intervenant && window.crviACFData && window.crviACFData.indisponibilites_intervenants) { const intervenantId = parseInt(eventProps.id_intervenant); const intervenantDispo = window.crviACFData.indisponibilites_intervenants[intervenantId]; if (intervenantDispo && eventProps.date_rdv) { const eventDateObj = new Date(eventProps.date_rdv + 'T00:00:00'); const dayOfWeek = eventDateObj.getDay(); const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; const dayName = dayNames[dayOfWeek]; // VĂ©rifier les jours de disponibilitĂ© if (intervenantDispo.jours_dispo && Array.isArray(intervenantDispo.jours_dispo) && intervenantDispo.jours_dispo.length > 0 && !intervenantDispo.jours_dispo.includes(dayName)) { isPermanenceNonDisponible = true; } // VĂ©rifier les congĂ©s if (!isPermanenceNonDisponible && intervenantDispo.conges && Array.isArray(intervenantDispo.conges)) { for (const conge of intervenantDispo.conges) { if (conge.debut && conge.fin) { let debutDate, finDate; if (typeof conge.debut === 'string' && conge.debut.includes('/')) { const debutParts = conge.debut.split('/'); const finParts = conge.fin.split('/'); if (debutParts.length === 3 && finParts.length === 3) { debutDate = new Date(parseInt(debutParts[2]), parseInt(debutParts[1]) - 1, parseInt(debutParts[0])); finDate = new Date(parseInt(finParts[2]), parseInt(finParts[1]) - 1, parseInt(finParts[0])); } } else { debutDate = new Date(conge.debut); finDate = new Date(conge.fin); } if (debutDate && finDate && !isNaN(debutDate.getTime()) && !isNaN(finDate.getTime())) { debutDate.setHours(0, 0, 0, 0); finDate.setHours(23, 59, 59, 999); if (eventDateObj >= debutDate && eventDateObj <= finDate) { isPermanenceNonDisponible = true; break; } } } } } } } if (isPermanenceNonAttribuee) { // 1) Non attribuĂ©e : dispo ? couleur dispo : couleur indispo if (isPermanenceNonDisponible && window.crviAjax && window.crviAjax.couleur_permanence_non_disponible) { bgColor = window.crviAjax.couleur_permanence_non_disponible; } else if (window.crviAjax && window.crviAjax.couleur_permanence_non_attribuee) { bgColor = window.crviAjax.couleur_permanence_non_attribuee; } } else if (isPermanenceAssigned) { // 2) AttribuĂ©e : a type intervention ? couleur type : couleur dĂ©faut const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null; if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) { const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId]; if (couleurType) { bgColor = couleurType; } } if (!bgColor) { bgColor = (crviACFData && crviACFData.couleurs_permanence && crviACFData.couleurs_permanence.permanence) ? crviACFData.couleurs_permanence.permanence : '#9e9e9e'; } } } else { // Pour les RDV : type d'intervention ou orange par dĂ©faut const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null; if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) { const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId]; if (couleurType) { bgColor = couleurType; } } if (!bgColor) { bgColor = '#ff9800'; // Orange par dĂ©faut } } // S'assurer qu'une couleur est toujours dĂ©finie if (!bgColor) { bgColor = '#6c757d'; // Gris par dĂ©faut } txtColor = bgColor ? getTextColor(bgColor) : '#000000'; return { bgColor, txtColor }; } // DĂ©terminer la couleur let { bgColor, txtColor } = determineEventColor(); // VĂ©rifier que les couleurs sont valides et utiliser des valeurs par dĂ©faut si nĂ©cessaire if (!bgColor || !txtColor) { console.warn('⚠ [eventDidMount] Couleurs invalides dĂ©tectĂ©es:', { bgColor, txtColor, eventId: event.id }); bgColor = bgColor || '#6c757d'; txtColor = txtColor || '#000000'; } console.log('🎹 [eventDidMount] Couleur dĂ©terminĂ©e:', { eventId: event.id, eventTitle: event.title, type: eventProps.type, backgroundColor: bgColor, textColor: txtColor }); // Fonction pour appliquer les styles function applyEventStyles() { try { if (bgColor) { eventEl.style.setProperty('background-color', bgColor, 'important'); eventEl.style.setProperty('border-color', bgColor, 'important'); } if (txtColor) { eventEl.style.setProperty('color', txtColor, 'important'); // Appliquer aussi sur les Ă©lĂ©ments de texte const textElements = eventEl.querySelectorAll('.fc-event-title, .fc-event-time, .fc-event-main, .fc-event-main-frame'); textElements.forEach(el => { el.style.setProperty('color', txtColor, 'important'); }); } } catch (error) { console.error('Erreur lors de l\'application des styles:', error); } } // Appliquer les styles immĂ©diatement applyEventStyles(); // Puis avec requestAnimationFrame requestAnimationFrame(applyEventStyles); // Et avec un dĂ©lai pour s'assurer que c'est fait setTimeout(applyEventStyles, 100); // Et encore une fois aprĂšs un dĂ©lai plus long setTimeout(() => { applyEventStyles(); // VĂ©rifier le style final aprĂšs application const computedStyle = window.getComputedStyle(eventEl); console.log('🎹 [COULEUR FINALE APPLIQUÉE] Style calculĂ© sur l\'Ă©lĂ©ment:', { eventId: event.id, backgroundColor: computedStyle.backgroundColor, backgroundColorAttendu: bgColor, color: computedStyle.color }); }, 500); // Utiliser MutationObserver pour dĂ©tecter quand FullCalendar rĂ©applique ses styles const observer = new MutationObserver((mutations) => { let shouldReapply = false; mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'style') { shouldReapply = true; } }); if (shouldReapply) { console.log('🔄 [COULEUR] FullCalendar a modifiĂ© les styles, rĂ©application...'); applyEventStyles(); } }); observer.observe(eventEl, { attributes: true, attributeFilter: ['style'] }); // Ajouter l'icĂŽne du dĂ©partement si disponible - TRAITÉ PLUS TARD // Code temporairement dĂ©sactivĂ© pour ne garder que les couleurs de catĂ©gorie // Ajouter un popover Bootstrap pour afficher les dĂ©tails au survol // DĂ©terminer la couleur pour le popover (utiliser la mĂȘme logique que pour l'Ă©vĂ©nement) const popoverColor = bgColor || '#6c757d'; const popoverTextColor = txtColor || '#ffffff'; console.log('🎹 [POPOVER] Couleurs dĂ©terminĂ©es:', { eventId: event.id, popoverColor: popoverColor, popoverTextColor: popoverTextColor }); // Fonction utilitaire pour formater une date au format français function formatDateToFrench(dateString) { if (!dateString) return ''; try { const date = new Date(dateString); if (isNaN(date.getTime())) { // Si ce n'est pas une date valide, essayer le format YYYY-MM-DD const parts = dateString.split('-'); if (parts.length === 3) { return `${parts[2]}/${parts[1]}/${parts[0]}`; } return dateString; } return date.toLocaleDateString('fr-FR'); } catch (error) { console.error('Erreur lors du formatage de la date:', error); return dateString; } } // Fonction utilitaire pour rĂ©cupĂ©rer la couleur du statut depuis les donnĂ©es ACF function getStatutColor(statut) { if (!statut || !crviACFData || !crviACFData.statuts) { return '#6c757d'; // Couleur par dĂ©faut } const statutConfig = crviACFData.statuts[statut]; return statutConfig ? statutConfig.couleur : '#6c757d'; } // Fonction utilitaire pour extraire le nom complet d'une entitĂ© function getEntityDisplayName(entity, entityType = '') { if (!entity) return ''; // console.log('getEntityDisplayName - entity:', entity); // Si c'est dĂ©jĂ  une chaĂźne, la retourner if (typeof entity === 'string') return entity; // Si c'est un objet avec une propriĂ©tĂ© nom if (entity && typeof entity === 'object') { // Pour les bĂ©nĂ©ficiaires et intervenants, combiner prĂ©nom et nom if (entityType === 'beneficiaire' || entityType === 'intervenant') { const prenom = entity.prenom || ''; const nom = entity.nom || ''; return `${prenom} ${nom}`.trim() || entity.nom || entity.display_name || ''; } // Pour les locaux, utiliser le nom if (entityType === 'local') { return entity.nom || entity.display_name || ''; } // Fallback gĂ©nĂ©rique return entity.nom || entity.display_name || entity.name || ''; } return ''; } console.log('eventProps:', eventProps); // CrĂ©er le contenu du popover avec titre colorĂ© const popoverContent = `
${event.title}
Date: ${formatDateToFrench(eventProps.date_rdv) || ''}
Heure: ${eventProps.heure_rdv || ''}
Type: ${eventProps.type || ''}
Langue: ${eventProps.langue || ''}
${eventProps.beneficiaire ? `
Bénéficiaire: ${getEntityDisplayName(eventProps.beneficiaire, 'beneficiaire')}
` : ''} ${eventProps.intervenant ? `
Intervenant: ${getEntityDisplayName(eventProps.intervenant, 'intervenant')}
` : ''} ${eventProps.local ? `
Local: ${getEntityDisplayName(eventProps.local, 'local')}
` : ''} ${eventProps.commentaire ? `
Commentaire: ${eventProps.commentaire}
` : ''}
Statut: ${eventProps.statut}
Cliquez pour plus de détails
`; // Initialiser le popover Bootstrap avec titre colorĂ© if (window.bootstrap && window.bootstrap.Popover) { const popover = new bootstrap.Popover(eventEl, { title: 'DĂ©tails de l\'Ă©vĂ©nement', content: popoverContent, html: true, trigger: 'hover', placement: 'top', container: 'body', template: '' }); // Appliquer les couleurs au titre du popover aprĂšs crĂ©ation eventEl.addEventListener('shown.bs.popover', function() { const popoverElement = document.querySelector('.popover'); if (popoverElement) { const headerElement = popoverElement.querySelector('.popover-header'); if (headerElement) { headerElement.style.backgroundColor = popoverColor; headerElement.style.color = popoverTextColor; headerElement.style.borderBottom = `1px solid ${popoverColor}`; console.log('🎹 [POPOVER] Styles appliquĂ©s au header:', { backgroundColor: popoverColor, color: popoverTextColor }); } // Appliquer aussi les styles au body du popover si nĂ©cessaire const bodyElement = popoverElement.querySelector('.popover-body'); if (bodyElement && txtColor) { bodyElement.style.color = txtColor; } } }); // Écouter aussi l'Ă©vĂ©nement insertNode pour s'assurer que le popover est stylĂ© mĂȘme s'il est créé dynamiquement const popoverObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1 && node.classList && node.classList.contains('popover')) { const headerElement = node.querySelector('.popover-header'); if (headerElement) { headerElement.style.backgroundColor = popoverColor; headerElement.style.color = popoverTextColor; headerElement.style.borderBottom = `1px solid ${popoverColor}`; console.log('🎹 [POPOVER] Styles appliquĂ©s via observer:', { backgroundColor: popoverColor, color: popoverTextColor }); } } }); }); }); // Observer le body pour dĂ©tecter l'ajout du popover popoverObserver.observe(document.body, { childList: true, subtree: true }); } } }); calendar.render(); console.log('✅ Calendrier FullCalendar initialisĂ© avec succĂšs'); console.log('🎯 Gestionnaires d\'Ă©vĂ©nements configurĂ©s:'); console.log(' - eventClick: ✅'); console.log(' - eventDrop: ✅'); console.log(' - eventResize: ✅'); console.log(' - dateClick: ✅'); console.log(' - select: ✅'); // Stocker l'instance globalement pour pouvoir la rafraĂźchir window.currentCalendar = calendar; // Masquer l'indicateur de chargement const loadingIndicator = document.getElementById('loading-indicator'); if (loadingIndicator) { loadingIndicator.style.display = 'none'; } // Initialiser le bouton "Ajouter un RDV" initializeAddEventButton(calendar); // Initialiser les filtres dynamiques initializeFilters(calendar); // VĂ©rifier les permissions console.log('🔐 VĂ©rification des permissions:'); console.log(' - window.crviPermissions:', window.crviPermissions); console.log(' - can_create:', window.crviPermissions?.can_create); console.log(' - can_edit:', window.crviPermissions?.can_edit); console.log(' - can_view:', window.crviPermissions?.can_view); return calendar; } /** * Initialise le bouton "Ajouter un RDV" * @param {FullCalendar.Calendar} calendar - Instance du calendrier */ function initializeAddEventButton(calendar) { const addEventBtn = document.getElementById('addEventBtn'); if (!addEventBtn) { // console.warn('Bouton "Ajouter un RDV" non trouvĂ©'); return; } // VĂ©rifier les permissions de crĂ©ation const userCanCreate = window.crviPermissions && window.crviPermissions.can_create; if (!userCanCreate) { // Masquer le bouton si l'utilisateur ne peut pas crĂ©er addEventBtn.style.display = 'none'; // console.log('Bouton "Ajouter un RDV" masquĂ© - permissions insuffisantes'); return; } addEventBtn.addEventListener('click', function() { // CrĂ©er un objet de sĂ©lection avec la date d'aujourd'hui const today = new Date(); const todayStr = today.toISOString().split('T')[0]; // CrĂ©er les objets Date avec les heures correctes (09:00 et 09:15) const startDate = new Date(todayStr + 'T09:00:00'); const endDate = new Date(todayStr + 'T09:15:00'); const selectionInfo = { startStr: todayStr + 'T09:00:00', endStr: todayStr + 'T09:15:00', start: startDate, end: endDate, allDay: false }; // Ouvrir le modal en mode crĂ©ation openModal('create', selectionInfo); }); // console.log('Bouton "Ajouter un RDV" initialisĂ©'); } /** * VĂ©rifie la disponibilitĂ© d'un Ă©vĂ©nement pour une nouvelle date/heure * @param {FullCalendar.Event} event - L'Ă©vĂ©nement Ă  vĂ©rifier * @returns {Promise<{available: boolean, reason: string}>} */ async function checkEventAvailability(event) { try { const eventProps = event.extendedProps; const eventType = eventProps.type || null; // Extraire les informations de l'Ă©vĂ©nement const newDate = event.start.toISOString().split('T')[0]; const newTime = event.start.toTimeString().substring(0, 5); const newEndDate = event.end.toISOString().split('T')[0]; const newEndTime = event.end.toTimeString().substring(0, 5); // RĂ©cupĂ©rer les IDs des entitĂ©s const intervenantId = eventProps.id_intervenant; const traducteurId = eventProps.id_traducteur; const localId = eventProps.id_local; // Validation minimale commune : il faut toujours un intervenant if (!intervenantId) { return { available: false, reason: 'Informations manquantes sur l’intervenant' }; } // Pour les Ă©vĂ©nements standards (hors permanences), un local est requis. // Le traducteur reste optionnel. if (eventType !== 'permanence' && !localId) { return { available: false, reason: 'Informations manquantes sur le local pour cet Ă©vĂ©nement' }; } // VĂ©rifier si le traducteur est valide (non null, non undefined, et supĂ©rieur Ă  0) const hasValidTraducteur = traducteurId && parseInt(traducteurId, 10) > 0; console.log('🔍 VĂ©rification de disponibilitĂ© pour:', { date: newDate, time: newTime, endDate: newEndDate, endTime: newEndTime, intervenantId, traducteurId, hasValidTraducteur, localId }); // PrĂ©parer le body de la requĂȘte const requestBody = { date: newDate, heure: newTime, date_fin: newEndDate, heure_fin: newEndTime, id_intervenant: intervenantId, id_local: localId, event_id: event.id // Exclure l'Ă©vĂ©nement actuel de la vĂ©rification }; // N'envoyer id_traducteur que s'il est valide (supĂ©rieur Ă  0) if (hasValidTraducteur) { requestBody.id_traducteur = traducteurId; } // Appeler l'API pour vĂ©rifier les disponibilitĂ©s // apiFetch retourne directement les donnĂ©es (data.data), pas l'objet complet avec success const disponibilites = await apiFetch('agenda/disponibilites', { method: 'POST', body: JSON.stringify(requestBody) }); // VĂ©rifier que les disponibilitĂ©s sont valides if (!disponibilites || typeof disponibilites !== 'object') { console.error('RĂ©ponse invalide de l\'API de disponibilitĂ©s:', disponibilites); return { available: false, reason: 'Erreur lors de la vĂ©rification des disponibilitĂ©s' }; } // VĂ©rifier l'intervenant if (disponibilites.intervenants && disponibilites.intervenants.length === 0) { return { available: false, reason: 'L\'intervenant n\'est pas disponible Ă  cette date/heure' }; } // Pour les permanences, on ne bloque pas sur l’absence de traducteur ou de local : // l’objectif est de permettre le dĂ©placement du crĂ©neau de disponibilitĂ©. if (eventType !== 'permanence') { // VĂ©rifier le traducteur seulement s'il y en a un d'assignĂ© (valide, supĂ©rieur Ă  0) et que la vĂ©rification a Ă©tĂ© faite if (hasValidTraducteur && disponibilites.hasOwnProperty('traducteurs') && Array.isArray(disponibilites.traducteurs) && disponibilites.traducteurs.length === 0) { return { available: false, reason: 'Le traducteur n\'est pas disponible Ă  cette date/heure' }; } // VĂ©rifier le local if (disponibilites.locaux && disponibilites.locaux.length === 0) { return { available: false, reason: 'Le local n\'est pas disponible Ă  cette date/heure' }; } } console.log('✅ Toutes les entitĂ©s sont disponibles'); return { available: true, reason: 'Toutes les entitĂ©s sont disponibles' }; } catch (error) { console.error('Erreur lors de la vĂ©rification des disponibilitĂ©s:', error); return { available: false, reason: 'Erreur lors de la vĂ©rification des disponibilitĂ©s' }; } }