diff --git a/Back-End/Subscriptions/Configuration/automate.json b/Back-End/Subscriptions/Configuration/automate.json index d14852d..1dc13ad 100644 --- a/Back-End/Subscriptions/Configuration/automate.json +++ b/Back-End/Subscriptions/Configuration/automate.json @@ -30,6 +30,16 @@ "from": "SENT", "to": "UNDER_REVIEW" }, + { + "name": "EVENT_WAITING_FOR_SEPA", + "from": "SENT", + "to": "SEPA_TO_SEND" + }, + { + "name": "EVENT_SEND_SEPA", + "from": "SEPA_TO_SEND", + "to": "SEPA_SENT" + }, { "name": "EVENT_FOLLOW_UP", "from": "SENT", @@ -55,11 +65,6 @@ "from": "UNDER_REVIEW", "to": "VALIDATED" }, - { - "name": "EVENT_SEND_SEPA", - "from": "UNDER_REVIEW", - "to": "SEPA_SENT" - }, { "name": "EVENT_ARCHIVE", "from": "UNDER_REVIEW", diff --git a/Back-End/Subscriptions/automate.py b/Back-End/Subscriptions/automate.py index 91b8fa2..1c28ad9 100644 --- a/Back-End/Subscriptions/automate.py +++ b/Back-End/Subscriptions/automate.py @@ -10,7 +10,8 @@ state_mapping = { "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 + "SEPA_SENT": RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT, + "SEPA_TO_SEND": RegistrationForm.RegistrationFormStatus.RF_SEPA_TO_SEND } def load_config(config_file): diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index a0dd4af..e119336 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -182,6 +182,7 @@ class RegistrationForm(models.Model): RF_VALIDATED = 5, _('Dossier d\'inscription validé') RF_ARCHIVED = 6, _('Dossier d\'inscription archivé') RF_SEPA_SENT = 7, _('Mandat SEPA envoyé') + RF_SEPA_TO_SEND = 8, _('Mandat SEPA à envoyer') # One-to-One Relationship student = models.OneToOneField(Student, on_delete=models.CASCADE, primary_key=True) diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index ab1da49..50d7d74 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -252,6 +252,8 @@ class RegisterFormWithIdView(APIView): return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: + # Le parent a rempli le dossier d'inscription sans sélectionner "Prélèvement par Mandat SEPA" + # L'école doit désormais valider le dossier d'inscription try: # Génération de la fiche d'inscription au format PDF base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}") @@ -304,11 +306,14 @@ class RegisterFormWithIdView(APIView): 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'adresse {email}"}, safe=False) - return JsonResponse({"errorMessage": errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) + if errorMessage != '': + return JsonResponse({"errorMessage": errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) + registerForm.last_update = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + updateStateMachine(registerForm, 'EVENT_SEND_SEPA') + elif _status == RegistrationForm.RegistrationFormStatus.RF_SEPA_TO_SEND: + # Le parent a rempli le dossier d'inscription en sélectionnant "Prélèvement par Mandat SEPA" + # L'école doit désormais envoyer le mandat SEPA pour poursuivre l'inscription + updateStateMachine(registerForm, 'EVENT_WAITING_FOR_SEPA') # Retourner les données mises à jour return JsonResponse(studentForm_serializer.data, safe=False) diff --git a/Back-End/Subscriptions/views/student_views.py b/Back-End/Subscriptions/views/student_views.py index 8abcd99..70c590d 100644 --- a/Back-End/Subscriptions/views/student_views.py +++ b/Back-End/Subscriptions/views/student_views.py @@ -101,7 +101,8 @@ class ChildrenListView(APIView): status__in=[ RegistrationForm.RegistrationFormStatus.RF_SENT, RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW, - RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT + RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT, + RegistrationForm.RegistrationFormStatus.RF_SEPA_TO_SEND ] ).distinct() students_serializer = RegistrationFormByParentSerializer(students, many=True) 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 b4eb1d0..a968c10 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js @@ -7,6 +7,7 @@ import { useCsrfToken } from '@/context/CsrfContext'; import { useEstablishment } from '@/context/EstablishmentContext'; import { editRegisterForm } from '@/app/actions/subscriptionAction'; import logger from '@/utils/logger'; +import Loader from '@/components/Loader'; export default function Page() { const router = useRouter(); @@ -16,14 +17,18 @@ export default function Page() { const [formErrors, setFormErrors] = useState({}); const csrfToken = useCsrfToken(); const { selectedEstablishmentId } = useEstablishment(); + const [isLoading, setIsLoading] = useState(false); const handleSubmit = (data) => { + setIsLoading(true); editRegisterForm(studentId, data, csrfToken) .then((result) => { + setIsLoading(false); logger.debug('Success:', result); router.push(FE_ADMIN_SUBSCRIPTIONS_URL); }) .catch((error) => { + setIsLoading(false); logger.error('Error:', error.message); if (error.details) { logger.error('Form errors:', error.details); @@ -32,6 +37,10 @@ export default function Page() { }); }; + if (isLoading) { + return ; + } + return ( {}); + + const [isSepaUploadModalOpen, setIsSepaUploadModalOpen] = useState(false); + const [selectedRowForUpload, setSelectedRowForUpload] = useState(null); + const csrfToken = useCsrfToken(); const router = useRouter(); const { selectedEstablishmentId } = useEstablishment(); + const openSepaUploadModal = (row) => { + setSelectedRowForUpload(row); + setIsSepaUploadModalOpen(true); + }; + + const closeSepaUploadModal = () => { + setSelectedRowForUpload(null); + setIsSepaUploadModalOpen(false); + }; + const openModal = () => { setIsOpen(true); }; @@ -221,28 +237,6 @@ export default function Page({ params: { locale } }) { } }; - useEffect(() => { - if (selectedEstablishmentId) { - const fetchInitialData = () => { - Promise.all([ - fetchClasses(selectedEstablishmentId), - fetchStudents(selectedEstablishmentId), - ]) - .then(([classesData, studentsData]) => { - setClasses(classesData); - setEleves(studentsData); - logger.debug('Success - Classes:', classesData); - logger.debug('Success - Students:', studentsData); - }) - .catch((error) => { - logger.error('Error fetching initial data:', error); - }); - }; - - fetchInitialData(); - } - }, [selectedEstablishmentId]); - useEffect(() => { if (selectedEstablishmentId) { const fetchDataAndSetState = () => { @@ -257,6 +251,19 @@ export default function Page({ params: { locale } }) { ) .then(registerFormPendingDataHandler) .catch(requestErrorHandler), + + fetchClasses(selectedEstablishmentId) + .then((classesData) => { + setClasses(classesData); + }) + .catch(requestErrorHandler), + + fetchStudents(selectedEstablishmentId) + .then((studentsData) => { + setEleves(studentsData); + }) + .catch(requestErrorHandler), + fetchRegisterForms(selectedEstablishmentId, SUBSCRIBED) .then(registerFormSubscribedDataHandler) .catch(requestErrorHandler), @@ -378,6 +385,33 @@ export default function Page({ params: { locale } }) { setTotalPages(Math.ceil(totalArchives / itemsPerPage)); } }, [currentPage]); + + const handleSepaFileUpload = (file, row) => { + if (!file || !row) { + logger.error("Aucun fichier ou ligne sélectionnée pour l'upload."); + return; + } + + const formData = new FormData(); + formData.append('status', 7); + formData.append('sepa_file', file); + + // Appeler l'API pour uploader le fichier SEPA + sendSEPARegisterForm(row.student.id, formData, csrfToken) + .then((response) => { + logger.debug('Mandat SEPA uploadé avec succès :', response); + setPopupMessage('Le mandat SEPA a été uploadé avec succès.'); + setPopupVisible(true); + setReloadFetch(true); + closeSepaUploadModal(); + }) + .catch((error) => { + logger.error("Erreur lors de l'upload du mandat SEPA :", error); + setPopupMessage("Erreur lors de l'upload du mandat SEPA."); + setPopupVisible(true); + }); + }; + /** * Archives a registration form after user confirmation. * @@ -386,44 +420,56 @@ export default function Page({ params: { locale } }) { * @param {string} prenom - The first name of the person whose registration form is being archived. */ const archiveFicheInscription = (id, nom, prenom) => { - setPopup({ - visible: true, - message: `Attentions ! \nVous êtes sur le point d'archiver le dossier d'inscription de ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`, - onConfirm: () => { - archiveRegisterForm(id) - .then((data) => { - logger.debug('Success:', data); - setRegistrationForms( - registrationForms.filter((fiche) => fiche.id !== id) - ); - setReloadFetch(true); - alert("Le dossier d'inscription a été correctement archivé"); - }) - .catch((error) => { - logger.error('Error archiving data:', error); - alert( - "Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur." - ); - }); - }, + setConfirmPopupMessage( + `Attentions ! \nVous êtes sur le point d'archiver le dossier d'inscription de ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?` + ); + setConfirmPopupOnConfirm(() => () => { + archiveRegisterForm(id) + .then((data) => { + logger.debug('Success:', data); + setPopupMessage( + `Le dossier d'inscription a été correctement archivé` + ); + setPopupVisible(true); + setRegistrationForms( + registrationForms.filter((fiche) => fiche.id !== id) + ); + setReloadFetch(true); + }) + .catch((error) => { + logger.error('Error archiving data:', error); + setPopupMessage( + `Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur.` + ); + setPopupVisible(true); + }); + setConfirmPopupVisible(false); }); + setConfirmPopupVisible(true); }; const sendConfirmRegisterForm = (id, nom, prenom) => { - setPopup({ - visible: true, - message: `Avertissement ! \nVous êtes sur le point d'envoyer un dossier d'inscription à ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`, - onConfirm: () => { - sendRegisterForm(id) - .then((data) => { - logger.debug('Success:', data); - setReloadFetch(true); - }) - .catch((error) => { - logger.error('Error fetching data:', error); - }); - }, + setConfirmPopupMessage( + `Avertissement ! \nVous êtes sur le point d'envoyer un dossier d'inscription à ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?` + ); + setConfirmPopupOnConfirm(() => () => { + sendRegisterForm(id) + .then((data) => { + logger.debug('Success:', data); + setPopupMessage(`Le dossier d'inscription a été envoyé avec succès`); + setPopupVisible(true); + setReloadFetch(true); + }) + .catch((error) => { + logger.error('Error archiving data:', error); + setPopupMessage( + `Erreur lors de l'envoi du dossier d'inscription.\nContactez l'administrateur.` + ); + setPopupVisible(true); + }); + setConfirmPopupVisible(false); }); + setConfirmPopupVisible(true); }; const affectationClassFormSubmitHandler = (formdata) => { @@ -437,25 +483,6 @@ export default function Page({ params: { locale } }) { }); }; - const refuseRegistrationForm = (id, lastname, firstname, guardianEmail) => { - const data = { status: 2, establishment: selectedEstablishmentId }; - - setPopup({ - visible: true, - message: `Avertissement ! \nVous êtes sur le point de refuser le dossier d'inscription de ${lastname} ${firstname}\nUne notification va être envoyée à l'adresse ${guardianEmail}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`, - onConfirm: () => { - editRegisterForm(id, data, csrfToken) - .then((data) => { - logger.debug('Success:', data); - setReloadFetch(true); - }) - .catch((error) => { - logger.error('Error refusing RF:', error); - }); - }, - }); - }; - const updateStatusAction = (id, newStatus) => { logger.debug( `Mise à jour du statut du dossier d'inscription avec l'ID : ${id} vers le statut : ${newStatus}` @@ -551,6 +578,7 @@ export default function Page({ params: { locale } }) { establishment: selectedEstablishmentId, }; + setIsLoading(true); createRegisterForm(data, csrfToken) .then((data) => { // Cloner les schoolFileTemplates pour chaque templateMaster du fileGroup @@ -582,6 +610,7 @@ export default function Page({ params: { locale } }) { logger.debug('Template enregistré avec succès:', response); }) .catch((error) => { + setIsLoading(false); logger.error( "Erreur lors de l'enregistrement du template:", error @@ -589,6 +618,7 @@ export default function Page({ params: { locale } }) { }); }) .catch((error) => { + setIsLoading(false); logger.error('Error during cloning or sending:', error); }); }); @@ -608,6 +638,7 @@ export default function Page({ params: { locale } }) { logger.debug('Parent template enregistré avec succès:', response); }) .catch((error) => { + setIsLoading(false); logger.error( "Erreur lors de l'enregistrement du parent template:", error @@ -634,12 +665,15 @@ export default function Page({ params: { locale } }) { 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); }); }; @@ -810,6 +844,24 @@ export default function Page({ params: { locale } }) { onClick: () => openFilesModal(row), }, ], + 8: [ + { + icon: ( + + + + ), + onClick: () => openFilesModal(row), + }, + { + icon: ( + + + + ), + onClick: () => openSepaUploadModal(row), + }, + ], default: [ { icon: ( @@ -879,37 +931,6 @@ export default function Page({ params: { locale } }) { ), }, - { - name: t('files'), - transform: (row) => ( - - ), - }, { name: 'Actions', transform: (row) => ( @@ -1116,13 +1137,16 @@ export default function Page({ params: { locale } }) { ) : null} { - popup.onConfirm(); - setPopup({ ...popup, visible: false }); - }} - onCancel={() => setPopup({ ...popup, visible: false })} + visible={popupVisible} + message={popupMessage} + onConfirm={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + setConfirmPopupVisible(false)} /> {isOpen && ( @@ -1176,6 +1200,21 @@ export default function Page({ params: { locale } }) { )} /> )} + {isSepaUploadModalOpen && ( + ( + + handleSepaFileUpload(file, selectedRowForUpload) + } + /> + )} + /> + )} {isFilesModalOpen && ( { const userIdFromSession = user.user_id; setUserId(userIdFromSession); - console.log(selectedEstablishmentId); fetchChildren(userIdFromSession, selectedEstablishmentId).then((data) => { setChildren(data); }); - }, [selectedEstablishmentId]); + setReloadFetch(false); + }, [selectedEstablishmentId, reloadFetch]); function handleView(eleveId) { logger.debug(`View dossier for student id: ${eleveId}`); @@ -70,7 +71,8 @@ export default function ParentHomePage() { sendSEPARegisterForm(uploadingStudentId, formData, csrfToken) .then((response) => { logger.debug('RF mis à jour avec succès:', response); - // Logique supplémentaire après la mise à jour (par exemple, redirection ou notification) + setReloadFetch(true); + setUploadState('off'); }) .catch((error) => { logger.error('Erreur lors de la mise à jour du RF:', error); @@ -118,7 +120,7 @@ export default function ParentHomePage() { )} - {row.status === 3 && ( + {(row.status === 3 || row.status === 8) && (