feat: add csv import
This commit is contained in:
@@ -122,6 +122,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{# Action VIOLETTE - Import CSV d'élèves #}
|
||||
<a href="javascript:openCSVImportModal()"
|
||||
class="group bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl p-6 hover:from-purple-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-[1.02] shadow-lg hover:shadow-xl">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center mr-4 group-hover:bg-white/30 transition-colors">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM6.293 6.707a1 1 0 010-1.414l3-3a1 1 0 011.414 0l3 3a1 1 0 01-1.414 1.414L11 5.414V13a1 1 0 11-2 0V5.414L7.707 6.707a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold mb-1">Import CSV</h3>
|
||||
<p class="text-sm opacity-90">Ajouter plusieurs élèves</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# 3. Dashboard Statistiques par Trimestre #}
|
||||
@@ -441,6 +457,91 @@
|
||||
</div>
|
||||
</div> <!-- Fermeture max-w-7xl -->
|
||||
|
||||
{# Modal d'import CSV #}
|
||||
<div id="csvImportModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold">Import CSV - {{ class_group.name }}</h3>
|
||||
<button type="button" onclick="closeCSVImportModal()"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg class="w-6 h-6" 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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" 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>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h4 class="text-sm font-medium text-blue-800">Format attendu</h4>
|
||||
<p class="text-sm text-blue-700 mt-1">
|
||||
Fichier CSV avec séparateur <strong>;</strong><br>
|
||||
Première colonne : <strong>"NOM Prénoms"</strong> (ex: "DUPONT Marie Claire")<br>
|
||||
Les élèves déjà existants seront ignorés par défaut.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="csvImportForm" method="post" action="{{ url_for('classes.import_students_csv', id=class_group.id) }}" enctype="multipart/form-data">
|
||||
<div class="mb-4">
|
||||
<label for="csv_file_dashboard" class="block text-sm font-medium text-gray-700 mb-2">Fichier CSV</label>
|
||||
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md hover:border-gray-400 transition-colors">
|
||||
<div class="space-y-1 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div class="flex text-sm text-gray-600">
|
||||
<label for="csv_file_dashboard" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-blue-500">
|
||||
<span>Sélectionner un fichier</span>
|
||||
<input id="csv_file_dashboard" name="csv_file" type="file" accept=".csv" required class="sr-only">
|
||||
</label>
|
||||
<p class="pl-1">ou glisser-déposer</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">CSV jusqu'à 10MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="enrollment_date_csv_dashboard" class="block text-sm font-medium text-gray-700 mb-2">Date d'inscription</label>
|
||||
<input type="date" name="enrollment_date" id="enrollment_date_csv_dashboard" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="skip_duplicates" value="true" checked
|
||||
class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<span class="ml-2 text-sm text-gray-700">Ignorer les élèves déjà existants</span>
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 mt-1">Si décoché, l'import échouera en cas de doublon</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="closeCSVImportModal()"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md transition-colors flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM6.293 6.707a1 1 0 010-1.414l3-3a1 1 0 011.414 0l3 3a1 1 0 01-1.414 1.414L11 5.414V13a1 1 0 11-2 0V5.414L7.707 6.707a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Importer les élèves
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- Fermeture class-dashboard -->
|
||||
|
||||
{% endblock %}
|
||||
@@ -452,6 +553,40 @@
|
||||
window.initialStatsData = {{ stats_data|tojson|safe }};
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/ClassDashboard.js') }}"></script>
|
||||
<script>
|
||||
// Fonctions pour le modal d'import CSV
|
||||
function openCSVImportModal() {
|
||||
document.getElementById('csvImportModal').classList.remove('hidden');
|
||||
document.getElementById('csvImportModal').classList.add('flex');
|
||||
// Initialiser la date d'aujourd'hui
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('enrollment_date_csv_dashboard').value = today;
|
||||
}
|
||||
|
||||
function closeCSVImportModal() {
|
||||
document.getElementById('csvImportModal').classList.add('hidden');
|
||||
document.getElementById('csvImportModal').classList.remove('flex');
|
||||
// Réinitialiser le formulaire
|
||||
document.getElementById('csvImportForm').reset();
|
||||
}
|
||||
|
||||
// Fermer le modal avec Escape ou clic à l'extérieur
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
const modal = document.getElementById('csvImportModal');
|
||||
if (modal && !modal.classList.contains('hidden')) {
|
||||
closeCSVImportModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(event) {
|
||||
const modal = document.getElementById('csvImportModal');
|
||||
if (event.target === modal) {
|
||||
closeCSVImportModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
/* Fix pour éviter le clipping des hover effects sur cette page */
|
||||
.class-dashboard {
|
||||
|
||||
@@ -36,14 +36,27 @@
|
||||
) }}
|
||||
|
||||
{# Breadcrumb Navigation #}
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<a href="{{ url_for('classes.dashboard', id=class_group.id) }}"
|
||||
class="hover:text-blue-600 transition-colors flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Retour au dashboard
|
||||
</a>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<a href="{{ url_for('classes.dashboard', id=class_group.id) }}"
|
||||
class="hover:text-blue-600 transition-colors flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Retour au dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Actions secondaires #}
|
||||
<div class="flex space-x-3">
|
||||
<button onclick="openCSVImportModal()"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM6.293 6.707a1 1 0 010-1.414l3-3a1 1 0 011.414 0l3 3a1 1 0 01-1.414 1.414L11 5.414V13a1 1 0 11-2 0V5.414L7.707 6.707a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Import CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Statistiques d'effectifs #}
|
||||
@@ -456,6 +469,91 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Modal d'import CSV #}
|
||||
<div id="csvImportModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold">Import CSV - {{ class_group.name }}</h3>
|
||||
<button type="button" onclick="closeCSVImportModal()"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg class="w-6 h-6" 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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" 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>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h4 class="text-sm font-medium text-blue-800">Format attendu</h4>
|
||||
<p class="text-sm text-blue-700 mt-1">
|
||||
Fichier CSV avec séparateur <strong>;</strong><br>
|
||||
Première colonne : <strong>"NOM Prénoms"</strong> (ex: "DUPONT Marie Claire")<br>
|
||||
Les élèves déjà existants seront ignorés par défaut.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="csvImportForm" method="post" action="{{ url_for('classes.import_students_csv', id=class_group.id) }}" enctype="multipart/form-data">
|
||||
<div class="mb-4">
|
||||
<label for="csv_file" class="block text-sm font-medium text-gray-700 mb-2">Fichier CSV</label>
|
||||
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md hover:border-gray-400 transition-colors">
|
||||
<div class="space-y-1 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div class="flex text-sm text-gray-600">
|
||||
<label for="csv_file" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-blue-500">
|
||||
<span>Sélectionner un fichier</span>
|
||||
<input id="csv_file" name="csv_file" type="file" accept=".csv" required class="sr-only">
|
||||
</label>
|
||||
<p class="pl-1">ou glisser-déposer</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">CSV jusqu'à 10MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="enrollment_date_csv" class="block text-sm font-medium text-gray-700 mb-2">Date d'inscription</label>
|
||||
<input type="date" name="enrollment_date" id="enrollment_date_csv" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="skip_duplicates" value="true" checked
|
||||
class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<span class="ml-2 text-sm text-gray-700">Ignorer les élèves déjà existants</span>
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 mt-1">Si décoché, l'import échouera en cas de doublon</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="closeCSVImportModal()"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md transition-colors flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM6.293 6.707a1 1 0 010-1.414l3-3a1 1 0 011.414 0l3 3a1 1 0 01-1.414 1.414L11 5.414V13a1 1 0 11-2 0V5.414L7.707 6.707a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Importer les élèves
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialiser la date d'aujourd'hui dans les champs de date
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -463,6 +561,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('enrollment_date').value = today;
|
||||
document.getElementById('transfer_date').value = today;
|
||||
document.getElementById('departure_date').value = today;
|
||||
document.getElementById('enrollment_date_csv').value = today;
|
||||
|
||||
// Vérifier s'il y a eu un rechargement après inscription
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -493,6 +592,23 @@ function closeEnrollModal() {
|
||||
document.getElementById('enrollment_date').value = today;
|
||||
}
|
||||
|
||||
// Gestion du modal d'import CSV
|
||||
function openCSVImportModal() {
|
||||
document.getElementById('csvImportModal').classList.remove('hidden');
|
||||
document.getElementById('csvImportModal').classList.add('flex');
|
||||
// Réinitialiser le formulaire
|
||||
document.getElementById('csvImportForm').reset();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('enrollment_date_csv').value = today;
|
||||
}
|
||||
|
||||
function closeCSVImportModal() {
|
||||
document.getElementById('csvImportModal').classList.add('hidden');
|
||||
document.getElementById('csvImportModal').classList.remove('flex');
|
||||
// Réinitialiser le formulaire
|
||||
document.getElementById('csvImportForm').reset();
|
||||
}
|
||||
|
||||
// Gestion du changement de mode d'inscription
|
||||
function switchEnrollMode(mode) {
|
||||
const existingTab = document.getElementById('existingStudentTab');
|
||||
@@ -581,7 +697,7 @@ function closeCancelDepartureModal() {
|
||||
|
||||
// Fermer les modals en cliquant à l'extérieur
|
||||
document.addEventListener('click', function(event) {
|
||||
const modals = ['enrollModal', 'transferModal', 'departureModal', 'cancelDepartureModal'];
|
||||
const modals = ['enrollModal', 'transferModal', 'departureModal', 'cancelDepartureModal', 'csvImportModal'];
|
||||
modals.forEach(modalId => {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (event.target === modal) {
|
||||
@@ -594,7 +710,7 @@ document.addEventListener('click', function(event) {
|
||||
// Fermer les modals avec la touche Échap
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
const modals = ['enrollModal', 'transferModal', 'departureModal', 'cancelDepartureModal'];
|
||||
const modals = ['enrollModal', 'transferModal', 'departureModal', 'cancelDepartureModal', 'csvImportModal'];
|
||||
modals.forEach(modalId => {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal && !modal.classList.contains('hidden')) {
|
||||
|
||||
Reference in New Issue
Block a user