fix: js errors

This commit is contained in:
2025-08-12 06:41:39 +02:00
parent c132419213
commit 11bfc5c5cb
3 changed files with 1284 additions and 1 deletions

View File

@@ -1058,9 +1058,15 @@ class FocusManager {
// Vider le conteneur
focusContainer.innerHTML = '';
// Sauvegarder les données JSON avant clonage pour éviter la troncature
const savedJsonData = this.preserveJsonDataBeforeCloning(currentStudent);
// Cloner l'élément élève
const clonedStudent = currentStudent.cloneNode(true);
// Restaurer les données JSON après clonage
this.restoreJsonDataAfterCloning(clonedStudent, savedJsonData);
// Marquer comme élément focus pour la synchronisation
const studentId = clonedStudent.dataset.studentCard;
clonedStudent.setAttribute('data-focus-clone-of', studentId);
@@ -1152,6 +1158,406 @@ class FocusManager {
}
});
}
// 4. Gestion des barres de progression
this.setupProgressBars(clonedStudent);
}
setupProgressBars(clonedStudent) {
// Configure les interactions avec les barres de progression des compétences et domaines
try {
// Trouver toutes les barres de progression
const progressBars = clonedStudent.querySelectorAll('.progress-bar-container[data-assessments]');
if (progressBars.length === 0) {
return;
}
progressBars.forEach((bar) => {
try {
// Ajouter les tooltips dynamiques pour la barre complète
this.setupProgressBarTooltip(bar);
// Configurer les segments individuels
this.setupProgressBarSegments(bar);
// Déclencher l'animation d'apparition après un court délai
setTimeout(() => {
bar.classList.add('progress-animated');
}, 300);
} catch (barError) {
console.error('❌ Erreur configuration barre de progression:', barError.message);
}
});
// Animation séquentielle des barres et segments
this.animateProgressBarsSequentially(clonedStudent);
} catch (error) {
console.error('❌ Erreur lors de la configuration des barres de progression:', error.message);
}
}
setupProgressBarTooltip(progressBar) {
try {
const competenceName = progressBar.dataset.competenceName || progressBar.dataset.domainName;
// Nouvelle approche : Extraire les données depuis les segments visibles
const assessmentsData = this.extractAssessmentDataFromSegments(progressBar);
if (assessmentsData && assessmentsData.length > 0) {
// Construire le contenu du tooltip depuis les données extraites
let tooltipContent = `${competenceName || 'Progression'}:\n`;
assessmentsData.forEach(assessment => {
const percentage = assessment.max > 0 ? Math.round((assessment.earned / assessment.max) * 100) : 0;
tooltipContent += `${assessment.title}: ${assessment.earned}/${assessment.max} (${percentage}%)\n`;
});
// Ajouter l'attribut data-tooltip pour les CSS
progressBar.setAttribute('data-tooltip', tooltipContent);
// Événements hover pour améliorer l'UX
progressBar.addEventListener('mouseenter', () => {
progressBar.style.zIndex = '1001';
});
progressBar.addEventListener('mouseleave', () => {
progressBar.style.zIndex = '';
});
} else {
// Fallback : Essayer l'ancienne méthode comme dernier recours
this.setupProgressBarTooltipFallback(progressBar);
}
} catch (error) {
console.error('❌ Erreur setup tooltip barre de progression:', error.message);
// En cas d'erreur, essayer le fallback
this.setupProgressBarTooltipFallback(progressBar);
}
}
extractAssessmentDataFromSegments(progressBar) {
try {
const segments = progressBar.querySelectorAll('.progress-segment[data-assessment-title]');
if (segments.length === 0) {
return [];
}
const assessmentsData = [];
segments.forEach(segment => {
const assessmentData = {
id: segment.dataset.assessmentId || Math.random(),
title: segment.dataset.assessmentTitle || 'Évaluation',
earned: parseFloat(segment.dataset.earnedThis) || 0,
max: parseFloat(segment.dataset.maxThis) || 0,
performance: parseFloat(segment.dataset.assessmentPerformance) || 0,
contribution: parseFloat(segment.dataset.contributionPercentage) || 0
};
// Validation des données extraites
if (assessmentData.title && assessmentData.title !== 'Évaluation') {
assessmentsData.push(assessmentData);
}
});
return assessmentsData;
} catch (error) {
console.error('❌ Erreur extraction données segments:', error.message);
return [];
}
}
setupProgressBarTooltipFallback(progressBar) {
try {
const rawAssessmentsData = progressBar.dataset.assessments;
if (!rawAssessmentsData || rawAssessmentsData.trim() === '') {
return;
}
// Protection contre les JSON malformés
if (!rawAssessmentsData.trim().startsWith('[') && !rawAssessmentsData.trim().startsWith('{')) {
return;
}
let assessmentsData;
try {
assessmentsData = JSON.parse(rawAssessmentsData);
} catch (parseError) {
return;
}
const competenceName = progressBar.dataset.competenceName || progressBar.dataset.domainName;
if (assessmentsData && Array.isArray(assessmentsData) && assessmentsData.length > 0) {
let tooltipContent = `${competenceName || 'Progression'}:\n`;
assessmentsData.forEach(assessment => {
const earned = assessment.earned_this || assessment.earned || 0;
const max = assessment.max_this || assessment.max || 0;
const title = assessment.title || 'Évaluation';
const percentage = max > 0 ? Math.round((earned / max) * 100) : 0;
tooltipContent += `${title}: ${earned}/${max} (${percentage}%)\n`;
});
progressBar.setAttribute('data-tooltip', tooltipContent);
}
} catch (error) {
// Échec silencieux pour le fallback
}
}
setupProgressBarSegments(progressBar) {
// Configure les interactions avec les nouvelles barres segmentées
const segmentedProgressBar = progressBar.querySelector('.segmented-progress-bar');
const segments = progressBar.querySelectorAll('.progress-segment');
const progressContainer = progressBar.closest('.segmented-progress');
if (!segmentedProgressBar || !progressContainer) return;
// Configuration de l'expansion/contraction
this.setupSegmentedProgressInteractions(progressContainer, segmentedProgressBar, segments);
// Configuration des segments individuels
segments.forEach((segment, index) => {
this.setupSegmentInteractions(segment, index, segments);
});
// Support clavier pour l'accessibilité
this.setupKeyboardNavigation(progressContainer, segments);
}
setupSegmentedProgressInteractions(container, progressBar, segments) {
// Le mode expandé est maintenant par défaut, donc moins d'interactions nécessaires
// Garde juste les interactions de base pour la consistance
// Effet de focus pour l'accessibilité
container.addEventListener('mouseenter', () => {
container.style.transform = 'translateY(-1px)';
});
container.addEventListener('mouseleave', () => {
container.style.transform = '';
});
}
setupSegmentInteractions(segment, index, allSegments) {
// Tooltip enrichi au hover
this.enhanceSegmentTooltip(segment);
// Clic sur un segment pour afficher les détails
segment.addEventListener('click', (e) => {
e.stopPropagation();
this.showSegmentDetails(segment);
});
// Effets visuels améliorés
segment.addEventListener('mouseenter', () => {
this.highlightSegment(segment, allSegments);
});
segment.addEventListener('mouseleave', () => {
this.resetSegmentHighlights(allSegments);
});
// Support focus pour accessibilité
segment.addEventListener('focus', () => {
this.highlightSegment(segment, allSegments);
});
segment.addEventListener('blur', () => {
this.resetSegmentHighlights(allSegments);
});
}
// Méthodes utilitaires pour les barres segmentées
setupKeyboardNavigation(container, segments) {
container.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowRight':
case 'ArrowLeft':
// Navigation directe entre les segments
e.preventDefault();
this.navigateSegments(segments, e.key === 'ArrowRight' ? 1 : -1);
break;
case 'Enter':
case ' ':
// Activer le segment focalisé
e.preventDefault();
const focusedSegment = document.activeElement;
if (focusedSegment && focusedSegment.classList.contains('progress-segment')) {
focusedSegment.click();
}
break;
}
});
}
navigateSegments(segments, direction) {
const focusedSegment = document.activeElement;
const currentIndex = Array.from(segments).indexOf(focusedSegment);
if (currentIndex !== -1) {
const nextIndex = Math.max(0, Math.min(segments.length - 1, currentIndex + direction));
segments[nextIndex].focus();
} else if (segments.length > 0) {
segments[direction > 0 ? 0 : segments.length - 1].focus();
}
}
highlightSegment(targetSegment, allSegments) {
allSegments.forEach((segment, index) => {
if (segment === targetSegment) {
segment.style.filter = 'brightness(1.2) saturate(1.1)';
segment.style.transform = 'scaleY(1.05)';
segment.style.zIndex = '20';
} else {
segment.style.filter = 'brightness(0.8) saturate(0.8)';
segment.style.opacity = '0.7';
}
});
}
resetSegmentHighlights(allSegments) {
allSegments.forEach(segment => {
segment.style.filter = '';
segment.style.transform = '';
segment.style.zIndex = '';
segment.style.opacity = '';
});
}
showSegmentDetails(segment) {
const assessmentTitle = segment.dataset.assessmentTitle;
const earnedThis = segment.dataset.earnedThis;
const maxThis = segment.dataset.maxThis;
const performance = segment.dataset.assessmentPerformance;
const contributionPercentage = segment.dataset.contributionPercentage;
const content = `
<div class="space-y-3">
<div class="text-center">
<h4 class="font-semibold text-lg">${assessmentTitle}</h4>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="bg-blue-50 p-3 rounded-lg">
<div class="font-medium text-blue-800">Points obtenus</div>
<div class="text-xl font-bold text-blue-900">${earnedThis}/${maxThis}</div>
</div>
<div class="bg-green-50 p-3 rounded-lg">
<div class="font-medium text-green-800">Performance</div>
<div class="text-xl font-bold text-green-900">${performance}%</div>
</div>
</div>
<div class="bg-purple-50 p-3 rounded-lg">
<div class="font-medium text-purple-800 mb-1">Contribution au total</div>
<div class="w-full bg-purple-200 rounded-full h-2">
<div class="bg-purple-600 h-2 rounded-full transition-all duration-500"
style="width: ${contributionPercentage}%"></div>
</div>
<div class="text-sm text-purple-700 mt-1">${contributionPercentage}% du total de la compétence</div>
</div>
</div>
`;
const modal = this.parent.ui.createModal(`Détails - ${assessmentTitle}`, content);
document.body.appendChild(modal);
}
announceStateChange(state) {
// Annonce vocale pour les lecteurs d'écran
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = `Barre de progression ${state}`;
document.body.appendChild(announcement);
setTimeout(() => announcement.remove(), 1000);
}
enhanceSegmentTooltip(segment) {
// Améliore le tooltip avec plus d'informations visuelles
const assessmentTitle = segment.dataset.assessmentTitle;
const performance = segment.dataset.assessmentPerformance;
const earnedThis = segment.dataset.earnedThis;
const maxThis = segment.dataset.maxThis;
const contributionPercentage = segment.dataset.contributionPercentage;
// Créer un tooltip enrichi pour les nouveaux segments
const tooltipContent = `${assessmentTitle}: ${earnedThis}/${maxThis} pts (${performance}%) - ${contributionPercentage}% du total. Cliquez pour plus de détails.`;
segment.title = tooltipContent;
// Ajouter une classe pour identifier le niveau de performance
const perfNum = parseInt(performance);
if (perfNum >= 90) {
segment.classList.add('segment-excellent');
} else if (perfNum >= 70) {
segment.classList.add('segment-good');
} else if (perfNum >= 50) {
segment.classList.add('segment-average');
} else {
segment.classList.add('segment-struggling');
}
// Ajouter un indicateur visuel de performance dans le style
segment.style.setProperty('--performance-level', perfNum);
}
animateProgressBarsSequentially(clonedStudent) {
// Anime les barres de progression de façon séquentielle pour un effet visuel agréable
const competenceBars = clonedStudent.querySelectorAll('.competence-progress-bar .progress-bar-fill');
const domainBars = clonedStudent.querySelectorAll('.domain-progress-bar .progress-bar-fill');
// Combiner toutes les barres
const allBars = [...competenceBars, ...domainBars];
// Animer chaque barre avec un délai progressif
allBars.forEach((bar, index) => {
// Sauvegarder la largeur finale
const finalWidth = bar.style.width;
// Commencer à 0
bar.style.width = '0%';
// Animer vers la valeur finale avec un délai
setTimeout(() => {
bar.style.width = finalWidth;
bar.style.transition = 'width 0.8s ease-out';
// Effet de "pulse" léger à la fin de l'animation
setTimeout(() => {
bar.style.transform = 'scaleY(1.1)';
setTimeout(() => {
bar.style.transform = 'scaleY(1)';
bar.style.transition = 'width 0.8s ease-out, transform 0.2s ease-out';
}, 150);
}, 800);
}, index * 100); // Délai de 100ms entre chaque barre
});
}
syncProgressBarsToOriginal(studentId) {
// Synchronise les barres de progression entre l'élément original et le clone focus
// Cette fonction sera appelée quand les données changent
// Pour l'instant, les données sont statiques, mais elle sera utile pour les futures évolutions
const originalStudent = document.querySelector(`[data-student-card="${studentId}"]`);
const focusStudent = document.querySelector(`[data-focus-clone-of="${studentId}"]`);
if (!originalStudent || !focusStudent) return;
// Synchroniser les valeurs des barres si nécessaire
console.log(`🔄 Synchronisation des barres de progression pour l'élève ${studentId}`);
}
async saveFocusAppreciation(studentId, appreciation, immediate = false) {
@@ -1352,6 +1758,77 @@ class FocusManager {
window.scrollTo(0, 0);
}
preserveJsonDataBeforeCloning(originalStudent) {
const jsonDataMap = new Map();
try {
const progressBars = originalStudent.querySelectorAll('.progress-bar-container[data-assessments]');
progressBars.forEach((bar, index) => {
const rawData = bar.dataset.assessments;
if (rawData && rawData.trim() !== '') {
const competenceName = bar.dataset.competenceName || bar.dataset.domainName || `progress-${index}`;
const key = `${competenceName}-${index}`;
const preservedData = {
rawData: rawData,
competenceName: competenceName,
domainName: bar.dataset.domainName,
index: index
};
// Essayer de parser pour valider
try {
preservedData.parsedData = JSON.parse(rawData);
preservedData.isValid = true;
} catch (parseError) {
preservedData.isValid = false;
preservedData.parseError = parseError.message;
}
jsonDataMap.set(key, preservedData);
}
});
} catch (error) {
console.error('❌ Erreur sauvegarde données JSON:', error.message);
}
return jsonDataMap;
}
restoreJsonDataAfterCloning(clonedStudent, savedJsonData) {
if (!savedJsonData || savedJsonData.size === 0) {
return;
}
try {
const progressBars = clonedStudent.querySelectorAll('.progress-bar-container');
progressBars.forEach((bar, index) => {
const competenceName = bar.dataset.competenceName || bar.dataset.domainName || `progress-${index}`;
const key = `${competenceName}-${index}`;
const savedData = savedJsonData.get(key);
if (savedData) {
// Restaurer les données sauvegardées
bar.dataset.assessments = savedData.rawData;
if (savedData.competenceName) {
bar.dataset.competenceName = savedData.competenceName;
}
if (savedData.domainName) {
bar.dataset.domainName = savedData.domainName;
}
}
});
} catch (error) {
console.error('❌ Erreur restauration données JSON:', error.message);
}
}
// Méthode appelée après les filtres - NON UTILISÉE en mode focus
onFiltersChanged() {
// En mode focus, on ignore les filtres