1865 lines
72 KiB
JavaScript
1865 lines
72 KiB
JavaScript
/**
|
|
* 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 `
|
|
<div class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="bg-blue-50 p-4 rounded-lg">
|
|
<div class="text-2xl font-bold text-blue-900">${stats.totalStudents}</div>
|
|
<div class="text-blue-700">Élèves total</div>
|
|
</div>
|
|
<div class="bg-green-50 p-4 rounded-lg">
|
|
<div class="text-2xl font-bold text-green-900">${stats.withAppreciation}</div>
|
|
<div class="text-green-700">Appréciations rédigées</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-50 p-4 rounded-lg">
|
|
<div class="flex justify-between items-center">
|
|
<span>Progression des appréciations</span>
|
|
<span class="font-bold">${stats.appreciationPercentage}%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2 mt-2">
|
|
<div class="bg-gradient-to-r from-green-500 to-green-600 h-2 rounded-full transition-all duration-500"
|
|
style="width: ${stats.appreciationPercentage}%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="text-center pt-4 border-t">
|
|
<div class="text-lg">Moyenne de classe : <span class="font-bold text-orange-600">${stats.classAverage}/20</span></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 = `
|
|
<div class="bg-white rounded-xl p-6 max-w-md w-full mx-4">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-semibold">${title}</h3>
|
|
<button class="text-gray-400 hover:text-gray-600" onclick="this.closest('.fixed').remove()">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
${content}
|
|
<div class="mt-6 flex justify-end">
|
|
<button class="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
|
|
onclick="this.closest('.fixed').remove()">
|
|
Fermer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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;
|
|
|
|
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
|
|
this.parent.elements.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 = '<span class="w-2 h-2 bg-yellow-400 rounded-full mr-1"></span>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 = '<svg class="animate-spin w-3 h-3 mr-1" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>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 = '<span class="w-2 h-2 bg-green-400 rounded-full mr-1"></span>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 = '<span class="w-2 h-2 bg-red-400 rounded-full mr-1"></span>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 = '<span class="w-2 h-2 bg-green-400 rounded-full mr-1"></span>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 = '<span class="w-2 h-2 bg-orange-400 rounded-full mr-1"></span>À 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 = `
|
|
<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
|
|
}
|
|
|
|
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 = '<span class="w-2 h-2 bg-green-400 rounded-full mr-1"></span>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 = '<span class="w-2 h-2 bg-orange-400 rounded-full mr-1"></span>À 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 = '<svg class="animate-spin w-3 h-3 mr-1" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>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 = '<span class="w-2 h-2 bg-green-400 rounded-full mr-1"></span>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 = '<span class="w-2 h-2 bg-red-400 rounded-full mr-1"></span>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; |