From 76f9a7dd14d7065f4add01718fda499fbb9183c7 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Mon, 5 May 2025 20:57:51 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Formulaire=20de=20cr=C3=A9ation=20RF=20?= =?UTF-8?q?sur=20une=20seule=20pag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/commands/init_mock_datas.py | 12 +- Back-End/Subscriptions/models.py | 9 +- Back-End/Subscriptions/serializers.py | 4 +- Back-End/Subscriptions/util.py | 43 +- .../views/register_form_views.py | 30 +- Front-End/messages/en/subscriptions.json | 3 +- Front-End/messages/fr/subscriptions.json | 3 +- .../subscriptions/createSubscription/page.js | 1010 +++++++++++++++++ .../app/[locale]/admin/subscriptions/page.js | 424 ++----- .../src/app/actions/subscriptionAction.js | 8 +- Front-End/src/components/InputTextIcon.js | 20 +- .../components/Inscription/InscriptionForm.js | 21 + .../components/Inscription/StudentInfoForm.js | 18 +- Front-End/src/components/SectionTitle.js | 21 +- .../Tarification/DiscountsSection.js | 20 +- .../Structure/Tarification/FeesSection.js | 16 +- Front-End/src/utils/Date.js | 42 + Front-End/src/utils/Url.js | 1 + Front-End/src/utils/constants.js | 16 + 19 files changed, 1299 insertions(+), 422 deletions(-) create mode 100644 Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js create mode 100644 Front-End/src/utils/constants.js 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 ( +
+

Créer un dossier d'inscription

+ + {/* Sélection de l'année scolaire */} +
+
+ + setFormData((prevData) => ({ + ...prevData, + schoolYear: e.target.value, + })) + } + errorLocalMsg={getLocalError('schoolYear')} + required + /> +
+
+ + {/* Informations sur l'élève */} + + {/* Nom et Prénom côte à côte */} +
+ + +
+ + {/* Genre et Niveau côte à côte */} +
+ + setFormData((prevData) => ({ + ...prevData, + studentGender: e.target.value, + })) + } + /> + + setFormData((prevData) => ({ + ...prevData, + studentLevel: e.target.value, + })) + } + /> +
+
+ + {/* Informations sur le responsable */} + + + + {/* Nom et Prénom côte à côte */} +
+ + +
+ + {/* Email et Numéro de téléphone côte à côte */} +
+ + +
+
+ + {/* Tableau des élèves pour responsable existant */} + {!isNewResponsable && ( +
+ ( +
+ {row.photo ? ( + + {`${row.first_name} + + ) : ( +
+ + {row.first_name[0]} + {row.last_name[0]} + +
+ )} +
+ ), + }, + { + name: 'Nom', + transform: (row) => row.last_name, + }, + { + name: 'Prénom', + transform: (row) => row.first_name, + }, + { + name: 'Niveau', + transform: (row) => ( +
{getNiveauLabel(row.level)}
+ ), + }, + ]} + isSelectable={true} + onRowClick={(event) => { + if (event.deselected) { + handleEleveDeselection(); + } else { + handleEleveSelection(event); + } + }} + rowClassName={(row) => + selectedStudent && selectedStudent.id === row.id + ? 'bg-emerald-600 text-white' + : '' + } + selectedRows={selectedStudent ? [selectedStudent.id] : []} // Assurez-vous que selectedRows est un tableau + /> + + {selectedStudent && ( +
+

+ Responsables associés à {selectedStudent.last_name}{' '} + {selectedStudent.first_name} : +

+ {existingGuardians.map((guardian) => ( +
+ handleGuardianCheckboxChange(guardian)} + fieldName="selectedGuardians" + itemLabelFunc={() => + guardian.last_name && guardian.first_name + ? `${guardian.last_name} ${guardian.first_name} - ${guardian.associated_profile_email}` + : `${guardian.associated_profile_email}` + } + /> +
+ ))} +
+ )} + + )} + + {/* Section des frais d'inscription */} + + {registrationFees.length > 0 || registrationDiscounts.length > 0 ? ( + <> +
+ {/* Tableau des frais */} +
+ +
+ + {/* Tableau des réductions */} +
+ {registrationDiscounts.length > 0 ? ( + + ) : ( +

+ Information + + Aucune réduction n'a été créée sur les frais + d'inscription. + +

+ )} +
+
+ + {/* Montant total */} +
+ + Montant total des frais d'inscription : + + + {totalRegistrationAmount} € + +
+ + ) : ( +

+ Attention! + + Aucun frais d'inscription n'a été créé. + +

+ )} +
+ + {/* Section des frais de scolarité */} + + {tuitionFees.length > 0 || tuitionDiscounts.length > 0 ? ( + <> +
+ {/* Tableau des frais */} +
+ +
+ + {/* Tableau des réductions */} +
+ {tuitionDiscounts.length > 0 ? ( + + ) : ( +

+ Information + + Aucune réduction n'a été créée sur les frais de + scolarité. + +

+ )} +
+
+ + {/* Montant total */} +
+ + Montant total des frais de scolarité : + + + {totalTuitionAmount} € + +
+ + ) : ( +

+ Attention! + + Aucun frais de scolarité n'a été créé. + +

+ )} +
+ + {/* Section des groupes de documents */} + + {groups.length > 0 ? ( +
+ ({ + id: group.id, + label: `${group.name}${ + group.description ? ` (${group.description})` : '' + }`, + }))} + formData={{ + ...formData, + selectedFileGroup: parseInt(formData.selectedFileGroup, 10), + }} + handleChange={(e) => { + const value = parseInt(e.target.value, 10); + setFormData((prevData) => ({ + ...prevData, + selectedFileGroup: value, + })); + }} + fieldName="selectedFileGroup" + required + className="mt-4" + /> +
+ ) : ( +

+ Attention! + + Aucun groupe de documents n'a été créé. + +

+ )} +
+ + {/* Bouton de soumission */} +
+
+ + ); +} diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 299543d..f3c904e 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -25,40 +25,24 @@ import InscriptionForm from '@/components/Inscription/InscriptionForm'; import { useEstablishment } from '@/context/EstablishmentContext'; import { - PENDING, - SUBSCRIBED, - ARCHIVED, + CURRENT_YEAR, + NEXT_YEAR, + HISTORICAL, fetchRegisterForms, - createRegisterForm, sendRegisterForm, archiveRegisterForm, - fetchStudents, editRegisterForm, editRegisterFormWithBinaryFile, } from '@/app/actions/subscriptionAction'; -import { - fetchRegistrationSchoolFileMasters, - fetchRegistrationParentFileMasters, - createRegistrationSchoolFileTemplate, - createRegistrationParentFileTemplate, - fetchRegistrationFileGroups, - cloneTemplate, -} from '@/app/actions/registerFileGroupAction'; - -import { - fetchClasses, - fetchRegistrationDiscounts, - fetchTuitionDiscounts, - fetchRegistrationFees, - fetchTuitionFees, -} from '@/app/actions/schoolAction'; +import { fetchClasses, updateDatas } from '@/app/actions/schoolAction'; import { fetchProfiles } from '@/app/actions/authAction'; import { FE_ADMIN_SUBSCRIPTIONS_EDIT_URL, FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL, + FE_ADMIN_SUBSCRIPTIONS_CREATE_URL, BASE_URL, } from '@/utils/Url'; @@ -68,41 +52,41 @@ import logger from '@/utils/logger'; import { PhoneLabel } from '@/components/PhoneLabel'; import FileUpload from '@/components/FileUpload'; import FilesModal from '@/components/Inscription/FilesModal'; +import { + getCurrentSchoolYear, + getNextSchoolYear, + getHistoricalYears, +} from '@/utils/Date'; export default function Page({ params: { locale } }) { const t = useTranslations('subscriptions'); const [registrationForms, setRegistrationForms] = useState([]); - const [registrationFormsDataPending, setRegistrationFormsDataPending] = + const [ + registrationFormsDataCurrentYear, + setRegistrationFormsDataCurrentYear, + ] = useState([]); + const [registrationFormsDataNextYear, setRegistrationFormsDataNextYear] = useState([]); - const [registrationFormsDataSubscribed, setRegistrationFormsDataSubscribed] = + const [registrationFormsDataHistorical, setRegistrationFormsDataHistorical] = useState([]); - const [registrationFormsDataArchived, setRegistrationFormsDataArchived] = - useState([]); - // const [filter, setFilter] = useState('*'); + const currentSchoolYear = getCurrentSchoolYear(); // Exemple : "2024-2025" + const nextSchoolYear = getNextSchoolYear(); // Exemple : "2025-2026" + const historicalYears = getHistoricalYears(); const [searchTerm, setSearchTerm] = useState(''); const [alertPage, setAlertPage] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [activeTab, setActiveTab] = useState('pending'); + const [activeTab, setActiveTab] = useState('currentYear'); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); - const [totalPending, setTotalPending] = useState(0); - const [totalSubscribed, setTotalSubscribed] = useState(0); - const [totalArchives, setTotalArchives] = useState(0); + const [totalCurrentYear, setTotalCurrentYear] = useState(0); + const [totalSubscribed, setTotalNextYear] = useState(0); + const [totalHistorical, setTotalHistorical] = useState(0); const [itemsPerPage, setItemsPerPage] = useState(10); // Définir le nombre d'éléments par page - const [schoolFileMasters, setSchoolFileMasters] = useState([]); - const [parentFileMasters, setParentFileMasters] = useState([]); - const [isOpen, setIsOpen] = useState(false); const [student, setStudent] = useState(''); const [classes, setClasses] = useState([]); - const [students, setEleves] = useState([]); const [reloadFetch, setReloadFetch] = useState(false); - const [registrationDiscounts, setRegistrationDiscounts] = useState([]); - const [tuitionDiscounts, setTuitionDiscounts] = useState([]); - const [registrationFees, setRegistrationFees] = useState([]); - const [tuitionFees, setTuitionFees] = useState([]); - const [groups, setGroups] = useState([]); const [profiles, setProfiles] = useState([]); const [isOpenAddGuardian, setIsOpenAddGuardian] = useState(false); @@ -132,14 +116,6 @@ export default function Page({ params: { locale } }) { setIsSepaUploadModalOpen(false); }; - const openModal = () => { - setIsOpen(true); - }; - - const closeModal = () => { - setIsOpen(false); - }; - const openFilesModal = (row) => { setSelectedRegisterForm(row || []); setIsFilesModalOpen(true); @@ -160,11 +136,11 @@ export default function Page({ params: { locale } }) { if (data) { const { registerForms, count, page_size } = data; if (registerForms) { - setRegistrationFormsDataPending(registerForms); + setRegistrationFormsDataCurrentYear(registerForms); } const calculatedTotalPages = count === 0 ? 1 : Math.ceil(count / page_size); - setTotalPending(count); + setTotalCurrentYear(count); setTotalPages(calculatedTotalPages); } }; @@ -179,9 +155,9 @@ export default function Page({ params: { locale } }) { const registerFormSubscribedDataHandler = (data) => { if (data) { const { registerForms, count, page_size } = data; - setTotalSubscribed(count); + setTotalNextYear(count); if (registerForms) { - setRegistrationFormsDataSubscribed(registerForms); + setRegistrationFormsDataNextYear(registerForms); } } }; @@ -197,9 +173,9 @@ export default function Page({ params: { locale } }) { if (data) { const { registerForms, count, page_size } = data; - setTotalArchives(count); + setTotalHistorical(count); if (registerForms) { - setRegistrationFormsDataArchived(registerForms); + setRegistrationFormsDataHistorical(registerForms); } } }; @@ -211,7 +187,7 @@ export default function Page({ params: { locale } }) { Promise.all([ fetchRegisterForms( selectedEstablishmentId, - PENDING, + CURRENT_YEAR, currentPage, itemsPerPage, searchTerm @@ -225,59 +201,12 @@ export default function Page({ params: { locale } }) { }) .catch(requestErrorHandler), - fetchStudents(selectedEstablishmentId) - .then((studentsData) => { - setEleves(studentsData); - }) - .catch(requestErrorHandler), - - fetchRegisterForms(selectedEstablishmentId, SUBSCRIBED) + fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR) .then(registerFormSubscribedDataHandler) .catch(requestErrorHandler), - fetchRegisterForms(selectedEstablishmentId, ARCHIVED) + fetchRegisterForms(selectedEstablishmentId, HISTORICAL) .then(registerFormArchivedDataHandler) .catch(requestErrorHandler), - fetchRegistrationSchoolFileMasters() - .then((data) => { - setSchoolFileMasters(data); - }) - .catch((err) => { - logger.debug(err.message); - }), - fetchRegistrationParentFileMasters() - .then((data) => { - setParentFileMasters(data); - }) - .catch((err) => { - logger.debug(err.message); - }), - fetchRegistrationDiscounts(selectedEstablishmentId) - .then((data) => { - setRegistrationDiscounts(data); - }) - .catch(requestErrorHandler), - fetchTuitionDiscounts(selectedEstablishmentId) - .then((data) => { - setTuitionDiscounts(data); - }) - .catch(requestErrorHandler), - fetchRegistrationFees(selectedEstablishmentId) - .then((data) => { - setRegistrationFees(data); - }) - .catch(requestErrorHandler), - fetchTuitionFees(selectedEstablishmentId) - .then((data) => { - setTuitionFees(data); - }) - .catch(requestErrorHandler), - fetchRegistrationFileGroups(selectedEstablishmentId) - .then((data) => { - setGroups(data); - }) - .catch((error) => { - logger.error('Error fetching file groups:', error); - }), fetchProfiles() .then((data) => { setProfiles(data); @@ -307,27 +236,19 @@ export default function Page({ params: { locale } }) { setIsLoading(true); fetchRegisterForms( selectedEstablishmentId, - PENDING, + CURRENT_YEAR, currentPage, itemsPerPage, searchTerm ) .then(registerFormPendingDataHandler) .catch(requestErrorHandler); - fetchRegisterForms(selectedEstablishmentId, SUBSCRIBED) + fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR) .then(registerFormSubscribedDataHandler) .catch(requestErrorHandler); - fetchRegisterForms(selectedEstablishmentId, ARCHIVED) + fetchRegisterForms(selectedEstablishmentId, HISTORICAL) .then(registerFormArchivedDataHandler) .catch(requestErrorHandler); - fetchRegistrationSchoolFileMasters() - .then((data) => { - setSchoolFileMasters(data); - }) - .catch((err) => { - err = err.message; - logger.debug(err); - }); setIsLoading(false); setReloadFetch(false); @@ -344,12 +265,12 @@ export default function Page({ params: { locale } }) { * UseEffect to update page count of tab */ useEffect(() => { - if (activeTab === 'pending') { - setTotalPages(Math.ceil(totalPending / itemsPerPage)); - } else if (activeTab === 'subscribed') { + if (activeTab === 'currentYear') { + setTotalPages(Math.ceil(totalCurrentYear / itemsPerPage)); + } else if (activeTab === 'nextYear') { setTotalPages(Math.ceil(totalSubscribed / itemsPerPage)); - } else if (activeTab === 'archived') { - setTotalPages(Math.ceil(totalArchives / itemsPerPage)); + } else if (activeTab === 'historical') { + setTotalPages(Math.ceil(totalHistorical / itemsPerPage)); } }, [currentPage]); @@ -460,187 +381,6 @@ export default function Page({ params: { locale } }) { setCurrentPage(newPage); }; - const createRF = (updatedData) => { - logger.debug('createRF updatedData:', updatedData); - - const selectedRegistrationFeesIds = - updatedData.selectedRegistrationFees.map((feeId) => feeId); - const selectedRegistrationDiscountsIds = - updatedData.selectedRegistrationDiscounts.map((discountId) => discountId); - const selectedTuitionFeesIds = updatedData.selectedTuitionFees.map( - (feeId) => feeId - ); - const selectedTuitionDiscountsIds = - updatedData.selectedTuitionDiscounts.map((discountId) => discountId); - const selectedFileGroup = updatedData.selectedFileGroup; - const allFeesIds = [ - ...selectedRegistrationFeesIds, - ...selectedTuitionFeesIds, - ]; - const allDiscountsds = [ - ...selectedRegistrationDiscountsIds, - ...selectedTuitionDiscountsIds, - ]; - - const data = { - student: { - last_name: updatedData.studentLastName, - first_name: updatedData.studentFirstName, - guardians: - updatedData.selectedGuardians.length !== 0 - ? updatedData.selectedGuardians.map((guardianId) => ({ - id: guardianId, - })) - : (() => { - if (updatedData.isExistingParentProfile) { - return [ - { - profile_role_data: { - establishment: selectedEstablishmentId, - role_type: 2, - is_active: false, - profile: updatedData.existingProfileId, // Associer au profil existant - }, - last_name: updatedData.guardianLastName, - first_name: updatedData.guardianFirstName, - birth_date: updatedData.guardianBirthDate, - address: updatedData.guardianAddress, - phone: updatedData.guardianPhone, - profession: updatedData.guardianProfession, - }, - ]; - } - - // Si aucun profil existant n'est trouvé, créer un nouveau profil - return [ - { - profile_role_data: { - establishment: selectedEstablishmentId, - role_type: 2, - is_active: false, - profile_data: { - email: updatedData.guardianEmail, - password: 'Provisoire01!', - username: updatedData.guardianEmail, - }, - }, - last_name: updatedData.guardianLastName, - first_name: updatedData.guardianFirstName, - birth_date: updatedData.guardianBirthDate, - address: updatedData.guardianAddress, - phone: updatedData.guardianPhone, - profession: updatedData.guardianProfession, - }, - ]; - })(), - sibling: [], - }, - fees: allFeesIds, - discounts: allDiscountsds, - fileGroup: selectedFileGroup, - establishment: selectedEstablishmentId, - }; - - setIsLoading(true); - createRegisterForm(data, csrfToken) - .then((data) => { - // Cloner les schoolFileTemplates pour chaque templateMaster du fileGroup - const masters = schoolFileMasters.filter((file) => - file.groups.includes(selectedFileGroup) - ); - const parent_masters = parentFileMasters.filter((file) => - file.groups.includes(selectedFileGroup) - ); - - const clonePromises = masters.map((templateMaster) => { - return cloneTemplate( - templateMaster.id, - updatedData.guardianEmail, - templateMaster.is_required - ) - .then((clonedDocument) => { - // Sauvegarde des schoolFileTemplates clonés dans la base de données - const cloneData = { - name: `${templateMaster.name}_${updatedData.studentFirstName}_${updatedData.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); - }); - }); - - // Créer les parentFileTemplates pour chaque parentMaster - const parentClonePromises = parent_masters.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 (school et parent) soient créés - Promise.all([...clonePromises, ...parentClonePromises]) - .then(() => { - // Mise à jour immédiate des données - setRegistrationFormsDataPending((prevState) => [ - ...(prevState || []), - data, - ]); - setTotalPending((prev) => prev + 1); - if (updatedData.autoMail) { - sendConfirmRegisterForm( - data.student.id, - updatedData.studentLastName, - updatedData.studentFirstName - ); - } - closeModal(); // Appeler closeModal ici après que tout soit terminé - // Forcer le rechargement complet des données - setReloadFetch(true); - setIsLoading(false); - }) - .catch((error) => { - setIsLoading(false); - logger.error('Error during cloning or sending:', error); - }); - }) - .catch((error) => { - setIsLoading(false); - logger.error('Error:', error); - }); - }; - const updateRF = (updatedData) => { logger.debug('updateRF updatedData:', updatedData); @@ -700,11 +440,11 @@ export default function Page({ params: { locale } }) { editRegisterForm(student.id, data, csrfToken) .then((data) => { // Mise à jour immédiate des données - setRegistrationFormsDataPending((prevState) => [ + setRegistrationFormsDataCurrentYear((prevState) => [ ...(prevState || []), data, ]); - setTotalPending((prev) => prev + 1); + setTotalCurrentYear((prev) => prev + 1); if (updatedData.autoMail) { sendConfirmRegisterForm( data.student.id, @@ -980,7 +720,7 @@ export default function Page({ params: { locale } }) { } else { if ( registrationForms.length === 0 && - registrationFormsDataArchived.length === 0 && + registrationFormsDataHistorical.length === 0 && alertPage ) { return ( @@ -997,49 +737,54 @@ export default function Page({ params: { locale } }) {
+ {/* Tab pour l'année scolaire en cours */} - {t('pending')} + {currentSchoolYear} - ({totalPending}) + ({totalCurrentYear}) } - active={activeTab === 'pending'} - onClick={() => setActiveTab('pending')} + active={activeTab === 'currentYear'} + onClick={() => setActiveTab('currentYear')} /> + + {/* Tab pour l'année scolaire prochaine */} - {t('subscribed')} + {nextSchoolYear} ({totalSubscribed}) } - active={activeTab === 'subscribed'} - onClick={() => setActiveTab('subscribed')} + active={activeTab === 'nextYear'} + onClick={() => setActiveTab('nextYear')} /> + + {/* Tab pour l'historique */} - {t('archived')} + {t('historical')} - ({totalArchives}) + ({totalHistorical}) } - active={activeTab === 'archived'} - onClick={() => setActiveTab('archived')} + active={activeTab === 'historical'} + onClick={() => setActiveTab('historical')} />
+
- {/*SI STATE == pending || subscribe || archived */} - {activeTab === 'pending' || - activeTab === 'subscribed' || - activeTab === 'archived' ? ( + {activeTab === 'currentYear' || + activeTab === 'nextYear' || + activeTab === 'historical' ? (
@@ -1056,7 +801,10 @@ export default function Page({ params: { locale } }) { />
setConfirmPopupVisible(false)} /> - {isOpen && ( - ( - fee.is_active - )} - tuitionFees={tuitionFees.filter((fee) => fee.is_active)} - groups={groups} - profiles={profiles} - onSubmit={createRF} - /> - )} - /> - )} {isSepaUploadModalOpen && ( { const body = await response.json(); @@ -23,7 +23,7 @@ const requestResponseHandler = async (response) => { export const fetchRegisterForms = ( establishment, - filter = PENDING, + filter = CURRENT_YEAR, page = '', pageSize = '', search = '' diff --git a/Front-End/src/components/InputTextIcon.js b/Front-End/src/components/InputTextIcon.js index 447ce2e..42c1f42 100644 --- a/Front-End/src/components/InputTextIcon.js +++ b/Front-End/src/components/InputTextIcon.js @@ -6,8 +6,11 @@ export default function InputTextIcon({ value, onChange, errorMsg, + errorLocalMsg, placeholder, className, + required, + enable = true, // Nouvelle prop pour activer/désactiver le champ }) { return ( <> @@ -17,9 +20,16 @@ export default function InputTextIcon({ className="block text-sm font-medium text-gray-700" > {label} + {required && *}
{IconItem && } @@ -30,8 +40,12 @@ export default function InputTextIcon({ placeholder={placeholder} name={name} value={value} - onChange={onChange} - className="flex-1 px-3 py-2 block w-full rounded-r-md sm:text-sm border-none focus:ring-0 outline-none" + onChange={enable ? onChange : undefined} + className={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md ${ + !enable ? 'bg-gray-100 cursor-not-allowed' : '' + }`} + required={required} + readOnly={!enable ? 'readOnly' : ''} />
{errorMsg &&

{errorMsg}

} diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index a72c3b3..7d2140e 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -14,6 +14,8 @@ import InputPhone from '../InputPhone'; import { PhoneLabel } from '../PhoneLabel'; import CheckBox from '@/components/CheckBox'; import RadioList from '@/components/RadioList'; +import SelectChoice from '@/components/SelectChoice'; +import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date'; const InscriptionForm = ({ students, @@ -52,6 +54,7 @@ const InscriptionForm = ({ selectedTuitionDiscounts: [], selectedTuitionFees: [], selectedFileGroup: null, // Ajout du groupe de fichiers sélectionné + schoolYear: getCurrentSchoolYear(), }; }); @@ -390,6 +393,24 @@ const InscriptionForm = ({ {step === 1 && (
+ {/* Sélection de l'année scolaire */} + + setFormData((prevData) => ({ + ...prevData, + schoolYear: e.target.value, + })) + } + className="w-full mt-4" + /> { +const SectionTitle = ({ title, children }) => { return ( -
-
-
-
-
-
{title}
+
+ {/* Liseré vertical */} +
+ + {/* Contenu de la section */} +
+ {/* Titre avec liseré horizontal */} +
+

{title}

+
+
+ {/* Contenu passé en children */} + {children}
); diff --git a/Front-End/src/components/Structure/Tarification/DiscountsSection.js b/Front-End/src/components/Structure/Tarification/DiscountsSection.js index 83428e0..ab079cd 100644 --- a/Front-End/src/components/Structure/Tarification/DiscountsSection.js +++ b/Front-End/src/components/Structure/Tarification/DiscountsSection.js @@ -345,15 +345,17 @@ const DiscountsSection = ({ ]; return ( -
- +
+ {!subscriptionMode && ( + + )}
- + {!subscriptionMode && ( + + )}
{ const day = date.getDay(); return day === 0 || day === 6; }; + +/** + * Retourne l'année scolaire en cours au format "YYYY-YYYY". + * Exemple : Si nous sommes en octobre 2023, retourne "2023-2024". + */ +export function getCurrentSchoolYear() { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; // Les mois sont indexés à partir de 0 + + // Si nous sommes avant septembre, l'année scolaire a commencé l'année précédente + const startYear = currentMonth >= 9 ? currentYear : currentYear - 1; + return `${startYear}-${startYear + 1}`; +} + +/** + * Retourne l'année scolaire suivante au format "YYYY-YYYY". + * Exemple : Si nous sommes en octobre 2023, retourne "2024-2025". + */ +export function getNextSchoolYear() { + const currentSchoolYear = getCurrentSchoolYear(); + const [startYear, endYear] = currentSchoolYear.split('-').map(Number); + return `${startYear + 1}-${endYear + 1}`; +} + +/** + * Retourne un tableau des années scolaires passées au format "YYYY-YYYY". + * Exemple : ["2022-2023", "2021-2022", "2020-2021"]. + * @param {number} count - Le nombre d'années scolaires passées à inclure. + */ +export function getHistoricalYears(count = 5) { + const currentSchoolYear = getCurrentSchoolYear(); + const [startYear] = currentSchoolYear.split('-').map(Number); + + const historicalYears = []; + for (let i = 1; i <= count; i++) { + const historicalStartYear = startYear - i; + historicalYears.push(`${historicalStartYear}-${historicalStartYear + 1}`); + } + + return historicalYears; +} diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index 17ee144..7b2e7ff 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -68,6 +68,7 @@ export const FE_ADMIN_HOME_URL = `/admin`; // ADMIN/SUBSCRIPTIONS URL export const FE_ADMIN_SUBSCRIPTIONS_URL = `/admin/subscriptions`; +export const FE_ADMIN_SUBSCRIPTIONS_CREATE_URL = `/admin/subscriptions/createSubscription`; export const FE_ADMIN_SUBSCRIPTIONS_EDIT_URL = `/admin/subscriptions/editInscription`; export const FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL = `/admin/subscriptions/validateSubscription`; diff --git a/Front-End/src/utils/constants.js b/Front-End/src/utils/constants.js new file mode 100644 index 0000000..7af7231 --- /dev/null +++ b/Front-End/src/utils/constants.js @@ -0,0 +1,16 @@ +export const levels = [ + { value: '1', label: 'TPS - Très Petite Section' }, + { value: '2', label: 'PS - Petite Section' }, + { value: '3', label: 'MS - Moyenne Section' }, + { value: '4', label: 'GS - Grande Section' }, + { value: '5', label: 'CP' }, + { value: '6', label: 'CE1' }, + { value: '7', label: 'CE2' }, + { value: '8', label: 'CM1' }, + { value: '9', label: 'CM2' }, +]; + +export const genders = [ + { value: '1', label: 'Garçon' }, + { value: '2', label: 'Fille' }, +];