diff --git a/Back-End/School/management/commands/References/LMDE/mandat_prelevement_sepa_interentreprise.pdf b/Back-End/School/management/commands/References/LMDE/mandat_prelevement_sepa_interentreprise.pdf new file mode 100644 index 0000000..71d16dc Binary files /dev/null and b/Back-End/School/management/commands/References/LMDE/mandat_prelevement_sepa_interentreprise.pdf differ diff --git a/Back-End/Subscriptions/mailManager.py b/Back-End/Subscriptions/mailManager.py index 3d393dc..ddd0648 100644 --- a/Back-End/Subscriptions/mailManager.py +++ b/Back-End/Subscriptions/mailManager.py @@ -26,7 +26,6 @@ def envoieReinitMotDePasse(recipients, code): def sendRegisterForm(recipients, establishment_id): errorMessage = '' try: - print(f'{settings.EMAIL_HOST_USER}') # Préparation du contexte pour le template EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Dossier Inscription' context = { @@ -47,6 +46,29 @@ def sendRegisterForm(recipients, establishment_id): return errorMessage +def sendMandatSEPA(recipients, establishment_id): + errorMessage = '' + try: + # Préparation du contexte pour le template + EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Mandat de prélèvement SEPA' + context = { + 'BASE_URL': settings.BASE_URL, + 'email': recipients, + 'establishment': establishment_id + } + + subject = EMAIL_INSCRIPTION_SUBJECT + html_message = render_to_string('emails/sepa.html', context) + plain_message = strip_tags(html_message) + from_email = settings.EMAIL_HOST_USER + + send_mail(subject, plain_message, from_email, [recipients], html_message=html_message) + + except Exception as e: + errorMessage = str(e) + + return errorMessage + def envoieRelanceDossierInscription(recipients, code): EMAIL_RELANCE_SUBJECT = '[N3WT-SCHOOL] Relance - Dossier Inscription' EMAIL_RELANCE_CORPUS = 'Bonjour,\nN\'ayant pas eu de retour de votre part, nous vous renvoyons le lien vers le formulaire d\'inscription : ' + BASE_URL + '/users/login\nCordialement' diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 42de8e4..86f5b85 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -198,6 +198,11 @@ class RegistrationForm(models.Model): null=True, blank=True ) + sepa_file = models.FileField( + upload_to=registration_file_path, + null=True, + blank=True + ) associated_rf = models.CharField(max_length=200, default="", blank=True) # Many-to-Many Relationship diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index c80357a..36519df 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -10,6 +10,7 @@ from N3wtSchool import settings from django.utils import timezone import pytz from datetime import datetime +import Subscriptions.util as util class RegistrationTemplateMasterSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) @@ -195,6 +196,7 @@ class StudentSerializer(serializers.ModelSerializer): class RegistrationFormSerializer(serializers.ModelSerializer): student = StudentSerializer(many=False, required=False) registration_file = serializers.FileField(required=False) + sepa_file = serializers.FileField(required=False) status_label = serializers.SerializerMethodField() formatted_last_update = serializers.SerializerMethodField() registration_files = RegistrationTemplateSerializer(many=True, required=False) @@ -232,6 +234,8 @@ class RegistrationFormSerializer(serializers.ModelSerializer): setattr(instance, field, validated_data[field]) except KeyError: pass + + instance.last_update = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') instance.save() # Associer les IDs des objets Fee et Discount au RegistrationForm diff --git a/Back-End/Subscriptions/templates/emails/sepa.html b/Back-End/Subscriptions/templates/emails/sepa.html new file mode 100644 index 0000000..556e185 --- /dev/null +++ b/Back-End/Subscriptions/templates/emails/sepa.html @@ -0,0 +1,51 @@ + + + + + Finalisation de l'inscription + + + +
+
+

Finalisation de l'inscription

+
+
+

Bonjour,

+

Un mandat de prélèvement SEPA vous a été envoyé

+

Le document est à votre disposition sur votre espace parent : {{BASE_URL}}/users/login

+

Merci de compléter puis de signer le document afin de valider votre inscription

+

Cordialement,

+

L'équipe N3wt School

+
+ +
+ + \ 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 bb30d4c..b803cde 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -1,7 +1,6 @@ from django.http.response import JsonResponse from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect from django.utils.decorators import method_decorator -from rest_framework.parsers import JSONParser from rest_framework.views import APIView from rest_framework.decorators import action, api_view from rest_framework import status @@ -230,10 +229,16 @@ class RegisterFormWithIdView(APIView): """ Modifie un dossier d'inscription donné. """ - studentForm_data = JSONParser().parse(request) + # Récupérer les données de la requête + studentForm_data = request.data.copy() + + logger.info(f"Mise à jour du dossier d'inscription {studentForm_data}") _status = studentForm_data.pop('status', 0) - studentForm_data["last_update"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M')) + if isinstance(_status, list): # Cas Multipart/data, les données sont envoyées sous forme de liste, c'est nul + _status = int(_status[0]) + else: + _status = int(_status) # Récupérer le dossier d'inscription registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) @@ -282,6 +287,17 @@ class RegisterFormWithIdView(APIView): if registerForm.status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: updateStateMachine(registerForm, 'EVENT_REFUSE') util.delete_registration_files(registerForm) + elif _status == RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT: + # Sauvegarde du mandat SEPA + student = registerForm.student + guardian = student.getMainGuardian() + email = guardian.profile_role.profile.email + errorMessage = mailer.sendMandatSEPA(email, registerForm.establishment.pk) + if errorMessage == '': + registerForm.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + updateStateMachine(registerForm, 'EVENT_SEND_SEPA') + return JsonResponse({"message": f"Le mandat SEPA a bien été envoyé à l'addresse {email}"}, safe=False) + return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) # Retourner les données mises à jour return JsonResponse(studentForm_serializer.data, safe=False) diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index c02ee1a..680c723 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -14,6 +14,7 @@ import Modal from '@/components/Modal'; import InscriptionForm from '@/components/Inscription/InscriptionForm' import AffectationClasseForm from '@/components/AffectationClasseForm' import { useEstablishment } from '@/context/EstablishmentContext'; +import ValidateSubscription from '@/components/Inscription/ValidateSubscription'; import { PENDING, @@ -44,7 +45,8 @@ import { createProfile, deleteProfile, fetchProfiles } from '@/app/actions/authA import { BASE_URL, - FE_ADMIN_SUBSCRIPTIONS_EDIT_URL } from '@/utils/Url'; + FE_ADMIN_SUBSCRIPTIONS_EDIT_URL, + FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL } from '@/utils/Url'; import DjangoCSRFToken from '@/components/DjangoCSRFToken' import { useCsrfToken } from '@/context/CsrfContext'; @@ -594,11 +596,7 @@ useEffect(()=>{ 3: [ { icon: , - onClick: () => openModalAssociationEleve(row.student), - }, - { - icon: , - onClick: () => refuseRegistrationForm(row.student.id, row.student.last_name, row.student.first_name, row.student.guardians[0].associated_profile_email), + onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&paymentMode=${row.registration_payment}&file=${row.registration_file}`, }, ], 5: [ diff --git a/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js new file mode 100644 index 0000000..fe5c8e1 --- /dev/null +++ b/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js @@ -0,0 +1,53 @@ +'use client' +import React from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import ValidateSubscription from '@/components/Inscription/ValidateSubscription'; +import { sendSEPARegisterForm } from "@/app/actions/subscriptionAction" +import { useCsrfToken } from '@/context/CsrfContext'; +import logger from '@/utils/logger'; +import { FE_ADMIN_SUBSCRIPTIONS_URL} from '@/utils/Url'; + +export default function Page() { + const searchParams = useSearchParams(); + const router = useRouter(); + + // Récupérer les paramètres de la requête + const studentId = searchParams.get('studentId'); + const firstName = searchParams.get('firstName'); + const lastName = searchParams.get('lastName'); + const paymentMode = searchParams.get('paymentMode'); + const file = searchParams.get('file'); + + const csrfToken = useCsrfToken(); + + const handleAcceptRF = (data) => { + logger.debug('Mise à jour du RF avec les données:', data); + + const {status, sepa_file} = data + const formData = new FormData(); + formData.append('status', status); // Ajoute le statut + formData.append('sepa_file', sepa_file); // Ajoute le fichier SEPA + + // Appeler l'API pour mettre à jour le RF + sendSEPARegisterForm(studentId, formData, csrfToken) + .then((response) => { + logger.debug('RF mis à jour avec succès:', response); + router.push(FE_ADMIN_SUBSCRIPTIONS_URL); + // Logique supplémentaire après la mise à jour (par exemple, redirection ou notification) + }) + .catch((error) => { + logger.error('Erreur lors de la mise à jour du RF:', error); + }); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/Front-End/src/app/[locale]/parents/editInscription/page.js b/Front-End/src/app/[locale]/parents/editInscription/page.js index 8feaf4c..490fc46 100644 --- a/Front-End/src/app/[locale]/parents/editInscription/page.js +++ b/Front-End/src/app/[locale]/parents/editInscription/page.js @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared'; import { useSearchParams, useRouter } from 'next/navigation'; import { useCsrfToken } from '@/context/CsrfContext'; +import { useEstablishment } from '@/context/EstablishmentContext'; import { FE_PARENTS_HOME_URL} from '@/utils/Url'; import { editRegisterForm} from '@/app/actions/subscriptionAction'; import logger from '@/utils/logger'; @@ -13,6 +14,7 @@ export default function Page() { const studentId = searchParams.get('studentId'); const router = useRouter(); const csrfToken = useCsrfToken(); + const { selectedEstablishmentId } = useEstablishment(); const handleSubmit = async (data) => { try { @@ -28,6 +30,7 @@ export default function Page() { diff --git a/Front-End/src/app/[locale]/parents/page.js b/Front-End/src/app/[locale]/parents/page.js index ac8b1ea..2fabe0d 100644 --- a/Front-End/src/app/[locale]/parents/page.js +++ b/Front-End/src/app/[locale]/parents/page.js @@ -62,25 +62,6 @@ export default function ParentHomePage() { { name: 'Action', transform: (row) => row.action }, ]; - const getShadowColor = (status) => { - switch (status) { - case 1: - return 'shadow-blue-500'; // Couleur d'ombre plus visible - case 2: - return 'shadow-orange-500'; // Couleur d'ombre plus visible - case 3: - return 'shadow-purple-500'; // Couleur d'ombre plus visible - case 4: - return 'shadow-red-500'; // Couleur d'ombre plus visible - case 5: - return 'shadow-green-500'; // Couleur d'ombre plus visible - case 6: - return 'shadow-red-500'; // Couleur d'ombre plus visible - default: - return 'shadow-green-500'; // Couleur d'ombre plus visible - } - }; - // Définir les colonnes du tableau const childrenColumns = [ { name: 'Nom', transform: (row) => `${row.student.last_name}` }, @@ -89,7 +70,7 @@ export default function ParentHomePage() { name: 'Statut', transform: (row) => (
- +
) }, diff --git a/Front-End/src/app/actions/registerFileGroupAction.js b/Front-End/src/app/actions/registerFileGroupAction.js index a22fb9d..604ca7b 100644 --- a/Front-End/src/app/actions/registerFileGroupAction.js +++ b/Front-End/src/app/actions/registerFileGroupAction.js @@ -2,7 +2,8 @@ import { BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL, BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL, BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL, FE_API_DOCUSEAL_CLONE_URL, - FE_API_DOCUSEAL_DOWNLOAD_URL + FE_API_DOCUSEAL_DOWNLOAD_URL, + FE_API_DOCUSEAL_GENERATE_TOKEN } from '@/utils/Url'; const requestResponseHandler = async (response) => { @@ -220,4 +221,14 @@ export const downloadTemplate = (slug) => { } }) .then(requestResponseHandler) -} \ No newline at end of file +} + +export const generateToken = (email, id = null) => { + return fetch(`${FE_API_DOCUSEAL_GENERATE_TOKEN}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ user_email: email, id }), + }).then(requestResponseHandler); +}; \ No newline at end of file diff --git a/Front-End/src/app/actions/subscriptionAction.js b/Front-End/src/app/actions/subscriptionAction.js index 9024fa5..7309b38 100644 --- a/Front-End/src/app/actions/subscriptionAction.js +++ b/Front-End/src/app/actions/subscriptionAction.js @@ -56,6 +56,17 @@ export const editRegisterForm=(id, data, csrfToken)=>{ .then(requestResponseHandler) }; +export const sendSEPARegisterForm=(id, data, csrfToken)=>{ + return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, { + method: 'PUT', + headers: { + 'X-CSRFToken': csrfToken + }, + body: data, + credentials: 'include' + }) + .then(requestResponseHandler) +}; export const createRegisterForm=(data, csrfToken)=>{ const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`; diff --git a/Front-End/src/components/Inscription/ValidateSubscription.js b/Front-End/src/components/Inscription/ValidateSubscription.js new file mode 100644 index 0000000..536ce23 --- /dev/null +++ b/Front-End/src/components/Inscription/ValidateSubscription.js @@ -0,0 +1,175 @@ +'use client' +import React, { useState, useEffect } from 'react'; +import { DocusealBuilder } from '@docuseal/react'; +import Button from '@/components/Button'; +import { BASE_URL } from '@/utils/Url'; +import { generateToken } from '@/app/actions/registerFileGroupAction'; +import logger from '@/utils/logger'; +import { GraduationCap, CloudUpload } from 'lucide-react'; + +export default function ValidateSubscription({ studentId, firstName, lastName, paymentMode, file, onAccept }) { + const [token, setToken] = useState(null); + const [uploadedFileName, setUploadedFileName] = useState(''); + const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`); + const [isSepa, setIsSepa] = useState(paymentMode === '1'); // Vérifie si le mode de paiement est SEPA + const [currentPage, setCurrentPage] = useState(1); // Gestion des pages + + useEffect(() => { + if (isSepa) { + generateToken('n3wt.school@gmail.com') + .then((data) => { + setToken(data.token); + }) + .catch((error) => logger.error('Erreur lors de la génération du token:', error)); + } + }, [isSepa]); + + const handleUpload = (detail) => { + logger.debug('Uploaded file detail:', detail); + setUploadedFileName(detail.name); + }; + + const handleAccept = () => { + const fileInput = document.getElementById('fileInput'); // Récupère l'élément input + const file = fileInput?.files[0]; // Récupère le fichier sélectionné + + if (!file) { + logger.error('Aucun fichier sélectionné pour le champ SEPA.'); + return; + } + + const data = { + status: 7, + sepa_file: file, + }; + + // Appeler la fonction passée par le parent pour mettre à jour le RF + onAccept(data); + }; + + const handleRefuse = () => { + logger.debug('Dossier refusé pour l\'étudiant:', studentId); + // Logique pour refuser l'inscription + }; + + const isValidateButtonDisabled = isSepa && !uploadedFileName; + + const goToNextPage = () => { + if (currentPage < (isSepa ? 2 : 1)) { + setCurrentPage(currentPage + 1); + } + }; + + const goToPreviousPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + return ( +
+ {/* Titre */} +
+
+ +
+
+

+ Dossier scolaire de {firstName} {lastName} +

+

+ Année scolaire {new Date().getFullYear()}-{new Date().getFullYear() + 1} +

+
+
+ + {/* Contenu principal */} + {currentPage === 1 && ( +
+