feat: improve student listing and email editing
This commit is contained in:
@@ -797,6 +797,125 @@ def cancel_departure():
|
|||||||
flash('Erreur lors de l\'annulation du départ', 'error')
|
flash('Erreur lors de l\'annulation du départ', 'error')
|
||||||
return redirect(request.referrer or url_for('classes'))
|
return redirect(request.referrer or url_for('classes'))
|
||||||
|
|
||||||
|
@bp.route('/<int:class_id>/students/<int:student_id>/edit', methods=['POST'])
|
||||||
|
@handle_db_errors
|
||||||
|
def edit_student(class_id, student_id):
|
||||||
|
"""Modification des informations d'un élève."""
|
||||||
|
try:
|
||||||
|
# Vérifier que la classe existe
|
||||||
|
class_repo = ClassRepository()
|
||||||
|
class_group = class_repo.get_or_404(class_id)
|
||||||
|
|
||||||
|
# Vérifier que l'élève existe
|
||||||
|
student = Student.query.get_or_404(student_id)
|
||||||
|
|
||||||
|
# Vérifier que l'élève est bien dans cette classe actuellement
|
||||||
|
current_class = student.get_current_class()
|
||||||
|
if not current_class or current_class.id != class_id:
|
||||||
|
flash("L'élève n'est pas inscrit dans cette classe", 'error')
|
||||||
|
return redirect(url_for('classes.students', id=class_id))
|
||||||
|
|
||||||
|
# Récupérer les données du formulaire
|
||||||
|
first_name = request.form.get('first_name', '').strip()
|
||||||
|
last_name = request.form.get('last_name', '').strip()
|
||||||
|
email = request.form.get('email', '').strip()
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
if not first_name or not last_name:
|
||||||
|
flash('Le prénom et le nom sont obligatoires', 'error')
|
||||||
|
return redirect(url_for('classes.students', id=class_id))
|
||||||
|
|
||||||
|
# Vérifier l'unicité de l'email si fourni
|
||||||
|
if email:
|
||||||
|
existing_student = Student.query.filter_by(email=email).first()
|
||||||
|
if existing_student and existing_student.id != student_id:
|
||||||
|
flash('Un autre élève utilise déjà cet email', 'error')
|
||||||
|
return redirect(url_for('classes.students', id=class_id))
|
||||||
|
|
||||||
|
# Sauvegarder les anciennes valeurs pour le log
|
||||||
|
old_values = {
|
||||||
|
'first_name': student.first_name,
|
||||||
|
'last_name': student.last_name,
|
||||||
|
'email': student.email
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mettre à jour l'élève
|
||||||
|
student.first_name = first_name
|
||||||
|
student.last_name = last_name
|
||||||
|
student.email = email if email else None
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Log de l'opération
|
||||||
|
changes = []
|
||||||
|
if old_values['first_name'] != first_name:
|
||||||
|
changes.append(f"prénom: '{old_values['first_name']}' → '{first_name}'")
|
||||||
|
if old_values['last_name'] != last_name:
|
||||||
|
changes.append(f"nom: '{old_values['last_name']}' → '{last_name}'")
|
||||||
|
if old_values['email'] != (email if email else None):
|
||||||
|
old_email = old_values['email'] or '(vide)'
|
||||||
|
new_email = email or '(vide)'
|
||||||
|
changes.append(f"email: '{old_email}' → '{new_email}'")
|
||||||
|
|
||||||
|
if changes:
|
||||||
|
current_app.logger.info(f'Élève modifié - ID {student_id}: {", ".join(changes)}')
|
||||||
|
|
||||||
|
flash(f'Élève {first_name} {last_name} modifié avec succès', 'success')
|
||||||
|
return redirect(url_for('classes.students', id=class_id) + '?reload=1')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f'Erreur modification élève {student_id}: {e}')
|
||||||
|
flash('Erreur lors de la modification de l\'élève', 'error')
|
||||||
|
return redirect(url_for('classes.students', id=class_id))
|
||||||
|
|
||||||
|
@bp.route('/<int:class_id>/students/<int:student_id>/update-email', methods=['POST'])
|
||||||
|
@handle_db_errors
|
||||||
|
def update_student_email(class_id, student_id):
|
||||||
|
"""Mise à jour de l'email d'un élève (AJAX)."""
|
||||||
|
try:
|
||||||
|
# Vérifier que la classe existe
|
||||||
|
class_repo = ClassRepository()
|
||||||
|
class_group = class_repo.get_or_404(class_id)
|
||||||
|
|
||||||
|
# Vérifier que l'élève existe
|
||||||
|
student = Student.query.get_or_404(student_id)
|
||||||
|
|
||||||
|
# Vérifier que l'élève est bien dans cette classe actuellement
|
||||||
|
current_class = student.get_current_class()
|
||||||
|
if not current_class or current_class.id != class_id:
|
||||||
|
return jsonify({'success': False, 'error': "L'élève n'est pas inscrit dans cette classe"}), 403
|
||||||
|
|
||||||
|
# Récupérer le nouvel email
|
||||||
|
new_email = request.form.get('email', '').strip()
|
||||||
|
|
||||||
|
# Vérifier l'unicité de l'email si fourni
|
||||||
|
if new_email:
|
||||||
|
existing_student = Student.query.filter_by(email=new_email).first()
|
||||||
|
if existing_student and existing_student.id != student_id:
|
||||||
|
return jsonify({'success': False, 'error': 'Un autre élève utilise déjà cet email'}), 400
|
||||||
|
|
||||||
|
# Sauvegarder l'ancienne valeur pour le log
|
||||||
|
old_email = student.email
|
||||||
|
|
||||||
|
# Mettre à jour l'email
|
||||||
|
student.email = new_email if new_email else None
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Log de l'opération
|
||||||
|
current_app.logger.info(f'Email élève modifié - ID {student_id}: "{old_email or "(vide)"}" → "{new_email or "(vide)"}"')
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': f'Email de {student.first_name} {student.last_name} mis à jour',
|
||||||
|
'new_email': new_email or None
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f'Erreur mise à jour email élève {student_id}: {e}')
|
||||||
|
return jsonify({'success': False, 'error': 'Erreur lors de la mise à jour de l\'email'}), 500
|
||||||
|
|
||||||
@bp.route('/<int:id>/import-students-csv', methods=['POST'])
|
@bp.route('/<int:id>/import-students-csv', methods=['POST'])
|
||||||
@handle_db_errors
|
@handle_db_errors
|
||||||
def import_students_csv(id):
|
def import_students_csv(id):
|
||||||
|
|||||||
@@ -59,53 +59,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Statistiques d'effectifs #}
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div class="bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl p-6">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center mr-4">
|
|
||||||
<svg class="w-6 h-6" 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>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-2xl font-bold">{{ stats.total_current }}</h3>
|
|
||||||
<p class="text-sm opacity-90">Élèves actuels</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gradient-to-r from-green-500 to-green-600 text-white rounded-xl p-6">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center mr-4">
|
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-2xl font-bold">{{ stats.recent_arrivals }}</h3>
|
|
||||||
<p class="text-sm opacity-90">Arrivées (30j)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gradient-to-r from-orange-500 to-orange-600 text-white rounded-xl p-6">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center mr-4">
|
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fill-rule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-2xl font-bold">{{ stats.recent_departures }}</h3>
|
|
||||||
<p class="text-sm opacity-90">Départs (30j)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Liste des élèves actuels #}
|
{# Tableau des élèves actuels #}
|
||||||
<div class="bg-white shadow rounded-lg">
|
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||||
<div class="px-6 py-4 border-b border-gray-200">
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
<h2 class="text-xl font-semibold text-gray-900 flex items-center">
|
<h2 class="text-xl font-semibold text-gray-900 flex items-center">
|
||||||
<svg class="w-6 h-6 mr-2 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-6 h-6 mr-2 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
@@ -116,34 +72,192 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_students %}
|
{% if current_students %}
|
||||||
<div class="divide-y divide-gray-200">
|
{# Version desktop/tablette : tableau complet #}
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Élève
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Inscription
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
{% for student in current_students %}
|
||||||
|
{% set enrollment = student.get_current_enrollment() %}
|
||||||
|
<tr class="hover:bg-gray-50 transition-colors">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<span class="text-sm font-medium text-blue-600">{{ student.first_name[0] }}{{ student.last_name[0] }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-900">{{ student.first_name }} {{ student.last_name }}</div>
|
||||||
|
<div class="text-sm text-gray-500">ID: {{ student.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="flex items-center group">
|
||||||
|
<!-- Vue normale de l'email -->
|
||||||
|
<div id="email-display-{{ student.id }}" class="flex items-center">
|
||||||
|
{% if student.email %}
|
||||||
|
<div class="text-sm text-gray-900">{{ student.email }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-sm text-gray-400 italic">Cliquez pour ajouter</span>
|
||||||
|
<svg class="w-4 h-4 ml-2 text-orange-400" 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>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulaire d'édition inline (caché par défaut) -->
|
||||||
|
<div id="email-edit-{{ student.id }}" class="hidden">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input type="email"
|
||||||
|
id="email-input-{{ student.id }}"
|
||||||
|
value="{{ student.email or '' }}"
|
||||||
|
placeholder="exemple@etablissement.fr"
|
||||||
|
class="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-48">
|
||||||
|
<button onclick="saveEmail({{ student.id }})"
|
||||||
|
class="text-green-600 hover:text-green-700 p-1"
|
||||||
|
title="Enregistrer">
|
||||||
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button onclick="cancelEmailEdit({{ student.id }})"
|
||||||
|
class="text-gray-400 hover:text-gray-600 p-1"
|
||||||
|
title="Annuler">
|
||||||
|
<svg class="w-4 h-4" 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>
|
||||||
|
|
||||||
|
<!-- Bouton d'édition (stylo) - visible au survol -->
|
||||||
|
<button id="email-edit-btn-{{ student.id }}"
|
||||||
|
onclick="startEmailEdit({{ student.id }})"
|
||||||
|
class="ml-2 text-gray-400 hover:text-blue-600 p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="Modifier l'email">
|
||||||
|
<svg class="w-4 h-4" 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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
{% if enrollment %}
|
||||||
|
<div class="text-sm text-gray-900">{{ enrollment.enrollment_date.strftime('%d/%m/%Y') }}</div>
|
||||||
|
{% if enrollment.enrollment_reason %}
|
||||||
|
<div class="text-xs text-gray-500">{{ enrollment.enrollment_reason }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<div class="flex justify-end space-x-2">
|
||||||
|
<button onclick="transferStudent({{ student.id }}, '{{ student.first_name }} {{ student.last_name }}')"
|
||||||
|
class="bg-blue-100 hover:bg-blue-200 text-blue-700 px-3 py-1 rounded text-xs transition-colors">
|
||||||
|
Transférer
|
||||||
|
</button>
|
||||||
|
<button onclick="departureStudent({{ student.id }}, '{{ student.first_name }} {{ student.last_name }}')"
|
||||||
|
class="bg-orange-100 hover:bg-orange-200 text-orange-700 px-3 py-1 rounded text-xs transition-colors">
|
||||||
|
Départ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Version mobile : cards compactes #}
|
||||||
|
<div class="md:hidden divide-y divide-gray-200">
|
||||||
{% for student in current_students %}
|
{% for student in current_students %}
|
||||||
{% set enrollment = student.get_current_enrollment() %}
|
{% set enrollment = student.get_current_enrollment() %}
|
||||||
<div class="px-6 py-4 flex items-center justify-between hover:bg-gray-50">
|
<div class="px-4 py-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-start justify-between">
|
||||||
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-4">
|
<div class="flex items-start">
|
||||||
<span class="text-sm font-medium text-blue-600">{{ student.first_name[0] }}{{ student.last_name[0] }}</span>
|
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">
|
||||||
</div>
|
<span class="text-xs font-medium text-blue-600">{{ student.first_name[0] }}{{ student.last_name[0] }}</span>
|
||||||
<div>
|
</div>
|
||||||
<div class="text-lg font-medium text-gray-900">{{ student.first_name }} {{ student.last_name }}</div>
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-sm text-gray-500">
|
<div class="text-sm font-medium text-gray-900 truncate">{{ student.first_name }} {{ student.last_name }}</div>
|
||||||
|
<div class="mt-1 group">
|
||||||
|
<!-- Vue normale de l'email mobile -->
|
||||||
|
<div id="email-display-mobile-{{ student.id }}" class="flex items-center">
|
||||||
|
{% if student.email %}
|
||||||
|
<div class="text-xs text-gray-600">{{ student.email }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-xs text-gray-400 italic flex items-center">
|
||||||
|
<svg class="w-3 h-3 mr-1 text-orange-400" 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>
|
||||||
|
Cliquez pour ajouter
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<button onclick="startEmailEdit({{ student.id }})"
|
||||||
|
class="ml-2 text-gray-400 hover:text-blue-600 p-1"
|
||||||
|
title="Modifier l'email">
|
||||||
|
<svg class="w-3 h-3" 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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulaire d'édition inline mobile (caché par défaut) -->
|
||||||
|
<div id="email-edit-mobile-{{ student.id }}" class="hidden mt-1">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input type="email"
|
||||||
|
id="email-input-mobile-{{ student.id }}"
|
||||||
|
value="{{ student.email or '' }}"
|
||||||
|
placeholder="exemple@etablissement.fr"
|
||||||
|
class="text-xs border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent flex-1">
|
||||||
|
<button onclick="saveEmail({{ student.id }})"
|
||||||
|
class="text-green-600 hover:text-green-700 p-1"
|
||||||
|
title="Enregistrer">
|
||||||
|
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button onclick="cancelEmailEdit({{ student.id }})"
|
||||||
|
class="text-gray-400 hover:text-gray-600 p-1"
|
||||||
|
title="Annuler">
|
||||||
|
<svg class="w-3 h-3" 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>
|
||||||
|
</div>
|
||||||
{% if enrollment %}
|
{% if enrollment %}
|
||||||
Inscrit depuis le {{ enrollment.enrollment_date.strftime('%d/%m/%Y') }}
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
{% if enrollment.enrollment_reason %}
|
Inscrit le {{ enrollment.enrollment_date.strftime('%d/%m/%Y') }}
|
||||||
({{ enrollment.enrollment_reason }})
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
<div class="flex space-x-2">
|
|
||||||
<button onclick="transferStudent({{ student.id }}, '{{ student.first_name }} {{ student.last_name }}')"
|
<button onclick="transferStudent({{ student.id }}, '{{ student.first_name }} {{ student.last_name }}')"
|
||||||
class="bg-blue-100 hover:bg-blue-200 text-blue-700 px-3 py-1 rounded text-sm transition-colors">
|
class="bg-blue-100 hover:bg-blue-200 text-blue-700 px-2 py-1 rounded text-xs transition-colors">
|
||||||
Transférer
|
Transférer
|
||||||
</button>
|
</button>
|
||||||
<button onclick="departureStudent({{ student.id }}, '{{ student.first_name }} {{ student.last_name }}')"
|
<button onclick="departureStudent({{ student.id }}, '{{ student.first_name }} {{ student.last_name }}')"
|
||||||
class="bg-orange-100 hover:bg-orange-200 text-orange-700 px-3 py-1 rounded text-sm transition-colors">
|
class="bg-orange-100 hover:bg-orange-200 text-orange-700 px-2 py-1 rounded text-xs transition-colors">
|
||||||
Départ
|
Départ
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -469,6 +583,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{# Modal d'import CSV #}
|
{# 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 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="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
|
||||||
@@ -707,6 +822,209 @@ document.addEventListener('click', function(event) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gestion de l'édition inline des emails
|
||||||
|
function startEmailEdit(studentId) {
|
||||||
|
// Masquer l'affichage normal
|
||||||
|
const displayDesktop = document.getElementById(`email-display-${studentId}`);
|
||||||
|
const displayMobile = document.getElementById(`email-display-mobile-${studentId}`);
|
||||||
|
const editDesktop = document.getElementById(`email-edit-${studentId}`);
|
||||||
|
const editMobile = document.getElementById(`email-edit-mobile-${studentId}`);
|
||||||
|
const editBtn = document.getElementById(`email-edit-btn-${studentId}`);
|
||||||
|
|
||||||
|
if (displayDesktop) {
|
||||||
|
displayDesktop.classList.add('hidden');
|
||||||
|
editDesktop.classList.remove('hidden');
|
||||||
|
editBtn.classList.add('hidden');
|
||||||
|
|
||||||
|
// Focus sur l'input
|
||||||
|
const input = document.getElementById(`email-input-${studentId}`);
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayMobile) {
|
||||||
|
displayMobile.classList.add('hidden');
|
||||||
|
editMobile.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Focus sur l'input mobile
|
||||||
|
const inputMobile = document.getElementById(`email-input-mobile-${studentId}`);
|
||||||
|
if (inputMobile) {
|
||||||
|
inputMobile.focus();
|
||||||
|
inputMobile.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEmailEdit(studentId) {
|
||||||
|
// Restaurer l'affichage normal
|
||||||
|
const displayDesktop = document.getElementById(`email-display-${studentId}`);
|
||||||
|
const displayMobile = document.getElementById(`email-display-mobile-${studentId}`);
|
||||||
|
const editDesktop = document.getElementById(`email-edit-${studentId}`);
|
||||||
|
const editMobile = document.getElementById(`email-edit-mobile-${studentId}`);
|
||||||
|
const editBtn = document.getElementById(`email-edit-btn-${studentId}`);
|
||||||
|
|
||||||
|
if (displayDesktop) {
|
||||||
|
displayDesktop.classList.remove('hidden');
|
||||||
|
editDesktop.classList.add('hidden');
|
||||||
|
editBtn.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayMobile) {
|
||||||
|
displayMobile.classList.remove('hidden');
|
||||||
|
editMobile.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEmail(studentId) {
|
||||||
|
// Récupérer la nouvelle valeur
|
||||||
|
let newEmail = '';
|
||||||
|
const inputDesktop = document.getElementById(`email-input-${studentId}`);
|
||||||
|
const inputMobile = document.getElementById(`email-input-mobile-${studentId}`);
|
||||||
|
|
||||||
|
// Vérifier quel input est actuellement visible/actif
|
||||||
|
const editDesktop = document.getElementById(`email-edit-${studentId}`);
|
||||||
|
const editMobile = document.getElementById(`email-edit-mobile-${studentId}`);
|
||||||
|
|
||||||
|
if (editDesktop && !editDesktop.classList.contains('hidden') && inputDesktop) {
|
||||||
|
newEmail = inputDesktop.value.trim();
|
||||||
|
console.log('Desktop email value:', newEmail);
|
||||||
|
} else if (editMobile && !editMobile.classList.contains('hidden') && inputMobile) {
|
||||||
|
newEmail = inputMobile.value.trim();
|
||||||
|
console.log('Mobile email value:', newEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final email to save:', newEmail);
|
||||||
|
|
||||||
|
// Validation basique
|
||||||
|
if (newEmail && !isValidEmail(newEmail)) {
|
||||||
|
alert('Veuillez saisir un email valide');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envoyer la requête AJAX
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('email', newEmail);
|
||||||
|
|
||||||
|
fetch(`/classes/{{ class_group.id }}/students/${studentId}/update-email`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Mettre à jour l'affichage
|
||||||
|
updateEmailDisplay(studentId, newEmail);
|
||||||
|
cancelEmailEdit(studentId);
|
||||||
|
|
||||||
|
// Afficher un message de succès discret
|
||||||
|
showToast('Email mis à jour avec succès', 'success');
|
||||||
|
} else {
|
||||||
|
alert('Erreur: ' + (data.error || 'Erreur lors de la mise à jour'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erreur:', error);
|
||||||
|
alert('Erreur lors de la mise à jour de l\'email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEmailDisplay(studentId, newEmail) {
|
||||||
|
// Mettre à jour l'affichage desktop
|
||||||
|
const displayDesktop = document.getElementById(`email-display-${studentId}`);
|
||||||
|
if (displayDesktop) {
|
||||||
|
if (newEmail) {
|
||||||
|
displayDesktop.innerHTML = `<div class="text-sm text-gray-900">${newEmail}</div>`;
|
||||||
|
} else {
|
||||||
|
displayDesktop.innerHTML = `
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-sm text-gray-400 italic">Cliquez pour ajouter</span>
|
||||||
|
<svg class="w-4 h-4 ml-2 text-orange-400" 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour l'affichage mobile
|
||||||
|
const displayMobile = document.getElementById(`email-display-mobile-${studentId}`);
|
||||||
|
if (displayMobile) {
|
||||||
|
if (newEmail) {
|
||||||
|
displayMobile.innerHTML = `
|
||||||
|
<div class="text-xs text-gray-600">${newEmail}</div>
|
||||||
|
<button onclick="startEmailEdit(${studentId})"
|
||||||
|
class="ml-2 text-gray-400 hover:text-blue-600 p-1"
|
||||||
|
title="Modifier l'email">
|
||||||
|
<svg class="w-3 h-3" 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>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
displayMobile.innerHTML = `
|
||||||
|
<div class="text-xs text-gray-400 italic flex items-center">
|
||||||
|
<svg class="w-3 h-3 mr-1 text-orange-400" 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>
|
||||||
|
Cliquez pour ajouter
|
||||||
|
</div>
|
||||||
|
<button onclick="startEmailEdit(${studentId})"
|
||||||
|
class="ml-2 text-gray-400 hover:text-blue-600 p-1"
|
||||||
|
title="Modifier l'email">
|
||||||
|
<svg class="w-3 h-3" 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>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour la valeur dans les inputs pour la prochaine édition
|
||||||
|
const inputDesktop = document.getElementById(`email-input-${studentId}`);
|
||||||
|
const inputMobile = document.getElementById(`email-input-mobile-${studentId}`);
|
||||||
|
if (inputDesktop) inputDesktop.value = newEmail;
|
||||||
|
if (inputMobile) inputMobile.value = newEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidEmail(email) {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = 'success') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
const bgColor = type === 'success' ? 'bg-green-500' : 'bg-red-500';
|
||||||
|
toast.className = `fixed top-4 right-4 ${bgColor} text-white px-4 py-2 rounded-lg shadow-lg z-50 transition-opacity`;
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// Fade out après 3 secondes
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(toast);
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion des touches Entrée et Échap dans les inputs d'email
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (event.target.type === 'email' && event.target.id.startsWith('email-input')) {
|
||||||
|
const studentId = event.target.id.match(/\d+/)[0];
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
saveEmail(studentId);
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
cancelEmailEdit(studentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Fermer les modals avec la touche Échap
|
// Fermer les modals avec la touche Échap
|
||||||
document.addEventListener('keydown', function(event) {
|
document.addEventListener('keydown', function(event) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
|
|||||||
Reference in New Issue
Block a user