Crvi/assets/js/modules/agenda-fullcalendar.js
2026-01-20 07:54:37 +01:00

1553 lines
81 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = `
<div class="event-popover">
<h6 class="mb-2">${event.title}</h6>
<div class="mb-1"><strong>Date:</strong> ${formatDateToFrench(eventProps.date_rdv) || ''}</div>
<div class="mb-1"><strong>Heure:</strong> ${eventProps.heure_rdv || ''}</div>
<div class="mb-1"><strong>Type:</strong> ${eventProps.type || ''}</div>
<div class="mb-1"><strong>Langue:</strong> ${eventProps.langue || ''}</div>
${eventProps.beneficiaire ? `<div class="mb-1"><strong>Bénéficiaire:</strong> ${getEntityDisplayName(eventProps.beneficiaire, 'beneficiaire')}</div>` : ''}
${eventProps.intervenant ? `<div class="mb-1"><strong>Intervenant:</strong> ${getEntityDisplayName(eventProps.intervenant, 'intervenant')}</div>` : ''}
${eventProps.local ? `<div class="mb-1"><strong>Local:</strong> ${getEntityDisplayName(eventProps.local, 'local')}</div>` : ''}
${eventProps.commentaire ? `<div class="mb-1"><strong>Commentaire:</strong> ${eventProps.commentaire}</div>` : ''}
<div class="mb-1"><strong>Statut:</strong> <span class="event-status" style="background-color: ${getStatutColor(eventProps.statut)};">${eventProps.statut}</span></div>
<div class="mt-2">
<small class="text-muted">Cliquez pour plus de détails</small>
</div>
</div>
`;
// 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: '<div class="popover event-popover" role="tooltip" style="opacity:1;"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'
});
// 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 lintervenant'
};
}
// 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 labsence de traducteur ou de local :
// lobjectif 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'
};
}
}