Feat: add class page

This commit is contained in:
2025-08-09 11:32:36 +02:00
parent 17995f913e
commit 0e87a457af
18 changed files with 6974 additions and 14 deletions

View File

@@ -6,6 +6,7 @@
<title>{% block title %}Gestion Scolaire{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/design-system.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/class-dashboard.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
{% block head %}{% endblock %}
<script>
@@ -82,7 +83,7 @@
</div>
</nav>
<main class="max-w-7xl mx-auto px-4 py-8" role="main">
<main class="{% block main_class %}max-w-7xl mx-auto px-4 py-8{% endblock %}" role="main">
<!-- Messages flash améliorés pour l'accessibilité -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}

View File

@@ -0,0 +1,463 @@
{% extends "base.html" %}
{% from 'components/common/macros.html' import hero_section, action_card, progress_indicator, stat_card %}
{% block title %}{{ class_group.name }} - Dashboard{% endblock %}
{# Override le style du main container pour éviter le clipping des hover effects #}
{% block main_class %}w-full px-8 py-8 bg-gray-100{% endblock %}
{% block content %}
<div class="class-dashboard" data-class-dashboard data-class-id="{{ class_group.id }}">
<!-- Live region pour les annonces d'accessibilité -->
<div aria-live="polite" aria-atomic="true" class="live-region" data-live-region></div>
<!-- Loading overlay -->
<div class="loading-overlay hidden" data-loading-overlay>
<div class="loading-spinner"></div>
</div>
<!-- Error container -->
<div class="hidden" data-error-container></div>
<div class="max-w-7xl mx-auto">
<div class="space-y-8" style="overflow: visible; padding: 8px; margin: -8px;">
{# 1. Hero Section avec Contexte de Classe #}
{% set meta_info = [
{
'icon': '<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/></svg>',
'text': class_group.students|length ~ ' élèves'
},
{
'icon': '<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/></svg>',
'text': 'Trimestre ' ~ (selected_trimester or 'Global')
},
{
'icon': '<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><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>',
'text': (class_group.assessments|selectattr('grading_progress.status', 'ne', 'completed')|list|length) ~ ' évaluations en attente'
}
] %}
{{ hero_section(
title=class_group.name ~ " 🏫",
subtitle="Dashboard de gestion de classe",
meta_info=meta_info,
gradient_class="from-indigo-600 to-purple-600"
) }}
{# Breadcrumb de retour #}
<div class="flex items-center text-sm text-gray-600">
<a href="{{ url_for('classes') }}" class="hover:text-blue-600 transition-colors flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
Toutes les classes
</a>
</div>
{# 2. Actions Principales avec Code Couleur Prioritaire #}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{# Action ROUGE - Corrections en attente (priorité max) #}
{% set pending_assessments = class_group.assessments|selectattr('grading_progress.status', 'ne', 'completed')|list %}
{% if pending_assessments %}
<a href="{{ url_for('grading.assessment_grading', assessment_id=pending_assessments[0].id) }}"
class="group bg-gradient-to-r from-red-500 to-red-600 text-white rounded-xl p-6 hover:from-red-600 hover:to-red-700 transition-all duration-300 transform hover:scale-[1.02] shadow-lg hover:shadow-xl">
<div class="flex items-center">
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center mr-4 group-hover:bg-white/30 transition-colors">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<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>
<h3 class="text-lg font-bold mb-1">Terminer corrections</h3>
<p class="text-sm opacity-90">{{ pending_assessments|length }} évaluation(s) en attente</p>
</div>
</div>
</a>
{% endif %}
{# Action VERTE - Nouvelle évaluation #}
<a href="{{ url_for('assessments.new') }}?class_id={{ class_group.id }}"
class="group bg-gradient-to-r from-green-500 to-green-600 text-white rounded-xl p-6 hover:from-green-600 hover:to-green-700 transition-all duration-300 transform hover:scale-[1.02] shadow-lg hover:shadow-xl">
<div class="flex items-center">
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center mr-4 group-hover:bg-white/30 transition-colors">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
</svg>
</div>
<div>
<h3 class="text-lg font-bold mb-1">Nouvelle évaluation</h3>
<p class="text-sm opacity-90">Créer pour {{ class_group.name }}</p>
</div>
</div>
</a>
{# Action BLEUE - Gérer les élèves #}
<a href="{{ url_for('students') }}?class_id={{ class_group.id }}"
class="group bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl p-6 hover:from-blue-600 hover:to-blue-700 transition-all duration-300 transform hover:scale-[1.02] shadow-lg hover:shadow-xl">
<div class="flex items-center">
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center mr-4 group-hover:bg-white/30 transition-colors">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
</svg>
</div>
<div>
<h3 class="text-lg font-bold mb-1">Gérer élèves</h3>
<p class="text-sm opacity-90">{{ class_group.students|length }} élèves inscrits</p>
</div>
</div>
</a>
{# Action ORANGE - Voir statistiques #}
<button class="group bg-gradient-to-r from-orange-500 to-orange-600 text-white rounded-xl p-6 hover:from-orange-600 hover:to-orange-700 transition-all duration-300 transform hover:scale-[1.02] shadow-lg hover:shadow-xl">
<div class="flex items-center">
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center mr-4 group-hover:bg-white/30 transition-colors">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/>
</svg>
</div>
<div>
<h3 class="text-lg font-bold mb-1">Voir statistiques</h3>
<p class="text-sm opacity-90">Résultats et analyses</p>
</div>
</div>
</button>
</div>
{# 3. Dashboard Statistiques par Trimestre #}
<div class="bg-white shadow rounded-xl">
{# Header de la section #}
<div class="px-6 py-5 border-b border-gray-200 flex justify-between items-center">
<div class="flex items-center">
<h2 class="text-xl font-bold text-gray-900">Statistiques par trimestre</h2>
<span class="ml-3 bg-blue-100 text-blue-800 text-sm px-3 py-1 rounded-full font-medium">
Données temps réel
</span>
</div>
</div>
{# Navigation tabs des trimestres #}
<div class="px-6 pt-6">
<nav class="trimester-tabs" role="tablist" aria-label="Navigation par trimestre">
<button data-trimester-tab="1"
class="trimester-tab {% if selected_trimester == 1 %}active{% endif %}"
role="tab"
aria-selected="{% if selected_trimester == 1 %}true{% else %}false{% endif %}">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-gradient-to-r from-blue-500 to-blue-600 mr-2"></div>
Trimestre 1
</div>
</button>
<button data-trimester-tab="2"
class="trimester-tab {% if selected_trimester == 2 %}active{% endif %}"
role="tab"
aria-selected="{% if selected_trimester == 2 %}true{% else %}false{% endif %}">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-gradient-to-r from-green-500 to-green-600 mr-2"></div>
Trimestre 2
</div>
</button>
<button data-trimester-tab="3"
class="trimester-tab {% if selected_trimester == 3 %}active{% endif %}"
role="tab"
aria-selected="{% if selected_trimester == 3 %}true{% else %}false{% endif %}">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-gradient-to-r from-orange-500 to-orange-600 mr-2"></div>
Trimestre 3
</div>
</button>
<button data-trimester-tab="global"
class="trimester-tab {% if not selected_trimester %}active{% endif %}"
role="tab"
aria-selected="{% if not selected_trimester %}true{% else %}false{% endif %}">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-gradient-to-r from-purple-500 to-purple-600 mr-2"></div>
Global
</div>
</button>
</nav>
</div>
{# 4 Cards de statistiques en grid responsive #}
<div class="px-6 pb-6" data-stats-content>
<div class="stats-grid" data-stats-cards>
{# Card 1 - Domaines #}
<div class="bg-white rounded-xl shadow-lg p-6" data-stats-card="domains">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-green-900 flex items-center">
<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2z" clip-rule="evenodd"/>
</svg>
Domaines
</h3>
<div class="w-10 h-10 bg-green-500 rounded-xl flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2z" clip-rule="evenodd"/>
</svg>
</div>
</div>
<div class="space-y-4">
<div class="text-center">
<div class="text-3xl font-bold text-green-900" data-domains-count>0</div>
<div class="text-sm text-green-700">domaines évalués</div>
</div>
<div class="space-y-2" data-domains-list>
<!-- Dynamic content populated by JavaScript -->
</div>
</div>
</div>
{# Card 2 - Compétences #}
<div class="bg-white rounded-xl shadow-lg p-6" data-stats-card="competences">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-purple-900 flex items-center">
<svg class="w-5 h-5 text-purple-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
Compétences
</h3>
<div class="w-10 h-10 bg-purple-500 rounded-xl flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</div>
</div>
<div class="space-y-4">
<div class="text-center">
<div class="text-3xl font-bold text-purple-900" data-competences-count>0</div>
<div class="text-sm text-purple-700">compétences évaluées</div>
</div>
<div class="space-y-2" data-competences-list>
<!-- Dynamic content populated by JavaScript -->
</div>
</div>
</div>
{# Card 3 - Résultats #}
<div class="bg-white rounded-xl shadow-lg p-6" data-stats-card="results">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-orange-900 flex items-center">
<svg class="w-5 h-5 text-orange-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/>
</svg>
Résultats
</h3>
<div class="w-10 h-10 bg-orange-500 rounded-xl flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/>
</svg>
</div>
</div>
<div class="space-y-4">
<div class="text-center">
<div class="text-3xl font-bold text-orange-900" data-result="mean">0.0</div>
<div class="text-sm text-orange-700">moyenne générale</div>
<div class="text-xs text-orange-600 mt-1" data-result="assessments_count">0 évaluation(s)</div>
</div>
<div class="grid grid-cols-2 gap-3 text-sm">
<div class="bg-orange-50 rounded-lg p-3 border border-orange-100">
<div class="flex justify-between items-center">
<span class="text-orange-800 font-medium">Min:</span>
<span class="text-orange-900 font-bold" data-result="min">0.0</span>
</div>
</div>
<div class="bg-orange-50 rounded-lg p-3 border border-orange-100">
<div class="flex justify-between items-center">
<span class="text-orange-800 font-medium">Max:</span>
<span class="text-orange-900 font-bold" data-result="max">0.0</span>
</div>
</div>
<div class="bg-orange-50 rounded-lg p-3 border border-orange-100">
<div class="flex justify-between items-center">
<span class="text-orange-800 font-medium">Médiane:</span>
<span class="text-orange-900 font-bold" data-result="median">0.0</span>
</div>
</div>
<div class="bg-orange-50 rounded-lg p-3 border border-orange-100">
<div class="flex justify-between items-center">
<span class="text-orange-800 font-medium">Écart-type:</span>
<span class="text-orange-900 font-bold" data-result="std_dev">0.0</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{# 4. Tableau de Bord Évaluations #}
<div class="bg-white shadow rounded-xl">
<div class="px-6 py-5 border-b border-gray-200 flex items-center justify-between">
<div class="flex items-center">
<h2 class="text-xl font-bold text-gray-900">
Toutes les Évaluations
</h2>
<span class="ml-3 bg-blue-100 text-blue-800 text-sm px-3 py-1 rounded-full font-medium">
{{ class_group.assessments|length }} évaluations
</span>
</div>
</div>
<div class="px-6 py-6">
{% if class_group.assessments %}
<div class="space-y-4">
{% for assessment in class_group.assessments|sort(attribute='date', reverse=True) %}
<div class="flex items-center justify-between p-4 bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-md transition-shadow duration-300 group">
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="flex items-center space-x-4 flex-1 min-w-0">
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-bold text-sm">
{{ assessment.title[0].upper() }}
</div>
<div class="min-w-0 flex-1">
<h3 class="text-sm font-semibold text-gray-900 group-hover:text-blue-700 transition-colors truncate">{{ assessment.title }}</h3>
<div class="flex items-center text-xs text-gray-500 space-x-2">
<span class="flex items-center">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/>
</svg>
{{ assessment.date.strftime('%d/%m/%Y') }}
</span>
<span></span>
<span>Coeff. {{ assessment.coefficient }}</span>
<span></span>
<span>T{{ assessment.trimester }}</span>
</div>
</div>
</a>
<div class="flex items-center space-x-3 flex-shrink-0">
{# Indicateur de progression #}
{{ progress_indicator(assessment.grading_progress, clickable=True, assessment_id=assessment.id, compact=True) }}
{# Action directe selon statut #}
{% if assessment.grading_progress.status == 'completed' %}
<a href="{{ url_for('assessments.results', id=assessment.id) }}"
class="text-xs bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white px-3 py-1.5 rounded-lg transition-all duration-300 font-medium transform hover:scale-[1.02] shadow-lg hover:shadow-xl">
Voir résultats
</a>
{% else %}
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}"
class="text-xs bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white px-3 py-1.5 rounded-lg transition-all duration-300 font-medium transform hover:scale-[1.02] shadow-lg hover:shadow-xl">
Noter
</a>
{% endif %}
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-blue-600 group-hover:text-blue-800 opacity-70 group-hover:opacity-100 transition-all">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-12">
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/>
<path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 102 0V3a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 2a1 1 0 000 2h2a1 1 0 100-2H7z" clip-rule="evenodd"/>
</svg>
</div>
<h3 class="text-sm font-medium text-gray-900 mb-1">
Aucune évaluation pour cette classe
</h3>
<p class="text-sm text-gray-500 mb-4">Créez votre première évaluation pour cette classe</p>
<a href="{{ url_for('assessments.new') }}?class_id={{ class_group.id }}"
class="inline-flex items-center text-sm bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-4 py-2 rounded-lg transition-all duration-300 font-semibold shadow-lg hover:shadow-xl transform hover:scale-[1.02]">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
</svg>
Créer une évaluation
</a>
</div>
{% endif %}
</div>
</div>
{# 5. Aperçu Élèves (simplifié) #}
<div class="bg-white shadow rounded-xl">
<div class="px-6 py-5 border-b border-gray-200 flex items-center justify-between">
<div class="flex items-center">
<h2 class="text-xl font-bold text-gray-900">Élèves de la classe</h2>
<span class="ml-3 bg-green-100 text-green-800 text-sm px-3 py-1 rounded-full font-medium">
{{ class_group.students|length }} élèves
</span>
</div>
</div>
<div class="px-6 py-6">
{% if class_group.students %}
<div class="text-center">
<div class="flex items-center justify-center space-x-4 text-sm text-gray-600 mb-4">
<span class="flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
</svg>
Effectif complet : {{ class_group.students|length }} élèves
</span>
</div>
<a href="{{ url_for('students') }}?class_id={{ class_group.id }}"
class="inline-flex items-center text-sm bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-4 py-2 rounded-lg transition-all duration-300 font-semibold shadow-lg hover:shadow-xl transform hover:scale-[1.02]">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
</svg>
Voir tous les élèves
</a>
</div>
{% else %}
<div class="text-center py-8">
<div class="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
</svg>
</div>
<h3 class="text-sm font-medium text-gray-900 mb-2">Aucun élève inscrit</h3>
<p class="text-sm text-gray-500 mb-4">Ajoutez des élèves à cette classe pour commencer les évaluations</p>
<a href="{{ url_for('students') }}?class_id={{ class_group.id }}"
class="inline-flex items-center text-sm bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white px-4 py-2 rounded-lg transition-all duration-300 font-semibold shadow-lg hover:shadow-xl transform hover:scale-[1.02]">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
</svg>
Ajouter des élèves
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div> <!-- Fermeture max-w-7xl -->
</div> <!-- Fermeture class-dashboard -->
{% endblock %}
{% block head %}
<script src="{{ url_for('static', filename='js/ClassDashboard.js') }}"></script>
<style>
/* Fix pour éviter le clipping des hover effects sur cette page */
.class-dashboard {
overflow: visible !important;
}
.class-dashboard .grid {
overflow: visible !important;
}
.class-dashboard [class*="transform"][class*="hover:scale"] {
transform-origin: center;
}
/* Assurer que les conteneurs parents permettent l'overflow */
main {
overflow: visible !important;
}
/* Ajout d'un peu d'espace pour les animations */
.class-dashboard .grid > * {
margin: 4px;
position: relative;
z-index: 1;
}
.class-dashboard .grid > *:hover {
z-index: 10;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,261 @@
# 🧩 Composants Réutilisables pour Pages de Classe
Deux nouveaux composants modulaires ont été créés pour la nouvelle page de présentation de classe de Notytex.
## 📁 Structure des Fichiers
```
templates/components/class/
├── class_stats_card.html # Card de statistiques extensible (4 types)
├── trimester_nav.html # Navigation par trimestre avec couleurs thématiques
├── example_usage.html # Exemples d'utilisation complets
└── README_COMPONENTS.md # Documentation technique (ce fichier)
```
## 🎨 1. Composant `class_stats_card`
### **Utilisation**
```jinja2
{% from 'components/class/class_stats_card.html' import class_stats_card %}
{{ class_stats_card(
type='quantity',
data={"total": 8, "completed": 6, "in_progress": 2, "not_started": 0},
expanded=False
) }}
```
### **Types Supportés**
#### `type='quantity'` - Évaluations
```python
data = {
"total": 8, # Nombre total d'évaluations
"completed": 6, # Évaluations terminées (100%)
"in_progress": 2, # Évaluations en cours de correction
"not_started": 0 # Évaluations non commencées (0%)
}
```
#### `type='domains'` - Domaines Mathématiques
```python
data = [
{
"name": "Nombres et calculs",
"color": "#3B82F6", # Couleur hex du domaine
"total_points": 45.0, # Points total du domaine
"elements_count": 12 # Nombre d'éléments de notation
},
# ... autres domaines
]
```
#### `type='competences'` - Compétences Transversales
```python
data = [
{
"name": "Chercher",
"color": "#8B5CF6",
"total_points": 25.0,
"elements_count": 6
},
# ... autres compétences
]
```
#### `type='results'` - Analyse des Résultats
```python
data = {
"mean": 14.2, # Moyenne de classe
"median": 15.0, # Médiane
"distribution": [0,1,3,8,12,6,2,0], # Histogramme par tranches de 1 point
"min": 8.0, # Note minimum
"max": 20.0 # Note maximum
}
```
### **Fonctionnalités**
-**Progressive Disclosure** : Contenu collapsed/expanded
-**Code Couleur** : Chaque type a sa couleur thématique
-**Actions Contextuelles** : Liens vers analyses détaillées
-**Responsive Design** : Adaptation mobile/desktop
-**JavaScript Intégré** : Interactions d'expansion automatiques
## 🗓️ 2. Composant `trimester_nav`
### **Utilisation**
```jinja2
{% from 'components/class/trimester_nav.html' import trimester_nav %}
{{ trimester_nav(
class_id=42,
base_url=url_for('classes.class_detail', class_id=42),
selected_trimester=2,
badges={1: 4, 2: 3, 3: 2, 'global': 9}
) }}
```
### **Paramètres**
| Paramètre | Type | Requis | Description |
|-----------|------|---------|-------------|
| `class_id` | int | ✅ | ID de la classe (pour navigation) |
| `base_url` | str | ✅ | URL de base pour navigation |
| `selected_trimester` | int/None | ❌ | Trimestre actuel (1,2,3 ou None pour Global) |
| `badges` | dict | ❌ | Nombre d'évaluations par trimestre |
### **Couleurs Thématiques**
| Période | Couleurs | Symbolisme |
|---------|----------|------------|
| **Global** | `from-purple-500 to-purple-600` | 🟣 Synthétique (toute l'année) |
| **Trimestre 1** | `from-blue-500 to-blue-600` | 🔵 Hiver (Sept-Déc) |
| **Trimestre 2** | `from-green-500 to-green-600` | 🟢 Printemps (Jan-Mars) |
| **Trimestre 3** | `from-orange-500 to-orange-600` | 🟠 Été (Avr-Juil) |
### **Fonctionnalités**
-**Navigation URL** : Génération automatique des liens `?trimestre=X`
-**Badges Dynamiques** : Compteurs d'évaluations par période
-**Responsive** : Version desktop (horizontal) + mobile (scroll)
-**Tooltips** : Descriptions contextuelles au hover
-**Animation** : Transitions fluides entre les états
## 🎯 Intégration dans Notytex
### **Dans les Contrôleurs Flask**
```python
# routes/classes.py
@classes_bp.route('/classes/<int:class_id>')
def class_detail(class_id):
selected_trimester = request.args.get('trimestre', type=int)
# Données pour class_stats_card
quantity_data = {
"total": Assessment.query.filter_by(class_group_id=class_id).count(),
"completed": Assessment.query.filter_by(class_group_id=class_id).filter(
Assessment.grading_progress['status'] == 'completed'
).count(),
# ... autres calculs
}
# Données pour badges trimester_nav
badges = {}
for t in [1, 2, 3]:
badges[t] = Assessment.query.filter_by(
class_group_id=class_id,
trimester=t
).count()
badges['global'] = sum(badges.values())
return render_template('classes/detail.html',
quantity_data=quantity_data,
badges=badges,
selected_trimester=selected_trimester)
```
### **Dans les Templates**
```jinja2
{# classes/detail.html #}
{% extends "base.html" %}
{% from 'components/class/class_stats_card.html' import class_stats_card %}
{% from 'components/class/trimester_nav.html' import trimester_nav %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 py-8">
<!-- Navigation trimestre -->
{{ trimester_nav(
class_id=class_group.id,
base_url=url_for('classes.class_detail', class_id=class_group.id),
selected_trimester=selected_trimester,
badges=badges
) }}
<!-- Grille de statistiques -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6">
{{ class_stats_card('quantity', quantity_data) }}
{{ class_stats_card('domains', domains_data) }}
{{ class_stats_card('competences', competences_data) }}
{{ class_stats_card('results', results_data) }}
</div>
</div>
{% endblock %}
```
## 🔧 Architecture Technique
### **Design System Cohérent**
- **TailwindCSS** : Classes cohérentes avec l'existant (`rounded-xl`, `shadow-lg`, etc.)
- **Couleurs Sémantiques** : Rouge/Orange/Vert pour les états, couleurs thématiques pour les trimestres
- **Espacements Harmonieux** : Grid `gap-6`, padding `p-6`, marges cohérentes
- **Typography** : Hiérarchie claire avec `text-lg font-semibold`, `text-2xl font-bold`
### **Progressive Enhancement**
- **HTML First** : Fonctionnel même sans JavaScript
- **JavaScript Optionnel** : Améliore l'UX (animations, interactions)
- **Responsive** : Mobile-first avec breakpoints TailwindCSS
- **Accessibilité** : Attributs ARIA, focus management, keyboard navigation
### **Performance**
- **CSS Pur** : Pas de frameworks JS lourds (React, Vue)
- **Lazy Loading** : Contenu étendu masqué par défaut
- **Cache Friendly** : Templates statiques cachables
- **Minimal JavaScript** : Functions natives, pas de dépendances
## 🚀 Extensibilité Future
### **Nouveaux Types de Stats**
Pour ajouter un nouveau type à `class_stats_card` :
1. **Ajouter la configuration** dans `config` et `icons`
2. **Implémenter le rendu** dans le bloc `{% elif type == 'nouveau_type' %}`
3. **Définir la structure de données** attendue
4. **Tester avec des données d'exemple**
### **Nouvelles Périodes**
Pour ajouter des périodes à `trimester_nav` :
1. **Étendre le tableau `trimesters`** avec nouvelle configuration
2. **Ajouter les couleurs thématiques** appropriées
3. **Adapter la logique de navigation** dans JavaScript
4. **Mettre à jour les contrôleurs** Flask pour supporter la nouvelle période
## 🧪 Tests et Validation
### **Tests de Syntaxe** ✅
```bash
uv run python -c "import jinja2; env = jinja2.Environment(); template = env.from_string(open('templates/components/class/class_stats_card.html').read()); print('✅ Syntaxe OK')"
uv run python -c "import jinja2; env = jinja2.Environment(); template = env.from_string(open('templates/components/class/trimester_nav.html').read()); print('✅ Syntaxe OK')"
```
### **Tests d'Intégration** (À implémenter)
```python
# tests/test_class_components.py
def test_class_stats_card_rendering():
"""Test le rendu des 4 types de stats cards"""
pass
def test_trimester_nav_navigation():
"""Test la génération des URLs de navigation"""
pass
def test_responsive_behavior():
"""Test l'adaptation mobile/desktop"""
pass
```
---
**Ces composants transforment la page de classe en une interface moderne, interactive et parfaitement intégrée au design system Notytex** ! 🎓✨

View File

@@ -67,13 +67,14 @@
Modifier
</a>
<button onclick="confirmDeleteClass('{{ class.id }}', '{{ class.name }}', {{ class.students|length }}, {{ class.assessments|length }})"
class="flex-1 text-center text-gray-500 hover:text-red-700 text-xs font-medium transition-colors flex items-center justify-center py-2 hover:bg-red-50 rounded-lg">
<a href="{{ url_for('classes.dashboard', id=class.id) }}"
class="flex-1 text-center text-gray-600 hover:text-green-700 text-xs font-medium transition-colors flex items-center justify-center py-2 hover:bg-green-50 rounded-lg">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
</svg>
Supprimer
</button>
Voir
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,297 @@
{#
Macro : class_stats_card(type, data, expanded=False, trimester_colors={})
TYPES : 'quantity', 'domains', 'competences', 'results'
DATA STRUCTURE attendue :
- quantity: {"total": 8, "completed": 6, "in_progress": 2, "not_started": 0}
- domains: [{"name": "Calcul", "color": "#3B82F6", "total_points": 45.0, "elements_count": 12}]
- competences: [{"name": "Calculer", "color": "#8B5CF6", "total_points": 25.0, "elements_count": 6}]
- results: {"mean": 14.2, "median": 15.0, "distribution": [0,1,3,8,12,6,2,0], "min": 8.0, "max": 20.0}
#}
{% macro class_stats_card(type, data, expanded=False, trimester_colors={}) %}
{# Configuration des icônes par type #}
{% set icons = {
'quantity': '<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2v1a1 1 0 001 1h6a1 1 0 001-1V3a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h2a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>',
'domains': '<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"/></svg>',
'competences': '<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M6 6V5a3 3 0 013-3h2a3 3 0 013 3v1h2a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h2zm-1 5a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zm2-3a1 1 0 000 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>',
'results': '<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"/><path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"/></svg>'
} %}
{# Configuration des titres et couleurs par type #}
{% set config = {
'quantity': {
'title': 'Évaluations',
'color_class': 'blue',
'icon_color': 'text-blue-600',
'bg_color': 'bg-blue-50',
'border_color': 'border-blue-200'
},
'domains': {
'title': 'Domaines',
'color_class': 'purple',
'icon_color': 'text-purple-600',
'bg_color': 'bg-purple-50',
'border_color': 'border-purple-200'
},
'competences': {
'title': 'Compétences',
'color_class': 'indigo',
'icon_color': 'text-indigo-600',
'bg_color': 'bg-indigo-50',
'border_color': 'border-indigo-200'
},
'results': {
'title': 'Résultats',
'color_class': 'green',
'icon_color': 'text-green-600',
'bg_color': 'bg-green-50',
'border_color': 'border-green-200'
}
} %}
{% set card_config = config[type] %}
<div class="stats-card bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 border {{ card_config.border_color }}"
data-type="{{ type }}"
data-expanded="{{ 'true' if expanded else 'false' }}">
<!-- Header - Toujours visible -->
<div class="p-6">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-12 h-12 {{ card_config.bg_color }} rounded-xl flex items-center justify-center mr-4 {{ card_config.icon_color }}">
{{ icons[type]|safe }}
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-1">{{ card_config.title }}</h3>
{% if type == 'quantity' %}
<p class="text-2xl font-bold {{ card_config.icon_color }}">{{ data.total }} évaluations</p>
<p class="text-sm text-gray-500">{{ data.completed }} terminées</p>
{% elif type == 'domains' %}
<p class="text-2xl font-bold {{ card_config.icon_color }}">{{ data|length }} domaines</p>
<p class="text-sm text-gray-500">{{ data|sum(attribute='total_points')|round(1) }} points total</p>
{% elif type == 'competences' %}
<p class="text-2xl font-bold {{ card_config.icon_color }}">{{ data|length }} compétences</p>
<p class="text-sm text-gray-500">{{ data|sum(attribute='elements_count') }} éléments</p>
{% elif type == 'results' %}
<p class="text-2xl font-bold {{ card_config.icon_color }}">{{ data.mean|round(1) }}/20</p>
<p class="text-sm text-gray-500">Moyenne générale</p>
{% endif %}
</div>
</div>
<!-- Bouton d'expansion -->
<button type="button"
class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-2 rounded-lg hover:bg-gray-50"
onclick="toggleStatsCard(this)">
<svg class="w-5 h-5 transform transition-transform {{ 'rotate-180' if expanded else '' }}"
fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
</div>
</div>
<!-- Contenu étendu -->
<div class="expanded-content {{ 'hidden' if not expanded else '' }} border-t {{ card_config.border_color }}">
<div class="p-6">
{% if type == 'quantity' %}
<!-- Détails des évaluations par statut -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-green-50 rounded-lg p-4 border border-green-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-green-800">Terminées</p>
<p class="text-xl font-bold text-green-900">{{ data.completed }}</p>
</div>
<svg class="w-8 h-8 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
</div>
</div>
<div class="bg-orange-50 rounded-lg p-4 border border-orange-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-orange-800">En cours</p>
<p class="text-xl font-bold text-orange-900">{{ data.in_progress }}</p>
</div>
<svg class="w-8 h-8 text-orange-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM7 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H7z" clip-rule="evenodd"/>
</svg>
</div>
</div>
<div class="bg-red-50 rounded-lg p-4 border border-red-200">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-red-800">Non commencées</p>
<p class="text-xl font-bold text-red-900">{{ data.not_started }}</p>
</div>
<svg class="w-8 h-8 text-red-600" fill="currentColor" viewBox="0 0 20 20">
<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.92z" clip-rule="evenodd"/>
</svg>
</div>
</div>
</div>
<!-- Actions contextuelles -->
<div class="flex flex-wrap gap-2 mt-4 pt-4 border-t border-gray-200">
<a href="#" class="text-sm text-blue-600 hover:text-blue-800 font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
</svg>
Voir toutes les évaluations
</a>
<a href="#" class="text-sm text-green-600 hover:text-green-800 font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"/>
</svg>
Créer une évaluation
</a>
</div>
{% elif type == 'domains' %}
<!-- Liste des domaines avec codes couleurs -->
<div class="space-y-3">
{% for domain in data %}
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center">
<div class="w-4 h-4 rounded-full mr-3" style="background-color: {{ domain.color }}"></div>
<div>
<p class="font-medium text-gray-900">{{ domain.name }}</p>
<p class="text-sm text-gray-500">{{ domain.elements_count }} éléments</p>
</div>
</div>
<div class="text-right">
<p class="font-semibold text-gray-900">{{ domain.total_points|round(1) }} pts</p>
</div>
</div>
{% endfor %}
</div>
<!-- Actions contextuelles -->
<div class="flex flex-wrap gap-2 mt-4 pt-4 border-t border-gray-200">
<a href="#" class="text-sm text-purple-600 hover:text-purple-800 font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"/>
</svg>
Analyse par domaines
</a>
</div>
{% elif type == 'competences' %}
<!-- Liste des compétences avec badges -->
<div class="space-y-3">
{% for competence in data %}
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center">
<div class="px-2 py-1 text-xs font-medium rounded-full text-white mr-3"
style="background-color: {{ competence.color }}">
{{ competence.name[:2]|upper }}
</div>
<div>
<p class="font-medium text-gray-900">{{ competence.name }}</p>
<p class="text-sm text-gray-500">{{ competence.elements_count }} éléments</p>
</div>
</div>
<div class="text-right">
<p class="font-semibold text-gray-900">{{ competence.total_points|round(1) }} pts</p>
</div>
</div>
{% endfor %}
</div>
<!-- Actions contextuelles -->
<div class="flex flex-wrap gap-2 mt-4 pt-4 border-t border-gray-200">
<a href="#" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3z" clip-rule="evenodd"/>
</svg>
Évaluation par compétences
</a>
</div>
{% elif type == 'results' %}
<!-- Statistiques détaillées -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="text-center p-3 bg-blue-50 rounded-lg">
<p class="text-sm font-medium text-blue-800">Moyenne</p>
<p class="text-xl font-bold text-blue-900">{{ data.mean|round(1) }}</p>
</div>
<div class="text-center p-3 bg-green-50 rounded-lg">
<p class="text-sm font-medium text-green-800">Médiane</p>
<p class="text-xl font-bold text-green-900">{{ data.median|round(1) }}</p>
</div>
<div class="text-center p-3 bg-red-50 rounded-lg">
<p class="text-sm font-medium text-red-800">Minimum</p>
<p class="text-xl font-bold text-red-900">{{ data.min|round(1) }}</p>
</div>
<div class="text-center p-3 bg-purple-50 rounded-lg">
<p class="text-sm font-medium text-purple-800">Maximum</p>
<p class="text-xl font-bold text-purple-900">{{ data.max|round(1) }}</p>
</div>
</div>
<!-- Mini distribution des notes -->
<div class="mb-4">
<p class="text-sm font-medium text-gray-700 mb-2">Distribution des notes :</p>
<div class="flex items-end space-x-1 h-12">
{% for count in data.distribution %}
<div class="flex-1 bg-blue-200 rounded-t" style="height: {{ (count / (data.distribution|max or 1) * 100)|round }}%">
{% if count > 0 %}
<div class="text-xs text-center text-blue-800 pt-1">{{ count }}</div>
{% endif %}
</div>
{% endfor %}
</div>
<div class="flex justify-between text-xs text-gray-500 mt-1">
{% for i in range(data.distribution|length) %}
<span>{{ i }}-{{ i+1 }}</span>
{% endfor %}
</div>
</div>
<!-- Actions contextuelles -->
<div class="flex flex-wrap gap-2 pt-4 border-t border-gray-200">
<a href="#" class="text-sm text-green-600 hover:text-green-800 font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"/>
</svg>
Analyse détaillée
</a>
<a href="#" class="text-sm text-blue-600 hover:text-blue-800 font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z"/>
</svg>
Voir les graphiques
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endmacro %}
{# JavaScript pour l'interaction d'expansion - À inclure dans la page qui utilise le composant #}
<script>
function toggleStatsCard(button) {
const card = button.closest('.stats-card');
const expandedContent = card.querySelector('.expanded-content');
const chevron = button.querySelector('svg');
const isExpanded = card.dataset.expanded === 'true';
if (isExpanded) {
expandedContent.classList.add('hidden');
chevron.classList.remove('rotate-180');
card.dataset.expanded = 'false';
} else {
expandedContent.classList.remove('hidden');
chevron.classList.add('rotate-180');
card.dataset.expanded = 'true';
}
}
</script>

View File

@@ -0,0 +1,139 @@
{#
EXEMPLE D'UTILISATION des composants class/
Ce fichier montre comment utiliser les macros class_stats_card et trimester_nav
#}
{# Importer les composants #}
{% from 'components/class/class_stats_card.html' import class_stats_card %}
{% from 'components/class/trimester_nav.html' import trimester_nav %}
{# EXEMPLE 1 : Navigation par trimestre avec badges #}
<section class="mb-8">
{{ trimester_nav(
class_id=42,
base_url=url_for('classes.class_detail', class_id=42),
selected_trimester=2,
badges={1: 4, 2: 3, 3: 2, 'global': 9}
) }}
</section>
{# EXEMPLE 2 : Grille de cards de statistiques #}
<section class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6">
{# Card Évaluations (quantity) #}
{{ class_stats_card(
type='quantity',
data={
"total": 8,
"completed": 6,
"in_progress": 2,
"not_started": 0
},
expanded=False
) }}
{# Card Domaines (domains) #}
{{ class_stats_card(
type='domains',
data=[
{"name": "Nombres et calculs", "color": "#3B82F6", "total_points": 45.0, "elements_count": 12},
{"name": "Géométrie", "color": "#10B981", "total_points": 38.5, "elements_count": 9},
{"name": "Grandeurs et mesures", "color": "#F59E0B", "total_points": 22.0, "elements_count": 6},
{"name": "Organisation et gestion de données", "color": "#EF4444", "total_points": 15.5, "elements_count": 4}
],
expanded=True
) }}
{# Card Compétences (competences) #}
{{ class_stats_card(
type='competences',
data=[
{"name": "Chercher", "color": "#8B5CF6", "total_points": 25.0, "elements_count": 6},
{"name": "Modéliser", "color": "#06B6D4", "total_points": 20.0, "elements_count": 5},
{"name": "Représenter", "color": "#84CC16", "total_points": 18.0, "elements_count": 4},
{"name": "Calculer", "color": "#F97316", "total_points": 22.0, "elements_count": 7},
{"name": "Communiquer", "color": "#EC4899", "total_points": 16.0, "elements_count": 3}
]
) }}
{# Card Résultats (results) #}
{{ class_stats_card(
type='results',
data={
"mean": 14.2,
"median": 15.0,
"distribution": [0, 1, 3, 8, 12, 6, 2, 0],
"min": 8.0,
"max": 20.0
}
) }}
</section>
{# EXEMPLE 3 : Usage avancé avec trimestres colorés #}
<section class="mt-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Statistiques par trimestre</h2>
{% for trimester in [1, 2, 3] %}
<div class="mb-6">
{{ trimester_nav(
class_id=42,
base_url=url_for('classes.class_detail', class_id=42),
selected_trimester=trimester,
badges={1: 4, 2: 3, 3: 2, 'global': 9}
) }}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
{{ class_stats_card(
type='quantity',
data={
"total": [4, 3, 2][loop.index0],
"completed": [3, 2, 1][loop.index0],
"in_progress": [1, 1, 1][loop.index0],
"not_started": [0, 0, 0][loop.index0]
}
) }}
{{ class_stats_card(
type='results',
data={
"mean": [13.5, 14.8, 15.1][loop.index0],
"median": [14.0, 15.5, 15.0][loop.index0],
"distribution": [[0, 2, 4, 6, 8, 4, 1, 0], [0, 1, 2, 5, 10, 7, 3, 0], [0, 0, 1, 3, 8, 9, 4, 1]][loop.index0],
"min": [7.0, 9.5, 10.0][loop.index0],
"max": [18.5, 19.0, 20.0][loop.index0]
}
) }}
</div>
</div>
{% endfor %}
</section>
{# JavaScript global requis #}
<script>
// JavaScript pour les interactions des composants
// (Déjà inclus dans chaque composant, mais peut être externalisé)
// Fonction globale pour gérer les stats cards
function initializeStatsCards() {
document.querySelectorAll('.stats-card').forEach(card => {
const expandBtn = card.querySelector('.expand-btn');
if (expandBtn) {
expandBtn.addEventListener('click', () => toggleStatsCard(expandBtn));
}
});
}
// Fonction globale pour gérer la navigation trimestre
function initializeTrimesterNav() {
document.querySelectorAll('.trimester-nav .tab-btn').forEach(btn => {
btn.addEventListener('click', () => navigateToTrimester(btn));
});
}
// Initialisation au chargement de la page
document.addEventListener('DOMContentLoaded', function() {
initializeStatsCards();
initializeTrimesterNav();
});
</script>

View File

@@ -0,0 +1,280 @@
{#
Macro : trimester_nav(class_id, base_url, selected_trimester=None, badges={})
COULEURS THÉMATIQUES :
- T1 : from-blue-500 to-blue-600 (Hiver)
- T2 : from-green-500 to-green-600 (Printemps)
- T3 : from-orange-500 to-orange-600 (Été)
- Global : from-purple-500 to-purple-600 (Synthétique)
BADGES : dict optionnel avec le nombre d'évaluations par trimestre
Exemple: badges = {1: 4, 2: 3, 3: 2, 'global': 9}
#}
{% macro trimester_nav(class_id, base_url, selected_trimester=None, badges={}) %}
{# Configuration des trimestres avec couleurs thématiques #}
{% set trimesters = [
{
'key': 'global',
'label': 'Global',
'short': 'Global',
'gradient': 'from-purple-500 to-purple-600',
'hover_gradient': 'from-purple-600 to-purple-700',
'text_color': 'text-purple-600',
'bg_color': 'bg-purple-50',
'border_color': 'border-purple-200',
'icon': '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"/><path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"/></svg>',
'description': 'Vue d\'ensemble de l\'année scolaire'
},
{
'key': 1,
'label': 'Trimestre 1',
'short': 'T1',
'gradient': 'from-blue-500 to-blue-600',
'hover_gradient': 'from-blue-600 to-blue-700',
'text_color': 'text-blue-600',
'bg_color': 'bg-blue-50',
'border_color': 'border-blue-200',
'icon': '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V4a2 2 0 00-2-2h-8zM6 4h8v4l-4-2-4 2V4z"/></svg>',
'description': 'Septembre - Décembre'
},
{
'key': 2,
'label': 'Trimestre 2',
'short': 'T2',
'gradient': 'from-green-500 to-green-600',
'hover_gradient': 'from-green-600 to-green-700',
'text_color': 'text-green-600',
'bg_color': 'bg-green-50',
'border_color': 'border-green-200',
'icon': '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/></svg>',
'description': 'Janvier - Mars'
},
{
'key': 3,
'label': 'Trimestre 3',
'short': 'T3',
'gradient': 'from-orange-500 to-orange-600',
'hover_gradient': 'from-orange-600 to-orange-700',
'text_color': 'text-orange-600',
'bg_color': 'bg-orange-50',
'border_color': 'border-orange-200',
'icon': '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2L3 7v11a2 2 0 002 2h4v-6a1 1 0 011-1h2a1 1 0 011 1v6h4a2 2 0 002-2V7l-7-5zM8 15v2H6v-2h2z"/></svg>',
'description': 'Avril - Juillet'
}
] %}
<div class="trimester-nav bg-white rounded-xl shadow-lg p-6 mb-6" data-class-id="{{ class_id }}">
<!-- En-tête avec titre -->
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-lg font-semibold text-gray-900">Navigation par trimestre</h2>
<p class="text-sm text-gray-500">Sélectionnez une période pour filtrer les données</p>
</div>
<!-- Indicateur de sélection actuelle -->
<div class="hidden md:block">
{% set current_trimester = trimesters|selectattr('key', 'equalto', selected_trimester)|first %}
{% if current_trimester %}
<div class="flex items-center text-sm text-gray-600">
<span class="mr-2">Période active :</span>
<div class="flex items-center px-3 py-1 bg-gradient-to-r {{ current_trimester.gradient }} text-white rounded-lg">
{{ current_trimester.icon|safe }}
<span class="ml-2 font-medium">{{ current_trimester.label }}</span>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Tabs de navigation - Version desktop -->
<div class="hidden md:flex space-x-2 overflow-x-auto">
{% for trimester in trimesters %}
{% set is_active = selected_trimester == trimester.key or (selected_trimester is none and trimester.key == 'global') %}
{% set badge_count = badges.get(trimester.key, 0) %}
<button type="button"
class="tab-btn flex-shrink-0 group relative px-6 py-3 rounded-xl font-medium transition-all duration-300 transform hover:scale-105
{% if is_active %}
bg-gradient-to-r {{ trimester.gradient }} text-white shadow-lg
{% else %}
bg-gray-50 text-gray-600 hover:bg-gray-100 border border-gray-200
{% endif %}"
data-trimester="{{ trimester.key }}"
data-base-url="{{ base_url }}"
data-class-id="{{ class_id }}"
onclick="navigateToTrimester(this)">
<!-- Icône -->
<div class="flex items-center">
<div class="mr-3 {% if not is_active %}{{ trimester.text_color }}{% endif %}">
{{ trimester.icon|safe }}
</div>
<!-- Label et badge -->
<div class="flex items-center">
<span>{{ trimester.label }}</span>
{% if badge_count > 0 %}
<span class="ml-2 px-2 py-0.5 text-xs font-bold rounded-full
{% if is_active %}
bg-white/20 text-white
{% else %}
bg-{{ trimester.text_color.replace('text-', '').replace('-600', '') }}-100 {{ trimester.text_color }}
{% endif %}">
{{ badge_count }}
</span>
{% endif %}
</div>
</div>
<!-- Tooltip -->
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-gray-900 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
{{ trimester.description }}
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
</div>
</button>
{% endfor %}
</div>
<!-- Tabs de navigation - Version mobile (scroll horizontal) -->
<div class="md:hidden overflow-x-auto">
<div class="flex space-x-2 pb-2">
{% for trimester in trimesters %}
{% set is_active = selected_trimester == trimester.key or (selected_trimester is none and trimester.key == 'global') %}
{% set badge_count = badges.get(trimester.key, 0) %}
<button type="button"
class="tab-btn flex-shrink-0 px-4 py-2 rounded-lg font-medium text-sm transition-all duration-300
{% if is_active %}
bg-gradient-to-r {{ trimester.gradient }} text-white shadow-md
{% else %}
bg-gray-50 text-gray-600 hover:bg-gray-100 border border-gray-200
{% endif %}"
data-trimester="{{ trimester.key }}"
data-base-url="{{ base_url }}"
data-class-id="{{ class_id }}"
onclick="navigateToTrimester(this)">
<div class="flex items-center">
<div class="mr-2 {% if not is_active %}{{ trimester.text_color }}{% endif %}">
{{ trimester.icon|safe }}
</div>
<span>{{ trimester.short }}</span>
{% if badge_count > 0 %}
<span class="ml-1 px-1.5 py-0.5 text-xs font-bold rounded-full
{% if is_active %}
bg-white/20 text-white
{% else %}
bg-{{ trimester.text_color.replace('text-', '').replace('-600', '') }}-100 {{ trimester.text_color }}
{% endif %}">
{{ badge_count }}
</span>
{% endif %}
</div>
</button>
{% endfor %}
</div>
</div>
<!-- Informations contextuelles -->
<div class="mt-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
{% set current_trimester = trimesters|selectattr('key', 'equalto', selected_trimester)|first %}
{% if not current_trimester %}
{% set current_trimester = trimesters|selectattr('key', 'equalto', 'global')|first %}
{% endif %}
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-8 h-8 {{ current_trimester.bg_color }} rounded-lg flex items-center justify-center mr-3 {{ current_trimester.text_color }}">
{{ current_trimester.icon|safe }}
</div>
<div>
<p class="text-sm font-medium text-gray-900">{{ current_trimester.label }}</p>
<p class="text-xs text-gray-500">{{ current_trimester.description }}</p>
</div>
</div>
<!-- Statistiques rapides -->
{% set current_badge = badges.get(selected_trimester or 'global', 0) %}
{% if current_badge > 0 %}
<div class="text-right">
<p class="text-sm font-semibold {{ current_trimester.text_color }}">{{ current_badge }}</p>
<p class="text-xs text-gray-500">évaluation{{ 's' if current_badge > 1 else '' }}</p>
</div>
{% endif %}
</div>
</div>
</div>
{# JavaScript pour la navigation - À inclure dans la page qui utilise le composant #}
<script>
function navigateToTrimester(button) {
const trimester = button.dataset.trimester;
const baseUrl = button.dataset.baseUrl;
const classId = button.dataset.classId;
// Construire l'URL avec le paramètre trimestre
let url = baseUrl;
if (url.includes('?')) {
url += '&';
} else {
url += '?';
}
// Ajouter le paramètre trimestre (sauf pour 'global')
if (trimester !== 'global') {
url += `trimestre=${trimester}`;
} else {
// Pour global, on ne met pas de paramètre trimestre
url = baseUrl;
}
// Animation de transition
button.style.transform = 'scale(0.95)';
setTimeout(() => {
button.style.transform = '';
window.location.href = url;
}, 150);
}
// Gestion du scroll horizontal sur mobile
document.addEventListener('DOMContentLoaded', function() {
const mobileNav = document.querySelector('.trimester-nav .md\\:hidden .flex');
const activeButton = mobileNav?.querySelector('.bg-gradient-to-r');
// Scroller automatiquement vers le bouton actif sur mobile
if (activeButton && mobileNav) {
activeButton.scrollIntoView({
behavior: 'smooth',
inline: 'center',
block: 'nearest'
});
}
});
// Animation au hover pour les tooltips desktop
document.addEventListener('DOMContentLoaded', function() {
const buttons = document.querySelectorAll('.trimester-nav .tab-btn');
buttons.forEach(button => {
let timeout;
button.addEventListener('mouseenter', function() {
timeout = setTimeout(() => {
const tooltip = this.querySelector('.absolute.bottom-full');
if (tooltip) {
tooltip.style.transform = 'translateX(-50%) translateY(-2px)';
}
}, 300);
});
button.addEventListener('mouseleave', function() {
clearTimeout(timeout);
const tooltip = this.querySelector('.absolute.bottom-full');
if (tooltip) {
tooltip.style.transform = 'translateX(-50%) translateY(0)';
}
});
});
});
</script>
{% endmacro %}