diff --git a/Back-End/Subscriptions/Configuration/automate.json b/Back-End/Subscriptions/Configuration/automate.json index 0249aa3..a142479 100644 --- a/Back-End/Subscriptions/Configuration/automate.json +++ b/Back-End/Subscriptions/Configuration/automate.json @@ -44,6 +44,11 @@ "from": "ENVOYE", "to": "ARCHIVE" }, + { + "name": "refuseDI", + "from": "EN_VALIDATION", + "to": "ENVOYE" + }, { "name": "valideDI", "from": "EN_VALIDATION", diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index 622bac5..21e0a0a 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -16,7 +16,9 @@ from enum import Enum import random import string from rest_framework.parsers import JSONParser -import pymupdf +from PyPDF2 import PdfMerger + +import shutil def recupereListeFichesInscription(): """ @@ -96,23 +98,29 @@ def merge_files_pdf(filenames, output_filename): Fusionne plusieurs fichiers PDF en un seul document. Vérifie l'existence des fichiers sources avant la fusion. """ - merger = pymupdf.open() + 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) - # Fusionner les fichiers valides + if not valid_files: + raise FileNotFoundError("Aucun fichier valide à fusionner.") + + # Ajouter les fichiers valides au merger for filename in valid_files: - merger.insert_file(filename) + merger.append(filename) # S'assurer que le dossier de destination existe os.makedirs(os.path.dirname(output_filename), exist_ok=True) # Sauvegarder le fichier fusionné - merger.save(output_filename) + merger.write(output_filename) merger.close() return output_filename @@ -134,6 +142,11 @@ def rfToPDF(registerForm, filename): # Générer le PDF pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) + # Vérifier si un fichier avec le même nom existe déjà et le supprimer + if registerForm.registration_file and os.path.exists(registerForm.registration_file.path): + 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) @@ -146,4 +159,16 @@ def rfToPDF(registerForm, filename): save=True ) - return registerForm.registration_file \ No newline at end of file + return filename + +def delete_registration_files(registerForm): + """ + Supprime le fichier et le dossier associés à un RegistrationForm. + """ + base_dir = f"registration_files/dossier_rf_{registerForm.pk}" + if registerForm.registration_file and os.path.exists(registerForm.registration_file.path): + os.remove(registerForm.registration_file.path) + registerForm.registration_file.delete(save=False) + + if os.path.exists(base_dir): + shutil.rmtree(base_dir) \ No newline at end of file diff --git a/Back-End/Subscriptions/views/guardian_views.py b/Back-End/Subscriptions/views/guardian_views.py index 221c2fd..d04fe84 100644 --- a/Back-End/Subscriptions/views/guardian_views.py +++ b/Back-End/Subscriptions/views/guardian_views.py @@ -4,10 +4,12 @@ from rest_framework.views import APIView from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi -from Subscriptions.models import Guardian, Student +from Subscriptions.models import Guardian, Student, RegistrationForm from Auth.models import ProfileRole from N3wtSchool import bdd +import Subscriptions.util as util + class GuardianView(APIView): """ Gestion des responsables légaux. @@ -74,6 +76,16 @@ class DissociateGuardianView(APIView): # Supprimer le guardian guardian.delete() + # Récupérer le RegistrationForm associé au Student + registerForm = bdd.getObject(RegistrationForm, "student__id", student_id) + if registerForm: + # Réinitialiser le statut en "Créé" + registerForm.status = RegistrationForm.RegistrationFormStatus.RF_CREATED + registerForm.save() + + # Supprimer le fichier et le dossier associés + util.delete_registration_files(registerForm) + return JsonResponse( { "message": f"Le guardian {guardian.last_name} {guardian.first_name} a été dissocié de l'étudiant {student.last_name} {student.first_name}.", diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index 347aa33..b9c304e 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -238,7 +238,7 @@ class RegisterFormWithIdView(APIView): if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: try: # Génération de la fiche d'inscription au format PDF - base_dir = f"data/registration_files/dossier_rf_{registerForm.pk}" + base_dir = f"registration_files/dossier_rf_{registerForm.pk}" os.makedirs(base_dir, exist_ok=True) # Fichier PDF initial @@ -248,11 +248,13 @@ 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) # Création du fichier PDF Fusionné merged_pdf = f"{base_dir}/dossier_complet_{registerForm.pk}.pdf" + util.merge_files_pdf(fileNames, merged_pdf) # Mise à jour du champ registration_file avec le fichier fusionné @@ -271,6 +273,14 @@ class RegisterFormWithIdView(APIView): # 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 + util.delete_registration_files(registerForm) studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data) if studentForm_serializer.is_valid(): diff --git a/Back-End/requirements.txt b/Back-End/requirements.txt index 85047c7..dd546a6 100644 Binary files a/Back-End/requirements.txt and b/Back-End/requirements.txt differ diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 45218ff..628fc89 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -1,7 +1,6 @@ 'use client' import React, { useState, useEffect } from 'react'; import Table from '@/components/Table'; -import {mockFicheInscription} from '@/data/mockFicheInscription'; import Tab from '@/components/Tab'; import { useTranslations } from 'next-intl'; import StatusLabel from '@/components/StatusLabel'; @@ -11,7 +10,7 @@ import Loader from '@/components/Loader'; import AlertWithModal from '@/components/AlertWithModal'; import DropdownMenu from "@/components/DropdownMenu"; import { formatPhoneNumber } from '@/utils/Telephone'; -import { MoreVertical, Send, Edit, Trash2, FileText, CheckCircle, Plus } from 'lucide-react'; +import { MoreVertical, Send, Edit, Trash2, FileText, CheckCircle, Plus, TicketX } from 'lucide-react'; import Modal from '@/components/Modal'; import InscriptionForm from '@/components/Inscription/InscriptionForm' import AffectationClasseForm from '@/components/AffectationClasseForm' @@ -364,8 +363,27 @@ useEffect(()=>{ }); } + 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('Edit fiche inscription with id:', id); + logger.debug(`Mise à jour du statut du dossier d'inscription avec l'ID : ${id} vers le statut : ${newStatus}`); }; const handleSearchChange = (event) => { @@ -447,7 +465,7 @@ useEffect(()=>{ .then(clonedDocument => { // Sauvegarde des templates clonés dans la base de données const cloneData = { - name: `clone_${clonedDocument.id}`, + name: `${templateMaster.name}_${updatedData.guardianFirstName}_${updatedData.guardianLastName}`, slug: clonedDocument.slug, id: clonedDocument.id, master: templateMaster.id, @@ -555,6 +573,80 @@ useEffect(()=>{ }); } + const getActionsByStatus = (row) => { + const actions = { + 1: [ + { + label: ( + <> + Envoyer + + ), + onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name), + }, + { + label: ( + <> + Modifier + + ), + onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`, + }, + ], + 2: [ + { + label: ( + <> + Modifier + + ), + onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`, + }, + ], + 3: [ + { + label: ( + <> + Valider + + ), + onClick: () => openModalAssociationEleve(row.student), + }, + { + label: ( + <> + Refuser + + ), + onClick: () => refuseRegistrationForm(row.student.id, row.student.last_name, row.student.first_name, row.student.guardians[0].associated_profile_email), + }, + ], + 5: [ + { + label: ( + <> + Rattacher + + ), + onClick: () => openModalAssociationEleve(row.student), + }, + ], + default: [ + { + label: ( + <> + Archiver + + ), + onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name), + }, + ], + }; + + // Combine actions for the specific status and default actions + return [...(actions[row.status] || []), ...(row.status !== 6 ? actions.default : [])]; + }; + const columns = [ { name: t('studentName'), transform: (row) => row.student.last_name }, { name: t('studentFistName'), transform: (row) => row.student.first_name }, @@ -588,69 +680,21 @@ const columns = [ { name: t('files'), transform: (row) => (row.registration_file != null) &&( ) }, - { name: 'Actions', transform: (row) => ( - } - items={[ - ...(row.status === 1 ? [{ - label: ( - <> - Envoyer - - ), - onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name), - }] : []), - ...(row.status === 1 ? [{ - label: ( - <> - Modifier - - ), - onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`, - }] : []), - ...(row.status === 2 ? [{ - label: ( - <> - Modifier - - ), - onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`, - }] : []), - ...(row.status === 3 ? [{ - label: ( - <> - Valider - - ), - onClick: () => openModalAssociationEleve(row.student), - }] : []), - ...(row.status === 5 ? [{ - label: ( - <> - Rattacher - - ), - onClick: () => openModalAssociationEleve(row.student), - }] : []), - ...(row.status !== 6 ? [{ - label: ( - <> - Archiver - - ), - onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name), - }] : []), - ]} - buttonClassName="text-gray-400 hover:text-gray-600" - menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center" - /> - ) }, + { name: 'Actions', + transform: (row) => ( + } + items={getActionsByStatus(row)} + buttonClassName="text-gray-400 hover:text-gray-600" + menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center" + /> + ), }, ]; diff --git a/Front-End/src/app/[locale]/parents/page.js b/Front-End/src/app/[locale]/parents/page.js index 185ea8d..ac8b1ea 100644 --- a/Front-End/src/app/[locale]/parents/page.js +++ b/Front-End/src/app/[locale]/parents/page.js @@ -83,7 +83,8 @@ export default function ParentHomePage() { // Définir les colonnes du tableau const childrenColumns = [ - { name: 'Nom', transform: (row) => `${row.student.last_name} ${row.student.first_name}` }, + { name: 'Nom', transform: (row) => `${row.student.last_name}` }, + { name: 'Prénom', transform: (row) => `${row.student.first_name}` }, { name: 'Statut', transform: (row) => ( diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index 1aa1c46..8bbc4c3 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -21,7 +21,7 @@ import DraggableFileUpload from '@/components/DraggableFileUpload'; import Modal from '@/components/Modal'; import FileStatusLabel from '@/components/FileStatusLabel'; import logger from '@/utils/logger'; -import StudentInfoForm from '@/components/Inscription/StudentInfoForm'; +import StudentInfoForm, { validateStudentInfo } from '@/components/Inscription/StudentInfoForm'; import FilesToSign from '@/components/Inscription/FilesToSign'; import FilesToUpload from '@/components/Inscription/FilesToUpload'; import { DocusealForm } from '@docuseal/react'; @@ -70,6 +70,14 @@ export default function InscriptionFormShared({ const [currentPage, setCurrentPage] = useState(1); + const isCurrentPageValid = () => { + if (currentPage === 1) { + const isValid = validateStudentInfo(formData); + return isValid; + } + return true; + }; + // Chargement initial des données // Mettre à jour les données quand initialData change useEffect(() => { @@ -178,12 +186,25 @@ export default function InscriptionFormShared({ ...formData, guardians }, - establishment: ESTABLISHMENT_ID, + establishment: 1, status:3 } onSubmit(data); }; + // Soumission du formulaire + const handleSave = (e) => { + e.preventDefault(); + const data ={ + student: { + ...formData, + guardians + }, + establishment: 1 + } + onSubmit(data); + }; + // Récupération des messages d'erreur const getError = (field) => { return errors?.student?.[field]?.[0]; @@ -276,31 +297,52 @@ export default function InscriptionFormShared({ {/* Pages suivantes : Section Fichiers d'inscription */} {currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
-

{requiredFileTemplates[currentPage - 2].name}

- { - downloadTemplate(requiredFileTemplates[currentPage - 2].slug) - .then((data) => fetch(data)) - .then((response) => response.blob()) - .then((blob) => { - const file = new File([blob], `${requiredFileTemplates[currentPage - 2].name}.pdf`, { type: blob.type }); - const updateData = new FormData(); - updateData.append('file', file); - - return editRegistrationTemplates(requiredFileTemplates[currentPage - 2].id, updateData, csrfToken); - }) - .then((data) => { - logger.debug("EDIT TEMPLATE : ", data); - }) - .catch((error) => { - logger.error("error editing template : ", error); - }); - }} - > - + {/* Titre du document */} +
+

+ {requiredFileTemplates[currentPage - 2].name || "Document sans nom"} +

+

+ {requiredFileTemplates[currentPage - 2].description || "Aucune description disponible pour ce document."} +

+
+ + {/* Affichage du formulaire ou du document */} + {requiredFileTemplates[currentPage - 2].file === "" ? ( + { + downloadTemplate(requiredFileTemplates[currentPage - 2].slug) + .then((data) => fetch(data)) + .then((response) => response.blob()) + .then((blob) => { + const file = new File([blob], `${requiredFileTemplates[currentPage - 2].name}.pdf`, { type: blob.type }); + const updateData = new FormData(); + updateData.append('file', file); + + return editRegistrationTemplates(requiredFileTemplates[currentPage - 2].id, updateData, csrfToken); + }) + .then((data) => { + logger.debug("EDIT TEMPLATE : ", data); + }) + .catch((error) => { + logger.error("error editing template : ", error); + }); + }} + /> + ) : ( +