diff --git a/assets/js/modules/agenda-entity-creator.js b/assets/js/modules/agenda-entity-creator.js index ef03668..d3ff9ea 100644 --- a/assets/js/modules/agenda-entity-creator.js +++ b/assets/js/modules/agenda-entity-creator.js @@ -1,7 +1,8 @@ // Module ES6 pour la création d'entités depuis le modal d'événement import { apiFetch } from './agenda-api.js'; import { notifyError, notifySuccess } from './agenda-notifications.js'; -import { populateSelects, preserveModalData } from './agenda-modal.js'; +import { openSubModal } from './agenda-modal.js'; +import { populateSelects } from './agenda-modal-select.js'; // Configuration des entités const ENTITY_CONFIG = { @@ -97,49 +98,18 @@ function openCreateEntityModal(entityType) { return; } - const modal = document.getElementById(config.modalId); - if (!modal) { - console.error(`Modal non trouvé: ${config.modalId}`); - return; - } - - // Réinitialiser le formulaire - const form = document.getElementById(config.formId); - if (form) { - form.reset(); - } - - // Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2() - - // 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(); + // Utiliser la fonction générique pour ouvrir la sous-modale + openSubModal( + config.modalId, + // Callback avant ouverture : réinitialiser le formulaire + (subModal) => { + const form = document.getElementById(config.formId); + if (form) { + form.reset(); + } + // Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2() } - } - - // Attendre un peu que le modal principal se ferme avant d'ouvrir le nouveau - setTimeout(() => { - // Ouvrir le modal de création - 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 + ); } // Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2() diff --git a/assets/js/modules/agenda-modal-buttons.js b/assets/js/modules/agenda-modal-buttons.js new file mode 100644 index 0000000..9f6d700 --- /dev/null +++ b/assets/js/modules/agenda-modal-buttons.js @@ -0,0 +1,269 @@ +// Module de gestion des boutons de la modale +// Contient les gestionnaires d'événements pour tous les boutons de la modale + +import { deleteEvent, changeEventStatus } from './agenda-api.js'; +import { notifyError, notifySuccess } from './agenda-notifications.js'; + +/** + * Helper pour gérer l'overlay de chargement + * @param {Function} asyncAction - Action asynchrone à exécuter + * @returns {Promise} + */ +async function withLoadingOverlay(asyncAction) { + const overlayTarget = document.querySelector('#eventModal .modal-content') || document.getElementById('eventModal'); + try { + if (window.CRVI_OVERLAY && overlayTarget) { + window.CRVI_OVERLAY.show(overlayTarget); + } + await asyncAction(); + } finally { + if (window.CRVI_OVERLAY && overlayTarget) { + window.CRVI_OVERLAY.hide(overlayTarget); + } + } +} + +/** + * Helper pour rafraîchir les calendriers + */ +function refreshCalendars() { + if (window.currentCalendar) { + window.currentCalendar.refetchEvents(); + } + if (window.currentColleaguesCalendar) { + window.currentColleaguesCalendar.refetchEvents(); + } +} + +/** + * Helper pour fermer le modal + * @param {Function} onClosed - Callback après fermeture + */ +function closeModal(onClosed = null) { + const modal = document.getElementById('eventModal'); + if (modal) { + const bsModal = bootstrap.Modal.getInstance(modal); + if (bsModal) { + if (onClosed) { + modal.addEventListener('hidden.bs.modal', onClosed, { once: true }); + } + bsModal.hide(); + } + } +} + +/** + * Vérifie les permissions utilisateur + * @param {string} permission - Type de permission ('can_edit', 'can_delete', 'can_create') + * @returns {boolean} + */ +function checkPermission(permission) { + const hasPermission = window.crviPermissions && window.crviPermissions[permission]; + if (!hasPermission) { + console.warn(`Utilisateur non autorisé: ${permission}`); + } + return hasPermission; +} + +/** + * Initialise le bouton de fermeture + * @param {string} buttonId - ID du bouton + */ +export function initializeCloseButton(buttonId) { + const button = document.getElementById(buttonId); + if (button) { + button.onclick = () => closeModal(); + } +} + +/** + * Initialise le bouton de suppression + * @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement + * @param {Function} onDeleted - Callback après suppression + */ +export function initializeDeleteButton(getCurrentEventData, onDeleted = null) { + const deleteBtn = document.getElementById('deleteEvent'); + if (!deleteBtn) return; + + deleteBtn.onclick = async function() { + if (!checkPermission('can_delete')) return; + + if (!confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) { + return; + } + + const currentEventData = getCurrentEventData(); + const eventId = currentEventData ? currentEventData.id : null; + + if (!eventId) { + notifyError('ID d\'événement manquant'); + return; + } + + await withLoadingOverlay(async () => { + try { + await deleteEvent(eventId); + refreshCalendars(); + closeModal(onDeleted); + } catch (error) { + console.error('Erreur lors de la suppression:', error); + notifyError('Erreur lors de la suppression de l\'événement'); + } + }); + }; +} + +/** + * Initialise le bouton de validation de présence + * @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement + * @param {Function} openCheckPresenceModal - Fonction pour ouvrir la modal de validation de présence de groupe + * @param {Function} onStatusChanged - Callback après changement de statut + */ +export function initializeMarkPresentButton(getCurrentEventData, openCheckPresenceModal, onStatusChanged = null) { + const markPresentBtn = document.getElementById('markPresentBtn'); + if (!markPresentBtn) return; + + markPresentBtn.onclick = async function() { + if (!checkPermission('can_edit')) return; + + const currentEventData = getCurrentEventData(); + const eventId = currentEventData ? currentEventData.id : null; + + if (!eventId) { + notifyError('ID d\'événement manquant'); + 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 + if (!confirm('Confirmer la présence à cet événement ?')) { + return; + } + + await withLoadingOverlay(async () => { + try { + await changeEventStatus(eventId, 'present'); + notifySuccess('Présence validée'); + refreshCalendars(); + closeModal(onStatusChanged); + } catch (error) { + console.error('Erreur lors du changement de statut:', error); + notifyError('Erreur lors de la validation de présence'); + } + }); + } + }; +} + +/** + * Initialise le bouton pour marquer comme absent + * @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement + * @param {Function} onStatusChanged - Callback après changement de statut + */ +export function initializeMarkAbsentButton(getCurrentEventData, onStatusChanged = null) { + const markAbsentBtn = document.getElementById('markAbsentBtn'); + if (!markAbsentBtn) return; + + markAbsentBtn.onclick = async function() { + if (!checkPermission('can_edit')) return; + + const currentEventData = getCurrentEventData(); + const eventId = currentEventData ? currentEventData.id : null; + + if (!eventId) { + notifyError('ID d\'événement manquant'); + return; + } + + if (!confirm('Marquer cet événement comme absent ?')) { + return; + } + + await withLoadingOverlay(async () => { + try { + await changeEventStatus(eventId, 'absence'); + notifySuccess('Événement marqué comme absent'); + refreshCalendars(); + closeModal(onStatusChanged); + } catch (error) { + console.error('Erreur lors du changement de statut:', error); + notifyError('Erreur lors du changement de statut'); + } + }); + }; +} + +/** + * Initialise le bouton d'annulation de rendez-vous + * @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement + * @param {Function} onCancelled - Callback après annulation + */ +export function initializeCancelAppointmentButton(getCurrentEventData, onCancelled = null) { + const cancelAppointmentBtn = document.getElementById('cancelAppointmentBtn'); + if (!cancelAppointmentBtn) return; + + cancelAppointmentBtn.onclick = async function() { + if (!checkPermission('can_edit')) return; + + const currentEventData = getCurrentEventData(); + const eventId = currentEventData ? currentEventData.id : null; + + if (!eventId) { + notifyError('ID d\'événement manquant'); + return; + } + + const motif = prompt('Motif de l\'annulation (optionnel):'); + if (motif === null) { + return; // L'utilisateur a cliqué sur Annuler + } + + await withLoadingOverlay(async () => { + try { + await changeEventStatus(eventId, 'annule', motif); + notifySuccess('Rendez-vous annulé'); + refreshCalendars(); + closeModal(onCancelled); + } catch (error) { + console.error('Erreur lors de l\'annulation:', error); + notifyError('Erreur lors de l\'annulation du rendez-vous'); + } + }); + }; +} + +/** + * Initialise tous les boutons de la modale + * @param {Object} options - Options de configuration + * @param {Function} options.getCurrentEventData - Fonction pour obtenir les données de l'événement + * @param {Function} options.openCheckPresenceModal - Fonction pour ouvrir la modal de validation de présence + * @param {Function} options.onDeleted - Callback après suppression + * @param {Function} options.onStatusChanged - Callback après changement de statut + */ +export function initializeModalButtons(options = {}) { + const { + getCurrentEventData, + openCheckPresenceModal, + onDeleted, + onStatusChanged + } = options; + + // Boutons de fermeture + initializeCloseButton('closeModalBtn'); + initializeCloseButton('closeViewBtn'); + + // Boutons d'action + if (getCurrentEventData) { + initializeDeleteButton(getCurrentEventData, onDeleted); + initializeMarkPresentButton(getCurrentEventData, openCheckPresenceModal, onStatusChanged); + initializeMarkAbsentButton(getCurrentEventData, onStatusChanged); + initializeCancelAppointmentButton(getCurrentEventData, onStatusChanged); + } +} diff --git a/assets/js/modules/agenda-modal-display.js b/assets/js/modules/agenda-modal-display.js new file mode 100644 index 0000000..fe77737 --- /dev/null +++ b/assets/js/modules/agenda-modal-display.js @@ -0,0 +1,289 @@ +// Module d'affichage des données de la modale +// Contient les fonctions d'affichage en mode lecture seule + +import { setText, toggleElement } from './agenda-modal-dom.js'; + +/** + * Extrait la date et l'heure d'un événement selon différentes sources + * @param {Object} event - Données de l'événement + * @returns {Object} - {dateFormatted, heureFormatted} + */ +function extractDateTime(event) { + let dateFormatted = ''; + let heureFormatted = ''; + + // Priorité 1: Données directes de l'API + 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 + 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' }); + } + + return { dateFormatted, heureFormatted }; +} + +/** + * Récupère le nom d'une entité depuis le select ou les données + * @param {string} entityName - Nom de l'entité ('beneficiaire', 'intervenant', etc.) + * @param {Object} event - Données de l'événement + * @param {string} selectId - ID du select + * @param {string} idField - Nom du champ ID + * @returns {string} - Nom formaté + */ +function getEntityName(entityName, event, selectId, idField) { + let name = ''; + + // Vérifier dans les relations directes + if (event?.[entityName]?.nom) { + name = event[entityName].nom + (event[entityName].prenom ? ' ' + event[entityName].prenom : ''); + } else if (event?.extendedProps?.[entityName]?.nom) { + name = event.extendedProps[entityName].nom + (event.extendedProps[entityName].prenom ? ' ' + event.extendedProps[entityName].prenom : ''); + } else { + // Chercher par ID dans le select + const entityId = event?.[idField] || event?.extendedProps?.[idField]; + if (entityId) { + const select = document.getElementById(selectId); + // Vérifier si c'est un SELECT (pour intervenant côté front c'est un input hidden) + if (select && select.tagName === 'SELECT') { + const option = select.querySelector(`option[value="${entityId}"]`); + if (option) { + name = option.textContent; + } + } else if (select && selectId === 'id_intervenant') { + // Cas spécial pour intervenant côté front (input hidden) + const displayEl = document.getElementById('id_intervenant_display'); + if (displayEl && displayEl.textContent) { + name = displayEl.textContent; + } + } + + if (!name) { + name = `ID: ${entityId}`; + } + } + } + + return name; +} + +/** + * Récupère le nom d'un terme taxonomy depuis les données ou le select + * @param {Object} event - Données de l'événement + * @param {string} labelField - Champ de label (_label) + * @param {string} relationField - Champ de relation + * @param {string} idField - Champ ID + * @param {string} selectId - ID du select + * @returns {string} - Nom du terme + */ +function getTermName(event, labelField, relationField, idField, selectId) { + let name = ''; + + // Priorité 1: Label direct + name = event?.[labelField] || event?.extendedProps?.[labelField] || ''; + + // Priorité 2: Relation + if (!name && event?.[relationField]?.nom) { + name = event[relationField].nom; + } else if (!name && event?.extendedProps?.[relationField]?.nom) { + name = event.extendedProps[relationField].nom; + } + + // Priorité 3: Chercher dans le select + if (!name) { + const termId = event?.[idField] || event?.extendedProps?.[idField]; + if (termId && termId !== '0' && termId !== 0) { + const select = document.getElementById(selectId); + if (select) { + const option = select.querySelector(`option[value="${termId}"]`); + if (option) { + name = option.textContent; + } + } + if (!name) { + name = `ID: ${termId}`; + } + } + } + + return name; +} + +/** + * Remplit le bloc de vue avec les données d'un événement + * @param {Object} event - Données de l'événement + */ +export function fillViewBlock(event) { + // Date et heure + const { dateFormatted, heureFormatted } = extractDateTime(event); + setText('view_date_rdv', dateFormatted); + setText('view_heure_rdv', heureFormatted); + + // Type et langue + const type = event?.type || event?.extendedProps?.type || ''; + setText('view_type', type); + + let langue = event?.langue_label || event?.extendedProps?.langue_label || ''; + 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 { + langue = langueId; + } + } else { + langue = langueId; + } + } + } + if (!langue) { + langue = event?.langue || event?.extendedProps?.langue || ''; + } + setText('view_langue', langue); + + // Entités + setText('view_beneficiaire', getEntityName('beneficiaire', event, 'id_beneficiaire', 'id_beneficiaire')); + setText('view_intervenant', getEntityName('intervenant', event, 'id_intervenant', 'id_intervenant')); + setText('view_local', getEntityName('local', event, 'id_local', 'id_local')); + + // Termes taxonomy + setText('view_departement', getTermName(event, 'departement_label', 'departement', 'id_departement', 'id_departement')); + setText('view_type_intervention', getTermName(event, 'type_intervention_label', 'type_intervention', 'id_type_intervention', 'id_type_intervention')); + + // Traducteur (logique spéciale) + let traducteurNom = ''; + const traducteurId = event?.id_traducteur || event?.extendedProps?.id_traducteur; + const hasValidTraducteurId = traducteurId && parseInt(traducteurId, 10) > 0; + + if (hasValidTraducteurId) { + traducteurNom = getEntityName('traducteur', event, 'id_traducteur', 'id_traducteur'); + } else { + traducteurNom = event?.nom_traducteur || event?.extendedProps?.nom_traducteur || ''; + } + setText('view_traducteur', traducteurNom); + + // Données 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 = ''); + setText('view_nb_participants', event?.nb_participants || event?.extendedProps?.nb_participants || ''); + setText('view_nb_hommes', event?.nb_hommes || event?.extendedProps?.nb_hommes || ''); + setText('view_nb_femmes', event?.nb_femmes || event?.extendedProps?.nb_femmes || ''); + } else { + groupeFields.forEach(field => field.style.display = 'none'); + setText('view_nb_participants', ''); + setText('view_nb_hommes', ''); + setText('view_nb_femmes', ''); + } + + // Commentaire + setText('view_commentaire', event?.commentaire || event?.extendedProps?.commentaire || ''); + + // Bouton historique bénéficiaire (uniquement pour événements individuels) + const historiqueBtn = document.getElementById('showBeneficiaireHistoriqueBtn'); + if (historiqueBtn) { + const beneficiaireId = event?.id_beneficiaire || event?.extendedProps?.id_beneficiaire; + if (eventType === 'individuel' && beneficiaireId) { + historiqueBtn.style.display = 'block'; + historiqueBtn.setAttribute('data-benef', beneficiaireId); + } else { + historiqueBtn.style.display = 'none'; + historiqueBtn.removeAttribute('data-benef'); + } + } +} + +/** + * Met à jour l'affichage de la modale selon le mode + * @param {string} mode - Mode actuel ('view', 'edit', 'create') + * @param {boolean} canEdit - Permission d'édition + * @param {boolean} canDelete - Permission de suppression + */ +export function updateModalDisplay(mode, canEdit, canDelete) { + const viewBlock = document.getElementById('eventViewBlock'); + const editBlock = document.getElementById('eventEditBlock'); + const editBtn = document.getElementById('editEventBtn'); + const deleteBtn = document.getElementById('deleteEvent'); + const saveBtn = document.getElementById('saveEvent'); + const cancelBtn = document.getElementById('cancelEditBtn'); + const closeViewBtn = document.getElementById('closeViewBtn'); + const markPresentBtn = document.getElementById('markPresentBtn'); + const markAbsentBtn = document.getElementById('markAbsentBtn'); + const cancelAppointmentBtn = document.getElementById('cancelAppointmentBtn'); + const reportIncidentBtn = document.getElementById('reportIncidentBtn'); + + if (mode === 'view') { + if (viewBlock) viewBlock.style.display = 'block'; + if (editBlock) editBlock.style.display = 'none'; + if (editBtn) editBtn.style.display = canEdit ? 'inline-block' : 'none'; + if (deleteBtn) deleteBtn.style.display = canDelete ? 'inline-block' : 'none'; + if (saveBtn) saveBtn.style.display = 'none'; + if (cancelBtn) cancelBtn.style.display = 'none'; + if (closeViewBtn) closeViewBtn.style.display = 'inline-block'; + if (markPresentBtn) markPresentBtn.style.display = canEdit ? 'inline-block' : 'none'; + if (markAbsentBtn) markAbsentBtn.style.display = canEdit ? 'inline-block' : 'none'; + if (cancelAppointmentBtn) cancelAppointmentBtn.style.display = canEdit ? 'inline-block' : 'none'; + if (reportIncidentBtn) reportIncidentBtn.style.display = 'inline-block'; + } else if (mode === 'edit' || mode === 'create') { + if (viewBlock) viewBlock.style.display = 'none'; + if (editBlock) editBlock.style.display = 'block'; + if (editBtn) editBtn.style.display = 'none'; + if (deleteBtn) deleteBtn.style.display = 'none'; + if (saveBtn) saveBtn.style.display = 'inline-block'; + if (cancelBtn) cancelBtn.style.display = 'inline-block'; + if (closeViewBtn) closeViewBtn.style.display = 'none'; + if (markPresentBtn) markPresentBtn.style.display = 'none'; + if (markAbsentBtn) markAbsentBtn.style.display = 'none'; + if (cancelAppointmentBtn) cancelAppointmentBtn.style.display = 'none'; + if (reportIncidentBtn) reportIncidentBtn.style.display = 'none'; + } +} + +/** + * Vérifie si un événement est passé + * @param {Object} eventData - Données de l'événement + * @returns {boolean} - True si l'événement est passé + */ +export function checkIfEventIsPast(eventData) { + if (!eventData) return false; + + 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) { + 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; + + const eventDateTime = new Date(`${eventDate}T${eventTime}`); + const now = new Date(); + + return eventDateTime <= now; +} diff --git a/assets/js/modules/agenda-modal-dom.js b/assets/js/modules/agenda-modal-dom.js new file mode 100644 index 0000000..7ae80cf --- /dev/null +++ b/assets/js/modules/agenda-modal-dom.js @@ -0,0 +1,190 @@ +// Module de gestion du DOM pour les modales +// Contient les helpers pour accéder et manipuler les éléments DOM de manière optimisée + +/** + * Cache pour les éléments DOM fréquemment accédés + */ +const domCache = new Map(); + +/** + * Obtient un élément du DOM avec mise en cache + * @param {string} elementId - ID de l'élément + * @returns {HTMLElement|null} - L'élément ou null + */ +export function getElement(elementId) { + if (domCache.has(elementId)) { + const cached = domCache.get(elementId); + // Vérifier que l'élément est toujours dans le DOM + if (cached && document.contains(cached)) { + return cached; + } + domCache.delete(elementId); + } + + const element = document.getElementById(elementId); + if (element) { + domCache.set(elementId, element); + } + return element; +} + +/** + * Vide le cache DOM (à appeler lors de la fermeture du modal) + */ +export function clearDomCache() { + domCache.clear(); +} + +/** + * Définit la valeur d'un élément de manière sécurisée + * @param {string} elementId - ID de l'élément + * @param {*} value - Valeur à définir + * @returns {boolean} - True si succès + */ +export function safeSetValue(elementId, value) { + const element = getElement(elementId); + if (!element) { + console.warn(`Élément ${elementId} non trouvé dans le DOM`); + return false; + } + + // Si c'est un select avec Select2 initialisé + if (element.tagName === 'SELECT' && window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) { + jQuery(element).val(value).trigger('change'); + } else { + element.value = value; + } + return true; +} + +/** + * Récupère la valeur d'un élément de manière sécurisée + * @param {string} elementId - ID de l'élément + * @returns {string|null} - Valeur de l'élément ou null + */ +export function safeGetValue(elementId) { + const element = getElement(elementId); + if (!element) { + console.warn(`Élément ${elementId} non trouvé dans le DOM`); + return null; + } + return element.value; +} + +/** + * Affiche ou cache un élément + * @param {string} elementId - ID de l'élément + * @param {boolean} show - True pour afficher, false pour cacher + */ +export function toggleElement(elementId, show) { + const element = getElement(elementId); + if (element) { + element.style.display = show ? '' : 'none'; + } +} + +/** + * Ajoute une classe à un élément + * @param {string} elementId - ID de l'élément + * @param {string} className - Classe à ajouter + */ +export function addClass(elementId, className) { + const element = getElement(elementId); + if (element) { + element.classList.add(className); + } +} + +/** + * Retire une classe d'un élément + * @param {string} elementId - ID de l'élément + * @param {string} className - Classe à retirer + */ +export function removeClass(elementId, className) { + const element = getElement(elementId); + if (element) { + element.classList.remove(className); + } +} + +/** + * Vérifie si un élément a une classe + * @param {string} elementId - ID de l'élément + * @param {string} className - Classe à vérifier + * @returns {boolean} - True si l'élément a la classe + */ +export function hasClass(elementId, className) { + const element = getElement(elementId); + return element ? element.classList.contains(className) : false; +} + +/** + * Définit le texte HTML d'un élément + * @param {string} elementId - ID de l'élément + * @param {string} html - HTML à définir + */ +export function setHTML(elementId, html) { + const element = getElement(elementId); + if (element) { + element.innerHTML = html; + } +} + +/** + * Définit le texte d'un élément + * @param {string} elementId - ID de l'élément + * @param {string} text - Texte à définir + */ +export function setText(elementId, text) { + const element = getElement(elementId); + if (element) { + element.textContent = text; + } +} + +/** + * Ajoute un event listener à un élément + * @param {string} elementId - ID de l'élément + * @param {string} event - Type d'événement + * @param {Function} handler - Gestionnaire d'événement + */ +export function addListener(elementId, event, handler) { + const element = getElement(elementId); + if (element) { + element.addEventListener(event, handler); + } +} + +/** + * Nettoie tous les champs d'un formulaire + * @param {Array} textFieldIds - IDs des champs texte + * @param {Array} selectFieldIds - IDs des selects + */ +export function clearFormFields(textFieldIds, selectFieldIds) { + // Nettoyer les champs de texte + textFieldIds.forEach(fieldId => { + const field = getElement(fieldId); + if (field) { + field.value = ''; + } + }); + + // Nettoyer les selects + selectFieldIds.forEach(selectId => { + const select = getElement(selectId); + if (select) { + select.value = ''; + + // Si c'est un (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 +/** + * Nettoie 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', + '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 - - - - - - - - - - `; - 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 = ''; - - // 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 = ''; - for (let hour = 8; hour <= 18; hour++) { - heureRdvInput.innerHTML += ``; - if (hour < 18) { // Pas de 18h30 - heureRdvInput.innerHTML += ``; - } - } - } - }); - } - - // 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(); - }); - } -}); \ No newline at end of file +// Réexporter les fonctions nécessaires +export { fillFormWithDate, fillFormWithEvent, resetForm, showFormErrors, clearFormErrors, handleEventFormSubmit };