diff --git a/Back-End/School/management/commands/init_mock_datas.py b/Back-End/School/management/commands/init_mock_datas.py index fe69619..48d026a 100644 --- a/Back-End/School/management/commands/init_mock_datas.py +++ b/Back-End/School/management/commands/init_mock_datas.py @@ -44,6 +44,8 @@ from Auth.serializers import ProfileSerializer, ProfileRoleSerializer from Establishment.serializers import EstablishmentSerializer from Subscriptions.serializers import RegistrationFormSerializer, StudentSerializer +from Subscriptions.util import getCurrentSchoolYear, getNextSchoolYear # Import des fonctions nécessaires + # Définir le chemin vers le dossier mock_datas MOCK_DATAS_PATH = os.path.join(settings.BASE_DIR, 'School', 'management', 'mock_datas') @@ -371,7 +373,7 @@ class Command(BaseCommand): "first_name": fake.first_name(), "birth_date": fake.date_of_birth().strftime('%Y-%m-%d'), "address": fake.address(), - "phone":"+33122334455", + "phone": "+33122334455", "profession": fake.job() } @@ -403,11 +405,15 @@ class Command(BaseCommand): fees = Fee.objects.filter(id__in=[1, 2, 3, 4]) discounts = Discount.objects.filter(id__in=[1]) + # Déterminer l'année scolaire (soit l'année en cours, soit l'année prochaine) + school_year = random.choice([getCurrentSchoolYear(), getNextSchoolYear()]) + # Créer les données du formulaire d'inscription register_form_data = { "fileGroup": RegistrationFileGroup.objects.get(id=fake.random_int(min=1, max=file_group_count)), "establishment": profile_role.establishment, - "status": fake.random_int(min=1, max=3) + "status": fake.random_int(min=1, max=3), + "school_year": school_year # Ajouter l'année scolaire } # Créer ou mettre à jour le formulaire d'inscription @@ -420,7 +426,7 @@ class Command(BaseCommand): if created: register_form.fees.set(fees) register_form.discounts.set(discounts) - self.stdout.write(self.style.SUCCESS(f'RegistrationForm for student {student.last_name} created successfully')) + self.stdout.write(self.style.SUCCESS(f'RegistrationForm for student {student.last_name} created successfully with school year {school_year}')) else: self.stdout.write(self.style.SUCCESS(f'RegistrationForm for student {student.last_name} already exists')) diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index c4c8ae2..daf2b24 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -25,8 +25,8 @@ class Guardian(models.Model): """ Représente un responsable légal (parent/tuteur) d’un élève. """ - last_name = models.CharField(max_length=200, default="") - first_name = models.CharField(max_length=200, default="") + last_name = models.CharField(max_length=200, null=True, blank=True) + first_name = models.CharField(max_length=200, null=True, blank=True) birth_date = models.DateField(null=True, blank=True) address = models.CharField(max_length=200, default="", blank=True) phone = models.CharField(max_length=200, default="", blank=True) @@ -49,8 +49,8 @@ class Sibling(models.Model): """ Représente un frère ou une sœur d’un élève. """ - last_name = models.CharField(max_length=200, default="") - first_name = models.CharField(max_length=200, default="") + last_name = models.CharField(max_length=200, null=True, blank=True) + first_name = models.CharField(max_length=200, null=True, blank=True) birth_date = models.DateField(null=True, blank=True) def __str__(self): @@ -218,6 +218,7 @@ class RegistrationForm(models.Model): student = models.OneToOneField(Student, on_delete=models.CASCADE, primary_key=True) status = models.IntegerField(choices=RegistrationFormStatus, default=RegistrationFormStatus.RF_IDLE) last_update = models.DateTimeField(auto_now=True) + school_year = models.CharField(max_length=9, default="", blank=True) notes = models.CharField(max_length=200, blank=True) registration_link_code = models.CharField(max_length=200, default="", blank=True) registration_file = models.FileField( diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index 864f28e..d67a246 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -343,7 +343,7 @@ class GuardianByDICreationSerializer(serializers.ModelSerializer): class Meta: model = Guardian - fields = ['id', 'last_name', 'first_name', 'associated_profile_email'] + fields = ['id', 'last_name', 'first_name', 'associated_profile_email', 'phone'] def get_associated_profile_email(self, obj): if obj.profile_role and obj.profile_role.profile: @@ -356,7 +356,7 @@ class StudentByRFCreationSerializer(serializers.ModelSerializer): class Meta: model = Student - fields = ['id', 'last_name', 'first_name', 'guardians'] + fields = ['id', 'last_name', 'first_name', 'guardians', 'level'] def __init__(self, *args, **kwargs): super(StudentByRFCreationSerializer, self).__init__(*args, **kwargs) diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index f97d5b8..b33f244 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -174,4 +174,45 @@ def delete_registration_files(registerForm): registerForm.registration_file.delete(save=False) if os.path.exists(base_dir): - shutil.rmtree(base_dir) \ No newline at end of file + shutil.rmtree(base_dir) + +from datetime import datetime + +def getCurrentSchoolYear(): + """ + Retourne l'année scolaire en cours au format "YYYY-YYYY". + Exemple : Si nous sommes en octobre 2023, retourne "2023-2024". + """ + now = datetime.now() + current_year = now.year + current_month = now.month + + # Si nous sommes avant septembre, l'année scolaire a commencé l'année précédente + start_year = current_year if current_month >= 9 else current_year - 1 + return f"{start_year}-{start_year + 1}" + +def getNextSchoolYear(): + """ + Retourne l'année scolaire suivante au format "YYYY-YYYY". + Exemple : Si nous sommes en octobre 2023, retourne "2024-2025". + """ + current_school_year = getCurrentSchoolYear() + start_year, end_year = map(int, current_school_year.split('-')) + return f"{start_year + 1}-{end_year + 1}" + + +def getHistoricalYears(count=5): + """ + Retourne un tableau des années scolaires passées au format "YYYY-YYYY". + Exemple : ["2022-2023", "2021-2022", "2020-2021"]. + :param count: Le nombre d'années scolaires passées à inclure. + """ + current_school_year = getCurrentSchoolYear() + start_year = int(current_school_year.split('-')[0]) + + historical_years = [] + for i in range(1, count + 1): + historical_start_year = start_year - i + historical_years.append(f"{historical_start_year}-{historical_start_year + 1}") + + return historical_years \ No newline at end of file diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index 3a4c090..53efedc 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -33,7 +33,7 @@ class RegisterFormView(APIView): @swagger_auto_schema( manual_parameters=[ - openapi.Parameter('filter', openapi.IN_QUERY, description="filtre", type=openapi.TYPE_STRING, enum=['pending', 'archived', 'subscribed'], required=True), + openapi.Parameter('filter', openapi.IN_QUERY, description="filtre", type=openapi.TYPE_STRING, enum=['current_year', 'next_year', 'historical'], required=True), openapi.Parameter('search', openapi.IN_QUERY, description="search", type=openapi.TYPE_STRING, required=False), openapi.Parameter('page_size', openapi.IN_QUERY, description="limite de page lors de la pagination", type=openapi.TYPE_INTEGER, required=False), openapi.Parameter('establishment_id', openapi.IN_QUERY, description="ID de l'établissement", type=openapi.TYPE_INTEGER, required=True), @@ -51,7 +51,7 @@ class RegisterFormView(APIView): "last_name": "Doe", "date_of_birth": "2010-01-01" }, - "status": "pending", + "status": "current_year", "last_update": "10-02-2025 10:00" }, { @@ -62,7 +62,7 @@ class RegisterFormView(APIView): "last_name": "Doe", "date_of_birth": "2011-02-02" }, - "status": "archived", + "status": "historical", "last_update": "09-02-2025 09:00" } ] @@ -85,14 +85,19 @@ class RegisterFormView(APIView): except ValueError: page_size = settings.NB_RESULT_PER_PAGE - # Récupérer les dossier d'inscriptions en fonction du filtre + # Récupérer les années scolaires + current_year = util.getCurrentSchoolYear() + next_year = util.getNextSchoolYear() + historical_years = util.getHistoricalYears() + + # Récupérer les dossiers d'inscriptions en fonction du filtre registerForms_List = None - if filter == 'pending': - exclude_states = [RegistrationForm.RegistrationFormStatus.RF_VALIDATED, RegistrationForm.RegistrationFormStatus.RF_ARCHIVED] - registerForms_List = bdd.searchObjects(RegistrationForm, search, _excludeStates=exclude_states) - elif filter == 'archived': - registerForms_List = bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_ARCHIVED) - elif filter == 'subscribed': + if filter == 'current_year': + registerForms_List = RegistrationForm.objects.filter(school_year=current_year) + elif filter == 'next_year': + registerForms_List = RegistrationForm.objects.filter(school_year=next_year) + elif filter == 'historical': + registerForms_List = RegistrationForm.objects.filter(school_year__in=historical_years) registerForms_List = bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_VALIDATED) else: registerForms_List = None @@ -126,7 +131,7 @@ class RegisterFormView(APIView): "last_name": "Doe", "date_of_birth": "2010-01-01" }, - "status": "pending", + "status": "current_year", "last_update": "10-02-2025 10:00", "codeLienInscription": "ABC123XYZ456" } @@ -514,4 +519,5 @@ def get_parent_file_templates_by_rf(request, id): # Retourner les données sérialisées return JsonResponse(serializer.data, safe=False) except RegistrationParentFileTemplate.DoesNotExist: - return JsonResponse({'error': 'Aucune pièce à fournir trouvée pour ce dossier d\'inscription'}, status=status.HTTP_404_NOT_FOUND) \ No newline at end of file + return JsonResponse({'error': 'Aucune pièce à fournir trouvée pour ce dossier d\'inscription'}, status=status.HTTP_404_NOT_FOUND) + \ No newline at end of file diff --git a/Front-End/messages/en/subscriptions.json b/Front-End/messages/en/subscriptions.json index d9d745c..53bf5cb 100644 --- a/Front-End/messages/en/subscriptions.json +++ b/Front-End/messages/en/subscriptions.json @@ -30,5 +30,6 @@ "classe": "Class", "registrationFileStatus": "Registration file status", "files": "Files", - "subscribeFiles": "Subscribe files" + "subscribeFiles": "Subscribe files", + "historical": "Historical" } diff --git a/Front-End/messages/fr/subscriptions.json b/Front-End/messages/fr/subscriptions.json index 81ea7ed..413f570 100644 --- a/Front-End/messages/fr/subscriptions.json +++ b/Front-End/messages/fr/subscriptions.json @@ -30,5 +30,6 @@ "classe": "Classe", "registrationFileStatus": "État du dossier d'inscription", "files": "Fichiers", - "subscribeFiles": "Fichiers d'inscription" + "subscribeFiles": "Fichiers d'inscription", + "historical": "Historique" } diff --git a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js new file mode 100644 index 0000000..e4bfccd --- /dev/null +++ b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js @@ -0,0 +1,1010 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { User, Mail } from 'lucide-react'; +import InputTextIcon from '@/components/InputTextIcon'; +import ToggleSwitch from '@/components/ToggleSwitch'; +import Button from '@/components/Button'; +import Table from '@/components/Table'; +import FeesSection from '@/components/Structure/Tarification/FeesSection'; +import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection'; +import SectionTitle from '@/components/SectionTitle'; +import Popup from '@/components/Popup'; +import InputPhone from '@/components/InputPhone'; +import CheckBox from '@/components/CheckBox'; +import RadioList from '@/components/RadioList'; +import SelectChoice from '@/components/SelectChoice'; +import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date'; +import logger from '@/utils/logger'; +import { levels, genders } from '@/utils/constants'; +import { useEstablishment } from '@/context/EstablishmentContext'; +import { useRouter } from 'next/navigation'; +import { + fetchStudents, + createRegisterForm, +} from '@/app/actions/subscriptionAction'; +import { + fetchRegistrationDiscounts, + fetchTuitionDiscounts, + fetchRegistrationFees, + fetchTuitionFees, +} from '@/app/actions/schoolAction'; +import { + fetchRegistrationFileGroups, + fetchRegistrationSchoolFileMasters, + fetchRegistrationParentFileMasters, + cloneTemplate, + createRegistrationSchoolFileTemplate, + createRegistrationParentFileTemplate, +} from '@/app/actions/registerFileGroupAction'; +import { useClasses } from '@/context/ClassesContext'; +import { useCsrfToken } from '@/context/CsrfContext'; +import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url'; + +export default function CreateSubscriptionPage() { + const [formData, setFormData] = useState({ + studentLastName: '', + studentFirstName: '', + studentLevel: '', + guardianLastName: '', + guardianFirstName: '', + guardianEmail: '', + guardianPhone: '', + selectedGuardians: [], + autoMail: false, + selectedRegistrationDiscounts: [], + selectedRegistrationFees: [], + selectedTuitionDiscounts: [], + selectedTuitionFees: [], + selectedFileGroup: null, + schoolYear: getCurrentSchoolYear(), + }); + + const [students, setStudents] = useState([]); + const [registrationDiscounts, setRegistrationDiscounts] = useState([]); + const [tuitionDiscounts, setTuitionDiscounts] = useState([]); + const [registrationFees, setRegistrationFees] = useState([]); + const [tuitionFees, setTuitionFees] = useState([]); + const [groups, setGroups] = useState([]); + const [schoolFileMasters, setSchoolFileMasters] = useState([]); + + const [parentFileMasters, setParentFileMasters] = useState([]); + + const [isLoading, setIsLoading] = useState(false); + + const [existingGuardians, setExistingGuardians] = useState([]); + const [totalRegistrationAmount, setTotalRegistrationAmount] = useState(0); + const [totalTuitionAmount, setTotalTuitionAmount] = useState(0); + const [popupVisible, setPopupVisible] = useState(false); + const [popupMessage, setPopupMessage] = useState(''); + const [selectedStudent, setSelectedEleve] = useState(null); + const [isNewResponsable, setIsNewResponsable] = useState(true); + + const { getNiveauLabel } = useClasses(); + + const formDataRef = useRef(formData); + const { selectedEstablishmentId } = useEstablishment(); + + const csrfToken = useCsrfToken(); + const router = useRouter(); + + const isSubmitDisabled = () => { + // Vérifie si les champs requis sont remplis + const requiredFields = ['schoolYear', 'studentLastName', 'studentFirstName']; + const hasErrors = requiredFields.some((field) => getLocalError(field) !== ''); + + // Vérifie si un groupe de fichiers est sélectionné + const isFileGroupSelected = formData.selectedFileGroup !== null; + + // Vérifie si au moins un frais de scolarité est sélectionné + const hasSelectedTuitionFees = formData.selectedTuitionFees.length > 0; + + // Vérifie les conditions spécifiques pour le responsable + const isGuardianValid = isNewResponsable + ? getLocalError('guardianEmail') === '' + : formData.selectedGuardians.length > 0; + + // Retourne true si une des conditions n'est pas remplie + return ( + hasErrors || + !isFileGroupSelected || + !hasSelectedTuitionFees || + !isGuardianValid + ); + }; + + const getLocalError = (field) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + // Extraire tous les guardians des students + const allGuardians = students.flatMap((student) => student.guardians); + + if ( + // Student Form + (field === 'schoolYear' && + (!formData.schoolYear || String(formData.schoolYear).trim() === '')) || + (field === 'studentLastName' && + (!formData.studentLastName || + formData.studentLastName.trim() === '')) || + (field === 'studentFirstName' && + (!formData.studentFirstName || + formData.studentFirstName.trim() === '')) || + (field === 'guardianEmail' && + (!formData.guardianEmail || + formData.guardianEmail.trim() === '' || + !emailRegex.test(formData.guardianEmail) || + allGuardians.some( + (guardian) => + guardian.associated_profile_email === formData.guardianEmail + ))) // Vérifie si l'email existe déjà dans la liste des guardians + ) { + return field === 'guardianEmail' && + allGuardians.some( + (guardian) => + guardian.associated_profile_email === formData.guardianEmail + ) + ? 'Cet email est déjà associé à un responsable existant' + : 'Champs requis'; + } + + return ''; + }; + const requestErrorHandler = (err) => { + logger.error('Error fetching data:', err); + setErrors(err); + }; + + useEffect(() => { + formDataRef.current = formData; + }, [formData]); + + useEffect(() => { + if (selectedEstablishmentId) { + fetchStudents(selectedEstablishmentId) + .then((studentsData) => { + setStudents(studentsData); + }) + .catch(requestErrorHandler); + + fetchRegistrationDiscounts(selectedEstablishmentId) + .then((data) => { + setRegistrationDiscounts(data); + }) + .catch(requestErrorHandler); + fetchTuitionDiscounts(selectedEstablishmentId) + .then((data) => { + setTuitionDiscounts(data); + }) + .catch(requestErrorHandler); + fetchRegistrationFees(selectedEstablishmentId) + .then((data) => { + setRegistrationFees(data); + // Sélectionner par défaut les frais d'inscription + setFormData((prevData) => ({ + ...prevData, + selectedRegistrationFees: data.map((fee) => fee.id), + })); + }) + .catch(requestErrorHandler); + + fetchTuitionFees(selectedEstablishmentId) + .then((data) => { + setTuitionFees(data); + }) + .catch(requestErrorHandler); + + fetchRegistrationFileGroups(selectedEstablishmentId) + .then((data) => { + setGroups(data); + }) + .catch(requestErrorHandler); + + fetchRegistrationSchoolFileMasters() + .then((data) => { + setSchoolFileMasters(data); + }) + .catch(requestErrorHandler); + + fetchRegistrationParentFileMasters() + .then((data) => { + setParentFileMasters(data); + }) + .catch(requestErrorHandler); + } + }, [selectedEstablishmentId]); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prevState) => ({ + ...prevState, + [name]: value, + })); + }; + + const resetGuardianFields = () => { + setFormData((prevData) => ({ + ...prevData, + guardianLastName: '', + guardianFirstName: '', + guardianEmail: '', + guardianPhone: '', + selectedGuardians: [], + })); + }; + + // Gestion du changement du toggle + const handleToggleChange = (e) => { + const isChecked = e.target.checked; + setIsNewResponsable(isChecked); + + if (isChecked) { + // Réinitialiser les champs si on passe à "Nouveau responsable" + resetGuardianFields(); + } + }; + + const createRF = () => { + logger.debug('createRF formData:', formData); + + // Préparation des données + const selectedRegistrationFeesIds = formData.selectedRegistrationFees.map( + (feeId) => feeId + ); + const selectedRegistrationDiscountsIds = + formData.selectedRegistrationDiscounts.map((discountId) => discountId); + const selectedTuitionFeesIds = formData.selectedTuitionFees.map( + (feeId) => feeId + ); + const selectedTuitionDiscountsIds = formData.selectedTuitionDiscounts.map( + (discountId) => discountId + ); + const selectedFileGroup = formData.selectedFileGroup; + + const allFeesIds = [ + ...selectedRegistrationFeesIds, + ...selectedTuitionFeesIds, + ]; + const allDiscountsIds = [ + ...selectedRegistrationDiscountsIds, + ...selectedTuitionDiscountsIds, + ]; + + const data = { + student: { + last_name: formData.studentLastName, + first_name: formData.studentFirstName, + level: formData.studentLevel, + gender: formData.studentGender, + guardians: formData.selectedGuardians.length + ? formData.selectedGuardians.map((guardianId) => ({ id: guardianId })) + : formData.isExistingParentProfile + ? [ + { + profile_role_data: { + establishment: selectedEstablishmentId, + role_type: 2, + is_active: false, + profile: formData.existingProfileId, + }, + last_name: formData.guardianLastName, + first_name: formData.guardianFirstName, + birth_date: formData.guardianBirthDate, + phone: formData.guardianPhone, + }, + ] + : [ + { + profile_role_data: { + establishment: selectedEstablishmentId, + role_type: 2, + is_active: false, + profile_data: { + email: formData.guardianEmail, + password: 'Provisoire01!', + username: formData.guardianEmail, + }, + }, + last_name: formData.guardianLastName, + first_name: formData.guardianFirstName, + phone: formData.guardianPhone, + }, + ], + sibling: [], + }, + fees: allFeesIds, + discounts: allDiscountsIds, + fileGroup: selectedFileGroup, + establishment: selectedEstablishmentId, + school_year: formData.schoolYear, + }; + + setIsLoading(true); + + // Création du dossier d'inscription + createRegisterForm(data, csrfToken) + .then((data) => { + // Clonage des schoolFileTemplates + const masters = schoolFileMasters.filter((file) => + file.groups.includes(selectedFileGroup) + ); + const parentMasters = parentFileMasters.filter((file) => + file.groups.includes(selectedFileGroup) + ); + + const clonePromises = masters.map((templateMaster) => + cloneTemplate( + templateMaster.id, + formData.guardianEmail, + templateMaster.is_required + ) + .then((clonedDocument) => { + const cloneData = { + name: `${templateMaster.name}_${formData.studentFirstName}_${formData.studentLastName}`, + slug: clonedDocument.slug, + id: clonedDocument.id, + master: templateMaster.id, + registration_form: data.student.id, + }; + + return createRegistrationSchoolFileTemplate(cloneData, csrfToken) + .then((response) => + logger.debug('Template enregistré avec succès:', response) + ) + .catch((error) => { + setIsLoading(false); + logger.error( + "Erreur lors de l'enregistrement du template:", + error + ); + }); + }) + .catch((error) => { + setIsLoading(false); + logger.error('Error during cloning or sending:', error); + }) + ); + + // Clonage des parentFileTemplates + const parentClonePromises = parentMasters.map((parentMaster) => { + const parentTemplateData = { + master: parentMaster.id, + registration_form: data.student.id, + }; + + return createRegistrationParentFileTemplate( + parentTemplateData, + csrfToken + ) + .then((response) => + logger.debug('Parent template enregistré avec succès:', response) + ) + .catch((error) => { + setIsLoading(false); + logger.error( + "Erreur lors de l'enregistrement du parent template:", + error + ); + }); + }); + + // Attendre que tous les clones soient créés + Promise.all([...clonePromises, ...parentClonePromises]) + .then(() => { + // Redirection après succès + router.push(FE_ADMIN_SUBSCRIPTIONS_URL); + }) + .catch((error) => { + setIsLoading(false); + logger.error('Error during cloning or sending:', error); + }); + }) + .catch((error) => { + setIsLoading(false); + logger.error('Error during register form creation:', error); + }); + }; + const handleEleveSelection = (student) => { + setSelectedEleve(student); + setExistingGuardians(student.guardians); + }; + + // Gestion des changements dans la checkbox + const handleGuardianCheckboxChange = (guardian) => { + setFormData((prevData) => { + const isSelected = prevData.selectedGuardians.includes(guardian.id); + const updatedSelectedGuardians = isSelected + ? prevData.selectedGuardians.filter((id) => id !== guardian.id) + : [...prevData.selectedGuardians, guardian.id]; + + const updatedFormData = { + ...prevData, + selectedGuardians: updatedSelectedGuardians, + }; + + if (!isSelected) { + // Si le guardian est sélectionné, remplir les champs + updatedFormData.guardianLastName = guardian.last_name || ''; + updatedFormData.guardianFirstName = guardian.first_name || ''; + updatedFormData.guardianEmail = guardian.associated_profile_email || ''; + updatedFormData.guardianPhone = guardian.phone || ''; + } else { + // Réinitialiser les champs si le guardian est désélectionné + resetGuardianFields(); + } + + return updatedFormData; + }); + }; + + // Gestion de la désélection d'un élève + const handleEleveDeselection = () => { + setSelectedEleve(null); + setExistingGuardians([]); + resetGuardianFields(); + }; + + const handleRegistrationFeeSelection = (feeId) => { + setFormData((prevData) => { + const selectedRegistrationFees = + prevData.selectedRegistrationFees.includes(feeId) + ? prevData.selectedRegistrationFees.filter((id) => id !== feeId) + : [...prevData.selectedRegistrationFees, feeId]; + const finalAmount = calculateFinalRegistrationAmount( + selectedRegistrationFees, + prevData.selectedRegistrationDiscounts + ); + setTotalRegistrationAmount(finalAmount); + return { ...prevData, selectedRegistrationFees }; + }); + }; + + const handleTuitionFeeSelection = (feeId) => { + setFormData((prevData) => { + const selectedTuitionFees = prevData.selectedTuitionFees.includes(feeId) + ? prevData.selectedTuitionFees.filter((id) => id !== feeId) + : [...prevData.selectedTuitionFees, feeId]; + const finalAmount = calculateFinalTuitionAmount( + selectedTuitionFees, + prevData.selectedTuitionDiscounts + ); + setTotalTuitionAmount(finalAmount); + return { ...prevData, selectedTuitionFees }; + }); + }; + + const handleRegistrationDiscountSelection = (discountId) => { + setFormData((prevData) => { + const selectedRegistrationDiscounts = + prevData.selectedRegistrationDiscounts.includes(discountId) + ? prevData.selectedRegistrationDiscounts.filter( + (id) => id !== discountId + ) + : [...prevData.selectedRegistrationDiscounts, discountId]; + const finalAmount = calculateFinalRegistrationAmount( + prevData.selectedRegistrationFees, + selectedRegistrationDiscounts + ); + setTotalRegistrationAmount(finalAmount); + return { ...prevData, selectedRegistrationDiscounts }; + }); + }; + + const handleTuitionDiscountSelection = (discountId) => { + setFormData((prevData) => { + const selectedTuitionDiscounts = + prevData.selectedTuitionDiscounts.includes(discountId) + ? prevData.selectedTuitionDiscounts.filter((id) => id !== discountId) + : [...prevData.selectedTuitionDiscounts, discountId]; + const finalAmount = calculateFinalTuitionAmount( + prevData.selectedTuitionFees, + selectedTuitionDiscounts + ); + setTotalTuitionAmount(finalAmount); + return { ...prevData, selectedTuitionDiscounts }; + }); + }; + + const calculateFinalRegistrationAmount = ( + selectedRegistrationFees, + selectedRegistrationDiscounts + ) => { + const totalFees = selectedRegistrationFees.reduce((sum, feeId) => { + const fee = registrationFees.find((f) => f.id === feeId); + if (fee && !isNaN(parseFloat(fee.base_amount))) { + return sum + parseFloat(fee.base_amount); + } + return sum; + }, 0); + + const totalDiscounts = selectedRegistrationDiscounts.reduce( + (sum, discountId) => { + const discount = registrationDiscounts.find((d) => d.id === discountId); + if (discount) { + if ( + discount.discount_type === 0 && + !isNaN(parseFloat(discount.amount)) + ) { + // Currency + return sum + parseFloat(discount.amount); + } else if ( + discount.discount_type === 1 && + !isNaN(parseFloat(discount.amount)) + ) { + // Percent + return sum + (totalFees * parseFloat(discount.amount)) / 100; + } + } + return sum; + }, + 0 + ); + + const finalAmount = totalFees - totalDiscounts; + + return finalAmount.toFixed(2); + }; + + const calculateFinalTuitionAmount = ( + selectedTuitionFees, + selectedTuitionDiscounts + ) => { + const totalFees = selectedTuitionFees.reduce((sum, feeId) => { + const fee = tuitionFees.find((f) => f.id === feeId); + if (fee && !isNaN(parseFloat(fee.base_amount))) { + return sum + parseFloat(fee.base_amount); + } + return sum; + }, 0); + + const totalDiscounts = selectedTuitionDiscounts.reduce( + (sum, discountId) => { + const discount = tuitionDiscounts.find((d) => d.id === discountId); + if (discount) { + if ( + discount.discount_type === 0 && + !isNaN(parseFloat(discount.amount)) + ) { + // Currency + return sum + parseFloat(discount.amount); + } else if ( + discount.discount_type === 1 && + !isNaN(parseFloat(discount.amount)) + ) { + // Percent + return sum + (totalFees * parseFloat(discount.amount)) / 100; + } + } + return sum; + }, + 0 + ); + + const finalAmount = totalFees - totalDiscounts; + + return finalAmount.toFixed(2); + }; + + return ( +