4426 lines
183 KiB
JavaScript
4426 lines
183 KiB
JavaScript
import { createEvent, updateEvent, deleteEvent, changeEventStatus, getFilters, apiFetch } from './agenda-api.js';
|
||
import { notifyError, notifySuccess } from './agenda-notifications.js';
|
||
import { initializeEntityCreators } from './agenda-entity-creator.js';
|
||
|
||
// Flag pour activer/désactiver les logs de debug détaillés
|
||
const DEBUG_SELECTS = true; // Mettre à false pour désactiver les logs
|
||
|
||
// Flag global pour empêcher les boucles infinies lors du filtrage
|
||
let isUpdatingSelects = false;
|
||
|
||
let currentMode = 'view'; // 'view', 'edit', 'create'
|
||
let currentEventData = null;
|
||
// Variables pour préserver les données lors de l'ouverture de sous-modales
|
||
let preservedMode = null;
|
||
let preservedEventData = null;
|
||
|
||
// Note : Le fix global pour le stacking context des modales est géré dans crvi_main.js
|
||
|
||
/**
|
||
* Vérifie si un événement est passé
|
||
* @param {Object} eventData - Les données de l'événement
|
||
* @returns {boolean} - True si l'événement est passé
|
||
*/
|
||
function checkIfEventIsPast(eventData) {
|
||
if (!eventData) return false;
|
||
|
||
// Récupérer la date de l'événement
|
||
let eventDate = null;
|
||
let eventTime = null;
|
||
|
||
// Essayer différentes sources pour la date
|
||
if (eventData.date_rdv) {
|
||
eventDate = eventData.date_rdv;
|
||
eventTime = eventData.heure_rdv || '00:00';
|
||
} else if (eventData.start) {
|
||
// Format FullCalendar
|
||
const startDate = new Date(eventData.start);
|
||
eventDate = startDate.toISOString().split('T')[0];
|
||
eventTime = startDate.toTimeString().substring(0, 5);
|
||
} else if (eventData.extendedProps?.date_rdv) {
|
||
eventDate = eventData.extendedProps.date_rdv;
|
||
eventTime = eventData.extendedProps.heure_rdv || '00:00';
|
||
}
|
||
|
||
if (!eventDate) return false;
|
||
|
||
// Créer une date complète avec l'heure
|
||
const eventDateTime = new Date(`${eventDate}T${eventTime}`);
|
||
const now = new Date();
|
||
|
||
// Retourner true si l'événement est dans le passé
|
||
return eventDateTime < now;
|
||
}
|
||
|
||
// Module ES6 pour la gestion du modal d'événement
|
||
export function openModal(mode, eventData = null) {
|
||
// Vérifier s'il y a des données préservées à restaurer
|
||
if (restoreModalData()) {
|
||
// Utiliser les données restaurées au lieu des paramètres
|
||
mode = currentMode;
|
||
eventData = currentEventData;
|
||
// console.log('Modal ouvert avec données restaurées:', { mode, eventData });
|
||
} else {
|
||
// Si on a des données d'événement mais pas de mode spécifié, ouvrir en mode view
|
||
if (eventData && !mode) {
|
||
mode = 'view';
|
||
}
|
||
|
||
currentMode = mode;
|
||
currentEventData = eventData;
|
||
}
|
||
|
||
const modal = document.getElementById('eventModal');
|
||
|
||
// Vérifier que l'élément modal existe
|
||
if (!modal) {
|
||
console.error('❌ Élément modal non trouvé');
|
||
return;
|
||
}
|
||
|
||
// Vérifier s'il existe déjà une instance de modal Bootstrap
|
||
let bsModal = window.bootstrap && window.bootstrap.Modal ? window.bootstrap.Modal.getInstance(modal) : null;
|
||
|
||
// Si aucune instance n'existe, en créer une nouvelle
|
||
if (!bsModal && window.bootstrap && window.bootstrap.Modal) {
|
||
bsModal = new window.bootstrap.Modal(modal);
|
||
}
|
||
|
||
// Note : Le fix stacking context est géré globalement au chargement via fixAllModalsStackingContext()
|
||
|
||
// Initialiser les créateurs d'entités
|
||
initializeEntityCreators();
|
||
|
||
// Initialiser Select2 sur les selects du modal
|
||
initializeSelect2();
|
||
|
||
// Initialiser la fonction de confort pour les sélecteurs d'heure
|
||
initializeTimeComfort();
|
||
|
||
// Initialiser le filtre de traducteurs par langue
|
||
initializeLangueFilter();
|
||
|
||
// Initialiser les boutons du modal (une seule fois)
|
||
initializeModalButtons();
|
||
|
||
// Déterminer les permissions utilisateur
|
||
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||
const userCanCreate = window.crviPermissions && window.crviPermissions.can_create;
|
||
const userCanDelete = window.crviPermissions && window.crviPermissions.can_delete;
|
||
|
||
// Vérifier si l'événement est passé
|
||
const isEventPast = checkIfEventIsPast(eventData);
|
||
|
||
// Ajuster le mode selon les permissions
|
||
if (mode === 'edit' && !userCanEdit) {
|
||
mode = 'view';
|
||
currentMode = 'view';
|
||
}
|
||
// Empêcher l'édition d'événements passés
|
||
if (mode === 'edit' && isEventPast) {
|
||
console.warn('Impossible de modifier un événement passé');
|
||
notifyError('Impossible de modifier un événement passé');
|
||
mode = 'view';
|
||
currentMode = 'view';
|
||
}
|
||
if (mode === 'create' && !userCanCreate) {
|
||
console.warn('Utilisateur non autorisé à créer des événements');
|
||
return;
|
||
}
|
||
|
||
// Affichage des blocs
|
||
updateModalDisplay(userCanEdit, userCanDelete);
|
||
|
||
// Variable pour éviter les appels multiples à populateSelects
|
||
let shouldPopulateSelects = false;
|
||
|
||
if (mode === 'view') {
|
||
fillViewBlock(eventData);
|
||
clearFormErrors();
|
||
} else if (mode === 'edit') {
|
||
fillFormWithEvent(eventData);
|
||
shouldPopulateSelects = true;
|
||
clearFormErrors();
|
||
} else if (mode === 'create') {
|
||
// Pour la création, utiliser fillFormWithDate si les données ont startStr/endStr
|
||
if (eventData && eventData.startStr) {
|
||
fillFormWithDate(eventData);
|
||
} else {
|
||
fillFormWithEvent(eventData);
|
||
}
|
||
shouldPopulateSelects = true;
|
||
clearFormErrors();
|
||
|
||
// Activer la synchronisation des dates pour la création
|
||
enableDateSynchronization();
|
||
|
||
// Empêcher la sélection de dates passées en mode création
|
||
preventPastDates();
|
||
}
|
||
|
||
// Ajouter un event listener pour nettoyer les champs lors de la fermeture
|
||
modal.addEventListener('hidden.bs.modal', function() {
|
||
// console.log('Modal fermé, nettoyage des champs...');
|
||
|
||
// Ne pas nettoyer les champs si des données sont préservées
|
||
if (preservedMode === null && preservedEventData === null) {
|
||
clearModalFields();
|
||
// Réinitialiser les variables globales
|
||
currentMode = 'view';
|
||
currentEventData = null;
|
||
} else {
|
||
// console.log('Données préservées détectées, pas de nettoyage des champs');
|
||
}
|
||
|
||
// Désactiver la synchronisation des dates
|
||
disableDateSynchronization();
|
||
});
|
||
|
||
// Ajouter des event listeners pour le filtrage automatique avec debounce
|
||
const dateField = document.getElementById('date_rdv');
|
||
const heureField = document.getElementById('heure_rdv');
|
||
|
||
// Les event listeners pour date/heure sont gérés globalement plus bas dans le code
|
||
// (voir ligne ~3613 avec addEventListener sur date_rdv, heure_rdv, type)
|
||
|
||
bsModal.show();
|
||
|
||
// Les gestionnaires d'événements pour les boutons de fermeture sont maintenant gérés dans initializeModalButtons()
|
||
|
||
// Si les données ont été restaurées, s'assurer que l'affichage est correct
|
||
if (currentEventData && (currentMode === 'edit' || currentMode === 'view' || currentMode === 'create')) {
|
||
// console.log('Données restaurées détectées, mise à jour de l\'affichage...');
|
||
// Forcer la mise à jour de l'affichage selon le mode
|
||
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||
const userCanDelete = window.crviPermissions && window.crviPermissions.can_delete;
|
||
updateModalDisplay(userCanEdit, userCanDelete);
|
||
|
||
if (currentMode === 'view') {
|
||
fillViewBlock(currentEventData);
|
||
} else if (currentMode === 'edit') {
|
||
fillFormWithEvent(currentEventData);
|
||
shouldPopulateSelects = true;
|
||
} else if (currentMode === 'create') {
|
||
fillFormWithEvent(currentEventData);
|
||
shouldPopulateSelects = true;
|
||
}
|
||
}
|
||
|
||
// Appeler populateSelects une seule fois à la fin si nécessaire
|
||
if (shouldPopulateSelects) {
|
||
// Attendre un peu que les champs soient remplis avant de peupler les selects
|
||
setTimeout(() => {
|
||
populateSelects(currentEventData);
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
function updateModalDisplay(userCanEdit, userCanDelete) {
|
||
const viewBlock = document.getElementById('eventViewBlock');
|
||
const formBlock = document.getElementById('eventForm');
|
||
const viewFooter = document.getElementById('eventViewFooter');
|
||
const editFooter = document.getElementById('eventEditFooter');
|
||
const statusButtons = document.getElementById('eventStatusButtons');
|
||
const editBtn = document.getElementById('editEventBtn');
|
||
const cancelBtn = document.getElementById('cancelEditBtn');
|
||
const saveBtn = document.getElementById('saveEvent');
|
||
const deleteBtn = document.getElementById('deleteEvent');
|
||
|
||
const userCanView = window.crviPermissions && window.crviPermissions.can_view;
|
||
|
||
// Vérifier si l'événement est de type individuel (le bouton incident n'est affiché que pour les événements individuels)
|
||
const eventType = currentEventData?.type || currentEventData?.extendedProps?.type || '';
|
||
const isIndividuel = eventType !== 'groupe';
|
||
const isGroupe = eventType === 'groupe';
|
||
|
||
// Vérifier si l'événement est clôturé
|
||
const clotureFlag = currentEventData?.cloture_flag || currentEventData?.extendedProps?.cloture_flag;
|
||
const isEventCloture = clotureFlag === 1 || clotureFlag === '1' || clotureFlag === true;
|
||
|
||
// Adapter le bouton "Valider présence" selon le type d'événement
|
||
const markPresentBtn = document.getElementById('markPresentBtn');
|
||
if (markPresentBtn) {
|
||
const icon = markPresentBtn.querySelector('i');
|
||
if (isGroupe) {
|
||
// Pour les événements de groupe : "Valider les présences" avec icône fa-users
|
||
if (icon) {
|
||
icon.className = 'fas fa-users me-1';
|
||
}
|
||
markPresentBtn.innerHTML = '<i class="fas fa-users me-1"></i>Valider les présences';
|
||
markPresentBtn.title = 'Valider les présences';
|
||
markPresentBtn.setAttribute('data-event-type', 'groupe');
|
||
} else {
|
||
// Pour les événements individuels : "Valider présence" avec icône fa-user-check
|
||
if (icon) {
|
||
icon.className = 'fas fa-user-check me-1';
|
||
}
|
||
markPresentBtn.innerHTML = '<i class="fas fa-user-check me-1"></i>Valider présence';
|
||
markPresentBtn.title = 'Valider la présence';
|
||
markPresentBtn.setAttribute('data-event-type', 'individuel');
|
||
}
|
||
}
|
||
|
||
if (currentMode === 'view') {
|
||
// Mode lecture seule
|
||
viewBlock.style.display = 'block';
|
||
formBlock.style.display = 'none';
|
||
viewFooter.style.display = 'block';
|
||
editFooter.style.display = 'none';
|
||
|
||
// Masquer tous les boutons de statut en mode vue (y compris "Signaler un incident")
|
||
if (statusButtons) {
|
||
statusButtons.style.display = 'none';
|
||
}
|
||
|
||
// Vérifier si l'événement est passé
|
||
const isEventPast = checkIfEventIsPast(currentEventData);
|
||
|
||
// Afficher/masquer le bouton Modifier selon les permissions, si l'événement est passé, et si l'événement n'est pas clôturé
|
||
if (editBtn) {
|
||
if (userCanEdit && !isEventPast && !isEventCloture) {
|
||
editBtn.style.display = 'inline-block';
|
||
} else {
|
||
editBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Réafficher le bouton de validation de présence par défaut (sera caché si déjà marqué ou si événement clôturé)
|
||
if (markPresentBtn) {
|
||
if (!isEventCloture) {
|
||
markPresentBtn.style.display = 'inline-block';
|
||
} else {
|
||
markPresentBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Désactiver la synchronisation des dates en mode vue
|
||
disableDateSynchronization();
|
||
} else {
|
||
// Mode édition/création
|
||
viewBlock.style.display = 'none';
|
||
formBlock.style.display = 'block';
|
||
viewFooter.style.display = 'none';
|
||
editFooter.style.display = 'block';
|
||
|
||
// Afficher/masquer le bouton Supprimer selon le mode et les permissions
|
||
deleteBtn.style.display = (currentMode === 'edit' && userCanDelete) ? 'inline-block' : 'none';
|
||
|
||
// Afficher les boutons de statut uniquement en mode édition et si l'événement n'est pas clôturé
|
||
if (currentMode === 'edit' && userCanEdit && !isEventCloture) {
|
||
statusButtons.style.display = 'block';
|
||
// Afficher/masquer les boutons selon le type d'événement
|
||
const allButtons = statusButtons.querySelectorAll('button');
|
||
allButtons.forEach(btn => {
|
||
// Masquer "Absent" et "Historique bénéficiaire" pour les événements de groupe
|
||
if (btn.id === 'markAbsentBtn' || btn.id === 'showBeneficiaireHistoriqueBtn') {
|
||
btn.style.display = isGroupe ? 'none' : 'inline-block';
|
||
}
|
||
// Le bouton "Détail incident(s)" garde son état (géré par checkAndDisplayIncidentsButton)
|
||
else if (btn.id === 'viewIncidentsBtn') {
|
||
// Ne rien faire, l'état est géré ailleurs
|
||
}
|
||
// Afficher tous les autres boutons (y compris "Signaler un incident")
|
||
else {
|
||
btn.style.display = 'inline-block';
|
||
}
|
||
});
|
||
} else {
|
||
// Masquer tous les boutons de statut si pas en mode édition, si l'utilisateur ne peut pas éditer, ou si l'événement est clôturé
|
||
statusButtons.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
function fillViewBlock(event) {
|
||
// console.log('fillViewBlock - données reçues:', event);
|
||
|
||
// Extraire la date et l'heure - vérifier plusieurs sources possibles
|
||
let dateFormatted = '';
|
||
let heureFormatted = '';
|
||
|
||
// Priorité 1: Données directes de l'API (date et heure)
|
||
if (event?.date && event?.heure) {
|
||
const dateObj = new Date(event.date + 'T' + event.heure);
|
||
dateFormatted = dateObj.toLocaleDateString('fr-FR');
|
||
heureFormatted = dateObj.toLocaleTimeString('fr-FR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
// Priorité 2: Données dans extendedProps
|
||
else if (event?.extendedProps?.date && event?.extendedProps?.heure) {
|
||
const dateObj = new Date(event.extendedProps.date + 'T' + event.extendedProps.heure);
|
||
dateFormatted = dateObj.toLocaleDateString('fr-FR');
|
||
heureFormatted = dateObj.toLocaleTimeString('fr-FR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
// Priorité 3: Données FullCalendar (start)
|
||
else if (event?.start) {
|
||
const startDate = new Date(event.start);
|
||
dateFormatted = startDate.toLocaleDateString('fr-FR');
|
||
heureFormatted = startDate.toLocaleTimeString('fr-FR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
const type = event?.type || event?.extendedProps?.type || '';
|
||
// Utiliser langue_label si disponible (nom du terme), sinon chercher le nom dans le select
|
||
let langue = event?.langue_label || event?.extendedProps?.langue_label || '';
|
||
|
||
// Si langue_label est un ID (numérique) ou vide, chercher le nom dans les options du select
|
||
if (!langue || /^\d+$/.test(langue)) {
|
||
const langueId = langue || event?.langue || event?.extendedProps?.langue || '';
|
||
if (langueId) {
|
||
const langueSelect = document.getElementById('langue');
|
||
if (langueSelect) {
|
||
const option = langueSelect.querySelector(`option[value="${langueId}"]`);
|
||
if (option) {
|
||
langue = option.textContent;
|
||
} else if (langueId && !langue) {
|
||
// Si on n'a pas trouvé l'option mais qu'on a un ID, utiliser l'ID comme fallback
|
||
langue = langueId;
|
||
}
|
||
} else if (langueId && !langue) {
|
||
// Si le select n'existe pas encore, utiliser l'ID comme fallback
|
||
langue = langueId;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Si toujours vide, utiliser langue (ID) comme dernier recours
|
||
if (!langue) {
|
||
langue = event?.langue || event?.extendedProps?.langue || '';
|
||
}
|
||
|
||
document.getElementById('view_date_rdv').textContent = dateFormatted;
|
||
document.getElementById('view_heure_rdv').textContent = heureFormatted;
|
||
document.getElementById('view_type').textContent = type;
|
||
document.getElementById('view_langue').textContent = langue;
|
||
|
||
// Bénéficiaire - chercher dans les données directes ou les relations
|
||
let beneficiaireNom = '';
|
||
if (event?.beneficiaire?.nom) {
|
||
beneficiaireNom = event.beneficiaire.nom + ' ' + (event.beneficiaire.prenom || '');
|
||
} else if (event?.extendedProps?.beneficiaire?.nom) {
|
||
beneficiaireNom = event.extendedProps.beneficiaire.nom + ' ' + (event.extendedProps.beneficiaire.prenom || '');
|
||
} else if (event?.id_beneficiaire || event?.extendedProps?.id_beneficiaire) {
|
||
const beneficiaireId = event?.id_beneficiaire || event?.extendedProps?.id_beneficiaire;
|
||
// Si on a juste l'ID, essayer de trouver le nom dans les options du select
|
||
const beneficiaireSelect = document.getElementById('id_beneficiaire');
|
||
if (beneficiaireSelect) {
|
||
const option = beneficiaireSelect.querySelector(`option[value="${beneficiaireId}"]`);
|
||
if (option) {
|
||
beneficiaireNom = option.textContent;
|
||
}
|
||
}
|
||
// Si pas trouvé dans le select, afficher l'ID temporairement
|
||
if (!beneficiaireNom) {
|
||
beneficiaireNom = `ID: ${beneficiaireId}`;
|
||
}
|
||
}
|
||
document.getElementById('view_beneficiaire').textContent = beneficiaireNom;
|
||
|
||
// Intervenant
|
||
let intervenantNom = '';
|
||
if (event?.intervenant?.nom) {
|
||
intervenantNom = event.intervenant.nom + ' ' + (event.intervenant.prenom || '');
|
||
} else if (event?.extendedProps?.intervenant?.nom) {
|
||
intervenantNom = event.extendedProps.intervenant.nom + ' ' + (event.extendedProps.intervenant.prenom || '');
|
||
} else if (event?.id_intervenant || event?.extendedProps?.id_intervenant) {
|
||
const intervenantId = event?.id_intervenant || event?.extendedProps?.id_intervenant;
|
||
const intervenantSelect = document.getElementById('id_intervenant');
|
||
if (intervenantSelect && intervenantSelect.tagName === 'SELECT') {
|
||
const option = intervenantSelect.querySelector(`option[value="${intervenantId}"]`);
|
||
if (option) {
|
||
intervenantNom = option.textContent;
|
||
}
|
||
} else {
|
||
// Front: input hidden + affichage texte à côté
|
||
const displayEl = document.getElementById('id_intervenant_display');
|
||
if (displayEl && displayEl.textContent) {
|
||
intervenantNom = displayEl.textContent;
|
||
}
|
||
}
|
||
// Si pas trouvé dans le select, afficher l'ID temporairement
|
||
if (!intervenantNom) {
|
||
intervenantNom = `ID: ${intervenantId}`;
|
||
}
|
||
}
|
||
document.getElementById('view_intervenant').textContent = intervenantNom;
|
||
|
||
// Département - utiliser departement_label si disponible, sinon chercher dans le select
|
||
let departementNom = '';
|
||
// Priorité 1: departement_label (nom du terme)
|
||
departementNom = event?.departement_label || event?.extendedProps?.departement_label || '';
|
||
|
||
// Priorité 2: relation departement
|
||
if (!departementNom) {
|
||
if (event?.departement?.nom) {
|
||
departementNom = event.departement.nom;
|
||
} else if (event?.extendedProps?.departement?.nom) {
|
||
departementNom = event.extendedProps.departement.nom;
|
||
}
|
||
}
|
||
|
||
// Priorité 3: chercher dans le select si on a un ID
|
||
if (!departementNom) {
|
||
const departementId = event?.id_departement || event?.extendedProps?.id_departement;
|
||
if (departementId && departementId !== '0' && departementId !== 0) {
|
||
const departementSelect = document.getElementById('id_departement');
|
||
if (departementSelect) {
|
||
const option = departementSelect.querySelector(`option[value="${departementId}"]`);
|
||
if (option) {
|
||
departementNom = option.textContent;
|
||
}
|
||
}
|
||
// Si pas trouvé dans le select, afficher l'ID temporairement
|
||
if (!departementNom) {
|
||
departementNom = `ID: ${departementId}`;
|
||
}
|
||
}
|
||
}
|
||
document.getElementById('view_departement').textContent = departementNom;
|
||
|
||
// Type d'intervention - utiliser type_intervention_label si disponible, sinon chercher dans le select
|
||
let typeInterventionNom = '';
|
||
// Priorité 1: type_intervention_label (nom du terme)
|
||
typeInterventionNom = event?.type_intervention_label || event?.extendedProps?.type_intervention_label || '';
|
||
|
||
// Priorité 2: relation type_intervention
|
||
if (!typeInterventionNom) {
|
||
if (event?.type_intervention?.nom) {
|
||
typeInterventionNom = event.type_intervention.nom;
|
||
} else if (event?.extendedProps?.type_intervention?.nom) {
|
||
typeInterventionNom = event.extendedProps.type_intervention.nom;
|
||
}
|
||
}
|
||
|
||
// Priorité 3: chercher dans le select si on a un ID
|
||
if (!typeInterventionNom) {
|
||
const typeInterventionId = event?.id_type_intervention || event?.extendedProps?.id_type_intervention;
|
||
if (typeInterventionId && typeInterventionId !== '0' && typeInterventionId !== 0) {
|
||
const typeInterventionSelect = document.getElementById('id_type_intervention');
|
||
if (typeInterventionSelect) {
|
||
const option = typeInterventionSelect.querySelector(`option[value="${typeInterventionId}"]`);
|
||
if (option) {
|
||
typeInterventionNom = option.textContent;
|
||
}
|
||
}
|
||
// Si pas trouvé dans le select, afficher l'ID temporairement
|
||
if (!typeInterventionNom) {
|
||
typeInterventionNom = `ID: ${typeInterventionId}`;
|
||
}
|
||
}
|
||
}
|
||
document.getElementById('view_type_intervention').textContent = typeInterventionNom;
|
||
|
||
// Traducteur
|
||
let traducteurNom = '';
|
||
const traducteurId = event?.id_traducteur || event?.extendedProps?.id_traducteur;
|
||
const hasValidTraducteurId = traducteurId && parseInt(traducteurId, 10) > 0;
|
||
|
||
if (hasValidTraducteurId) {
|
||
// Si on a un ID de traducteur valide, chercher dans les relations ou le select
|
||
if (event?.traducteur?.nom) {
|
||
traducteurNom = event.traducteur.nom + ' ' + (event.traducteur.prenom || '');
|
||
} else if (event?.extendedProps?.traducteur?.nom) {
|
||
traducteurNom = event.extendedProps.traducteur.nom + ' ' + (event.extendedProps.traducteur.prenom || '');
|
||
} else {
|
||
const traducteurSelect = document.getElementById('id_traducteur');
|
||
if (traducteurSelect) {
|
||
const option = traducteurSelect.querySelector(`option[value="${traducteurId}"]`);
|
||
if (option) {
|
||
traducteurNom = option.textContent;
|
||
}
|
||
}
|
||
// Si pas trouvé dans le select, afficher l'ID temporairement
|
||
if (!traducteurNom) {
|
||
traducteurNom = `ID: ${traducteurId}`;
|
||
}
|
||
}
|
||
} else {
|
||
// Si id_traducteur est 0, null ou invalide, utiliser nom_traducteur depuis les données
|
||
// Priorité 1: nom_traducteur dans extendedProps (pour FullCalendar)
|
||
traducteurNom = event?.nom_traducteur || event?.extendedProps?.nom_traducteur || '';
|
||
}
|
||
document.getElementById('view_traducteur').textContent = traducteurNom;
|
||
|
||
// Local
|
||
let localNom = '';
|
||
if (event?.local?.nom) {
|
||
localNom = event.local.nom;
|
||
} else if (event?.extendedProps?.local?.nom) {
|
||
localNom = event.extendedProps.local.nom;
|
||
} else if (event?.id_local || event?.extendedProps?.id_local) {
|
||
const localId = event?.id_local || event?.extendedProps?.id_local;
|
||
const localSelect = document.getElementById('id_local');
|
||
if (localSelect) {
|
||
const option = localSelect.querySelector(`option[value="${localId}"]`);
|
||
if (option) {
|
||
localNom = option.textContent;
|
||
}
|
||
}
|
||
// Si pas trouvé dans le select, afficher l'ID temporairement
|
||
if (!localNom) {
|
||
localNom = `ID: ${localId}`;
|
||
}
|
||
}
|
||
document.getElementById('view_local').textContent = localNom;
|
||
|
||
// Données de groupe
|
||
const nbParticipants = event?.nb_participants || event?.extendedProps?.nb_participants || '';
|
||
const nbHommes = event?.nb_hommes || event?.extendedProps?.nb_hommes || '';
|
||
const nbFemmes = event?.nb_femmes || event?.extendedProps?.nb_femmes || '';
|
||
|
||
// Afficher les champs de groupe seulement si c'est un RDV de groupe
|
||
const eventType = event?.type || event?.extendedProps?.type || '';
|
||
const groupeFields = document.querySelectorAll('.groupe-only-field');
|
||
|
||
if (eventType === 'groupe') {
|
||
groupeFields.forEach(field => {
|
||
field.style.display = '';
|
||
});
|
||
document.getElementById('view_nb_participants').textContent = nbParticipants;
|
||
document.getElementById('view_nb_hommes').textContent = nbHommes;
|
||
document.getElementById('view_nb_femmes').textContent = nbFemmes;
|
||
} else {
|
||
groupeFields.forEach(field => {
|
||
field.style.display = 'none';
|
||
});
|
||
document.getElementById('view_nb_participants').textContent = '';
|
||
document.getElementById('view_nb_hommes').textContent = '';
|
||
document.getElementById('view_nb_femmes').textContent = '';
|
||
}
|
||
|
||
// Commentaire
|
||
const commentaire = event?.commentaire || event?.extendedProps?.commentaire || '';
|
||
document.getElementById('view_commentaire').textContent = commentaire;
|
||
|
||
/* console.log('fillViewBlock - données affichées-555:', {
|
||
date, heure, type, langue,
|
||
beneficiaire: beneficiaireNom,
|
||
intervenant: intervenantNom,
|
||
traducteur: traducteurNom,
|
||
local: localNom,
|
||
nbParticipants, nbHommes, nbFemmes, commentaire
|
||
}); */
|
||
|
||
// Vérifier et afficher les alertes d'absence
|
||
checkAndDisplayAbsenceAlert(event);
|
||
|
||
// Récupérer et afficher le statut de présence
|
||
checkAndDisplayPresenceStatus(event);
|
||
|
||
// Afficher les informations de clôture si l'événement est clôturé
|
||
displayClotureInfo(event);
|
||
|
||
// Gérer l'affichage conditionnel du bouton "Historique bénéficiaire"
|
||
const historiqueBtn = document.getElementById('showBeneficiaireHistoriqueBtn');
|
||
if (historiqueBtn) {
|
||
const eventType = event?.type || event?.extendedProps?.type || '';
|
||
const beneficiaireId = event?.id_beneficiaire || event?.extendedProps?.id_beneficiaire;
|
||
|
||
// Afficher le bouton uniquement si l'événement est individuel et a un bénéficiaire
|
||
if (eventType === 'individuel' && beneficiaireId) {
|
||
historiqueBtn.style.display = 'block';
|
||
historiqueBtn.setAttribute('data-benef', beneficiaireId);
|
||
} else {
|
||
historiqueBtn.style.display = 'none';
|
||
historiqueBtn.removeAttribute('data-benef');
|
||
}
|
||
}
|
||
|
||
// Vérifier et afficher le bouton "Détail incident(s)" si l'événement a des incidents
|
||
const eventId = event?.id || event?.extendedProps?.id;
|
||
if (eventId) {
|
||
checkAndDisplayIncidentsButton(eventId);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Affiche les informations de clôture dans le bloc de vue
|
||
* @param {Object} event - Les données de l'événement
|
||
*/
|
||
function displayClotureInfo(event) {
|
||
const clotureSection = document.getElementById('cloture_info_section');
|
||
const clotureFlag = event?.cloture_flag || event?.extendedProps?.cloture_flag;
|
||
const statutEvent = event?.statut || event?.extendedProps?.statut;
|
||
|
||
if (!clotureSection) {
|
||
return;
|
||
}
|
||
|
||
// Afficher la section uniquement si l'événement est clôturé
|
||
if (clotureFlag === 1 || clotureFlag === '1' || clotureFlag === true) {
|
||
clotureSection.style.display = '';
|
||
|
||
// Afficher le statut (présent/absent)
|
||
const viewStatutCloture = document.getElementById('view_statut_cloture');
|
||
if (viewStatutCloture) {
|
||
let statutLabel = '';
|
||
let statutClass = '';
|
||
|
||
if (statutEvent === 'present') {
|
||
statutLabel = 'Présent';
|
||
statutClass = 'badge bg-success';
|
||
} else if (statutEvent === 'absence') {
|
||
statutLabel = 'Absent';
|
||
statutClass = 'badge bg-danger';
|
||
} else {
|
||
statutLabel = statutEvent || 'Non défini';
|
||
statutClass = 'badge bg-secondary';
|
||
}
|
||
|
||
viewStatutCloture.innerHTML = `<span class="${statutClass}">${statutLabel}</span>`;
|
||
}
|
||
|
||
// Afficher la date de clôture
|
||
const viewClotureRdv = document.getElementById('view_cloture_rdv');
|
||
if (viewClotureRdv) {
|
||
const clotureRdv = event?.cloture_rdv || event?.extendedProps?.cloture_rdv;
|
||
if (clotureRdv) {
|
||
const clotureDate = new Date(clotureRdv);
|
||
const dateFormatted = clotureDate.toLocaleDateString('fr-FR');
|
||
const timeFormatted = clotureDate.toLocaleTimeString('fr-FR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
viewClotureRdv.textContent = `${dateFormatted} à ${timeFormatted}`;
|
||
} else {
|
||
viewClotureRdv.textContent = 'Non renseignée';
|
||
}
|
||
}
|
||
|
||
// Afficher qui a clôturé (à charger via l'API si nécessaire)
|
||
const viewCloturePar = document.getElementById('view_cloture_par');
|
||
if (viewCloturePar) {
|
||
const cloturePar = event?.cloture_par || event?.extendedProps?.cloture_par;
|
||
if (cloturePar) {
|
||
// Charger le nom de l'utilisateur via l'API WordPress
|
||
loadUserName(cloturePar).then(userName => {
|
||
viewCloturePar.textContent = userName || `ID: ${cloturePar}`;
|
||
});
|
||
} else {
|
||
viewCloturePar.textContent = 'Non renseigné';
|
||
}
|
||
}
|
||
} else {
|
||
clotureSection.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Charge le nom d'un utilisateur WordPress par son ID
|
||
* @param {number} userId - L'ID de l'utilisateur
|
||
* @returns {Promise<string>} Le nom complet de l'utilisateur
|
||
*/
|
||
async function loadUserName(userId) {
|
||
try {
|
||
const response = await fetch(`${window.crviAgendaData.apiUrl}wp/v2/users/${userId}`, {
|
||
headers: {
|
||
'X-WP-Nonce': window.crviAgendaData.nonce
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const user = await response.json();
|
||
return user.name || `Utilisateur ${userId}`;
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement du nom d\'utilisateur:', error);
|
||
}
|
||
|
||
return `Utilisateur ${userId}`;
|
||
}
|
||
|
||
/**
|
||
* Affiche les informations de clôture dans le formulaire d'édition
|
||
* @param {Object} event - Les données de l'événement
|
||
*/
|
||
function displayClotureInfoEdit(event) {
|
||
const clotureSection = document.getElementById('cloture_info_section_edit');
|
||
const clotureFlag = event?.cloture_flag || event?.extendedProps?.cloture_flag;
|
||
const statutEvent = event?.statut || event?.extendedProps?.statut;
|
||
|
||
if (!clotureSection) {
|
||
return;
|
||
}
|
||
|
||
// Afficher la section uniquement si l'événement est clôturé
|
||
if (clotureFlag === 1 || clotureFlag === '1' || clotureFlag === true) {
|
||
clotureSection.style.display = '';
|
||
|
||
// Afficher le statut (présent/absent)
|
||
const editStatutCloture = document.getElementById('edit_statut_cloture');
|
||
if (editStatutCloture) {
|
||
let statutLabel = '';
|
||
let statutClass = '';
|
||
|
||
if (statutEvent === 'present') {
|
||
statutLabel = 'Présent';
|
||
statutClass = 'badge bg-success';
|
||
} else if (statutEvent === 'absence') {
|
||
statutLabel = 'Absent';
|
||
statutClass = 'badge bg-danger';
|
||
} else {
|
||
statutLabel = statutEvent || 'Non défini';
|
||
statutClass = 'badge bg-secondary';
|
||
}
|
||
|
||
editStatutCloture.innerHTML = `<span class="${statutClass}">${statutLabel}</span>`;
|
||
}
|
||
|
||
// Afficher la date de clôture
|
||
const editClotureRdv = document.getElementById('edit_cloture_rdv');
|
||
if (editClotureRdv) {
|
||
const clotureRdv = event?.cloture_rdv || event?.extendedProps?.cloture_rdv;
|
||
if (clotureRdv) {
|
||
const clotureDate = new Date(clotureRdv);
|
||
const dateFormatted = clotureDate.toLocaleDateString('fr-FR');
|
||
const timeFormatted = clotureDate.toLocaleTimeString('fr-FR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
editClotureRdv.textContent = `${dateFormatted} à ${timeFormatted}`;
|
||
} else {
|
||
editClotureRdv.textContent = 'Non renseignée';
|
||
}
|
||
}
|
||
|
||
// Afficher qui a clôturé
|
||
const editCloturePar = document.getElementById('edit_cloture_par');
|
||
if (editCloturePar) {
|
||
const cloturePar = event?.cloture_par || event?.extendedProps?.cloture_par;
|
||
if (cloturePar) {
|
||
// Charger le nom de l'utilisateur via l'API WordPress
|
||
loadUserName(cloturePar).then(userName => {
|
||
editCloturePar.textContent = userName || `ID: ${cloturePar}`;
|
||
});
|
||
} else {
|
||
editCloturePar.textContent = 'Non renseigné';
|
||
}
|
||
}
|
||
} else {
|
||
clotureSection.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vérifie si le bénéficiaire a des absences et affiche les alertes appropriées
|
||
* @param {Object} event - Les données de l'événement
|
||
*/
|
||
async function checkAndDisplayAbsenceAlert(event) {
|
||
// Extraire l'ID du bénéficiaire
|
||
const beneficiaireId = event?.id_beneficiaire || event?.extendedProps?.id_beneficiaire;
|
||
|
||
if (!beneficiaireId) {
|
||
// console.log('checkAndDisplayAbsenceAlert: Pas d\'ID bénéficiaire, masquer les alertes');
|
||
hideAbsenceAlerts();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Appel API pour vérifier les absences du bénéficiaire
|
||
const response = await apiFetch(`beneficiaires/${beneficiaireId}/absences`);
|
||
|
||
if (response.success && response.data) {
|
||
const absences = response.data;
|
||
|
||
// Vérifier s'il y a des absences récentes (par exemple, dans les 30 derniers jours)
|
||
const hasRecentAbsences = checkRecentAbsences(absences);
|
||
|
||
if (hasRecentAbsences) {
|
||
// console.log('checkAndDisplayAbsenceAlert: Absences détectées, afficher les alertes');
|
||
showAbsenceAlerts();
|
||
} else {
|
||
// console.log('checkAndDisplayAbsenceAlert: Pas d\'absences récentes, masquer les alertes');
|
||
hideAbsenceAlerts();
|
||
}
|
||
} else {
|
||
// console.log('checkAndDisplayAbsenceAlert: Pas de données d\'absence, masquer les alertes');
|
||
hideAbsenceAlerts();
|
||
}
|
||
} catch (error) {
|
||
console.error('checkAndDisplayAbsenceAlert: Erreur lors de la vérification des absences:', error);
|
||
// En cas d'erreur, masquer les alertes par défaut
|
||
hideAbsenceAlerts();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vérifie s'il y a des absences récentes
|
||
* @param {Array} absences - Liste des absences
|
||
* @returns {boolean} - True si il y a des absences récentes
|
||
*/
|
||
function checkRecentAbsences(absences) {
|
||
if (!absences || !Array.isArray(absences)) {
|
||
return false;
|
||
}
|
||
|
||
const thirtyDaysAgo = new Date();
|
||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||
|
||
return absences.some(absence => {
|
||
const absenceDate = new Date(absence.date);
|
||
return absenceDate >= thirtyDaysAgo && absence.statut === 'absent';
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Affiche les alertes d'absence dans les deux modes (vue et édition)
|
||
*/
|
||
function showAbsenceAlerts() {
|
||
const viewAlert = document.getElementById('absence_alert');
|
||
const editAlert = document.getElementById('absence_alert_edit');
|
||
|
||
if (viewAlert) {
|
||
viewAlert.style.display = 'block';
|
||
}
|
||
|
||
if (editAlert) {
|
||
editAlert.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Masque les alertes d'absence dans les deux modes (vue et édition)
|
||
*/
|
||
function hideAbsenceAlerts() {
|
||
const viewAlert = document.getElementById('absence_alert');
|
||
const editAlert = document.getElementById('absence_alert_edit');
|
||
|
||
if (viewAlert) {
|
||
viewAlert.style.display = 'none';
|
||
}
|
||
|
||
if (editAlert) {
|
||
editAlert.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vérifie et affiche le statut de présence pour un événement
|
||
* @param {Object} event - Les données de l'événement
|
||
*/
|
||
async function checkAndDisplayPresenceStatus(event) {
|
||
const eventId = event?.id || event?.extendedProps?.id;
|
||
const eventType = event?.type || event?.extendedProps?.type || '';
|
||
const statut = event?.statut || event?.extendedProps?.statut || '';
|
||
const assign = event?.assign || event?.extendedProps?.assign || 0;
|
||
|
||
if (!eventId) {
|
||
hidePresenceBadge();
|
||
return;
|
||
}
|
||
|
||
// Pour les événements individuels, afficher uniquement le statut
|
||
if (eventType === 'individuel' || eventType === 'individuelle') {
|
||
if (statut === 'cloture') {
|
||
showPresenceBadge(null, null, 'individuel', statut);
|
||
hidePresenceButtons();
|
||
} else {
|
||
hidePresenceBadge();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Pour les permanences non attribuées, ne rien afficher
|
||
if (eventType === 'permanence' && assign === 0) {
|
||
hidePresenceBadge();
|
||
return;
|
||
}
|
||
|
||
// Appel API uniquement pour les rendez-vous de groupe
|
||
if (eventType === 'groupe') {
|
||
try {
|
||
const response = await apiFetch(`events/${eventId}/presences`);
|
||
|
||
if (response.success && response.data) {
|
||
const data = response.data;
|
||
const presences = data.presences || [];
|
||
const hasPresenceData = data.has_presence_data || false;
|
||
|
||
if (hasPresenceData && presences.length > 0) {
|
||
// Compter les présents et absents
|
||
const presentCount = presences.filter(p => p.is_present).length;
|
||
const absentCount = presences.filter(p => !p.is_present).length;
|
||
|
||
if (presentCount > 0 || absentCount > 0) {
|
||
showPresenceBadge(presentCount, absentCount, 'groupe');
|
||
// Cacher les boutons de validation de présence car elle est déjà enregistrée
|
||
hidePresenceButtons();
|
||
} else {
|
||
hidePresenceBadge();
|
||
}
|
||
} else {
|
||
hidePresenceBadge();
|
||
}
|
||
} else {
|
||
hidePresenceBadge();
|
||
}
|
||
} catch (error) {
|
||
console.error('checkAndDisplayPresenceStatus: Erreur lors de la récupération des présences:', error);
|
||
hidePresenceBadge();
|
||
}
|
||
} else {
|
||
hidePresenceBadge();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Affiche le badge de présence
|
||
* @param {number|null} presentCount - Nombre de présents (pour groupe)
|
||
* @param {number|null} absentCount - Nombre d'absents (pour groupe)
|
||
* @param {string} type - Type d'événement ('groupe' ou 'individuel')
|
||
* @param {string} statut - Statut de l'événement (pour individuel)
|
||
*/
|
||
function showPresenceBadge(presentCount, absentCount, type, statut) {
|
||
const badge = document.getElementById('presence_badge');
|
||
if (!badge) return;
|
||
|
||
if (type === 'groupe') {
|
||
// Pour les événements de groupe, afficher le décompte
|
||
let badgeHTML = '';
|
||
if (presentCount > 0) {
|
||
badgeHTML += `<span class="badge bg-success" title="Présents"><i class="fas fa-check me-1"></i>${presentCount}</span> `;
|
||
}
|
||
if (absentCount > 0) {
|
||
badgeHTML += `<span class="badge bg-danger" title="Absents"><i class="fas fa-times me-1"></i>${absentCount}</span>`;
|
||
}
|
||
badge.innerHTML = badgeHTML;
|
||
badge.style.display = 'inline';
|
||
} else if (type === 'individuel') {
|
||
// Pour les événements individuels, afficher un macaron selon le statut
|
||
if (statut === 'cloture') {
|
||
badge.innerHTML = '<span class="badge bg-success" title="Présent"><i class="fas fa-check me-1"></i>Présent</span>';
|
||
} else if (statut === 'absence') {
|
||
badge.innerHTML = '<span class="badge bg-danger" title="Absent"><i class="fas fa-times me-1"></i>Absent</span>';
|
||
} else {
|
||
badge.innerHTML = '<span class="badge bg-secondary" title="Clôturé"><i class="fas fa-check me-1"></i>Clôturé</span>';
|
||
}
|
||
badge.style.display = 'inline';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Masque le badge de présence
|
||
*/
|
||
function hidePresenceBadge() {
|
||
const badge = document.getElementById('presence_badge');
|
||
if (badge) {
|
||
badge.style.display = 'none';
|
||
badge.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Masque les boutons de validation de présence
|
||
*/
|
||
function hidePresenceButtons() {
|
||
const markPresentBtn = document.getElementById('markPresentBtn');
|
||
if (markPresentBtn) {
|
||
markPresentBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Fonction utilitaire pour calculer l'heure de fin (+1h)
|
||
function calculateHeureFin(heureDebut) {
|
||
if (!heureDebut) return '10:00';
|
||
|
||
console.log('calculateHeureFin - heureDebut:', heureDebut);
|
||
|
||
const [h, m] = heureDebut.split(':').map(Number);
|
||
if (isNaN(h) || isNaN(m)) return '10:00';
|
||
|
||
let hFin = h + 1;
|
||
if (hFin > 23) hFin = 23;
|
||
|
||
return `${hFin.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
// Fonction pour forcer la fermeture complète du modal
|
||
function forceCloseModal() {
|
||
const modal = document.getElementById('eventModal');
|
||
if (!modal) return;
|
||
|
||
// Essayer d'abord la méthode Bootstrap standard
|
||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||
if (bsModal) {
|
||
bsModal.hide();
|
||
return;
|
||
}
|
||
|
||
// Si pas d'instance, créer une nouvelle et fermer
|
||
if (window.bootstrap && window.bootstrap.Modal) {
|
||
const newModal = new window.bootstrap.Modal(modal);
|
||
newModal.hide();
|
||
}
|
||
|
||
// Nettoyer le backdrop seulement si il reste après la fermeture
|
||
setTimeout(() => {
|
||
const backdrops = document.querySelectorAll('.modal-backdrop');
|
||
if (backdrops.length > 0) {
|
||
backdrops.forEach(backdrop => {
|
||
backdrop.remove();
|
||
});
|
||
document.body.classList.remove('modal-open');
|
||
document.body.style.overflow = '';
|
||
document.body.style.paddingRight = '';
|
||
}
|
||
}, 300);
|
||
}
|
||
|
||
export function fillFormWithDate(data) {
|
||
const dateRdv = data?.startStr?.split('T')[0] || '';
|
||
safeSetValue('date_rdv', dateRdv);
|
||
|
||
const heureDebut = data?.startStr?.split('T')[1]?.substring(0, 5) || '09:00';
|
||
safeSetValue('heure_rdv', heureDebut);
|
||
|
||
// Pour la date de fin, utiliser la même date que le début (même jour)
|
||
// FullCalendar passe parfois endStr avec +1 jour pour les sélections allDay
|
||
safeSetValue('date_fin', dateRdv);
|
||
|
||
// Extraire l'heure de fin depuis endStr, ou utiliser 09:15 par défaut
|
||
const heureFin = data?.endStr?.split('T')[1]?.substring(0, 5) || '09:15';
|
||
safeSetValue('heure_fin', heureFin);
|
||
}
|
||
|
||
export function fillFormWithEvent(event) {
|
||
if (!event) {
|
||
console.warn('fillFormWithEvent: event est undefined');
|
||
return;
|
||
}
|
||
|
||
// console.log('fillFormWithEvent - données reçues:', event);
|
||
// console.log('fillFormWithEvent - date_rdv:', event.date);
|
||
// console.log('fillFormWithEvent - heure_rdv:', event.heure);
|
||
// console.log('fillFormWithEvent - date_fin:', event.date_fin);
|
||
// console.log('fillFormWithEvent - heure_fin:', event.heure_fin);
|
||
// console.log('fillFormWithEvent - extendedProps:', event.extendedProps);
|
||
|
||
// Utiliser directement les données de l'API si elles sont disponibles
|
||
if (event.date && event.heure) {
|
||
// console.log('fillFormWithEvent - Utilisation des données directes de l\'API');
|
||
// Données directes de l'API
|
||
safeSetValue('date_rdv', event.date);
|
||
const heureDebut = event.heure.substring(0, 5); // Enlever les secondes
|
||
safeSetValue('heure_rdv', heureDebut);
|
||
// S'assurer que la date de fin est cohérente avec la date de début
|
||
const dateFin = event.date_fin && event.date_fin >= event.date ? event.date_fin : event.date;
|
||
safeSetValue('date_fin', dateFin);
|
||
// Utiliser l'heure de fin existante ou calculer +1h
|
||
const heureFin = event.heure_fin ? event.heure_fin.substring(0, 5) : calculateHeureFin(heureDebut);
|
||
safeSetValue('heure_fin', heureFin);
|
||
} else if (event.extendedProps?.date && event.extendedProps?.heure) {
|
||
// console.log('fillFormWithEvent - Utilisation des données extendedProps');
|
||
// Données dans extendedProps
|
||
safeSetValue('date_rdv', event.extendedProps.date);
|
||
const heureDebut = event.extendedProps.heure.substring(0, 5);
|
||
safeSetValue('heure_rdv', heureDebut);
|
||
// S'assurer que la date de fin est cohérente avec la date de début
|
||
const dateFin = event.extendedProps.date_fin && event.extendedProps.date_fin >= event.extendedProps.date ? event.extendedProps.date_fin : event.extendedProps.date;
|
||
safeSetValue('date_fin', dateFin);
|
||
// Utiliser l'heure de fin existante ou calculer +1h
|
||
const heureFin = event.extendedProps.heure_fin ? event.extendedProps.heure_fin.substring(0, 5) : calculateHeureFin(heureDebut);
|
||
safeSetValue('heure_fin', heureFin);
|
||
} else if (event.start && event.end) {
|
||
// console.log('fillFormWithEvent - Utilisation des données FullCalendar (start/end)');
|
||
// Données FullCalendar (fallback)
|
||
try {
|
||
const startDate = new Date(event.start);
|
||
const endDate = new Date(event.end);
|
||
|
||
if (!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
|
||
// Utiliser le timezone local au lieu d'UTC pour éviter les décalages
|
||
// Utiliser le timezone local pour les dates
|
||
const year = startDate.getFullYear();
|
||
const month = String(startDate.getMonth() + 1).padStart(2, '0');
|
||
const day = String(startDate.getDate()).padStart(2, '0');
|
||
const startDateStr = `${year}-${month}-${day}`;
|
||
|
||
const endYear = endDate.getFullYear();
|
||
const endMonth = String(endDate.getMonth() + 1).padStart(2, '0');
|
||
const endDay = String(endDate.getDate()).padStart(2, '0');
|
||
const endDateStr = `${endYear}-${endMonth}-${endDay}`;
|
||
|
||
// Utiliser le timezone local pour les heures
|
||
const startHours = String(startDate.getHours()).padStart(2, '0');
|
||
const startMinutes = String(startDate.getMinutes()).padStart(2, '0');
|
||
const heureDebut = `${startHours}:${startMinutes}`;
|
||
|
||
const endHours = String(endDate.getHours()).padStart(2, '0');
|
||
const endMinutes = String(endDate.getMinutes()).padStart(2, '0');
|
||
const heureFin = `${endHours}:${endMinutes}`;
|
||
|
||
safeSetValue('date_rdv', startDateStr);
|
||
safeSetValue('heure_rdv', heureDebut);
|
||
// S'assurer que la date de fin est cohérente avec la date de début
|
||
safeSetValue('date_fin', endDateStr >= startDateStr ? endDateStr : startDateStr);
|
||
safeSetValue('heure_fin', heureFin);
|
||
} else {
|
||
console.warn('fillFormWithEvent: dates FullCalendar invalides');
|
||
// Utiliser la date actuelle comme fallback (timezone local)
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||
const day = String(now.getDate()).padStart(2, '0');
|
||
const nowStr = `${year}-${month}-${day}`;
|
||
const hours = String(now.getHours()).padStart(2, '0');
|
||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||
const heureDebut = `${hours}:${minutes}`;
|
||
safeSetValue('date_rdv', nowStr);
|
||
safeSetValue('heure_rdv', heureDebut);
|
||
safeSetValue('date_fin', nowStr);
|
||
const heureFin = calculateHeureFin(heureDebut);
|
||
safeSetValue('heure_fin', heureFin);
|
||
}
|
||
} catch (error) {
|
||
console.error('fillFormWithEvent: erreur lors du parsing des dates FullCalendar', error);
|
||
// Utiliser la date actuelle comme fallback (timezone local)
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||
const day = String(now.getDate()).padStart(2, '0');
|
||
const nowStr = `${year}-${month}-${day}`;
|
||
const hours = String(now.getHours()).padStart(2, '0');
|
||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||
const heureDebut = `${hours}:${minutes}`;
|
||
safeSetValue('date_rdv', nowStr);
|
||
safeSetValue('heure_rdv', heureDebut);
|
||
safeSetValue('date_fin', nowStr);
|
||
const heureFin = calculateHeureFin(heureDebut);
|
||
safeSetValue('heure_fin', heureFin);
|
||
}
|
||
} else {
|
||
console.warn('fillFormWithEvent: aucune donnée de date/heure trouvée - utilisation de la date actuelle');
|
||
// Utiliser la date actuelle comme fallback (timezone local)
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||
const day = String(now.getDate()).padStart(2, '0');
|
||
const nowStr = `${year}-${month}-${day}`;
|
||
const hours = String(now.getHours()).padStart(2, '0');
|
||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||
const heureDebut = `${hours}:${minutes}`;
|
||
safeSetValue('date_rdv', nowStr);
|
||
safeSetValue('heure_rdv', heureDebut);
|
||
safeSetValue('date_fin', nowStr);
|
||
const heureFin = calculateHeureFin(heureDebut);
|
||
safeSetValue('heure_fin', heureFin);
|
||
}
|
||
|
||
// Protection contre extendedProps undefined
|
||
const extendedProps = event.extendedProps || {};
|
||
safeSetValue('type', extendedProps.type || event.type || '');
|
||
const langueValue = extendedProps.langue || event.langue || '';
|
||
if (DEBUG_SELECTS) {
|
||
console.log('📝 [FILL_FORM] Définition de la langue:', langueValue);
|
||
}
|
||
safeSetValue('langue', langueValue);
|
||
const beneficiaireId = extendedProps.id_beneficiaire || event.id_beneficiaire || '';
|
||
safeSetValue('id_beneficiaire', beneficiaireId);
|
||
safeSetValue('id_intervenant', extendedProps.id_intervenant || event.id_intervenant || '');
|
||
safeSetValue('id_traducteur', extendedProps.id_traducteur || event.id_traducteur || '');
|
||
safeSetValue('nom_traducteur', extendedProps.nom_traducteur || event.nom_traducteur || '');
|
||
safeSetValue('id_local', extendedProps.id_local || event.id_local || '');
|
||
safeSetValue('id_departement', extendedProps.id_departement || event.id_departement || '');
|
||
safeSetValue('id_type_intervention', extendedProps.id_type_intervention || event.id_type_intervention || '');
|
||
|
||
// Charger le statut liste rouge si un bénéficiaire est sélectionné
|
||
if (beneficiaireId) {
|
||
// Déclencher l'événement change pour afficher la case à cocher
|
||
const beneficiaireSelect = document.getElementById('id_beneficiaire');
|
||
if (beneficiaireSelect) {
|
||
// Utiliser setTimeout pour s'assurer que le DOM est mis à jour
|
||
setTimeout(() => {
|
||
const event = new Event('change', { bubbles: true });
|
||
beneficiaireSelect.dispatchEvent(event);
|
||
}, 100);
|
||
}
|
||
}
|
||
safeSetValue('commentaire', extendedProps.commentaire || event.commentaire || '');
|
||
|
||
// Mettre à jour les champs Select2 après avoir défini leurs valeurs
|
||
// Note: safeSetValue gère déjà Select2, mais on s'assure ici que Select2 est bien synchronisé
|
||
// au cas où Select2 n'était pas encore initialisé au moment de l'appel à safeSetValue
|
||
const select2Fields = ['type', 'langue', 'id_beneficiaire', 'id_intervenant', 'id_traducteur', 'id_local', 'id_departement', 'id_type_intervention'];
|
||
select2Fields.forEach(fieldId => {
|
||
const element = document.getElementById(fieldId);
|
||
if (element && element.tagName === 'SELECT') {
|
||
// Si Select2 est maintenant initialisé mais ne l'était pas avant, synchroniser
|
||
if (window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) {
|
||
const currentValue = element.value;
|
||
if (currentValue) {
|
||
// S'assurer que Select2 affiche la bonne valeur
|
||
jQuery(element).val(currentValue).trigger('change');
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Filtrer les traducteurs selon la langue sélectionnée
|
||
setTimeout(() => {
|
||
filterTraducteursByLangue();
|
||
}, 50);
|
||
|
||
// Gérer l'état du sélecteur traducteur selon la case "traducteur existant"
|
||
const tradSelect = document.getElementById('id_traducteur');
|
||
const useExistingCheckbox = document.getElementById('use_existing_traducteur');
|
||
const tradContainer = document.getElementById('traducteur-select-container');
|
||
if (useExistingCheckbox && tradContainer && tradSelect) {
|
||
// Vérifier que id_traducteur existe ET est supérieur à 0 (pas 0, null, undefined, ou vide)
|
||
const traducteurId = extendedProps.id_traducteur || event.id_traducteur;
|
||
const hasTraducteur = traducteurId && parseInt(traducteurId, 10) > 0;
|
||
useExistingCheckbox.checked = hasTraducteur;
|
||
if (hasTraducteur) {
|
||
tradContainer.classList.remove('d-none');
|
||
tradSelect.setAttribute('required', 'required');
|
||
} else {
|
||
tradContainer.classList.add('d-none');
|
||
tradSelect.removeAttribute('required');
|
||
safeSetValue('id_traducteur', '');
|
||
}
|
||
}
|
||
|
||
// Appliquer la logique d'affichage conditionnel selon le type
|
||
const type = extendedProps.type || event.type || '';
|
||
if (type === 'groupe') {
|
||
// Afficher les champs de groupe et masquer le bénéficiaire
|
||
document.getElementById('groupeFields').style.display = '';
|
||
document.getElementById('nb_participants').required = true;
|
||
document.getElementById('id_beneficiaire').closest('.col-md-6').style.display = 'none';
|
||
document.getElementById('id_beneficiaire').required = false;
|
||
|
||
// Remplir les champs de groupe s'ils existent
|
||
if (extendedProps.nb_participants || event.nb_participants) {
|
||
safeSetValue('nb_participants', extendedProps.nb_participants || event.nb_participants);
|
||
}
|
||
if (extendedProps.nb_hommes || event.nb_hommes) {
|
||
safeSetValue('nb_hommes', extendedProps.nb_hommes || event.nb_hommes);
|
||
}
|
||
if (extendedProps.nb_femmes || event.nb_femmes) {
|
||
safeSetValue('nb_femmes', extendedProps.nb_femmes || event.nb_femmes);
|
||
}
|
||
} else {
|
||
// Masquer les champs de groupe et afficher le bénéficiaire
|
||
document.getElementById('groupeFields').style.display = 'none';
|
||
document.getElementById('nb_participants').required = false;
|
||
document.getElementById('id_beneficiaire').closest('.col-md-6').style.display = '';
|
||
document.getElementById('id_beneficiaire').required = true;
|
||
}
|
||
|
||
// console.log('fillFormWithEvent - formulaire rempli avec succès');
|
||
|
||
// Vérifier et afficher les alertes d'absence
|
||
checkAndDisplayAbsenceAlert(event);
|
||
|
||
// Afficher les informations de clôture si l'événement est clôturé
|
||
displayClotureInfoEdit(event);
|
||
}
|
||
|
||
export function resetForm() {
|
||
document.getElementById('eventForm').reset();
|
||
clearFormErrors();
|
||
|
||
// Réinitialiser l'affichage conditionnel
|
||
document.getElementById('groupeFields').style.display = 'none';
|
||
document.getElementById('nb_participants').required = false;
|
||
document.getElementById('id_beneficiaire').closest('.col-md-6').style.display = '';
|
||
document.getElementById('id_beneficiaire').required = true;
|
||
|
||
// Réinitialiser le bloc traducteur
|
||
const tradContainer = document.getElementById('traducteur-select-container');
|
||
const tradSelect = document.getElementById('id_traducteur');
|
||
const useExistingCheckbox = document.getElementById('use_existing_traducteur');
|
||
if (tradContainer) tradContainer.classList.add('d-none');
|
||
if (tradSelect) {
|
||
tradSelect.removeAttribute('required');
|
||
tradSelect.value = '';
|
||
if (jQuery(tradSelect).hasClass('select2-hidden-accessible')) {
|
||
jQuery(tradSelect).val('').trigger('change');
|
||
}
|
||
}
|
||
if (useExistingCheckbox) useExistingCheckbox.checked = false;
|
||
}
|
||
|
||
// Affiche les erreurs inline sous les champs
|
||
export function showFormErrors(errors) {
|
||
clearFormErrors();
|
||
let hasGeneral = false;
|
||
for (const [field, message] of Object.entries(errors)) {
|
||
const input = document.getElementById(field);
|
||
if (input) {
|
||
input.classList.add('is-invalid');
|
||
let feedback = input.parentNode.querySelector('.invalid-feedback');
|
||
if (!feedback) {
|
||
feedback = document.createElement('div');
|
||
feedback.className = 'invalid-feedback';
|
||
input.parentNode.appendChild(feedback);
|
||
}
|
||
feedback.textContent = message;
|
||
} else {
|
||
hasGeneral = true;
|
||
}
|
||
}
|
||
// Affiche un message général si erreur non liée à un champ
|
||
const errorBox = document.getElementById('eventFormErrors');
|
||
if (hasGeneral && errorBox) {
|
||
errorBox.textContent = errors._general || 'Erreur lors de la validation du formulaire';
|
||
errorBox.classList.remove('d-none');
|
||
}
|
||
}
|
||
|
||
// Réinitialise les erreurs inline
|
||
export function clearFormErrors() {
|
||
const form = document.getElementById('eventForm');
|
||
if (!form) return;
|
||
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
|
||
form.querySelectorAll('.invalid-feedback').forEach(el => el.remove());
|
||
const errorBox = document.getElementById('eventFormErrors');
|
||
if (errorBox) {
|
||
errorBox.textContent = '';
|
||
errorBox.classList.add('d-none');
|
||
}
|
||
}
|
||
|
||
// Gestion de la soumission du formulaire (création/édition)
|
||
export async function handleEventFormSubmit(mode, eventId = null, onSuccess = null) {
|
||
clearFormErrors();
|
||
const form = document.getElementById('eventForm');
|
||
const formData = new FormData(form);
|
||
const data = Object.fromEntries(formData.entries());
|
||
|
||
// Validation côté client
|
||
const errors = {};
|
||
|
||
// Champs requis de base
|
||
const requiredFields = ['date_rdv', 'heure_rdv', 'type', 'langue', 'id_intervenant', 'id_local'];
|
||
|
||
// Ajouter le bénéficiaire seulement si ce n'est pas un RDV de groupe
|
||
if (data.type !== 'groupe') {
|
||
requiredFields.push('id_beneficiaire');
|
||
}
|
||
|
||
// Appliquer la logique "traducteur existant"
|
||
const useExisting = data.use_existing_traducteur === '1' || data.use_existing_traducteur === 'on' || data.use_existing_traducteur === 'true';
|
||
const traducteurId = parseInt(data.id_traducteur, 10);
|
||
const hasValidTraducteurId = !isNaN(traducteurId) && traducteurId > 0;
|
||
|
||
if (useExisting && hasValidTraducteurId) {
|
||
// Mode traducteur existant : utiliser id_traducteur et ignorer nom_traducteur
|
||
delete data.nom_traducteur;
|
||
// S'assurer que id_traducteur est bien un nombre
|
||
data.id_traducteur = traducteurId.toString();
|
||
} else {
|
||
// Pas de traducteur existant ou pas d'id valide : utiliser nom_traducteur
|
||
// Mettre id_traducteur à null (le serveur l'attend comme null, pas 0)
|
||
// Ne pas envoyer le champ du tout ou l'envoyer explicitement comme null
|
||
// En JSON, on peut envoyer null directement
|
||
data.id_traducteur = null;
|
||
// Si nom_traducteur est vide, on peut le supprimer pour éviter d'envoyer une chaîne vide
|
||
if (!data.nom_traducteur || data.nom_traducteur.trim() === '') {
|
||
delete data.nom_traducteur;
|
||
}
|
||
}
|
||
|
||
requiredFields.forEach(field => {
|
||
const value = data[field];
|
||
if (!value || value === '' || value === '0' || value === 'null') {
|
||
errors[field] = `Le champ est requis`;
|
||
}
|
||
});
|
||
|
||
// Champs conditionnels pour groupe
|
||
if (data.type === 'groupe') {
|
||
if (!data.nb_participants || parseInt(data.nb_participants) < 1) {
|
||
errors['nb_participants'] = 'Nombre de participants requis (>0)';
|
||
}
|
||
}
|
||
|
||
// Validation: empêcher la création d'événements à une date passée
|
||
if (mode === 'create' && data.date_rdv) {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0); // Mettre à minuit pour comparer uniquement les dates
|
||
const eventDate = new Date(data.date_rdv + 'T00:00:00');
|
||
eventDate.setHours(0, 0, 0, 0);
|
||
|
||
if (eventDate < today) {
|
||
errors['date_rdv'] = 'La date de rendez-vous ne peut pas être dans le passé';
|
||
}
|
||
}
|
||
|
||
// Cohérence des dates/heures
|
||
if (data.date_fin && data.date_rdv) {
|
||
if (data.date_fin < data.date_rdv) {
|
||
errors['date_fin'] = 'La date de fin doit être après ou égale à la date de début';
|
||
} else if (data.date_fin === data.date_rdv && data.heure_fin && data.heure_rdv) {
|
||
// Si même jour, vérifier que l'heure de fin est après l'heure de début
|
||
if (data.heure_fin <= data.heure_rdv) {
|
||
errors['heure_fin'] = 'L\'heure de fin doit être après l\'heure de début pour un événement sur la même journée';
|
||
}
|
||
}
|
||
}
|
||
|
||
if (Object.keys(errors).length > 0) {
|
||
showFormErrors(errors);
|
||
notifyError('Veuillez corriger les erreurs du formulaire');
|
||
return;
|
||
}
|
||
|
||
// Appel API
|
||
try {
|
||
if (mode === 'create') {
|
||
await createEvent(data);
|
||
notifySuccess('Événement créé avec succès');
|
||
} else if (mode === 'edit' && eventId) {
|
||
await updateEvent(eventId, data);
|
||
notifySuccess('Événement modifié avec succès');
|
||
}
|
||
if (onSuccess) onSuccess();
|
||
} catch (e) {
|
||
// Gestion des erreurs serveur (format attendu : { error: { code, message, details: { field, message } } })
|
||
let serverErrors = {};
|
||
if (e && e.message) {
|
||
// Si le message contient un champ, on tente de l'afficher inline
|
||
if (e.details && e.details.field) {
|
||
serverErrors[e.details.field] = e.details.message;
|
||
} else {
|
||
serverErrors._general = e.message;
|
||
}
|
||
} else {
|
||
serverErrors._general = 'Erreur serveur inconnue';
|
||
}
|
||
showFormErrors(serverErrors);
|
||
notifyError(e.message || 'Erreur lors de la sauvegarde');
|
||
}
|
||
}
|
||
|
||
// Cache JS pour les listes dynamiques
|
||
const selectCache = {};
|
||
|
||
// Fonction utilitaire pour générer une clé de cache
|
||
function getCacheKey(params) {
|
||
return [params.date_rdv, params.heure_rdv, params.type].join('|');
|
||
}
|
||
|
||
// Fusionne les données de l'événement avec les disponibilités
|
||
function mergeEventDataWithAvailability(availabilityData, eventData) {
|
||
if (DEBUG_SELECTS) {
|
||
console.group('🔍 [MERGE] Fusion des données');
|
||
console.log('availabilityData:', availabilityData);
|
||
console.log('eventData:', eventData);
|
||
}
|
||
|
||
const merged = { ...availabilityData };
|
||
|
||
if (!eventData) {
|
||
if (DEBUG_SELECTS) {
|
||
console.log('⚠️ Pas d\'eventData fourni');
|
||
console.groupEnd();
|
||
}
|
||
return merged;
|
||
}
|
||
|
||
const extendedProps = eventData.extendedProps || {};
|
||
if (DEBUG_SELECTS) {
|
||
console.log('extendedProps complet:', JSON.stringify(extendedProps, null, 2));
|
||
console.log('eventData complet (clés):', Object.keys(eventData));
|
||
console.log('Recherche des valeurs critiques:');
|
||
console.log(' - id_departement:', extendedProps.id_departement || eventData.id_departement || 'NOT FOUND');
|
||
console.log(' - id_type_intervention:', extendedProps.id_type_intervention || eventData.id_type_intervention || 'NOT FOUND');
|
||
console.log(' - langue:', extendedProps.langue || eventData.langue || 'NOT FOUND');
|
||
console.log(' - id_traducteur:', extendedProps.id_traducteur || eventData.id_traducteur || 'NOT FOUND');
|
||
}
|
||
|
||
// Fonction helper pour fusionner un tableau d'entités
|
||
function mergeEntities(availabilityEntities, eventEntityId, eventEntityData) {
|
||
if (!eventEntityId || !eventEntityData) {
|
||
return availabilityEntities;
|
||
}
|
||
|
||
// Vérifier si l'entité de l'événement est déjà dans les disponibilités
|
||
const exists = availabilityEntities.some(entity => entity.id == eventEntityId);
|
||
|
||
if (!exists) {
|
||
// Ajouter l'entité de l'événement aux disponibilités
|
||
return [...availabilityEntities, eventEntityData];
|
||
}
|
||
|
||
return availabilityEntities;
|
||
}
|
||
|
||
// Fusionner chaque type d'entité
|
||
if (extendedProps.id_beneficiaire && eventData.beneficiaire) {
|
||
merged.beneficiaires = mergeEntities(
|
||
merged.beneficiaires || [],
|
||
extendedProps.id_beneficiaire,
|
||
{
|
||
id: extendedProps.id_beneficiaire,
|
||
nom: eventData.beneficiaire.nom + ' ' + (eventData.beneficiaire.prenom || ''),
|
||
prenom: eventData.beneficiaire.prenom
|
||
}
|
||
);
|
||
}
|
||
|
||
if (extendedProps.id_intervenant && eventData.intervenant) {
|
||
merged.intervenants = mergeEntities(
|
||
merged.intervenants || [],
|
||
extendedProps.id_intervenant,
|
||
{
|
||
id: extendedProps.id_intervenant,
|
||
nom: eventData.intervenant.nom + ' ' + (eventData.intervenant.prenom || ''),
|
||
prenom: eventData.intervenant.prenom
|
||
}
|
||
);
|
||
}
|
||
|
||
if (extendedProps.id_traducteur && eventData.traducteur) {
|
||
if (DEBUG_SELECTS) console.log('✅ Fusion traducteur - id:', extendedProps.id_traducteur);
|
||
merged.traducteurs = mergeEntities(
|
||
merged.traducteurs || [],
|
||
extendedProps.id_traducteur,
|
||
{
|
||
id: extendedProps.id_traducteur,
|
||
nom: eventData.traducteur.nom + ' ' + (eventData.traducteur.prenom || ''),
|
||
prenom: eventData.traducteur.prenom
|
||
}
|
||
);
|
||
} else if (DEBUG_SELECTS) {
|
||
console.log('⚠️ Pas de traducteur à fusionner');
|
||
}
|
||
|
||
if (extendedProps.id_local && eventData.local) {
|
||
merged.locaux = mergeEntities(
|
||
merged.locaux || [],
|
||
extendedProps.id_local,
|
||
{
|
||
id: extendedProps.id_local,
|
||
nom: eventData.local.nom
|
||
}
|
||
);
|
||
}
|
||
|
||
if (extendedProps.langue || eventData.langue) {
|
||
const langueId = extendedProps.langue || eventData.langue;
|
||
// Si eventData.langue est un objet, utiliser ses propriétés
|
||
let langueNom = 'Langue';
|
||
let langueSlug = null;
|
||
|
||
if (typeof eventData.langue === 'object' && eventData.langue) {
|
||
langueNom = eventData.langue.nom || 'Langue';
|
||
langueSlug = eventData.langue.slug || eventData.langue.id || langueId;
|
||
} else {
|
||
// Si c'est juste un ID/slug, l'utiliser comme slug aussi
|
||
langueSlug = langueId;
|
||
}
|
||
|
||
if (DEBUG_SELECTS) console.log('✅ Fusion langue - id:', langueId, 'slug:', langueSlug);
|
||
|
||
const langueData = {
|
||
id: langueId,
|
||
nom: langueNom
|
||
};
|
||
|
||
// Ajouter le slug si disponible
|
||
if (langueSlug) {
|
||
langueData.slug = langueSlug;
|
||
}
|
||
|
||
merged.langues = mergeEntities(
|
||
merged.langues || [],
|
||
langueId,
|
||
langueData
|
||
);
|
||
} else if (DEBUG_SELECTS) {
|
||
console.log('⚠️ Pas de langue à fusionner');
|
||
}
|
||
|
||
// Fusionner le département
|
||
if (extendedProps.id_departement || eventData.id_departement) {
|
||
const departementId = extendedProps.id_departement || eventData.id_departement;
|
||
const departementNom = eventData.departement?.nom || eventData.departement || 'Département';
|
||
if (DEBUG_SELECTS) console.log('✅ Fusion département - id:', departementId);
|
||
merged.departements = mergeEntities(
|
||
merged.departements || [],
|
||
departementId,
|
||
{
|
||
id: departementId,
|
||
nom: departementNom
|
||
}
|
||
);
|
||
} else if (DEBUG_SELECTS) {
|
||
console.log('⚠️ Pas de département à fusionner');
|
||
}
|
||
|
||
// Fusionner le type d'intervention
|
||
if (extendedProps.id_type_intervention || eventData.id_type_intervention) {
|
||
const typeInterventionId = extendedProps.id_type_intervention || eventData.id_type_intervention;
|
||
const typeInterventionNom = eventData.type_intervention?.nom || eventData.type_intervention || 'Type d\'intervention';
|
||
if (DEBUG_SELECTS) console.log('✅ Fusion type intervention - id:', typeInterventionId);
|
||
merged.types_intervention = mergeEntities(
|
||
merged.types_intervention || [],
|
||
typeInterventionId,
|
||
{
|
||
id: typeInterventionId,
|
||
nom: typeInterventionNom
|
||
}
|
||
);
|
||
} else if (DEBUG_SELECTS) {
|
||
console.log('⚠️ Pas de type d\'intervention à fusionner');
|
||
}
|
||
|
||
if (DEBUG_SELECTS) {
|
||
console.log('Résultat final - traducteurs:', merged.traducteurs?.length || 0,
|
||
'départements:', merged.departements?.length || 0,
|
||
'types_intervention:', merged.types_intervention?.length || 0,
|
||
'langues:', merged.langues?.length || 0);
|
||
console.groupEnd();
|
||
}
|
||
return merged;
|
||
}
|
||
|
||
// Cache pour les données de disponibilité
|
||
let disponibilitesCache = new Map();
|
||
let lastPopulateCall = null;
|
||
let populateSelectsTimeout = null;
|
||
let isPopulateSelectsRunning = false; // Verrou pour éviter les exécutions simultanées
|
||
let populateCallCount = 0; // Compteur pour déboguer les appels multiples
|
||
let lastPopulateCallTime = 0; // Timestamp du dernier appel
|
||
|
||
// Fonction utilitaire pour sécuriser l'accès aux éléments du DOM
|
||
function safeSetValue(elementId, value) {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
// Si c'est un select avec Select2 initialisé, utiliser val() de Select2
|
||
if (element.tagName === 'SELECT' && window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) {
|
||
jQuery(element).val(value).trigger('change');
|
||
} else {
|
||
// Sinon, définir la valeur directement
|
||
element.value = value;
|
||
}
|
||
return true;
|
||
}
|
||
console.warn(`Élément ${elementId} non trouvé dans le DOM`);
|
||
return false;
|
||
}
|
||
|
||
function safeGetValue(elementId) {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
return element.value;
|
||
}
|
||
console.warn(`Élément ${elementId} non trouvé dans le DOM`);
|
||
return null;
|
||
}
|
||
|
||
// Fonction interne pour exécuter la population des selects
|
||
async function _executePopulateSelects(eventData = null) {
|
||
// Vérifier si une exécution est déjà en cours
|
||
if (isPopulateSelectsRunning) {
|
||
if (DEBUG_SELECTS) {
|
||
console.warn('⚠️ _executePopulateSelects déjà en cours, ignoré');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Activer le verrou
|
||
isPopulateSelectsRunning = true;
|
||
isUpdatingSelects = true; // Empêcher les boucles de change events
|
||
|
||
if (DEBUG_SELECTS) {
|
||
console.log('🔒 Verrous activés (isPopulateSelectsRunning + isUpdatingSelects)');
|
||
}
|
||
|
||
try {
|
||
const date = safeGetValue('date_rdv');
|
||
const heure = safeGetValue('heure_rdv');
|
||
|
||
if (!date || !heure) {
|
||
console.warn('Date et heure requises pour filtrer les disponibilités');
|
||
return;
|
||
}
|
||
|
||
// Créer une clé de cache unique
|
||
const cacheKey = `${date}|${heure}|${eventData ? eventData.id || 'new' : 'new'}`;
|
||
|
||
// Vérifier si on a déjà les données en cache
|
||
if (disponibilitesCache.has(cacheKey)) {
|
||
// Utilisation des données en cache (log désactivé pour réduire le bruit)
|
||
const cachedData = disponibilitesCache.get(cacheKey);
|
||
|
||
// Fusionner les données de l'événement avec les disponibilités
|
||
const mergedData = mergeEventDataWithAvailability(cachedData, eventData);
|
||
filterSelectOptions(mergedData);
|
||
disableUnavailableDates();
|
||
preselectValues(eventData);
|
||
return;
|
||
}
|
||
|
||
// Éviter les appels simultanés avec les mêmes paramètres
|
||
const currentCall = `${date}|${heure}`;
|
||
if (lastPopulateCall === currentCall) {
|
||
return;
|
||
}
|
||
|
||
lastPopulateCall = currentCall;
|
||
|
||
const params = {
|
||
date: date,
|
||
heure: heure
|
||
};
|
||
|
||
if (DEBUG_SELECTS) {
|
||
console.group('🔍 [POPULATE] Récupération des disponibilités');
|
||
console.log('Params:', params);
|
||
}
|
||
|
||
const data = await getFilters('disponibilites', params);
|
||
|
||
if (DEBUG_SELECTS) {
|
||
console.log('Données API - départements:', data.departements?.length || 0,
|
||
'traducteurs:', data.traducteurs?.length || 0,
|
||
'types_intervention:', data.types_intervention?.length || 0,
|
||
'langues:', data.langues?.length || 0);
|
||
}
|
||
|
||
// Mettre en cache les données (expire après 5 minutes)
|
||
disponibilitesCache.set(cacheKey, data);
|
||
setTimeout(() => {
|
||
disponibilitesCache.delete(cacheKey);
|
||
}, 5 * 60 * 1000); // 5 minutes
|
||
|
||
// Fusionner les données de l'événement avec les disponibilités
|
||
const mergedData = mergeEventDataWithAvailability(data, eventData);
|
||
|
||
if (DEBUG_SELECTS) {
|
||
console.groupEnd();
|
||
}
|
||
|
||
filterSelectOptions(mergedData);
|
||
|
||
// Mettre à jour les disponibilités de dates après avoir filtré les selects
|
||
disableUnavailableDates();
|
||
|
||
// Pré-sélectionner les valeurs
|
||
preselectValues(eventData);
|
||
|
||
// Filtrer les traducteurs selon la langue sélectionnée après le chargement
|
||
setTimeout(() => {
|
||
filterTraducteursByLangue();
|
||
}, 100);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur lors du filtrage des selects:', error);
|
||
} finally {
|
||
// Libérer le verrou après un court délai pour éviter les boucles
|
||
setTimeout(() => {
|
||
isPopulateSelectsRunning = false;
|
||
isUpdatingSelects = false;
|
||
if (DEBUG_SELECTS) {
|
||
console.log('🔓 Verrous désactivés');
|
||
}
|
||
}, 200);
|
||
|
||
// Réinitialiser lastPopulateCall après un délai
|
||
const currentCall = `${safeGetValue('date_rdv')}|${safeGetValue('heure_rdv')}`;
|
||
setTimeout(() => {
|
||
if (lastPopulateCall === currentCall) {
|
||
lastPopulateCall = null;
|
||
}
|
||
}, 1000);
|
||
}
|
||
}
|
||
|
||
// Version debounced de populateSelects pour éviter les appels multiples
|
||
export async function populateSelects(eventData = null) {
|
||
const now = Date.now();
|
||
populateCallCount++;
|
||
|
||
// Logger les appels trop rapprochés (moins de 100ms)
|
||
if (DEBUG_SELECTS && now - lastPopulateCallTime < 100) {
|
||
console.warn(`⚠️ populateSelects appelé ${populateCallCount} fois (${now - lastPopulateCallTime}ms depuis dernier appel)`);
|
||
}
|
||
lastPopulateCallTime = now;
|
||
|
||
// Si une exécution est en cours, ne rien faire
|
||
if (isPopulateSelectsRunning) {
|
||
if (DEBUG_SELECTS) {
|
||
console.log('🔒 populateSelects ignoré (exécution en cours)');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Annuler le timeout précédent s'il existe
|
||
if (populateSelectsTimeout) {
|
||
clearTimeout(populateSelectsTimeout);
|
||
if (DEBUG_SELECTS) {
|
||
console.log('⏱️ Timeout précédent annulé, nouveau debounce');
|
||
}
|
||
}
|
||
|
||
// Créer un nouveau timeout qui exécutera la fonction après un délai
|
||
return new Promise((resolve) => {
|
||
populateSelectsTimeout = setTimeout(async () => {
|
||
if (DEBUG_SELECTS) {
|
||
console.log(`🚀 Exécution de populateSelects (${populateCallCount} appels total)`);
|
||
populateCallCount = 0; // Reset le compteur
|
||
}
|
||
await _executePopulateSelects(eventData);
|
||
resolve();
|
||
}, 500); // Délai augmenté à 500ms pour plus de stabilité
|
||
});
|
||
}
|
||
|
||
// Fonction helper pour pré-sélectionner les valeurs
|
||
function preselectValues(eventData) {
|
||
if (!eventData) {
|
||
return;
|
||
}
|
||
|
||
const extendedProps = eventData.extendedProps || {};
|
||
const selects = {
|
||
'id_beneficiaire': extendedProps.id_beneficiaire || eventData.id_beneficiaire,
|
||
'id_intervenant': extendedProps.id_intervenant || eventData.id_intervenant,
|
||
'id_traducteur': extendedProps.id_traducteur || eventData.id_traducteur,
|
||
'id_local': extendedProps.id_local || eventData.id_local,
|
||
'id_departement': extendedProps.id_departement || eventData.id_departement,
|
||
'id_type_intervention': extendedProps.id_type_intervention || eventData.id_type_intervention,
|
||
'langue': extendedProps.langue || eventData.langue,
|
||
'type': extendedProps.type || eventData.type
|
||
};
|
||
|
||
if (DEBUG_SELECTS) {
|
||
console.group('🔍 [PRESELECT] Pré-sélection des valeurs');
|
||
const problematicValues = {
|
||
'id_departement': selects.id_departement,
|
||
'id_traducteur': selects.id_traducteur,
|
||
'id_type_intervention': selects.id_type_intervention,
|
||
'langue': selects.langue
|
||
};
|
||
console.log('Valeurs problématiques:', problematicValues);
|
||
}
|
||
|
||
// Attendre un peu que les selects soient mis à jour
|
||
setTimeout(() => {
|
||
const problematicSelects = ['id_departement', 'id_traducteur', 'id_type_intervention', 'langue'];
|
||
const results = [];
|
||
|
||
Object.entries(selects).forEach(([selectId, value]) => {
|
||
if (value) {
|
||
const select = document.getElementById(selectId);
|
||
const isProblematic = problematicSelects.includes(selectId);
|
||
|
||
if (select && select.options) {
|
||
// Vérifier que l'option existe avant de définir la valeur
|
||
const optionExists = Array.from(select.options).some(opt => opt.value == value);
|
||
|
||
if (optionExists) {
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
results.push(`${selectId}=${value} ✅`);
|
||
}
|
||
// Mettre à jour Select2 si il est initialisé (utiliser val() de Select2 qui est plus fiable)
|
||
if (jQuery(select).hasClass('select2-hidden-accessible')) {
|
||
jQuery(select).val(value).trigger('change');
|
||
} else {
|
||
// Si Select2 n'est pas initialisé, définir la valeur directement
|
||
select.value = value;
|
||
}
|
||
// Déclencher l'événement change pour les selects qui ont des logiques conditionnelles
|
||
if (selectId === 'type') {
|
||
select.dispatchEvent(new Event('change'));
|
||
}
|
||
} else {
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
results.push(`${selectId}=${value} ⚠️ option non trouvée`);
|
||
}
|
||
|
||
// Si l'option n'existe pas encore, réessayer après un délai supplémentaire
|
||
// Pour tous les selects qui utilisent Select2, on peut avoir besoin d'attendre le chargement des options
|
||
const select2Fields = ['langue', 'id_beneficiaire', 'id_intervenant', 'id_traducteur', 'id_local', 'id_departement', 'id_type_intervention'];
|
||
if (select2Fields.includes(selectId)) {
|
||
setTimeout(() => {
|
||
const retrySelect = document.getElementById(selectId);
|
||
if (retrySelect && retrySelect.options) {
|
||
const retryOptionExists = Array.from(retrySelect.options).some(opt => opt.value == value);
|
||
if (retryOptionExists) {
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
console.log(`✅ RETRY ${selectId}=${value} réussi`);
|
||
}
|
||
// Mettre à jour Select2 si il est initialisé (utiliser val() de Select2 qui est plus fiable)
|
||
if (jQuery(retrySelect).hasClass('select2-hidden-accessible')) {
|
||
jQuery(retrySelect).val(value).trigger('change');
|
||
} else {
|
||
// Si Select2 n'est pas initialisé, définir la valeur directement
|
||
retrySelect.value = value;
|
||
}
|
||
// Déclencher l'événement change pour les selects qui ont des logiques conditionnelles
|
||
if (selectId === 'type') {
|
||
retrySelect.dispatchEvent(new Event('change'));
|
||
}
|
||
} else {
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
const options = retrySelect.options ? Array.from(retrySelect.options).map(opt => opt.value).filter(v => v !== '') : [];
|
||
console.warn(`❌ RETRY ${selectId}=${value} échoué. Options: [${options.join(', ')}]`);
|
||
}
|
||
}
|
||
}
|
||
}, 200);
|
||
}
|
||
}
|
||
} else if (DEBUG_SELECTS && isProblematic) {
|
||
results.push(`${selectId} ❌ select non trouvé`);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (DEBUG_SELECTS && results.length > 0) {
|
||
console.log('Résultats:', results.join(' | '));
|
||
console.groupEnd();
|
||
}
|
||
}, 50);
|
||
}
|
||
|
||
// Filtre les options des selects en cachant celles qui ne sont pas disponibles
|
||
function filterSelectOptions(data) {
|
||
if (DEBUG_SELECTS) {
|
||
console.group('🔍 [FILTER_OPTIONS] Filtrage des selects');
|
||
}
|
||
|
||
filterSelect('id_beneficiaire', data.beneficiaires);
|
||
filterSelect('id_intervenant', data.intervenants);
|
||
filterSelect('id_traducteur', data.traducteurs);
|
||
filterSelect('id_local', data.locaux);
|
||
filterSelect('id_departement', data.departements);
|
||
filterSelect('id_type_intervention', data.types_intervention);
|
||
|
||
// Filtrer les langues selon langues_disponibles si c'est une permanence
|
||
const extendedProps = currentEventData?.extendedProps || {};
|
||
const isPermanence = extendedProps.type === 'permanence';
|
||
const languesDisponibles = extendedProps.langues_disponibles;
|
||
|
||
if (isPermanence && languesDisponibles && languesDisponibles.trim() !== '') {
|
||
// Si la permanence a des langues_disponibles, filtrer selon celles-ci
|
||
const languesPermises = languesDisponibles.split(',').map(l => l.trim()).filter(l => l !== '');
|
||
const languesFiltrees = data.langues.filter(langue => {
|
||
return languesPermises.includes(langue.id || langue.slug || langue);
|
||
});
|
||
filterSelect('langue', languesFiltrees);
|
||
} else {
|
||
// Sinon, utiliser toutes les langues disponibles
|
||
filterSelect('langue', data.langues);
|
||
}
|
||
|
||
if (DEBUG_SELECTS) {
|
||
console.groupEnd();
|
||
}
|
||
}
|
||
|
||
function filterSelect(selectId, availableItems) {
|
||
// Selects problématiques à surveiller
|
||
const problematicSelects = ['id_departement', 'id_traducteur', 'id_type_intervention', 'langue'];
|
||
const isProblematic = problematicSelects.includes(selectId);
|
||
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
console.group(`🔍 ${selectId}`);
|
||
}
|
||
|
||
const select = document.getElementById(selectId);
|
||
if (!select) {
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
console.warn(`❌ Select non trouvé`);
|
||
console.groupEnd();
|
||
}
|
||
return;
|
||
}
|
||
// Si ce n'est pas un <select> (ex: input hidden pour id_intervenant en front), ne rien faire
|
||
if (select.tagName !== 'SELECT') {
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
console.log(`⚠️ Pas un SELECT, ignoré`);
|
||
console.groupEnd();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Vérifier que availableItems est un tableau valide
|
||
if (!Array.isArray(availableItems)) {
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
console.warn(`❌ availableItems invalide:`, availableItems);
|
||
console.groupEnd();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Créer un ensemble des IDs disponibles pour un accès rapide
|
||
const availableIds = new Set(availableItems
|
||
.filter(item => item && item.id != null) // Filtrer les éléments null/undefined
|
||
.map(item => item.id.toString())
|
||
);
|
||
|
||
// Pour les langues, créer aussi un set des slugs si disponibles
|
||
const availableSlugs = selectId === 'langue' ? new Set(
|
||
availableItems
|
||
.filter(item => item != null)
|
||
.flatMap(item => {
|
||
const slugs = [];
|
||
// Si l'item a un slug explicite, l'ajouter
|
||
if (item.slug != null) {
|
||
slugs.push(item.slug.toString());
|
||
}
|
||
// Si l'ID est une chaîne (probablement un slug), l'ajouter aussi
|
||
if (item.id != null && isNaN(item.id)) {
|
||
slugs.push(item.id.toString());
|
||
}
|
||
return slugs;
|
||
})
|
||
) : new Set();
|
||
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
if (selectId === 'langue') {
|
||
console.log(`Items disponibles: ${availableItems.length} (IDs: [${Array.from(availableIds).join(', ')}]) (Slugs: [${Array.from(availableSlugs).join(', ')}])`);
|
||
// Logger les options HTML du select pour comprendre le mismatch
|
||
console.log('Options HTML du select langue:');
|
||
if (select.options) {
|
||
Array.from(select.options).forEach(opt => {
|
||
if (opt.value !== '') {
|
||
console.log(` - value="${opt.value}", data-slug="${opt.getAttribute('data-slug') || '(none)'}", text="${opt.text}"`);
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
console.log(`Items disponibles: ${availableItems.length} (IDs: [${Array.from(availableIds).join(', ')}])`);
|
||
}
|
||
}
|
||
|
||
// Récupérer la valeur actuellement sélectionnée pour les exceptions d'édition
|
||
const currentValue = select.value;
|
||
|
||
// Parcourir toutes les options du select
|
||
let visibleCount = 0;
|
||
let hiddenCount = 0;
|
||
const visibleOptions = [];
|
||
|
||
if (!select.options) {
|
||
console.warn(`⚠️ ${selectId}.options est undefined`);
|
||
return;
|
||
}
|
||
|
||
Array.from(select.options).forEach(option => {
|
||
if (option.value === '') {
|
||
// Garder l'option "Sélectionner..." toujours visible
|
||
option.style.display = '';
|
||
return;
|
||
}
|
||
|
||
// Exception pour l'édition : garder l'option actuellement sélectionnée visible
|
||
const isCurrentlySelected = option.value === currentValue;
|
||
|
||
// Pour les langues, vérifier aussi le slug dans data-slug ET le text
|
||
let isAvailable = availableIds.has(option.value);
|
||
let matchReason = '';
|
||
|
||
if (!isAvailable && selectId === 'langue') {
|
||
// Vérifier si l'option a un data-slug qui correspond
|
||
const optionSlug = option.getAttribute('data-slug');
|
||
if (optionSlug && optionSlug !== '(none)' && availableSlugs.has(optionSlug)) {
|
||
isAvailable = true;
|
||
matchReason = 'slug match';
|
||
}
|
||
// Ou vérifier si la valeur elle-même est un slug disponible
|
||
if (!isAvailable && availableSlugs.has(option.value)) {
|
||
isAvailable = true;
|
||
matchReason = 'value is slug';
|
||
}
|
||
// Ou vérifier si le text (nom) de l'option correspond à un slug disponible
|
||
if (!isAvailable && availableSlugs.size > 0) {
|
||
const optionText = option.text.toLowerCase().trim();
|
||
// Essayer d'abord le match exact
|
||
if (availableSlugs.has(optionText)) {
|
||
isAvailable = true;
|
||
matchReason = 'text matches slug';
|
||
} else {
|
||
// Normaliser les accents pour le matching (français → francais)
|
||
const normalizedText = optionText.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||
if (availableSlugs.has(normalizedText)) {
|
||
isAvailable = true;
|
||
matchReason = 'text matches slug (normalized)';
|
||
}
|
||
}
|
||
}
|
||
} else if (isAvailable) {
|
||
matchReason = 'id match';
|
||
}
|
||
|
||
// Cacher ou afficher l'option selon sa disponibilité
|
||
if (isAvailable || isCurrentlySelected) {
|
||
option.style.display = '';
|
||
option.disabled = false;
|
||
visibleCount++;
|
||
if (DEBUG_SELECTS && isProblematic && selectId === 'langue') {
|
||
visibleOptions.push(`${option.value}${isCurrentlySelected ? ' (sélectionné)' : ''} [${matchReason || 'selected'}]`);
|
||
} else if (DEBUG_SELECTS && isProblematic) {
|
||
visibleOptions.push(`${option.value}${isCurrentlySelected ? ' (sélectionné)' : ''}`);
|
||
}
|
||
} else {
|
||
option.style.display = 'none';
|
||
option.disabled = true;
|
||
hiddenCount++;
|
||
if (DEBUG_SELECTS && isProblematic && selectId === 'langue') {
|
||
const optionSlug = option.getAttribute('data-slug');
|
||
console.log(` ❌ Option cachée: value="${option.value}", data-slug="${optionSlug || '(none)'}", text="${option.text}"`);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (DEBUG_SELECTS && isProblematic) {
|
||
console.log(`Résultat: ${visibleCount} visibles, ${hiddenCount} cachées`);
|
||
console.log(`Valeur actuelle: ${currentValue || '(vide)'}`);
|
||
if (visibleOptions.length > 0 && visibleOptions.length <= 10) {
|
||
console.log(`Options visibles: ${visibleOptions.join(', ')}`);
|
||
}
|
||
console.groupEnd();
|
||
}
|
||
|
||
// Vérifier que jQuery et Select2 sont disponibles
|
||
if (!window.jQuery || !window.jQuery.fn.select2) {
|
||
console.warn('Select2 non disponible pour filterSelect');
|
||
return;
|
||
}
|
||
|
||
// Mettre à jour Select2 si il est initialisé
|
||
// Ne PAS déclencher 'change' pendant isUpdatingSelects pour éviter les boucles
|
||
if (jQuery(select).hasClass('select2-hidden-accessible')) {
|
||
// Utiliser change.select2 pour mise à jour visuelle sans déclencher les handlers
|
||
jQuery(select).trigger('change.select2');
|
||
}
|
||
}
|
||
|
||
// Fonction pour nettoyer tous les champs du modal
|
||
function clearModalFields() {
|
||
// Nettoyer les champs de texte
|
||
const textFields = [
|
||
'date_rdv', 'heure_rdv', 'date_fin', 'heure_fin',
|
||
'commentaire', 'nb_participants', 'nb_hommes', 'nb_femmes'
|
||
];
|
||
|
||
textFields.forEach(fieldId => {
|
||
const field = document.getElementById(fieldId);
|
||
if (field) {
|
||
field.value = '';
|
||
}
|
||
});
|
||
|
||
// Nettoyer les selects
|
||
const selectFields = [
|
||
'type', 'langue', 'id_beneficiaire', 'id_intervenant',
|
||
'id_traducteur', 'id_local', 'id_departement', 'id_type_intervention'
|
||
];
|
||
|
||
selectFields.forEach(selectId => {
|
||
const select = document.getElementById(selectId);
|
||
if (select) {
|
||
select.value = '';
|
||
// Si c'est un <select>, gérer Select2 et les options
|
||
if (select.tagName === 'SELECT' && select.options) {
|
||
if (jQuery(select).hasClass('select2-hidden-accessible')) {
|
||
jQuery(select).val('').trigger('change');
|
||
}
|
||
Array.from(select.options).forEach(option => {
|
||
option.style.display = '';
|
||
option.disabled = false;
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// Nettoyer les erreurs de formulaire
|
||
clearFormErrors();
|
||
|
||
// Réinitialiser les champs conditionnels
|
||
const typeField = document.getElementById('type');
|
||
if (typeField) {
|
||
typeField.dispatchEvent(new Event('change'));
|
||
}
|
||
|
||
// console.log('Tous les champs du modal ont été nettoyés');
|
||
}
|
||
|
||
// Fonction pour initialiser Select2 sur les selects du modal
|
||
function initializeSelect2() {
|
||
// Vérifier que jQuery et Select2 sont disponibles
|
||
if (!window.jQuery || !window.jQuery.fn.select2) {
|
||
console.warn('Select2 non disponible, réessai dans 100ms...');
|
||
setTimeout(initializeSelect2, 100);
|
||
return;
|
||
}
|
||
|
||
jQuery('#eventModal select:not(.skip-select2)').each(function() {
|
||
const $select = jQuery(this);
|
||
|
||
// Si Select2 est déjà initialisé (ex: init global), le détruire pour réappliquer avec dropdownParent
|
||
if ($select.hasClass('select2-hidden-accessible') || $select.data('select2')) {
|
||
try { $select.select2('destroy'); } catch (e) {}
|
||
}
|
||
|
||
$select.select2({
|
||
width: '100%',
|
||
placeholder: 'Sélectionner...',
|
||
allowClear: true,
|
||
dropdownParent: jQuery('#eventModal')
|
||
});
|
||
});
|
||
}
|
||
|
||
// Initialiser les gestionnaires d'événements pour les boutons
|
||
/**
|
||
* Charge l'historique des 3 derniers rendez-vous d'un bénéficiaire
|
||
* @param {number} beneficiaireId - ID du bénéficiaire
|
||
* @returns {Promise<Array>} Tableau des rendez-vous avec leurs incidents
|
||
*/
|
||
async function loadBeneficiaireHistorique(beneficiaireId) {
|
||
try {
|
||
const historique = await apiFetch(`beneficiaires/${beneficiaireId}/historique`);
|
||
return historique || [];
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement de l\'historique:', error);
|
||
notifyError('Erreur lors du chargement de l\'historique du bénéficiaire');
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Affiche l'historique sous forme de ligne du temps verticale
|
||
* @param {Array} historiqueData - Tableau des rendez-vous avec leurs incidents
|
||
*/
|
||
function displayHistoriqueTimeline(historiqueData) {
|
||
const timelineContainer = document.getElementById('historiqueTimeline');
|
||
if (!timelineContainer) {
|
||
console.error('Conteneur de timeline non trouvé');
|
||
return;
|
||
}
|
||
|
||
if (!historiqueData || historiqueData.length === 0) {
|
||
timelineContainer.innerHTML = `
|
||
<div class="alert alert-info text-center">
|
||
<i class="fas fa-info-circle me-2"></i>
|
||
Aucun rendez-vous trouvé pour ce bénéficiaire.
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="timeline">';
|
||
|
||
historiqueData.forEach((rdv, index) => {
|
||
const date = new Date(rdv.date_rdv + 'T' + rdv.heure_rdv);
|
||
const dateFormatted = date.toLocaleDateString('fr-FR', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric'
|
||
});
|
||
const heureFormatted = date.toLocaleTimeString('fr-FR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
|
||
const intervenantNom = rdv.intervenant
|
||
? `${rdv.intervenant.nom || ''} ${rdv.intervenant.prenom || ''}`.trim()
|
||
: 'Non renseigné';
|
||
|
||
const typeInterventionNom = rdv.type_intervention?.nom || 'Non renseigné';
|
||
|
||
const hasIncident = rdv.incident && rdv.incident.id;
|
||
const incidentClass = hasIncident ? 'border-danger border-start border-3' : '';
|
||
const incidentIcon = hasIncident ? '<i class="fas fa-exclamation-triangle text-danger me-2"></i>' : '';
|
||
|
||
html += `
|
||
<div class="timeline-item mb-4 ${incidentClass}" style="padding-left: 20px;">
|
||
<div class="d-flex align-items-start">
|
||
<div class="timeline-marker me-3" style="width: 12px; height: 12px; background-color: ${hasIncident ? '#dc3545' : '#0d6efd'}; border-radius: 50%; margin-top: 6px; flex-shrink: 0;"></div>
|
||
<div class="flex-grow-1">
|
||
<div class="card ${hasIncident ? 'border-danger' : ''}">
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||
<h6 class="card-title mb-0">
|
||
${incidentIcon}
|
||
<i class="fas fa-calendar-alt me-2"></i>${dateFormatted} à ${heureFormatted}
|
||
</h6>
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong><i class="fas fa-user-md me-2"></i>Intervenant:</strong> ${intervenantNom}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong><i class="fas fa-tools me-2"></i>Type d'intervention:</strong> ${typeInterventionNom}
|
||
</div>
|
||
${hasIncident ? `
|
||
<div class="alert alert-warning mt-3 mb-0">
|
||
<strong><i class="fas fa-exclamation-triangle me-2"></i>Incident signalé:</strong>
|
||
<div class="mt-2">
|
||
<strong>Résumé:</strong> ${rdv.incident.resume_incident || 'Non renseigné'}
|
||
</div>
|
||
${rdv.incident.commentaire_incident ? `
|
||
<div class="mt-2">
|
||
<strong>Commentaire:</strong> ${rdv.incident.commentaire_incident}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
${index < historiqueData.length - 1 ? '<div class="timeline-line" style="width: 2px; height: 20px; background-color: #dee2e6; margin-left: 5px; margin-top: 10px;"></div>' : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += '</div>';
|
||
timelineContainer.innerHTML = html;
|
||
}
|
||
|
||
function initializeModalButtons() {
|
||
// Bouton Modifier
|
||
const editBtn = document.getElementById('editEventBtn');
|
||
if (editBtn) {
|
||
editBtn.onclick = async function() {
|
||
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||
if (!userCanEdit) {
|
||
console.warn('Utilisateur non autorisé à modifier des événements');
|
||
return;
|
||
}
|
||
|
||
currentMode = 'edit';
|
||
updateModalDisplay(userCanEdit, window.crviPermissions && window.crviPermissions.can_delete);
|
||
|
||
// Remplir le formulaire avec les données actuelles
|
||
fillFormWithEvent(currentEventData);
|
||
// Overlay de chargement sur la modale pendant la population asynchrone
|
||
let overlayTarget = document.querySelector('#eventModal .modal-content') || document.getElementById('eventModal');
|
||
try {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.show(overlayTarget); }
|
||
await populateSelects(currentEventData);
|
||
} finally {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.hide(overlayTarget); }
|
||
}
|
||
clearFormErrors();
|
||
|
||
// Activer la synchronisation des dates
|
||
enableDateSynchronization();
|
||
};
|
||
}
|
||
|
||
// Bouton de fermeture du header
|
||
const closeModalBtn = document.getElementById('closeModalBtn');
|
||
if (closeModalBtn) {
|
||
closeModalBtn.onclick = function() {
|
||
const modal = document.getElementById('eventModal');
|
||
if (modal) {
|
||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||
if (bsModal) {
|
||
bsModal.hide();
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Bouton de fermeture du footer (mode vue)
|
||
const closeViewBtn = document.getElementById('closeViewBtn');
|
||
if (closeViewBtn) {
|
||
closeViewBtn.onclick = function() {
|
||
const modal = document.getElementById('eventModal');
|
||
if (modal) {
|
||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||
if (bsModal) {
|
||
bsModal.hide();
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Bouton Annuler
|
||
const cancelBtn = document.getElementById('cancelEditBtn');
|
||
if (cancelBtn) {
|
||
cancelBtn.onclick = function() {
|
||
if (currentMode === 'create') {
|
||
// En mode création, fermer le modal avec la méthode Bootstrap standard
|
||
const modal = document.getElementById('eventModal');
|
||
if (modal) {
|
||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||
if (bsModal) {
|
||
bsModal.hide();
|
||
}
|
||
}
|
||
} else if (currentMode === 'edit' && currentEventData) {
|
||
// En mode édition, retourner en mode vue
|
||
currentMode = 'view';
|
||
updateModalDisplay(
|
||
window.crviPermissions && window.crviPermissions.can_edit,
|
||
window.crviPermissions && window.crviPermissions.can_delete
|
||
);
|
||
fillViewBlock(currentEventData);
|
||
clearFormErrors();
|
||
|
||
// Désactiver la synchronisation des dates
|
||
disableDateSynchronization();
|
||
}
|
||
};
|
||
}
|
||
|
||
// Bouton Enregistrer
|
||
const saveBtn = document.getElementById('saveEvent');
|
||
if (saveBtn) {
|
||
saveBtn.onclick = async function() {
|
||
const mode = currentMode;
|
||
const eventId = currentEventData ? currentEventData.id : null;
|
||
|
||
let overlayTarget = document.querySelector('#eventModal .modal-content') || document.getElementById('eventModal');
|
||
try {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.show(overlayTarget); }
|
||
await handleEventFormSubmit(mode, eventId, function() {
|
||
// Succès : fermer le modal et rafraîchir le calendrier
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('eventModal'));
|
||
if (modal) {
|
||
modal.hide();
|
||
}
|
||
|
||
// Rafraîchir les calendriers (intervenant et collègues)
|
||
if (window.currentCalendar) {
|
||
window.currentCalendar.refetchEvents();
|
||
}
|
||
if (window.currentColleaguesCalendar) {
|
||
window.currentColleaguesCalendar.refetchEvents();
|
||
}
|
||
|
||
// Désactiver la synchronisation des dates
|
||
disableDateSynchronization();
|
||
});
|
||
} finally {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.hide(overlayTarget); }
|
||
}
|
||
};
|
||
}
|
||
|
||
// Bouton Supprimer
|
||
const deleteBtn = document.getElementById('deleteEvent');
|
||
if (deleteBtn) {
|
||
deleteBtn.onclick = async function() {
|
||
const userCanDelete = window.crviPermissions && window.crviPermissions.can_delete;
|
||
if (!userCanDelete) {
|
||
console.warn('Utilisateur non autorisé à supprimer des événements');
|
||
return;
|
||
}
|
||
|
||
if (confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) {
|
||
const eventId = currentEventData ? currentEventData.id : null;
|
||
if (eventId) {
|
||
let overlayTarget = document.querySelector('#eventModal .modal-content') || document.getElementById('eventModal');
|
||
try {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.show(overlayTarget); }
|
||
await deleteEvent(eventId);
|
||
// Succès : fermer le modal et rafraîchir le calendrier
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('eventModal'));
|
||
if (modal) {
|
||
modal.hide();
|
||
}
|
||
if (window.currentCalendar) {
|
||
window.currentCalendar.refetchEvents();
|
||
}
|
||
if (window.currentColleaguesCalendar) {
|
||
window.currentColleaguesCalendar.refetchEvents();
|
||
}
|
||
disableDateSynchronization();
|
||
} catch (error) {
|
||
console.error('Erreur lors de la suppression:', error);
|
||
notifyError('Erreur lors de la suppression de l\'événement');
|
||
} finally {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.hide(overlayTarget); }
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Bouton Valider présence
|
||
const markPresentBtn = document.getElementById('markPresentBtn');
|
||
if (markPresentBtn) {
|
||
markPresentBtn.onclick = async function() {
|
||
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||
if (!userCanEdit) {
|
||
console.warn('Utilisateur non autorisé à modifier des événements');
|
||
return;
|
||
}
|
||
|
||
const eventId = currentEventData ? currentEventData.id : null;
|
||
if (!eventId) {
|
||
return;
|
||
}
|
||
|
||
// Vérifier le type d'événement
|
||
const eventType = currentEventData?.type || currentEventData?.extendedProps?.type || '';
|
||
const isGroupe = eventType === 'groupe';
|
||
|
||
if (isGroupe) {
|
||
// Pour les événements de groupe, ouvrir la modal de validation des présences
|
||
openCheckPresenceModal(currentEventData);
|
||
} else {
|
||
// Pour les événements individuels, comportement actuel
|
||
if (confirm('Confirmer la présence à cet événement ?')) {
|
||
let overlayTarget = document.querySelector('#eventModal .modal-content') || document.getElementById('eventModal');
|
||
try {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.show(overlayTarget); }
|
||
await changeEventStatus(eventId, 'present');
|
||
notifySuccess('Présence validée');
|
||
if (window.currentCalendar) {
|
||
window.currentCalendar.refetchEvents();
|
||
}
|
||
if (window.currentColleaguesCalendar) {
|
||
window.currentColleaguesCalendar.refetchEvents();
|
||
}
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('eventModal'));
|
||
if (modal) {
|
||
modal.hide();
|
||
}
|
||
disableDateSynchronization();
|
||
} catch (error) {
|
||
console.error('Erreur lors du changement de statut:', error);
|
||
notifyError('Erreur lors de la validation de présence');
|
||
} finally {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.hide(overlayTarget); }
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Bouton Marquer comme absent
|
||
const markAbsentBtn = document.getElementById('markAbsentBtn');
|
||
if (markAbsentBtn) {
|
||
markAbsentBtn.onclick = async function() {
|
||
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||
if (!userCanEdit) {
|
||
console.warn('Utilisateur non autorisé à modifier des événements');
|
||
return;
|
||
}
|
||
|
||
const eventId = currentEventData ? currentEventData.id : null;
|
||
if (eventId) {
|
||
if (confirm('Marquer cet événement comme absent ?')) {
|
||
let overlayTarget = document.querySelector('#eventModal .modal-content') || document.getElementById('eventModal');
|
||
try {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.show(overlayTarget); }
|
||
await changeEventStatus(eventId, 'absence');
|
||
notifySuccess('Événement marqué comme absent');
|
||
if (window.currentCalendar) {
|
||
window.currentCalendar.refetchEvents();
|
||
}
|
||
if (window.currentColleaguesCalendar) {
|
||
window.currentColleaguesCalendar.refetchEvents();
|
||
}
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('eventModal'));
|
||
if (modal) {
|
||
modal.hide();
|
||
}
|
||
disableDateSynchronization();
|
||
} catch (error) {
|
||
console.error('Erreur lors du changement de statut:', error);
|
||
notifyError('Erreur lors du changement de statut');
|
||
} finally {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.hide(overlayTarget); }
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Bouton Annuler le rendez-vous
|
||
const cancelAppointmentBtn = document.getElementById('cancelAppointmentBtn');
|
||
if (cancelAppointmentBtn) {
|
||
cancelAppointmentBtn.onclick = async function() {
|
||
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||
if (!userCanEdit) {
|
||
console.warn('Utilisateur non autorisé à modifier des événements');
|
||
return;
|
||
}
|
||
|
||
const eventId = currentEventData ? currentEventData.id : null;
|
||
if (eventId) {
|
||
const motif = prompt('Motif de l\'annulation (optionnel):');
|
||
if (motif !== null) { // L'utilisateur n'a pas cliqué sur Annuler
|
||
let overlayTarget = document.querySelector('#eventModal .modal-content') || document.getElementById('eventModal');
|
||
try {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.show(overlayTarget); }
|
||
await changeEventStatus(eventId, 'annule', motif);
|
||
notifySuccess('Rendez-vous annulé');
|
||
if (window.currentCalendar) {
|
||
window.currentCalendar.refetchEvents();
|
||
}
|
||
if (window.currentColleaguesCalendar) {
|
||
window.currentColleaguesCalendar.refetchEvents();
|
||
}
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('eventModal'));
|
||
if (modal) {
|
||
modal.hide();
|
||
}
|
||
disableDateSynchronization();
|
||
} catch (error) {
|
||
console.error('Erreur lors de l\'annulation:', error);
|
||
notifyError('Erreur lors de l\'annulation du rendez-vous');
|
||
} finally {
|
||
if (window.CRVI_OVERLAY && overlayTarget) { window.CRVI_OVERLAY.hide(overlayTarget); }
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Bouton Debug SMS (affiché conditionnellement par le template si activé)
|
||
const debugSmsBtn = document.getElementById('debugSmsBtn');
|
||
if (debugSmsBtn) {
|
||
debugSmsBtn.onclick = function() {
|
||
const eventId = currentEventData ? currentEventData.id : null;
|
||
console.log('[DEBUG_SMS] Données événement actuelles:', currentEventData);
|
||
|
||
// Construire un message simple pour le debug
|
||
const dateStr = (document.getElementById('date_rdv') && document.getElementById('date_rdv').value) || currentEventData?.date || '';
|
||
const timeStr = (document.getElementById('heure_rdv') && document.getElementById('heure_rdv').value) || currentEventData?.time || '';
|
||
const typeStr = (document.getElementById('type') && document.getElementById('type').value) || currentEventData?.type || '';
|
||
|
||
const msg = `Debug SMS - Event ${eventId ?? 'N/A'} - ${dateStr} ${timeStr} - type: ${typeStr}`;
|
||
const phone = '0485500723';
|
||
|
||
if (!window.crviAjax || !window.crviAjax.url || !window.crviAjax.nonce) {
|
||
alert('Debug SMS: configuration AJAX manquante.');
|
||
return;
|
||
}
|
||
|
||
const params = new URLSearchParams();
|
||
params.append('action', 'crvi_send_sms_debug');
|
||
params.append('nonce', window.crviAjax.nonce);
|
||
params.append('phone', phone);
|
||
params.append('message', msg);
|
||
if (eventId) {
|
||
params.append('id_event', String(eventId));
|
||
}
|
||
params.append('sujet', 'debug');
|
||
|
||
fetch(window.crviAjax.url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||
},
|
||
body: params.toString()
|
||
}).then(async (res) => {
|
||
let json;
|
||
try { json = await res.json(); } catch (_) {}
|
||
if (!res.ok || !json?.success) {
|
||
const errMsg = json?.data?.message || 'Erreur lors de l’envoi SMS (debug).';
|
||
throw new Error(errMsg);
|
||
}
|
||
alert('SMS de debug envoyé.');
|
||
}).catch((err) => {
|
||
console.error('[DEBUG_SMS] Échec envoi:', err);
|
||
alert('Échec envoi SMS de debug: ' + (err?.message || 'Erreur inconnue'));
|
||
});
|
||
};
|
||
}
|
||
|
||
// Bouton Signaler un incident
|
||
const reportIncidentBtn = document.getElementById('reportIncidentBtn');
|
||
if (reportIncidentBtn) {
|
||
reportIncidentBtn.onclick = async function() {
|
||
const userCanView = window.crviPermissions && window.crviPermissions.can_view;
|
||
if (!userCanView) {
|
||
console.warn('Utilisateur non autorisé à voir les événements');
|
||
return;
|
||
}
|
||
|
||
if (!currentEventData || !currentEventData.id) {
|
||
notifyError('Aucun événement sélectionné');
|
||
return;
|
||
}
|
||
|
||
// Récupérer le type d'événement
|
||
const eventType = currentEventData.type || currentEventData.extendedProps?.type || 'individuel';
|
||
const isGroupe = eventType === 'groupe';
|
||
|
||
// Pré-remplir le formulaire de la modal d'incident
|
||
const incidentModal = document.getElementById('declarationIncidentModal');
|
||
if (incidentModal) {
|
||
const eventIdInput = document.getElementById('incident_event_id');
|
||
const eventTypeInput = document.getElementById('incident_event_type');
|
||
const sectionIndividuel = document.getElementById('incident_individuel_section');
|
||
const sectionGroupe = document.getElementById('incident_groupe_section');
|
||
const beneficiaireIdInput = document.getElementById('incident_beneficiaire_id');
|
||
const beneficiaireNomInput = document.getElementById('incident_beneficiaire_nom');
|
||
const resumeInput = document.getElementById('resume_incident');
|
||
const commentaireInput = document.getElementById('commentaire_incident');
|
||
const commentaireGroupeInput = document.getElementById('commentaire_incident_groupe');
|
||
|
||
// Remplir les champs communs
|
||
if (eventIdInput) {
|
||
eventIdInput.value = currentEventData.id;
|
||
}
|
||
if (eventTypeInput) {
|
||
eventTypeInput.value = eventType;
|
||
}
|
||
|
||
// Afficher/masquer les sections selon le type
|
||
if (isGroupe) {
|
||
// Mode groupe : afficher uniquement la section commentaire
|
||
if (sectionIndividuel) {
|
||
sectionIndividuel.style.display = 'none';
|
||
}
|
||
if (sectionGroupe) {
|
||
sectionGroupe.style.display = 'block';
|
||
}
|
||
// Réinitialiser les champs de la section individuel
|
||
if (beneficiaireIdInput) {
|
||
beneficiaireIdInput.value = '';
|
||
}
|
||
if (beneficiaireNomInput) {
|
||
beneficiaireNomInput.value = '';
|
||
}
|
||
if (resumeInput) {
|
||
resumeInput.value = '';
|
||
resumeInput.removeAttribute('required');
|
||
}
|
||
if (commentaireInput) {
|
||
commentaireInput.value = '';
|
||
}
|
||
if (commentaireGroupeInput) {
|
||
commentaireGroupeInput.value = '';
|
||
commentaireGroupeInput.setAttribute('required', 'required');
|
||
}
|
||
} else {
|
||
// Mode individuel : afficher la section avec nom du bénéficiaire (pas de chargement de liste)
|
||
if (sectionIndividuel) {
|
||
sectionIndividuel.style.display = 'block';
|
||
}
|
||
if (sectionGroupe) {
|
||
sectionGroupe.style.display = 'none';
|
||
}
|
||
// Réinitialiser les champs
|
||
if (commentaireGroupeInput) {
|
||
commentaireGroupeInput.value = '';
|
||
commentaireGroupeInput.removeAttribute('required');
|
||
}
|
||
// Récupérer les informations du bénéficiaire depuis les données de l'événement
|
||
const beneficiaireId = currentEventData.id_beneficiaire || currentEventData.extendedProps?.id_beneficiaire;
|
||
const beneficiaireNom = currentEventData.beneficiaire?.nom_complet ||
|
||
currentEventData.extendedProps?.beneficiaire?.nom_complet ||
|
||
(currentEventData.beneficiaire_nom || currentEventData.extendedProps?.beneficiaire_nom) ||
|
||
'Non spécifié';
|
||
|
||
// Afficher le nom du bénéficiaire et l'ID dans les champs cachés
|
||
const beneficiaireNomInput = document.getElementById('incident_beneficiaire_nom');
|
||
const beneficiaireIdInput = document.getElementById('incident_beneficiaire_id');
|
||
|
||
if (beneficiaireNomInput) {
|
||
beneficiaireNomInput.value = beneficiaireNom;
|
||
}
|
||
if (beneficiaireIdInput) {
|
||
beneficiaireIdInput.value = beneficiaireId || '';
|
||
}
|
||
|
||
if (resumeInput) {
|
||
resumeInput.value = '';
|
||
resumeInput.setAttribute('required', 'required');
|
||
}
|
||
if (commentaireInput) {
|
||
commentaireInput.value = '';
|
||
}
|
||
}
|
||
|
||
// Préserver les données de la modale principale avant de la fermer
|
||
preserveModalData();
|
||
|
||
// Fermer le modal principal d'événement s'il est ouvert
|
||
const eventModal = document.getElementById('eventModal');
|
||
if (eventModal) {
|
||
const eventBsModal = bootstrap.Modal.getInstance(eventModal);
|
||
if (eventBsModal) {
|
||
eventBsModal.hide();
|
||
}
|
||
}
|
||
|
||
// Attendre un peu que le modal principal se ferme avant d'ouvrir le nouveau
|
||
setTimeout(() => {
|
||
// Ouvrir le modal d'incident
|
||
if (window.bootstrap && window.bootstrap.Modal) {
|
||
const bsModal = new window.bootstrap.Modal(incidentModal);
|
||
bsModal.show();
|
||
|
||
// Ajouter un event listener pour rouvrir le modal principal quand on ferme
|
||
incidentModal.addEventListener('hidden.bs.modal', function() {
|
||
// Rouvrir le modal principal d'événement avec les données préservées
|
||
if (eventModal && window.bootstrap && window.bootstrap.Modal) {
|
||
const newEventModal = new window.bootstrap.Modal(eventModal);
|
||
newEventModal.show();
|
||
}
|
||
}, { once: true }); // Une seule fois
|
||
}
|
||
}, 300); // Délai pour la transition
|
||
}
|
||
};
|
||
}
|
||
|
||
// Bouton Enregistrer incident
|
||
const saveIncidentBtn = document.getElementById('saveIncidentBtn');
|
||
if (saveIncidentBtn) {
|
||
saveIncidentBtn.onclick = async function() {
|
||
await saveIncident();
|
||
};
|
||
}
|
||
|
||
// Bouton Voir les incidents
|
||
const viewIncidentsBtn = document.getElementById('viewIncidentsBtn');
|
||
if (viewIncidentsBtn) {
|
||
viewIncidentsBtn.onclick = async function() {
|
||
const eventId = currentEventData?.id || currentEventData?.extendedProps?.id;
|
||
|
||
if (!eventId) {
|
||
notifyError('ID de l\'événement introuvable');
|
||
return;
|
||
}
|
||
|
||
await openIncidentsViewModal(eventId);
|
||
};
|
||
}
|
||
|
||
// Bouton Historique bénéficiaire
|
||
const historiqueBtn = document.getElementById('showBeneficiaireHistoriqueBtn');
|
||
if (historiqueBtn) {
|
||
historiqueBtn.onclick = async function() {
|
||
const beneficiaireId = historiqueBtn.getAttribute('data-benef') ||
|
||
currentEventData?.id_beneficiaire ||
|
||
currentEventData?.extendedProps?.id_beneficiaire;
|
||
|
||
if (!beneficiaireId) {
|
||
notifyError('ID du bénéficiaire introuvable');
|
||
return;
|
||
}
|
||
|
||
const historiqueModal = document.getElementById('beneficiaireHistoriqueModal');
|
||
if (!historiqueModal) {
|
||
notifyError('Modal d\'historique introuvable');
|
||
return;
|
||
}
|
||
|
||
// Afficher le spinner de chargement
|
||
const timelineContainer = document.getElementById('historiqueTimeline');
|
||
if (timelineContainer) {
|
||
timelineContainer.innerHTML = `
|
||
<div class="text-center py-4">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Chargement...</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Préserver les données de la modale principale avant de la fermer
|
||
preserveModalData();
|
||
|
||
// Fermer le modal principal d'événement s'il est ouvert
|
||
const eventModal = document.getElementById('eventModal');
|
||
if (eventModal) {
|
||
const eventBsModal = bootstrap.Modal.getInstance(eventModal);
|
||
if (eventBsModal) {
|
||
eventBsModal.hide();
|
||
}
|
||
}
|
||
|
||
// Attendre un peu que le modal principal se ferme avant d'ouvrir le nouveau
|
||
setTimeout(() => {
|
||
// Ouvrir le modal d'historique
|
||
if (window.bootstrap && window.bootstrap.Modal) {
|
||
const bsModal = new window.bootstrap.Modal(historiqueModal);
|
||
bsModal.show();
|
||
|
||
// Ajouter un event listener pour rouvrir le modal principal quand on ferme
|
||
historiqueModal.addEventListener('hidden.bs.modal', function() {
|
||
// Rouvrir le modal principal d'événement avec les données préservées
|
||
if (eventModal && window.bootstrap && window.bootstrap.Modal) {
|
||
const newEventModal = new window.bootstrap.Modal(eventModal);
|
||
newEventModal.show();
|
||
}
|
||
}, { once: true }); // Une seule fois
|
||
}
|
||
|
||
// Charger l'historique après l'ouverture du modal
|
||
loadBeneficiaireHistorique(parseInt(beneficiaireId))
|
||
.then(historiqueData => {
|
||
// Afficher les données
|
||
displayHistoriqueTimeline(historiqueData);
|
||
})
|
||
.catch(error => {
|
||
console.error('Erreur lors du chargement de l\'historique:', error);
|
||
if (timelineContainer) {
|
||
timelineContainer.innerHTML = `
|
||
<div class="alert alert-danger text-center">
|
||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||
Erreur lors du chargement de l'historique.
|
||
</div>
|
||
`;
|
||
}
|
||
});
|
||
}, 300); // Délai pour la transition
|
||
};
|
||
}
|
||
|
||
// Bouton Valider présences (modal de validation des présences pour les rendez-vous de groupe)
|
||
const savePresencesBtn = document.getElementById('savePresencesBtn');
|
||
if (savePresencesBtn) {
|
||
savePresencesBtn.onclick = async function() {
|
||
const form = document.getElementById('eventCheckPresenceForm');
|
||
if (!form) {
|
||
notifyError('Formulaire de présences introuvable');
|
||
return;
|
||
}
|
||
|
||
const eventIdInput = document.getElementById('presence_event_id');
|
||
const eventId = eventIdInput ? parseInt(eventIdInput.value, 10) : null;
|
||
|
||
if (!eventId) {
|
||
notifyError('ID d\'événement manquant');
|
||
return;
|
||
}
|
||
|
||
// Récupérer toutes les lignes de présence
|
||
const rows = document.querySelectorAll('#presence_rows tr');
|
||
const presenceData = [];
|
||
|
||
rows.forEach((row, index) => {
|
||
const checkbox = row.querySelector(`input[type="checkbox"][id^="presence_${index}"]`);
|
||
const nomInput = row.querySelector(`input[id="presence_nom_${index}"]`);
|
||
const prenomInput = row.querySelector(`input[id="presence_prenom_${index}"]`);
|
||
const beneficiaireIdInput = row.querySelector(`input[id="presence_beneficiaire_id_${index}"]`);
|
||
|
||
if (nomInput && prenomInput) {
|
||
const nom = nomInput.value.trim();
|
||
const prenom = prenomInput.value.trim();
|
||
const isPresent = checkbox ? checkbox.checked : false;
|
||
const beneficiaireId = beneficiaireIdInput ? beneficiaireIdInput.value : null;
|
||
|
||
// Ne pas inclure les lignes vides (nom et prénom vides)
|
||
if (nom || prenom) {
|
||
const presenceItem = {
|
||
nom: nom,
|
||
prenom: prenom,
|
||
is_present: isPresent
|
||
};
|
||
|
||
// Ajouter beneficiaire_id si un bénéficiaire existant a été sélectionné
|
||
if (beneficiaireId) {
|
||
presenceItem.beneficiaire_id = parseInt(beneficiaireId, 10);
|
||
}
|
||
|
||
presenceData.push(presenceItem);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (presenceData.length === 0) {
|
||
notifyError('Veuillez remplir au moins une ligne de présence');
|
||
return;
|
||
}
|
||
|
||
// Afficher un overlay de chargement
|
||
const modal = document.getElementById('eventCheckPresenceModal');
|
||
let overlayTarget = modal ? modal.querySelector('.modal-content') : null;
|
||
try {
|
||
if (window.CRVI_OVERLAY && overlayTarget) {
|
||
window.CRVI_OVERLAY.show(overlayTarget);
|
||
}
|
||
await handleGroupPresenceSubmission(eventId, presenceData);
|
||
} finally {
|
||
if (window.CRVI_OVERLAY && overlayTarget) {
|
||
window.CRVI_OVERLAY.hide(overlayTarget);
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// Initialiser la gestion de la case à cocher liste rouge
|
||
initializeListeRougeCheckbox();
|
||
}
|
||
|
||
/**
|
||
* Initialise la gestion de la case à cocher liste rouge
|
||
*/
|
||
function initializeListeRougeCheckbox() {
|
||
const listeRougeCheckbox = document.getElementById('liste_rouge');
|
||
const listeRougeContainer = document.getElementById('liste-rouge-container');
|
||
const beneficiaireSelect = document.getElementById('id_beneficiaire');
|
||
|
||
if (!listeRougeCheckbox || !listeRougeContainer || !beneficiaireSelect) {
|
||
return;
|
||
}
|
||
|
||
// Afficher/masquer la case selon si un bénéficiaire est sélectionné
|
||
function toggleListeRougeVisibility() {
|
||
const beneficiaireId = beneficiaireSelect.value;
|
||
if (beneficiaireId && beneficiaireId !== '') {
|
||
listeRougeContainer.style.display = 'block';
|
||
// Charger l'état actuel du bénéficiaire
|
||
loadListeRougeStatus(beneficiaireId);
|
||
} else {
|
||
listeRougeContainer.style.display = 'none';
|
||
listeRougeCheckbox.checked = false;
|
||
}
|
||
}
|
||
|
||
// Écouter le changement du select bénéficiaire
|
||
beneficiaireSelect.addEventListener('change', toggleListeRougeVisibility);
|
||
|
||
// Variable pour empêcher les appels multiples simultanés
|
||
let isUpdatingListeRouge = false;
|
||
|
||
// Écouter le changement de la case à cocher
|
||
listeRougeCheckbox.addEventListener('change', async function() {
|
||
// Si un appel est déjà en cours, ignorer ce changement
|
||
if (isUpdatingListeRouge) {
|
||
return;
|
||
}
|
||
|
||
const beneficiaireId = beneficiaireSelect.value;
|
||
if (!beneficiaireId || beneficiaireId === '') {
|
||
return;
|
||
}
|
||
|
||
const isChecked = listeRougeCheckbox.checked;
|
||
|
||
try {
|
||
// Marquer qu'un appel est en cours
|
||
isUpdatingListeRouge = true;
|
||
|
||
// Désactiver la case pendant l'appel
|
||
listeRougeCheckbox.disabled = true;
|
||
|
||
// Appel API pour mettre à jour le statut liste rouge
|
||
const response = await apiFetch(`beneficiaires/${beneficiaireId}/liste-rouge`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({
|
||
liste_rouge: isChecked
|
||
})
|
||
});
|
||
|
||
if (response && response.liste_rouge !== undefined) {
|
||
notifySuccess(response.message || (isChecked ? 'Bénéficiaire ajouté à la liste rouge' : 'Bénéficiaire retiré de la liste rouge'));
|
||
} else {
|
||
throw new Error('Réponse invalide de l\'API');
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors de la mise à jour du statut liste rouge:', error);
|
||
// Restaurer l'état précédent en cas d'erreur
|
||
listeRougeCheckbox.checked = !isChecked;
|
||
notifyError('Erreur lors de la mise à jour du statut liste rouge');
|
||
} finally {
|
||
// Réactiver la case et permettre de nouveaux appels
|
||
listeRougeCheckbox.disabled = false;
|
||
isUpdatingListeRouge = false;
|
||
}
|
||
});
|
||
|
||
// Initialiser l'état au chargement
|
||
toggleListeRougeVisibility();
|
||
}
|
||
|
||
/**
|
||
* Charge l'état actuel du statut liste rouge pour un bénéficiaire
|
||
* @param {number} beneficiaireId - ID du bénéficiaire
|
||
*/
|
||
async function loadListeRougeStatus(beneficiaireId) {
|
||
const listeRougeCheckbox = document.getElementById('liste_rouge');
|
||
if (!listeRougeCheckbox) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Récupérer les données du bénéficiaire
|
||
const response = await apiFetch(`beneficiaires/${beneficiaireId}`);
|
||
|
||
if (response && response.personne_en_liste_rouge) {
|
||
// Le champ ACF checkbox retourne un tableau, on vérifie si "oui" est présent
|
||
const isListeRouge = Array.isArray(response.personne_en_liste_rouge)
|
||
? response.personne_en_liste_rouge.includes('oui')
|
||
: response.personne_en_liste_rouge === 'oui' || response.personne_en_liste_rouge === true;
|
||
|
||
listeRougeCheckbox.checked = isListeRouge;
|
||
} else {
|
||
listeRougeCheckbox.checked = false;
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement du statut liste rouge:', error);
|
||
listeRougeCheckbox.checked = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Charge la liste des bénéficiaires dans un select
|
||
* @param {HTMLElement} selectElement - L'élément select à remplir
|
||
*/
|
||
async function loadBeneficiairesForIncident(selectElement) {
|
||
if (!selectElement) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Vider le select (garder seulement l'option par défaut)
|
||
selectElement.innerHTML = '<option value="">Sélectionner un bénéficiaire...</option>';
|
||
|
||
// Charger les bénéficiaires depuis l'API
|
||
const disponibilites = await apiFetch('agenda/disponibilites');
|
||
|
||
if (disponibilites && disponibilites.beneficiaires && Array.isArray(disponibilites.beneficiaires)) {
|
||
disponibilites.beneficiaires.forEach(benef => {
|
||
const option = document.createElement('option');
|
||
option.value = benef.id;
|
||
option.textContent = benef.nom;
|
||
selectElement.appendChild(option);
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement des bénéficiaires:', error);
|
||
notifyError('Erreur lors du chargement de la liste des bénéficiaires');
|
||
}
|
||
}
|
||
|
||
// Fonction pour sauvegarder un incident
|
||
async function saveIncident() {
|
||
const form = document.getElementById('declarationIncidentForm');
|
||
if (!form) {
|
||
notifyError('Formulaire d\'incident introuvable');
|
||
return;
|
||
}
|
||
|
||
// Récupérer le type d'événement
|
||
const eventType = document.getElementById('incident_event_type')?.value || 'individuel';
|
||
const isGroupe = eventType === 'groupe';
|
||
const eventId = document.getElementById('incident_event_id')?.value;
|
||
|
||
let data = {
|
||
event_id: parseInt(eventId)
|
||
};
|
||
|
||
if (isGroupe) {
|
||
// Mode groupe : seulement commentaire
|
||
const commentaireGroupe = document.getElementById('commentaire_incident_groupe')?.value;
|
||
|
||
if (!commentaireGroupe || !commentaireGroupe.trim()) {
|
||
notifyError('Veuillez remplir le commentaire de l\'incident');
|
||
return;
|
||
}
|
||
|
||
data.commentaire_incident = commentaireGroupe.trim();
|
||
data.resume_incident = commentaireGroupe.trim().substring(0, 255); // Utiliser les 255 premiers caractères comme résumé
|
||
} else {
|
||
// Mode individuel : bénéficiaire + résumé + commentaire
|
||
const beneficiaireId = document.getElementById('incident_beneficiaire_id')?.value;
|
||
const resumeIncident = document.getElementById('resume_incident')?.value;
|
||
const commentaireIncident = document.getElementById('commentaire_incident')?.value;
|
||
|
||
// Validation
|
||
if (!beneficiaireId || !resumeIncident || !resumeIncident.trim()) {
|
||
notifyError('Veuillez remplir tous les champs obligatoires');
|
||
return;
|
||
}
|
||
|
||
data.beneficiaire_id = parseInt(beneficiaireId);
|
||
data.resume_incident = resumeIncident.trim();
|
||
data.commentaire_incident = commentaireIncident ? commentaireIncident.trim() : null;
|
||
}
|
||
|
||
if (!eventId) {
|
||
notifyError('ID d\'événement manquant');
|
||
return;
|
||
}
|
||
|
||
const incidentModal = document.getElementById('declarationIncidentModal');
|
||
const overlayTarget = incidentModal?.querySelector('.modal-content') || incidentModal;
|
||
|
||
try {
|
||
if (window.CRVI_OVERLAY && overlayTarget) {
|
||
window.CRVI_OVERLAY.show(overlayTarget);
|
||
}
|
||
|
||
// Appel API
|
||
const response = await apiFetch('incidents', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
// Si on arrive ici sans exception, c'est un succès
|
||
notifySuccess('Incident signalé avec succès');
|
||
|
||
// Fermer la modal
|
||
const bsModal = bootstrap.Modal.getInstance(incidentModal);
|
||
if (bsModal) {
|
||
bsModal.hide();
|
||
}
|
||
|
||
// Réinitialiser le formulaire
|
||
form.reset();
|
||
|
||
// Recharger et afficher le bouton incidents si on est encore dans le modal événement
|
||
if (currentEventData?.id) {
|
||
await checkAndDisplayIncidentsButton(currentEventData.id);
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors de la sauvegarde de l\'incident:', error);
|
||
notifyError(error.message || 'Erreur lors de la sauvegarde de l\'incident');
|
||
} finally {
|
||
if (window.CRVI_OVERLAY && overlayTarget) {
|
||
window.CRVI_OVERLAY.hide(overlayTarget);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Charge les incidents d'un événement
|
||
* @param {number} eventId - ID de l'événement
|
||
* @returns {Promise<Array>} - Tableau des incidents
|
||
*/
|
||
async function loadEventIncidents(eventId) {
|
||
try {
|
||
const incidents = await apiFetch(`events/${eventId}/incidents`);
|
||
return incidents || [];
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement des incidents:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ouvre la modal d'incidents en mode affichage
|
||
* @param {number} eventId - ID de l'événement
|
||
*/
|
||
async function openIncidentsViewModal(eventId) {
|
||
const incidentModal = document.getElementById('declarationIncidentModal');
|
||
const modalTitle = document.getElementById('declarationIncidentModalLabel');
|
||
const viewSection = document.getElementById('incidentsViewSection');
|
||
const formSection = document.getElementById('incidentsFormSection');
|
||
const viewFooter = document.getElementById('incidentsViewFooter');
|
||
const formFooter = document.getElementById('incidentsFormFooter');
|
||
const listContainer = document.getElementById('incidentsListContainer');
|
||
|
||
if (!incidentModal || !viewSection || !formSection || !listContainer) {
|
||
notifyError('Modal d\'incidents introuvable');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Charger les incidents
|
||
const incidents = await loadEventIncidents(eventId);
|
||
|
||
if (incidents.length === 0) {
|
||
listContainer.innerHTML = '<p class="text-muted">Aucun incident signalé pour cet événement.</p>';
|
||
} else {
|
||
// Afficher les incidents
|
||
let html = '<div class="list-group">';
|
||
incidents.forEach((incident, index) => {
|
||
const createdAt = incident.created_at ? new Date(incident.created_at).toLocaleDateString('fr-FR') : 'Date inconnue';
|
||
html += `
|
||
<div class="list-group-item">
|
||
<div class="d-flex w-100 justify-content-between">
|
||
<h6 class="mb-1">Incident #${index + 1}</h6>
|
||
<small class="text-muted">${createdAt}</small>
|
||
</div>
|
||
${incident.resume_incident ? `<p class="mb-1"><strong>Résumé :</strong> ${incident.resume_incident}</p>` : ''}
|
||
${incident.commentaire_incident ? `<p class="mb-0 text-muted">${incident.commentaire_incident}</p>` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
listContainer.innerHTML = html;
|
||
}
|
||
|
||
// Changer le titre de la modal
|
||
if (modalTitle) {
|
||
modalTitle.innerHTML = '<i class="fas fa-eye me-2"></i>Détail des incidents';
|
||
}
|
||
|
||
// Afficher la section de vue, masquer le formulaire
|
||
viewSection.style.display = 'block';
|
||
formSection.style.display = 'none';
|
||
viewFooter.style.display = 'block';
|
||
formFooter.style.display = 'none';
|
||
|
||
// Ouvrir la modal
|
||
const bsModal = new bootstrap.Modal(incidentModal);
|
||
bsModal.show();
|
||
|
||
// Réinitialiser au mode formulaire quand la modal se ferme
|
||
incidentModal.addEventListener('hidden.bs.modal', function resetModalMode() {
|
||
viewSection.style.display = 'none';
|
||
formSection.style.display = 'block';
|
||
viewFooter.style.display = 'none';
|
||
formFooter.style.display = 'block';
|
||
if (modalTitle) {
|
||
modalTitle.innerHTML = '<i class="fas fa-exclamation-triangle me-2"></i>Signaler un incident';
|
||
}
|
||
// Retirer l'écouteur pour éviter les duplications
|
||
incidentModal.removeEventListener('hidden.bs.modal', resetModalMode);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur lors de l\'ouverture de la modal d\'incidents:', error);
|
||
notifyError('Erreur lors du chargement des incidents');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vérifie si l'événement a des incidents et affiche le bouton en conséquence
|
||
* @param {number} eventId - ID de l'événement
|
||
*/
|
||
async function checkAndDisplayIncidentsButton(eventId) {
|
||
const viewIncidentsBtn = document.getElementById('viewIncidentsBtn');
|
||
|
||
if (!viewIncidentsBtn) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const incidents = await loadEventIncidents(eventId);
|
||
|
||
if (incidents.length > 0) {
|
||
viewIncidentsBtn.style.display = 'inline-block';
|
||
// Mettre à jour le texte du bouton avec le nombre
|
||
const icon = viewIncidentsBtn.querySelector('i');
|
||
const iconHtml = icon ? icon.outerHTML : '<i class="fas fa-eye me-1"></i>';
|
||
viewIncidentsBtn.innerHTML = `${iconHtml}Détail incident(s) (${incidents.length})`;
|
||
} else {
|
||
viewIncidentsBtn.style.display = 'none';
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors de la vérification des incidents:', error);
|
||
viewIncidentsBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Fonction pour synchroniser l'heure de fin avec l'heure de début (+1h si type individuel)
|
||
function syncHeureFin() {
|
||
const heureRdvInput = document.getElementById('heure_rdv');
|
||
const heureFinInput = document.getElementById('heure_fin');
|
||
const typeInput = document.getElementById('type');
|
||
|
||
if (!heureRdvInput || !heureFinInput || !typeInput) return;
|
||
if (typeInput.value !== 'individuel') return;
|
||
const heureDebut = heureRdvInput.value;
|
||
if (!heureDebut) return;
|
||
|
||
// Utiliser la fonction calculateHeureFin pour la cohérence
|
||
const heureFinStr = calculateHeureFin(heureDebut);
|
||
heureFinInput.value = heureFinStr;
|
||
// console.log('Heure de fin synchronisée (+1h) avec heure de début:', heureFinStr);
|
||
}
|
||
|
||
// Fonction pour activer la synchronisation automatique des dates et heures
|
||
function enableDateSynchronization() {
|
||
const dateRdvInput = document.getElementById('date_rdv');
|
||
const dateFinInput = document.getElementById('date_fin');
|
||
const heureRdvInput = document.getElementById('heure_rdv');
|
||
const heureFinInput = document.getElementById('heure_fin');
|
||
const typeInput = document.getElementById('type');
|
||
|
||
// Synchronisation date fin
|
||
if (dateRdvInput && dateFinInput) {
|
||
dateRdvInput.removeEventListener('change', syncDateFin);
|
||
dateRdvInput.addEventListener('change', syncDateFin);
|
||
}
|
||
|
||
// Synchronisation heure fin (+1h si type individuel)
|
||
if (heureRdvInput && heureFinInput && typeInput) {
|
||
heureRdvInput.removeEventListener('change', syncHeureFin);
|
||
heureRdvInput.addEventListener('change', syncHeureFin);
|
||
}
|
||
|
||
// Sélectionner 'individuel' par défaut en création
|
||
if (typeInput && currentMode === 'create') {
|
||
if (!typeInput.value) {
|
||
typeInput.value = 'individuel';
|
||
typeInput.dispatchEvent(new Event('change'));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fonction pour désactiver la synchronisation automatique des dates et heures
|
||
function disableDateSynchronization() {
|
||
const dateRdvInput = document.getElementById('date_rdv');
|
||
const heureRdvInput = document.getElementById('heure_rdv');
|
||
if (dateRdvInput) {
|
||
dateRdvInput.removeEventListener('change', syncDateFin);
|
||
}
|
||
if (heureRdvInput) {
|
||
heureRdvInput.removeEventListener('change', syncHeureFin);
|
||
}
|
||
// console.log('Synchronisation des dates et heures désactivée');
|
||
}
|
||
|
||
// Fonction pour synchroniser la date de fin avec la date de début
|
||
function syncDateFin() {
|
||
const dateRdvInput = document.getElementById('date_rdv');
|
||
const dateFinInput = document.getElementById('date_fin');
|
||
if (dateRdvInput && dateFinInput && dateRdvInput.value) {
|
||
dateFinInput.value = dateRdvInput.value;
|
||
}
|
||
}
|
||
|
||
// Fonction pour empêcher la sélection de dates dans le passé
|
||
function preventPastDates() {
|
||
const dateRdvInput = document.getElementById('date_rdv');
|
||
const dateFinInput = document.getElementById('date_fin');
|
||
|
||
if (dateRdvInput) {
|
||
// Définir la date minimale à aujourd'hui
|
||
const today = new Date().toISOString().split('T')[0];
|
||
dateRdvInput.setAttribute('min', today);
|
||
|
||
// Écouter les changements pour mettre à jour la date de fin
|
||
dateRdvInput.addEventListener('change', function() {
|
||
const selectedDate = this.value;
|
||
if (dateFinInput && selectedDate) {
|
||
dateFinInput.setAttribute('min', selectedDate);
|
||
// Si la date de fin est antérieure à la nouvelle date de début, la synchroniser
|
||
if (dateFinInput.value && dateFinInput.value < selectedDate) {
|
||
dateFinInput.value = selectedDate;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
if (dateFinInput) {
|
||
// La date de fin ne peut pas être antérieure à la date de début
|
||
dateRdvInput?.addEventListener('change', function() {
|
||
const startDate = this.value;
|
||
if (startDate) {
|
||
dateFinInput.setAttribute('min', startDate);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Fonction pour désactiver les dates et créneaux indisponibles
|
||
async function disableUnavailableDates() {
|
||
const dateRdvInput = document.getElementById('date_rdv');
|
||
const dateFinInput = document.getElementById('date_fin');
|
||
const heureRdvInput = document.getElementById('heure_rdv');
|
||
const heureFinInput = document.getElementById('heure_fin');
|
||
|
||
if (!dateRdvInput) return;
|
||
|
||
// Éviter les appels multiples en vérifiant si on a déjà les données
|
||
if (dateRdvInput.hasAttribute('data-availability-loaded')) {
|
||
// console.log('Données de disponibilité déjà chargées');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Récupérer les disponibilités des entités sélectionnées
|
||
const intervenantId = document.getElementById('id_intervenant')?.value;
|
||
const traducteurId = document.getElementById('id_traducteur')?.value;
|
||
const localId = document.getElementById('id_local')?.value;
|
||
|
||
if (!intervenantId || !traducteurId || !localId) {
|
||
// console.log('Toutes les entités ne sont pas sélectionnées, pas de restriction de dates');
|
||
return;
|
||
}
|
||
|
||
// Appel API pour récupérer les disponibilités
|
||
const response = await apiFetch('agenda/disponibilites', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
id_intervenant: intervenantId,
|
||
id_traducteur: traducteurId,
|
||
id_local: localId,
|
||
date_debut: new Date().toISOString().split('T')[0], // À partir d'aujourd'hui
|
||
date_fin: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] // +1 an
|
||
})
|
||
});
|
||
|
||
if (response.success && response.data) {
|
||
// Créer une liste des dates et créneaux indisponibles
|
||
const unavailableDates = response.data.unavailable_dates || [];
|
||
const unavailableSlots = response.data.unavailable_slots || [];
|
||
|
||
// Ajouter un attribut data pour stocker les données indisponibles
|
||
dateRdvInput.setAttribute('data-unavailable-dates', JSON.stringify(unavailableDates));
|
||
dateRdvInput.setAttribute('data-unavailable-slots', JSON.stringify(unavailableSlots));
|
||
dateRdvInput.setAttribute('data-availability-loaded', 'true');
|
||
|
||
// Ajouter un event listener pour vérifier lors de la sélection de date
|
||
if (!dateRdvInput.hasAttribute('data-date-listener-added')) {
|
||
dateRdvInput.addEventListener('change', function() {
|
||
const selectedDate = this.value;
|
||
const unavailableDates = JSON.parse(this.getAttribute('data-unavailable-dates') || '[]');
|
||
|
||
if (unavailableDates.includes(selectedDate)) {
|
||
notifyError('Cette date n\'est pas disponible pour les entités sélectionnées');
|
||
this.value = '';
|
||
return;
|
||
}
|
||
|
||
// Si la date est disponible, mettre à jour les heures disponibles
|
||
updateAvailableHours(selectedDate);
|
||
});
|
||
dateRdvInput.setAttribute('data-date-listener-added', 'true');
|
||
}
|
||
|
||
// Ajouter un event listener pour vérifier lors de la sélection d'heure
|
||
if (heureRdvInput && !heureRdvInput.hasAttribute('data-hour-listener-added')) {
|
||
heureRdvInput.addEventListener('change', function() {
|
||
const selectedDate = dateRdvInput.value;
|
||
const selectedHour = this.value;
|
||
const unavailableSlots = JSON.parse(dateRdvInput.getAttribute('data-unavailable-slots') || '[]');
|
||
|
||
if (selectedDate && selectedHour) {
|
||
const slotKey = selectedDate + ' ' + selectedHour;
|
||
if (unavailableSlots.includes(slotKey)) {
|
||
notifyError('Ce créneau horaire n\'est pas disponible pour les entités sélectionnées');
|
||
this.value = '';
|
||
return;
|
||
}
|
||
}
|
||
});
|
||
heureRdvInput.setAttribute('data-hour-listener-added', 'true');
|
||
}
|
||
|
||
// console.log('Dates indisponibles configurées:', unavailableDates);
|
||
// console.log('Créneaux indisponibles configurés:', unavailableSlots);
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors de la récupération des disponibilités:', error);
|
||
}
|
||
}
|
||
|
||
// Fonction pour mettre à jour les heures disponibles selon la date sélectionnée
|
||
function updateAvailableHours(selectedDate) {
|
||
const heureRdvInput = document.getElementById('heure_rdv');
|
||
const heureFinInput = document.getElementById('heure_fin');
|
||
|
||
if (!heureRdvInput) return;
|
||
|
||
const unavailableSlots = JSON.parse(document.getElementById('date_rdv').getAttribute('data-unavailable-slots') || '[]');
|
||
|
||
// Filtrer les créneaux indisponibles pour cette date
|
||
const unavailableHoursForDate = unavailableSlots
|
||
.filter(slot => slot.startsWith(selectedDate + ' '))
|
||
.map(slot => slot.split(' ')[1]);
|
||
|
||
// Désactiver les heures indisponibles dans le select
|
||
const options = heureRdvInput.querySelectorAll('option');
|
||
options.forEach(option => {
|
||
if (unavailableHoursForDate.includes(option.value)) {
|
||
option.disabled = true;
|
||
option.style.color = '#ccc';
|
||
} else {
|
||
option.disabled = false;
|
||
option.style.color = '';
|
||
}
|
||
});
|
||
|
||
// Si l'heure actuellement sélectionnée n'est plus disponible, la vider
|
||
if (heureRdvInput.value && unavailableHoursForDate.includes(heureRdvInput.value)) {
|
||
heureRdvInput.value = '';
|
||
notifyError('L\'heure sélectionnée n\'est plus disponible pour cette date');
|
||
}
|
||
}
|
||
|
||
// Fonction pour initialiser les validations de dates
|
||
function initializeDateValidations() {
|
||
// preventPastDates est déjà appelé dans openModal, pas besoin de le rappeler
|
||
// preventPastDates();
|
||
disableUnavailableDates();
|
||
}
|
||
|
||
// Debounce pour filterTraducteursByLangue
|
||
let filterTraducteursTimeout = null;
|
||
let isFilteringTraducteurs = false;
|
||
|
||
// Fonction pour filtrer les traducteurs selon la langue sélectionnée
|
||
function filterTraducteursByLangue() {
|
||
// Ignorer si on est en train de mettre à jour les selects (évite les boucles)
|
||
if (isUpdatingSelects) {
|
||
if (DEBUG_SELECTS) {
|
||
console.log('⏸️ filterTraducteursByLangue ignoré (mise à jour en cours)');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Annuler le timeout précédent
|
||
if (filterTraducteursTimeout) {
|
||
clearTimeout(filterTraducteursTimeout);
|
||
}
|
||
|
||
// Si déjà en cours d'exécution, ignorer
|
||
if (isFilteringTraducteurs) {
|
||
if (DEBUG_SELECTS) {
|
||
console.log('⏸️ filterTraducteursByLangue ignoré (déjà en cours)');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Créer un nouveau timeout
|
||
filterTraducteursTimeout = setTimeout(() => {
|
||
_executeFilterTraducteurs();
|
||
}, 100);
|
||
}
|
||
|
||
function _executeFilterTraducteurs() {
|
||
if (isFilteringTraducteurs) {
|
||
return;
|
||
}
|
||
|
||
isFilteringTraducteurs = true;
|
||
|
||
try {
|
||
const langueSelect = document.getElementById('langue');
|
||
const traducteurSelect = document.getElementById('id_traducteur');
|
||
|
||
if (!langueSelect || !traducteurSelect || traducteurSelect.tagName !== 'SELECT') {
|
||
return;
|
||
}
|
||
|
||
const selectedLangue = langueSelect.value;
|
||
const currentTraducteurValue = traducteurSelect.value;
|
||
|
||
// Si aucune langue n'est sélectionnée, afficher tous les traducteurs
|
||
if (!selectedLangue) {
|
||
if (!traducteurSelect.options) {
|
||
console.warn('⚠️ traducteurSelect.options est undefined');
|
||
return;
|
||
}
|
||
Array.from(traducteurSelect.options).forEach(option => {
|
||
if (option.value === '') {
|
||
option.style.display = '';
|
||
} else {
|
||
option.style.display = '';
|
||
option.disabled = false;
|
||
}
|
||
});
|
||
|
||
// Mettre à jour Select2 si initialisé (utiliser change.select2 pour éviter les boucles)
|
||
if (jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) {
|
||
jQuery(traducteurSelect).trigger('change.select2');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Récupérer le slug de la langue depuis l'option sélectionnée
|
||
const langueOption = langueSelect.options[langueSelect.selectedIndex];
|
||
let langueSlug = null;
|
||
|
||
// Si l'option a un attribut data-slug, l'utiliser
|
||
if (langueOption && langueOption.dataset.slug) {
|
||
langueSlug = langueOption.dataset.slug;
|
||
} else {
|
||
// Si pas de slug disponible, on ne peut pas filtrer correctement
|
||
console.warn('Aucun slug disponible pour la langue sélectionnée');
|
||
return;
|
||
}
|
||
|
||
// Parcourir toutes les options du select traducteur
|
||
if (!traducteurSelect.options) {
|
||
console.warn('⚠️ traducteurSelect.options est undefined');
|
||
return;
|
||
}
|
||
Array.from(traducteurSelect.options).forEach(option => {
|
||
if (option.value === '') {
|
||
// Garder l'option "Sélectionner..." toujours visible
|
||
option.style.display = '';
|
||
return;
|
||
}
|
||
|
||
// Récupérer les langues du traducteur depuis l'attribut data-langue
|
||
const traducteurLangues = option.getAttribute('data-langue');
|
||
|
||
// Exception : garder l'option actuellement sélectionnée visible même si elle ne correspond pas
|
||
const isCurrentlySelected = option.value === currentTraducteurValue;
|
||
|
||
if (!traducteurLangues) {
|
||
// Si pas d'attribut data-langue, afficher l'option (pour compatibilité)
|
||
option.style.display = isCurrentlySelected ? '' : 'none';
|
||
option.disabled = !isCurrentlySelected;
|
||
} else {
|
||
// Vérifier si la langue sélectionnée est dans les langues du traducteur
|
||
const languesArray = traducteurLangues.split(',');
|
||
const langueMatches = languesArray.some(lang => {
|
||
// Comparer le slug exact ou l'ID
|
||
return lang.trim() === langueSlug || lang.trim() === selectedLangue;
|
||
});
|
||
|
||
if (langueMatches || isCurrentlySelected) {
|
||
option.style.display = '';
|
||
option.disabled = false;
|
||
} else {
|
||
option.style.display = 'none';
|
||
option.disabled = true;
|
||
}
|
||
}
|
||
});
|
||
|
||
// Si le traducteur actuellement sélectionné ne correspond plus à la langue, le vider
|
||
if (currentTraducteurValue && selectedLangue) {
|
||
const currentOption = traducteurSelect.options[traducteurSelect.selectedIndex];
|
||
if (currentOption && currentOption.value !== '' && currentOption.style.display === 'none') {
|
||
traducteurSelect.value = '';
|
||
if (jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) {
|
||
jQuery(traducteurSelect).val('').trigger('change.select2');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Mettre à jour Select2 si initialisé (utiliser change.select2 pour éviter les boucles)
|
||
if (jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) {
|
||
jQuery(traducteurSelect).trigger('change.select2');
|
||
}
|
||
} finally {
|
||
isFilteringTraducteurs = false;
|
||
}
|
||
}
|
||
|
||
// Rafraîchit les selects si l'utilisateur change date/heure/type
|
||
['date_rdv', 'heure_rdv', 'type'].forEach(id => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
element.addEventListener('change', () => {
|
||
// Ignorer si on est en train de mettre à jour les selects (évite les boucles)
|
||
if (isUpdatingSelects) {
|
||
if (DEBUG_SELECTS) {
|
||
console.log(`⏸️ Changement de ${id} ignoré (mise à jour en cours)`);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Ne rafraîchir que si on est en mode edit ou create
|
||
if (currentMode === 'edit' || currentMode === 'create') {
|
||
if (DEBUG_SELECTS) {
|
||
console.log(`📅 Changement de ${id}, rafraîchissement des selects`);
|
||
}
|
||
populateSelects(currentEventData);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Initialiser le filtre de traducteurs par langue
|
||
function initializeLangueFilter() {
|
||
const langueSelect = document.getElementById('langue');
|
||
if (langueSelect && !langueSelect.hasAttribute('data-langue-filter-listener')) {
|
||
langueSelect.addEventListener('change', () => {
|
||
filterTraducteursByLangue();
|
||
});
|
||
langueSelect.setAttribute('data-langue-filter-listener', 'true');
|
||
}
|
||
}
|
||
|
||
// Initialiser le filtre quand le modal s'ouvre
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const eventModal = document.getElementById('eventModal');
|
||
if (eventModal) {
|
||
eventModal.addEventListener('shown.bs.modal', function() {
|
||
initializeLangueFilter();
|
||
// Filtrer immédiatement si une langue est déjà sélectionnée
|
||
filterTraducteursByLangue();
|
||
});
|
||
}
|
||
});
|
||
|
||
// Mettre à jour les disponibilités de dates quand les entités changent
|
||
['id_intervenant', 'id_traducteur', 'id_local'].forEach(id => {
|
||
const element = document.getElementById(id);
|
||
if (element && !element.hasAttribute('data-availability-change-listener')) {
|
||
element.addEventListener('change', () => {
|
||
// Réinitialiser les données de disponibilité pour forcer un nouvel appel
|
||
const dateRdvInput = document.getElementById('date_rdv');
|
||
if (dateRdvInput) {
|
||
dateRdvInput.removeAttribute('data-availability-loaded');
|
||
dateRdvInput.removeAttribute('data-date-listener-added');
|
||
dateRdvInput.removeAttribute('data-unavailable-dates');
|
||
dateRdvInput.removeAttribute('data-unavailable-slots');
|
||
}
|
||
const heureRdvInput = document.getElementById('heure_rdv');
|
||
if (heureRdvInput) {
|
||
heureRdvInput.removeAttribute('data-hour-listener-added');
|
||
}
|
||
disableUnavailableDates();
|
||
});
|
||
element.setAttribute('data-availability-change-listener', 'true');
|
||
}
|
||
});
|
||
|
||
// Initialiser les validations de dates quand le modal s'ouvre
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const eventModal = document.getElementById('eventModal');
|
||
if (eventModal) {
|
||
eventModal.addEventListener('shown.bs.modal', function() {
|
||
initializeDateValidations();
|
||
});
|
||
}
|
||
});
|
||
|
||
// Initialiser le toggle du sélecteur traducteur quand le modal s'ouvre
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const eventModal = document.getElementById('eventModal');
|
||
if (!eventModal) return;
|
||
|
||
function initTraducteurToggle() {
|
||
const useExistingCheckbox = document.getElementById('use_existing_traducteur');
|
||
const tradContainer = document.getElementById('traducteur-select-container');
|
||
const tradSelect = document.getElementById('id_traducteur');
|
||
const nameInput = document.getElementById('nom_traducteur');
|
||
const nameContainer = nameInput ? (nameInput.closest('.mb-2') || nameInput.parentElement) : null;
|
||
if (!useExistingCheckbox || !tradContainer || !tradSelect) return;
|
||
|
||
const applyState = () => {
|
||
if (useExistingCheckbox.checked) {
|
||
tradContainer.classList.remove('d-none');
|
||
tradSelect.setAttribute('required', 'required');
|
||
if (nameContainer) nameContainer.classList.add('d-none');
|
||
if (nameInput) nameInput.setAttribute('disabled', 'disabled');
|
||
} else {
|
||
tradContainer.classList.add('d-none');
|
||
tradSelect.removeAttribute('required');
|
||
tradSelect.value = '';
|
||
if (jQuery(tradSelect).hasClass('select2-hidden-accessible')) {
|
||
jQuery(tradSelect).val('').trigger('change');
|
||
}
|
||
if (nameContainer) nameContainer.classList.remove('d-none');
|
||
if (nameInput) nameInput.removeAttribute('disabled');
|
||
}
|
||
};
|
||
|
||
// Déterminer l'état initial en fonction de la valeur actuelle du select
|
||
// Ne cocher la checkbox que si id_traducteur est supérieur à 0 (pas 0, null, undefined, ou vide)
|
||
const currentId = parseInt(tradSelect.value, 10);
|
||
if (!isNaN(currentId) && currentId > 0) {
|
||
useExistingCheckbox.checked = true;
|
||
} else {
|
||
// Si id_traducteur est 0 ou vide, ne pas cocher la checkbox pour préserver nom_traducteur
|
||
useExistingCheckbox.checked = false;
|
||
}
|
||
|
||
// État initial + écouteurs
|
||
applyState();
|
||
useExistingCheckbox.removeEventListener('change', applyState);
|
||
useExistingCheckbox.addEventListener('change', applyState);
|
||
|
||
// Synchroniser quand la valeur du select change (programmatique ou utilisateur)
|
||
if (!tradSelect.hasAttribute('data-trad-listener-added')) {
|
||
tradSelect.addEventListener('change', () => {
|
||
const v = parseInt(tradSelect.value, 10);
|
||
// Ne cocher la checkbox que si id_traducteur est supérieur à 0
|
||
if (!isNaN(v) && v > 0) {
|
||
useExistingCheckbox.checked = true;
|
||
} else {
|
||
// Si id_traducteur est 0 ou vide, décocher pour préserver nom_traducteur
|
||
useExistingCheckbox.checked = false;
|
||
}
|
||
applyState();
|
||
});
|
||
tradSelect.setAttribute('data-trad-listener-added', 'true');
|
||
}
|
||
}
|
||
|
||
eventModal.addEventListener('shown.bs.modal', function() {
|
||
initTraducteurToggle();
|
||
});
|
||
});
|
||
|
||
// Fonction pour préserver les données actuelles
|
||
export function preserveModalData() {
|
||
preservedMode = currentMode;
|
||
preservedEventData = currentEventData;
|
||
// console.log('Données de modal préservées:', { mode: preservedMode, eventData: preservedEventData });
|
||
}
|
||
|
||
// Fonction pour restaurer les données préservées
|
||
export function restoreModalData() {
|
||
if (preservedMode !== null && preservedEventData !== null) {
|
||
currentMode = preservedMode;
|
||
currentEventData = preservedEventData;
|
||
// console.log('Données de modal restaurées:', { mode: currentMode, eventData: currentEventData });
|
||
|
||
// Réinitialiser les variables de préservation
|
||
preservedMode = null;
|
||
preservedEventData = null;
|
||
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Fonction pour ouvrir la modal de validation des présences pour les rendez-vous de groupe
|
||
export function openCheckPresenceModal(eventData) {
|
||
const modal = document.getElementById('eventCheckPresenceModal');
|
||
if (!modal) {
|
||
console.error('Modal eventCheckPresenceModal non trouvée');
|
||
return;
|
||
}
|
||
|
||
// Préserver les données du modal principal
|
||
preserveModalData();
|
||
|
||
// Récupérer les données de l'événement
|
||
const eventId = eventData?.id || eventData?.extendedProps?.id || null;
|
||
const nbParticipants = eventData?.nb_participants || eventData?.extendedProps?.nb_participants || 0;
|
||
const eventType = eventData?.type || eventData?.extendedProps?.type || '';
|
||
const langue = eventData?.langue || eventData?.extendedProps?.langue || '';
|
||
|
||
// Vérifier que c'est bien un événement de groupe
|
||
if (eventType !== 'groupe') {
|
||
console.warn('openCheckPresenceModal appelée pour un événement non-groupe');
|
||
return;
|
||
}
|
||
|
||
// Vérifier que nb_participants est valide
|
||
const numParticipants = parseInt(nbParticipants, 10);
|
||
if (!numParticipants || numParticipants < 1) {
|
||
notifyError('Le nombre de participants n\'est pas valide');
|
||
return;
|
||
}
|
||
|
||
// Remplir les informations de l'événement
|
||
const eventInfoEl = document.getElementById('presence_event_info');
|
||
if (eventInfoEl) {
|
||
const date = eventData?.date || eventData?.extendedProps?.date || '';
|
||
const heure = eventData?.heure || eventData?.extendedProps?.heure || '';
|
||
eventInfoEl.textContent = `Événement du ${date} à ${heure} - ${numParticipants} participant(s)`;
|
||
}
|
||
|
||
// Définir l'event_id dans le formulaire
|
||
const eventIdInput = document.getElementById('presence_event_id');
|
||
if (eventIdInput) {
|
||
eventIdInput.value = eventId;
|
||
}
|
||
|
||
// Générer les lignes de présence
|
||
const tbody = document.getElementById('presence_rows');
|
||
if (tbody) {
|
||
tbody.innerHTML = '';
|
||
for (let i = 0; i < numParticipants; i++) {
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>
|
||
<input type="checkbox" class="form-check-input" id="presence_${i}" name="presences[${i}][is_present]" checked>
|
||
</td>
|
||
<td style="position: relative;">
|
||
<input type="text" class="form-control presence-nom-input" id="presence_nom_${i}" name="presences[${i}][nom]"
|
||
data-row-index="${i}" autocomplete="off" required>
|
||
<input type="hidden" id="presence_beneficiaire_id_${i}" name="presences[${i}][beneficiaire_id]">
|
||
<div class="autocomplete-suggestions" id="autocomplete_${i}" style="display: none;"></div>
|
||
</td>
|
||
<td>
|
||
<input type="text" class="form-control presence-prenom-input" id="presence_prenom_${i}" name="presences[${i}][prenom]"
|
||
data-row-index="${i}" autocomplete="off" required>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
}
|
||
|
||
// Initialiser l'autocomplétion pour tous les champs nom
|
||
initializePresenceAutocomplete();
|
||
}
|
||
|
||
// Fermer le modal principal d'événement s'il est ouvert
|
||
const eventModal = document.getElementById('eventModal');
|
||
if (eventModal) {
|
||
const eventBsModal = window.bootstrap && window.bootstrap.Modal ? window.bootstrap.Modal.getInstance(eventModal) : null;
|
||
if (eventBsModal) {
|
||
eventBsModal.hide();
|
||
}
|
||
}
|
||
|
||
// Attendre un peu que le modal principal se ferme avant d'ouvrir le nouveau
|
||
setTimeout(() => {
|
||
// Ouvrir le modal de validation des présences
|
||
if (window.bootstrap && window.bootstrap.Modal) {
|
||
const bsModal = new window.bootstrap.Modal(modal);
|
||
bsModal.show();
|
||
|
||
// Ajouter un event listener pour rouvrir le modal principal quand on ferme
|
||
modal.addEventListener('hidden.bs.modal', function() {
|
||
// Rouvrir le modal principal d'événement avec les données préservées
|
||
if (eventModal && window.bootstrap && window.bootstrap.Modal) {
|
||
const newEventModal = new window.bootstrap.Modal(eventModal);
|
||
newEventModal.show();
|
||
}
|
||
}, { once: true }); // Une seule fois
|
||
}
|
||
}, 300); // Délai pour la transition
|
||
}
|
||
|
||
// Fonction pour gérer la soumission du formulaire de présences
|
||
async function handleGroupPresenceSubmission(eventId, presenceData) {
|
||
if (!eventId) {
|
||
notifyError('ID d\'événement manquant');
|
||
return;
|
||
}
|
||
|
||
if (!presenceData || !Array.isArray(presenceData) || presenceData.length === 0) {
|
||
notifyError('Aucune donnée de présence à enregistrer');
|
||
return;
|
||
}
|
||
|
||
// Valider les données
|
||
for (let i = 0; i < presenceData.length; i++) {
|
||
const presence = presenceData[i];
|
||
if (!presence.nom || !presence.prenom) {
|
||
notifyError(`Les champs nom et prénom sont requis pour le participant ${i + 1}`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
// Envoyer les données à l'API REST
|
||
const response = await apiFetch(`events/${eventId}/presences`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
presences: presenceData
|
||
})
|
||
});
|
||
|
||
if (response && response.success) {
|
||
notifySuccess('Présences enregistrées avec succès');
|
||
|
||
// Fermer la modal
|
||
const modal = document.getElementById('eventCheckPresenceModal');
|
||
if (modal && window.bootstrap && window.bootstrap.Modal) {
|
||
const bsModal = window.bootstrap.Modal.getInstance(modal);
|
||
if (bsModal) {
|
||
bsModal.hide();
|
||
}
|
||
}
|
||
|
||
// Rafraîchir le calendrier si disponible
|
||
if (window.currentCalendar) {
|
||
window.currentCalendar.refetchEvents();
|
||
}
|
||
if (window.currentColleaguesCalendar) {
|
||
window.currentColleaguesCalendar.refetchEvents();
|
||
}
|
||
} else {
|
||
const errorMessage = response?.message || 'Erreur lors de l\'enregistrement des présences';
|
||
notifyError(errorMessage);
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors de l\'enregistrement des présences:', error);
|
||
notifyError('Erreur lors de l\'enregistrement des présences');
|
||
}
|
||
}
|
||
|
||
// Fonction pour initialiser l'autocomplétion des champs nom/prénom
|
||
function initializePresenceAutocomplete() {
|
||
const nomInputs = document.querySelectorAll('.presence-nom-input');
|
||
|
||
nomInputs.forEach(nomInput => {
|
||
let searchTimeout = null;
|
||
let currentSuggestions = [];
|
||
let selectedIndex = -1;
|
||
|
||
const rowIndex = nomInput.getAttribute('data-row-index');
|
||
const prenomInput = document.getElementById(`presence_prenom_${rowIndex}`);
|
||
const beneficiaireIdInput = document.getElementById(`presence_beneficiaire_id_${rowIndex}`);
|
||
const suggestionsDiv = document.getElementById(`autocomplete_${rowIndex}`);
|
||
|
||
// Fonction pour rechercher des bénéficiaires
|
||
async function searchBeneficiaires(searchTerm) {
|
||
if (searchTerm.length < 5) {
|
||
suggestionsDiv.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await apiFetch(`beneficiaires/search?search=${encodeURIComponent(searchTerm)}`, {
|
||
method: 'GET'
|
||
});
|
||
|
||
if (response && response.success && response.data) {
|
||
currentSuggestions = response.data;
|
||
displaySuggestions();
|
||
} else {
|
||
currentSuggestions = [];
|
||
suggestionsDiv.style.display = 'none';
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors de la recherche de bénéficiaires:', error);
|
||
currentSuggestions = [];
|
||
suggestionsDiv.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Fonction pour afficher les suggestions
|
||
function displaySuggestions() {
|
||
if (currentSuggestions.length === 0) {
|
||
suggestionsDiv.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
suggestionsDiv.innerHTML = '';
|
||
currentSuggestions.forEach((beneficiaire, index) => {
|
||
const suggestionItem = document.createElement('div');
|
||
suggestionItem.className = 'autocomplete-item';
|
||
suggestionItem.textContent = `${beneficiaire.prenom} ${beneficiaire.nom}`;
|
||
suggestionItem.dataset.index = index;
|
||
suggestionItem.dataset.beneficiaireId = beneficiaire.id;
|
||
|
||
suggestionItem.addEventListener('click', () => {
|
||
selectBeneficiaire(beneficiaire);
|
||
});
|
||
|
||
suggestionsDiv.appendChild(suggestionItem);
|
||
});
|
||
|
||
// Positionner la div de suggestions sous le champ nom
|
||
const nomInputRect = nomInput.getBoundingClientRect();
|
||
const modalBody = nomInput.closest('.modal-body');
|
||
if (modalBody) {
|
||
const modalBodyRect = modalBody.getBoundingClientRect();
|
||
suggestionsDiv.style.position = 'absolute';
|
||
suggestionsDiv.style.top = (nomInputRect.bottom - modalBodyRect.top + modalBody.scrollTop) + 'px';
|
||
suggestionsDiv.style.left = (nomInputRect.left - modalBodyRect.left + modalBody.scrollLeft) + 'px';
|
||
}
|
||
|
||
suggestionsDiv.style.display = 'block';
|
||
}
|
||
|
||
// Fonction pour sélectionner un bénéficiaire
|
||
function selectBeneficiaire(beneficiaire) {
|
||
nomInput.value = beneficiaire.nom;
|
||
prenomInput.value = beneficiaire.prenom;
|
||
if (beneficiaireIdInput) {
|
||
beneficiaireIdInput.value = beneficiaire.id;
|
||
}
|
||
suggestionsDiv.style.display = 'none';
|
||
currentSuggestions = [];
|
||
}
|
||
|
||
// Écouter les changements dans le champ nom
|
||
nomInput.addEventListener('input', function() {
|
||
const searchTerm = this.value.trim();
|
||
|
||
// Réinitialiser beneficiaire_id si l'utilisateur modifie le nom
|
||
if (beneficiaireIdInput) {
|
||
beneficiaireIdInput.value = '';
|
||
}
|
||
|
||
// Délai pour éviter trop de requêtes
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => {
|
||
if (searchTerm.length >= 5) {
|
||
searchBeneficiaires(searchTerm);
|
||
} else {
|
||
suggestionsDiv.style.display = 'none';
|
||
}
|
||
}, 300);
|
||
});
|
||
|
||
// Masquer les suggestions quand on clique ailleurs
|
||
document.addEventListener('click', function(e) {
|
||
if (!nomInput.contains(e.target) && !suggestionsDiv.contains(e.target)) {
|
||
suggestionsDiv.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Fonction de confort pour les sélecteurs d'heure
|
||
function initializeTimeComfort() {
|
||
const heureRdvInput = document.getElementById('heure_rdv');
|
||
const heureFinInput = document.getElementById('heure_fin');
|
||
|
||
if (!heureRdvInput) return;
|
||
|
||
// Fonction pour obtenir l'heure suivante avec snapping aux demi-heures
|
||
function getNextComfortableTime() {
|
||
const now = new Date();
|
||
const currentHour = now.getHours();
|
||
const currentMinute = now.getMinutes();
|
||
|
||
// Heures de bureau : 8h à 18h
|
||
const businessStartHour = 8;
|
||
const businessEndHour = 18;
|
||
|
||
// Si on est avant les heures de bureau, proposer 8h00
|
||
if (currentHour < businessStartHour) {
|
||
return `${String(businessStartHour).padStart(2, '0')}:00`;
|
||
}
|
||
|
||
// Si on est après les heures de bureau, proposer 8h00 du lendemain
|
||
if (currentHour >= businessEndHour) {
|
||
return `${String(businessStartHour).padStart(2, '0')}:00`;
|
||
}
|
||
|
||
// Si on est après 30 minutes, proposer l'heure suivante
|
||
if (currentMinute > 30) {
|
||
const nextHour = currentHour + 1;
|
||
// S'assurer qu'on ne dépasse pas 18h
|
||
if (nextHour <= businessEndHour) {
|
||
return `${String(nextHour).padStart(2, '0')}:00`;
|
||
} else {
|
||
return `${String(businessStartHour).padStart(2, '0')}:00`;
|
||
}
|
||
}
|
||
// Sinon, proposer la demi-heure suivante
|
||
else if (currentMinute > 0) {
|
||
// S'assurer qu'on ne dépasse pas 17h30
|
||
if (currentHour < businessEndHour - 1) {
|
||
return `${String(currentHour).padStart(2, '0')}:30`;
|
||
} else {
|
||
return `${String(businessStartHour).padStart(2, '0')}:00`;
|
||
}
|
||
}
|
||
// Si on est pile sur l'heure, proposer la demi-heure
|
||
else {
|
||
// S'assurer qu'on ne dépasse pas 17h30
|
||
if (currentHour < businessEndHour - 1) {
|
||
return `${String(currentHour).padStart(2, '0')}:30`;
|
||
} else {
|
||
return `${String(businessStartHour).padStart(2, '0')}:00`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fonction pour créer les options d'heure avec snapping
|
||
function createTimeOptions() {
|
||
const options = [];
|
||
const now = new Date();
|
||
const currentHour = now.getHours();
|
||
const currentMinute = now.getMinutes();
|
||
|
||
// Heures de bureau : 8h à 18h
|
||
const businessStartHour = 8;
|
||
const businessEndHour = 18;
|
||
|
||
// Déterminer l'heure de départ
|
||
let startHour = Math.max(currentHour, businessStartHour);
|
||
let startMinute = 0;
|
||
|
||
// Si on est après 30 minutes, commencer à l'heure suivante
|
||
if (currentMinute > 30) {
|
||
startHour = currentHour + 1;
|
||
startMinute = 0;
|
||
}
|
||
// Si on est entre 0 et 30 minutes, commencer à la demi-heure
|
||
else if (currentMinute > 0) {
|
||
startMinute = 30;
|
||
}
|
||
// Si on est pile sur l'heure, commencer à la demi-heure
|
||
else {
|
||
startMinute = 30;
|
||
}
|
||
|
||
// S'assurer que l'heure de départ est dans les heures de bureau
|
||
if (startHour < businessStartHour) {
|
||
startHour = businessStartHour;
|
||
startMinute = 0;
|
||
}
|
||
|
||
// Générer les options pour les heures de bureau
|
||
for (let hour = startHour; hour <= businessEndHour; hour++) {
|
||
// Ajouter l'heure (si on n'est pas déjà passé)
|
||
if (hour > currentHour || (hour === currentHour && startMinute === 0)) {
|
||
options.push({
|
||
value: `${String(hour).padStart(2, '0')}:00`,
|
||
text: `${String(hour).padStart(2, '0')}:00`
|
||
});
|
||
}
|
||
|
||
// Ajouter la demi-heure (si on n'est pas déjà passé et pas la dernière heure)
|
||
if ((hour > currentHour || (hour === currentHour && startMinute === 30)) && hour < businessEndHour) {
|
||
options.push({
|
||
value: `${String(hour).padStart(2, '0')}:30`,
|
||
text: `${String(hour).padStart(2, '0')}:30`
|
||
});
|
||
}
|
||
}
|
||
|
||
return options;
|
||
}
|
||
|
||
// Fonction pour mettre à jour les options du select
|
||
function updateTimeSelect(selectElement, defaultTime = null) {
|
||
if (!selectElement) return;
|
||
|
||
// Vider le select
|
||
selectElement.innerHTML = '<option value="">Sélectionner...</option>';
|
||
|
||
// Créer les nouvelles options
|
||
const timeOptions = createTimeOptions();
|
||
|
||
// Ajouter les options
|
||
timeOptions.forEach(option => {
|
||
const optionElement = document.createElement('option');
|
||
optionElement.value = option.value;
|
||
optionElement.textContent = option.text;
|
||
selectElement.appendChild(optionElement);
|
||
});
|
||
|
||
// Définir la valeur par défaut si fournie
|
||
if (defaultTime) {
|
||
selectElement.value = defaultTime;
|
||
} else {
|
||
// Proposer l'heure confortable par défaut
|
||
const comfortableTime = getNextComfortableTime();
|
||
selectElement.value = comfortableTime;
|
||
}
|
||
}
|
||
|
||
// Initialiser les selects d'heure
|
||
updateTimeSelect(heureRdvInput);
|
||
|
||
// Mettre à jour le select de fin d'heure quand l'heure de début change
|
||
heureRdvInput.addEventListener('change', function() {
|
||
if (heureRdvInput.value && heureFinInput) {
|
||
// Calculer l'heure de fin (1 heure plus tard par défaut)
|
||
const startTime = heureRdvInput.value;
|
||
const [startHour, startMinute] = startTime.split(':').map(Number);
|
||
const endHour = (startHour + 1) % 24;
|
||
const endTime = `${String(endHour).padStart(2, '0')}:${String(startMinute).padStart(2, '0')}`;
|
||
|
||
// Mettre à jour le select de fin
|
||
updateTimeSelect(heureFinInput, endTime);
|
||
}
|
||
});
|
||
|
||
// Mettre à jour les options quand la date change
|
||
const dateRdvInput = document.getElementById('date_rdv');
|
||
if (dateRdvInput) {
|
||
dateRdvInput.addEventListener('change', function() {
|
||
const selectedDate = dateRdvInput.value;
|
||
const today = new Date().toISOString().split('T')[0];
|
||
|
||
// Si la date sélectionnée est aujourd'hui, utiliser les options confortables
|
||
if (selectedDate === today) {
|
||
updateTimeSelect(heureRdvInput);
|
||
} else {
|
||
// Pour les autres dates, réinitialiser avec les heures de bureau
|
||
heureRdvInput.innerHTML = '<option value="">Sélectionner...</option>';
|
||
for (let hour = 8; hour <= 18; hour++) {
|
||
heureRdvInput.innerHTML += `<option value="${String(hour).padStart(2, '0')}:00">${String(hour).padStart(2, '0')}:00</option>`;
|
||
if (hour < 18) { // Pas de 18h30
|
||
heureRdvInput.innerHTML += `<option value="${String(hour).padStart(2, '0')}:30">${String(hour).padStart(2, '0')}:30</option>`;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// console.log('Fonction de confort pour les sélecteurs d\'heure initialisée');
|
||
}
|
||
|
||
// Initialiser la fonction de confort quand le modal s'ouvre
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const eventModal = document.getElementById('eventModal');
|
||
if (eventModal) {
|
||
eventModal.addEventListener('shown.bs.modal', function() {
|
||
initializeTimeComfort();
|
||
});
|
||
}
|
||
});
|