From 5a7661db93454b9a73b9f6bd46646c6135a0f203 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sun, 6 Apr 2025 20:45:41 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Ajout=20de=20la=20s=C3=A9lection=20des?= =?UTF-8?q?=20modes=20de=20paiements=20/=20refactoring=20de=20l'automate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Subscriptions/Configuration/automate.json | 91 +++++++++++-------- Back-End/Subscriptions/automate.py | 15 +-- Back-End/Subscriptions/mailManager.py | 2 +- Back-End/Subscriptions/models.py | 17 ++-- Back-End/Subscriptions/serializers.py | 11 ++- Back-End/Subscriptions/tasks.py | 4 +- .../templates/pdfs/dossier_inscription.html | 9 +- .../templatetags/myTemplateTag.py | 10 +- Back-End/Subscriptions/util.py | 61 +++++-------- .../views/register_form_views.py | 63 ++++++------- .../subscriptions/editInscription/page.js | 11 +-- .../app/[locale]/admin/subscriptions/page.js | 26 +++--- Front-End/src/components/FileStatusLabel.js | 2 +- .../Inscription/InscriptionFormShared.js | 78 +++++++++++----- .../Inscription/PaymentMethodSelector.js | 34 +++++++ .../components/Inscription/StudentInfoForm.js | 34 ++++++- Front-End/src/components/StatusLabel.js | 6 +- .../Structure/Tarification/FeesManagement.js | 1 - Front-End/src/context/EstablishmentContext.js | 1 - 19 files changed, 286 insertions(+), 190 deletions(-) create mode 100644 Front-End/src/components/Inscription/PaymentMethodSelector.js diff --git a/Back-End/Subscriptions/Configuration/automate.json b/Back-End/Subscriptions/Configuration/automate.json index a142479..d14852d 100644 --- a/Back-End/Subscriptions/Configuration/automate.json +++ b/Back-End/Subscriptions/Configuration/automate.json @@ -1,68 +1,79 @@ { "states": [ - "ABSENT", - "CREE", - "ENVOYE", - "EN_VALIDATION", - "A_RELANCER", - "VALIDE", - "ARCHIVE" + "IDLE", + "INITIALIZED", + "SENT", + "UNDER_REVIEW", + "TO_BE_FOLLOWED_UP", + "VALIDATED", + "ARCHIVED", + "SEPA_SENT" ], "transitions": [ { - "name": "creationDI", - "from": "ABSENT", - "to": "CREE" + "name": "EVENT_INIT", + "from": "IDLE", + "to": "INITIALIZED" }, { - "name": "envoiDI", - "from": "CREE", - "to": "ENVOYE" + "name": "EVENT_SEND", + "from": "INITIALIZED", + "to": "SENT" }, { - "name": "archiveDI", - "from": "CREE", - "to": "ARCHIVE" + "name": "EVENT_ARCHIVE", + "from": "INITIALIZED", + "to": "ARCHIVED" }, { - "name": "saisiDI", - "from": "ENVOYE", - "to": "EN_VALIDATION" + "name": "EVENT_SIGNATURE", + "from": "SENT", + "to": "UNDER_REVIEW" }, { - "name": "relanceDI", - "from": "ENVOYE", - "to": "A_RELANCER" + "name": "EVENT_FOLLOW_UP", + "from": "SENT", + "to": "TO_BE_FOLLOWED_UP" }, { - "name": "archiveDI", - "from": "A_RELANCER", - "to": "ARCHIVE" + "name": "EVENT_ARCHIVE", + "from": "SENT", + "to": "ARCHIVED" }, { - "name": "archiveDI", - "from": "ENVOYE", - "to": "ARCHIVE" + "name": "EVENT_ARCHIVE", + "from": "TO_BE_FOLLOWED_UP", + "to": "ARCHIVED" }, { - "name": "refuseDI", - "from": "EN_VALIDATION", - "to": "ENVOYE" + "name": "EVENT_REFUSE", + "from": "UNDER_REVIEW", + "to": "SENT" }, { - "name": "valideDI", - "from": "EN_VALIDATION", - "to": "VALIDE" + "name": "EVENT_VALIDATE", + "from": "UNDER_REVIEW", + "to": "VALIDATED" }, { - "name": "archiveDI", - "from": "EN_VALIDATION", - "to": "ARCHIVE" + "name": "EVENT_SEND_SEPA", + "from": "UNDER_REVIEW", + "to": "SEPA_SENT" }, { - "name": "archiveDI", - "from": "VALIDE", - "to": "ARCHIVE" + "name": "EVENT_ARCHIVE", + "from": "UNDER_REVIEW", + "to": "ARCHIVED" + }, + { + "name": "EVENT_SIGNATURE_SEPA", + "from": "SEPA_SENT", + "to": "UNDER_REVIEW" + }, + { + "name": "EVENT_ARCHIVE", + "from": "VALIDATED", + "to": "ARCHIVED" } ] } diff --git a/Back-End/Subscriptions/automate.py b/Back-End/Subscriptions/automate.py index 5b8180f..91b8fa2 100644 --- a/Back-End/Subscriptions/automate.py +++ b/Back-End/Subscriptions/automate.py @@ -3,13 +3,14 @@ import json from Subscriptions.models import RegistrationForm state_mapping = { - "ABSENT": RegistrationForm.RegistrationFormStatus.RF_ABSENT, - "CREE": RegistrationForm.RegistrationFormStatus.RF_CREATED, - "ENVOYE": RegistrationForm.RegistrationFormStatus.RF_SENT, - "EN_VALIDATION": RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW, - "A_RELANCER": RegistrationForm.RegistrationFormStatus.RF_TO_BE_FOLLOWED_UP, - "VALIDE": RegistrationForm.RegistrationFormStatus.RF_VALIDATED, - "ARCHIVE": RegistrationForm.RegistrationFormStatus.RF_ARCHIVED + "IDLE": RegistrationForm.RegistrationFormStatus.RF_IDLE, + "INITIALIZED": RegistrationForm.RegistrationFormStatus.RF_INITIALIZED, + "SENT": RegistrationForm.RegistrationFormStatus.RF_SENT, + "UNDER_REVIEW": RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW, + "TO_BE_FOLLOWED_UP": RegistrationForm.RegistrationFormStatus.RF_TO_BE_FOLLOWED_UP, + "VALIDATED": RegistrationForm.RegistrationFormStatus.RF_VALIDATED, + "ARCHIVED": RegistrationForm.RegistrationFormStatus.RF_ARCHIVED, + "SEPA_SENT": RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT } def load_config(config_file): diff --git a/Back-End/Subscriptions/mailManager.py b/Back-End/Subscriptions/mailManager.py index 26342e3..3d393dc 100644 --- a/Back-End/Subscriptions/mailManager.py +++ b/Back-End/Subscriptions/mailManager.py @@ -65,7 +65,7 @@ def envoieRelanceDossierInscription(recipients, code): return errorMessage def isValid(message, fiche_inscription): - # Est-ce que la référence du dossier est VALIDE + # Est-ce que la référence du dossier est VALIDATED subject = message.subject print ("++++ " + subject) responsableMail = message.from_header diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index d1ffd38..3b3703a 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -3,7 +3,7 @@ from django.utils.timezone import now from django.conf import settings from django.utils.translation import gettext_lazy as _ -from School.models import SchoolClass, Fee, Discount +from School.models import SchoolClass, Fee, Discount, PaymentModeType from Auth.models import ProfileRole from Establishment.models import Establishment @@ -62,11 +62,6 @@ class Student(models.Model): MS = 3, _('MS - Moyenne Section') GS = 4, _('GS - Grande Section') - class PaymentMethod(models.IntegerChoices): - NONE = 0, _('Sélection du mode de paiement') - SEPA_DIRECT_DEBIT = 1, _('Prélèvement SEPA') - CHECK = 2, _('Chèques') - last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") gender = models.IntegerField(choices=StudentGender, default=StudentGender.NONE, blank=True) @@ -77,7 +72,6 @@ class Student(models.Model): birth_place = models.CharField(max_length=200, default="", blank=True) birth_postal_code = models.IntegerField(default=0, blank=True) attending_physician = models.CharField(max_length=200, default="", blank=True) - payment_method = models.IntegerField(choices=PaymentMethod, default=PaymentMethod.NONE, blank=True) # Many-to-Many Relationship profiles = models.ManyToManyField('Auth.Profile', blank=True) @@ -184,17 +178,18 @@ class RegistrationTemplateMaster(models.Model): class RegistrationForm(models.Model): class RegistrationFormStatus(models.IntegerChoices): - RF_ABSENT = 0, _('Pas de dossier d\'inscription') - RF_CREATED = 1, _('Dossier d\'inscription créé') + RF_IDLE = 0, _('Pas de dossier d\'inscription') + RF_INITIALIZED = 1, _('Dossier d\'inscription initialisé') RF_SENT = 2, _('Dossier d\'inscription envoyé') RF_UNDER_REVIEW = 3, _('Dossier d\'inscription en cours de validation') RF_TO_BE_FOLLOWED_UP = 4, _('Dossier d\'inscription à relancer') RF_VALIDATED = 5, _('Dossier d\'inscription validé') RF_ARCHIVED = 6, _('Dossier d\'inscription archivé') + RF_SEPA_SENT = 7, _('Mandat SEPA envoyé') # One-to-One Relationship student = models.OneToOneField(Student, on_delete=models.CASCADE, primary_key=True) - status = models.IntegerField(choices=RegistrationFormStatus, default=RegistrationFormStatus.RF_ABSENT) + status = models.IntegerField(choices=RegistrationFormStatus, default=RegistrationFormStatus.RF_IDLE) last_update = models.DateTimeField(auto_now=True) notes = models.CharField(max_length=200, blank=True) registration_link_code = models.CharField(max_length=200, default="", blank=True) @@ -217,6 +212,8 @@ class RegistrationForm(models.Model): blank=True) establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='register_forms') + registration_payment = models.IntegerField(choices=PaymentModeType.choices, null=True, blank=True) + tuition_payment = models.IntegerField(choices=PaymentModeType.choices, null=True, blank=True) def __str__(self): return "RF_" + self.student.last_name + "_" + self.student.first_name diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index 1ed09df..c80357a 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -221,8 +221,8 @@ class RegistrationFormSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): student_data = validated_data.pop('student', None) - fees_data = validated_data.pop('fees', []) - discounts_data = validated_data.pop('discounts', []) + fees_data = validated_data.pop('fees', None) + discounts_data = validated_data.pop('discounts', None) if student_data: student = instance.student StudentSerializer.update(StudentSerializer(), student, student_data) @@ -235,8 +235,11 @@ class RegistrationFormSerializer(serializers.ModelSerializer): instance.save() # Associer les IDs des objets Fee et Discount au RegistrationForm - instance.fees.set([fee.id for fee in fees_data]) - instance.discounts.set([discount.id for discount in discounts_data]) + if fees_data is not None: + instance.fees.set([fee.id for fee in fees_data]) + + if discounts_data is not None: + instance.discounts.set([discount.id for discount in discounts_data]) return instance diff --git a/Back-End/Subscriptions/tasks.py b/Back-End/Subscriptions/tasks.py index e89d2b7..cae3930 100644 --- a/Back-End/Subscriptions/tasks.py +++ b/Back-End/Subscriptions/tasks.py @@ -21,10 +21,10 @@ def check_for_signature_deadlines(): send_notification(dossier) def send_notification(dossier): - logger.debug(f'Dossier en attente.... {dossier} - Positionnement à l\'état A_RELANCER') + logger.debug(f'Dossier en attente.... {dossier} - Positionnement à l\'état TO_BE_FOLLOWED_UP') # Changer l'état de l'automate - updateStateMachine(dossier, 'relanceDI') + updateStateMachine(dossier, 'EVENT_FOLLOW_UP') url = settings.URL_DJANGO + 'GestionMessagerie/message' diff --git a/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html b/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html index cb01e82..f23d394 100644 --- a/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html +++ b/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html @@ -194,8 +194,13 @@

MODALITÉS DE PAIEMENT

- {% with paymentMethod=student|getStudentPaymentMethod %} -

{{ paymentMethod }}

+

Frais d'inscription

+ {% with registrationPayment=student|getRegistrationPaymentMethod %} +

{{ registrationPayment }}

+ {% endwith %} +

Frais de scolarité

+ {% with tuitionPayment=student|getTuitionPaymentMethod %} +

{{ tuitionPayment }}

{% endwith %}
diff --git a/Back-End/Subscriptions/templatetags/myTemplateTag.py b/Back-End/Subscriptions/templatetags/myTemplateTag.py index 67db926..da1a88e 100644 --- a/Back-End/Subscriptions/templatetags/myTemplateTag.py +++ b/Back-End/Subscriptions/templatetags/myTemplateTag.py @@ -1,11 +1,17 @@ from Subscriptions.models import RegistrationForm, Student +from School.models import PaymentModeType from django import template register = template.Library() @register.filter -def getStudentPaymentMethod(pk): +def getRegistrationPaymentMethod(pk): registerForm = RegistrationForm.objects.get(student=pk) - return Student.PaymentMethod(int(registerForm.student.payment_method)).label + return PaymentModeType(registerForm.registration_payment).label + +@register.filter +def getTuitionPaymentMethod(pk): + registerForm = RegistrationForm.objects.get(student=pk) + return PaymentModeType(registerForm.tuition_payment).label @register.filter def getStudentLevel(pk): diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index 21e0a0a..b440d5d 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -93,42 +93,35 @@ def getArgFromRequest(_argument, _request): resultat = data[_argument] return resultat -def merge_files_pdf(filenames, output_filename): +from io import BytesIO +from PyPDF2 import PdfMerger + +def merge_files_pdf(file_paths): """ - Fusionne plusieurs fichiers PDF en un seul document. - Vérifie l'existence des fichiers sources avant la fusion. + Fusionne plusieurs fichiers PDF et retourne le contenu fusionné en mémoire. """ merger = PdfMerger() - valid_files = [] - - # Vérifier l'existence des fichiers et ne garder que ceux qui existent - print(f'filenames : {filenames}') - for filename in filenames: - print(f'check exists filename : {filename}') - if os.path.exists(filename): - print(f'append filename : {filename}') - valid_files.append(filename) - - if not valid_files: - raise FileNotFoundError("Aucun fichier valide à fusionner.") # Ajouter les fichiers valides au merger - for filename in valid_files: - merger.append(filename) + for file_path in file_paths: + merger.append(file_path) - # S'assurer que le dossier de destination existe - os.makedirs(os.path.dirname(output_filename), exist_ok=True) - - # Sauvegarder le fichier fusionné - merger.write(output_filename) + # Sauvegarder le fichier fusionné en mémoire + merged_pdf = BytesIO() + merger.write(merged_pdf) merger.close() - return output_filename + # Revenir au début du fichier en mémoire + merged_pdf.seek(0) + + return merged_pdf def rfToPDF(registerForm, filename): """ Génère le PDF d'un dossier d'inscription et l'associe au RegistrationForm. """ + filename = filename.replace(" ", "_") + data = { 'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}", 'signatureDate': convertToStr(_now(), '%d-%m-%Y'), @@ -136,9 +129,6 @@ def rfToPDF(registerForm, filename): 'student': registerForm.student, } - # S'assurer que le dossier parent existe - os.makedirs(os.path.dirname(filename), exist_ok=True) - # Générer le PDF pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) @@ -147,19 +137,14 @@ def rfToPDF(registerForm, filename): os.remove(registerForm.registration_file.path) registerForm.registration_file.delete(save=False) - # Écrire le fichier directement - with open(filename, 'wb') as f: - f.write(pdf.content) + # Enregistrer directement le fichier dans le champ registration_file + registerForm.registration_file.save( + os.path.basename(filename), + File(BytesIO(pdf.content)), # Utilisation de BytesIO pour éviter l'écriture sur le disque + save=True + ) - # Mettre à jour le champ registration_file du registerForm - with open(filename, 'rb') as f: - registerForm.registration_file.save( - os.path.basename(filename), - File(f), - save=True - ) - - return filename + return registerForm.registration_file.path def delete_registration_files(registerForm): """ diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index b9c304e..bb30d4c 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -155,7 +155,7 @@ class RegisterFormView(APIView): di = registerForm_serializer.save() # Mise à jour de l'automate - updateStateMachine(di, 'creationDI') + updateStateMachine(di, 'EVENT_INIT') # Récupération du reponsable associé for guardianId in guardiansId: @@ -231,14 +231,25 @@ class RegisterFormWithIdView(APIView): Modifie un dossier d'inscription donné. """ studentForm_data = JSONParser().parse(request) + _status = studentForm_data.pop('status', 0) studentForm_data["last_update"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M')) + + # Récupérer le dossier d'inscription registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + if not registerForm: + return JsonResponse({"error": "Dossier d'inscription introuvable"}, status=status.HTTP_404_NOT_FOUND) + + studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data, partial=True) + if studentForm_serializer.is_valid(): + studentForm_serializer.save() + else: + return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: try: # Génération de la fiche d'inscription au format PDF - base_dir = f"registration_files/dossier_rf_{registerForm.pk}" + base_dir = f"data/registration_files/dossier_rf_{registerForm.pk}" os.makedirs(base_dir, exist_ok=True) # Fichier PDF initial @@ -248,46 +259,32 @@ class RegisterFormWithIdView(APIView): # Récupération des fichiers d'inscription fileNames = RegistrationTemplate.get_files_from_rf(registerForm.pk) - if registerForm.registration_file: - fileNames.insert(0, registerForm.registration_file.path) + fileNames.insert(0, registerForm.registration_file.path) # Création du fichier PDF Fusionné - merged_pdf = f"{base_dir}/dossier_complet_{registerForm.pk}.pdf" - - util.merge_files_pdf(fileNames, merged_pdf) + merged_pdf_content = util.merge_files_pdf(fileNames) # Mise à jour du champ registration_file avec le fichier fusionné - with open(merged_pdf, 'rb') as f: - registerForm.registration_file.save( - os.path.basename(merged_pdf), - File(f), - save=True - ) - + registerForm.registration_file.save( + f"dossier_complet_{registerForm.pk}.pdf", + File(merged_pdf_content), + save=True + ) # Mise à jour de l'automate - updateStateMachine(registerForm, 'saisiDI') + updateStateMachine(registerForm, 'EVENT_SIGNATURE') except Exception as e: return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: - # L'école a validé le dossier d'inscription - # Mise à jour de l'automate - updateStateMachine(registerForm, 'valideDI') - elif _status == RegistrationForm.RegistrationFormStatus.RF_SENT: - # Vérifier si l'étape précédente était RF_UNDER_REVIEW - if registerForm.status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: - # Mise à jour de l'automate - updateStateMachine(registerForm, 'refuseDI') - # Supprimer le fichier et le dossier associés + elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: + updateStateMachine(registerForm, 'EVENT_VALIDATE') + elif _status == RegistrationForm.RegistrationFormStatus.RF_SENT: + if registerForm.status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: + updateStateMachine(registerForm, 'EVENT_REFUSE') util.delete_registration_files(registerForm) - studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data) - if studentForm_serializer.is_valid(): - studentForm_serializer.save() - return JsonResponse(studentForm_serializer.data, safe=False) - - return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + # Retourner les données mises à jour + return JsonResponse(studentForm_serializer.data, safe=False) @swagger_auto_schema( responses={204: 'No Content'}, @@ -334,7 +331,7 @@ def send(request,id): errorMessage = mailer.sendRegisterForm(email, register_form.establishment.pk) if errorMessage == '': register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - updateStateMachine(register_form, 'envoiDI') + updateStateMachine(register_form, 'EVENT_SEND') return JsonResponse({"message": f"Le dossier d'inscription a bien été envoyé à l'addresse {email}"}, safe=False) return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) @@ -356,7 +353,7 @@ def archive(request,id): register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) if register_form != None: register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - updateStateMachine(register_form, 'archiveDI') + updateStateMachine(register_form, 'EVENT_ARCHIVE') return JsonResponse({"message": "Le dossier a été archivé avec succès"}, safe=False) return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js index 72afd98..4f7ecd7 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js @@ -4,27 +4,21 @@ import { useSearchParams, useRouter } from 'next/navigation'; import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared'; import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url'; import { useCsrfToken } from '@/context/CsrfContext'; +import { useEstablishment } from '@/context/EstablishmentContext'; import { editRegisterForm } from '@/app/actions/subscriptionAction'; import logger from '@/utils/logger'; -const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; export default function Page() { const router = useRouter(); const searchParams = useSearchParams(); - const idProfil = searchParams.get('id'); const studentId = searchParams.get('studentId'); // Changé de codeDI à studentId - const [initialData, setInitialData] = useState(null); - const [formErrors, setFormErrors] = useState({}); const csrfToken = useCsrfToken(); + const { selectedEstablishmentId } = useEstablishment(); const handleSubmit = (data) => { - if (useFakeData) { - logger.debug('Fake submit:', data); - return; - } editRegisterForm(studentId, data, csrfToken) @@ -46,6 +40,7 @@ export default function Page() { { const actions = { 1: [ { - icon: , - onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name), + icon: , + onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`, }, { - icon: , - onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`, + icon: , + onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name), }, ], 2: [ { - icon: , + icon: , onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`, }, ], 3: [ { - icon: , + icon: , onClick: () => openModalAssociationEleve(row.student), }, { - icon: , + icon: , onClick: () => refuseRegistrationForm(row.student.id, row.student.last_name, row.student.first_name, row.student.guardians[0].associated_profile_email), }, ], 5: [ { - icon: , + icon: , onClick: () => openModalAssociationEleve(row.student), }, ], default: [ { - icon: , + icon: , onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name), }, ], @@ -703,14 +703,14 @@ const columnsSubscribed = [ items={[ { label: ( <> - Rattacher + Rattacher ), onClick: () => openModalAssociationEleve(row.student) }, { label: ( <> - Archiver + Archiver ), onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name), diff --git a/Front-End/src/components/FileStatusLabel.js b/Front-End/src/components/FileStatusLabel.js index 29330c5..21a3a1a 100644 --- a/Front-End/src/components/FileStatusLabel.js +++ b/Front-End/src/components/FileStatusLabel.js @@ -6,7 +6,7 @@ const FileStatusLabel = ({ status }) => { switch (status) { case 'sent': return { - label: 'Envoyé', + label: 'En attente', className: 'bg-green-50 text-green-600', icon: }; diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index 8bbc4c3..7713b16 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -1,20 +1,17 @@ // Import des dépendances nécessaires import React, { useState, useEffect } from 'react'; -import InputText from '@/components/InputText'; -import SelectChoice from '@/components/SelectChoice'; -import ResponsableInputFields from '@/components/Inscription/ResponsableInputFields'; import Loader from '@/components/Loader'; import Button from '@/components/Button'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; -import Table from '@/components/Table'; import { fetchRegisterForm, fetchTemplatesFromRegistrationFiles } from '@/app/actions/subscriptionAction'; -import { fetchRegistrationFileFromGroup, - fetchRegistrationTemplateMaster, - downloadTemplate, - createRegistrationTemplates, - editRegistrationTemplates, - deleteRegistrationTemplates - } from '@/app/actions/registerFileGroupAction'; +import { downloadTemplate, + createRegistrationTemplates, + editRegistrationTemplates, + deleteRegistrationTemplates +} from '@/app/actions/registerFileGroupAction'; +import { + fetchRegistrationPaymentModes, + fetchTuitionPaymentModes } from '@/app/actions/schoolAction'; import { Download, Upload, Trash2, Eye } from 'lucide-react'; import { BASE_URL } from '@/utils/Url'; import DraggableFileUpload from '@/components/DraggableFileUpload'; @@ -22,10 +19,8 @@ import Modal from '@/components/Modal'; import FileStatusLabel from '@/components/FileStatusLabel'; import logger from '@/utils/logger'; import StudentInfoForm, { validateStudentInfo } from '@/components/Inscription/StudentInfoForm'; -import FilesToSign from '@/components/Inscription/FilesToSign'; import FilesToUpload from '@/components/Inscription/FilesToUpload'; import { DocusealForm } from '@docuseal/react'; -import { ESTABLISHMENT_ID } from '@/utils/Url'; /** * Composant de formulaire d'inscription partagé @@ -38,6 +33,7 @@ import { ESTABLISHMENT_ID } from '@/utils/Url'; export default function InscriptionFormShared({ studentId, csrfToken, + selectedEstablishmentId, onSubmit, cancelUrl, errors = {} // Nouvelle prop pour les erreurs @@ -54,11 +50,16 @@ export default function InscriptionFormShared({ birth_postal_code: '', nationality: '', attending_physician: '', - level: '' + level: '', + registration_payment: '', + tuition_payment: '' }); const [guardians, setGuardians] = useState([]); + const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]); + const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]); + // États pour la gestion des fichiers const [uploadedFiles, setUploadedFiles] = useState([]); const [fileTemplates, setFileTemplates] = useState([]); @@ -95,7 +96,11 @@ export default function InscriptionFormShared({ birth_postal_code: data?.student?.birth_postal_code || '', nationality: data?.student?.nationality || '', attending_physician: data?.student?.attending_physician || '', - level: data?.student?.level || '' + level: data?.student?.level || '', + registration_payment: data?.registration_payment || '', + tuition_payment: data?.tuition_payment || '', + totalRegistrationFees: data?.totalRegistrationFees, + totalTuitionFees: data?.totalTuitionFees, }); setGuardians(data?.student?.guardians || []); setUploadedFiles(data.registration_files || []); @@ -111,6 +116,34 @@ export default function InscriptionFormShared({ }) }, []); + useEffect(() => { + if (selectedEstablishmentId) { + // Fetch data for registration payment modes + handleRegistrationPaymentModes(); + + // Fetch data for tuition payment modes + handleTuitionPaymentModes(); + } + }, [selectedEstablishmentId]); + + const handleRegistrationPaymentModes = () => { + fetchRegistrationPaymentModes(selectedEstablishmentId) + .then(data => { + const activePaymentModes = data.filter(mode => mode.is_active === true); + setRegistrationPaymentModes(activePaymentModes); + }) + .catch(error => logger.error('Error fetching registration payment modes:', error)); + }; + + const handleTuitionPaymentModes = () => { + fetchTuitionPaymentModes(selectedEstablishmentId) + .then(data => { + const activePaymentModes = data.filter(mode => mode.is_active === true); + setTuitionPaymentModes(activePaymentModes); + }) + .catch(error => logger.error('Error fetching tuition payment modes:', error)); + }; + // Fonctions de gestion du formulaire et des fichiers const updateFormField = (field, value) => { setFormData(prev => ({...prev, [field]: value})); @@ -186,8 +219,10 @@ export default function InscriptionFormShared({ ...formData, guardians }, - establishment: 1, - status:3 + establishment: selectedEstablishmentId, + status:3, + tuition_payment:formData.tuition_payment, + registration_payment:formData.registration_payment } onSubmit(data); }; @@ -200,16 +235,11 @@ export default function InscriptionFormShared({ ...formData, guardians }, - establishment: 1 + establishment: selectedEstablishmentId } onSubmit(data); }; - // Récupération des messages d'erreur - const getError = (field) => { - return errors?.student?.[field]?.[0]; - }; - const handleNextPage = () => { setCurrentPage(currentPage + 1); }; @@ -290,6 +320,8 @@ export default function InscriptionFormShared({ updateFormField={updateFormField} guardians={guardians} setGuardians={setGuardians} + registrationPaymentModes={registrationPaymentModes} + tuitionPaymentModes={tuitionPaymentModes} errors={errors} /> )} diff --git a/Front-End/src/components/Inscription/PaymentMethodSelector.js b/Front-End/src/components/Inscription/PaymentMethodSelector.js new file mode 100644 index 0000000..4e84e53 --- /dev/null +++ b/Front-End/src/components/Inscription/PaymentMethodSelector.js @@ -0,0 +1,34 @@ +import React from 'react'; +import SelectChoice from '@/components/SelectChoice'; + +export default function PaymentMethodSelector({ formData, title, name, updateFormField, selected, paymentModes, paymentModesOptions, amount, getError }) { + console.log(paymentModes) + console.log(selected) + return ( +
+ {/* Titre */} +

{title}

+ + {/* Section d'information */} +
+

+ Montant : {amount} € +

+
+ + updateFormField(name, e.target.value)} + choices={paymentModes.map((mode) => ({ + value: mode.mode, + label: paymentModesOptions.find(option => option.id === mode.mode)?.name || 'Mode inconnu' + }))} + required + errorMsg={getError('payment_method')} + /> +
+ ); +} \ No newline at end of file diff --git a/Front-End/src/components/Inscription/StudentInfoForm.js b/Front-End/src/components/Inscription/StudentInfoForm.js index a929ca5..8e92f21 100644 --- a/Front-End/src/components/Inscription/StudentInfoForm.js +++ b/Front-End/src/components/Inscription/StudentInfoForm.js @@ -2,6 +2,7 @@ import React from 'react'; import InputText from '@/components/InputText'; import SelectChoice from '@/components/SelectChoice'; import ResponsableInputFields from '@/components/Inscription/ResponsableInputFields'; +import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector'; const levels = [ { value:'1', label: 'TPS - Très Petite Section'}, @@ -10,6 +11,13 @@ const levels = [ { value:'4', label: 'GS - Grande Section'}, ]; +const paymentModesOptions = [ + { id: 1, name: 'Prélèvement SEPA' }, + { id: 2, name: 'Virement' }, + { id: 3, name: 'Chèque' }, + { id: 4, name: 'Espèce' }, + ]; + // Fonction de validation pour vérifier les champs requis export function validateStudentInfo(formData) { const requiredFields = [ @@ -32,7 +40,7 @@ export function validateStudentInfo(formData) { return isValid; } -export default function StudentInfoForm({ formData, updateFormField, guardians, setGuardians, errors }) { +export default function StudentInfoForm({ formData, updateFormField, guardians, setGuardians, registrationPaymentModes, tuitionPaymentModes, errors }) { const getError = (field) => { return errors?.student?.[field]?.[0]; }; @@ -143,6 +151,30 @@ export default function StudentInfoForm({ formData, updateFormField, guardians, errors={errors?.student?.guardians || []} /> + + + + ); } \ No newline at end of file diff --git a/Front-End/src/components/StatusLabel.js b/Front-End/src/components/StatusLabel.js index 142c0e2..c780089 100644 --- a/Front-End/src/components/StatusLabel.js +++ b/Front-End/src/components/StatusLabel.js @@ -5,9 +5,9 @@ import DropdownMenu from './DropdownMenu'; const StatusLabel = ({ status, onChange, showDropdown = true }) => { const [dropdownOpen, setDropdownOpen] = useState(false); const statusOptions = [ - { value: 1, label: 'Créé' }, - { value: 2, label: 'Envoyé' }, - { value: 3, label: 'En Validation' }, + { value: 1, label: 'A envoyer' }, + { value: 2, label: 'En attente' }, + { value: 3, label: 'Signé' }, { value: 4, label: 'A Relancer' }, { value: 5, label: 'Validé' }, { value: 6, label: 'Archivé' }, diff --git a/Front-End/src/components/Structure/Tarification/FeesManagement.js b/Front-End/src/components/Structure/Tarification/FeesManagement.js index 685ec72..dfc2037 100644 --- a/Front-End/src/components/Structure/Tarification/FeesManagement.js +++ b/Front-End/src/components/Structure/Tarification/FeesManagement.js @@ -4,7 +4,6 @@ import DiscountsSection from '@/components/Structure/Tarification/DiscountsSecti import PaymentPlanSelector from '@/components/PaymentPlanSelector'; import PaymentModeSelector from '@/components/PaymentModeSelector'; import { BE_SCHOOL_FEES_URL, BE_SCHOOL_DISCOUNTS_URL, BE_SCHOOL_PAYMENT_PLANS_URL, BE_SCHOOL_PAYMENT_MODES_URL } from '@/utils/Url'; -import { set } from 'lodash'; const FeesManagement = ({ registrationDiscounts, setRegistrationDiscounts, diff --git a/Front-End/src/context/EstablishmentContext.js b/Front-End/src/context/EstablishmentContext.js index 33e3dd8..d2435fb 100644 --- a/Front-End/src/context/EstablishmentContext.js +++ b/Front-End/src/context/EstablishmentContext.js @@ -16,7 +16,6 @@ export const EstablishmentProvider = ({ children }) => { if (session && session.user) { setUser(session.user); - console.log("getSession"); const userEstablishments = session.user.roles.map(role => ({ id: role.establishment__id, name: role.establishment__name,