1376 lines
53 KiB
JavaScript
1376 lines
53 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();
|
|
|
|
console.log('✅ CouncilPreparation initialized successfully');
|
|
console.log('🎯 Focus Manager ready:', !!this.focusManager);
|
|
|
|
} 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() {
|
|
const saveButtons = document.querySelectorAll('[data-save-manual]');
|
|
const finalizeButtons = document.querySelectorAll('[data-finalize]');
|
|
|
|
saveButtons.forEach(button => {
|
|
const studentId = button.dataset.saveManual;
|
|
button.addEventListener('click', () => {
|
|
const textarea = document.querySelector(`[data-appreciation-textarea][data-student-id="${studentId}"]`);
|
|
if (textarea) {
|
|
this.queueSave(studentId, textarea.value, true);
|
|
}
|
|
});
|
|
});
|
|
|
|
finalizeButtons.forEach(button => {
|
|
const studentId = button.dataset.finalize;
|
|
button.addEventListener('click', () => {
|
|
this.finalizeAppreciation(studentId);
|
|
});
|
|
});
|
|
}
|
|
|
|
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]')) {
|
|
console.log('🎯 Bouton Mode Focus cliqué !');
|
|
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;
|
|
|
|
console.log(`🔄 Basculement vers mode: ${newState ? 'FOCUS' : 'LISTE'}`);
|
|
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');
|
|
|
|
console.log('✨ Mode focus activé - Focus sur première appréciation');
|
|
}
|
|
|
|
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 = '';
|
|
|
|
// Cloner l'élément élève
|
|
const clonedStudent = currentStudent.cloneNode(true);
|
|
|
|
// 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';
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Focus automatique sur le textarea de l'appréciation
|
|
this.focusAppreciationTextarea(clonedStudent);
|
|
|
|
// Optimiser la hauteur pour éviter le scroll
|
|
this.optimizeHeight();
|
|
}
|
|
|
|
bindFocusStudentEvents(clonedStudent, studentId) {
|
|
console.log(`🔧 Attachement des événements pour l'élève ${studentId} en mode focus`);
|
|
|
|
// 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(() => {
|
|
console.log(`💾 Sauvegarde en focus pour élève ${studentId}`);
|
|
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)) {
|
|
console.log(`💾 Sauvegarde blur en focus pour élève ${studentId}`);
|
|
this.saveFocusAppreciation(studentId, textarea.value, true);
|
|
}
|
|
});
|
|
|
|
// Setup character counter
|
|
this.parent.autoSaveManager.setupCharacterCounter(textarea, studentId);
|
|
}
|
|
|
|
// 2. Boutons de sauvegarde manuelle
|
|
const saveButton = clonedStudent.querySelector(`[data-save-manual="${studentId}"]`);
|
|
if (saveButton) {
|
|
saveButton.addEventListener('click', () => {
|
|
const textareaValue = clonedStudent.querySelector(`[data-appreciation-textarea][data-student-id="${studentId}"]`)?.value || '';
|
|
console.log(`💾 Sauvegarde manuelle en focus pour élève ${studentId}`);
|
|
this.saveFocusAppreciation(studentId, textareaValue, true);
|
|
});
|
|
}
|
|
|
|
// 3. Bouton de finalisation
|
|
const finalizeButton = clonedStudent.querySelector(`[data-finalize="${studentId}"]`);
|
|
if (finalizeButton) {
|
|
finalizeButton.addEventListener('click', () => {
|
|
const textareaValue = clonedStudent.querySelector(`[data-appreciation-textarea][data-student-id="${studentId}"]`)?.value || '';
|
|
if (!textareaValue.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.')) {
|
|
console.log(`✅ Finalisation en focus pour élève ${studentId}`);
|
|
this.saveFocusAppreciation(studentId, textareaValue, true);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
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) {
|
|
console.log(`✅ Sauvegarde réussie en focus pour élève ${studentId}`);
|
|
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) {
|
|
console.log('🎯 Focus automatique sur le textarea d\'appréciation');
|
|
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();
|
|
console.log('⬅️ Navigation vers élève précédent avec focus sur appréciation');
|
|
}
|
|
}
|
|
|
|
navigateNext() {
|
|
if (this.parent.state.focusCurrentIndex < this.parent.state.filteredStudents.length - 1) {
|
|
this.parent.state.focusCurrentIndex++;
|
|
this.showCurrentStudent();
|
|
console.log('➡️ Navigation vers élève suivant avec focus sur appréciation');
|
|
}
|
|
}
|
|
|
|
optimizeHeight() {
|
|
const focusContainer = this.parent.elements.focusModeDisplay;
|
|
if (!focusContainer) return;
|
|
|
|
const student = focusContainer.querySelector('.focus-mode-student');
|
|
if (!student) return;
|
|
|
|
// Calculer la hauteur disponible
|
|
const windowHeight = window.innerHeight;
|
|
const headerHeight = 200; // Approximation header + navigation + contrôles
|
|
const maxHeight = windowHeight - headerHeight;
|
|
|
|
// Ajuster la hauteur de la carte
|
|
student.style.maxHeight = `${maxHeight}px`;
|
|
|
|
// Scroll vers le haut si nécessaire
|
|
window.scrollTo(0, 0);
|
|
}
|
|
|
|
// 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; |