Feat: add class page
This commit is contained in:
@@ -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 %}
|
||||
|
||||
463
templates/class_dashboard.html
Normal file
463
templates/class_dashboard.html
Normal 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 %}
|
||||
261
templates/components/class/README_COMPONENTS.md
Normal file
261
templates/components/class/README_COMPONENTS.md
Normal 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** ! 🎓✨
|
||||
@@ -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>
|
||||
|
||||
297
templates/components/class/class_stats_card.html
Normal file
297
templates/components/class/class_stats_card.html
Normal 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>
|
||||
139
templates/components/class/example_usage.html
Normal file
139
templates/components/class/example_usage.html
Normal 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>
|
||||
280
templates/components/class/trimester_nav.html
Normal file
280
templates/components/class/trimester_nav.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user