Crvi/assets/js/modules/agenda-admin-permanences.js
2026-01-21 21:11:35 +01:00

842 lines
28 KiB
JavaScript

/**
* Module Permanences Admin
* Gestion du formulaire d'encodage des permanences pour l'admin (avec sélection d'intervenant)
*/
import { apiFetch } from './agenda-api.js';
import toastr from 'toastr';
/**
* Initialise le module permanences admin
*/
export function initializeAdminPermanences() {
console.log('🚀 Initialisation du module permanences admin...');
const form = document.getElementById('admin-permanences-form');
if (!form) {
console.error('Formulaire admin-permanences-form non trouvé');
return;
}
// Préselection des jours et heures selon l'intervenant
setupIntervenantDefaults();
// Initialiser le champ mois de début avec le mois actuel
const moisDebutInput = document.getElementById('mois-debut');
if (moisDebutInput && !moisDebutInput.value) {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
moisDebutInput.value = `${year}-${month}`;
}
// Initialiser Select2 pour le champ langues
// Utiliser un petit délai pour s'assurer que le DOM est complètement prêt
setTimeout(() => {
initializeSelect2();
}, 100);
// Réinitialiser Select2 quand l'onglet devient visible (problème avec éléments cachés)
setupSelect2TabListener();
// Écouter les changements pour mettre à jour l'aperçu
setupPreviewListeners();
// Gérer la soumission du formulaire
form.addEventListener('submit', handleFormSubmit);
// Calculer l'aperçu initial
updatePreview();
// Initialiser le formulaire d'import CSV
initializeCsvImport();
}
// Variable de garde pour éviter les initialisations multiples simultanées
let isInitializingSelect2 = false;
let select2Initialized = false; // Garde globale pour éviter toute réinitialisation
/**
* Initialise Select2 pour le champ de sélection des langues
*/
function initializeSelect2() {
console.log('🔍 initializeSelect2() appelée - Stack:', new Error().stack);
// Éviter les appels multiples simultanés
if (isInitializingSelect2) {
console.log('⏳ Initialisation Select2 déjà en cours, attente...');
return;
}
// Si Select2 est déjà initialisé et fonctionne, ne pas réinitialiser
const languesSelect = document.getElementById('langues-permanences');
if (languesSelect && select2Initialized) {
const $select = jQuery(languesSelect);
if ($select.data('select2') && $select.hasClass('select2-hidden-accessible')) {
console.log('✅ Select2 déjà initialisé (flag=true), pas de réinitialisation');
return;
}
}
if (!languesSelect) {
console.warn('Select langues-permanences non trouvé');
return;
}
// Vérifier que jQuery et Select2 sont disponibles
if (typeof jQuery === 'undefined' || !jQuery.fn.select2) {
console.warn('jQuery ou Select2 non disponible, réessai dans 100ms...');
setTimeout(initializeSelect2, 100);
return;
}
const $select = jQuery(languesSelect);
// DESTRUCTION AGRESSIVE : Détruire tout Select2 au début, sans pitié !
try {
// Détruire l'instance Select2 si elle existe
if ($select.data('select2')) {
$select.select2('destroy');
}
// Supprimer TOUS les conteneurs Select2 dans le parent
const parentContainer = languesSelect.closest('.card-body') || languesSelect.parentElement;
if (parentContainer) {
// Supprimer tous les conteneurs Select2
const select2Containers = parentContainer.querySelectorAll('span.select2-container');
select2Containers.forEach(container => container.remove());
}
// Nettoyer tous les attributs Select2 du select
$select.removeClass('select2-hidden-accessible');
$select.removeAttr('data-select2-id');
$select.removeAttr('aria-hidden');
$select.removeAttr('tabindex');
console.log('💥 Select2 complètement détruit (méchamment)');
} catch (e) {
console.warn('Erreur lors de la destruction agressive:', e);
}
// Marquer qu'on est en train d'initialiser
isInitializingSelect2 = true;
select2Initialized = false; // Réinitialiser le flag
// Vérifier que le select a des options
const options = languesSelect.querySelectorAll('option');
if (options.length === 0) {
console.warn('Le select langues-permanences n\'a pas d\'options');
isInitializingSelect2 = false;
return;
}
// Initialiser Select2
try {
// Vérifier que les options sont valides
const validOptions = Array.from(options).filter(opt => {
return opt.value && opt.value.trim() !== '' && opt.textContent && opt.textContent.trim() !== '';
});
console.log('📋 Options valides trouvées:', validOptions.length, 'sur', options.length);
if (validOptions.length === 0) {
console.error('Aucune option valide trouvée dans le select');
return;
}
// S'assurer que le select est visible avant l'initialisation
const isVisible = languesSelect.offsetParent !== null;
if (!isVisible) {
console.warn('Le select langues-permanences n\'est pas visible, réessai dans 200ms...');
setTimeout(initializeSelect2, 200);
return;
}
// Initialiser Select2 - laisser Select2 lire automatiquement depuis le select natif
// Ne pas passer data pour éviter les conflits avec le select natif
$select.select2({
placeholder: 'Sélectionnez une ou plusieurs langues',
allowClear: true,
width: '100%',
language: {
noResults: function() {
return "Aucun résultat trouvé";
}
}
});
// Vérifier que Select2 a bien chargé les options
setTimeout(() => {
const select2Instance = $select.data('select2');
if (select2Instance) {
// Forcer Select2 à recharger les options depuis le select natif
$select.trigger('change.select2');
console.log('✅ Select2 initialisé pour langues-permanences avec', validOptions.length, 'options');
// Marquer comme initialisé
select2Initialized = true;
} else {
console.warn('⚠️ Select2 n\'a pas été correctement initialisé');
select2Initialized = false;
}
// Libérer la garde
isInitializingSelect2 = false;
}, 50);
} catch (e) {
console.error('Erreur lors de l\'initialisation de Select2:', e);
isInitializingSelect2 = false;
select2Initialized = false;
}
}
/**
* Configure un listener pour réinitialiser Select2 quand l'onglet devient visible
* (uniquement si Select2 n'est pas déjà correctement initialisé)
*/
function setupSelect2TabListener() {
const tabButtons = document.querySelectorAll('#permanences-tabs button[data-bs-toggle="tab"]');
tabButtons.forEach(button => {
button.addEventListener('shown.bs.tab', function(e) {
// Vérifier si l'onglet qui devient visible contient le select langues-permanences
const targetTabId = e.target.getAttribute('data-bs-target');
const targetTab = document.querySelector(targetTabId);
if (targetTab && targetTab.querySelector('#langues-permanences')) {
// Ne PAS réinitialiser si Select2 est déjà initialisé
// La garde globale select2Initialized empêche toute réinitialisation
if (!select2Initialized) {
console.log('🔄 Onglet changé, vérification Select2...');
setTimeout(() => {
initializeSelect2();
}, 100);
} else {
console.log('✅ Select2 déjà initialisé, pas de réinitialisation');
}
}
});
});
}
/**
* Configure la préselection des inputs jours[] et heures[]
* à partir des attributs data-days et data-time-slots de l'option sélectionnée
*/
function setupIntervenantDefaults() {
const select = document.getElementById('intervenant-select');
if (!select) return;
// Suivi des changements manuels pour ne pas écraser l'utilisateur
let hasUserChanged = false;
// Marquer qu'il y a eu modification dès qu'une case est cochée/décochée
const markChanged = () => { hasUserChanged = true; };
document.querySelectorAll('input[name="jours[]"]').forEach(input => {
input.addEventListener('change', markChanged);
});
document.querySelectorAll('input[name="heures[]"]').forEach(input => {
input.addEventListener('change', markChanged);
});
const applyDefaults = () => {
// Si l'utilisateur a déjà modifié les cases, ne pas écraser ses choix
if (hasUserChanged) {
// Laisser l'aperçu se recalculer d'après l'état courant
updatePreview();
return;
}
const option = select.options[select.selectedIndex];
if (!option) return;
const daysStr = option.getAttribute('data-days') || '';
const timesStr = option.getAttribute('data-time-slots') || '';
const days = daysStr
.split(',')
.map(s => s.trim().toLowerCase())
.filter(Boolean);
const times = timesStr
.split(',')
.map(s => s.trim())
.filter(Boolean);
// Réinitialiser toutes les cases
document.querySelectorAll('input[name="jours[]"]').forEach(input => {
input.checked = false;
});
document.querySelectorAll('input[name="heures[]"]').forEach(input => {
input.checked = false;
});
// Appliquer les jours
if (days.length > 0) {
document.querySelectorAll('input[name="jours[]"]').forEach(input => {
const value = String(input.value || '').toLowerCase();
if (days.includes(value)) {
input.checked = true;
}
});
}
// Appliquer les heures
if (times.length > 0) {
document.querySelectorAll('input[name="heures[]"]').forEach(input => {
const value = String(input.value || '').trim();
if (times.includes(value)) {
input.checked = true;
}
});
}
// Mettre à jour l'aperçu
updatePreview();
};
// Sur changement d'intervenant
select.addEventListener('change', applyDefaults);
// Initialisation à l'ouverture si une option est déjà sélectionnée
applyDefaults();
// Si le formulaire est réinitialisé, réactiver l'application auto des valeurs par défaut
const form = document.getElementById('admin-permanences-form');
if (form) {
form.addEventListener('reset', () => {
// Attendre la remise à zéro effective du DOM
setTimeout(() => {
hasUserChanged = false;
applyDefaults();
}, 0);
});
}
}
/**
* Configure les listeners pour mettre à jour l'aperçu en temps réel
*/
function setupPreviewListeners() {
// Écouter les changements de mois de début
const moisDebut = document.getElementById('mois-debut');
if (moisDebut) {
moisDebut.addEventListener('change', updatePreview);
moisDebut.addEventListener('input', updatePreview);
}
// Écouter les changements de période
const periodInputs = document.querySelectorAll('input[name="periode"]');
periodInputs.forEach(input => {
input.addEventListener('change', updatePreview);
});
// Écouter les changements de jours
const joursInputs = document.querySelectorAll('input[name="jours[]"]');
joursInputs.forEach(input => {
input.addEventListener('change', updatePreview);
});
// Écouter les changements des heures sélectionnées
const heuresInputs = document.querySelectorAll('input[name="heures[]"]');
heuresInputs.forEach(input => {
input.addEventListener('change', updatePreview);
});
// Écouter les changements de durée de permanence (1h ou 15min)
const dureeInputs = document.querySelectorAll('input[name="duree_permanence"]');
dureeInputs.forEach(input => {
input.addEventListener('change', updatePreview);
});
// Écouter les changements du nombre de tranches (si 15min)
const nbTranchesSelect = document.getElementById('nb-tranches');
if (nbTranchesSelect) {
nbTranchesSelect.addEventListener('change', updatePreview);
}
}
/**
* Calcule et affiche l'aperçu des permanences qui seront créées
*/
function updatePreview() {
const periode = parseInt(document.querySelector('input[name="periode"]:checked')?.value || '3');
const joursChecked = Array.from(document.querySelectorAll('input[name="jours[]"]:checked'))
.map(input => input.value);
const heuresChecked = Array.from(document.querySelectorAll('input[name="heures[]"]:checked'))
.map(input => input.value)
.sort(); // Trier les heures pour un affichage cohérent
const moisDebut = document.getElementById('mois-debut')?.value;
const dureePermanence = document.querySelector('input[name="duree_permanence"]:checked')?.value || '1h';
const nbTranches = dureePermanence === '15min' ? parseInt(document.getElementById('nb-tranches')?.value || '1') : 1;
if (heuresChecked.length === 0) {
clearPreview();
return;
}
if (!moisDebut) {
clearPreview();
return;
}
if (joursChecked.length === 0) {
clearPreview();
return;
}
// Calculer les tranches horaires à partir des heures sélectionnées
let tranches = [];
if (dureePermanence === '15min') {
// Mode 15 minutes : créer des tranches de 15 minutes
heuresChecked.forEach(heureDebut => {
const [h, m] = heureDebut.split(':').map(Number);
for (let i = 0; i < nbTranches; i++) {
const minutesDebut = m + (i * 15);
const minutesFin = m + ((i + 1) * 15);
const trancheDebut = `${String(h).padStart(2, '0')}:${String(minutesDebut).padStart(2, '0')}`;
const trancheFin = `${String(h).padStart(2, '0')}:${String(minutesFin).padStart(2, '0')}`;
tranches.push({
debut: trancheDebut,
fin: trancheFin
});
}
});
} else {
// Mode 1 heure : chaque heure sélectionnée = 1 tranche d'1 heure
tranches = heuresChecked.map(heureDebut => {
const [h, m] = heureDebut.split(':').map(Number);
const heureFin = `${String(h + 1).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
return {
debut: heureDebut,
fin: heureFin
};
});
}
// Calculer les dates à partir du mois de début sélectionné
const [year, month] = moisDebut.split('-').map(Number);
const startDate = new Date(year, month - 1, 1); // Premier jour du mois sélectionné
const endDate = new Date(year, month - 1 + periode, 0); // Dernier jour du mois de fin
// Compter les occurrences pour chaque jour sélectionné
let totalEvents = 0;
const joursMapping = {
'lundi': 1,
'mardi': 2,
'mercredi': 3,
'jeudi': 4,
'vendredi': 5,
'samedi': 6,
'dimanche': 0
};
joursChecked.forEach(jour => {
const jourNum = joursMapping[jour.toLowerCase()];
if (jourNum !== undefined) {
const occurrences = countDayOccurrences(jourNum, startDate, endDate);
totalEvents += occurrences * tranches.length;
}
});
// Afficher les tranches
displayTranches(tranches);
// Afficher l'estimation
displayEstimation(totalEvents);
}
/**
* Calcule les tranches horaires d'1 heure dans une plage
*/
function calculateTranches(heureDebut, heureFin) {
const tranches = [];
const [debutH, debutM] = heureDebut.split(':').map(Number);
const [finH, finM] = heureFin.split(':').map(Number);
let currentH = debutH;
let currentM = debutM;
while (currentH < finH || (currentH === finH && currentM < finM)) {
const trancheDebut = `${String(currentH).padStart(2, '0')}:${String(currentM).padStart(2, '0')}`;
// Ajouter 1 heure
currentH += 1;
const trancheFin = `${String(currentH).padStart(2, '0')}:${String(currentM).padStart(2, '0')}`;
// Vérifier que la tranche ne dépasse pas l'heure de fin
if (currentH < finH || (currentH === finH && currentM <= finM)) {
tranches.push({
debut: trancheDebut,
fin: trancheFin
});
} else {
break;
}
}
return tranches;
}
/**
* Compte le nombre d'occurrences d'un jour de la semaine dans une plage de dates
*/
function countDayOccurrences(jourSemaine, startDate, endDate) {
let count = 0;
const current = new Date(startDate);
// Ajuster au premier jour de la semaine ciblée
const currentDay = current.getDay();
const diff = (jourSemaine - currentDay + 7) % 7;
current.setDate(current.getDate() + diff);
// Compter les occurrences
while (current <= endDate) {
count++;
current.setDate(current.getDate() + 7); // Semaine suivante
}
return count;
}
/**
* Affiche les tranches horaires générées
*/
function displayTranches(tranches) {
const previewEl = document.getElementById('tranches-preview');
if (!previewEl) return;
if (tranches.length === 0) {
previewEl.innerHTML = '<i class="fas fa-info-circle me-2"></i>Veuillez sélectionner une plage horaire valide.';
return;
}
const tranchesList = tranches.map(t => `<li>${t.debut}${t.fin}</li>`).join('');
previewEl.innerHTML = `
<ul>${tranchesList}</ul>
`;
}
/**
* Affiche l'estimation du nombre d'événements
*/
function displayEstimation(count) {
const containerEl = document.getElementById('estimation-container');
const countEl = document.getElementById('estimation-count');
if (!containerEl || !countEl) return;
if (count > 0) {
countEl.textContent = count;
containerEl.style.display = 'block';
} else {
containerEl.style.display = 'none';
}
}
/**
* Efface l'aperçu
*/
function clearPreview() {
const previewEl = document.getElementById('tranches-preview');
if (previewEl) {
previewEl.innerHTML = '<i class="fas fa-info-circle me-2"></i>Veuillez sélectionner une plage horaire pour voir l\'aperçu.';
}
const containerEl = document.getElementById('estimation-container');
if (containerEl) {
containerEl.style.display = 'none';
}
}
/**
* Gère la soumission du formulaire
*/
async function handleFormSubmit(e) {
e.preventDefault();
const form = e.target;
const submitBtn = document.getElementById('submit-permanences-btn');
// Validation
if (!validateForm()) {
return;
}
// Récupérer les données du formulaire
const formData = new FormData(form);
const intervenantId = formData.get('intervenant_id');
const periode = parseInt(formData.get('periode'));
const moisDebut = formData.get('mois_debut');
const jours = formData.getAll('jours[]');
const heures = formData.getAll('heures[]');
const dureePermanence = document.querySelector('input[name="duree_permanence"]:checked')?.value || '1h';
const nbTranches = dureePermanence === '15min' ? parseInt(document.getElementById('nb-tranches')?.value || '1') : null;
// Récupérer les langues sélectionnées via Select2
const languesSelect = document.getElementById('langues-permanences');
const langues = languesSelect && typeof jQuery !== 'undefined' && jQuery(languesSelect).val()
? jQuery(languesSelect).val().filter(value => value !== '')
: [];
const informationsComplementaires = formData.get('informations_complementaires') || '';
// Validation supplémentaire
if (!intervenantId) {
toastr.error('Veuillez sélectionner un intervenant', 'Erreur');
return;
}
if (!moisDebut) {
toastr.error('Veuillez sélectionner un mois de début', 'Erreur');
return;
}
if (jours.length === 0) {
toastr.error('Veuillez sélectionner au moins un jour de la semaine', 'Erreur');
return;
}
if (heures.length === 0) {
toastr.error('Veuillez sélectionner au moins une heure de permanence', 'Erreur');
return;
}
// Calculer la plage horaire globale (min et max des heures sélectionnées)
const heuresSorted = heures.map(h => {
const [hour] = h.split(':').map(Number);
return hour;
}).sort((a, b) => a - b);
const heureMin = heuresSorted[0];
const heureMax = heuresSorted[heuresSorted.length - 1];
const plageDebut = `${String(heureMin).padStart(2, '0')}:00`;
const plageFin = `${String(heureMax + 1).padStart(2, '0')}:00`;
// Désactiver le bouton
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Création en cours...';
// Préparer les données pour l'API
const data = {
intervenant_id: parseInt(intervenantId),
periode: periode,
mois_debut: moisDebut,
jours: jours,
plage_horaire: {
debut: plageDebut,
fin: plageFin
},
heures: heures, // Envoyer aussi les heures individuelles pour le backend
duree_permanence: dureePermanence, // '1h' ou '15min'
nb_tranches: nbTranches, // Nombre de tranches si 15min (1-4), null si 1h
langues: langues, // Langues sélectionnées (peut être un tableau vide)
informations_complementaires: informationsComplementaires
};
try {
// Appel API (apiFetch retourne directement data, pas response.data)
const result = await apiFetch('admin/permanences', {
method: 'POST',
body: JSON.stringify(data)
});
toastr.success(
`${result.message} (${result.permanences_crees} permanences créées)`,
'Succès',
{ timeOut: 5000 }
);
// Réinitialiser le formulaire
form.reset();
clearPreview();
// Réinitialiser la période par défaut
document.getElementById('periode-3').checked = true;
} catch (error) {
console.error('Erreur lors de la création des permanences:', error);
toastr.error('Une erreur est survenue lors de la création des permanences', 'Erreur');
} finally {
// Réactiver le bouton
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Enregistrer les permanences';
}
}
/**
* Valide le formulaire avant soumission
*/
function validateForm() {
const intervenantId = document.getElementById('intervenant-select')?.value;
const periode = document.querySelector('input[name="periode"]:checked')?.value;
const moisDebut = document.getElementById('mois-debut')?.value;
const joursChecked = document.querySelectorAll('input[name="jours[]"]:checked');
const heuresChecked = document.querySelectorAll('input[name="heures[]"]:checked');
if (!intervenantId) {
toastr.error('Veuillez sélectionner un intervenant', 'Validation');
return false;
}
if (!moisDebut) {
toastr.error('Veuillez sélectionner un mois de début', 'Validation');
return false;
}
if (!periode) {
toastr.error('Veuillez sélectionner une période (3 ou 6 mois)', 'Validation');
return false;
}
if (joursChecked.length === 0) {
toastr.error('Veuillez sélectionner au moins un jour de la semaine', 'Validation');
return false;
}
if (heuresChecked.length === 0) {
toastr.error('Veuillez sélectionner au moins une heure de permanence', 'Validation');
return false;
}
return true;
}
/**
* Initialise le formulaire d'import CSV
*/
function initializeCsvImport() {
const csvForm = document.getElementById('import-csv-form');
if (!csvForm) {
console.warn('Formulaire import-csv-form non trouvé');
return;
}
csvForm.addEventListener('submit', handleCsvImportSubmit);
}
/**
* Gère la soumission du formulaire d'import CSV
*/
async function handleCsvImportSubmit(e) {
e.preventDefault();
const form = e.target;
const fileInput = document.getElementById('csv-file');
const submitBtn = document.getElementById('submit-csv-import-btn');
const resultContainer = document.getElementById('csv-import-result');
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
toastr.error('Veuillez sélectionner un fichier CSV', 'Erreur');
return;
}
const file = fileInput.files[0];
// Vérifier que c'est un fichier CSV
if (!file.name.toLowerCase().endsWith('.csv')) {
toastr.error('Le fichier doit être au format CSV', 'Erreur');
return;
}
// Désactiver le bouton
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Import en cours...';
// Créer FormData pour l'upload
const formData = new FormData();
formData.append('file', file);
try {
// Appel API pour l'import CSV
const url = '/wp-json/crvi/v1/admin/permanences/import-csv';
const response = await fetch(url, {
method: 'POST',
headers: {
'X-WP-Nonce': window.wpApiSettings?.nonce || ''
},
body: formData
});
const data = await response.json();
if (!response.ok || !data.success) {
const errorMessage = data.error?.message || data.message || 'Erreur lors de l\'import';
throw new Error(errorMessage);
}
// Afficher les résultats
displayCsvImportResult(data.data, resultContainer);
toastr.success(
`Import réussi : ${data.data.created || 0} créés, ${data.data.errors || 0} erreurs`,
'Succès',
{ timeOut: 5000 }
);
// Réinitialiser le formulaire
form.reset();
} catch (error) {
console.error('Erreur lors de l\'import CSV:', error);
toastr.error(error.message || 'Une erreur est survenue lors de l\'import CSV', 'Erreur');
if (resultContainer) {
resultContainer.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
<strong>Erreur :</strong> ${error.message || 'Une erreur est survenue lors de l\'import'}
</div>
`;
resultContainer.style.display = 'block';
}
} finally {
// Réactiver le bouton
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-upload me-2"></i>Importer le CSV';
}
}
/**
* Affiche les résultats de l'import CSV
*/
function displayCsvImportResult(result, container) {
if (!container) return;
let html = '';
if (result.errors && result.errors.length > 0) {
html += `
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Attention :</strong> ${result.errors.length} erreur(s) détectée(s)
</div>
`;
if (result.errors.length <= 10) {
html += '<ul class="list-group mt-2">';
result.errors.forEach((error, index) => {
html += `
<li class="list-group-item">
<strong>Ligne ${error.line || '?'} :</strong> ${error.message || 'Erreur inconnue'}
</li>
`;
});
html += '</ul>';
}
}
if (result.created > 0) {
html += `
<div class="alert alert-success mt-3">
<i class="fas fa-check-circle me-2"></i>
<strong>Succès :</strong> ${result.created} permanence(s) créée(s) avec succès
</div>
`;
}
container.innerHTML = html;
container.style.display = 'block';
}