Crvi/assets/js/modules/agenda-modal.js
2026-01-20 07:54:37 +01:00

4426 lines
183 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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