Crvi/assets/js/modules/agenda-fullcalendar.js
2026-01-20 16:15:24 +01:00

1232 lines
62 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 { mapEventToFullCalendar, getTextColor, getLuminance } from './agenda-event-mapper.js';
import toastr from 'toastr';
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import listPlugin from '@fullcalendar/list';
import interactionPlugin from '@fullcalendar/interaction';
import { apiFetch } from './agenda-api.js';
// Ajoutez d'autres plugins si besoin
export function initializeCalendar() {
console.log('🚀 Initialisation de FullCalendar...');
const calendarEl = document.getElementById('agenda-calendar');
if (!calendarEl) {
console.error('❌ Élément agenda-calendar non trouvé');
return null;
}
console.log('✅ Élément agenda-calendar trouvé, création du calendrier...');
console.log('🔧 Configuration FullCalendar - Options d\'édition:');
console.log(' - editable: true');
console.log(' - eventStartEditable: true');
console.log(' - eventResizableFromStart: true');
console.log(' - eventDurationEditable: true');
console.log(' - selectable: true');
console.log(' - dateClick: implémenté');
const calendar = new Calendar(calendarEl, {
plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin], // Ajoutez ici les autres plugins si besoin
initialView: 'timeGridWeek',
eventDisplay: 'block', // Afficher les événements comme des blocs pleins
slotEventOverlap: false, // Empêche le chevauchement visuel des événements
eventMaxStack: 1, // Limite l'empilement des événements à 1
eventMinHeight: 20, // Hauteur minimale des événements en pixels
eventShortHeight: 20, // Hauteur des événements courts en pixels
expandRows: true, // Permet d'étendre les lignes pour afficher tous les événements
locale: 'fr',
firstDay: 1, // La semaine commence le lundi (0 = dimanche, 1 = lundi)
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
},
buttonText: {
today: 'Aujourd\'hui',
month: 'Mois',
week: 'Semaine',
day: 'Jour',
list: 'Liste'
},
height: 'auto',
editable: true,
eventStartEditable: true, // Permet le drag & drop des événements
eventDurationEditable: true, // Permet le redimensionnement des événements
eventResizableFromStart: true, // Permet le redimensionnement depuis le début
selectable: true,
selectMirror: true,
dayMaxEventRows: true, // Active la limite de lignes d'événements par jour
dayMaxEvents: 3, // Afficher maximum 3 événements par jour, puis "+x plus"
slotDuration: '00:30:00', // Durée de chaque créneau horaire : 30 minutes
slotLabelInterval: '01:00', // Afficher les étiquettes d'heures toutes les heures
weekends: true,
moreLinkClick: 'popover', // Afficher les événements cachés dans un popover
moreLinkContent: function(arg) {
// Personnaliser le texte du lien "+x plus"
const count = arg.num;
return `+${count} plus`;
},
events: async function(fetchInfo, successCallback, failureCallback) {
try {
const params = {
start: fetchInfo.startStr.split('T')[0], // Date de début du mois
end: fetchInfo.endStr.split('T')[0] // Date de fin du mois
};
const apiEvents = await getEvents(params);
// Mapping des objets API vers le format FullCalendar
const events = apiEvents.map(ev => mapEventToFullCalendar(ev));
successCallback(events);
} catch (e) {
console.error('Erreur lors du chargement des événements:', e);
notifyError('Erreur lors du chargement des événements');
failureCallback(e);
}
},
select: function(arg) {
// Vérifier que la date de début n'est pas dans le passé
if (arg.startStr) {
const startDateStr = arg.startStr.split('T')[0];
const today = new Date();
today.setHours(0, 0, 0, 0);
const selectedDate = new Date(startDateStr + 'T00:00:00');
selectedDate.setHours(0, 0, 0, 0);
if (selectedDate < today) {
notifyError('Impossible de créer un événement à une date passée');
console.warn('⚠️ Tentative de création d\'événement à une date passée:', startDateStr);
return; // Empêcher l'ouverture du modal
}
}
// Corriger les données pour que la date de fin soit le même jour que le début
// (FullCalendar ajoute +1 jour pour les sélections allDay)
const dateDebut = arg.startStr?.split('T')[0];
const heureDebut = arg.startStr?.split('T')[1] || '09:00:00';
const heureFin = arg.endStr?.split('T')[1] || '09:15:00';
// Créer de nouveaux objets Date avec les bonnes heures en timezone local
const startDate = new Date(`${dateDebut}T${heureDebut}`);
const endDate = new Date(`${dateDebut}T${heureFin}`);
const correctedArg = {
...arg,
start: startDate,
end: endDate,
startStr: `${dateDebut}T${heureDebut}`,
endStr: `${dateDebut}T${heureFin}`,
allDay: false
};
openModal('create', correctedArg);
},
// dateClick désactivé car select gère déjà la création d'événements
// et évite les doubles appels
/*dateClick: function(info) {
// Vérifier les permissions de création
const userCanCreate = window.crviPermissions && window.crviPermissions.can_create;
if (!userCanCreate) {
console.warn('⚠️ Utilisateur non autorisé à créer des événements');
return;
}
// Créer un objet avec les informations de la date cliquée
const clickedDate = info.date;
// CORRECTION: Utiliser dateStr si disponible, sinon extraire correctement la date locale
// Le problème de décalage vient du fait que info.date peut être en UTC
// et que getDate()/getMonth() utilisent le fuseau horaire local, causant un décalage
let dateStr;
if (info.dateStr) {
// FullCalendar fournit dateStr directement (format YYYY-MM-DD) - c'est la méthode la plus fiable
dateStr = info.dateStr;
} else {
// Fallback: utiliser toLocaleDateString avec des options pour obtenir YYYY-MM-DD
// Cela garantit qu'on obtient la date locale réelle, pas une date UTC convertie
const dateParts = clickedDate.toLocaleDateString('fr-CA', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
// toLocaleDateString avec 'fr-CA' retourne YYYY-MM-DD directement
dateStr = dateParts;
}
// Vérifier que la date n'est pas dans le passé
const today = new Date();
today.setHours(0, 0, 0, 0);
const clickedDateOnly = new Date(dateStr + 'T00:00:00');
clickedDateOnly.setHours(0, 0, 0, 0);
if (clickedDateOnly < today) {
notifyError('Impossible de créer un événement à une date passée');
return;
}
const eventData = {
startStr: dateStr + 'T09:00:00',
endStr: dateStr + 'T10:00:00',
start: clickedDate,
end: new Date(clickedDate.getTime() + 60 * 60 * 1000), // +1 heure
allDay: false
};
openModal('create', eventData);
},*/
eventDrop: async function(info) {
console.log('🔄 EVENT DROP DÉTECTÉ');
console.log('📋 Informations de l\'événement:', {
id: info.event.id,
title: info.event.title,
start: info.event.start,
end: info.event.end,
oldStart: info.oldEvent.start,
oldEnd: info.oldEvent.end
});
// Vérifier les permissions
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
if (!userCanEdit) {
console.warn('❌ Utilisateur non autorisé à déplacer des événements');
info.revert(); // Annuler le déplacement
notifyError('Vous n\'êtes pas autorisé à déplacer des événements');
return;
}
// Vérifier si l'ancien événement était passé (on ne peut pas déplacer un événement passé)
const oldStartDate = new Date(info.oldEvent.start);
const now = new Date();
if (oldStartDate < now) {
console.warn('❌ Impossible de déplacer un événement passé');
info.revert(); // Annuler le déplacement
notifyError('Impossible de déplacer un événement passé');
return;
}
// Vérifier les disponibilités avant de permettre le drop
const isAvailable = await checkEventAvailability(info.event);
if (!isAvailable.available) {
console.warn('❌ Événement non déplaçable - Conflit de disponibilité:', isAvailable.reason);
info.revert(); // Annuler le déplacement
notifyError(`Impossible de déplacer l'événement : ${isAvailable.reason}`);
return;
}
try {
console.log('✅ Disponibilité vérifiée - Déplacement autorisé');
// Préparer les données de mise à jour
const newStart = info.event.start;
const newEnd = info.event.end || new Date(newStart.getTime() + 60 * 60 * 1000); // +1h par défaut
const updateData = {
date_rdv: newStart.toISOString().split('T')[0],
heure_rdv: newStart.toTimeString().substring(0, 5),
date_fin: newEnd.toISOString().split('T')[0],
heure_fin: newEnd.toTimeString().substring(0, 5)
};
// Appeler l'API pour mettre à jour l'événement
await updateEvent(info.event.id, updateData);
// Succès
toastr.success('Événement déplacé avec succès');
} catch (error) {
console.error('Erreur lors du déplacement de l\'événement:', error);
info.revert(); // Annuler le déplacement en cas d'erreur
notifyError('Erreur lors du déplacement de l\'événement');
}
},
eventResize: async function(info) {
// console.log('📏 EVENT RESIZE DÉTECTÉ');
// console.log('📋 Informations de l\'événement:', {
// id: info.event.id,
// title: info.event.title,
// start: info.event.start,
// end: info.event.end,
// oldStart: info.oldEvent.start,
// oldEnd: info.oldEvent.end
// });
// Vérifier les permissions
// console.log('🔐 Vérification des permissions...');
// console.log('window.crviPermissions:', window.crviPermissions);
// console.log('window.crviPermissions.can_edit:', window.crviPermissions?.can_edit);
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
if (!userCanEdit) {
console.warn('❌ Utilisateur non autorisé à redimensionner des événements');
// console.log('🔒 Permissions insuffisantes - Annulation du redimensionnement');
info.revert(); // Annuler le redimensionnement
notifyError('Vous n\'êtes pas autorisé à redimensionner des événements');
return;
}
// console.log('✅ Permissions OK - Redimensionnement autorisé');
try {
// console.log('Redimensionnement d\'événement:', info.event.id, 'nouvelle fin:', info.event.end);
// Préparer les données de mise à jour
const newEnd = info.event.end;
const updateData = {
date_fin: newEnd.toISOString().split('T')[0],
heure_fin: newEnd.toTimeString().substring(0, 5)
};
// Appeler l'API pour mettre à jour l'événement
await updateEvent(info.event.id, updateData);
// Succès
toastr.success('Événement redimensionné avec succès');
// console.log('Événement redimensionné avec succès');
} catch (error) {
console.error('Erreur lors du redimensionnement de l\'événement:', error);
info.revert(); // Annuler le redimensionnement en cas d'erreur
notifyError('Erreur lors du redimensionnement de l\'événement');
}
},
eventClick: async function(arg) {
// Vérifier les permissions avant d'ouvrir le modal
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
const userCanView = window.crviPermissions && window.crviPermissions.can_view;
if (!userCanView) {
console.warn('Utilisateur non autorisé à voir les événements');
return;
}
try {
// Overlay ciblé sur le calendrier pendant le chargement des détails
const calendarEl = document.getElementById('agenda-calendar');
if (window.CRVI_OVERLAY && calendarEl) { window.CRVI_OVERLAY.show(calendarEl); }
// Charger les détails complets de l'événement depuis l'API
const eventId = arg.event.id;
// console.log('Chargement des détails de l\'événement:', eventId);
const eventDetails = await getEvent(eventId);
// console.log('eventClick - Détails de l\'événement reçus:', eventDetails);
// console.log('eventClick - date_rdv:', eventDetails.date);
// console.log('eventClick - heure_rdv:', eventDetails.heure);
// console.log('eventClick - date_fin:', eventDetails.date_fin);
// console.log('eventClick - heure_fin:', eventDetails.heure_fin);
// Vérifier et créer les dates de début et fin
let startDate, endDate;
try {
// Créer la date de début
if (eventDetails.date_rdv && eventDetails.heure_rdv) {
startDate = new Date(eventDetails.date_rdv + 'T' + eventDetails.heure_rdv);
if (isNaN(startDate.getTime())) {
// console.warn('Date de début invalide:', eventDetails.date_rdv, eventDetails.heure_rdv);
startDate = new Date();
}
} else {
// console.warn('Données de date/heure de début manquantes');
startDate = new Date();
}
// Créer la date de fin
if (eventDetails.date_fin && eventDetails.heure_fin) {
endDate = new Date(eventDetails.date_fin + 'T' + eventDetails.heure_fin);
if (isNaN(endDate.getTime())) {
// console.warn('Date de fin invalide:', eventDetails.date_fin, eventDetails.heure_fin);
endDate = startDate;
}
} else {
// console.warn('Données de date/heure de fin manquantes, utilisation de la date de début');
endDate = startDate;
}
} catch (error) {
console.error('Erreur lors de la création des dates:', error);
startDate = new Date();
endDate = new Date();
}
// Créer un objet événement avec les détails complets
const fullEvent = {
id: eventDetails.id,
start: startDate,
end: endDate,
extendedProps: {
// Données de base
type: eventDetails.type,
commentaire: eventDetails.commentaire,
date: eventDetails.date,
heure: eventDetails.heure,
date_fin: eventDetails.date_fin,
heure_fin: eventDetails.heure_fin,
date_rdv: eventDetails.date_rdv,
heure_rdv: eventDetails.heure_rdv,
// Relations avec les entités
id_beneficiaire: eventDetails.id_beneficiaire,
id_intervenant: eventDetails.id_intervenant,
id_traducteur: eventDetails.id_traducteur,
id_local: eventDetails.id_local,
id_departement: eventDetails.id_departement,
id_type_intervention: eventDetails.id_type_intervention || (eventDetails.type_intervention && eventDetails.type_intervention.id ? eventDetails.type_intervention.id : null),
langue: eventDetails.langue,
langues_disponibles: eventDetails.langues_disponibles || null,
nom_traducteur: eventDetails.nom_traducteur,
// Données des entités
beneficiaire: eventDetails.beneficiaire,
intervenant: eventDetails.intervenant,
traducteur: eventDetails.traducteur,
local: eventDetails.local,
departement: eventDetails.departement,
type_intervention: eventDetails.type_intervention,
// Données spécifiques aux groupes
nb_participants: eventDetails.nb_participants,
nb_hommes: eventDetails.nb_hommes,
nb_femmes: eventDetails.nb_femmes,
// Autres données
statut: eventDetails.statut,
assign: eventDetails.assign || 0,
created_at: eventDetails.created_at,
updated_at: eventDetails.updated_at
}
};
// Déterminer le mode selon les permissions
const mode = 'view'; // Toujours ouvrir en mode view par défaut
openModal(mode, fullEvent);
} catch (error) {
console.error('Erreur lors du chargement des détails de l\'événement:', error);
notifyError('Erreur lors du chargement des détails de l\'événement');
} finally {
const calendarEl = document.getElementById('agenda-calendar');
if (window.CRVI_OVERLAY && calendarEl) { window.CRVI_OVERLAY.hide(calendarEl); }
}
},
eventDidMount: function(arg) {
// Debug: Afficher les données de l'événement
const event = arg.event;
const eventProps = event.extendedProps;
// Appliquer l'attribut data-disabled si la permanence est désactivée
if (eventProps && eventProps.isDisabled) {
arg.el.setAttribute('data-disabled', 'true');
arg.el.style.opacity = '0.6';
arg.el.style.cursor = 'not-allowed';
}
// console.log('=== DEBUG ÉVÉNEMENT ===');
// console.log('Événement:', event.title);
// console.log('Intervenant:', eventProps.intervenant);
// console.log('Statut:', eventProps.statut);
// console.log('Background défini:', event.backgroundColor);
// console.log('Text color défini:', event.textColor);
// console.log('========================');
// Fonction utilitaire pour trouver la configuration d'un intervenant
function findIntervenantConfig(intervenant) {
if (!intervenant || !crviACFData || !crviACFData.intervenants) {
// console.log('❌ Données manquantes dans eventDidMount - intervenant:', !!intervenant, 'crviACFData:', !!crviACFData, 'intervenants:', !!crviACFData?.intervenants);
return null;
}
// Vérifier que l'intervenant a un ID
if (!intervenant.id) {
// console.log('❌ ID d\'intervenant manquant dans eventDidMount:', intervenant);
return null;
}
const intervenantId = parseInt(intervenant.id);
if (isNaN(intervenantId)) {
// console.log('❌ ID d\'intervenant invalide dans eventDidMount:', intervenant.id);
return null;
}
// Chercher directement par ID
let intervenantConfig = null;
for (const key in crviACFData.intervenants) {
const config = crviACFData.intervenants[key];
// Vérifier que la config a un ID
if (!config || !config.id) {
// console.log('⚠️ Configuration invalide dans eventDidMount pour la clé:', key, 'config:', config);
continue;
}
if (config.id == intervenantId) {
intervenantConfig = config;
// console.log('✅ Configuration trouvée dans eventDidMount pour ID:', intervenantId);
break;
}
}
if (!intervenantConfig) {
// console.log('❌ Aucune configuration trouvée dans eventDidMount pour l\'ID:', intervenantId);
// console.log('IDs disponibles:', Object.values(crviACFData.intervenants).map(c => c.id));
}
return intervenantConfig;
}
// Appliquer les couleurs selon la logique simplifiée
const eventEl = arg.el;
const intervenantConfig = findIntervenantConfig(eventProps.intervenant);
// Fonction pour déterminer la couleur selon la logique simplifiée
function determineEventColor() {
let bgColor = null;
let txtColor = null;
// Pour les permanences : logique simplifiée
if (eventProps.type === 'permanence') {
const isPermanenceAssigned = eventProps.assign === 1 || (eventProps.id_beneficiaire && eventProps.id_local);
const isPermanenceNonAttribuee = eventProps.assign === 0 && !eventProps.id_beneficiaire && !eventProps.id_local;
// Vérifier si indisponible (via isDisabled ou statut)
let isPermanenceNonDisponible = eventProps.isDisabled ||
(eventProps.statut && ['annule', 'non_tenu', 'absence'].includes(eventProps.statut));
// Vérifier aussi si l'intervenant est indisponible
if (!isPermanenceNonDisponible && eventProps.id_intervenant && window.crviACFData && window.crviACFData.indisponibilites_intervenants) {
const intervenantId = parseInt(eventProps.id_intervenant);
const intervenantDispo = window.crviACFData.indisponibilites_intervenants[intervenantId];
if (intervenantDispo && eventProps.date_rdv) {
const eventDateObj = new Date(eventProps.date_rdv + 'T00:00:00');
const dayOfWeek = eventDateObj.getDay();
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const dayName = dayNames[dayOfWeek];
// Vérifier les jours de disponibilité
if (intervenantDispo.jours_dispo && Array.isArray(intervenantDispo.jours_dispo) &&
intervenantDispo.jours_dispo.length > 0 &&
!intervenantDispo.jours_dispo.includes(dayName)) {
isPermanenceNonDisponible = true;
}
// Vérifier les congés
if (!isPermanenceNonDisponible && intervenantDispo.conges && Array.isArray(intervenantDispo.conges)) {
for (const conge of intervenantDispo.conges) {
if (conge.debut && conge.fin) {
let debutDate, finDate;
if (typeof conge.debut === 'string' && conge.debut.includes('/')) {
const debutParts = conge.debut.split('/');
const finParts = conge.fin.split('/');
if (debutParts.length === 3 && finParts.length === 3) {
debutDate = new Date(parseInt(debutParts[2]), parseInt(debutParts[1]) - 1, parseInt(debutParts[0]));
finDate = new Date(parseInt(finParts[2]), parseInt(finParts[1]) - 1, parseInt(finParts[0]));
}
} else {
debutDate = new Date(conge.debut);
finDate = new Date(conge.fin);
}
if (debutDate && finDate && !isNaN(debutDate.getTime()) && !isNaN(finDate.getTime())) {
debutDate.setHours(0, 0, 0, 0);
finDate.setHours(23, 59, 59, 999);
if (eventDateObj >= debutDate && eventDateObj <= finDate) {
isPermanenceNonDisponible = true;
break;
}
}
}
}
}
}
}
if (isPermanenceNonAttribuee) {
// 1) Non attribuée : dispo ? couleur dispo : couleur indispo
if (isPermanenceNonDisponible && window.crviAjax && window.crviAjax.couleur_permanence_non_disponible) {
bgColor = window.crviAjax.couleur_permanence_non_disponible;
} else if (window.crviAjax && window.crviAjax.couleur_permanence_non_attribuee) {
bgColor = window.crviAjax.couleur_permanence_non_attribuee;
}
} else if (isPermanenceAssigned) {
// 2) Attribuée : ordre de priorité - département puis local
// Priorité 1 : Couleur du département
const departementId = eventProps.id_departement ? parseInt(eventProps.id_departement) : null;
// 🔍 DEBUG pour événement 410
if (event.id === '410') {
console.log('🔍 [DEBUG 410] Début analyse couleur département:', {
eventId: event.id,
id_departement_brut: eventProps.id_departement,
departementId_parsed: departementId,
crviACFData_existe: !!crviACFData,
departements_existe: !!(crviACFData && crviACFData.departements),
departements_data: crviACFData ? crviACFData.departements : 'N/A'
});
}
if (departementId && !isNaN(departementId) && crviACFData && crviACFData.departements) {
// 🔍 DEBUG pour événement 410 - liste des départements
if (event.id === '410') {
console.log('🔍 [DEBUG 410] Recherche département ID:', departementId);
console.log('🔍 [DEBUG 410] Départements disponibles:',
Object.keys(crviACFData.departements).map(key => ({
key: key,
id: crviACFData.departements[key].id,
nom: crviACFData.departements[key].nom,
couleur: crviACFData.departements[key].couleur
}))
);
}
// Chercher le département par ID
for (const key in crviACFData.departements) {
const dept = crviACFData.departements[key];
// 🔍 DEBUG pour événement 410 - comparaison
if (event.id === '410') {
console.log('🔍 [DEBUG 410] Comparaison:', {
key: key,
dept_id: dept.id,
dept_id_type: typeof dept.id,
recherche_id: departementId,
recherche_id_type: typeof departementId,
sont_egaux: dept.id === departementId,
dept_couleur: dept.couleur
});
}
if (dept.id === departementId && dept.couleur) {
bgColor = dept.couleur;
console.log('🎨 [COULEUR] Département trouvé:', {
eventId: event.id,
departementId: departementId,
departementNom: dept.nom,
couleur: bgColor
});
// 🔍 DEBUG pour événement 410 - succès
if (event.id === '410') {
console.log('✅ [DEBUG 410] Couleur département appliquée:', {
departementNom: dept.nom,
couleurAppliquee: bgColor
});
}
break;
}
}
// 🔍 DEBUG pour événement 410 - échec de recherche
if (event.id === '410' && !bgColor) {
console.warn('⚠️ [DEBUG 410] Aucun département correspondant trouvé!');
}
}
// Priorité 2 : Couleur du local (type de local)
if (!bgColor && eventProps.local) {
const localType = eventProps.local.type || eventProps.local_type;
if (localType && crviACFData && crviACFData.types_local) {
const typeLocalConfig = crviACFData.types_local[localType];
if (typeLocalConfig && typeLocalConfig.couleur) {
bgColor = typeLocalConfig.couleur;
console.log('🎨 [COULEUR] Type de local trouvé:', {
eventId: event.id,
localType: localType,
couleur: bgColor
});
}
}
}
// Fallback : couleur par défaut des permanences
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();
// 🔍 DEBUG FINAL pour événement 410
if (event.id === '410') {
console.log('🔍 [DEBUG 410] COULEUR FINALE après determineEventColor:', {
eventId: event.id,
bgColor: bgColor,
txtColor: txtColor,
eventProps_complet: eventProps
});
}
// 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
});
// 🔍 DEBUG FINAL pour événement 410 - après validation
if (event.id === '410') {
console.log('🔍 [DEBUG 410] COULEUR APPLIQUÉE (après validation):', {
eventId: event.id,
bgColor_final: bgColor,
txtColor_final: 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';
// Recalculer la couleur du texte en fonction de la luminosité du fond du popover
// pour éviter un texte blanc sur fond blanc pour les permanences non attribuées
const popoverTextColor = getTextColor(popoverColor);
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 pour obtenir le statut et sa couleur avec gestion des cas spéciaux
function getStatutDisplay(statut, eventType) {
let displayText = statut;
let displayColor = '#6c757d'; // Gris par défaut
// Cas spécial : permanence non attribuée
if (!statut && eventType === 'permanence') {
displayText = 'Non attribué';
displayColor = '#6c757d'; // Gris
return { text: displayText, color: displayColor };
}
// Si pas de statut, retourner une valeur par défaut
if (!statut) {
displayText = 'Non défini';
return { text: displayText, color: displayColor };
}
// Normaliser le statut (enlever accents et mettre en minuscules pour comparaison)
const statutLower = statut.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
// Déterminer la couleur selon le statut
if (statutLower.includes('absent')) {
displayColor = '#ffb3ba'; // Rouge pastel
} else if (statutLower.includes('present')) {
displayColor = '#90ee90'; // Vert clair
} else if (statutLower.includes('prevu')) {
displayColor = '#add8e6'; // Bleu clair
} else {
// Essayer de récupérer depuis les données ACF
if (crviACFData && crviACFData.statuts) {
const statutConfig = crviACFData.statuts[statut];
if (statutConfig && statutConfig.couleur) {
displayColor = statutConfig.couleur;
}
}
}
return { text: displayText, color: displayColor };
}
// 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);
// Obtenir le statut formaté avec sa couleur
const statutDisplay = getStatutDisplay(eventProps.statut, eventProps.type);
// 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: ${statutDisplay.color}; color: ${getTextColor(statutDisplay.color)}; padding: 2px 8px; border-radius: 3px;">${statutDisplay.text}</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
});
}
// S'assurer que le body du popover a une couleur de texte lisible
const bodyElement = popoverElement.querySelector('.popover-body');
if (bodyElement) {
// Le body du popover a un fond blanc, donc le texte doit toujours être sombre
bodyElement.style.color = '#000000';
}
// Appliquer les styles du statut
const statutElement = popoverElement.querySelector('.event-status');
if (statutElement) {
const statutDisplay = getStatutDisplay(eventProps.statut, eventProps.type);
statutElement.style.backgroundColor = statutDisplay.color;
statutElement.style.color = getTextColor(statutDisplay.color);
statutElement.style.padding = '2px 8px';
statutElement.style.borderRadius = '3px';
console.log('🎨 [POPOVER] Styles de statut appliqués:', {
statut: statutDisplay.text,
backgroundColor: statutDisplay.color
});
}
}
});
// É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
});
}
// S'assurer que le body du popover a une couleur de texte lisible
const bodyElement = node.querySelector('.popover-body');
if (bodyElement) {
// Le body du popover a un fond blanc, donc le texte doit toujours être sombre
bodyElement.style.color = '#000000';
}
// Appliquer aussi les styles du statut
const statutElement = node.querySelector('.event-status');
if (statutElement) {
const statutDisplay = getStatutDisplay(eventProps.statut, eventProps.type);
statutElement.style.backgroundColor = statutDisplay.color;
statutElement.style.color = getTextColor(statutDisplay.color);
statutElement.style.padding = '2px 8px';
statutElement.style.borderRadius = '3px';
console.log('🎨 [POPOVER] Styles de statut appliqués via observer:', {
statut: statutDisplay.text,
backgroundColor: statutDisplay.color
});
}
}
});
});
});
// 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'
};
}
}