// 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 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 /** * Calcule la luminosité d'une couleur hexadécimale * @param {string} hexColor - Couleur au format #RRGGBB * @returns {number} - Luminosité entre 0 et 1 */ function getLuminance(hexColor) { // Vérifier que hexColor n'est pas null ou undefined if (!hexColor || typeof hexColor !== 'string') { console.warn('⚠️ [getLuminance] Valeur hexColor invalide:', hexColor); return 0.5; // Retourner une valeur moyenne par défaut } // Convertir hex en RGB const hex = hexColor.replace('#', ''); const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); // Calculer la luminosité relative selon WCAG return (0.299 * r + 0.587 * g + 0.114 * b) / 255; } /** * Détermine la couleur de texte optimale selon le contraste * @param {string} backgroundColor - Couleur de fond au format #RRGGBB * @returns {string} - Couleur de texte (#000000 ou #ffffff) */ function getTextColor(backgroundColor) { // Vérifier que backgroundColor n'est pas null ou undefined if (!backgroundColor || typeof backgroundColor !== 'string') { console.warn('⚠️ [getTextColor] Valeur backgroundColor invalide:', backgroundColor); return '#000000'; // Retourner noir par défaut } const luminance = getLuminance(backgroundColor); return luminance > 0.5 ? '#000000' : '#ffffff'; } /** * Convertit une couleur hexadécimale en RGBA avec opacité * @param {string} hex - Couleur au format #RRGGBB * @param {number} alpha - Opacité entre 0 et 1 * @returns {string} - Couleur au format rgba(r, g, b, a) */ function hexToRgba(hex, alpha) { // Vérifier que hex n'est pas null ou undefined if (!hex || typeof hex !== 'string') { console.warn('⚠️ [hexToRgba] Valeur hex invalide:', hex); return `rgba(108, 117, 125, ${alpha || 1})`; // Retourner gris par défaut } const hexClean = hex.replace('#', ''); const r = parseInt(hexClean.substr(0, 2), 16); const g = parseInt(hexClean.substr(2, 2), 16); const b = parseInt(hexClean.substr(4, 2), 16); const rgbaValue = `rgba(${r}, ${g}, ${b}, ${alpha || 1})`; console.log('🔧 [hexToRgba] Conversion:', { hex, alpha, result: rgbaValue }); return rgbaValue; } 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 => { // Fonction utilitaire pour trouver la configuration d'un intervenant function findIntervenantConfig(intervenant) { if (!intervenant || !crviACFData || !crviACFData.intervenants) { // console.log('❌ Données manquantes - intervenant:', !!intervenant, 'crviACFData:', !!crviACFData, 'intervenants:', !!crviACFData?.intervenants); return null; } // console.log('Intervenants disponibles:', crviACFData.intervenants); // console.log('Intervenant recherché:', intervenant); // Vérifier que l'intervenant a un ID if (!intervenant.id) { // console.log('❌ ID d\'intervenant manquant:', intervenant); return null; } const intervenantId = parseInt(intervenant.id); if (isNaN(intervenantId)) { // console.log('❌ ID d\'intervenant invalide:', 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 pour la clé:', key, 'config:', config); continue; } // console.log('Comparaison - ID recherché:', intervenantId, 'ID config:', config.id, 'Type config.id:', typeof config.id); if (config.id == intervenantId) { intervenantConfig = config; // console.log('✅ Configuration trouvée pour ID:', intervenantId); break; } } if (!intervenantConfig) { // console.log('❌ Aucune configuration trouvée pour l\'ID:', intervenantId); // console.log('IDs disponibles:', Object.values(crviACFData.intervenants).map(c => c.id)); } return intervenantConfig; } // Hiérarchie de détermination des couleurs : // 1) Pour RDV assignés (individuel/groupe) avec type d'intervention : couleur du type d'intervention // 2) Pour RDV assignés (individuel/groupe) sans type d'intervention : orange (par défaut) // 3) Pour permanences : couleur selon le type (assignée, non attribuée, non disponible) let backgroundColor = null; let textColor = null; // Vérifier si l'événement est assigné (a un intervenant et un local/beneficiaire) const isEventAssigned = ev.id_intervenant && (ev.id_local || ev.id_beneficiaire); // Pour les RDV (individuel/groupe) assignés if (ev.type && ev.type !== 'permanence' && isEventAssigned) { // Vérifier si l'événement a un type d'intervention défini let typeInterventionId = null; if (ev.id_type_intervention) { typeInterventionId = parseInt(ev.id_type_intervention); } else if (ev.type_intervention && ev.type_intervention.id) { typeInterventionId = parseInt(ev.type_intervention.id); } // Si l'événement a un type d'intervention, utiliser sa couleur depuis crviAjax if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) { const couleurTypeIntervention = window.crviAjax.couleurs_types_intervention[typeInterventionId]; if (couleurTypeIntervention) { backgroundColor = couleurTypeIntervention; textColor = getTextColor(couleurTypeIntervention); console.log('🎨 [COULEUR] RDV assigné avec type d\'intervention:', { eventId: ev.id, type: ev.type, typeInterventionId: typeInterventionId, backgroundColor: backgroundColor, textColor: textColor, source: 'type_intervention' }); } } // Si pas de couleur définie (pas de type d'intervention), garder orange par défaut if (!backgroundColor) { backgroundColor = '#ff9800'; // Orange pour les événements assignés sans type d'intervention textColor = getTextColor(backgroundColor); console.log('🎨 [COULEUR] RDV assigné sans type d\'intervention (défaut orange):', { eventId: ev.id, type: ev.type, backgroundColor: backgroundColor, textColor: textColor, source: 'defaut_assigné' }); } } // Pour les RDV non assignés : couleur selon type_de_local (bureau ou salle) if (!backgroundColor && ev.type && ev.type !== 'permanence' && !isEventAssigned) { // Vérifier si l'événement a un local avec un type_de_local if (ev.local && ev.local.type_de_local) { const typeLocal = ev.local.type_de_local.toLowerCase(); if (crviACFData && crviACFData.couleurs_rdv) { const couleurRdv = crviACFData.couleurs_rdv[typeLocal]; if (couleurRdv) { backgroundColor = couleurRdv; textColor = getTextColor(couleurRdv); console.log('🎨 [COULEUR] RDV non assigné selon type de local:', { eventId: ev.id, type: ev.type, typeLocal: typeLocal, backgroundColor: backgroundColor, textColor: textColor, source: 'type_local' }); } } } } // Pour permanences (événements de type 'permanence') : logique simplifiée // 1) Non attribuée : dispo ? couleur dispo : couleur indispo // 2) Attribuée : a type intervention ? couleur type : couleur défaut let isPermanenceDisabled = false; if (!backgroundColor && ev.type === 'permanence') { // Une permanence est assignée si elle a un bénéficiaire ET un local (assign = 1) const isPermanenceAssigned = ev.assign === 1 || (ev.id_beneficiaire && ev.id_local); // Une permanence est non attribuée si elle n'est pas assignée (assign = 0) const isPermanenceNonAttribuee = ev.assign === 0 && !ev.id_beneficiaire && !ev.id_local; // Vérifier si le bénéficiaire est en congé (indisponibilitee_ponctuelle) let isPermanenceNonDisponible = false; if (ev.beneficiaire && ev.beneficiaire.indisponibilitee_ponctuelle && Array.isArray(ev.beneficiaire.indisponibilitee_ponctuelle)) { const eventDate = ev.date_rdv; // Format YYYY-MM-DD const eventDateObj = new Date(eventDate + 'T00:00:00'); // Vérifier si la date de l'événement est dans une période d'indisponibilité for (const indispo of ev.beneficiaire.indisponibilitee_ponctuelle) { if (indispo.debut && indispo.fin) { let debutDate, finDate; // Gérer le format d/m/Y (format ACF) if (typeof indispo.debut === 'string' && indispo.debut.includes('/')) { const debutParts = indispo.debut.split('/'); const finParts = indispo.fin.split('/'); if (debutParts.length === 3 && finParts.length === 3) { // Format d/m/Y 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 { // Format YYYY-MM-DD ou timestamp debutDate = new Date(indispo.debut); finDate = new Date(indispo.fin); } if (debutDate && finDate && !isNaN(debutDate.getTime()) && !isNaN(finDate.getTime())) { // Ajuster pour inclure toute la journée debutDate.setHours(0, 0, 0, 0); finDate.setHours(23, 59, 59, 999); if (eventDateObj >= debutDate && eventDateObj <= finDate) { isPermanenceNonDisponible = true; isPermanenceDisabled = true; break; } } } } } // Vérifier si l'intervenant est indisponible (congés ou jour non disponible) if (!isPermanenceNonDisponible && ev.id_intervenant && window.crviACFData && window.crviACFData.indisponibilites_intervenants) { const intervenantId = parseInt(ev.id_intervenant); const intervenantDispo = window.crviACFData.indisponibilites_intervenants[intervenantId]; if (intervenantDispo) { const eventDate = ev.date_rdv; // Format YYYY-MM-DD const eventDateObj = new Date(eventDate + 'T00:00:00'); // Vérifier les jours de disponibilité (0 = dimanche, 1 = lundi, etc.) const dayOfWeek = eventDateObj.getDay(); const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; const dayName = dayNames[dayOfWeek]; // Si l'intervenant a des jours de disponibilité définis et que ce jour n'en fait pas partie if (intervenantDispo.jours_dispo && Array.isArray(intervenantDispo.jours_dispo) && intervenantDispo.jours_dispo.length > 0 && !intervenantDispo.jours_dispo.includes(dayName)) { console.log('🚫 [PERMANENCE] Intervenant non disponible ce jour:', { intervenantId, date: eventDate, dayName, joursDisponibles: intervenantDispo.jours_dispo }); isPermanenceNonDisponible = true; isPermanenceDisabled = true; } // Vérifier les congés/indisponibilités ponctuelles if (!isPermanenceNonDisponible && intervenantDispo.conges && Array.isArray(intervenantDispo.conges)) { for (const conge of intervenantDispo.conges) { if (conge.debut && conge.fin) { let debutDate, finDate; // Gérer le format d/m/Y (format ACF) 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 { // Format YYYY-MM-DD ou timestamp 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) { console.log('🚫 [PERMANENCE] Intervenant en congé:', { intervenantId, date: eventDate, congeDebut: conge.debut, congeFin: conge.fin, congeType: conge.type }); isPermanenceNonDisponible = true; isPermanenceDisabled = true; break; } } } } } } } // Vérifier aussi le statut si pas déjà déterminé if (!isPermanenceNonDisponible && ev.statut && ['annule', 'non_tenu', 'absence'].includes(ev.statut)) { isPermanenceNonDisponible = true; } // LOGIQUE SIMPLIFIÉE if (isPermanenceNonAttribuee) { // 1) Non attribuée : dispo ? couleur dispo : couleur indispo if (isPermanenceNonDisponible && window.crviAjax && window.crviAjax.couleur_permanence_non_disponible) { backgroundColor = window.crviAjax.couleur_permanence_non_disponible; console.log('🎨 [COULEUR] Permanence non attribuée - indisponible:', { eventId: ev.id, backgroundColor: backgroundColor, source: 'permanence_non_attribuee_indispo' }); } else if (window.crviAjax && window.crviAjax.couleur_permanence_non_attribuee) { backgroundColor = window.crviAjax.couleur_permanence_non_attribuee; console.log('🎨 [COULEUR] Permanence non attribuée - disponible:', { eventId: ev.id, backgroundColor: backgroundColor, source: 'permanence_non_attribuee_dispo' }); } } else if (isPermanenceAssigned) { // 2) Attribuée : a type intervention ? couleur type : couleur défaut let typeInterventionId = null; if (ev.id_type_intervention) { typeInterventionId = parseInt(ev.id_type_intervention); } else if (ev.type_intervention && ev.type_intervention.id) { typeInterventionId = parseInt(ev.type_intervention.id); } if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) { const couleurTypeIntervention = window.crviAjax.couleurs_types_intervention[typeInterventionId]; if (couleurTypeIntervention) { backgroundColor = couleurTypeIntervention; console.log('🎨 [COULEUR] Permanence assignée - avec type intervention:', { eventId: ev.id, typeInterventionId: typeInterventionId, backgroundColor: backgroundColor, source: 'permanence_assignee_type' }); } } // Si pas de type d'intervention, utiliser couleur défaut if (!backgroundColor) { backgroundColor = (crviACFData && crviACFData.couleurs_permanence && crviACFData.couleurs_permanence.permanence) ? crviACFData.couleurs_permanence.permanence : '#9e9e9e'; console.log('🎨 [COULEUR] Permanence assignée - défaut:', { eventId: ev.id, backgroundColor: backgroundColor, source: 'permanence_assignee_defaut' }); } } // S'assurer que backgroundColor n'est pas null avant d'appeler getTextColor if (backgroundColor) { textColor = getTextColor(backgroundColor); } else { textColor = '#000000'; // Par défaut } } // Couleur par défaut si aucune condition n'est remplie if (!backgroundColor) { backgroundColor = '#6c757d'; // Gris par défaut textColor = getTextColor(backgroundColor); console.log('🎨 [COULEUR] Couleur par défaut (gris):', { eventId: ev.id, type: ev.type, backgroundColor: backgroundColor, textColor: textColor, source: 'defaut_general' }); } // Log final de la couleur appliquée console.log('🎨 [COULEUR FINALE] Événement:', { eventId: ev.id, title: ev.type || 'Sans type', backgroundColor: backgroundColor, borderColor: backgroundColor, textColor: textColor, isAssigned: isEventAssigned, type: ev.type }); // Générer un titre plus explicite let title = ev.type || 'Sans type'; // Gestion spéciale pour les permanences if (ev.type === 'permanence') { if (ev.intervenant) { const intervenant = ev.intervenant; const nomComplet = `${intervenant.prenom || ''} ${intervenant.nom || ''}`.trim(); title = 'p. ' + (nomComplet || 'Intervenant'); } else { title = 'p. Intervenant'; } } else { // Utiliser le nom de l'intervenant comme titre principal pour les autres événements if (ev.intervenant) { const intervenant = ev.intervenant; const nomComplet = `${intervenant.prenom || ''} ${intervenant.nom || ''}`.trim(); if (nomComplet) { title = nomComplet; } } // Ajouter le type d'intervention principal si disponible if (ev.intervenant && ev.intervenant.types_intervention_noms && ev.intervenant.types_intervention_noms.length > 0) { const primaryType = ev.intervenant.types_intervention_noms[0]; // Premier type d'intervention title += ` - ${primaryType}`; } // Ajouter le commentaire si disponible if (ev.commentaire) { title += ' - ' + ev.commentaire; } } // S'assurer que les couleurs sont toujours définies if (!backgroundColor || typeof backgroundColor !== 'string') { console.warn('⚠️ [Mapping] backgroundColor invalide pour l\'événement:', ev.id, backgroundColor); backgroundColor = '#6c757d'; // Gris par défaut } if (!textColor || typeof textColor !== 'string') { textColor = getTextColor(backgroundColor); } // Vérifier si l'événement est passé const eventStartDate = new Date(ev.date_rdv + 'T' + ev.heure_rdv); const now = new Date(); const isEventPast = eventStartDate < now; // Un événement est éditable seulement si : // 1. Il n'est pas désactivé (permanence) // 2. Il n'est pas passé const isEditable = !isPermanenceDisabled && !isEventPast; return { id: ev.id, title: title, start: ev.date_rdv + 'T' + ev.heure_rdv, end: ev.date_fin + 'T' + ev.heure_fin, backgroundColor: backgroundColor, borderColor: backgroundColor, textColor: textColor, editable: isEditable, // Désactiver l'édition si la permanence est désactivée OU si l'événement est passé durationEditable: isEditable, // Désactiver le redimensionnement si la permanence est désactivée OU si l'événement est passé startEditable: isEditable, // Désactiver le déplacement si la permanence est désactivée OU si l'événement est passé classNames: isPermanenceDisabled ? ['permanence-disabled'] : (isEventPast ? ['event-past'] : []), extendedProps: { // Données de base type: ev.type, commentaire: ev.commentaire, date_rdv: ev.date_rdv, heure_rdv: ev.heure_rdv, date_fin: ev.date_fin, heure_fin: ev.heure_fin, // Relations avec les entités id_beneficiaire: ev.id_beneficiaire, id_intervenant: ev.id_intervenant, id_traducteur: ev.id_traducteur, id_local: ev.id_local, id_departement: ev.id_departement, id_type_intervention: ev.id_type_intervention || (ev.type_intervention && ev.type_intervention.id ? ev.type_intervention.id : null), langue: ev.langue, langues_disponibles: ev.langues_disponibles || null, // Données des entités (si disponibles) beneficiaire: ev.beneficiaire, intervenant: ev.intervenant, traducteur: ev.traducteur, local: ev.local, // Données spécifiques aux groupes nb_participants: ev.nb_participants, nb_hommes: ev.nb_hommes, nb_femmes: ev.nb_femmes, // Autres données statut: ev.statut, assign: ev.assign || 0, isDisabled: isPermanenceDisabled, created_at: ev.created_at, updated_at: ev.updated_at } }; }); 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 = `