fix: js errors
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user