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 = '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 = '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 = `${statutLabel}`; } // 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} 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 = `${statutLabel}`; } // 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 += `${presentCount} `; } if (absentCount > 0) { badgeHTML += `${absentCount}`; } 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 = 'Présent'; } else if (statut === 'absence') { badge.innerHTML = 'Absent'; } else { badge.innerHTML = 'Clôturé'; } 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 , 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} 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 = `
Aucun rendez-vous trouvé pour ce bénéficiaire.
`; return; } let html = '
'; 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 ? '' : ''; html += `
${incidentIcon} ${dateFormatted} à ${heureFormatted}
Intervenant: ${intervenantNom}
Type d'intervention: ${typeInterventionNom}
${hasIncident ? `
Incident signalé:
Résumé: ${rdv.incident.resume_incident || 'Non renseigné'}
${rdv.incident.commentaire_incident ? `
Commentaire: ${rdv.incident.commentaire_incident}
` : ''}
` : ''}
${index < historiqueData.length - 1 ? '
' : ''}
`; }); html += '
'; 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 = `
Chargement...
`; } // 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 = `
Erreur lors du chargement de l'historique.
`; } }); }, 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 = ''; // 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} - 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 = '

Aucun incident signalé pour cet événement.

'; } else { // Afficher les incidents let html = '
'; incidents.forEach((incident, index) => { const createdAt = incident.created_at ? new Date(incident.created_at).toLocaleDateString('fr-FR') : 'Date inconnue'; html += `
Incident #${index + 1}
${createdAt}
${incident.resume_incident ? `

Résumé : ${incident.resume_incident}

` : ''} ${incident.commentaire_incident ? `

${incident.commentaire_incident}

` : ''}
`; }); html += '
'; listContainer.innerHTML = html; } // Changer le titre de la modal if (modalTitle) { modalTitle.innerHTML = '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 = '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 : ''; 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 = ` `; 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(); }); } });