feat: adding new classes is done
This commit is contained in:
209
templates/class_form.html
Normal file
209
templates/class_form.html
Normal file
@@ -0,0 +1,209 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ title }} - Gestion Scolaire{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="mb-6">
|
||||
<a href="{{ url_for('classes') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||||
← Retour aux classes
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
|
||||
{% if is_edit %}
|
||||
<p class="text-sm text-gray-600 mt-1">Modifiez les informations de votre classe</p>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-600 mt-1">Créez une nouvelle classe pour vos élèves</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="POST"
|
||||
action="{% if is_edit %}{{ url_for('classes.update', id=class_group.id) }}{% else %}{{ url_for('classes.create') }}{% endif %}"
|
||||
class="px-6 py-6 space-y-8" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Messages flash -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="space-y-3">
|
||||
{% for category, message in messages %}
|
||||
<div class="p-4 rounded-lg border-l-4 {% if category == 'error' %}bg-red-50 border-red-500 text-red-800{% else %}bg-green-50 border-green-500 text-green-800{% endif %}">
|
||||
<div class="flex items-start">
|
||||
{% if category == 'error' %}
|
||||
<svg class="w-5 h-5 mt-0.5 mr-3 flex-shrink-0" 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 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="w-5 h-5 mt-0.5 mr-3 flex-shrink-0" 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>
|
||||
{% endif %}
|
||||
<p class="font-medium">{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Section Informations de la classe -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h2 class="text-lg font-medium text-blue-900 mb-4">🏫 Informations de la classe</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="{{ form.name.id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ form.name.label.text }}
|
||||
</label>
|
||||
{{ form.name(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", placeholder="Ex: 6ème A, 5ème B, Terminale S1...") }}
|
||||
{% if form.name.errors %}
|
||||
<div class="mt-1 text-sm text-red-600">
|
||||
{% for error in form.name.errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="{{ form.year.id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ form.year.label.text }}
|
||||
</label>
|
||||
{{ form.year(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", placeholder="Ex: 2024-2025") }}
|
||||
{% if form.year.errors %}
|
||||
<div class="mt-1 text-sm text-red-600">
|
||||
{% for error in form.year.errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<label for="{{ form.description.id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ form.description.label.text }}
|
||||
<span class="text-gray-500 font-normal">(optionnel)</span>
|
||||
</label>
|
||||
{{ form.description(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", rows="3", placeholder="Description de la classe, remarques particulières...") }}
|
||||
{% if form.description.errors %}
|
||||
<div class="mt-1 text-sm text-red-600">
|
||||
{% for error in form.description.errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions du formulaire -->
|
||||
<div class="flex items-center justify-between pt-6 border-t border-gray-200">
|
||||
<a href="{{ url_for('classes') }}"
|
||||
class="inline-flex items-center px-6 py-3 border border-gray-300 text-base font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 transition-colors">
|
||||
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Annuler
|
||||
</a>
|
||||
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-8 py-3 bg-blue-600 hover:bg-blue-700 text-base font-medium rounded-lg text-white hover:shadow-lg transition-all duration-200">
|
||||
{% if is_edit %}
|
||||
<svg class="w-5 h-5 mr-2" 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>
|
||||
Modifier la classe
|
||||
{% else %}
|
||||
<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>
|
||||
Créer la classe
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript pour validation côté client -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.querySelector('form');
|
||||
const nameField = document.getElementById('{{ form.name.id }}');
|
||||
const yearField = document.getElementById('{{ form.year.id }}');
|
||||
|
||||
// Validation en temps réel du nom de classe
|
||||
nameField.addEventListener('blur', function() {
|
||||
if (this.value.length < 2) {
|
||||
showFieldError(this, 'Le nom de la classe doit contenir au moins 2 caractères');
|
||||
} else {
|
||||
clearFieldError(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Validation du format de l'année scolaire
|
||||
yearField.addEventListener('blur', function() {
|
||||
const yearPattern = /^\d{4}-\d{4}$/;
|
||||
if (!yearPattern.test(this.value)) {
|
||||
showFieldError(this, 'Format attendu: YYYY-YYYY (ex: 2024-2025)');
|
||||
} else {
|
||||
clearFieldError(this);
|
||||
}
|
||||
});
|
||||
|
||||
function showFieldError(field, message) {
|
||||
clearFieldError(field);
|
||||
field.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'text-sm text-red-600 flex items-center mt-1 field-error';
|
||||
errorDiv.innerHTML = `
|
||||
<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 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
${message}
|
||||
`;
|
||||
field.parentNode.appendChild(errorDiv);
|
||||
}
|
||||
|
||||
function clearFieldError(field) {
|
||||
field.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
const existingError = field.parentNode.querySelector('.field-error');
|
||||
if (existingError) {
|
||||
existingError.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Soumission du formulaire avec validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
let hasErrors = false;
|
||||
|
||||
// Validation du nom
|
||||
if (nameField.value.length < 2) {
|
||||
showFieldError(nameField, 'Le nom de la classe est obligatoire');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Validation de l'année
|
||||
const yearPattern = /^\d{4}-\d{4}$/;
|
||||
if (!yearPattern.test(yearField.value)) {
|
||||
showFieldError(yearField, 'Format d\'année invalide (ex: 2024-2025)');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (hasErrors) {
|
||||
e.preventDefault();
|
||||
// Scroll vers le premier champ en erreur
|
||||
const firstError = form.querySelector('.border-red-500');
|
||||
if (firstError) {
|
||||
firstError.focus();
|
||||
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
] %}
|
||||
{% set primary_action = {
|
||||
'url': '#',
|
||||
'url': url_for('classes.new'),
|
||||
'text': 'Nouvelle classe',
|
||||
'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>'
|
||||
} %}
|
||||
@@ -53,12 +53,12 @@
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<button class="inline-flex items-center bg-gradient-to-r from-blue-500 to-green-500 hover:from-blue-600 hover:to-green-600 text-white px-6 py-3 rounded-xl transition-all duration-300 font-semibold shadow-lg hover:shadow-xl transform hover:scale-105">
|
||||
<a href="{{ url_for('classes.new') }}" class="inline-flex items-center bg-gradient-to-r from-blue-500 to-green-500 hover:from-blue-600 hover:to-green-600 text-white px-6 py-3 rounded-xl transition-all duration-300 font-semibold shadow-lg hover:shadow-xl transform hover:scale-105">
|
||||
<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>
|
||||
Créer ma première classe
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<div class="text-sm text-gray-500">
|
||||
<p>💡 <strong>Astuce :</strong> Une classe peut contenir plusieurs élèves et être utilisée pour de nombreuses évaluations</p>
|
||||
@@ -67,4 +67,90 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Modal de confirmation de suppression -->
|
||||
<div id="deleteModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white rounded-xl shadow-xl max-w-md w-full">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="w-12 h-12 text-red-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">Supprimer la classe</h3>
|
||||
<p class="mt-1 text-sm text-gray-600" id="deleteMessage">
|
||||
Êtes-vous sûr de vouloir supprimer cette classe ?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
<button onclick="closeDeleteModal()" class="bg-white hover:bg-gray-50 border border-gray-300 text-gray-700 px-4 py-2 rounded-lg font-medium transition-colors">
|
||||
Annuler
|
||||
</button>
|
||||
<form id="deleteForm" method="POST" class="inline">
|
||||
<button type="submit" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors">
|
||||
Supprimer définitivement
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function confirmDeleteClass(classId, className, studentsCount, assessmentsCount) {
|
||||
const modal = document.getElementById('deleteModal');
|
||||
const message = document.getElementById('deleteMessage');
|
||||
const form = document.getElementById('deleteForm');
|
||||
|
||||
// Configuration du message selon le contenu de la classe
|
||||
if (studentsCount > 0 || assessmentsCount > 0) {
|
||||
message.innerHTML = `
|
||||
<strong>Impossible de supprimer la classe "${className}".</strong><br>
|
||||
Elle contient <strong>${studentsCount} élève(s)</strong> et <strong>${assessmentsCount} évaluation(s)</strong>.<br>
|
||||
Supprimez d'abord ces éléments pour pouvoir supprimer la classe.
|
||||
`;
|
||||
// Masquer le bouton de suppression et changer le titre
|
||||
document.querySelector('#deleteForm button').style.display = 'none';
|
||||
document.querySelector('#deleteModal h3').textContent = 'Suppression impossible';
|
||||
} else {
|
||||
message.innerHTML = `
|
||||
Êtes-vous sûr de vouloir supprimer la classe <strong>"${className}"</strong> ?<br>
|
||||
<span class="text-red-600">Cette action est irréversible.</span>
|
||||
`;
|
||||
// Réafficher le bouton de suppression et restaurer le titre
|
||||
document.querySelector('#deleteForm button').style.display = 'inline';
|
||||
document.querySelector('#deleteModal h3').textContent = 'Supprimer la classe';
|
||||
}
|
||||
|
||||
// Configuration de l'action du formulaire
|
||||
form.action = `/classes/${classId}/delete`;
|
||||
|
||||
// Afficher le modal
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
document.getElementById('deleteModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Fermeture au clic sur le fond
|
||||
document.getElementById('deleteModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Fermeture avec la touche Échap
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeDeleteModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -101,13 +101,22 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Action secondaire -->
|
||||
<div class="pt-2 border-t border-gray-100">
|
||||
<button class="w-full text-gray-600 hover:text-gray-900 text-xs font-medium transition-colors flex items-center justify-center py-2">
|
||||
<!-- Actions secondaires -->
|
||||
<div class="pt-2 border-t border-gray-100 flex gap-2">
|
||||
<a href="{{ url_for('classes.edit', id=class.id) }}"
|
||||
class="flex-1 text-center text-gray-600 hover:text-blue-700 text-xs font-medium transition-colors flex items-center justify-center py-2 hover:bg-blue-50 rounded-lg">
|
||||
<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>
|
||||
Modifier la classe
|
||||
Modifier
|
||||
</a>
|
||||
|
||||
<button onclick="confirmDeleteClass('{{ class.id }}', '{{ class.name }}', {{ class.students|length }}, {{ class.assessments|length }})"
|
||||
class="flex-1 text-center text-gray-500 hover:text-red-700 text-xs font-medium transition-colors flex items-center justify-center py-2 hover:bg-red-50 rounded-lg">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user