refactor: html organisation
This commit is contained in:
400
static/css/design-system.css
Normal file
400
static/css/design-system.css
Normal 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
387
static/js/notytex.js
Normal 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;
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>© 2025 Gestion Scolaire - Application de gestion des évaluations</p>
|
<p>© 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>
|
||||||
101
templates/components/assessment/assessment_card.html
Normal file
101
templates/components/assessment/assessment_card.html
Normal 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 %}
|
||||||
213
templates/components/common/macros.html
Normal file
213
templates/components/common/macros.html
Normal 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 %}
|
||||||
Reference in New Issue
Block a user