feat(class): improve class/id/student
All checks were successful
Build and Publish Docker Images / Build Backend Image (push) Successful in 3m1s
Build and Publish Docker Images / Build Frontend Image (push) Successful in 3m0s
Build and Publish Docker Images / Build Summary (push) Successful in 3s

This commit is contained in:
2025-12-03 06:32:16 +01:00
parent ab86bbb2e1
commit 08c8ee4931
6 changed files with 656 additions and 20 deletions

View File

@@ -41,7 +41,7 @@ from schemas.class_group import (
)
from domain.services.grading_calculator import GradingCalculator
from domain.services.class_statistics_service import ClassStatisticsService
from schemas.student import StudentWithClass, StudentList
from schemas.student import StudentWithClass, StudentList, StudentWithEnrollmentInfo, StudentEnrollmentList
from schemas.csv_import import (
CSVImportResponse,
ImportedStudentInfo,
@@ -145,7 +145,7 @@ async def get_class(
)
@router.get("/{class_id}/students", response_model=StudentList)
@router.get("/{class_id}/students", response_model=StudentEnrollmentList)
async def get_class_students(
class_id: int,
session: AsyncSessionDep,
@@ -153,7 +153,7 @@ async def get_class_students(
at_date: Optional[str] = Query(None, description="Filtrer les élèves inscrits à cette date (YYYY-MM-DD)"),
):
"""
Récupère la liste des étudiants d'une classe.
Récupère la liste des étudiants d'une classe avec leurs informations d'inscription.
Si at_date est fourni, retourne uniquement les élèves qui étaient inscrits à cette date.
"""
@@ -194,22 +194,29 @@ async def get_class_students(
students = []
for enrollment in enrollments:
student = enrollment.student
is_active = enrollment.departure_date is None
students.append(
StudentWithClass(
StudentWithEnrollmentInfo(
id=student.id,
last_name=student.last_name,
first_name=student.first_name,
email=student.email,
full_name=f"{student.first_name} {student.last_name}",
current_class_id=class_id if enrollment.departure_date is None else None,
current_class_name=cls.name if enrollment.departure_date is None else None
current_class_id=class_id if is_active else None,
current_class_name=cls.name if is_active else None,
enrollment_id=enrollment.id,
enrollment_date=enrollment.enrollment_date,
departure_date=enrollment.departure_date,
enrollment_reason=enrollment.enrollment_reason,
departure_reason=enrollment.departure_reason,
is_active=is_active
)
)
# Trier par nom de famille puis prénom
students.sort(key=lambda s: (s.last_name.lower(), s.first_name.lower()))
return StudentList(
return StudentEnrollmentList(
students=students,
total=len(students)
)

View File

@@ -281,6 +281,67 @@ async def update_student(
)
@router.patch("/{student_id}/email", response_model=StudentWithClass)
async def update_student_email(
student_id: int,
email: str,
session: AsyncSessionDep,
):
"""
Modifie rapidement l'email d'un étudiant.
Endpoint optimisé pour l'édition inline.
"""
# Récupérer l'étudiant
query = (
select(Student)
.options(
selectinload(Student.enrollments).selectinload(StudentEnrollment.class_group)
)
.where(Student.id == student_id)
)
result = await session.execute(query)
student = result.scalar_one_or_none()
if not student:
raise HTTPException(status_code=404, detail="Étudiant non trouvé")
# Vérifier l'unicité du nouvel email si fourni
if email and email != student.email:
existing_query = select(Student).where(
Student.email == email,
Student.id != student_id
)
existing_result = await session.execute(existing_query)
if existing_result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail=f"Un autre élève avec l'email '{email}' existe déjà"
)
# Mettre à jour l'email (permet de le vider avec chaîne vide)
student.email = email if email else None
await session.commit()
await session.refresh(student)
# Trouver la classe actuelle
current_enrollment = None
for enrollment in student.enrollments:
if enrollment.departure_date is None:
current_enrollment = enrollment
break
return StudentWithClass(
id=student.id,
last_name=student.last_name,
first_name=student.first_name,
email=student.email,
full_name=f"{student.first_name} {student.last_name}",
current_class_id=current_enrollment.class_group_id if current_enrollment else None,
current_class_name=current_enrollment.class_group.name if current_enrollment else None
)
@router.delete("/{student_id}", status_code=204)
async def delete_student(
student_id: int,

View File

@@ -36,6 +36,19 @@ class StudentWithClass(StudentRead):
current_class_name: Optional[str] = None
class StudentWithEnrollmentInfo(StudentRead):
"""Schema avec les informations d'inscription complètes."""
current_class_id: Optional[int] = None
current_class_name: Optional[str] = None
enrollment_id: Optional[int] = None
enrollment_date: Optional[date] = None
departure_date: Optional[date] = None
enrollment_reason: Optional[str] = None
departure_reason: Optional[str] = None
is_active: bool = True
class StudentDetail(StudentWithClass):
"""Schema détaillé avec historique d'inscriptions."""
@@ -49,6 +62,13 @@ class StudentList(BaseSchema):
total: int
class StudentEnrollmentList(BaseSchema):
"""Schema pour la liste des étudiants avec informations d'inscription."""
students: List[StudentWithEnrollmentInfo]
total: int
# Schemas pour les inscriptions temporelles
class EnrollmentBase(BaseSchema):
"""Schema de base pour StudentEnrollment."""

View File

@@ -23,8 +23,12 @@ export const classesService = {
// Get class students
// atDate: optional date string (YYYY-MM-DD) to filter students enrolled at that date
async getStudents(id, atDate = null) {
const params = atDate ? { at_date: atDate } : {}
// includeDeparted: include students who have left the class
async getStudents(id, atDate = null, includeDeparted = false) {
const params = {}
if (atDate) params.at_date = atDate
if (includeDeparted) params.include_departed = true
const response = await api.get(`/classes/${id}/students`, { params })
// API returns { students: [...], total: N }
return response.data.students || []

View File

@@ -27,6 +27,14 @@ export const studentsService = {
return response.data
},
// Update student email only (fast endpoint)
async updateEmail(id, email) {
const response = await api.patch(`/students/${id}/email`, null, {
params: { email }
})
return response.data
},
// Delete student
async delete(id) {
const response = await api.delete(`/students/${id}`)

View File

@@ -3,16 +3,29 @@
<LoadingSpinner v-if="loading" text="Chargement..." fullPage />
<template v-else>
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-2xl font-bold">{{ classData?.name }}</h1>
<p class="text-gray-500">{{ students.length }} élève(s)</p>
<p class="text-gray-500">{{ activeStudentsCount }} élève(s) actif(s)</p>
</div>
<div class="flex gap-3">
<label class="flex items-center gap-2 px-4 py-2 bg-gray-100 rounded-lg cursor-pointer hover:bg-gray-200 transition-colors">
<input
type="checkbox"
v-model="includeDeparted"
@change="loadStudents"
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span class="text-sm font-medium text-gray-700">Inclure les élèves partis</span>
</label>
<button @click="showAddModal = true" class="btn btn-primary">
+ Ajouter un élève
</button>
</div>
<button @click="showAddModal = true" class="btn btn-primary">
+ Ajouter un élève
</button>
</div>
<!-- Students table -->
<div class="card overflow-hidden">
<table class="table">
<thead>
@@ -20,54 +33,577 @@
<th>Nom</th>
<th>Prénom</th>
<th>Email</th>
<th>Statut</th>
<th>Inscrit le</th>
<th>Parti le</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="student in students" :key="student.id">
<tr v-for="student in students" :key="student.id" :class="{ 'bg-gray-50': !student.is_active }">
<!-- Nom -->
<td class="font-medium">{{ student.last_name }}</td>
<!-- Prénom -->
<td>{{ student.first_name }}</td>
<td class="text-gray-500">{{ student.email || '-' }}</td>
<!-- Email with inline editing -->
<td>
<div class="flex items-center gap-2 group">
<template v-if="editingEmailId === student.id">
<input
ref="emailInput"
v-model="editingEmailValue"
@blur="saveEmail(student)"
@keyup.enter="saveEmail(student)"
@keyup.escape="cancelEditEmail"
type="email"
class="flex-1 px-2 py-1 border border-indigo-300 rounded focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="email@exemple.com"
/>
<button @click="cancelEditEmail" class="text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</template>
<template v-else>
<span class="flex-1 text-gray-600">{{ student.email || '-' }}</span>
<button
@click="startEditEmail(student)"
class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-indigo-600 transition-opacity"
title="Modifier l'email"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</button>
<!-- Feedback icons -->
<span v-if="emailFeedback[student.id] === 'success'" class="text-green-500">
<svg class="w-4 h-4" 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>
<span v-if="emailFeedback[student.id] === 'error'" class="text-red-500">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</span>
</template>
</div>
</td>
<!-- Statut -->
<td>
<span
v-if="student.is_active"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
>
Actif
</span>
<span
v-else
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800"
>
Parti
</span>
</td>
<!-- Inscrit le -->
<td class="text-gray-500">{{ formatDate(student.enrollment_date) }}</td>
<!-- Parti le -->
<td class="text-gray-500">{{ formatDate(student.departure_date) }}</td>
<!-- Actions -->
<td class="text-right">
<button
v-if="student.is_active"
@click="openDepartureModal(student)"
class="text-sm text-red-600 hover:text-red-800 font-medium"
>
Sortir de la classe
</button>
<button
v-else
@click="openReenrollModal(student)"
class="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
>
Réinscrire
</button>
</td>
</tr>
</tbody>
</table>
<!-- Empty state -->
<div v-if="students.length === 0" class="p-8 text-center text-gray-500">
Aucun élève trouvé
</div>
</div>
</template>
<!-- Add modal placeholder -->
<Modal v-model="showAddModal" title="Ajouter un élève">
<p class="text-gray-500">Formulaire d'ajout d'élève (à implémenter)</p>
<!-- Add/Enroll Student Modal -->
<Modal v-model="showAddModal" title="Ajouter un élève" size="large">
<div class="space-y-4">
<!-- Tabs -->
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<button
@click="addMode = 'new'"
:class="[
'py-4 px-1 border-b-2 font-medium text-sm',
addMode === 'new'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
>
Nouvel élève
</button>
<button
@click="addMode = 'existing'"
:class="[
'py-4 px-1 border-b-2 font-medium text-sm',
addMode === 'existing'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
>
Élève existant
</button>
</nav>
</div>
<!-- New Student Form -->
<div v-if="addMode === 'new'" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nom *</label>
<input
v-model="newStudent.last_name"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Prénom *</label>
<input
v-model="newStudent.first_name"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input
v-model="newStudent.email"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date d'inscription *</label>
<input
v-model="newStudent.enrollment_date"
type="date"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Raison</label>
<input
v-model="newStudent.enrollment_reason"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="Ex: Nouvelle inscription"
/>
</div>
</div>
<!-- Existing Student Form -->
<div v-if="addMode === 'existing'" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Élève *</label>
<select
v-model="existingStudent.student_id"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option :value="null">Sélectionner un élève...</option>
<option v-for="student in availableStudents" :key="student.id" :value="student.id">
{{ student.last_name }} {{ student.first_name }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date d'inscription *</label>
<input
v-model="existingStudent.enrollment_date"
type="date"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Raison</label>
<input
v-model="existingStudent.enrollment_reason"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="Ex: Transfert depuis..."
/>
</div>
</div>
<!-- Actions -->
<div class="flex justify-end gap-3 pt-4">
<button
@click="showAddModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Annuler
</button>
<button
@click="enrollStudent"
:disabled="!canEnroll"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Inscrire
</button>
</div>
</div>
</Modal>
<!-- Departure Modal -->
<Modal v-model="showDepartureModal" title="Sortir un élève de la classe">
<div class="space-y-4">
<p class="text-sm text-gray-600">
Vous êtes sur le point de marquer <strong>{{ departureStudent?.first_name }} {{ departureStudent?.last_name }}</strong> comme parti(e) de la classe.
</p>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date de départ *</label>
<input
v-model="departureDate"
type="date"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Raison</label>
<input
v-model="departureReason"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="Ex: Déménagement, transfert..."
/>
</div>
<div class="flex justify-end gap-3 pt-4">
<button
@click="showDepartureModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Annuler
</button>
<button
@click="confirmDeparture"
:disabled="!departureDate"
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Confirmer le départ
</button>
</div>
</div>
</Modal>
<!-- Re-enroll Modal -->
<Modal v-model="showReenrollModal" title="Réinscrire un élève">
<div class="space-y-4">
<p class="text-sm text-gray-600">
Réinscrire <strong>{{ reenrollStudent?.first_name }} {{ reenrollStudent?.last_name }}</strong> dans la classe.
</p>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date de réinscription *</label>
<input
v-model="reenrollDate"
type="date"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Raison</label>
<input
v-model="reenrollReason"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="Ex: Retour après absence..."
/>
</div>
<div class="flex justify-end gap-3 pt-4">
<button
@click="showReenrollModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Annuler
</button>
<button
@click="confirmReenroll"
:disabled="!reenrollDate"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Réinscrire
</button>
</div>
</div>
</Modal>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { useClassesStore } from '@/stores/classes'
import { useNotificationsStore } from '@/stores/notifications'
import classesService from '@/services/classes'
import studentsService from '@/services/students'
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
import Modal from '@/components/common/Modal.vue'
const route = useRoute()
const classesStore = useClassesStore()
const notifications = useNotificationsStore()
const loading = ref(true)
const classData = ref(null)
const students = ref([])
const includeDeparted = ref(false)
// Email editing
const editingEmailId = ref(null)
const editingEmailValue = ref('')
const emailFeedback = ref({})
const emailInput = ref(null)
// Add/Enroll student
const showAddModal = ref(false)
const addMode = ref('new')
const availableStudents = ref([])
const newStudent = ref({
last_name: '',
first_name: '',
email: '',
enrollment_date: new Date().toISOString().split('T')[0],
enrollment_reason: ''
})
const existingStudent = ref({
student_id: null,
enrollment_date: new Date().toISOString().split('T')[0],
enrollment_reason: ''
})
// Departure
const showDepartureModal = ref(false)
const departureStudent = ref(null)
const departureDate = ref(new Date().toISOString().split('T')[0])
const departureReason = ref('')
// Re-enroll
const showReenrollModal = ref(false)
const reenrollStudent = ref(null)
const reenrollDate = ref(new Date().toISOString().split('T')[0])
const reenrollReason = ref('')
const activeStudentsCount = computed(() => {
return students.value.filter(s => s.is_active).length
})
const canEnroll = computed(() => {
if (addMode.value === 'new') {
return newStudent.value.last_name && newStudent.value.first_name && newStudent.value.enrollment_date
} else {
return existingStudent.value.student_id && existingStudent.value.enrollment_date
}
})
function formatDate(dateStr) {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleDateString('fr-FR')
}
async function loadStudents() {
try {
const id = route.params.id
students.value = await classesService.getStudents(id, null, includeDeparted.value)
} catch (error) {
notifications.error('Erreur lors du chargement des élèves')
console.error(error)
}
}
async function loadAvailableStudents() {
try {
const allStudents = await studentsService.getAll()
// Filter students without active enrollment
availableStudents.value = allStudents.filter(s => !s.current_class_id)
} catch (error) {
notifications.error('Erreur lors du chargement des élèves disponibles')
console.error(error)
}
}
// Email editing functions
function startEditEmail(student) {
editingEmailId.value = student.id
editingEmailValue.value = student.email || ''
nextTick(() => {
emailInput.value?.[0]?.focus()
})
}
function cancelEditEmail() {
editingEmailId.value = null
editingEmailValue.value = ''
}
async function saveEmail(student) {
if (editingEmailValue.value === student.email) {
cancelEditEmail()
return
}
try {
await studentsService.updateEmail(student.id, editingEmailValue.value)
student.email = editingEmailValue.value
// Show success feedback
emailFeedback.value[student.id] = 'success'
setTimeout(() => {
emailFeedback.value[student.id] = null
}, 2000)
cancelEditEmail()
} catch (error) {
// Show error feedback
emailFeedback.value[student.id] = 'error'
setTimeout(() => {
emailFeedback.value[student.id] = null
}, 2000)
notifications.error(error.response?.data?.detail || 'Erreur lors de la mise à jour de l\'email')
console.error(error)
}
}
// Enroll student
async function enrollStudent() {
try {
const classId = parseInt(route.params.id)
if (addMode.value === 'new') {
await studentsService.enroll({
first_name: newStudent.value.first_name,
last_name: newStudent.value.last_name,
email: newStudent.value.email || null,
class_group_id: classId,
enrollment_date: newStudent.value.enrollment_date,
enrollment_reason: newStudent.value.enrollment_reason || null
})
notifications.success('Élève créé et inscrit avec succès')
} else {
await studentsService.enroll({
student_id: existingStudent.value.student_id,
class_group_id: classId,
enrollment_date: existingStudent.value.enrollment_date,
enrollment_reason: existingStudent.value.enrollment_reason || null
})
notifications.success('Élève inscrit avec succès')
}
// Reset forms
newStudent.value = {
last_name: '',
first_name: '',
email: '',
enrollment_date: new Date().toISOString().split('T')[0],
enrollment_reason: ''
}
existingStudent.value = {
student_id: null,
enrollment_date: new Date().toISOString().split('T')[0],
enrollment_reason: ''
}
showAddModal.value = false
await loadStudents()
} catch (error) {
notifications.error(error.response?.data?.detail || 'Erreur lors de l\'inscription')
console.error(error)
}
}
// Departure functions
function openDepartureModal(student) {
departureStudent.value = student
departureDate.value = new Date().toISOString().split('T')[0]
departureReason.value = ''
showDepartureModal.value = true
}
async function confirmDeparture() {
try {
await studentsService.departure({
student_id: departureStudent.value.id,
departure_date: departureDate.value,
departure_reason: departureReason.value || null
})
notifications.success('Départ enregistré avec succès')
showDepartureModal.value = false
await loadStudents()
} catch (error) {
notifications.error(error.response?.data?.detail || 'Erreur lors de l\'enregistrement du départ')
console.error(error)
}
}
// Re-enroll functions
function openReenrollModal(student) {
reenrollStudent.value = student
reenrollDate.value = new Date().toISOString().split('T')[0]
reenrollReason.value = ''
showReenrollModal.value = true
}
async function confirmReenroll() {
try {
const classId = parseInt(route.params.id)
await studentsService.enroll({
student_id: reenrollStudent.value.id,
class_group_id: classId,
enrollment_date: reenrollDate.value,
enrollment_reason: reenrollReason.value || null
})
notifications.success('Élève réinscrit avec succès')
showReenrollModal.value = false
await loadStudents()
} catch (error) {
notifications.error(error.response?.data?.detail || 'Erreur lors de la réinscription')
console.error(error)
}
}
onMounted(async () => {
try {
const id = route.params.id
classData.value = await classesStore.fetchClass(id)
students.value = await classesService.getStudents(id)
await loadStudents()
await loadAvailableStudents()
} finally {
loading.value = false
}