893 lines
42 KiB
HTML
893 lines
42 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Résultats - {{ assessment.title }} - Gestion Scolaire{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="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">Résultats - {{ assessment.title }}</h1>
|
|
<p class="text-gray-600">{{ assessment.class_group.name }} - {{ assessment.date.strftime('%d/%m/%Y') }} - Trimestre {{ assessment.trimester }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistiques générales -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h2 class="text-lg font-medium text-gray-900">Statistiques de l'évaluation</h2>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-blue-600">{{ statistics.count }}</div>
|
|
<div class="text-sm text-gray-500">Élèves</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-green-600">{{ statistics.mean }}</div>
|
|
<div class="text-sm text-gray-500">Moyenne</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-purple-600">{{ statistics.median }}</div>
|
|
<div class="text-sm text-gray-500">Médiane</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-red-600">{{ statistics.min }}</div>
|
|
<div class="text-sm text-gray-500">Minimum</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-green-600">{{ statistics.max }}</div>
|
|
<div class="text-sm text-gray-500">Maximum</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-orange-600">{{ statistics.std_dev }}</div>
|
|
<div class="text-sm text-gray-500">Écart-type</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-sm text-gray-600 text-center">
|
|
Total des points de l'évaluation : {{ "%.1f"|format(total_max_points) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bouton d'envoi des bilans -->
|
|
<div class="flex justify-end mb-6">
|
|
<button id="sendReportsBtn" onclick="openSendReportsModal()"
|
|
class="inline-flex items-center px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
|
<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="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
</svg>
|
|
📧 Envoyer les bilans par email
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Histogramme des notes -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h2 class="text-lg font-medium text-gray-900">Distribution des notes</h2>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<canvas id="scoresChart" width="400" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tableau par compétences -->
|
|
{% if heatmap_competences %}
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h2 class="text-lg font-medium text-gray-900">Performance par compétence</h2>
|
|
<p class="text-sm text-gray-600 mt-1">Scores moyens de chaque élève par compétence (%) - Cliquez sur les en-têtes pour trier</p>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full sortable-table" id="competencesTable">
|
|
<thead>
|
|
<tr class="bg-gray-50">
|
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gray-50 sortable cursor-pointer hover:bg-gray-100" onclick="sortTable('competencesTable', 0)">
|
|
Élève
|
|
<span class="sort-arrow ml-1">⇅</span>
|
|
</th>
|
|
{% for competence in heatmap_competences.competences %}
|
|
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase tracking-wider min-w-24 sortable cursor-pointer hover:bg-gray-100"
|
|
style="color: {{ heatmap_competences.colors[competence] }}"
|
|
onclick="sortTable('competencesTable', {{ loop.index }})">
|
|
{{ competence }}
|
|
<span class="sort-arrow ml-1">⇅</span>
|
|
</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for student_index in range(heatmap_competences.students|length) %}
|
|
<tr class="{% if loop.index0 % 2 == 0 %}bg-white{% else %}bg-gray-50{% endif %}">
|
|
<td class="px-3 py-2 text-sm font-medium text-gray-900 sticky left-0 {% if loop.index0 % 2 == 0 %}bg-white{% else %}bg-gray-50{% endif %}">
|
|
{{ heatmap_competences.students[student_index] }}
|
|
</td>
|
|
{% for competence_index in range(heatmap_competences.competences|length) %}
|
|
{% set score = heatmap_competences.scores[student_index][competence_index] %}
|
|
{% set special_value = heatmap_competences.special_values[student_index][competence_index] %}
|
|
<td class="px-3 py-2 text-center text-sm"
|
|
{% if special_value and special_value in heatmap_grading_elements.scale_colors %}
|
|
{# Utiliser la couleur de la valeur spéciale #}
|
|
style="background-color: {{ heatmap_grading_elements.scale_colors[special_value].color }};
|
|
color: {% if heatmap_grading_elements.scale_colors[special_value].color in ['#ef4444', '#f97316', '#3b82f6', '#8b5cf6'] %}white{% else %}black{% endif %};"
|
|
{% elif score is not none %}
|
|
{# Utiliser le gradient pour les scores normaux #}
|
|
style="background-color: {{ '#ef4444' if score <= 25 else ('#f97316' if score <= 50 else ('#eab308' if score <= 75 else '#22c55e')) }};
|
|
color: {{ 'white' if score <= 50 else 'black' }};"
|
|
{% endif %}>
|
|
{% if special_value and special_value in heatmap_grading_elements.scale_colors %}
|
|
{{ special_value }}
|
|
{% elif score is not none %}
|
|
{{ "%.1f"|format(score) }}%
|
|
{% else %}
|
|
<span class="text-gray-400">-</span>
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-4 space-y-2">
|
|
<div class="flex justify-center">
|
|
<div class="flex items-center space-x-4 text-xs">
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 bg-red-500 mr-1"></div>
|
|
<span>0-25%</span>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 bg-orange-500 mr-1"></div>
|
|
<span>26-50%</span>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 bg-yellow-500 mr-1"></div>
|
|
<span>51-75%</span>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 bg-green-500 mr-1"></div>
|
|
<span>76-100%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-center">
|
|
<span class="text-xs text-gray-600">Les valeurs spéciales (., a, d, etc.) apparaissent avec leurs couleurs configurées</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Tableau par domaines -->
|
|
{% if heatmap_domains %}
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h2 class="text-lg font-medium text-gray-900">Performance par domaine</h2>
|
|
<p class="text-sm text-gray-600 mt-1">Scores moyens de chaque élève par domaine (%) - Cliquez sur les en-têtes pour trier</p>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full sortable-table" id="domainsTable">
|
|
<thead>
|
|
<tr class="bg-gray-50">
|
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gray-50 sortable cursor-pointer hover:bg-gray-100" onclick="sortTable('domainsTable', 0)">
|
|
Élève
|
|
<span class="sort-arrow ml-1">⇅</span>
|
|
</th>
|
|
{% for domain in heatmap_domains.domains %}
|
|
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase tracking-wider min-w-24 sortable cursor-pointer hover:bg-gray-100"
|
|
style="color: {{ heatmap_domains.colors[domain] }}"
|
|
onclick="sortTable('domainsTable', {{ loop.index }})">
|
|
{{ domain }}
|
|
<span class="sort-arrow ml-1">⇅</span>
|
|
</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for student_index in range(heatmap_domains.students|length) %}
|
|
<tr class="{% if loop.index0 % 2 == 0 %}bg-white{% else %}bg-gray-50{% endif %}">
|
|
<td class="px-3 py-2 text-sm font-medium text-gray-900 sticky left-0 {% if loop.index0 % 2 == 0 %}bg-white{% else %}bg-gray-50{% endif %}">
|
|
{{ heatmap_domains.students[student_index] }}
|
|
</td>
|
|
{% for domain_index in range(heatmap_domains.domains|length) %}
|
|
{% set score = heatmap_domains.scores[student_index][domain_index] %}
|
|
{% set special_value = heatmap_domains.special_values[student_index][domain_index] %}
|
|
<td class="px-3 py-2 text-center text-sm"
|
|
{% if special_value and special_value in heatmap_grading_elements.scale_colors %}
|
|
{# Utiliser la couleur de la valeur spéciale #}
|
|
style="background-color: {{ heatmap_grading_elements.scale_colors[special_value].color }};
|
|
color: {% if heatmap_grading_elements.scale_colors[special_value].color in ['#ef4444', '#f97316', '#3b82f6', '#8b5cf6'] %}white{% else %}black{% endif %};"
|
|
{% elif score is not none %}
|
|
{# Utiliser le gradient pour les scores normaux #}
|
|
style="background-color: {{ '#ef4444' if score <= 25 else ('#f97316' if score <= 50 else ('#eab308' if score <= 75 else '#22c55e')) }};
|
|
color: {{ 'white' if score <= 50 else 'black' }};"
|
|
{% endif %}>
|
|
{% if special_value and special_value in heatmap_grading_elements.scale_colors %}
|
|
{{ special_value }}
|
|
{% elif score is not none %}
|
|
{{ "%.1f"|format(score) }}%
|
|
{% else %}
|
|
<span class="text-gray-400">-</span>
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-4 space-y-2">
|
|
<div class="flex justify-center">
|
|
<div class="flex items-center space-x-4 text-xs">
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 bg-red-500 mr-1"></div>
|
|
<span>0-25%</span>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 bg-orange-500 mr-1"></div>
|
|
<span>26-50%</span>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 bg-yellow-500 mr-1"></div>
|
|
<span>51-75%</span>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 bg-green-500 mr-1"></div>
|
|
<span>76-100%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-center">
|
|
<span class="text-xs text-gray-600">Les valeurs spéciales (., a, d, etc.) apparaissent avec leurs couleurs configurées</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Tableau par éléments de notation -->
|
|
{% if heatmap_grading_elements %}
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h2 class="text-lg font-medium text-gray-900">Performance par élément de notation</h2>
|
|
<p class="text-sm text-gray-600 mt-1">Détail des scores de chaque élève sur chaque question/compétence - Cliquez sur les en-têtes pour trier</p>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full text-xs sortable-table" id="elementsTable">
|
|
<thead>
|
|
<tr class="bg-gray-50">
|
|
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gray-50 min-w-32 sortable cursor-pointer hover:bg-gray-100" onclick="sortTable('elementsTable', 0)">
|
|
Élève
|
|
<span class="sort-arrow ml-1">⇅</span>
|
|
</th>
|
|
{% for element in heatmap_grading_elements.elements %}
|
|
<th class="px-1 py-2 text-center text-xs font-medium text-gray-500 uppercase tracking-wider min-w-20 sortable cursor-pointer hover:bg-gray-100"
|
|
style="color: {{ heatmap_grading_elements.colors[element] }}"
|
|
title="{{ element }}"
|
|
onclick="sortTable('elementsTable', {{ loop.index }})">
|
|
{{ element|truncate(12, True) }}
|
|
<span class="sort-arrow ml-1">⇅</span>
|
|
</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for student_index in range(heatmap_grading_elements.students|length) %}
|
|
<tr class="{% if loop.index0 % 2 == 0 %}bg-white{% else %}bg-gray-50{% endif %}">
|
|
<td class="px-2 py-2 text-xs font-medium text-gray-900 sticky left-0 {% if loop.index0 % 2 == 0 %}bg-white{% else %}bg-gray-50{% endif %}">
|
|
{{ heatmap_grading_elements.students[student_index] }}
|
|
</td>
|
|
{% for element_index in range(heatmap_grading_elements.elements|length) %}
|
|
{% set grade_data = heatmap_grading_elements.detailed_scores[student_index][element_index] %}
|
|
<td class="px-1 py-2 text-center text-xs"
|
|
{% if grade_data %}
|
|
{% if grade_data.value in heatmap_grading_elements.scale_colors %}
|
|
style="background-color: {{ heatmap_grading_elements.scale_colors[grade_data.value].color }};
|
|
color: {% if heatmap_grading_elements.scale_colors[grade_data.value].color in ['#ef4444', '#f97316', '#3b82f6', '#8b5cf6'] %}white{% else %}black{% endif %};"
|
|
{% elif grade_data.grading_type == 'notes' %}
|
|
{% set percentage = (grade_data.value|float / grade_data.max_points) * 100 %}
|
|
style="background-color: {{ '#ef4444' if percentage <= 25 else ('#f97316' if percentage <= 50 else ('#eab308' if percentage <= 75 else '#22c55e')) }};
|
|
color: {{ 'white' if percentage <= 50 else 'black' }};"
|
|
{% endif %}
|
|
{% endif %}>
|
|
{% if grade_data %}
|
|
{{ grade_data.value }}
|
|
{% else %}
|
|
<span class="text-gray-400">-</span>
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-4 space-y-2">
|
|
<div class="flex justify-center">
|
|
<div class="text-sm font-medium text-gray-700 mb-2">Échelle de notation :</div>
|
|
</div>
|
|
<div class="flex justify-center flex-wrap gap-4 text-xs">
|
|
{% for value, config in heatmap_grading_elements.scale_colors.items() %}
|
|
<div class="flex items-center">
|
|
<div class="w-4 h-4 mr-1" style="background-color: {{ config.color }}"></div>
|
|
<span>{{ value }}: {{ config.label }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="flex justify-center mt-2">
|
|
<span class="text-xs text-gray-600">Notes numériques : dégradé rouge (faible) → vert (élevé)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Tableau des résultats -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h2 class="text-lg font-medium text-gray-900">Résultats par élève</h2>
|
|
<p class="text-sm text-gray-600 mt-1">Cliquez sur les en-têtes pour trier</p>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200 sortable-table" id="detailsTable">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sortable cursor-pointer hover:bg-gray-100" onclick="sortTable('detailsTable', 0)">
|
|
Élève
|
|
<span class="sort-arrow ml-1">⇅</span>
|
|
</th>
|
|
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider sortable cursor-pointer hover:bg-gray-100" onclick="sortTable('detailsTable', 1)">
|
|
Note totale
|
|
<span class="sort-arrow ml-1">⇅</span>
|
|
</th>
|
|
{% for exercise in assessment.exercises|sort(attribute='order') %}
|
|
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider sortable cursor-pointer hover:bg-gray-100" onclick="sortTable('detailsTable', {{ loop.index + 1 }})">
|
|
{{ exercise.title }}
|
|
<span class="sort-arrow ml-1">⇅</span>
|
|
</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
{% for student_data in students_scores %}
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-medium text-gray-900">
|
|
{{ student_data.student.last_name }} {{ student_data.student.first_name }}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-center">
|
|
{% if student_data.total_special_display %}
|
|
<div class="text-sm font-bold px-2 py-1 rounded text-center inline-block"
|
|
style="background-color: {{ heatmap_grading_elements.scale_colors[student_data.total_special_display].color }};
|
|
color: {% if heatmap_grading_elements.scale_colors[student_data.total_special_display].color in ['#ef4444', '#f97316', '#3b82f6', '#8b5cf6'] %}white{% else %}black{% endif %};">
|
|
{{ student_data.total_special_display }}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-sm font-bold text-gray-900">
|
|
{{ "%.1f"|format(student_data.total_score) }} / {{ "%.1f"|format(student_data.total_max_points) }}
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
{% for exercise in assessment.exercises|sort(attribute='order') %}
|
|
<td class="px-6 py-4 whitespace-nowrap text-center">
|
|
{% if exercise.id in student_data.exercises %}
|
|
{% set exercise_data = student_data.exercises[exercise.id] %}
|
|
{% if exercise_data.special_display %}
|
|
<div class="text-sm px-2 py-1 rounded text-center inline-block"
|
|
style="background-color: {{ heatmap_grading_elements.scale_colors[exercise_data.special_display].color }};
|
|
color: {% if heatmap_grading_elements.scale_colors[exercise_data.special_display].color in ['#ef4444', '#f97316', '#3b82f6', '#8b5cf6'] %}white{% else %}black{% endif %};">
|
|
{{ exercise_data.special_display }}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-sm text-gray-900">
|
|
{{ "%.1f"|format(exercise_data.score) }} / {{ "%.1f"|format(exercise_data.max_points) }}
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="text-sm text-gray-900">-</div>
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chart.js -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const ctx = document.getElementById('scoresChart').getContext('2d');
|
|
const scores = {{ scores_json | tojson }};
|
|
|
|
// Récupérer les données des élèves avec leurs noms et scores
|
|
const studentsData = [
|
|
{% for student_data in students_scores %}
|
|
{
|
|
name: "{{ student_data.student.last_name }} {{ student_data.student.first_name }}",
|
|
score: {{ student_data.total_score }}
|
|
}{% if not loop.last %},{% endif %}
|
|
{% endfor %}
|
|
];
|
|
|
|
// Créer les intervalles pour l'histogramme
|
|
const totalMaxPoints = {{ total_max_points }};
|
|
const minScore = 0;
|
|
const maxScore = totalMaxPoints;
|
|
const binWidth = 1; // Groupes de 1 point
|
|
const numBins = Math.ceil(maxScore - minScore); // Nombre de bins basé sur le total des points
|
|
|
|
const bins = [];
|
|
const labels = [];
|
|
const studentsInBins = []; // Nouveau: stocker les élèves pour chaque bin
|
|
|
|
for (let i = 0; i < numBins; i++) {
|
|
const binStart = minScore + i * binWidth;
|
|
const binEnd = minScore + (i + 1) * binWidth;
|
|
bins.push(0);
|
|
labels.push(`${binStart.toFixed(0)} - ${binEnd.toFixed(0)}`);
|
|
studentsInBins.push([]); // Initialiser la liste d'élèves pour ce bin
|
|
}
|
|
|
|
// Compter les scores dans chaque intervalle et stocker les noms des élèves
|
|
studentsData.forEach(student => {
|
|
let binIndex = Math.floor((student.score - minScore) / binWidth);
|
|
if (binIndex >= numBins) binIndex = numBins - 1; // Pour le score maximum
|
|
if (binIndex >= 0) {
|
|
bins[binIndex]++;
|
|
studentsInBins[binIndex].push(`${student.name} (${student.score.toFixed(1)})`);
|
|
}
|
|
});
|
|
|
|
new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Nombre d\'élèves',
|
|
data: bins,
|
|
backgroundColor: 'rgba(59, 130, 246, 0.5)',
|
|
borderColor: 'rgba(59, 130, 246, 1)',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Distribution des notes'
|
|
},
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
title: function(tooltipItems) {
|
|
const binIndex = tooltipItems[0].dataIndex;
|
|
return labels[binIndex];
|
|
},
|
|
label: function(tooltipItem) {
|
|
const binIndex = tooltipItem.dataIndex;
|
|
const count = bins[binIndex];
|
|
const students = studentsInBins[binIndex];
|
|
|
|
if (count === 0) {
|
|
return 'Aucun élève';
|
|
} else if (count === 1) {
|
|
return '1 élève : ' + students[0];
|
|
} else {
|
|
return `${count} élèves : ${students.join(', ')}`;
|
|
}
|
|
}
|
|
},
|
|
maxWidth: 400,
|
|
bodyFont: {
|
|
size: 12
|
|
},
|
|
titleFont: {
|
|
size: 13,
|
|
weight: 'bold'
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
stepSize: 1
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: 'Nombre d\'élèves'
|
|
}
|
|
},
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Intervalles de notes'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// === FONCTIONNALITÉ D'ENVOI DE BILANS ===
|
|
|
|
let eligibleStudents = [];
|
|
let selectedStudents = [];
|
|
|
|
// Charger les élèves éligibles au chargement de la page
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadEligibleStudents();
|
|
});
|
|
|
|
function loadEligibleStudents() {
|
|
fetch(`/assessments/{{ assessment.id }}/eligible-students`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
eligibleStudents = data.students;
|
|
updateSendReportsButton(data.with_email_count, data.total_count);
|
|
} else {
|
|
console.error('Erreur lors du chargement des élèves:', data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Erreur réseau:', error);
|
|
});
|
|
}
|
|
|
|
function updateSendReportsButton(withEmailCount, totalCount) {
|
|
const btn = document.getElementById('sendReportsBtn');
|
|
if (withEmailCount === 0) {
|
|
btn.disabled = true;
|
|
btn.classList.add('opacity-50', 'cursor-not-allowed');
|
|
btn.classList.remove('hover:bg-blue-700');
|
|
btn.innerHTML = `
|
|
<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="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
Aucune adresse email configurée
|
|
`;
|
|
} else {
|
|
btn.innerHTML = `
|
|
<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="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
</svg>
|
|
📧 Envoyer les bilans (${withEmailCount}/${totalCount} avec email)
|
|
`;
|
|
}
|
|
}
|
|
|
|
function openSendReportsModal() {
|
|
if (eligibleStudents.length === 0) {
|
|
alert('Aucun élève éligible trouvé');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('sendReportsModal').classList.remove('hidden');
|
|
populateStudentsList();
|
|
}
|
|
|
|
function closeSendReportsModal() {
|
|
document.getElementById('sendReportsModal').classList.add('hidden');
|
|
selectedStudents = [];
|
|
}
|
|
|
|
function populateStudentsList() {
|
|
const container = document.getElementById('studentsList');
|
|
const studentsWithEmail = eligibleStudents.filter(s => s.has_email);
|
|
|
|
if (studentsWithEmail.length === 0) {
|
|
container.innerHTML = '<p class="text-gray-500 text-center py-4">Aucun élève avec adresse email configurée</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = studentsWithEmail.map(student => `
|
|
<label class="flex items-center p-3 hover:bg-gray-50 rounded-lg cursor-pointer">
|
|
<input type="checkbox"
|
|
value="${student.id}"
|
|
onchange="updateSelectedStudents()"
|
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-3">
|
|
<div class="flex-1">
|
|
<div class="font-medium text-gray-900">${student.full_name}</div>
|
|
<div class="text-sm text-gray-500">${student.email}</div>
|
|
</div>
|
|
</label>
|
|
`).join('');
|
|
|
|
// Sélectionner tous par défaut
|
|
selectAllStudents();
|
|
}
|
|
|
|
function selectAllStudents() {
|
|
const checkboxes = document.querySelectorAll('#studentsList input[type="checkbox"]');
|
|
checkboxes.forEach(cb => cb.checked = true);
|
|
updateSelectedStudents();
|
|
}
|
|
|
|
function unselectAllStudents() {
|
|
const checkboxes = document.querySelectorAll('#studentsList input[type="checkbox"]');
|
|
checkboxes.forEach(cb => cb.checked = false);
|
|
updateSelectedStudents();
|
|
}
|
|
|
|
function updateSelectedStudents() {
|
|
const checkboxes = document.querySelectorAll('#studentsList input[type="checkbox"]:checked');
|
|
selectedStudents = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
|
|
|
const count = selectedStudents.length;
|
|
document.getElementById('selectedCount').textContent = count;
|
|
document.getElementById('sendReportsSubmitBtn').disabled = count === 0;
|
|
}
|
|
|
|
function sendReports() {
|
|
if (selectedStudents.length === 0) {
|
|
alert('Veuillez sélectionner au moins un élève');
|
|
return;
|
|
}
|
|
|
|
const customMessage = document.getElementById('customMessage').value.trim();
|
|
const submitBtn = document.getElementById('sendReportsSubmitBtn');
|
|
const originalText = submitBtn.innerHTML;
|
|
|
|
// Désactiver le bouton et afficher le loading
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = `
|
|
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" 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>
|
|
Envoi en cours...
|
|
`;
|
|
|
|
// Envoyer la requête
|
|
fetch(`/assessments/{{ assessment.id }}/send-reports`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
student_ids: selectedStudents,
|
|
custom_message: customMessage
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Restaurer le bouton
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = originalText;
|
|
|
|
if (data.success) {
|
|
// Afficher le résultat
|
|
showSendResult(data);
|
|
// Fermer la modal après un délai
|
|
setTimeout(() => {
|
|
closeSendReportsModal();
|
|
}, 3000);
|
|
} else {
|
|
alert(`Erreur lors de l'envoi: ${data.error}`);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
// Restaurer le bouton en cas d'erreur
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = originalText;
|
|
console.error('Erreur:', error);
|
|
alert('Erreur réseau lors de l\'envoi des bilans');
|
|
});
|
|
}
|
|
|
|
function showSendResult(result) {
|
|
const resultDiv = document.getElementById('sendResult');
|
|
|
|
let content = `<div class="mb-4">
|
|
<h3 class="text-lg font-medium ${result.success ? 'text-green-600' : 'text-red-600'} mb-2">
|
|
${result.message}
|
|
</h3>
|
|
</div>`;
|
|
|
|
if (result.sent_count > 0) {
|
|
content += `<div class="mb-3">
|
|
<p class="text-green-600">✅ ${result.sent_count} bilan(s) envoyé(s) avec succès</p>
|
|
</div>`;
|
|
}
|
|
|
|
if (result.error_count > 0) {
|
|
content += `<div class="mb-3">
|
|
<p class="text-red-600 font-medium">❌ ${result.error_count} erreur(s):</p>
|
|
<ul class="text-sm text-red-600 mt-2 max-h-32 overflow-y-auto">`;
|
|
|
|
result.errors.forEach(error => {
|
|
content += `<li class="py-1">• ${error}</li>`;
|
|
});
|
|
|
|
content += `</ul></div>`;
|
|
}
|
|
|
|
resultDiv.innerHTML = content;
|
|
resultDiv.classList.remove('hidden');
|
|
|
|
// Cacher les autres sections
|
|
document.getElementById('studentsSelection').classList.add('hidden');
|
|
document.getElementById('modalActions').classList.add('hidden');
|
|
}
|
|
|
|
// Fermer la modal en cliquant à côté
|
|
document.addEventListener('click', function(e) {
|
|
const modal = document.getElementById('sendReportsModal');
|
|
if (e.target === modal) {
|
|
closeSendReportsModal();
|
|
}
|
|
});
|
|
|
|
// === FONCTIONNALITÉ DE TRI DES TABLEAUX ===
|
|
|
|
function sortTable(tableId, columnIndex) {
|
|
const table = document.getElementById(tableId);
|
|
const tbody = table.querySelector('tbody');
|
|
const rows = Array.from(tbody.rows);
|
|
const isNumeric = checkIfNumericColumn(rows, columnIndex);
|
|
|
|
// Déterminer la direction du tri
|
|
const currentDirection = table.dataset.sortDirection || 'none';
|
|
const newDirection = currentDirection === 'asc' ? 'desc' : 'asc';
|
|
table.dataset.sortDirection = newDirection;
|
|
table.dataset.sortColumn = columnIndex;
|
|
|
|
// Tri des lignes
|
|
rows.sort((a, b) => {
|
|
let aVal = getCellValue(a, columnIndex);
|
|
let bVal = getCellValue(b, columnIndex);
|
|
|
|
if (isNumeric) {
|
|
aVal = parseFloat(aVal) || 0;
|
|
bVal = parseFloat(bVal) || 0;
|
|
} else {
|
|
aVal = aVal.toLowerCase();
|
|
bVal = bVal.toLowerCase();
|
|
}
|
|
|
|
if (aVal < bVal) return newDirection === 'asc' ? -1 : 1;
|
|
if (aVal > bVal) return newDirection === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
// Réorganiser les lignes dans le tableau
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
|
|
// Mettre à jour les indicateurs visuels
|
|
updateSortIndicators(table, columnIndex, newDirection);
|
|
}
|
|
|
|
function getCellValue(row, columnIndex) {
|
|
const cell = row.cells[columnIndex];
|
|
if (!cell) return '';
|
|
|
|
// Pour les cellules avec des couleurs de fond (valeurs spéciales), extraire le texte
|
|
let text = cell.textContent.trim();
|
|
|
|
// Nettoyer les valeurs numériques (enlever %, /X, etc.)
|
|
text = text.replace(/[%\/]/g, '').split(/[\s\/]/)[0];
|
|
|
|
return text;
|
|
}
|
|
|
|
function checkIfNumericColumn(rows, columnIndex) {
|
|
if (rows.length === 0) return false;
|
|
|
|
// Vérifier les 5 premières lignes pour déterminer si c'est numérique
|
|
const sampleSize = Math.min(5, rows.length);
|
|
let numericCount = 0;
|
|
|
|
for (let i = 0; i < sampleSize; i++) {
|
|
const value = getCellValue(rows[i], columnIndex);
|
|
if (value && !isNaN(parseFloat(value))) {
|
|
numericCount++;
|
|
}
|
|
}
|
|
|
|
return numericCount >= sampleSize / 2;
|
|
}
|
|
|
|
function updateSortIndicators(table, columnIndex, direction) {
|
|
// Réinitialiser tous les indicateurs
|
|
const headers = table.querySelectorAll('th.sortable');
|
|
headers.forEach(header => {
|
|
const arrow = header.querySelector('.sort-arrow');
|
|
if (arrow) {
|
|
arrow.textContent = '⇅';
|
|
}
|
|
});
|
|
|
|
// Mettre à jour l'indicateur de la colonne triée
|
|
const currentHeader = headers[columnIndex];
|
|
if (currentHeader) {
|
|
const arrow = currentHeader.querySelector('.sort-arrow');
|
|
if (arrow) {
|
|
arrow.textContent = direction === 'asc' ? '▲' : '▼';
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- Modal d'envoi des bilans -->
|
|
<div id="sendReportsModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
|
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-2xl shadow-lg rounded-md bg-white">
|
|
<div class="mt-3">
|
|
<!-- En-tête -->
|
|
<div class="flex justify-between items-center pb-4 border-b">
|
|
<h3 class="text-lg font-medium text-gray-900">📧 Envoyer les bilans par email</h3>
|
|
<button onclick="closeSendReportsModal()" class="text-gray-400 hover:text-gray-600">
|
|
<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"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Sélection des élèves -->
|
|
<div id="studentsSelection" class="py-4">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h4 class="font-medium text-gray-900">Sélectionner les élèves</h4>
|
|
<div class="flex space-x-2">
|
|
<button onclick="selectAllStudents()"
|
|
class="text-sm text-blue-600 hover:text-blue-800">Tout sélectionner</button>
|
|
<span class="text-gray-300">|</span>
|
|
<button onclick="unselectAllStudents()"
|
|
class="text-sm text-blue-600 hover:text-blue-800">Tout désélectionner</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="studentsList" class="max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-2 space-y-1">
|
|
<!-- Les élèves seront ajoutés ici par JavaScript -->
|
|
</div>
|
|
|
|
<p class="text-sm text-gray-600 mt-2">
|
|
<span id="selectedCount">0</span> élève(s) sélectionné(s)
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Message personnalisé -->
|
|
<div class="py-4 border-t">
|
|
<label for="customMessage" class="block text-sm font-medium text-gray-700 mb-2">
|
|
Message personnalisé (optionnel)
|
|
</label>
|
|
<textarea id="customMessage"
|
|
rows="3"
|
|
placeholder="Message du professeur à ajouter dans les bilans..."
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
|
</div>
|
|
|
|
<!-- Résultat de l'envoi -->
|
|
<div id="sendResult" class="hidden py-4 border-t">
|
|
<!-- Le résultat sera affiché ici -->
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div id="modalActions" class="flex justify-end space-x-3 pt-4 border-t">
|
|
<button onclick="closeSendReportsModal()"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
|
Annuler
|
|
</button>
|
|
<button id="sendReportsSubmitBtn" onclick="sendReports()"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
disabled>
|
|
📧 Envoyer les bilans
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %} |