1232 lines
62 KiB
JavaScript
1232 lines
62 KiB
JavaScript
// 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 l’intervenant'
|
||
};
|
||
}
|
||
|
||
// Pour les événements standards (hors permanences), un local est requis.
|
||
// Le traducteur reste optionnel.
|
||
if (eventType !== 'permanence' && !localId) {
|
||
return {
|
||
available: false,
|
||
reason: 'Informations manquantes sur le local pour cet événement'
|
||
};
|
||
}
|
||
|
||
// Vérifier si le traducteur est valide (non null, non undefined, et supérieur à 0)
|
||
const hasValidTraducteur = traducteurId && parseInt(traducteurId, 10) > 0;
|
||
|
||
console.log('🔍 Vérification de disponibilité pour:', {
|
||
date: newDate,
|
||
time: newTime,
|
||
endDate: newEndDate,
|
||
endTime: newEndTime,
|
||
intervenantId,
|
||
traducteurId,
|
||
hasValidTraducteur,
|
||
localId
|
||
});
|
||
|
||
// Préparer le body de la requête
|
||
const requestBody = {
|
||
date: newDate,
|
||
heure: newTime,
|
||
date_fin: newEndDate,
|
||
heure_fin: newEndTime,
|
||
id_intervenant: intervenantId,
|
||
id_local: localId,
|
||
event_id: event.id // Exclure l'événement actuel de la vérification
|
||
};
|
||
|
||
// N'envoyer id_traducteur que s'il est valide (supérieur à 0)
|
||
if (hasValidTraducteur) {
|
||
requestBody.id_traducteur = traducteurId;
|
||
}
|
||
|
||
// Appeler l'API pour vérifier les disponibilités
|
||
// apiFetch retourne directement les données (data.data), pas l'objet complet avec success
|
||
const disponibilites = await apiFetch('agenda/disponibilites', {
|
||
method: 'POST',
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
// Vérifier que les disponibilités sont valides
|
||
if (!disponibilites || typeof disponibilites !== 'object') {
|
||
console.error('Réponse invalide de l\'API de disponibilités:', disponibilites);
|
||
return {
|
||
available: false,
|
||
reason: 'Erreur lors de la vérification des disponibilités'
|
||
};
|
||
}
|
||
|
||
// Vérifier l'intervenant
|
||
if (disponibilites.intervenants && disponibilites.intervenants.length === 0) {
|
||
return {
|
||
available: false,
|
||
reason: 'L\'intervenant n\'est pas disponible à cette date/heure'
|
||
};
|
||
}
|
||
|
||
// Pour les permanences, on ne bloque pas sur l’absence de traducteur ou de local :
|
||
// l’objectif est de permettre le déplacement du créneau de disponibilité.
|
||
if (eventType !== 'permanence') {
|
||
// Vérifier le traducteur seulement s'il y en a un d'assigné (valide, supérieur à 0) et que la vérification a été faite
|
||
if (hasValidTraducteur && disponibilites.hasOwnProperty('traducteurs') && Array.isArray(disponibilites.traducteurs) && disponibilites.traducteurs.length === 0) {
|
||
return {
|
||
available: false,
|
||
reason: 'Le traducteur n\'est pas disponible à cette date/heure'
|
||
};
|
||
}
|
||
|
||
// Vérifier le local
|
||
if (disponibilites.locaux && disponibilites.locaux.length === 0) {
|
||
return {
|
||
available: false,
|
||
reason: 'Le local n\'est pas disponible à cette date/heure'
|
||
};
|
||
}
|
||
}
|
||
|
||
console.log('✅ Toutes les entités sont disponibles');
|
||
return {
|
||
available: true,
|
||
reason: 'Toutes les entités sont disponibles'
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('Erreur lors de la vérification des disponibilités:', error);
|
||
return {
|
||
available: false,
|
||
reason: 'Erreur lors de la vérification des disponibilités'
|
||
};
|
||
}
|
||
}
|