refactor: html organisation

This commit is contained in:
2025-08-05 10:56:28 +02:00
parent 1dccf28d5f
commit 4a2d8a73e1
6 changed files with 1209 additions and 240 deletions

View File

@@ -0,0 +1,400 @@
/**
* NOTYTEX DESIGN SYSTEM
* Système de design unifié pour l'application
*/
/* ========================================
DESIGN TOKENS
======================================== */
:root {
/* Couleurs primaires */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-800: #1e40af;
/* Couleurs fonctionnelles */
--color-success-50: #ecfdf5;
--color-success-100: #d1fae5;
--color-success-500: #10b981;
--color-success-600: #059669;
--color-success-700: #047857;
--color-warning-50: #fffbeb;
--color-warning-100: #fef3c7;
--color-warning-500: #f59e0b;
--color-warning-600: #d97706;
--color-warning-700: #b45309;
--color-danger-50: #fef2f2;
--color-danger-100: #fee2e2;
--color-danger-500: #ef4444;
--color-danger-600: #dc2626;
--color-danger-700: #b91c1c;
/* Couleurs neutres */
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-300: #d1d5db;
--color-gray-400: #9ca3af;
--color-gray-500: #6b7280;
--color-gray-600: #4b5563;
--color-gray-700: #374151;
--color-gray-800: #1f2937;
--color-gray-900: #111827;
/* Gradients standardisés */
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--gradient-success: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
--gradient-warning: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
--gradient-purple-blue: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-green: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
--gradient-blue: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
/* Espacements */
--space-xs: 0.25rem; /* 4px */
--space-sm: 0.5rem; /* 8px */
--space-md: 1rem; /* 16px */
--space-lg: 1.5rem; /* 24px */
--space-xl: 2rem; /* 32px */
--space-2xl: 3rem; /* 48px */
/* Tailles de police */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
--text-4xl: 2.25rem; /* 36px */
/* Rayons de bordure */
--radius-sm: 0.125rem; /* 2px */
--radius-md: 0.375rem; /* 6px */
--radius-lg: 0.5rem; /* 8px */
--radius-xl: 0.75rem; /* 12px */
--radius-2xl: 1rem; /* 16px */
--radius-full: 9999px;
/* Ombres */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
/* Transitions */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* ========================================
CLASSES UTILITAIRES PERSONNALISÉES
======================================== */
/* Focus visible amélioré */
.focus-ring {
@apply focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
}
/* Gradients réutilisables */
.bg-gradient-primary {
background: var(--gradient-primary);
}
.bg-gradient-success {
background: var(--gradient-success);
}
.bg-gradient-purple-blue {
background: var(--gradient-purple-blue);
}
/* Animation hover pour les cartes */
.card-hover {
@apply transition-all duration-300 hover:shadow-xl hover:scale-105;
}
/* Boutons avec styles cohérents */
.btn-base {
@apply inline-flex items-center justify-center font-semibold rounded-xl transition-all duration-300 focus-ring;
}
.btn-sm {
@apply btn-base px-3 py-2 text-sm;
}
.btn-md {
@apply btn-base px-4 py-2 text-base;
}
.btn-lg {
@apply btn-base px-6 py-3 text-lg;
}
.btn-primary {
@apply btn-base bg-blue-600 text-white hover:bg-blue-700;
}
.btn-success {
@apply btn-base bg-green-600 text-white hover:bg-green-700;
}
.btn-warning {
@apply btn-base bg-orange-600 text-white hover:bg-orange-700;
}
.btn-danger {
@apply btn-base bg-red-600 text-white hover:bg-red-700;
}
.btn-outline {
@apply btn-base bg-transparent border-2 border-gray-300 text-gray-700 hover:bg-gray-50;
}
/* États de progression standardisés */
.progress-not-started {
@apply bg-red-100 text-red-800 border border-red-200;
}
.progress-in-progress {
@apply bg-orange-100 text-orange-800 border border-orange-200;
}
.progress-completed {
@apply bg-green-100 text-green-800 border border-green-200;
}
/* Indicateurs visuels */
.indicator-badge {
@apply inline-flex items-center px-2 py-1 rounded-full text-xs font-medium;
}
/* ========================================
COMPOSANTS SPÉCIALISÉS
======================================== */
/* Hero sections */
.hero-section {
@apply bg-gradient-to-r text-white rounded-xl p-8 shadow-lg;
}
/* Cards uniformes */
.card {
@apply bg-white rounded-xl shadow-lg overflow-hidden;
}
.card-header {
@apply px-6 py-4 border-b border-gray-200 flex items-center justify-between;
}
.card-body {
@apply p-6;
}
.card-footer {
@apply px-6 py-4 border-t border-gray-200 bg-gray-50;
}
/* Grilles de statistiques */
.stats-grid {
@apply grid grid-cols-1 md:grid-cols-3 gap-6;
}
.stat-card {
@apply block bg-white rounded-lg shadow hover:shadow-lg transition-all duration-300 p-6 transform hover:scale-105 group;
}
/* Filtres */
.filter-section {
@apply bg-white rounded-lg shadow p-6;
}
.filter-control {
@apply border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent;
}
/* Actions grids */
.action-grid {
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4;
}
.action-card {
@apply group rounded-xl p-6 transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-xl text-white;
}
/* ========================================
ANIMATIONS ET EFFETS
======================================== */
/* Animation d'apparition */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-up {
animation: fadeInUp 0.5s ease-out;
}
/* Animation des messages flash */
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.flash-message {
animation: slideDown 0.3s ease-out;
}
/* Effet de pulsation pour les éléments en attente */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.pulse-animation {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Rotation pour les indicateurs de chargement */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spin-animation {
animation: spin 1s linear infinite;
}
/* ========================================
RESPONSIVE DESIGN
======================================== */
/* Masquer/afficher selon la taille d'écran */
@media (max-width: 768px) {
.hide-mobile {
display: none;
}
.show-mobile {
display: block;
}
/* Ajustements pour mobile */
.hero-section {
@apply p-6;
}
.hero-section h1 {
@apply text-2xl;
}
.action-grid {
@apply grid-cols-1 gap-3;
}
.stats-grid {
@apply grid-cols-1 gap-4;
}
}
@media (min-width: 769px) {
.hide-desktop {
display: none;
}
.show-desktop {
display: block;
}
}
/* ========================================
ACCESSIBILITÉ
======================================== */
/* Skip links */
.skip-link {
position: absolute;
top: -40px;
left: 6px;
background: var(--color-gray-900);
color: white;
padding: 8px;
text-decoration: none;
z-index: 1000;
border-radius: var(--radius-md);
}
.skip-link:focus {
top: 6px;
}
/* Contraste élevé */
@media (prefers-contrast: high) {
:root {
--color-gray-600: #000000;
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
}
.card {
@apply border-2 border-gray-800;
}
}
/* Réduction de mouvement */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Focus visible pour la navigation au clavier */
.focus-visible {
outline: 2px solid var(--color-primary-500);
outline-offset: 2px;
}
/* ========================================
DARK MODE (préparation future)
======================================== */
@media (prefers-color-scheme: dark) {
:root {
--color-gray-50: #1f2937;
--color-gray-100: #374151;
--color-gray-900: #f9fafb;
}
/* Note: Le dark mode sera implémenté dans une future phase */
}

387
static/js/notytex.js Normal file
View File

@@ -0,0 +1,387 @@
/**
* NOTYTEX - JavaScript Application Core
* Fonctionnalités JavaScript centralisées et réutilisables
*/
const Notytex = {
// Configuration globale
config: {
transitions: {
fast: 150,
normal: 300,
slow: 500
},
breakpoints: {
sm: 640,
md: 768,
lg: 1024,
xl: 1280
}
},
// Utilitaires généraux
utils: {
/**
* Débounce une fonction
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
/**
* Vérifie si un élément est visible dans le viewport
*/
isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
},
/**
* Anime l'apparition des éléments
*/
animateOnScroll() {
const elements = document.querySelectorAll('[data-animate]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const animationType = entry.target.dataset.animate;
entry.target.classList.add(`animate-${animationType}`);
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
elements.forEach(el => observer.observe(el));
},
/**
* Copie du texte dans le presse-papiers
*/
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
this.showToast('Copié dans le presse-papiers', 'success');
} catch (err) {
console.error('Erreur lors de la copie:', err);
this.showToast('Erreur lors de la copie', 'error');
}
},
/**
* Affiche une notification toast
*/
showToast(message, type = 'info', duration = 3000) {
const toast = document.createElement('div');
const typeClasses = {
success: 'bg-green-500 text-white',
error: 'bg-red-500 text-white',
warning: 'bg-orange-500 text-white',
info: 'bg-blue-500 text-white'
};
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${typeClasses[type]} transition-all duration-300 transform translate-x-full`;
toast.textContent = message;
document.body.appendChild(toast);
// Animation d'entrée
setTimeout(() => toast.classList.remove('translate-x-full'), 100);
// Suppression automatique
setTimeout(() => {
toast.classList.add('translate-x-full');
setTimeout(() => document.body.removeChild(toast), 300);
}, duration);
}
},
// Gestion des filtres
filters: {
/**
* Initialise les filtres pour une page
*/
init(config = {}) {
const defaultConfig = {
autoSubmit: true,
debounceTime: 300,
preserveState: true
};
const settings = { ...defaultConfig, ...config };
const filterElements = document.querySelectorAll('[data-filter]');
const applyFilters = Notytex.utils.debounce(this.applyFilters, settings.debounceTime);
filterElements.forEach(element => {
element.addEventListener('change', applyFilters);
});
// Restaurer l'état des filtres depuis l'URL
if (settings.preserveState) {
this.restoreFromURL();
}
},
/**
* Applique les filtres en construisant l'URL
*/
applyFilters() {
const params = new URLSearchParams();
const filterElements = document.querySelectorAll('[data-filter]');
filterElements.forEach(element => {
const filterName = element.dataset.filter;
const value = element.value;
if (value && value !== '') {
params.set(filterName, value);
}
});
// Mise à jour de l'URL
const url = new URL(window.location);
url.search = params.toString();
// Navigation sans rechargement complet si possible
if (history.pushState) {
history.pushState(null, '', url.toString());
// Déclenchement d'un événement personnalisé pour les composants qui écoutent
window.dispatchEvent(new CustomEvent('filtersChanged', { detail: params }));
} else {
// Fallback pour les anciens navigateurs
window.location.href = url.toString();
}
},
/**
* Restaure les filtres depuis l'URL
*/
restoreFromURL() {
const params = new URLSearchParams(window.location.search);
params.forEach((value, key) => {
const element = document.querySelector(`[data-filter="${key}"]`);
if (element) {
element.value = value;
}
});
}
},
// Gestion des modales
modals: {
/**
* Ouvre une modale
*/
open(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;
modal.classList.remove('hidden');
modal.classList.add('flex');
// Animation d'entrée
setTimeout(() => {
modal.querySelector('.modal-content')?.classList.add('scale-100');
modal.querySelector('.modal-content')?.classList.remove('scale-95');
}, 10);
// Focus sur la modale pour l'accessibilité
modal.setAttribute('aria-hidden', 'false');
modal.focus();
// Écouter la touche Escape
document.addEventListener('keydown', this.handleEscapeKey);
},
/**
* Ferme une modale
*/
close(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;
// Animation de sortie
modal.querySelector('.modal-content')?.classList.add('scale-95');
modal.querySelector('.modal-content')?.classList.remove('scale-100');
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
modal.setAttribute('aria-hidden', 'true');
}, 150);
document.removeEventListener('keydown', this.handleEscapeKey);
},
/**
* Gère la touche Escape pour fermer les modales
*/
handleEscapeKey(event) {
if (event.key === 'Escape') {
const openModal = document.querySelector('.modal:not(.hidden)');
if (openModal) {
Notytex.modals.close(openModal.id);
}
}
}
},
// Gestion des données en temps réel
realtime: {
/**
* Met à jour les indicateurs de progression
*/
updateProgressIndicators() {
const indicators = document.querySelectorAll('[data-progress]');
indicators.forEach(indicator => {
const assessmentId = indicator.dataset.assessmentId;
// Ici on peut faire un appel AJAX pour récupérer les données actualisées
// Pour l'instant, on simule avec une animation
this.animateProgress(indicator);
});
},
/**
* Animation des cercles de progression
*/
animateProgress(element) {
const circle = element.querySelector('circle[stroke-dashoffset]');
if (!circle) return;
const currentOffset = parseFloat(circle.getAttribute('stroke-dashoffset'));
const circumference = 2 * Math.PI * parseFloat(circle.getAttribute('r'));
// Animation fluide
circle.style.transition = 'stroke-dashoffset 1s ease-in-out';
}
},
// Gestion des confirmations
confirmations: {
/**
* Affiche une confirmation avant une action
*/
show(message, callback, options = {}) {
const defaultOptions = {
title: 'Confirmation',
confirmText: 'Confirmer',
cancelText: 'Annuler',
type: 'warning'
};
const settings = { ...defaultOptions, ...options };
// Si on a SweetAlert2 disponible, on l'utilise
if (typeof Swal !== 'undefined') {
Swal.fire({
title: settings.title,
text: message,
icon: settings.type,
showCancelButton: true,
confirmButtonText: settings.confirmText,
cancelButtonText: settings.cancelText,
confirmButtonColor: settings.type === 'danger' ? '#dc2626' : '#3b82f6'
}).then((result) => {
if (result.isConfirmed) {
callback();
}
});
} else {
// Fallback avec confirm() natif
if (confirm(message)) {
callback();
}
}
}
},
// Initialisation de l'application
init() {
console.log('🎓 Notytex Application Initialized');
// Initialisation des fonctionnalités de base
this.utils.animateOnScroll();
// Initialisation des filtres si présents
if (document.querySelector('[data-filter]')) {
this.filters.init();
}
// Gestionnaires d'événements globaux
this.bindGlobalEvents();
// Mise à jour périodique des indicateurs (optionnel)
if (document.querySelector('[data-progress]')) {
setInterval(() => {
this.realtime.updateProgressIndicators();
}, 30000); // Toutes les 30 secondes
}
},
// Événements globaux
bindGlobalEvents() {
// Fermeture des dropdowns au clic extérieur
document.addEventListener('click', (event) => {
const dropdowns = document.querySelectorAll('.dropdown-menu.show');
dropdowns.forEach(dropdown => {
if (!dropdown.contains(event.target)) {
dropdown.classList.remove('show');
}
});
});
// Gestion des liens de confirmation
document.addEventListener('click', (event) => {
const confirmLink = event.target.closest('[data-confirm]');
if (confirmLink) {
event.preventDefault();
const message = confirmLink.dataset.confirm;
this.confirmations.show(message, () => {
if (confirmLink.tagName === 'A') {
window.location.href = confirmLink.href;
} else if (confirmLink.tagName === 'BUTTON') {
confirmLink.click();
}
});
}
});
// Gestion des boutons de copie
document.addEventListener('click', (event) => {
const copyButton = event.target.closest('[data-copy]');
if (copyButton) {
const textToCopy = copyButton.dataset.copy;
this.utils.copyToClipboard(textToCopy);
}
});
}
};
// Auto-initialisation au chargement du DOM
document.addEventListener('DOMContentLoaded', () => {
Notytex.init();
});
// Export pour utilisation dans d'autres scripts
window.Notytex = Notytex;

View File

@@ -1,81 +1,73 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from 'components/common/macros.html' import hero_section, filter_section %}
{% from 'components/assessment/assessment_card.html' import assessment_card %}
{% block title %}Évaluations - Gestion Scolaire{% endblock %} {% block title %}Évaluations - Gestion Scolaire{% endblock %}
{% block content %} {% block content %}
<div class="space-y-8"> <div class="space-y-8">
<!-- Hero Section --> {# Hero Section avec composant réutilisable #}
<div class="bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-xl p-8 shadow-lg"> {% set meta_info = [
<div class="flex items-center justify-between"> {
<div> 'icon': '<svg class="w-4 h-4 mr-2" 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-2V5zm2.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z" clip-rule="evenodd"/></svg>',
<h1 class="text-4xl font-bold mb-2">Mes Évaluations 📚</h1> 'text': assessments|length ~ ' évaluations'
<p class="text-xl opacity-90 mb-1">Gérez et organisez toutes vos évaluations</p> },
<div class="flex items-center space-x-6 text-sm opacity-75 mt-3"> {
<span class="flex items-center"> '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>',
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"> 'text': 'Année scolaire 2024-2025'
<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-2V5zm2.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z" clip-rule="evenodd"/> ] %}
</svg> {% set primary_action = {
{{ assessments|length }} évaluations 'url': url_for('assessments.new'),
</span> 'text': 'Nouvelle évaluation',
<span class="flex items-center"> 'icon': '<svg class="w-5 h-5 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>'
<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"/> {{ hero_section(
</svg> title="Mes Évaluations 📚",
Année scolaire 2024-2025 subtitle="Gérez et organisez toutes vos évaluations",
</span> meta_info=meta_info,
</div> primary_action=primary_action,
</div> gradient_class="from-purple-600 to-blue-600"
<div class="hidden md:block"> ) }}
<a href="{{ url_for('assessments.new') }}"
class="bg-white/20 hover:bg-white/30 text-white px-6 py-3 rounded-xl transition-all duration-300 font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 flex items-center">
<svg class="w-5 h-5 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>
Nouvelle évaluation
</a>
</div>
</div>
</div>
<!-- Filtres et actions --> {# Filtres avec composant réutilisable #}
<div class="bg-white rounded-lg shadow p-6"> {% set class_options = [{'value': '', 'label': 'Toutes'}] %}
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0"> {% for class_group in classes %}
<div class="flex flex-col md:flex-row md:items-center space-y-3 md:space-y-0 md:space-x-4"> {% set _ = class_options.append({'value': class_group.id|string, 'label': class_group.name}) %}
<div class="flex items-center space-x-2"> {% endfor %}
<label class="text-sm font-medium text-gray-700">Trimestre :</label>
<select id="trimester-filter" class="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"> {% set filters = [
<option value="">Tous</option> {
<option value="1" {% if current_trimester == '1' %}selected{% endif %}>Trimestre 1</option> 'id': 'trimester-filter',
<option value="2" {% if current_trimester == '2' %}selected{% endif %}>Trimestre 2</option> 'label': 'Trimestre',
<option value="3" {% if current_trimester == '3' %}selected{% endif %}>Trimestre 3</option> 'options': [
</select> {'value': '', 'label': 'Tous'},
</div> {'value': '1', 'label': 'Trimestre 1'},
<div class="flex items-center space-x-2"> {'value': '2', 'label': 'Trimestre 2'},
<label class="text-sm font-medium text-gray-700">Classe :</label> {'value': '3', 'label': 'Trimestre 3'}
<select id="class-filter" class="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"> ]
<option value="">Toutes</option> },
{% for class_group in classes %} {
<option value="{{ class_group.id }}" {% if current_class == class_group.id|string %}selected{% endif %}> 'id': 'class-filter',
{{ class_group.name }} 'label': 'Classe',
</option> 'options': class_options
{% endfor %} },
</select> {
</div> 'id': 'sort-filter',
<div class="flex items-center space-x-2"> 'label': 'Tri',
<label class="text-sm font-medium text-gray-700">Tri :</label> 'options': [
<select id="sort-filter" class="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"> {'value': 'date_desc', 'label': 'Plus récent'},
<option value="date_desc" {% if current_sort == 'date_desc' %}selected{% endif %}>Plus récent</option> {'value': 'date_asc', 'label': 'Plus ancien'},
<option value="date_asc" {% if current_sort == 'date_asc' %}selected{% endif %}>Plus ancien</option> {'value': 'title', 'label': 'Titre A-Z'},
<option value="title" {% if current_sort == 'title' %}selected{% endif %}>Titre A-Z</option> {'value': 'class', 'label': 'Classe'}
<option value="class" {% if current_sort == 'class' %}selected{% endif %}>Classe</option> ]
</select> }
</div> ] %}
<!-- Compteur de résultats --> {% call filter_section(filters, {'trimester-filter': current_trimester, 'class-filter': current_class, 'sort-filter': current_sort}) %}
<div class="text-sm text-gray-500 font-medium"> <div class="flex items-center space-x-4">
{{ assessments|length }} évaluation(s) <div class="text-sm text-gray-500 font-medium">
</div> {{ assessments|length }} évaluation(s)
</div> </div>
<div class="md:hidden"> <div class="md:hidden">
<a href="{{ url_for('assessments.new') }}" <a href="{{ url_for('assessments.new') }}"
@@ -87,144 +79,13 @@
</a> </a>
</div> </div>
</div> </div>
</div> {% endcall %}
{% if assessments %} {% if assessments %}
<!-- Grille d'évaluations modernisée --> <!-- Grille d'évaluations avec composants -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for assessment in assessments %} {% for assessment in assessments %}
{% set trimester_colors = { {{ assessment_card(assessment) }}
1: {'bg': 'from-blue-500 to-blue-600', 'accent': 'blue', 'icon_bg': 'bg-blue-100', 'icon_text': 'text-blue-600'},
2: {'bg': 'from-green-500 to-green-600', 'accent': 'green', 'icon_bg': 'bg-green-100', 'icon_text': 'text-green-600'},
3: {'bg': 'from-orange-500 to-orange-600', 'accent': 'orange', 'icon_bg': 'bg-orange-100', 'icon_text': 'text-orange-600'}
} %}
{% set colors = trimester_colors[assessment.trimester] %}
<div class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 overflow-hidden">
<!-- Header avec gradient thématique -->
<div class="bg-gradient-to-r {{ colors.bg }} p-4 text-white">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
<svg class="w-5 h-5" 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-2V5zm2.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z" clip-rule="evenodd"/>
</svg>
</div>
<div class="text-sm font-semibold">Trimestre {{ assessment.trimester }}</div>
</div>
<div class="text-xs opacity-75">{{ assessment.date.strftime('%d/%m/%Y') }}</div>
</div>
</div>
<!-- Contenu principal -->
<div class="p-6">
<h3 class="text-lg font-bold text-gray-900 mb-2 line-clamp-2">{{ assessment.title }}</h3>
<!-- Informations clés -->
<div class="space-y-3 mb-4">
<div class="flex items-center text-sm text-gray-600">
<svg class="w-4 h-4 mr-2 {{ colors.icon_text }}" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span class="font-medium">{{ assessment.class_group.name }}</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center text-sm text-gray-600">
<svg class="w-4 h-4 mr-2 {{ colors.icon_text }}" fill="currentColor" viewBox="0 0 20 20">
<path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
</svg>
<span>{{ assessment.exercises|length }} exercice(s)</span>
</div>
<div class="bg-{{ colors.accent }}-100 text-{{ colors.accent }}-800 text-xs px-2 py-1 rounded-full font-medium">
Coeff. {{ assessment.coefficient }}
</div>
</div>
<!-- Indicateur de progression des notes -->
{% set progress = assessment.grading_progress %}
<div class="flex items-center justify-between pt-2 border-t border-gray-100">
<!-- Indicateur fusionné cliquable -->
{% if progress.status == 'completed' %}
<div class="inline-flex items-center px-3 py-2 rounded-lg text-sm font-medium bg-green-100 text-green-800 border border-green-200">
<svg class="w-4 h-4 mr-2" 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>
<span class="font-semibold">Correction 100% </span>
</div>
{% elif progress.status == 'in_progress' %}
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}"
class="inline-flex items-center px-3 py-2 rounded-lg text-sm font-medium bg-orange-100 text-orange-800 border border-orange-200 hover:bg-orange-200 transition-colors cursor-pointer">
<div class="relative w-4 h-4 mr-2">
<svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" class="text-orange-300"/>
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none"
class="text-orange-600" stroke-dasharray="37.7"
stroke-dashoffset="{{ 37.7 - (37.7 * progress.percentage / 100) }}"/>
</svg>
</div>
<span class="font-semibold">Correction {{ progress.percentage }}% </span>
</a>
{% elif progress.status == 'not_started' %}
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}"
class="inline-flex items-center px-3 py-2 rounded-lg text-sm font-medium bg-red-100 text-red-800 border border-red-200 hover:bg-red-200 transition-colors cursor-pointer">
<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>
<span class="font-semibold">Correction 0%</span>
</a>
{% else %}
<div class="inline-flex items-center px-3 py-2 rounded-lg text-sm font-medium bg-gray-100 text-gray-800 border border-gray-200">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>
<span class="font-semibold">Non définie</span>
</div>
{% endif %}
<!-- Info détaillée -->
<div class="text-xs text-gray-500 text-right">
{{ progress.completed }}/{{ progress.total }}
</div>
</div>
</div>
{% if assessment.description %}
<p class="text-sm text-gray-600 mb-4 line-clamp-2">{{ assessment.description }}</p>
{% endif %}
<!-- Actions -->
<div class="flex space-x-2">
<a href="{{ url_for('assessments.detail', id=assessment.id) }}"
class="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 hover:text-gray-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-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>
Détails
</a>
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}"
class="flex-1 bg-{{ colors.accent }}-50 hover:bg-{{ colors.accent }}-100 text-{{ colors.accent }}-700 hover:text-{{ colors.accent }}-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
</svg>
Noter
</a>
<a href="{{ url_for('assessments.edit', id=assessment.id) }}"
class="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 hover:text-gray-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/>
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"/>
</svg>
Modifier
</a>
</div>
</div>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
@@ -260,39 +121,6 @@
{% endif %} {% endif %}
</div> </div>
<script> {# JavaScript géré par le système centralisé #}
// Gestion des filtres
document.addEventListener('DOMContentLoaded', function() {
const trimesterFilter = document.getElementById('trimester-filter');
const classFilter = document.getElementById('class-filter');
const sortFilter = document.getElementById('sort-filter');
function applyFilters() {
const params = new URLSearchParams();
if (trimesterFilter.value) {
params.set('trimester', trimesterFilter.value);
}
if (classFilter.value) {
params.set('class', classFilter.value);
}
if (sortFilter.value) {
params.set('sort', sortFilter.value);
}
// Rediriger avec les nouveaux paramètres
const url = new URL(window.location);
url.search = params.toString();
window.location.href = url.toString();
}
// Écouter les changements sur tous les filtres
[trimesterFilter, classFilter, sortFilter].forEach(filter => {
filter.addEventListener('change', applyFilters);
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -5,17 +5,51 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Gestion Scolaire{% endblock %}</title> <title>{% block title %}Gestion Scolaire{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script> <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/app.css') }}">
<script> <script>
// Configuration Tailwind pour dark mode et focus // Configuration Tailwind étendue avec design tokens
tailwind.config = { tailwind.config = {
theme: { theme: {
extend: { extend: {
colors: { colors: {
primary: { primary: {
50: '#eff6ff', 50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb', 600: '#2563eb',
700: '#1d4ed8', 700: '#1d4ed8',
800: '#1e40af',
},
success: {
50: '#ecfdf5',
100: '#d1fae5',
500: '#10b981',
600: '#059669',
700: '#047857',
},
warning: {
50: '#fffbeb',
100: '#fef3c7',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
},
danger: {
50: '#fef2f2',
100: '#fee2e2',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
} }
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'fade-in-up': 'fadeInUp 0.5s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
'pulse-slow': 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
} }
} }
} }
@@ -82,5 +116,11 @@
<p>&copy; 2025 Gestion Scolaire - Application de gestion des évaluations</p> <p>&copy; 2025 Gestion Scolaire - Application de gestion des évaluations</p>
</div> </div>
</footer> </footer>
<!-- JavaScript Core -->
<script src="{{ url_for('static', filename='js/notytex.js') }}"></script>
<!-- JavaScript spécifique aux pages -->
{% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -0,0 +1,101 @@
{# Composant pour carte d'évaluation dans la liste #}
{% from 'components/common/macros.html' import progress_indicator %}
{% macro assessment_card(assessment) %}
{% set trimester_colors = {
1: {'bg': 'from-blue-500 to-blue-600', 'accent': 'blue', 'icon_bg': 'bg-blue-100', 'icon_text': 'text-blue-600'},
2: {'bg': 'from-green-500 to-green-600', 'accent': 'green', 'icon_bg': 'bg-green-100', 'icon_text': 'text-green-600'},
3: {'bg': 'from-orange-500 to-orange-600', 'accent': 'orange', 'icon_bg': 'bg-orange-100', 'icon_text': 'text-orange-600'}
} %}
{% set colors = trimester_colors[assessment.trimester] %}
<div class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 overflow-hidden">
<!-- Header avec gradient thématique -->
<div class="bg-gradient-to-r {{ colors.bg }} p-4 text-white">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
<svg class="w-5 h-5" 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-2V5zm2.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z" clip-rule="evenodd"/>
</svg>
</div>
<div class="text-sm font-semibold">Trimestre {{ assessment.trimester }}</div>
</div>
<div class="text-xs opacity-75">{{ assessment.date.strftime('%d/%m/%Y') }}</div>
</div>
</div>
<!-- Contenu principal -->
<div class="p-6">
<h3 class="text-lg font-bold text-gray-900 mb-2 line-clamp-2">{{ assessment.title }}</h3>
<!-- Informations clés -->
<div class="space-y-3 mb-4">
<div class="flex items-center text-sm text-gray-600">
<svg class="w-4 h-4 mr-2 {{ colors.icon_text }}" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span class="font-medium">{{ assessment.class_group.name }}</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center text-sm text-gray-600">
<svg class="w-4 h-4 mr-2 {{ colors.icon_text }}" fill="currentColor" viewBox="0 0 20 20">
<path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
</svg>
<span>{{ assessment.exercises|length }} exercice(s)</span>
</div>
<div class="bg-{{ colors.accent }}-100 text-{{ colors.accent }}-800 text-xs px-2 py-1 rounded-full font-medium">
Coeff. {{ assessment.coefficient }}
</div>
</div>
<!-- Indicateur de progression des notes -->
{% set progress = assessment.grading_progress %}
<div class="flex items-center justify-between pt-2 border-t border-gray-100">
{{ progress_indicator(progress, True, assessment.id) }}
<!-- Info détaillée -->
<div class="text-xs text-gray-500 text-right">
{{ progress.completed }}/{{ progress.total }}
</div>
</div>
</div>
{% if assessment.description %}
<p class="text-sm text-gray-600 mb-4 line-clamp-2">{{ assessment.description }}</p>
{% endif %}
<!-- Actions -->
<div class="flex space-x-2">
<a href="{{ url_for('assessments.detail', id=assessment.id) }}"
class="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 hover:text-gray-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-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>
Détails
</a>
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}"
class="flex-1 bg-{{ colors.accent }}-50 hover:bg-{{ colors.accent }}-100 text-{{ colors.accent }}-700 hover:text-{{ colors.accent }}-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
</svg>
Noter
</a>
<a href="{{ url_for('assessments.edit', id=assessment.id) }}"
class="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 hover:text-gray-900 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center justify-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/>
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"/>
</svg>
Modifier
</a>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -0,0 +1,213 @@
{# Macros pour composants réutilisables #}
{# Macro pour section hero avec gradient #}
{% macro hero_section(title, subtitle=None, meta_info=[], primary_action=None, icon=None, gradient_class="from-purple-600 to-blue-600") %}
<div class="bg-gradient-to-r {{ gradient_class }} text-white rounded-xl p-8 shadow-lg">
<div class="flex items-center justify-between">
<div class="flex-1">
{% if caller %}
{{ caller() }}
{% endif %}
<h1 class="text-4xl font-bold mb-2">{{ title }}</h1>
{% if subtitle %}
<p class="text-xl opacity-90 mb-1">{{ subtitle }}</p>
{% endif %}
{% if meta_info %}
<div class="flex items-center space-x-6 text-sm opacity-75 mt-3">
{% for info in meta_info %}
<span class="flex items-center">
{% if info.icon %}
{{ info.icon|safe }}
{% endif %}
{{ info.text }}
</span>
{% endfor %}
</div>
{% endif %}
</div>
<div class="hidden md:block">
{% if primary_action %}
<a href="{{ primary_action.url }}"
class="bg-white/20 hover:bg-white/30 text-white px-6 py-3 rounded-xl transition-all duration-300 font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 flex items-center">
{% if primary_action.icon %}
{{ primary_action.icon|safe }}
{% endif %}
{{ primary_action.text }}
</a>
{% elif icon %}
<div class="w-24 h-24 bg-white/20 rounded-full flex items-center justify-center">
{{ icon|safe }}
</div>
{% endif %}
</div>
</div>
</div>
{% endmacro %}
{# Macro pour card standardisée #}
{% macro card(title=None, actions=[], classes="", body_classes="p-6") %}
<div class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 {{ classes }}">
{% if title %}
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">{{ title }}</h2>
{% if actions %}
<div class="flex space-x-2">
{% for action in actions %}
<a href="{{ action.url }}" class="{{ action.classes|default('text-blue-600 hover:text-blue-800 text-sm font-medium') }}">
{{ action.text }}
</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
<div class="{{ body_classes }}">
{{ caller() }}
</div>
</div>
{% endmacro %}
{# Macro pour bouton avec gradient #}
{% macro gradient_button(url, text, icon=None, gradient_class="from-blue-500 to-blue-600", hover_gradient="from-blue-600 to-blue-700", size="md", classes="") %}
{% set size_classes = {
'sm': 'px-4 py-2 text-sm',
'md': 'px-6 py-3 text-base',
'lg': 'px-8 py-4 text-lg'
} %}
<a href="{{ url }}"
class="group bg-gradient-to-r {{ gradient_class }} hover:{{ hover_gradient }} text-white rounded-xl {{ size_classes[size] }} transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-xl font-semibold flex items-center {{ classes }}">
{% if icon %}
<div class="w-5 h-5 mr-2 group-hover:scale-110 transition-transform">
{{ icon|safe }}
</div>
{% endif %}
{{ text }}
</a>
{% endmacro %}
{# Macro pour action card (comme les cartes d'actions principales) #}
{% macro action_card(url, title, subtitle, icon, gradient_class="from-green-500 to-green-600", hover_gradient="from-green-600 to-green-700", is_button=False) %}
{% if is_button %}
<button onclick="{{ url }}"
{% else %}
<a href="{{ url }}"
{% endif %}
class="group bg-gradient-to-r {{ gradient_class }} hover:{{ hover_gradient }} text-white rounded-xl p-6 transition-all duration-300 transform hover:scale-105 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">
{{ icon|safe }}
</div>
<div>
<h3 class="text-lg font-bold mb-1">{{ title }}</h3>
<p class="text-sm opacity-90">{{ subtitle }}</p>
</div>
</div>
{% if is_button %}
</button>
{% else %}
</a>
{% endif %}
{% endmacro %}
{# Macro pour indicateur de progression #}
{% macro progress_indicator(progress, clickable=True, assessment_id=None, compact=False) %}
{% set base_classes = "inline-flex items-center rounded-full text-xs font-medium border" %}
{% set size_classes = "px-3 py-2" if not compact else "px-2 py-1" %}
{% if progress.status == 'completed' %}
<div class="{{ base_classes }} {{ size_classes }} bg-green-100 text-green-800 border-green-200">
<svg class="w-4 h-4 mr-2" 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>
<span class="font-semibold">Correction 100%</span>
</div>
{% elif progress.status == 'in_progress' %}
{% if clickable and assessment_id %}
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment_id) }}"
{% else %}
<div
{% endif %}
class="{{ base_classes }} {{ size_classes }} bg-orange-100 text-orange-800 border-orange-200 {% if clickable %}hover:bg-orange-200 transition-colors cursor-pointer{% endif %}">
<div class="relative w-4 h-4 mr-2">
<svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" class="text-orange-300"/>
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none"
class="text-orange-600" stroke-dasharray="37.7"
stroke-dashoffset="{{ 37.7 - (37.7 * progress.percentage / 100) }}"/>
</svg>
</div>
<span class="font-semibold">Correction {{ progress.percentage }}%</span>
{% if clickable and assessment_id %}</a>{% else %}</div>{% endif %}
{% elif progress.status == 'not_started' %}
{% if clickable and assessment_id %}
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment_id) }}"
{% else %}
<div
{% endif %}
class="{{ base_classes }} {{ size_classes }} bg-red-100 text-red-800 border-red-200 {% if clickable %}hover:bg-red-200 transition-colors cursor-pointer{% endif %}">
<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>
<span class="font-semibold">Correction 0%</span>
{% if clickable and assessment_id %}</a>{% else %}</div>{% endif %}
{% else %}
<div class="{{ base_classes }} {{ size_classes }} bg-gray-100 text-gray-800 border-gray-200">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>
<span class="font-semibold">Non définie</span>
</div>
{% endif %}
{% endmacro %}
{# Macro pour carte de statistique cliquable #}
{% macro stat_card(title, value, url, icon, color="blue", description=None) %}
<a href="{{ url }}" class="block bg-white rounded-lg shadow hover:shadow-lg transition-all duration-300 p-6 transform hover:scale-105 group">
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-medium text-gray-500 group-hover:text-{{ color }}-600 transition-colors">{{ title }}</div>
<div class="text-3xl font-bold text-gray-900 group-hover:text-{{ color }}-700 transition-colors">{{ value }}</div>
{% if description %}
<div class="text-xs text-{{ color }}-600 flex items-center mt-1">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
{{ description }}
</div>
{% endif %}
</div>
<div class="w-12 h-12 bg-{{ color }}-100 group-hover:bg-{{ color }}-200 rounded-full flex items-center justify-center transition-colors">
{{ icon|safe }}
</div>
</div>
</a>
{% endmacro %}
{# Macro pour filtres standardisés #}
{% macro filter_section(filters, current_values={}) %}
<div class="filter-section">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
<div class="flex flex-col md:flex-row md:items-center space-y-3 md:space-y-0 md:space-x-4">
{% for filter in filters %}
<div class="flex items-center space-x-2">
<label class="text-sm font-medium text-gray-700">{{ filter.label }} :</label>
<select data-filter="{{ filter.id.replace('-filter', '') }}" class="filter-control">
{% for option in filter.options %}
<option value="{{ option.value }}" {% if current_values.get(filter.id) == option.value %}selected{% endif %}>
{{ option.label }}
</option>
{% endfor %}
</select>
</div>
{% endfor %}
</div>
<!-- Zone pour contenu additionnel -->
{% if caller %}
<div class="flex items-center space-x-4">
{{ caller() }}
</div>
{% endif %}
</div>
</div>
{% endmacro %}