/** * NOTYTEX - Council Preparation Module * Gestion complète de la préparation du conseil de classe avec auto-sauvegarde * et navigation trimestre fluide */ class CouncilPreparation { constructor(classId, options = {}) { this.classId = classId; this.options = { debounceTime: 2000, // Auto-sauvegarde après 2s d'inactivité searchDebounceTime: 300, // Recherche instantanée cacheTimeout: 10 * 60 * 1000, // Cache 10 minutes animationDuration: 300, // Durée animations en ms enableTouchGestures: true, ...options }; // État centralisé this.state = { currentTrimester: parseInt(document.querySelector('[data-council-preparation]')?.dataset?.trimester) || 2, expandedStudents: new Set(), searchTerm: '', sortBy: 'alphabetical', // alphabetical, average, status filterStatus: 'all', // all, completed, pending, struggling cache: new Map(), savingStates: new Map(), // Track saving per student modifiedAppreciations: new Set(), isInitialized: false, // État mode focus isFocusMode: false, focusCurrentIndex: 0, filteredStudents: [] // Liste des élèves visibles (après filtres) }; // Gestionnaires de sauvegarde par élève this.saveHandlers = new Map(); this.init(); } async init() { try { this.cacheElements(); // Initialisation des gestionnaires this.stateManager = new StateManager(this); this.filterManager = new FilterManager(this); this.autoSaveManager = new AutoSaveManager(this); this.uiManager = new UIManager(this); this.focusManager = new FocusManager(this); // Restauration de l'état this.stateManager.restoreState(); // Initialisation des sous-modules this.filterManager.init(); this.autoSaveManager.init(); this.uiManager.init(); this.focusManager.init(); this.state.isInitialized = true; // Setup advanced features this.setupAdvancedFeatures(); } catch (error) { console.error('Erreur initialisation CouncilPreparation:', error); this.showError('Erreur lors du chargement de la page'); } } cacheElements() { this.elements = { container: document.querySelector('[data-council-preparation]'), studentsContainer: document.querySelector('[data-students-container]'), searchInput: document.querySelector('[data-search-students]'), sortSelect: document.querySelector('[data-sort-students]'), filterSelect: document.querySelector('[data-filter-status]'), loadingOverlay: document.querySelector('[data-loading-overlay]'), resultsCounter: document.querySelector('[data-results-counter]'), trimesterSelector: document.querySelector('[data-trimester-selector]'), focusModeToggle: document.querySelector('[data-toggle-focus-mode]'), focusModeText: document.querySelector('[data-focus-mode-text]'), focusContainer: document.querySelector('[data-focus-container]'), listModeControls: document.querySelector('.list-mode-controls'), focusModeControls: document.querySelector('.focus-mode-controls'), listModeDisplay: document.querySelector('.list-mode-display'), focusModeDisplay: document.querySelector('.focus-mode-display'), listModeFilters: document.querySelector('.list-mode-filters'), listModeActions: document.querySelector('.list-mode-actions'), listModeHero: document.querySelector('.list-mode-hero'), listModeBreadcrumb: document.querySelector('.list-mode-breadcrumb'), listModeFiltersSection: document.querySelector('.list-mode-filters-section'), listModeStats: document.querySelector('.list-mode-stats'), focusModeHeader: document.querySelector('.focus-mode-header'), focusPrevBtn: document.querySelector('[data-focus-prev]'), focusNextBtn: document.querySelector('[data-focus-next]'), focusCurrentSpan: document.querySelector('[data-focus-current]'), focusTotalSpan: document.querySelector('[data-focus-total]') }; } setupAdvancedFeatures() { // Global keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { switch (e.key) { case 's': e.preventDefault(); this.autoSaveManager.saveAllPending(); this.showToast('Toutes les appréciations ont été sauvegardées', 'success'); break; case 'f': e.preventDefault(); this.elements.searchInput?.focus(); break; } } }); // Save before page unload window.addEventListener('beforeunload', (e) => { if (this.state.modifiedAppreciations.size > 0) { this.autoSaveManager.saveAllPending(); e.returnValue = 'Des modifications non sauvegardées pourraient être perdues.'; } }); // Setup export handlers this.setupExportHandlers(); } setupExportHandlers() { const exportPdf = document.querySelector('[data-export-pdf]'); const classSynthesis = document.querySelector('[data-class-synthesis]'); exportPdf?.addEventListener('click', () => { this.showToast('Fonctionnalité d\'export PDF en cours de développement', 'info'); }); classSynthesis?.addEventListener('click', () => { this.showClassSynthesis(); }); } showClassSynthesis() { // Calculer et afficher une synthèse de classe const students = Array.from(document.querySelectorAll('[data-student-card]')); const stats = this.calculateClassStats(students); const modal = this.createModal('Synthèse de classe', this.generateSynthesisHTML(stats)); document.body.appendChild(modal); } calculateClassStats(students) { let totalStudents = students.length; let withAppreciation = 0; let averageSum = 0; let studentsWithGrades = 0; students.forEach(student => { if (student.dataset.hasAppreciation === 'true') { withAppreciation++; } const average = parseFloat(student.dataset.studentAverage); if (average > 0) { averageSum += average; studentsWithGrades++; } }); return { totalStudents, withAppreciation, appreciationPercentage: Math.round((withAppreciation / totalStudents) * 100), classAverage: studentsWithGrades > 0 ? (averageSum / studentsWithGrades).toFixed(2) : 'N/A' }; } generateSynthesisHTML(stats) { return `
${stats.totalStudents}
Élèves total
${stats.withAppreciation}
Appréciations rédigées
Progression des appréciations ${stats.appreciationPercentage}%
Moyenne de classe : ${stats.classAverage}/20
`; } createModal(title, content) { const modal = document.createElement('div'); modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; modal.innerHTML = `

${title}

${content}
`; // Close on outside click modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); return modal; } showError(message) { this.showToast(message, 'error'); } showToast(message, type = 'info', duration = 3000) { const toast = document.createElement('div'); const bgColor = { 'success': 'bg-green-500', 'error': 'bg-red-500', 'info': 'bg-blue-500', 'warning': 'bg-yellow-500' }[type] || 'bg-gray-500'; toast.className = `fixed top-4 right-4 ${bgColor} text-white px-4 py-2 rounded-lg shadow-lg z-50 transform translate-x-full transition-transform duration-300`; toast.textContent = message; document.body.appendChild(toast); // Animate in setTimeout(() => { toast.style.transform = 'translateX(0)'; }, 10); // Animate out and remove setTimeout(() => { toast.style.transform = 'translateX(full)'; setTimeout(() => toast.remove(), 300); }, duration); } } class StateManager { constructor(councilPrep) { this.parent = councilPrep; } restoreState() { // Restore from URL and localStorage const params = new URLSearchParams(location.search); this.parent.state.sortBy = params.get('sort') || 'alphabetical'; this.parent.state.filterStatus = params.get('filter') || 'all'; // Apply initial filters this.applyInitialState(); } applyInitialState() { if (this.parent.elements.sortSelect) { this.parent.elements.sortSelect.value = this.parent.state.sortBy; } if (this.parent.elements.filterSelect) { this.parent.elements.filterSelect.value = this.parent.state.filterStatus; } } saveState() { const params = new URLSearchParams(location.search); params.set('sort', this.parent.state.sortBy); params.set('filter', this.parent.state.filterStatus); history.replaceState(null, '', `${location.pathname}?${params.toString()}`); } } class FilterManager { constructor(councilPrep) { this.parent = councilPrep; this.searchHandler = null; } init() { this.bindSearchEvents(); this.bindSortEvents(); this.bindFilterEvents(); this.bindTrimesterEvents(); } bindSearchEvents() { const searchInput = this.parent.elements.searchInput; if (!searchInput) return; this.searchHandler = this.debounce((term) => { this.parent.state.searchTerm = term.toLowerCase(); this.applyFilters(); }, this.parent.options.searchDebounceTime); searchInput.addEventListener('input', (e) => { this.searchHandler(e.target.value); }); } bindSortEvents() { const sortSelect = this.parent.elements.sortSelect; if (!sortSelect) return; sortSelect.addEventListener('change', (e) => { this.parent.state.sortBy = e.target.value; this.parent.stateManager.saveState(); this.applyFilters(); }); } bindFilterEvents() { const filterSelect = this.parent.elements.filterSelect; if (!filterSelect) return; filterSelect.addEventListener('change', (e) => { this.parent.state.filterStatus = e.target.value; this.parent.stateManager.saveState(); this.applyFilters(); }); } bindTrimesterEvents() { const trimesterSelector = this.parent.elements.trimesterSelector; if (!trimesterSelector) return; // Support pour les nouveaux boutons tabs if (trimesterSelector.hasAttribute('data-trimester-selector')) { // Navigation tabs - écouter les clics sur les boutons const trimesterTabs = trimesterSelector.querySelectorAll('[data-trimester-tab]'); trimesterTabs.forEach(tab => { tab.addEventListener('click', (e) => { e.preventDefault(); const newTrimester = parseInt(tab.dataset.trimesterTab); if (newTrimester !== this.parent.state.currentTrimester) { this.changeTrimester(newTrimester); } }); // Support clavier pour accessibilité tab.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const newTrimester = parseInt(tab.dataset.trimesterTab); if (newTrimester !== this.parent.state.currentTrimester) { this.changeTrimester(newTrimester); } } }); }); } else { // Ancien sélecteur dropdown - pour compatibilité trimesterSelector.addEventListener('change', (e) => { const newTrimester = parseInt(e.target.value); if (newTrimester !== this.parent.state.currentTrimester) { this.changeTrimester(newTrimester); } }); } } async changeTrimester(newTrimester) { try { // Show loading overlay this.parent.elements.loadingOverlay?.classList.remove('hidden'); // Redirect to the new trimester URL const newUrl = `${window.location.pathname}?trimestre=${newTrimester}`; window.location.href = newUrl; } catch (error) { console.error('Erreur changement trimestre:', error); this.parent.showToast('Erreur lors du changement de trimestre', 'error'); // Revert selector to previous value const trimesterSelector = this.parent.elements.trimesterSelector; if (trimesterSelector.hasAttribute('data-trimester-selector')) { // Pour les tabs - remettre les bonnes classes active const tabs = trimesterSelector.querySelectorAll('[data-trimester-tab]'); tabs.forEach(tab => { const tabTrimester = parseInt(tab.dataset.trimesterTab); if (tabTrimester === this.parent.state.currentTrimester) { tab.classList.add('active'); tab.setAttribute('aria-selected', 'true'); } else { tab.classList.remove('active'); tab.setAttribute('aria-selected', 'false'); } }); } else { // Pour l'ancien dropdown trimesterSelector.value = this.parent.state.currentTrimester; } } finally { // Hide loading overlay this.parent.elements.loadingOverlay?.classList.add('hidden'); } } applyFilters() { const students = Array.from(document.querySelectorAll('[data-student-card]')); let visibleCount = 0; students.forEach((studentCard, index) => { const isVisible = this.shouldShowStudent(studentCard); if (isVisible) { studentCard.style.display = ''; visibleCount++; // Staggered animation studentCard.style.opacity = '0'; studentCard.style.transform = 'translateY(10px)'; setTimeout(() => { studentCard.style.transition = 'opacity 300ms ease, transform 300ms ease'; studentCard.style.opacity = '1'; studentCard.style.transform = 'translateY(0)'; }, index * 50); } else { studentCard.style.display = 'none'; } }); this.updateResultsCounter(visibleCount, students.length); this.applySorting(); // Notifier le focus manager si nécessaire if (this.parent.focusManager) { this.parent.focusManager.onFiltersChanged(); } } shouldShowStudent(studentCard) { const studentName = studentCard.dataset.studentName?.toLowerCase() || ''; const performanceStatus = studentCard.dataset.performanceStatus; const hasAppreciation = studentCard.dataset.hasAppreciation === 'true'; // Search filter if (this.parent.state.searchTerm && !studentName.includes(this.parent.state.searchTerm)) { return false; } // Status filter if (this.parent.state.filterStatus !== 'all') { switch (this.parent.state.filterStatus) { case 'completed': if (!hasAppreciation) return false; break; case 'pending': if (hasAppreciation) return false; break; case 'struggling': if (performanceStatus !== 'struggling') return false; break; } } return true; } applySorting() { const container = this.parent.elements.studentsContainer; if (!container) return; const students = Array.from(container.querySelectorAll('[data-student-card]:not([style*="display: none"])')); students.sort((a, b) => { switch (this.parent.state.sortBy) { case 'alphabetical': return (a.dataset.studentName || '').localeCompare(b.dataset.studentName || ''); case 'average': return (parseFloat(b.dataset.studentAverage) || 0) - (parseFloat(a.dataset.studentAverage) || 0); case 'status': const statusOrder = { 'struggling': 0, 'average': 1, 'good': 2, 'excellent': 3, 'no_data': 4 }; return statusOrder[a.dataset.performanceStatus] - statusOrder[b.dataset.performanceStatus]; default: return 0; } }); students.forEach((student, index) => { student.style.order = index; }); } updateResultsCounter(visible, total) { const counter = this.parent.elements.resultsCounter; if (counter) { counter.textContent = `${visible} élève${visible > 1 ? 's' : ''} affiché${visible > 1 ? 's' : ''} sur ${total}`; } } debounce(func, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } } class AutoSaveManager { constructor(councilPrep) { this.parent = councilPrep; this.pendingSaves = new Map(); this.saveQueue = []; this.isSaving = false; } init() { this.bindTextareaEvents(); this.bindManualSaveButtons(); } bindTextareaEvents() { const textareas = document.querySelectorAll('[data-appreciation-textarea]'); textareas.forEach(textarea => { const studentId = textarea.dataset.studentId; // Setup character counter this.setupCharacterCounter(textarea, studentId); // Debounced save handler const saveHandler = this.debounce(() => { this.queueSave(studentId, textarea.value); }, this.parent.options.debounceTime); textarea.addEventListener('input', (e) => { this.showModifiedState(studentId, true); this.parent.state.modifiedAppreciations.add(studentId); this.updateCharacterCounter(textarea, studentId); saveHandler(); }); textarea.addEventListener('blur', () => { if (this.parent.state.modifiedAppreciations.has(studentId)) { this.queueSave(studentId, textarea.value, true); } }); }); } bindManualSaveButtons() { // Boutons supprimés de l'interface - auto-sauvegarde uniquement } setupCharacterCounter(textarea, studentId) { const counter = textarea.closest('[data-student-details]')?.querySelector('[data-char-counter]'); if (counter) { this.updateCharacterCounter(textarea, studentId); } } updateCharacterCounter(textarea, studentId) { const counter = textarea.closest('[data-student-details]')?.querySelector('[data-char-counter]'); if (counter) { const count = textarea.value.length; counter.textContent = `${count} caractères`; if (count > 500) { counter.className = 'text-xs text-red-500'; } else if (count > 300) { counter.className = 'text-xs text-yellow-500'; } else { counter.className = 'text-xs text-gray-500'; } } } queueSave(studentId, appreciation, immediate = false) { const saveTask = { studentId, appreciation, timestamp: Date.now(), immediate }; if (immediate) { this.executeSave(saveTask); } else { // Remove previous queued save for this student this.saveQueue = this.saveQueue.filter(task => task.studentId !== studentId); this.saveQueue.push(saveTask); this.processSaveQueue(); } } async processSaveQueue() { if (this.isSaving || this.saveQueue.length === 0) return; this.isSaving = true; while (this.saveQueue.length > 0) { const task = this.saveQueue.shift(); await this.executeSave(task); await new Promise(resolve => setTimeout(resolve, 100)); // Throttle } this.isSaving = false; } async executeSave(saveTask) { const { studentId, appreciation } = saveTask; try { this.showSavingState(studentId, true); const response = await fetch(`/classes/${this.parent.classId}/council/appreciation/${studentId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ appreciation: appreciation, trimester: this.parent.state.currentTrimester }) }); const result = await response.json(); if (response.ok && result.success) { this.showSavedState(studentId); this.parent.state.modifiedAppreciations.delete(studentId); this.updateLastModified(studentId, result.last_modified); this.updateAppreciationStatus(studentId, result.has_content); } else { throw new Error(result.error || 'Erreur de sauvegarde'); } } catch (error) { console.error('Erreur sauvegarde appréciation:', error); this.showErrorState(studentId, error.message); this.parent.state.modifiedAppreciations.add(studentId); this.parent.showToast(`Erreur sauvegarde`, 'error'); } finally { this.showSavingState(studentId, false); } } async finalizeAppreciation(studentId) { const textarea = document.querySelector(`[data-appreciation-textarea][data-student-id="${studentId}"]`); if (!textarea || !textarea.value.trim()) { this.parent.showToast('Veuillez saisir une appréciation avant de finaliser', 'warning'); return; } if (confirm('Finaliser cette appréciation ? Elle ne pourra plus être modifiée.')) { // Pour l'instant, on sauvegarde normalement. La finalisation sera une fonctionnalité future this.queueSave(studentId, textarea.value, true); } } showModifiedState(studentId, isModified) { const indicator = document.querySelector(`[data-save-indicator="${studentId}"]`); if (!indicator) return; if (isModified) { indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-yellow-100 text-yellow-800'; indicator.innerHTML = 'Modifié'; indicator.classList.remove('hidden'); } } showSavingState(studentId, isSaving) { const indicator = document.querySelector(`[data-save-indicator="${studentId}"]`); if (!indicator) return; if (isSaving) { indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800'; indicator.innerHTML = 'Sauvegarde...'; indicator.classList.remove('hidden'); } } showSavedState(studentId) { const indicator = document.querySelector(`[data-save-indicator="${studentId}"]`); if (!indicator) return; indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800'; indicator.innerHTML = 'Sauvegardé'; setTimeout(() => { indicator.classList.add('hidden'); }, 2000); } showErrorState(studentId, error) { const indicator = document.querySelector(`[data-save-indicator="${studentId}"]`); if (!indicator) return; indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800'; indicator.innerHTML = 'Erreur'; indicator.title = error; indicator.classList.remove('hidden'); } updateLastModified(studentId, lastModified) { const element = document.querySelector(`[data-last-modified="${studentId}"]`); if (element) { const date = new Date(lastModified); element.textContent = date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); } } updateAppreciationStatus(studentId, hasContent) { const studentCard = document.querySelector(`[data-student-card="${studentId}"]`); if (studentCard) { studentCard.dataset.hasAppreciation = hasContent ? 'true' : 'false'; // Update status indicator in the card const indicator = studentCard.querySelector('.inline-flex.items-center.px-2.py-1.rounded-full'); if (indicator) { if (hasContent) { indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800'; indicator.innerHTML = 'Rédigée'; } else { indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-orange-100 text-orange-800'; indicator.innerHTML = 'À rédiger'; } } } } async saveAllPending() { const textareas = document.querySelectorAll('[data-appreciation-textarea]'); for (const textarea of textareas) { const studentId = textarea.dataset.studentId; if (this.parent.state.modifiedAppreciations.has(studentId)) { await this.executeSave({ studentId, appreciation: textarea.value, immediate: true }); } } } debounce(func, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } } class UIManager { constructor(councilPrep) { this.parent = councilPrep; } init() { this.bindCardToggles(); this.setupAccessibility(); } bindCardToggles() { const toggleButtons = document.querySelectorAll('[data-toggle-student]'); toggleButtons.forEach(button => { button.addEventListener('click', (e) => { const studentId = button.dataset.toggleStudent; this.toggleStudentCard(studentId); }); }); } toggleStudentCard(studentId) { const card = document.querySelector(`[data-student-card="${studentId}"]`); const details = card?.querySelector(`[data-student-details="${studentId}"]`); const icon = card?.querySelector('[data-toggle-icon]'); if (!card || !details) return; const isExpanded = this.parent.state.expandedStudents.has(studentId); if (isExpanded) { this.collapseCard(details, icon); this.parent.state.expandedStudents.delete(studentId); } else { this.expandCard(details, icon); this.parent.state.expandedStudents.add(studentId); // Auto-focus textarea setTimeout(() => { const textarea = details.querySelector('textarea'); textarea?.focus(); }, this.parent.options.animationDuration); } this.updateAccessibilityStates(card, !isExpanded); } expandCard(details, icon) { details.classList.remove('hidden'); details.style.height = '0px'; details.style.opacity = '0'; // Force reflow details.offsetHeight; const targetHeight = details.scrollHeight; details.style.transition = `height ${this.parent.options.animationDuration}ms ease-out, opacity ${this.parent.options.animationDuration}ms ease-out`; details.style.height = `${targetHeight}px`; details.style.opacity = '1'; if (icon) { icon.style.transform = 'rotate(180deg)'; } setTimeout(() => { details.style.height = 'auto'; }, this.parent.options.animationDuration); } collapseCard(details, icon) { const currentHeight = details.offsetHeight; details.style.height = `${currentHeight}px`; details.style.transition = `height ${this.parent.options.animationDuration}ms ease-out, opacity ${this.parent.options.animationDuration}ms ease-out`; // Force reflow details.offsetHeight; details.style.height = '0px'; details.style.opacity = '0'; if (icon) { icon.style.transform = 'rotate(0deg)'; } setTimeout(() => { details.classList.add('hidden'); details.style.height = ''; details.style.opacity = ''; details.style.transition = ''; }, this.parent.options.animationDuration); } setupAccessibility() { const toggleButtons = document.querySelectorAll('[data-toggle-student]'); toggleButtons.forEach(button => { button.setAttribute('role', 'button'); button.setAttribute('aria-expanded', 'false'); button.setAttribute('aria-label', 'Afficher/Masquer les détails de l\'élève'); }); document.addEventListener('keydown', (e) => { if (e.target.matches('[data-toggle-student]')) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.target.click(); } } }); } updateAccessibilityStates(card, isExpanded) { const button = card.querySelector('[data-toggle-student]'); if (button) { button.setAttribute('aria-expanded', isExpanded.toString()); } } } class FocusManager { constructor(councilPrep) { this.parent = councilPrep; } init() { this.bindFocusModeToggle(); this.bindNavigationControls(); this.bindKeyboardShortcuts(); this.updateFilteredStudents(); } bindFocusModeToggle() { // Utiliser la délégation d'événement pour gérer plusieurs boutons document.addEventListener('click', (e) => { if (e.target.matches('[data-toggle-focus-mode]') || e.target.closest('[data-toggle-focus-mode]')) { e.preventDefault(); this.toggleFocusMode(); } }); } bindNavigationControls() { // Les boutons de navigation sont gérés ici dans la méthode globale document.addEventListener('click', (e) => { if (e.target.matches('[data-focus-prev]') || e.target.closest('[data-focus-prev]')) { e.preventDefault(); this.navigatePrevious(); } else if (e.target.matches('[data-focus-next]') || e.target.closest('[data-focus-next]')) { e.preventDefault(); this.navigateNext(); } }); } bindKeyboardShortcuts() { document.addEventListener('keydown', (e) => { if (!this.parent.state.isFocusMode) return; switch (e.key) { case 'Escape': e.preventDefault(); this.toggleFocusMode(false); break; case 'ArrowLeft': e.preventDefault(); this.navigatePrevious(); break; case 'ArrowRight': e.preventDefault(); this.navigateNext(); break; } }); } toggleFocusMode(forcedState = null) { const newState = forcedState !== null ? forcedState : !this.parent.state.isFocusMode; this.parent.state.isFocusMode = newState; if (newState) { this.enterFocusMode(); } else { this.exitFocusMode(); } } enterFocusMode() { // Mettre à jour l'interface this.updateFocusModeUI(true); // Initialiser avec le premier élève this.updateFilteredStudents(); if (this.parent.state.filteredStudents.length > 0) { this.parent.state.focusCurrentIndex = 0; this.showCurrentStudent(); } // Sauvegarder l'état localStorage.setItem('council-focus-mode', 'true'); } exitFocusMode() { // Mettre à jour l'interface this.updateFocusModeUI(false); // Réinitialiser l'état this.parent.state.focusCurrentIndex = 0; // Sauvegarder l'état localStorage.removeItem('council-focus-mode'); } updateFocusModeUI(isFocusMode) { const elements = this.parent.elements; if (isFocusMode) { // Mode Focus : Interface minimaliste // Ajouter classe au body pour styles globaux document.body.classList.add('focus-mode'); // Masquer TOUS les éléments de liste elements.listModeDisplay?.classList.add('hidden'); elements.listModeControls?.classList.add('hidden'); elements.listModeFilters?.classList.add('hidden'); elements.listModeActions?.classList.add('hidden'); elements.listModeHero?.classList.add('hidden'); elements.listModeBreadcrumb?.classList.add('hidden'); elements.listModeFiltersSection?.classList.add('hidden'); elements.listModeStats?.classList.add('hidden'); // Afficher uniquement les éléments focus elements.focusModeDisplay?.classList.remove('hidden'); elements.focusModeHeader?.classList.remove('hidden'); elements.focusModeControls?.classList.add('hidden'); // Utiliser le header compact à la place // Changer le texte du bouton if (elements.focusModeText) { elements.focusModeText.textContent = 'Mode Liste'; } } else { // Mode Liste : Interface complète // Retirer classe du body document.body.classList.remove('focus-mode'); // Réafficher tous les éléments de liste elements.listModeDisplay?.classList.remove('hidden'); elements.listModeControls?.classList.remove('hidden'); elements.listModeFilters?.classList.remove('hidden'); elements.listModeActions?.classList.remove('hidden'); elements.listModeHero?.classList.remove('hidden'); elements.listModeBreadcrumb?.classList.remove('hidden'); elements.listModeFiltersSection?.classList.remove('hidden'); elements.listModeStats?.classList.remove('hidden'); // Masquer les éléments focus elements.focusModeDisplay?.classList.add('hidden'); elements.focusModeHeader?.classList.add('hidden'); elements.focusModeControls?.classList.remove('hidden'); // Changer le texte du bouton if (elements.focusModeText) { elements.focusModeText.textContent = 'Mode Focus'; } } } updateFilteredStudents() { // En mode focus, on prend TOUS les élèves (pas de filtres) const allStudents = Array.from(document.querySelectorAll('[data-student-card]')); this.parent.state.filteredStudents = allStudents; // Mettre à jour le compteur total if (this.parent.elements.focusTotalSpan) { this.parent.elements.focusTotalSpan.textContent = allStudents.length; } } showCurrentStudent() { const currentStudent = this.parent.state.filteredStudents[this.parent.state.focusCurrentIndex]; if (!currentStudent) return; // Cloner l'élève et l'afficher dans le conteneur focus const focusContainer = this.parent.elements.focusModeDisplay; if (!focusContainer) return; // 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); // Forcer l'expansion de l'appréciation en mode focus const detailsSection = clonedStudent.querySelector('[data-student-details]'); if (detailsSection) { detailsSection.classList.remove('hidden'); detailsSection.style.height = 'auto'; detailsSection.style.opacity = '1'; detailsSection.style.flex = '1'; detailsSection.style.display = 'block'; detailsSection.style.overflowY = 'auto'; // S'assurer que le contenu interne utilise Flexbox const innerContent = detailsSection.querySelector('.px-6.py-6.space-y-6'); if (innerContent) { innerContent.style.display = 'flex'; innerContent.style.flexDirection = 'column'; innerContent.style.height = '100%'; innerContent.style.gap = '1.5rem'; } } // Ajouter une classe spéciale pour le mode focus clonedStudent.classList.add('focus-mode-student'); // Ajouter au conteneur focusContainer.appendChild(clonedStudent); // Mettre à jour l'indicateur de position this.updatePositionIndicator(); // Mettre à jour les boutons de navigation this.updateNavigationButtons(); // Réattacher TOUS les événements pour le nouvel élément focus this.bindFocusStudentEvents(clonedStudent, studentId); // Assurer que toutes les sections sont visibles this.ensureAllSectionsVisible(clonedStudent); // Focus automatique sur le textarea de l'appréciation this.focusAppreciationTextarea(clonedStudent); // Optimiser la hauteur pour éviter le scroll this.optimizeHeight(); } bindFocusStudentEvents(clonedStudent, studentId) { // 1. Événements textarea avec synchronisation bidirectionnelle const textarea = clonedStudent.querySelector(`[data-appreciation-textarea][data-student-id="${studentId}"]`); if (textarea) { // Handler de sauvegarde avec synchronisation const saveHandler = this.parent.autoSaveManager.debounce(() => { this.saveFocusAppreciation(studentId, textarea.value); }, this.parent.options.debounceTime); // Événement input avec sync textarea.addEventListener('input', (e) => { this.parent.state.modifiedAppreciations.add(studentId); this.parent.autoSaveManager.updateCharacterCounter(textarea, studentId); this.syncAppreciationToOriginal(studentId, e.target.value); saveHandler(); }); // Événement blur avec sauvegarde immédiate textarea.addEventListener('blur', () => { if (this.parent.state.modifiedAppreciations.has(studentId)) { this.saveFocusAppreciation(studentId, textarea.value, true); } }); // Setup character counter this.parent.autoSaveManager.setupCharacterCounter(textarea, studentId); } // 2. Boutons supprimés - auto-sauvegarde uniquement // 4. Gestion des barres de progression this.setupProgressBars(clonedStudent); } ensureAllSectionsVisible(clonedStudent) { // S'assurer que les sections compétences/domaines sont visibles const competenceSection = clonedStudent.querySelector('.competence-domain-section'); if (competenceSection) { competenceSection.style.display = 'block'; competenceSection.style.minHeight = '200px'; competenceSection.style.flexShrink = '0'; } // S'assurer que les barres de progression sont configurées const progressBars = clonedStudent.querySelectorAll('.progress-bar-container'); progressBars.forEach(bar => { bar.style.display = 'block'; }); // Section info supprimée - maintenant intégrée dans la zone d'appréciation // S'assurer que les résultats d'évaluation sont visibles const evaluationResults = clonedStudent.querySelector('.evaluation-results'); if (evaluationResults) { evaluationResults.style.display = 'block'; } // S'assurer que la section progress-bars est visible const progressBarsSection = clonedStudent.querySelector('.progress-bars'); if (progressBarsSection) { progressBarsSection.style.display = 'block'; } } 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 = `

${assessmentTitle}

Points obtenus
${earnedThis}/${maxThis}
Performance
${performance}%
Contribution au total
${contributionPercentage}% du total de la compétence
`; 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 } async saveFocusAppreciation(studentId, appreciation, immediate = false) { try { // Afficher l'état de sauvegarde this.showFocusSavingState(studentId, true); const response = await fetch(`/classes/${this.parent.classId}/council/appreciation/${studentId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ appreciation: appreciation, trimester: this.parent.state.currentTrimester }) }); const result = await response.json(); if (response.ok && result.success) { this.showFocusSavedState(studentId); this.parent.state.modifiedAppreciations.delete(studentId); // Synchroniser avec l'élément original this.syncAppreciationStatusToOriginal(studentId, result.has_content); this.syncLastModifiedToOriginal(studentId, result.last_modified); // Toast de confirmation if (immediate) { this.parent.showToast('Appréciation sauvegardée', 'success'); } } else { throw new Error(result.error || 'Erreur de sauvegarde'); } } catch (error) { console.error('❌ Erreur sauvegarde appréciation focus:', error); this.showFocusErrorState(studentId, error.message); this.parent.state.modifiedAppreciations.add(studentId); this.parent.showToast('Erreur de sauvegarde', 'error'); } finally { this.showFocusSavingState(studentId, false); } } syncAppreciationToOriginal(studentId, value) { // Synchroniser le texte avec l'élément original dans la liste const originalTextarea = document.querySelector(`[data-student-card="${studentId}"] [data-appreciation-textarea]`); if (originalTextarea && originalTextarea.value !== value) { originalTextarea.value = value; } } syncAppreciationStatusToOriginal(studentId, hasContent) { // Synchroniser l'état d'appréciation avec l'élément original const originalCard = document.querySelector(`[data-student-card="${studentId}"]`); if (originalCard) { originalCard.dataset.hasAppreciation = hasContent ? 'true' : 'false'; // Mettre à jour l'indicateur de statut const indicator = originalCard.querySelector('.inline-flex.items-center.px-2.py-1.rounded-full'); if (indicator) { if (hasContent) { indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800'; indicator.innerHTML = 'Rédigée'; } else { indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-orange-100 text-orange-800'; indicator.innerHTML = 'À rédiger'; } } } } syncLastModifiedToOriginal(studentId, lastModified) { const originalElement = document.querySelector(`[data-student-card="${studentId}"] [data-last-modified="${studentId}"]`); if (originalElement) { const date = new Date(lastModified); originalElement.textContent = date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); } } showFocusSavingState(studentId, isSaving) { const indicator = document.querySelector(`[data-focus-clone-of="${studentId}"] [data-save-indicator="${studentId}"]`); if (!indicator) return; if (isSaving) { indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800'; indicator.innerHTML = 'Sauvegarde...'; indicator.classList.remove('hidden'); } } showFocusSavedState(studentId) { const indicator = document.querySelector(`[data-focus-clone-of="${studentId}"] [data-save-indicator="${studentId}"]`); if (!indicator) return; indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800'; indicator.innerHTML = 'Sauvegardé'; setTimeout(() => { indicator.classList.add('hidden'); }, 2000); } showFocusErrorState(studentId, error) { const indicator = document.querySelector(`[data-focus-clone-of="${studentId}"] [data-save-indicator="${studentId}"]`); if (!indicator) return; indicator.className = 'inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800'; indicator.innerHTML = 'Erreur'; indicator.title = error; indicator.classList.remove('hidden'); } focusAppreciationTextarea(clonedStudent) { // Attendre que l'élément soit complètement rendu setTimeout(() => { const textarea = clonedStudent.querySelector('[data-appreciation-textarea]'); if (textarea) { textarea.focus(); // Positionner le curseur à la fin du texte existant const textLength = textarea.value.length; textarea.setSelectionRange(textLength, textLength); // Scroll smooth vers le textarea si nécessaire textarea.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); } }, 100); // Délai pour s'assurer que l'animation est terminée } updatePositionIndicator() { if (this.parent.elements.focusCurrentSpan) { this.parent.elements.focusCurrentSpan.textContent = this.parent.state.focusCurrentIndex + 1; } } updateNavigationButtons() { const prevBtn = this.parent.elements.focusPrevBtn; const nextBtn = this.parent.elements.focusNextBtn; const currentIndex = this.parent.state.focusCurrentIndex; const totalStudents = this.parent.state.filteredStudents.length; if (prevBtn) { prevBtn.disabled = currentIndex <= 0; } if (nextBtn) { nextBtn.disabled = currentIndex >= totalStudents - 1; } } navigatePrevious() { if (this.parent.state.focusCurrentIndex > 0) { this.parent.state.focusCurrentIndex--; this.showCurrentStudent(); } } navigateNext() { if (this.parent.state.focusCurrentIndex < this.parent.state.filteredStudents.length - 1) { this.parent.state.focusCurrentIndex++; this.showCurrentStudent(); } } optimizeHeight() { const focusContainer = this.parent.elements.focusModeDisplay; if (!focusContainer) return; const student = focusContainer.querySelector('.focus-mode-student'); if (!student) return; // Forcer explicitement la hauteur complète student.style.height = '100%'; student.style.minHeight = '100%'; student.style.maxHeight = 'none'; student.style.display = 'flex'; student.style.flexDirection = 'column'; student.style.overflow = 'hidden'; // S'assurer que le header garde sa taille et ne grandit pas const headerContainer = student.querySelector('.px-6.py-4'); if (headerContainer) { headerContainer.style.flexShrink = '0'; headerContainer.style.flex = 'none'; } // S'assurer que la section détails utilise tout l'espace restant const detailsSection = student.querySelector('[data-student-details]'); if (detailsSection) { detailsSection.style.flex = '1 1 0'; detailsSection.style.height = '0'; // Force flexbox detailsSection.style.display = 'block'; detailsSection.style.overflowY = 'auto'; detailsSection.style.minHeight = '0'; } // Scroll vers le haut si nécessaire window.scrollTo(0, 0); // Debug des hauteurs const containerHeight = focusContainer.offsetHeight; const studentHeight = student.offsetHeight; } 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 if (this.parent.state.isFocusMode) { return; } } } // Auto-initialization document.addEventListener('DOMContentLoaded', () => { const councilContainer = document.querySelector('[data-council-preparation]'); if (councilContainer) { const classId = councilContainer.dataset.classId; if (classId) { window.currentCouncilPreparation = new CouncilPreparation(parseInt(classId)); } } }); // Global export window.CouncilPreparation = CouncilPreparation;