1152 lines
54 KiB
HTML
1152 lines
54 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Saisie des notes - {{ assessment.title }} - Gestion Scolaire{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Panneau d'aide raccourcis clavier -->
|
|
<div id="keyboard-help" class="fixed top-4 right-4 bg-gray-800 text-white p-4 rounded-lg shadow-lg z-50 hidden max-w-sm">
|
|
<div class="flex justify-between items-start mb-3">
|
|
<h3 class="font-semibold text-sm">Raccourcis clavier</h3>
|
|
<button onclick="toggleKeyboardHelp()" class="text-gray-300 hover:text-white">
|
|
<svg class="w-4 h-4" 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>
|
|
<div class="text-xs space-y-1">
|
|
<div><kbd class="bg-gray-700 px-1 rounded">Tab</kbd> / <kbd class="bg-gray-700 px-1 rounded">Shift+Tab</kbd> : Navigation</div>
|
|
<div><kbd class="bg-gray-700 px-1 rounded">Enter</kbd> : Champ suivant (même colonne)</div>
|
|
<div><kbd class="bg-gray-700 px-1 rounded">.</kbd> : Non évalué (scores)</div>
|
|
<div><kbd class="bg-gray-700 px-1 rounded">Ctrl+S</kbd> : Sauvegarder</div>
|
|
<div><kbd class="bg-gray-700 px-1 rounded">Ctrl+Z</kbd> : Annuler dernière saisie</div>
|
|
<div><kbd class="bg-gray-700 px-1 rounded">Échap</kbd> : Vider le champ actuel</div>
|
|
<div><kbd class="bg-gray-700 px-1 rounded">F1</kbd> : Afficher/Masquer cette aide</div>
|
|
</div>
|
|
</div>
|
|
<div class="grading-container w-full max-w-none space-y-6">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block">
|
|
← Retour à l'évaluation
|
|
</a>
|
|
<h1 class="text-2xl font-bold text-gray-900">Saisie des notes</h1>
|
|
<p class="text-gray-600">{{ assessment.title }} - {{ assessment.class_group.name }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Guide de saisie unifié moderne -->
|
|
<div class="bg-gradient-to-r from-blue-50 to-purple-50 border border-blue-200 rounded-lg p-4">
|
|
<div class="flex justify-between items-start">
|
|
<div class="flex-1">
|
|
<h3 class="text-sm font-semibold text-blue-900 mb-2">Guide de saisie unifié</h3>
|
|
<div class="grading-guide text-xs text-blue-800">
|
|
<span><strong>Notes :</strong> Valeurs décimales (ex: 15.5)</span>
|
|
<span class="mx-3">•</span>
|
|
<span><strong>Scores :</strong>
|
|
{% for value in ['0', '1', '2', '3'] %}
|
|
{% if value in scale_values %}
|
|
{% set config = scale_values[value] %}
|
|
<span style="color: {{ config.color }}; font-weight: bold;">{{ value }}={{ config.label }}</span>
|
|
{% if not loop.last %}, {% endif %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
</span>
|
|
<span class="mx-3">•</span>
|
|
<span><strong>Spéciaux :</strong>
|
|
{% set special_items = [] %}
|
|
{% for value in scale_values.keys() if not value.isdigit() %}
|
|
{% if value in scale_values %}
|
|
{% set config = scale_values[value] %}
|
|
{% set item = '<kbd class="bg-gray-200 px-1 rounded text-xs" style="color: ' + config.color + '; font-weight: bold;">' + value + '</kbd><span style="color: ' + config.color + ';">=' + config.label + '</span>' %}
|
|
{% set _ = special_items.append(item) %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
{{ special_items | join(', ') | safe }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
{% if not grading_elements %}
|
|
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-yellow-800">Aucun élément de notation</h3>
|
|
<div class="mt-2 text-sm text-yellow-700">
|
|
<p>Cette évaluation n'a pas encore d'éléments de notation configurés. Vous devez d'abord créer des exercices et leurs éléments de notation.</p>
|
|
</div>
|
|
<div class="mt-4">
|
|
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-sm font-medium text-yellow-800 underline hover:text-yellow-900">
|
|
Configurer l'évaluation →
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<form method="POST" action="{{ url_for('grading.save_grades', assessment_id=assessment.id) }}" class="space-y-6" id="grading-form">
|
|
<!-- Tableau de saisie avec header unifié -->
|
|
<div class="grading-table-wrapper bg-white shadow rounded-lg">
|
|
<!-- Header unique intelligent -->
|
|
<div class="table-header px-4 py-3 border-b border-gray-200 flex justify-between items-center">
|
|
<h2 class="text-base font-medium text-gray-900">
|
|
<span class="normal-title">Grille de notation</span>
|
|
<span class="fullscreen-title hidden">Grille de notation - Mode plein écran</span>
|
|
</h2>
|
|
<div class="flex items-center space-x-3">
|
|
<button type="button" onclick="toggleFullscreen()" id="fullscreen-btn" class="text-xs bg-purple-100 hover:bg-purple-200 text-purple-800 px-3 py-1 rounded transition-colors flex items-center space-x-1" title="Passer en plein écran">
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<!-- Icône expand -->
|
|
<path class="expand-icon" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
|
|
<!-- Icône close -->
|
|
<path class="close-icon hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
<span class="btn-text">Plein écran</span>
|
|
</button>
|
|
<button type="button" onclick="toggleKeyboardHelp()" class="text-xs bg-blue-100 hover:bg-blue-200 text-blue-800 px-2 py-1 rounded transition-colors">
|
|
📋 F1
|
|
</button>
|
|
<div class="text-xs">
|
|
<div class="text-blue-700">Progression :</div>
|
|
<div id="progress-indicator" class="font-semibold text-blue-900">0 / {{ (students|length * grading_elements|length) }} champs</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grading-table-container">
|
|
<table class="min-w-full divide-y divide-gray-200 table-fixed grading-table">
|
|
<!-- En-têtes des exercices -->
|
|
<thead class="bg-gradient-to-r from-indigo-50 to-purple-50 sticky top-0 z-50 shadow-sm">
|
|
<tr class="border-b-2 border-indigo-200">
|
|
<th scope="col" class="px-3 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gradient-to-r from-indigo-50 to-purple-50 border-r border-indigo-200 w-48">
|
|
Élève
|
|
</th>
|
|
{% set current_exercise = '' %}
|
|
{% set exercise_elements = {} %}
|
|
{% for element in grading_elements %}
|
|
{% if element.exercise.title not in exercise_elements %}
|
|
{% set _ = exercise_elements.update({element.exercise.title: []}) %}
|
|
{% endif %}
|
|
{% set _ = exercise_elements[element.exercise.title].append(element) %}
|
|
{% endfor %}
|
|
|
|
{% for exercise_title, elements in exercise_elements.items() %}
|
|
<th scope="colgroup" colspan="{{ elements|length }}" class="px-2 py-1.5 text-center text-sm font-bold text-indigo-900 bg-gradient-to-r from-indigo-100 to-purple-100 border-x border-indigo-300">
|
|
{{ exercise_title }}
|
|
<div class="text-xs font-normal text-indigo-700 mt-0.5">{{ elements|length }} élément{{ 's' if elements|length > 1 else '' }}</div>
|
|
</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
|
|
<!-- En-têtes des éléments de notation -->
|
|
<thead class="bg-gray-50 sticky z-40 shadow-sm" style="top: 3rem;">
|
|
<tr>
|
|
<th scope="col" class="px-3 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gray-50 border-r border-gray-200 w-48">
|
|
<!-- Filtre des élèves intégré -->
|
|
<div class="relative w-full">
|
|
<input type="text"
|
|
id="student-filter"
|
|
placeholder="Filtrer les élèves..."
|
|
class="w-full px-2 py-1 pl-6 text-xs border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white">
|
|
<div class="absolute inset-y-0 left-0 pl-2 flex items-center pointer-events-none">
|
|
<svg class="h-3 w-3 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<button type="button"
|
|
id="clear-filter"
|
|
class="absolute inset-y-0 right-0 pr-2 flex items-center text-gray-400 hover:text-gray-600 hidden"
|
|
onclick="clearStudentFilter()">
|
|
<svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="text-center mt-1">
|
|
<span id="student-count" class="text-xs text-gray-500">{{ students|length }} élève(s)</span>
|
|
</div>
|
|
</th>
|
|
{% set current_exercise = '' %}
|
|
{% for element in grading_elements %}
|
|
{% if element.exercise.title != current_exercise %}
|
|
{% set current_exercise = element.exercise.title %}
|
|
{% set exercise_color = 'border-l-4 border-' + ['blue', 'green', 'purple', 'orange', 'pink'][loop.index0 % 5] + '-400' %}
|
|
{% endif %}
|
|
|
|
<th scope="col" class="grading-header px-2 py-1.5 text-center text-xs font-medium text-gray-500 uppercase tracking-wider min-w-28 {{ exercise_color }} bg-gradient-to-b from-gray-50 to-gray-100">
|
|
<div class="element-label text-xs font-semibold text-gray-900 truncate">{{ element.label }}</div>
|
|
|
|
{% if element.description %}
|
|
<div class="text-xs text-gray-600 mt-1 leading-tight">{{ element.description }}</div>
|
|
{% endif %}
|
|
|
|
<div class="element-type font-normal text-xs mt-1 flex justify-center">
|
|
{% if element.grading_type == 'score' %}
|
|
<span class="badge-score inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
|
|
0-3
|
|
</span>
|
|
{% else %}
|
|
<span class="badge-notes inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
|
|
/{{ element.max_points }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if element.skill %}
|
|
<div class="text-xs text-gray-400 mt-0.5 truncate" title="{{ element.skill }}">{{ element.skill }}</div>
|
|
{% endif %}
|
|
</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-50">
|
|
{% for student in students %}
|
|
<tr class="student-row hover:bg-gray-25 text-sm transition-colors duration-150" data-student-name="{{ (student.first_name + ' ' + student.last_name)|lower }}">
|
|
<td class="px-3 py-1.5 whitespace-nowrap text-sm font-medium text-gray-900 sticky left-0 bg-white border-r border-gray-200">
|
|
<div class="flex items-center">
|
|
<div class="text-xs text-gray-500 w-6 student-index">{{ loop.index }}</div>
|
|
<div>{{ student.first_name }} {{ student.last_name }}</div>
|
|
</div>
|
|
</td>
|
|
{% set current_exercise = '' %}
|
|
{% for element in grading_elements %}
|
|
{% if element.exercise.title != current_exercise %}
|
|
{% set current_exercise = element.exercise.title %}
|
|
{% set exercise_bg_color = ['bg-blue-25', 'bg-green-25', 'bg-purple-25', 'bg-orange-25', 'bg-pink-25'][loop.index0 % 5] %}
|
|
{% set exercise_border_color = ['border-l-2 border-blue-300', 'border-l-2 border-green-300', 'border-l-2 border-purple-300', 'border-l-2 border-orange-300', 'border-l-2 border-pink-300'][loop.index0 % 5] %}
|
|
{% endif %}
|
|
|
|
{% set grade_key = student.id ~ '_' ~ element.id %}
|
|
{% set existing_grade = existing_grades.get(grade_key) %}
|
|
<td class="px-1 py-1.5 whitespace-nowrap text-center {{ exercise_border_color if loop.first or (grading_elements[loop.index0-1].exercise.title != element.exercise.title) else '' }} {{ exercise_bg_color }}">
|
|
<div class="space-y-1">
|
|
<!-- Champs de saisie unifiés -->
|
|
{% if element.grading_type == 'score' %}
|
|
<select name="grade_{{ student.id }}_{{ element.id }}"
|
|
class="grading-input block w-full text-xs border-gray-300 rounded focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all duration-200 py-0.5 h-7"
|
|
data-type="score"
|
|
data-student-id="{{ student.id }}"
|
|
data-element-id="{{ element.id }}"
|
|
data-row="{{ loop.index0 }}"
|
|
data-col="{{ loop.index0 }}"
|
|
onchange="handleGradeChange(this)"
|
|
onfocus="handleGradeFocus(this)"
|
|
onkeydown="handleGradeKeydown(event, this)">
|
|
<option value="" style="color: #6b7280;">-</option>
|
|
{% for value in ['0', '1', '2', '3'] %}
|
|
{% set display_info = config_manager.get_display_info(value, 'score') %}
|
|
<option value="{{ value }}" style="color: {{ display_info.color }}; font-weight: bold;" {% if existing_grade and existing_grade.value == value %}selected{% endif %}>{{ value }} - {{ display_info.label }}</option>
|
|
{% endfor %}
|
|
{% for special_value in scale_values.keys() if not special_value.isdigit() %}
|
|
{% if special_value in scale_values %}
|
|
{% set display_info = config_manager.get_display_info(special_value, 'score') %}
|
|
<option value="{{ special_value }}" style="color: {{ display_info.color }}; font-style: italic;" {% if existing_grade and existing_grade.value == special_value %}selected{% endif %}>{{ special_value }} - {{ display_info.label }}</option>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</select>
|
|
{% else %}
|
|
<input type="text"
|
|
name="grade_{{ student.id }}_{{ element.id }}"
|
|
value="{% if existing_grade %}{{ existing_grade.value }}{% endif %}"
|
|
class="grading-input block w-full text-xs border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-center transition-all duration-200 py-0.5 h-7"
|
|
data-type="notes"
|
|
data-max-points="{{ element.max_points }}"
|
|
data-student-id="{{ student.id }}"
|
|
data-element-id="{{ element.id }}"
|
|
data-row="{{ loop.index0 }}"
|
|
data-col="{{ loop.index0 }}"
|
|
placeholder="0-{{ element.max_points }}"
|
|
oninput="handleGradeChange(this)"
|
|
onfocus="handleGradeFocus(this)"
|
|
onkeydown="handleGradeKeydown(event, this)">
|
|
{% endif %}
|
|
<input type="text"
|
|
name="comment_{{ student.id }}_{{ element.id }}"
|
|
value="{% if existing_grade and existing_grade.comment %}{{ existing_grade.comment }}{% endif %}"
|
|
class="comment-input block w-full text-xs border-gray-200 rounded focus:ring-blue-400 focus:border-blue-400 transition-all duration-200 py-0.5 h-6 bg-gray-25"
|
|
data-student-id="{{ student.id }}"
|
|
data-element-id="{{ element.id }}"
|
|
data-row="{{ loop.index0 }}"
|
|
data-col="{{ loop.index0 }}"
|
|
placeholder="💬"
|
|
title="Commentaire (optionnel)"
|
|
onkeydown="handleCommentKeydown(event, this)">
|
|
</div>
|
|
</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200 flex justify-between items-center">
|
|
<div class="flex items-center space-x-4 text-sm text-gray-600">
|
|
<div id="save-status" class="flex items-center">
|
|
<span class="w-2 h-2 bg-gray-400 rounded-full mr-2"></span>
|
|
<span>Non sauvegardé</span>
|
|
</div>
|
|
<div id="current-position" class="text-xs">
|
|
Position : - / -
|
|
</div>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<button type="button" onclick="resetForm()" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
|
|
Réinitialiser
|
|
</button>
|
|
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
|
|
Annuler
|
|
</a>
|
|
<button type="submit" id="save-button" class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors flex items-center">
|
|
<span id="save-text">Sauvegarder les notes</span>
|
|
<span id="save-spinner" class="ml-2 w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin hidden"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- CSS optimisé avec mode plein écran ULTRA SIMPLE -->
|
|
<style>
|
|
/* ====== MODE PLEIN ÉCRAN SIMPLIFIEÉ ======
|
|
* Approche directe : mettre JUSTE le wrapper du tableau en plein écran
|
|
*/
|
|
|
|
/* Header intelligent - par défaut */
|
|
.fullscreen-title,
|
|
.close-icon {
|
|
display: none;
|
|
}
|
|
|
|
/* Quand fullscreen activé */
|
|
body.fullscreen-active {
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Le wrapper du tableau devient plein écran */
|
|
body.fullscreen-active .grading-table-wrapper {
|
|
position: fixed !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
width: 100vw !important;
|
|
height: 100vh !important;
|
|
z-index: 9999 !important;
|
|
background: white !important;
|
|
border-radius: 0 !important;
|
|
box-shadow: none !important;
|
|
margin: 0 !important;
|
|
display: flex !important;
|
|
flex-direction: column !important;
|
|
}
|
|
|
|
/* En plein écran : changer les textes/icônes */
|
|
body.fullscreen-active .normal-title,
|
|
body.fullscreen-active .expand-icon {
|
|
display: none !important;
|
|
}
|
|
|
|
body.fullscreen-active .fullscreen-title,
|
|
body.fullscreen-active .close-icon {
|
|
display: inline !important;
|
|
}
|
|
|
|
/* Bouton en mode rouge en plein écran */
|
|
body.fullscreen-active #fullscreen-btn {
|
|
background-color: #fef2f2 !important;
|
|
color: #dc2626 !important;
|
|
}
|
|
|
|
body.fullscreen-active #fullscreen-btn:hover {
|
|
background-color: #fecaca !important;
|
|
}
|
|
|
|
/* Le conteneur du tableau s'étire */
|
|
body.fullscreen-active .grading-table-container {
|
|
flex: 1 !important;
|
|
overflow: auto !important;
|
|
}
|
|
|
|
/* ====== TABLEAU DE NOTATION (styles de base) ====== */
|
|
.grading-table-container {
|
|
overflow: auto;
|
|
max-height: 100vh;
|
|
}
|
|
|
|
.grading-table thead {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 50;
|
|
background: white;
|
|
}
|
|
|
|
.grading-table thead:nth-child(2) {
|
|
top: 3rem;
|
|
z-index: 40;
|
|
}
|
|
|
|
.grading-table th[scope="col"]:first-child {
|
|
position: sticky;
|
|
left: 0;
|
|
z-index: 51;
|
|
background: white;
|
|
}
|
|
|
|
/* ====== ANIMATIONS ====== */
|
|
.grading-input, .comment-input {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.grading-input:focus, .comment-input:focus {
|
|
transform: scale(1.02);
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.grading-table-wrapper {
|
|
transition: all 0.3s ease;
|
|
}
|
|
</style>
|
|
|
|
<!-- Légende -->
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Toast de notification -->
|
|
<div id="toast" class="fixed bottom-4 right-4 transform translate-y-full transition-transform duration-300 z-50">
|
|
<div class="bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center">
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
</svg>
|
|
<span id="toast-message">Action réalisée</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ====== CONFIGURATION GLOBALE ======
|
|
const GRADING_CONFIG = {
|
|
types: {
|
|
notes: { label: 'Notes numériques', input_type: 'number' },
|
|
score: { label: 'Échelle de compétences (0-3)', max_value: 3, input_type: 'select' }
|
|
},
|
|
scale_values: {{ scale_values | tojson }},
|
|
special_values: {
|
|
{% for value in scale_values.keys() if not value.isdigit() %}
|
|
'{{ value }}' : {{ scale_values[value] | tojson }}{% if not loop.last %},{% endif %}
|
|
{% endfor %}
|
|
},
|
|
special_keys: Object.keys({{ scale_values | tojson }}).filter(key => !['0', '1', '2', '3'].includes(key))
|
|
};
|
|
|
|
// ====== ÉTAT GLOBAL ======
|
|
class GradingState {
|
|
constructor() {
|
|
this.currentRow = 0;
|
|
this.currentCol = 0;
|
|
this.totalRows = {{ students|length }};
|
|
this.totalCols = {{ grading_elements|length }};
|
|
this.unsavedChanges = new Set();
|
|
this.undoStack = [];
|
|
this.isAutoSaving = false;
|
|
this.filterIsActive = false;
|
|
this.isFullscreen = false;
|
|
}
|
|
|
|
reset() {
|
|
this.unsavedChanges.clear();
|
|
this.undoStack = [];
|
|
this.updateSaveStatus();
|
|
this.updateProgress();
|
|
}
|
|
|
|
updateSaveStatus() {
|
|
const statusEl = document.getElementById('save-status');
|
|
const indicator = statusEl?.querySelector('.w-2');
|
|
const text = statusEl?.querySelector('span:last-child');
|
|
|
|
if (!indicator || !text) return;
|
|
|
|
if (this.unsavedChanges.size > 0) {
|
|
indicator.className = 'w-2 h-2 bg-orange-400 rounded-full mr-2';
|
|
text.textContent = `${this.unsavedChanges.size} modification(s) non sauvegardée(s)`;
|
|
} else {
|
|
indicator.className = 'w-2 h-2 bg-green-400 rounded-full mr-2';
|
|
text.textContent = 'Toutes les modifications sauvegardées';
|
|
}
|
|
}
|
|
|
|
updateProgress() {
|
|
const filledCount = Array.from(document.querySelectorAll('.grading-input')).filter(input =>
|
|
input.value && input.value.trim() !== ''
|
|
).length;
|
|
const totalInputs = this.totalRows * this.totalCols;
|
|
|
|
const progressEl = document.getElementById('progress-indicator');
|
|
if (progressEl) {
|
|
progressEl.textContent = `${filledCount} / ${totalInputs} champs`;
|
|
progressEl.className = filledCount === 0 ? 'font-semibold text-red-600' :
|
|
filledCount === totalInputs ? 'font-semibold text-green-600' :
|
|
'font-semibold text-orange-600';
|
|
}
|
|
}
|
|
}
|
|
|
|
const state = new GradingState();
|
|
|
|
// ====== INITIALISATION ======
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
setupKeyboardNavigation();
|
|
state.updateProgress();
|
|
setupAutosave();
|
|
ColorManager.applyInitialColors();
|
|
FilterManager.setup();
|
|
});
|
|
|
|
// ====== GESTIONNAIRE DE COULEURS ======
|
|
class ColorManager {
|
|
static applyInitialColors() {
|
|
document.querySelectorAll('.grading-input').forEach(input => {
|
|
const value = input.value;
|
|
const type = input.dataset.type;
|
|
const maxPoints = parseFloat(input.dataset.maxPoints) || 20;
|
|
|
|
if (value && value.trim() !== '') {
|
|
const isValid = ValidationManager.validateGradeValue(value, type, maxPoints);
|
|
this.applyColorToInput(input, value, type, isValid, maxPoints);
|
|
}
|
|
});
|
|
}
|
|
|
|
static applyColorToInput(input, value, type, isValid, maxPoints) {
|
|
// Reset styles
|
|
input.className = input.className.replace(/border-\w+-\d+|bg-\w+-\d+/g, '').trim();
|
|
input.style.cssText = '';
|
|
|
|
if (!value || value.trim() === '') {
|
|
input.style.color = '#6b7280';
|
|
return;
|
|
}
|
|
|
|
// Couleurs configurées
|
|
if (GRADING_CONFIG.scale_values[value]) {
|
|
const config = GRADING_CONFIG.scale_values[value];
|
|
input.style.color = config.color;
|
|
input.style.backgroundColor = this.hexToRgba(config.color, 0.1);
|
|
input.style.borderColor = this.hexToRgba(config.color, 0.3);
|
|
input.style.fontWeight = 'bold';
|
|
|
|
if (isNaN(value)) input.style.fontStyle = 'italic';
|
|
return;
|
|
}
|
|
|
|
if (!isValid) {
|
|
input.style.color = '#dc2626';
|
|
input.style.backgroundColor = '#fef2f2';
|
|
input.style.borderColor = '#fca5a5';
|
|
|
|
const message = type === 'score' ?
|
|
'Valeur autorisée : 0, 1, 2, 3 ou valeurs spéciales' :
|
|
`Valeur autorisée : 0 à ${maxPoints} ou valeurs spéciales`;
|
|
UIManager.showValidationMessage(input, message);
|
|
return;
|
|
}
|
|
|
|
// Gradient pour notes numériques
|
|
if (type === 'notes') {
|
|
const percentage = Math.min(100, (parseFloat(value) / maxPoints) * 100);
|
|
const colors = ['#ef4444', '#f6d32d', '#22c55e', '#059669'];
|
|
const thresholds = [40, 60, 80];
|
|
|
|
let colorIndex = 0;
|
|
for (let i = 0; i < thresholds.length; i++) {
|
|
if (percentage >= thresholds[i]) colorIndex = i + 1;
|
|
}
|
|
|
|
const noteColor = colors[colorIndex];
|
|
input.style.color = noteColor;
|
|
input.style.backgroundColor = this.hexToRgba(noteColor, 0.1);
|
|
input.style.borderColor = this.hexToRgba(noteColor, 0.3);
|
|
input.style.fontWeight = 'bold';
|
|
}
|
|
}
|
|
|
|
static hexToRgba(hex, alpha) {
|
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
}
|
|
}
|
|
|
|
// ====== GESTIONNAIRE DE NAVIGATION ======
|
|
class NavigationManager {
|
|
static setup() {
|
|
document.addEventListener('keydown', this.handleGlobalKeydown.bind(this));
|
|
}
|
|
|
|
static handleGlobalKeydown(e) {
|
|
if (e.target.id === 'student-filter' || state.filterIsActive) return;
|
|
|
|
const handlers = {
|
|
'F1': () => { e.preventDefault(); UIManager.toggleKeyboardHelp(); },
|
|
's': () => { if (e.ctrlKey) { e.preventDefault(); FormManager.save(); }},
|
|
'z': () => { if (e.ctrlKey) { e.preventDefault(); this.undoLastChange(); }}
|
|
};
|
|
|
|
const handler = handlers[e.key];
|
|
if (handler) handler();
|
|
}
|
|
|
|
static navigateToNext(direction) {
|
|
if (state.filterIsActive) return;
|
|
|
|
const directions = {
|
|
up: () => ({ row: Math.max(0, state.currentRow - 1), col: state.currentCol }),
|
|
down: () => ({ row: Math.min(state.totalRows - 1, state.currentRow + 1), col: state.currentCol }),
|
|
left: () => ({ row: state.currentRow, col: Math.max(0, state.currentCol - 1) }),
|
|
right: () => ({ row: state.currentRow, col: Math.min(state.totalCols - 1, state.currentCol + 1) })
|
|
};
|
|
|
|
const newPos = directions[direction]();
|
|
const targetInput = document.querySelector(
|
|
`.grading-input[data-row="${newPos.row}"][data-col="${newPos.col}"]`
|
|
);
|
|
|
|
if (targetInput) targetInput.focus();
|
|
}
|
|
|
|
static undoLastChange() {
|
|
if (state.undoStack.length === 0) {
|
|
UIManager.showToast('Aucune modification à annuler', 'info');
|
|
return;
|
|
}
|
|
|
|
const lastChange = state.undoStack.pop();
|
|
lastChange.input.value = lastChange.oldValue;
|
|
InputManager.handleGradeChange(lastChange.input);
|
|
|
|
UIManager.showToast('Modification annulée', 'success');
|
|
}
|
|
}
|
|
|
|
function setupKeyboardNavigation() {
|
|
NavigationManager.setup();
|
|
}
|
|
|
|
// ====== GESTIONNAIRE D'ENTRÉES ======
|
|
class InputManager {
|
|
static handleGradeFocus(input) {
|
|
state.currentRow = parseInt(input.dataset.row);
|
|
state.currentCol = parseInt(input.dataset.col);
|
|
this.updateCurrentPosition();
|
|
|
|
if (input.type !== 'select-one') {
|
|
input.select();
|
|
}
|
|
}
|
|
|
|
static handleGradeKeydown(event, input) {
|
|
if (state.filterIsActive) return;
|
|
|
|
const keyHandlers = {
|
|
'Escape': () => { event.preventDefault(); input.value = ''; this.handleGradeChange(input); },
|
|
'Enter': () => { event.preventDefault(); NavigationManager.navigateToNext('down'); },
|
|
'ArrowUp': () => { event.preventDefault(); NavigationManager.navigateToNext('up'); },
|
|
'ArrowDown': () => { event.preventDefault(); NavigationManager.navigateToNext('down'); },
|
|
'ArrowLeft': () => { event.preventDefault(); NavigationManager.navigateToNext('left'); },
|
|
'ArrowRight': () => { event.preventDefault(); NavigationManager.navigateToNext('right'); }
|
|
};
|
|
|
|
// Valeurs spéciales (sauf point)
|
|
if (GRADING_CONFIG.special_keys.includes(event.key) && event.key !== '.') {
|
|
event.preventDefault();
|
|
input.value = event.key;
|
|
this.handleGradeChange(input);
|
|
NavigationManager.navigateToNext('down');
|
|
return;
|
|
}
|
|
|
|
const handler = keyHandlers[event.key];
|
|
if (handler) handler();
|
|
}
|
|
|
|
static handleCommentKeydown(event, input) {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
NavigationManager.navigateToNext('down');
|
|
}
|
|
}
|
|
|
|
static handleGradeChange(input) {
|
|
const key = `${input.dataset.studentId}_${input.dataset.elementId}`;
|
|
const type = input.dataset.type;
|
|
let value = input.value;
|
|
const maxPoints = parseFloat(input.dataset.maxPoints) || 20;
|
|
|
|
// Conversion virgule -> point
|
|
if (type === 'notes' && value.includes(',')) {
|
|
value = value.replace(',', '.');
|
|
input.value = value;
|
|
}
|
|
|
|
const isValid = ValidationManager.validateGradeValue(value, type, maxPoints);
|
|
ColorManager.applyColorToInput(input, value, type, isValid, maxPoints);
|
|
|
|
state.unsavedChanges.add(key);
|
|
|
|
// Historique pour annulation
|
|
state.undoStack.push({
|
|
input: input,
|
|
oldValue: input.defaultValue || '',
|
|
newValue: input.value
|
|
});
|
|
|
|
if (state.undoStack.length > 50) {
|
|
state.undoStack.shift();
|
|
}
|
|
|
|
state.updateSaveStatus();
|
|
state.updateProgress();
|
|
|
|
// Feedback visuel
|
|
input.classList.add('bg-yellow-50', 'border-yellow-300');
|
|
setTimeout(() => {
|
|
input.classList.remove('bg-yellow-50', 'border-yellow-300');
|
|
ColorManager.applyColorToInput(input, value, type, isValid, maxPoints);
|
|
}, 1000);
|
|
}
|
|
|
|
static updateCurrentPosition() {
|
|
const posEl = document.getElementById('current-position');
|
|
if (posEl) {
|
|
posEl.textContent = `Position : ${state.currentRow + 1} / ${state.totalRows} (ligne), ${state.currentCol + 1} / ${state.totalCols} (colonne)`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fonctions de compatibilité
|
|
function handleGradeFocus(input) { InputManager.handleGradeFocus(input); }
|
|
function handleGradeKeydown(event, input) { InputManager.handleGradeKeydown(event, input); }
|
|
function handleCommentKeydown(event, input) { InputManager.handleCommentKeydown(event, input); }
|
|
function handleGradeChange(input) { InputManager.handleGradeChange(input); }
|
|
function navigateToNext(direction) { NavigationManager.navigateToNext(direction); }
|
|
|
|
// ====== GESTIONNAIRE DE VALIDATION ======
|
|
class ValidationManager {
|
|
static validateGradeValue(value, type, maxPoints) {
|
|
if (!value || value.trim() === '') return true;
|
|
|
|
const trimmedValue = value.trim();
|
|
|
|
if (GRADING_CONFIG.special_values[trimmedValue]) return true;
|
|
|
|
if (type === 'score') {
|
|
return ['0', '1', '2', '3'].includes(trimmedValue);
|
|
}
|
|
|
|
if (type === 'notes') {
|
|
const normalizedValue = trimmedValue.replace(',', '.');
|
|
const numValue = parseFloat(normalizedValue);
|
|
|
|
return !isNaN(numValue) &&
|
|
numValue >= 0 &&
|
|
numValue <= maxPoints &&
|
|
/^[0-9]+([.,][0-9]+)?$/.test(trimmedValue);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ====== GESTIONNAIRE DE FORMULAIRE ======
|
|
class FormManager {
|
|
static reset() {
|
|
if (confirm('Êtes-vous sûr de vouloir réinitialiser toutes les notes ? Cette action est irréversible.')) {
|
|
document.querySelectorAll('.grading-input, .comment-input').forEach(input => {
|
|
input.value = '';
|
|
});
|
|
state.reset();
|
|
UIManager.showToast('Formulaire réinitialisé', 'info');
|
|
}
|
|
}
|
|
|
|
static save() {
|
|
if (state.isFullscreen) {
|
|
this.saveFromFullscreen();
|
|
} else {
|
|
this.saveNormal();
|
|
}
|
|
}
|
|
|
|
static async saveNormal() {
|
|
const form = document.getElementById('grading-form');
|
|
const saveButton = document.getElementById('save-button');
|
|
const saveText = document.getElementById('save-text');
|
|
const saveSpinner = document.getElementById('save-spinner');
|
|
|
|
saveButton.disabled = true;
|
|
saveText.textContent = 'Sauvegarde...';
|
|
saveSpinner.classList.remove('hidden');
|
|
|
|
try {
|
|
const formData = new FormData(form);
|
|
const response = await fetch(form.action, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
state.unsavedChanges.clear();
|
|
state.updateSaveStatus();
|
|
UIManager.showToast('Notes sauvegardées avec succès', 'success');
|
|
} else {
|
|
UIManager.showToast(result.message || 'Erreur lors de la sauvegarde', 'error');
|
|
}
|
|
} else {
|
|
UIManager.showToast('Erreur réseau lors de la sauvegarde', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur sauvegarde:', error);
|
|
UIManager.showToast('Erreur lors de la sauvegarde', 'error');
|
|
} finally {
|
|
saveButton.disabled = false;
|
|
saveText.textContent = 'Sauvegarder';
|
|
saveSpinner.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
static async saveFromFullscreen() {
|
|
const form = document.getElementById('grading-form');
|
|
const saveButtonFs = document.getElementById('save-button-fs');
|
|
const saveTextFs = document.getElementById('save-text-fs');
|
|
const saveSpinnerFs = document.getElementById('save-spinner-fs');
|
|
|
|
if (!form) {
|
|
UIManager.showToast('Erreur : formulaire non trouvé', 'error');
|
|
return;
|
|
}
|
|
|
|
if (saveButtonFs) {
|
|
saveButtonFs.disabled = true;
|
|
if (saveTextFs) saveTextFs.textContent = 'Sauvegarde...';
|
|
if (saveSpinnerFs) saveSpinnerFs.classList.remove('hidden');
|
|
}
|
|
|
|
try {
|
|
const formData = new FormData(form);
|
|
const response = await fetch(form.action, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
state.unsavedChanges.clear();
|
|
state.updateSaveStatus();
|
|
UIManager.showToast('Notes sauvegardées avec succès', 'success');
|
|
} else {
|
|
UIManager.showToast(result.message || 'Erreur lors de la sauvegarde', 'error');
|
|
}
|
|
} else {
|
|
UIManager.showToast('Erreur réseau lors de la sauvegarde', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur sauvegarde:', error);
|
|
UIManager.showToast('Erreur lors de la sauvegarde', 'error');
|
|
} finally {
|
|
if (saveButtonFs) {
|
|
saveButtonFs.disabled = false;
|
|
if (saveTextFs) saveTextFs.textContent = 'Sauvegarder';
|
|
if (saveSpinnerFs) saveSpinnerFs.classList.add('hidden');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Plus besoin de synchronisation - même tableau utilisé partout
|
|
}
|
|
|
|
// Fonctions de compatibilité
|
|
function resetForm() { FormManager.reset(); }
|
|
function saveForm() { FormManager.saveNormal(); }
|
|
function saveFormFromFullscreen() { FormManager.saveFromFullscreen(); }
|
|
|
|
|
|
// ====== GESTIONNAIRE D'INTERFACE UTILISATEUR ======
|
|
class UIManager {
|
|
static toggleKeyboardHelp() {
|
|
const help = document.getElementById('keyboard-help');
|
|
help.classList.toggle('hidden');
|
|
}
|
|
|
|
static showValidationMessage(input, message) {
|
|
const existingMessage = input.parentNode.querySelector('.validation-message');
|
|
if (existingMessage) existingMessage.remove();
|
|
|
|
const messageEl = document.createElement('div');
|
|
messageEl.className = 'validation-message absolute z-10 bg-red-100 border border-red-300 text-red-700 px-2 py-1 rounded text-xs mt-1 shadow';
|
|
messageEl.textContent = message;
|
|
messageEl.style.minWidth = '200px';
|
|
|
|
input.parentNode.style.position = 'relative';
|
|
input.parentNode.appendChild(messageEl);
|
|
|
|
setTimeout(() => {
|
|
if (messageEl.parentNode) messageEl.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
static showToast(message, type = 'success') {
|
|
const toast = document.getElementById('toast');
|
|
const toastMessage = document.getElementById('toast-message');
|
|
const toastDiv = toast.querySelector('div');
|
|
|
|
const colors = {
|
|
success: 'bg-green-500',
|
|
error: 'bg-red-500',
|
|
info: 'bg-blue-500',
|
|
warning: 'bg-orange-500'
|
|
};
|
|
|
|
toastDiv.className = `${colors[type]} text-white px-4 py-2 rounded-lg shadow-lg flex items-center`;
|
|
toastMessage.textContent = message;
|
|
|
|
toast.classList.remove('translate-y-full');
|
|
toast.classList.add('translate-y-0');
|
|
|
|
setTimeout(() => {
|
|
toast.classList.remove('translate-y-0');
|
|
toast.classList.add('translate-y-full');
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
// ====== SAUVEGARDE AUTOMATIQUE ======
|
|
function setupAutosave() {
|
|
setInterval(() => {
|
|
if (state.unsavedChanges.size > 0 && !state.isAutoSaving) {
|
|
autoSave();
|
|
}
|
|
}, 30000);
|
|
}
|
|
|
|
function autoSave() {
|
|
if (state.isAutoSaving) return;
|
|
|
|
state.isAutoSaving = true;
|
|
UIManager.showToast('Sauvegarde automatique...', 'info');
|
|
|
|
setTimeout(() => {
|
|
state.isAutoSaving = false;
|
|
state.unsavedChanges.clear();
|
|
state.updateSaveStatus();
|
|
UIManager.showToast('Sauvegardé automatiquement', 'success');
|
|
}, 2000);
|
|
}
|
|
|
|
// Fonctions de compatibilité
|
|
function toggleKeyboardHelp() { UIManager.toggleKeyboardHelp(); }
|
|
function showValidationMessage(input, message) { UIManager.showValidationMessage(input, message); }
|
|
function showToast(message, type) { UIManager.showToast(message, type); }
|
|
|
|
// ====== GESTIONNAIRE DE FILTRE ======
|
|
class FilterManager {
|
|
static setup() {
|
|
const filterInput = document.getElementById('student-filter');
|
|
const clearButton = document.getElementById('clear-filter');
|
|
|
|
if (!filterInput) return;
|
|
|
|
// Blocage de navigation pendant la saisie
|
|
['keydown', 'keyup', 'keypress'].forEach(eventType => {
|
|
filterInput.addEventListener(eventType, function(e) {
|
|
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
|
|
e.stopImmediatePropagation();
|
|
}
|
|
}, true);
|
|
});
|
|
|
|
filterInput.addEventListener('focus', () => {
|
|
state.filterIsActive = true;
|
|
filterInput.select();
|
|
});
|
|
|
|
filterInput.addEventListener('blur', () => {
|
|
setTimeout(() => {
|
|
if (document.activeElement !== filterInput) {
|
|
state.filterIsActive = false;
|
|
}
|
|
}, 50);
|
|
});
|
|
|
|
filterInput.addEventListener('input', function() {
|
|
state.filterIsActive = true;
|
|
const searchTerm = this.value.toLowerCase().trim();
|
|
FilterManager.filterStudents(searchTerm);
|
|
clearButton.classList.toggle('hidden', !searchTerm);
|
|
});
|
|
|
|
filterInput.addEventListener('keydown', function(e) {
|
|
const keyHandlers = {
|
|
'Escape': () => { e.preventDefault(); e.stopPropagation(); FilterManager.clear(); },
|
|
'Enter': () => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
state.filterIsActive = false;
|
|
const firstVisible = document.querySelector('.student-row:not([style*="display: none"]) .grading-input');
|
|
if (firstVisible) firstVisible.focus();
|
|
}
|
|
};
|
|
|
|
const handler = keyHandlers[e.key];
|
|
if (handler) {
|
|
handler();
|
|
} else if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
}
|
|
|
|
static filterStudents(searchTerm) {
|
|
const studentRows = document.querySelectorAll('.student-row');
|
|
const studentCount = document.getElementById('student-count');
|
|
let visibleCount = 0;
|
|
let visibleIndex = 1;
|
|
|
|
studentRows.forEach(row => {
|
|
const studentName = row.dataset.studentName;
|
|
const isVisible = !searchTerm || studentName.includes(searchTerm);
|
|
|
|
row.style.display = isVisible ? '' : 'none';
|
|
|
|
if (isVisible) {
|
|
visibleCount++;
|
|
const indexEl = row.querySelector('.student-index');
|
|
if (indexEl) indexEl.textContent = visibleIndex++;
|
|
}
|
|
});
|
|
|
|
// Mise à jour du compteur
|
|
if (studentCount) {
|
|
const totalStudents = studentRows.length;
|
|
if (searchTerm) {
|
|
studentCount.textContent = `${visibleCount} / ${totalStudents} élève(s)`;
|
|
studentCount.className = visibleCount === 0 ? 'text-xs text-red-500' : 'text-xs text-blue-600';
|
|
} else {
|
|
studentCount.textContent = `${totalStudents} élève(s)`;
|
|
studentCount.className = 'text-xs text-gray-500';
|
|
}
|
|
}
|
|
|
|
// Mise à jour navigation
|
|
state.totalRows = visibleCount;
|
|
|
|
// Réorientation si l'élève actuel est masqué
|
|
const currentRowEl = document.querySelector(`.student-row:nth-child(${state.currentRow + 1})`);
|
|
if (currentRowEl && currentRowEl.style.display === 'none') {
|
|
const firstVisibleRow = document.querySelector('.student-row:not([style*="display: none"])');
|
|
if (firstVisibleRow && !state.filterIsActive) {
|
|
const firstVisibleInput = firstVisibleRow.querySelector('.grading-input');
|
|
if (firstVisibleInput) firstVisibleInput.focus();
|
|
}
|
|
}
|
|
|
|
state.updateProgress();
|
|
}
|
|
|
|
static clear() {
|
|
const filterInput = document.getElementById('student-filter');
|
|
const clearButton = document.getElementById('clear-filter');
|
|
|
|
if (filterInput) {
|
|
filterInput.value = '';
|
|
this.filterStudents('');
|
|
clearButton.classList.add('hidden');
|
|
state.filterIsActive = true;
|
|
setTimeout(() => filterInput.focus(), 10);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fonctions de compatibilité
|
|
function setupStudentFilter() { FilterManager.setup(); }
|
|
function filterStudents(searchTerm) { FilterManager.filterStudents(searchTerm); }
|
|
function clearStudentFilter() { FilterManager.clear(); }
|
|
|
|
// ====== GESTIONNAIRE PLEIN ÉCRAN PURE CSS ======
|
|
class FullscreenManager {
|
|
static toggle() {
|
|
if (state.isFullscreen) {
|
|
this.exit();
|
|
} else {
|
|
this.enter();
|
|
}
|
|
}
|
|
|
|
static enter() {
|
|
// Basculement de classe CSS - tout est géré par CSS !
|
|
document.body.classList.add('fullscreen-active');
|
|
state.isFullscreen = true;
|
|
|
|
// Mise à jour du texte du bouton
|
|
const btnText = document.querySelector('#fullscreen-btn .btn-text');
|
|
if (btnText) btnText.textContent = 'Quitter';
|
|
|
|
const btn = document.getElementById('fullscreen-btn');
|
|
if (btn) btn.title = 'Quitter le plein écran';
|
|
}
|
|
|
|
static exit() {
|
|
// Suppression de classe CSS
|
|
document.body.classList.remove('fullscreen-active');
|
|
state.isFullscreen = false;
|
|
|
|
// Restauration du texte du bouton
|
|
const btnText = document.querySelector('#fullscreen-btn .btn-text');
|
|
if (btnText) btnText.textContent = 'Plein écran';
|
|
|
|
const btn = document.getElementById('fullscreen-btn');
|
|
if (btn) btn.title = 'Passer en plein écran';
|
|
}
|
|
}
|
|
|
|
function toggleFullscreen() { FullscreenManager.toggle(); }
|
|
|
|
// Raccourcis clavier globaux
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'F11') {
|
|
e.preventDefault();
|
|
FullscreenManager.toggle();
|
|
} else if (e.key === 'Escape' && state.isFullscreen) {
|
|
e.preventDefault();
|
|
FullscreenManager.exit();
|
|
}
|
|
});
|
|
|
|
// Gestion de la fermeture de la page avec modifications non sauvegardées
|
|
window.addEventListener('beforeunload', function(e) {
|
|
if (state.unsavedChanges.size > 0) {
|
|
e.preventDefault();
|
|
e.returnValue = 'Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir quitter ?';
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|