fix: js errors
This commit is contained in:
@@ -21,6 +21,7 @@ class StudentTrimesterSummary:
|
||||
grades_by_assessment: Dict[int, Dict] # assessment_id -> {'score': float, 'max': float, 'title': str}
|
||||
appreciation: Optional[CouncilAppreciation]
|
||||
performance_status: str # 'excellent', 'good', 'average', 'struggling'
|
||||
competence_domain_breakdown: Optional[Dict] = None # Données des compétences et domaines
|
||||
|
||||
@property
|
||||
def has_appreciation(self) -> bool:
|
||||
@@ -97,13 +98,17 @@ class StudentEvaluationService:
|
||||
student_id, student.class_group_id, trimester
|
||||
)
|
||||
|
||||
# Calculer les données de compétences et domaines
|
||||
competence_domain_breakdown = self.get_student_competence_domain_breakdown(student_id, trimester)
|
||||
|
||||
return StudentTrimesterSummary(
|
||||
student=student,
|
||||
overall_average=overall_average,
|
||||
assessment_count=len([a for a in assessments if self._has_grades(student_id, a)]),
|
||||
grades_by_assessment=grades_by_assessment,
|
||||
appreciation=appreciation,
|
||||
performance_status=performance_status
|
||||
performance_status=performance_status,
|
||||
competence_domain_breakdown=competence_domain_breakdown
|
||||
)
|
||||
|
||||
def get_students_summaries(self, class_group_id: int, trimester: int) -> List[StudentTrimesterSummary]:
|
||||
@@ -121,6 +126,229 @@ class StudentEvaluationService:
|
||||
|
||||
return summaries
|
||||
|
||||
def get_student_competence_domain_breakdown(self, student_id: int, trimester: int) -> Dict[str, List[Dict]]:
|
||||
"""
|
||||
Calcule la répartition des points par compétence et domaine pour un élève.
|
||||
|
||||
Returns:
|
||||
Dict avec 'competences' et 'domains', chacun contenant les points accumulés par évaluation
|
||||
"""
|
||||
from models import Student, db, GradingCalculator
|
||||
|
||||
student = Student.query.get(student_id)
|
||||
if not student:
|
||||
return {'competences': [], 'domains': []}
|
||||
|
||||
# Récupérer toutes les évaluations du trimestre
|
||||
assessments = self.assessment_repo.find_by_class_trimester_with_details(
|
||||
student.class_group_id, trimester
|
||||
)
|
||||
|
||||
# Structures pour accumuler les données
|
||||
competences_data = {}
|
||||
domains_data = {}
|
||||
|
||||
# Parcourir chaque évaluation
|
||||
for assessment in assessments:
|
||||
grades = self.grade_repo.get_student_grades_by_assessment(student_id, assessment.id)
|
||||
|
||||
for grade in grades:
|
||||
element = grade.grading_element
|
||||
|
||||
# Calculer le score pour cet élément
|
||||
if grade.value and GradingCalculator.is_counted_in_total(grade.value, element.grading_type):
|
||||
score = GradingCalculator.calculate_score(
|
||||
grade.value, element.grading_type, element.max_points
|
||||
)
|
||||
|
||||
if score is not None:
|
||||
# Traiter les compétences
|
||||
if element.skill:
|
||||
if element.skill not in competences_data:
|
||||
competences_data[element.skill] = {
|
||||
'name': element.skill,
|
||||
'earned_points': 0.0,
|
||||
'total_points': 0.0,
|
||||
'assessments': {}
|
||||
}
|
||||
|
||||
# Accumuler les points
|
||||
competences_data[element.skill]['earned_points'] += score
|
||||
competences_data[element.skill]['total_points'] += element.max_points
|
||||
|
||||
# Grouper par évaluation
|
||||
if assessment.id not in competences_data[element.skill]['assessments']:
|
||||
competences_data[element.skill]['assessments'][assessment.id] = {
|
||||
'id': assessment.id,
|
||||
'title': assessment.title,
|
||||
'earned': 0.0,
|
||||
'max': 0.0,
|
||||
'date': assessment.date
|
||||
}
|
||||
|
||||
competences_data[element.skill]['assessments'][assessment.id]['earned'] += score
|
||||
competences_data[element.skill]['assessments'][assessment.id]['max'] += element.max_points
|
||||
|
||||
# Traiter les domaines
|
||||
if element.domain_id:
|
||||
domain = element.domain
|
||||
domain_name = domain.name if domain else f"Domaine {element.domain_id}"
|
||||
|
||||
if domain_name not in domains_data:
|
||||
domains_data[domain_name] = {
|
||||
'id': element.domain_id,
|
||||
'name': domain_name,
|
||||
'color': domain.color if domain else '#6B7280',
|
||||
'earned_points': 0.0,
|
||||
'total_points': 0.0,
|
||||
'assessments': {}
|
||||
}
|
||||
|
||||
# Accumuler les points
|
||||
domains_data[domain_name]['earned_points'] += score
|
||||
domains_data[domain_name]['total_points'] += element.max_points
|
||||
|
||||
# Grouper par évaluation
|
||||
if assessment.id not in domains_data[domain_name]['assessments']:
|
||||
domains_data[domain_name]['assessments'][assessment.id] = {
|
||||
'id': assessment.id,
|
||||
'title': assessment.title,
|
||||
'earned': 0.0,
|
||||
'max': 0.0,
|
||||
'date': assessment.date
|
||||
}
|
||||
|
||||
domains_data[domain_name]['assessments'][assessment.id]['earned'] += score
|
||||
domains_data[domain_name]['assessments'][assessment.id]['max'] += element.max_points
|
||||
|
||||
# Récupérer la configuration des compétences pour les couleurs
|
||||
from app_config import config_manager
|
||||
competences_config = {comp['name']: comp for comp in config_manager.get_competences_list()}
|
||||
|
||||
# Finaliser les données des compétences
|
||||
competences = []
|
||||
for comp_name, comp_data in competences_data.items():
|
||||
config = competences_config.get(comp_name, {})
|
||||
|
||||
# Calculer le pourcentage
|
||||
percentage = (comp_data['earned_points'] / comp_data['total_points'] * 100) if comp_data['total_points'] > 0 else 0
|
||||
|
||||
# Convertir les assessments en liste avec calculs d'accumulation
|
||||
assessments_list = []
|
||||
# Palette de couleurs moderne et contrastée
|
||||
colors_palette = ['#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#06B6D4', '#84CC16']
|
||||
|
||||
# D'abord, trier par date pour avoir l'ordre chronologique
|
||||
sorted_assessments = sorted(comp_data['assessments'].values(),
|
||||
key=lambda x: x['date'] if x['date'] else datetime.min)
|
||||
|
||||
# Calculer l'accumulation progressive
|
||||
cumulative_earned = 0
|
||||
cumulative_max = 0
|
||||
|
||||
for i, assessment_data in enumerate(sorted_assessments):
|
||||
# Ajouter les points de cette évaluation à l'accumulation
|
||||
cumulative_earned += assessment_data['earned']
|
||||
cumulative_max += assessment_data['max']
|
||||
|
||||
# Calculer les pourcentages
|
||||
cumulative_percentage = (cumulative_earned / comp_data['total_points'] * 100) if comp_data['total_points'] > 0 else 0
|
||||
assessment_performance = (assessment_data['earned'] / assessment_data['max'] * 100) if assessment_data['max'] > 0 else 0
|
||||
# Calculer le pourcentage de contribution de cette évaluation par rapport au total final
|
||||
contribution_percentage = (assessment_data['earned'] / comp_data['total_points'] * 100) if comp_data['total_points'] > 0 else 0
|
||||
|
||||
# Couleur cohérente basée sur l'ID de l'évaluation pour garantir la cohérence entre compétences/domaines
|
||||
assessment_color = colors_palette[assessment_data['id'] % len(colors_palette)]
|
||||
|
||||
assessments_list.append({
|
||||
'id': assessment_data['id'],
|
||||
'title': assessment_data['title'],
|
||||
'earned_this': round(assessment_data['earned'], 2), # Points de cette évaluation
|
||||
'max_this': round(assessment_data['max'], 2), # Max de cette évaluation
|
||||
'earned_cumulative': round(cumulative_earned, 2), # Total cumulé jusqu'à cette éval
|
||||
'max_cumulative': round(cumulative_max, 2), # Max cumulé jusqu'à cette éval
|
||||
'date': assessment_data['date'],
|
||||
'percentage_cumulative': round(cumulative_percentage, 1), # % cumulé par rapport au total
|
||||
'percentage_contribution': round(contribution_percentage, 1), # % de contribution individuelle
|
||||
'performance': round(assessment_performance, 1), # % de réussite de cette évaluation seule
|
||||
'color': assessment_color # Couleur cohérente basée sur l'ID
|
||||
})
|
||||
|
||||
competences.append({
|
||||
'name': comp_name,
|
||||
'color': config.get('color', '#6B7280'),
|
||||
'earned_points': round(comp_data['earned_points'], 2),
|
||||
'total_points': round(comp_data['total_points'], 2),
|
||||
'percentage': round(percentage, 1),
|
||||
'assessments': assessments_list
|
||||
})
|
||||
|
||||
# Finaliser les données des domaines
|
||||
domains = []
|
||||
for domain_data in domains_data.values():
|
||||
# Calculer le pourcentage
|
||||
percentage = (domain_data['earned_points'] / domain_data['total_points'] * 100) if domain_data['total_points'] > 0 else 0
|
||||
|
||||
# Convertir les assessments en liste avec calculs d'accumulation
|
||||
assessments_list = []
|
||||
# Utiliser la même palette que les compétences pour la cohérence
|
||||
colors_palette = ['#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#06B6D4', '#84CC16']
|
||||
|
||||
# D'abord, trier par date pour avoir l'ordre chronologique
|
||||
sorted_assessments = sorted(domain_data['assessments'].values(),
|
||||
key=lambda x: x['date'] if x['date'] else datetime.min)
|
||||
|
||||
# Calculer l'accumulation progressive
|
||||
cumulative_earned = 0
|
||||
cumulative_max = 0
|
||||
|
||||
for i, assessment_data in enumerate(sorted_assessments):
|
||||
# Ajouter les points de cette évaluation à l'accumulation
|
||||
cumulative_earned += assessment_data['earned']
|
||||
cumulative_max += assessment_data['max']
|
||||
|
||||
# Calculer les pourcentages
|
||||
cumulative_percentage = (cumulative_earned / domain_data['total_points'] * 100) if domain_data['total_points'] > 0 else 0
|
||||
assessment_performance = (assessment_data['earned'] / assessment_data['max'] * 100) if assessment_data['max'] > 0 else 0
|
||||
# Calculer le pourcentage de contribution de cette évaluation par rapport au total final
|
||||
contribution_percentage = (assessment_data['earned'] / domain_data['total_points'] * 100) if domain_data['total_points'] > 0 else 0
|
||||
|
||||
# Couleur cohérente basée sur l'ID de l'évaluation (même logique que les compétences)
|
||||
assessment_color = colors_palette[assessment_data['id'] % len(colors_palette)]
|
||||
|
||||
assessments_list.append({
|
||||
'id': assessment_data['id'],
|
||||
'title': assessment_data['title'],
|
||||
'earned_this': round(assessment_data['earned'], 2), # Points de cette évaluation
|
||||
'max_this': round(assessment_data['max'], 2), # Max de cette évaluation
|
||||
'earned_cumulative': round(cumulative_earned, 2), # Total cumulé jusqu'à cette éval
|
||||
'max_cumulative': round(cumulative_max, 2), # Max cumulé jusqu'à cette éval
|
||||
'date': assessment_data['date'],
|
||||
'percentage_cumulative': round(cumulative_percentage, 1), # % cumulé par rapport au total
|
||||
'percentage_contribution': round(contribution_percentage, 1), # % de contribution individuelle
|
||||
'performance': round(assessment_performance, 1), # % de réussite de cette évaluation seule
|
||||
'color': assessment_color # Couleur cohérente basée sur l'ID
|
||||
})
|
||||
|
||||
domains.append({
|
||||
'id': domain_data['id'],
|
||||
'name': domain_data['name'],
|
||||
'color': domain_data['color'],
|
||||
'earned_points': round(domain_data['earned_points'], 2),
|
||||
'total_points': round(domain_data['total_points'], 2),
|
||||
'percentage': round(percentage, 1),
|
||||
'assessments': assessments_list
|
||||
})
|
||||
|
||||
# Trier par nom
|
||||
competences.sort(key=lambda x: x['name'].lower())
|
||||
domains.sort(key=lambda x: x['name'].lower())
|
||||
|
||||
return {
|
||||
'competences': competences,
|
||||
'domains': domains
|
||||
}
|
||||
|
||||
def _calculate_assessment_score_for_student(self, assessment: Assessment, student_id: int) -> Optional[float]:
|
||||
"""Calcule le score d'un élève pour une évaluation."""
|
||||
grades = self.grade_repo.get_student_grades_by_assessment(student_id, assessment.id)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -395,6 +395,128 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Section Compétences et Domaines #}
|
||||
{% if summary.competence_domain_breakdown and (summary.competence_domain_breakdown.competences or summary.competence_domain_breakdown.domains) %}
|
||||
<div class="competence-domain-section">
|
||||
<h4 class="font-medium text-gray-700 mb-3 flex items-center">
|
||||
<svg class="w-4 h-4 text-indigo-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Progression par compétence et domaine
|
||||
</h4>
|
||||
|
||||
{# Barres de compétences #}
|
||||
{% if summary.competence_domain_breakdown.competences %}
|
||||
<div class="mb-4">
|
||||
<h5 class="text-sm font-medium text-purple-700 mb-3 flex items-center">
|
||||
<svg class="w-3 h-3 text-purple-500 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
Compétences
|
||||
</h5>
|
||||
<div class="space-y-2">
|
||||
{% for competence in summary.competence_domain_breakdown.competences %}
|
||||
<div class="competence-progress-bar">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-sm font-medium text-gray-700">{{ competence.name }}</span>
|
||||
<span class="text-sm font-bold" style="color: {{ competence.color }}">
|
||||
{{ competence.percentage }}% ({{ competence.earned_points }}/{{ competence.total_points }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress-bar-container segmented-progress"
|
||||
data-competence-name="{{ competence.name }}"
|
||||
data-assessments="{{ competence.assessments | tojson | e }}"
|
||||
role="progressbar"
|
||||
aria-valuenow="{{ competence.percentage }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-label="Progression de la compétence {{ competence.name }}: {{ competence.percentage }}%"
|
||||
tabindex="0">
|
||||
<div class="segmented-progress-bar" data-expanded="true">
|
||||
<!-- Segments pour chaque évaluation -->
|
||||
{% for assessment in competence.assessments %}
|
||||
<div class="progress-segment"
|
||||
style="width: {{ assessment.percentage_contribution }}%; background-color: {{ assessment.color }};"
|
||||
data-assessment-id="{{ assessment.id }}"
|
||||
data-assessment-title="{{ assessment.title }}"
|
||||
data-assessment-performance="{{ assessment.performance }}"
|
||||
data-earned-this="{{ assessment.earned_this }}"
|
||||
data-max-this="{{ assessment.max_this }}"
|
||||
data-earned-cumulative="{{ assessment.earned_cumulative }}"
|
||||
data-max-cumulative="{{ assessment.max_cumulative }}"
|
||||
data-contribution-percentage="{{ assessment.percentage_contribution }}"
|
||||
title="{{ assessment.title }}: {{ assessment.earned_this }} pts ({{ assessment.performance }}%)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Évaluation {{ assessment.title }}: {{ assessment.earned_this }} points sur {{ assessment.max_this }}">
|
||||
<span class="segment-label">{{ assessment.title }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Barres de domaines #}
|
||||
{% if summary.competence_domain_breakdown.domains %}
|
||||
<div>
|
||||
<h5 class="text-sm font-medium text-orange-700 mb-3 flex items-center">
|
||||
<svg class="w-3 h-3 text-orange-500 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 01-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Domaines
|
||||
</h5>
|
||||
<div class="space-y-2">
|
||||
{% for domain in summary.competence_domain_breakdown.domains %}
|
||||
<div class="domain-progress-bar">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-sm font-medium text-gray-700">{{ domain.name }}</span>
|
||||
<span class="text-sm font-bold" style="color: {{ domain.color }}">
|
||||
{{ domain.percentage }}% ({{ domain.earned_points }}/{{ domain.total_points }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress-bar-container segmented-progress"
|
||||
data-domain-name="{{ domain.name }}"
|
||||
data-assessments="{{ domain.assessments | tojson | e }}"
|
||||
role="progressbar"
|
||||
aria-valuenow="{{ domain.percentage }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-label="Progression du domaine {{ domain.name }}: {{ domain.percentage }}%"
|
||||
tabindex="0">
|
||||
<div class="segmented-progress-bar" data-expanded="true">
|
||||
<!-- Segments pour chaque évaluation -->
|
||||
{% for assessment in domain.assessments %}
|
||||
<div class="progress-segment"
|
||||
style="width: {{ assessment.percentage_contribution }}%; background-color: {{ assessment.color }};"
|
||||
data-assessment-id="{{ assessment.id }}"
|
||||
data-assessment-title="{{ assessment.title }}"
|
||||
data-assessment-performance="{{ assessment.performance }}"
|
||||
data-earned-this="{{ assessment.earned_this }}"
|
||||
data-max-this="{{ assessment.max_this }}"
|
||||
data-earned-cumulative="{{ assessment.earned_cumulative }}"
|
||||
data-max-cumulative="{{ assessment.max_cumulative }}"
|
||||
data-contribution-percentage="{{ assessment.percentage_contribution }}"
|
||||
title="{{ assessment.title }}: {{ assessment.earned_this }} pts ({{ assessment.performance }}%)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Évaluation {{ assessment.title }}: {{ assessment.earned_this }} points sur {{ assessment.max_this }}">
|
||||
<span class="segment-label">{{ assessment.title }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Zone d'appréciation #}
|
||||
<div>
|
||||
<label class="block font-medium text-gray-700 mb-3 flex items-center">
|
||||
@@ -647,6 +769,462 @@ body.focus-mode {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* ========== BARRES DE PROGRESSION COMPÉTENCES/DOMAINES ========== */
|
||||
|
||||
/* ========== BARRES DE PROGRESSION SEGMENTÉES ========== */
|
||||
|
||||
/* Conteneur principal des barres de progression */
|
||||
.competence-progress-bar,
|
||||
.domain-progress-bar {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Conteneur segmenté avec apparence moderne et claire */
|
||||
.segmented-progress {
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 6px;
|
||||
min-height: 44px;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.segmented-progress:hover {
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.segmented-progress:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Barre segmentée - état expandé par défaut */
|
||||
.segmented-progress-bar {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
gap: 3px;
|
||||
padding: 3px;
|
||||
border-radius: 6px;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* État contracté (optionnel, si besoin futur) */
|
||||
.segmented-progress-bar[data-expanded="false"] {
|
||||
height: 16px;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
background: #e5e7eb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Segments individuels - état expandé par défaut */
|
||||
.progress-segment {
|
||||
height: 30px;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
min-width: 8px; /* Minimum pour les petites contributions */
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
animation: segmentAppear 0.6s ease-out forwards;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* État contracté des segments (optionnel) */
|
||||
.segmented-progress-bar[data-expanded="false"] .progress-segment {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Séparateurs visuels entre segments (état contracté seulement) */
|
||||
.segmented-progress-bar:not([data-expanded="true"]) .progress-segment:not(:last-child) {
|
||||
border-right: 2px solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Hover effects pour les segments */
|
||||
.progress-segment:hover {
|
||||
filter: brightness(1.15);
|
||||
transform: scaleY(1.05);
|
||||
z-index: 10;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.segmented-progress-bar[data-expanded="true"] .progress-segment:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Focus pour accessibilité clavier */
|
||||
.progress-segment:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
/* Labels des segments - visibles par défaut */
|
||||
.segment-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
max-width: 90%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* État contracté des labels (optionnel) */
|
||||
.segmented-progress-bar[data-expanded="false"] .segment-label {
|
||||
opacity: 0;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltips améliorés avec fond clair */
|
||||
.progress-segment:hover::after {
|
||||
content: attr(data-assessment-title) ': ' attr(data-earned-this) ' pts (' attr(data-assessment-performance) '%)';
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
color: #374151;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
z-index: 1001;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
animation: tooltipFadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.progress-segment:hover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: calc(100% + 2px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: rgba(255, 255, 255, 0.98);
|
||||
z-index: 1001;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Animation d'apparition des segments */
|
||||
@keyframes segmentAppear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
60% {
|
||||
opacity: 0.9;
|
||||
transform: scaleX(1.02);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tooltipFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(4px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation séquentielle des segments */
|
||||
.progress-segment:nth-child(1) { animation-delay: 0.1s; }
|
||||
.progress-segment:nth-child(2) { animation-delay: 0.2s; }
|
||||
.progress-segment:nth-child(3) { animation-delay: 0.3s; }
|
||||
.progress-segment:nth-child(4) { animation-delay: 0.4s; }
|
||||
.progress-segment:nth-child(5) { animation-delay: 0.5s; }
|
||||
.progress-segment:nth-child(6) { animation-delay: 0.6s; }
|
||||
|
||||
/* ========== RESPONSIVE ET ACCESSIBILITÉ ========== */
|
||||
|
||||
/* Responsive pour mobile */
|
||||
@media (max-width: 640px) {
|
||||
.segmented-progress {
|
||||
padding: 3px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.segmented-progress-bar {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.segmented-progress-bar[data-expanded="true"] {
|
||||
height: 44px; /* Touch target minimum 44px */
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.segmented-progress-bar[data-expanded="true"] .progress-segment {
|
||||
height: 40px;
|
||||
min-width: 44px; /* Touch target minimum */
|
||||
}
|
||||
|
||||
.segment-label {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.segmented-progress-bar[data-expanded="true"] .segment-label {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.progress-segment:hover::after {
|
||||
font-size: 0.7rem;
|
||||
padding: 6px 10px;
|
||||
bottom: calc(100% + 12px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive pour très petits écrans */
|
||||
@media (max-width: 480px) {
|
||||
.competence-progress-bar .flex.items-center.justify-between,
|
||||
.domain-progress-bar .flex.items-center.justify-between {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-segment:hover::after {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
bottom: auto;
|
||||
max-width: 280px;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mode sombre - Réduction de mouvement pour accessibilité */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.progress-segment,
|
||||
.segmented-progress,
|
||||
.segmented-progress-bar,
|
||||
.segment-label,
|
||||
.progress-summary {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.progress-segment:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Contraste élevé pour accessibilité */
|
||||
@media (prefers-contrast: high) {
|
||||
.segmented-progress {
|
||||
border: 3px solid #000000;
|
||||
}
|
||||
|
||||
.progress-segment {
|
||||
border: 2px solid #ffffff;
|
||||
filter: contrast(1.2);
|
||||
}
|
||||
|
||||
.segment-label {
|
||||
text-shadow: 1px 1px 0 #000000;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mode focus amélioré pour les barres segmentées */
|
||||
.focus-mode-student .segmented-progress {
|
||||
padding: 2px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.focus-mode-student .segmented-progress-bar {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.focus-mode-student .segmented-progress-bar[data-expanded="true"] {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.focus-mode-student .segmented-progress-bar[data-expanded="true"] .progress-segment {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* ========== CLASSES UTILITAIRES D'ACCESSIBILITÉ ========== */
|
||||
|
||||
/* Screen reader only content */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Skip link pour navigation au clavier */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 6px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
z-index: 9999;
|
||||
transition: top 0.3s;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
/* Focus visible amélioré pour tous les éléments interactifs */
|
||||
.progress-segment:focus-visible,
|
||||
.segmented-progress:focus-visible {
|
||||
outline: 3px solid #4f46e5;
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Indicateur de charge pour les animations */
|
||||
.progress-segment[aria-busy="true"]::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border: 1px solid rgba(255,255,255,0.8);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Mode sombre - support système */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.segmented-progress {
|
||||
background: rgba(31, 41, 55, 0.8);
|
||||
border-color: rgba(75, 85, 99, 0.4);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.segmented-progress:hover {
|
||||
background: rgba(31, 41, 55, 0.95);
|
||||
border-color: rgba(75, 85, 99, 0.6);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.progress-segment {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.segment-label {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.progress-segment:hover::after {
|
||||
background: rgba(17, 24, 39, 0.95);
|
||||
color: #f9fafb;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.progress-segment:hover::before {
|
||||
border-top-color: rgba(17, 24, 39, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== CLASSES DE PERFORMANCE POUR LES NOUVEAUX SEGMENTS ========== */
|
||||
|
||||
/* Indicateurs visuels de performance pour les nouveaux segments */
|
||||
.progress-segment.segment-excellent {
|
||||
box-shadow: inset 0 0 0 2px rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.progress-segment.segment-good {
|
||||
box-shadow: inset 0 0 0 2px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.progress-segment.segment-average {
|
||||
box-shadow: inset 0 0 0 2px rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.progress-segment.segment-struggling {
|
||||
box-shadow: inset 0 0 0 2px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
/* Effet de pulsation pour les segments excellents */
|
||||
.progress-segment.segment-excellent:hover {
|
||||
animation: excellentPulse 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes excellentPulse {
|
||||
0%, 100% { filter: brightness(1.2); }
|
||||
50% { filter: brightness(1.4); }
|
||||
}
|
||||
|
||||
/* ========== ANIMATION D'APPARITION DE LA SECTION ========== */
|
||||
|
||||
/* Animation d'apparition pour toute la section */
|
||||
.competence-domain-section {
|
||||
animation: slideInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Boutons de navigation désactivés */
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
Reference in New Issue
Block a user