// Module de gestion des selects pour les modales // Contient la logique de filtrage, Select2 et population des selects import { getFilters } from './agenda-api.js'; import { notifyError } from './agenda-notifications.js'; // Configuration const DEBUG_SELECTS = false; // Activer pour déboguer // Flags pour éviter les boucles infinies let isUpdatingSelects = false; let isFilteringTraducteurs = false; // Cache pour les disponibilités const disponibilitesCache = new Map(); let lastPopulateCall = null; let isPopulateSelectsRunning = false; /** * Initialise Select2 sur tous les selects de la modale */ export function initializeSelect2() { 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é, le détruire pour réappliquer 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') }); }); } /** * Fusionne les données de l'événement avec les disponibilités * @param {Object} availabilityData - Données de disponibilité de l'API * @param {Object} eventData - Données de l'événement * @returns {Object} - Données fusionnées */ function mergeEventDataWithAvailability(availabilityData, eventData) { if (DEBUG_SELECTS) { console.log('🔍 [MERGE] Fusion des données', { availabilityData, eventData }); } const merged = { ...availabilityData }; if (!eventData) return merged; const extendedProps = eventData.extendedProps || {}; // Helper pour fusionner un tableau d'entités function mergeEntities(availabilityEntities, eventEntityId, eventEntityData) { if (!eventEntityId || !eventEntityData) { return availabilityEntities; } // Vérifier si l'entité existe déjà (conversion en string pour comparaison) const eventEntityIdStr = eventEntityId != null ? eventEntityId.toString() : null; const exists = availabilityEntities.some(entity => entity.id != null && entity.id.toString() === eventEntityIdStr ); if (!exists) { 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) { merged.traducteurs = mergeEntities( merged.traducteurs || [], extendedProps.id_traducteur, { id: extendedProps.id_traducteur, nom: eventData.traducteur.nom + ' ' + (eventData.traducteur.prenom || ''), prenom: eventData.traducteur.prenom } ); } 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; 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 { // Vérifier si langue_label existe et n'est pas égal à l'ID if (eventData.langue_label && eventData.langue_label !== langueId) { langueNom = eventData.langue_label; } else if (extendedProps.langue_label && extendedProps.langue_label !== langueId) { langueNom = extendedProps.langue_label; } langueSlug = langueId; } const langueData = { id: langueId, nom: langueNom }; if (langueSlug) langueData.slug = langueSlug; merged.langues = mergeEntities( merged.langues || [], langueId, langueData ); } if (extendedProps.id_departement || eventData.id_departement) { const departementId = extendedProps.id_departement || eventData.id_departement; const departementNom = eventData.departement?.nom || eventData.departement || 'Département'; merged.departements = mergeEntities( merged.departements || [], departementId, { id: departementId, nom: departementNom } ); } 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'; merged.types_intervention = mergeEntities( merged.types_intervention || [], typeInterventionId, { id: typeInterventionId, nom: typeInterventionNom } ); } return merged; } /** * Filtre un select selon les items disponibles * @param {string} selectId - ID du select * @param {Array} availableItems - Items disponibles */ function filterSelect(selectId, availableItems) { const select = document.getElementById(selectId); if (!select || select.tagName !== 'SELECT') return; if (!Array.isArray(availableItems)) { if (DEBUG_SELECTS) console.warn(`Items invalides pour ${selectId}:`, availableItems); return; } // Créer un ensemble des IDs disponibles const availableIds = new Set( availableItems .filter(item => item && item.id != null) .map(item => item.id.toString()) ); // Pour les langues, créer aussi un set des slugs const availableSlugs = selectId === 'langue' ? new Set( availableItems .filter(item => item != null) .flatMap(item => { const slugs = []; if (item.slug != null) slugs.push(item.slug.toString()); if (item.id != null && isNaN(item.id)) slugs.push(item.id.toString()); return slugs; }) ) : new Set(); const currentValue = select.value; // Parcourir les options du select if (!select.options) return; Array.from(select.options).forEach(option => { if (option.value === '') { option.style.display = ''; return; } const isCurrentlySelected = option.value === currentValue; let isAvailable = availableIds.has(option.value); // Pour les langues, vérifier aussi le slug if (!isAvailable && selectId === 'langue') { const optionSlug = option.getAttribute('data-slug'); if (optionSlug && availableSlugs.has(optionSlug)) { isAvailable = true; } else if (availableSlugs.has(option.value)) { isAvailable = true; } else if (availableSlugs.size > 0) { const optionText = option.text.toLowerCase().trim(); if (availableSlugs.has(optionText)) { isAvailable = true; } else { const normalizedText = optionText.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); if (availableSlugs.has(normalizedText)) { isAvailable = true; } } } } // Afficher ou cacher l'option if (isAvailable || isCurrentlySelected) { option.style.display = ''; option.disabled = false; } else { option.style.display = 'none'; option.disabled = true; } }); // Mettre à jour Select2 si initialisé if (window.jQuery && jQuery(select).hasClass('select2-hidden-accessible')) { jQuery(select).trigger('change.select2'); } } /** * Filtre les options des selects selon les disponibilités * @param {Object} data - Données de disponibilité * @param {Object} currentEventData - Données de l'événement actuel */ function filterSelectOptions(data, currentEventData = null) { if (DEBUG_SELECTS) { console.log('🎯 [FILTER] Filtrage des selects', { data }); } // Filtrer chaque type de select 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; const langueSelect = document.getElementById('langue'); if (langueSelect && isPermanence && languesDisponibles && typeof languesDisponibles === 'string' && languesDisponibles.trim() !== '') { const languesPermises = languesDisponibles.split(',').map(l => l.trim()).filter(l => l !== ''); if (languesPermises.length > 0) { // Parcourir les options et cacher celles dont le data-slug n'est pas dans langues_disponibles const currentValue = langueSelect.value; // Récupérer la valeur actuellement sélectionnée Array.from(langueSelect.options).forEach(option => { if (option.value === '') { // Garder l'option vide visible option.style.display = ''; option.disabled = false; return; } // Garder l'option sélectionnée visible même si elle n'est pas dans langues_disponibles const isCurrentlySelected = option.value === currentValue; const optionSlug = option.getAttribute('data-slug'); const isPermise = optionSlug && languesPermises.includes(optionSlug); if (isPermise || isCurrentlySelected) { option.style.display = ''; option.disabled = false; } else { option.style.display = 'none'; option.disabled = true; } }); console.log('✅ [LANGUE] Options filtrées selon langues_disponibles:', languesPermises); } else { // Si langues_disponibles est vide, afficher toutes les options Array.from(langueSelect.options).forEach(option => { option.style.display = ''; option.disabled = false; }); } } else { // Pas de permanence ou pas de langues_disponibles : afficher toutes les options if (langueSelect) { Array.from(langueSelect.options).forEach(option => { option.style.display = ''; option.disabled = false; }); } filterSelect('langue', data.langues || []); } } /** * Réinitialise le display des options du select langue * À appeler lors de la fermeture du modal pour restaurer toutes les options */ export function resetLangueSelectDisplay() { const langueSelect = document.getElementById('langue'); if (!langueSelect || langueSelect.tagName !== 'SELECT') return; // Réinitialiser le display de toutes les options Array.from(langueSelect.options).forEach(option => { option.style.display = ''; option.disabled = false; }); console.log('🔄 [LANGUE] Display des options réinitialisé'); } /** * Présélectionne les valeurs dans les selects * @param {Object} eventData - Données de l'événement */ function preselectValues(eventData) { if (!eventData) return; const extendedProps = eventData.extendedProps || {}; // Mapper les champs à présélectionner const fieldsToPreselect = { 'id_beneficiaire': extendedProps.id_beneficiaire, 'id_intervenant': extendedProps.id_intervenant, 'id_traducteur': extendedProps.id_traducteur, 'id_local': extendedProps.id_local, 'id_departement': extendedProps.id_departement, 'id_type_intervention': extendedProps.id_type_intervention, 'langue': extendedProps.langue || eventData.langue }; Object.entries(fieldsToPreselect).forEach(([fieldId, value]) => { if (!value) return; const element = document.getElementById(fieldId); if (!element || element.tagName !== 'SELECT') return; // Vérifier que l'option existe dans le select avant de la sélectionner const optionExists = Array.from(element.options).some(opt => opt.value === value.toString()); if (!optionExists) { console.warn(`⚠️ Option avec valeur "${value}" non trouvée dans le select ${fieldId}`); return; } // Si Select2 est actif if (window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) { // Définir la valeur et déclencher l'événement change.select2 jQuery(element).val(value).trigger('change.select2'); } else { // Select natif element.value = value; // Déclencher l'événement change pour notifier les autres listeners element.dispatchEvent(new Event('change', { bubbles: true })); } }); } /** * Population des selects selon les disponibilités * @param {Object} eventData - Données de l'événement */ export async function populateSelects(eventData = null) { // Vérifier si une exécution est déjà en cours if (isPopulateSelectsRunning) { if (DEBUG_SELECTS) console.warn('⚠️ populateSelects déjà en cours'); return; } isPopulateSelectsRunning = true; isUpdatingSelects = true; try { const dateInput = document.getElementById('date_rdv'); const heureInput = document.getElementById('heure_rdv'); if (!dateInput || !heureInput) { console.warn('Champs date/heure non trouvés'); return; } const date = dateInput.value; const heure = heureInput.value; if (!date || !heure) { console.warn('Date et heure requises pour filtrer les disponibilités'); return; } // Créer une clé de cache const cacheKey = `${date}|${heure}|${eventData ? eventData.id || 'new' : 'new'}`; // Vérifier le cache if (disponibilitesCache.has(cacheKey)) { const cachedData = disponibilitesCache.get(cacheKey); const mergedData = mergeEventDataWithAvailability(cachedData, eventData); filterSelectOptions(mergedData, eventData); // S'assurer que Select2 est initialisé avant de présélectionner initializeSelect2(); // Petit délai pour s'assurer que le filtrage des langues est bien appliqué avant la présélection // Et que les options sont bien dans le DOM setTimeout(() => { preselectValues(eventData); }, 100); return; } // Éviter les appels simultanés const currentCall = `${date}|${heure}`; if (lastPopulateCall === currentCall) return; lastPopulateCall = currentCall; // Appel API const params = { date, heure }; const data = await getFilters('disponibilites', params); // Mettre en cache (expire après 5 minutes) disponibilitesCache.set(cacheKey, data); setTimeout(() => disponibilitesCache.delete(cacheKey), 5 * 60 * 1000); // Fusionner et filtrer const mergedData = mergeEventDataWithAvailability(data, eventData); filterSelectOptions(mergedData, eventData); // S'assurer que Select2 est initialisé avant de présélectionner initializeSelect2(); // Petit délai pour s'assurer que le filtrage des langues est bien appliqué avant la présélection // Et que les options sont bien dans le DOM setTimeout(() => { preselectValues(eventData); }, 100); } catch (error) { console.error('Erreur lors de la récupération des disponibilités:', error); notifyError('Erreur lors de la récupération des disponibilités'); } finally { isPopulateSelectsRunning = false; isUpdatingSelects = false; } } /** * Filtre les traducteurs selon la langue sélectionnée */ export function filterTraducteursByLangue() { if (isUpdatingSelects || 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 ? langueSelect.value.toString() : ''; const currentTraducteurValue = traducteurSelect.value; // Si aucune langue sélectionnée, afficher tous les traducteurs if (!selectedLangue) { if (traducteurSelect.options) { Array.from(traducteurSelect.options).forEach(option => { option.style.display = ''; option.disabled = false; }); if (window.jQuery && jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) { jQuery(traducteurSelect).trigger('change.select2'); } } return; } // Récupérer le slug de la langue const langueOption = langueSelect.options[langueSelect.selectedIndex]; let langueSlug = null; if (langueOption && langueOption.dataset.slug) { langueSlug = langueOption.dataset.slug; } else { console.warn('Aucun slug disponible pour la langue sélectionnée'); return; } // Filtrer les traducteurs if (traducteurSelect.options) { Array.from(traducteurSelect.options).forEach(option => { if (option.value === '') { option.style.display = ''; return; } const traducteurLangues = option.getAttribute('data-langue'); const isCurrentlySelected = option.value === currentTraducteurValue; if (!traducteurLangues) { option.style.display = isCurrentlySelected ? '' : 'none'; option.disabled = !isCurrentlySelected; } else { const languesArray = traducteurLangues.split(','); const langueMatches = languesArray.some(lang => { 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 actuel ne correspond plus, le vider if (currentTraducteurValue && selectedLangue) { const currentOption = traducteurSelect.options[traducteurSelect.selectedIndex]; if (currentOption && currentOption.value !== '' && currentOption.style.display === 'none') { traducteurSelect.value = ''; if (window.jQuery && jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) { jQuery(traducteurSelect).val('').trigger('change.select2'); } } } // Mettre à jour Select2 if (window.jQuery && jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) { jQuery(traducteurSelect).trigger('change.select2'); } } } finally { isFilteringTraducteurs = false; } } /** * Obtient l'état du flag isUpdatingSelects * @returns {boolean} */ export function getIsUpdatingSelects() { return isUpdatingSelects; } /** * Vide le cache des disponibilités */ export function clearDisponibilitesCache() { disponibilitesCache.clear(); lastPopulateCall = null; } /** * Initialise le listener pour afficher l'alerte "Personne en liste rouge" */ export function initializeBeneficiaireListeRougeAlert() { const beneficiaireSelect = document.getElementById('id_beneficiaire'); const alertElement = document.getElementById('beneficiaire-liste-rouge-alert'); if (!beneficiaireSelect || !alertElement) { return; } // Fonction pour vérifier et afficher l'alerte const checkListeRouge = () => { const selectedBeneficiaireId = beneficiaireSelect.value; // Vérifier si le bénéficiaire est en liste rouge via crviACFData if (selectedBeneficiaireId && window.crviACFData && window.crviACFData.beneficiaires) { const beneficiaireData = window.crviACFData.beneficiaires[selectedBeneficiaireId]; if (beneficiaireData && beneficiaireData.personne_en_liste_rouge) { alertElement.style.display = 'block'; } else { alertElement.style.display = 'none'; } } else { alertElement.style.display = 'none'; } }; // Écouter les changements du select beneficiaireSelect.addEventListener('change', checkListeRouge); // Écouter aussi les événements Select2 si présent if (window.jQuery) { jQuery(beneficiaireSelect).on('select2:select', checkListeRouge); jQuery(beneficiaireSelect).on('select2:clear', checkListeRouge); } }