Crvi/assets/js/modules/agenda-modal-select.js
2026-01-21 22:20:02 +01:00

708 lines
28 KiB
JavaScript

// 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;
}
// Vérifier spécifiquement le select langue avant initialisation
const langueSelect = document.getElementById('langue');
if (langueSelect) {
// Forcer l'ajout de la classe skip-select2 si elle n'est pas présente
if (!langueSelect.classList.contains('skip-select2')) {
console.warn('⚠️ [LANGUE] Classe skip-select2 manquante, ajout forcé');
langueSelect.classList.add('skip-select2');
}
const hasSkipSelect2 = langueSelect.classList.contains('skip-select2');
const hasSelect2Class = langueSelect.classList.contains('select2');
const isAlreadySelect2 = window.jQuery && jQuery(langueSelect).hasClass('select2-hidden-accessible');
console.log('🔵 [LANGUE] initializeSelect2 - skip-select2:', hasSkipSelect2, '| classe select2:', hasSelect2Class, '| Déjà Select2:', isAlreadySelect2, '| ID:', langueSelect.id, '| Classes:', langueSelect.className, '| HTML:', langueSelect.outerHTML.substring(0, 200));
// Si le select langue a skip-select2 mais est déjà en Select2, le détruire
if (hasSkipSelect2 && isAlreadySelect2) {
console.log('⚠️ [LANGUE] Select2 détecté sur select avec skip-select2, destruction...');
try {
jQuery(langueSelect).select2('destroy');
// Nettoyer les classes et attributs Select2
langueSelect.classList.remove('select2-hidden-accessible', 'select2');
langueSelect.removeAttribute('data-select2-id');
langueSelect.removeAttribute('tabindex');
langueSelect.removeAttribute('aria-hidden');
console.log('✅ [LANGUE] Select2 détruit, classes après:', langueSelect.className);
} catch (e) {
console.warn('Erreur lors de la destruction Select2 pour langue:', e);
}
}
// S'assurer que la classe select2 n'est pas présente si skip-select2 est là
if (hasSkipSelect2 && hasSelect2Class) {
langueSelect.classList.remove('select2');
console.log('🧹 [LANGUE] Classe select2 retirée (skip-select2 présent)');
}
}
jQuery('#eventModal select:not(.skip-select2)').each(function() {
const $select = jQuery(this);
// Ignorer explicitement le select langue s'il a skip-select2
if ($select.attr('id') === 'langue' && $select.hasClass('skip-select2')) {
console.log('⏭️ [LANGUE] Ignoré dans initializeSelect2 (skip-select2)');
return;
}
// 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;
}
});
} 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;
});
}
// Ne pas filtrer les langues en mode edit (quand l'événement a un ID)
// Le filtrage par disponibilités ne doit s'appliquer qu'en mode création
const isEditMode = currentEventData && (currentEventData.id || currentEventData.extendedProps?.id);
if (!isEditMode) {
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;
});
}
/**
* 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;
// Tracer spécifiquement pour le champ langue
if (fieldId === 'langue') {
const currentValue = element.value || '';
const isSelect2 = window.jQuery && jQuery(element).hasClass('select2-hidden-accessible');
const hasSkipSelect2 = element.classList.contains('skip-select2');
const optionCount = element.options.length;
const optionExists = Array.from(element.options).some(opt => opt.value === value.toString());
console.log('🟣 [LANGUE] preselectValues - Valeur actuelle:', currentValue, '| Valeur à appliquer:', value, '| Source:', extendedProps.langue ? 'extendedProps' : eventData.langue ? 'eventData' : 'N/A', '| Select2:', isSelect2, '| skip-select2:', hasSkipSelect2, '| Options:', optionCount, '| Option existe:', optionExists);
}
// 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) {
if (fieldId === 'langue') {
console.warn(`⚠️ [LANGUE] Option avec valeur "${value}" non trouvée dans le select`);
} else {
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 }));
}
// Vérifier la valeur après application pour le champ langue
if (fieldId === 'langue') {
const finalValue = element.value || '';
const selectedOption = element.options[element.selectedIndex];
const selectedText = selectedOption ? selectedOption.text : 'N/A';
console.log('🟠 [LANGUE] preselectValues - Valeur après application:', finalValue, '| Option sélectionnée:', selectedText, '| selectedIndex:', element.selectedIndex);
}
});
}
/**
* 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);
}
}