feat: Gestion de la sauvegarde du fichier d'inscription / affichage du

fichier avec le bon nom / possibilité de refuser un DI
This commit is contained in:
N3WT DE COMPET
2025-03-31 20:13:10 +02:00
parent e0bfd3e115
commit d6edf250bb
11 changed files with 281 additions and 102 deletions

View File

@ -44,6 +44,11 @@
"from": "ENVOYE", "from": "ENVOYE",
"to": "ARCHIVE" "to": "ARCHIVE"
}, },
{
"name": "refuseDI",
"from": "EN_VALIDATION",
"to": "ENVOYE"
},
{ {
"name": "valideDI", "name": "valideDI",
"from": "EN_VALIDATION", "from": "EN_VALIDATION",

View File

@ -16,7 +16,9 @@ from enum import Enum
import random import random
import string import string
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
import pymupdf from PyPDF2 import PdfMerger
import shutil
def recupereListeFichesInscription(): def recupereListeFichesInscription():
""" """
@ -96,23 +98,29 @@ def merge_files_pdf(filenames, output_filename):
Fusionne plusieurs fichiers PDF en un seul document. Fusionne plusieurs fichiers PDF en un seul document.
Vérifie l'existence des fichiers sources avant la fusion. Vérifie l'existence des fichiers sources avant la fusion.
""" """
merger = pymupdf.open() merger = PdfMerger()
valid_files = [] valid_files = []
# Vérifier l'existence des fichiers et ne garder que ceux qui existent # Vérifier l'existence des fichiers et ne garder que ceux qui existent
print(f'filenames : {filenames}')
for filename in filenames: for filename in filenames:
print(f'check exists filename : {filename}')
if os.path.exists(filename): if os.path.exists(filename):
print(f'append filename : {filename}')
valid_files.append(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: for filename in valid_files:
merger.insert_file(filename) merger.append(filename)
# S'assurer que le dossier de destination existe # S'assurer que le dossier de destination existe
os.makedirs(os.path.dirname(output_filename), exist_ok=True) os.makedirs(os.path.dirname(output_filename), exist_ok=True)
# Sauvegarder le fichier fusionné # Sauvegarder le fichier fusionné
merger.save(output_filename) merger.write(output_filename)
merger.close() merger.close()
return output_filename return output_filename
@ -134,6 +142,11 @@ def rfToPDF(registerForm, filename):
# Générer le PDF # Générer le PDF
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) 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 # Écrire le fichier directement
with open(filename, 'wb') as f: with open(filename, 'wb') as f:
f.write(pdf.content) f.write(pdf.content)
@ -146,4 +159,16 @@ def rfToPDF(registerForm, filename):
save=True save=True
) )
return registerForm.registration_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)

View File

@ -4,10 +4,12 @@ from rest_framework.views import APIView
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from Subscriptions.models import Guardian, Student from Subscriptions.models import Guardian, Student, RegistrationForm
from Auth.models import ProfileRole from Auth.models import ProfileRole
from N3wtSchool import bdd from N3wtSchool import bdd
import Subscriptions.util as util
class GuardianView(APIView): class GuardianView(APIView):
""" """
Gestion des responsables légaux. Gestion des responsables légaux.
@ -74,6 +76,16 @@ class DissociateGuardianView(APIView):
# Supprimer le guardian # Supprimer le guardian
guardian.delete() 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( return JsonResponse(
{ {
"message": f"Le guardian {guardian.last_name} {guardian.first_name} a été dissocié de l'étudiant {student.last_name} {student.first_name}.", "message": f"Le guardian {guardian.last_name} {guardian.first_name} a été dissocié de l'étudiant {student.last_name} {student.first_name}.",

View File

@ -238,7 +238,7 @@ class RegisterFormWithIdView(APIView):
if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
try: try:
# Génération de la fiche d'inscription au format PDF # 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) os.makedirs(base_dir, exist_ok=True)
# Fichier PDF initial # Fichier PDF initial
@ -248,11 +248,13 @@ class RegisterFormWithIdView(APIView):
# Récupération des fichiers d'inscription # Récupération des fichiers d'inscription
fileNames = RegistrationTemplate.get_files_from_rf(registerForm.pk) fileNames = RegistrationTemplate.get_files_from_rf(registerForm.pk)
if registerForm.registration_file: if registerForm.registration_file:
fileNames.insert(0, registerForm.registration_file.path) fileNames.insert(0, registerForm.registration_file.path)
# Création du fichier PDF Fusionné # Création du fichier PDF Fusionné
merged_pdf = f"{base_dir}/dossier_complet_{registerForm.pk}.pdf" merged_pdf = f"{base_dir}/dossier_complet_{registerForm.pk}.pdf"
util.merge_files_pdf(fileNames, merged_pdf) util.merge_files_pdf(fileNames, merged_pdf)
# Mise à jour du champ registration_file avec le fichier fusionné # 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 # L'école a validé le dossier d'inscription
# Mise à jour de l'automate # Mise à jour de l'automate
updateStateMachine(registerForm, 'valideDI') 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) studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data)
if studentForm_serializer.is_valid(): if studentForm_serializer.is_valid():

Binary file not shown.

View File

@ -1,7 +1,6 @@
'use client' 'use client'
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import {mockFicheInscription} from '@/data/mockFicheInscription';
import Tab from '@/components/Tab'; import Tab from '@/components/Tab';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import StatusLabel from '@/components/StatusLabel'; import StatusLabel from '@/components/StatusLabel';
@ -11,7 +10,7 @@ import Loader from '@/components/Loader';
import AlertWithModal from '@/components/AlertWithModal'; import AlertWithModal from '@/components/AlertWithModal';
import DropdownMenu from "@/components/DropdownMenu"; import DropdownMenu from "@/components/DropdownMenu";
import { formatPhoneNumber } from '@/utils/Telephone'; 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 Modal from '@/components/Modal';
import InscriptionForm from '@/components/Inscription/InscriptionForm' import InscriptionForm from '@/components/Inscription/InscriptionForm'
import AffectationClasseForm from '@/components/AffectationClasseForm' 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) => { 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) => { const handleSearchChange = (event) => {
@ -447,7 +465,7 @@ useEffect(()=>{
.then(clonedDocument => { .then(clonedDocument => {
// Sauvegarde des templates clonés dans la base de données // Sauvegarde des templates clonés dans la base de données
const cloneData = { const cloneData = {
name: `clone_${clonedDocument.id}`, name: `${templateMaster.name}_${updatedData.guardianFirstName}_${updatedData.guardianLastName}`,
slug: clonedDocument.slug, slug: clonedDocument.slug,
id: clonedDocument.id, id: clonedDocument.id,
master: templateMaster.id, master: templateMaster.id,
@ -555,6 +573,80 @@ useEffect(()=>{
}); });
} }
const getActionsByStatus = (row) => {
const actions = {
1: [
{
label: (
<>
<Send size={16} className="mr-2" /> Envoyer
</>
),
onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name),
},
{
label: (
<>
<Edit size={16} className="mr-2" /> Modifier
</>
),
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`,
},
],
2: [
{
label: (
<>
<Edit size={16} className="mr-2" /> Modifier
</>
),
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`,
},
],
3: [
{
label: (
<>
<CheckCircle size={16} className="mr-2" /> Valider
</>
),
onClick: () => openModalAssociationEleve(row.student),
},
{
label: (
<>
<TicketX size={16} className="mr-2 text-red-700" /> Refuser
</>
),
onClick: () => refuseRegistrationForm(row.student.id, row.student.last_name, row.student.first_name, row.student.guardians[0].associated_profile_email),
},
],
5: [
{
label: (
<>
<CheckCircle size={16} className="mr-2" /> Rattacher
</>
),
onClick: () => openModalAssociationEleve(row.student),
},
],
default: [
{
label: (
<>
<Trash2 size={16} className="mr-2 text-red-700" /> 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 = [ const columns = [
{ name: t('studentName'), transform: (row) => row.student.last_name }, { name: t('studentName'), transform: (row) => row.student.last_name },
{ name: t('studentFistName'), transform: (row) => row.student.first_name }, { name: t('studentFistName'), transform: (row) => row.student.first_name },
@ -588,69 +680,21 @@ const columns = [
{ name: t('files'), transform: (row) => { name: t('files'), transform: (row) =>
(row.registration_file != null) &&( (row.registration_file != null) &&(
<ul> <ul>
<li className="flex items-center gap-2"> <li className="flex justify-center items-center gap-2">
<FileText size={16} /> <FileText size={16} />
<a href={ `${BASE_URL}${row.registration_file}`} target='_blank'>{row.registration_file?.split('/').pop()}</a> <a href={ `${BASE_URL}${row.registration_file}`} target='_blank'>{row.registration_file?.split('/').pop()}</a>
</li> </li>
</ul> </ul>
) }, ) },
{ name: 'Actions', transform: (row) => ( { name: 'Actions',
transform: (row) => (
<DropdownMenu <DropdownMenu
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />} buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
items={[ items={getActionsByStatus(row)}
...(row.status === 1 ? [{
label: (
<>
<Send size={16} className="mr-2" /> Envoyer
</>
),
onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name),
}] : []),
...(row.status === 1 ? [{
label: (
<>
<Edit size={16} className="mr-2" /> Modifier
</>
),
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`,
}] : []),
...(row.status === 2 ? [{
label: (
<>
<Edit size={16} className="mr-2" /> Modifier
</>
),
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`,
}] : []),
...(row.status === 3 ? [{
label: (
<>
<CheckCircle size={16} className="mr-2" /> Valider
</>
),
onClick: () => openModalAssociationEleve(row.student),
}] : []),
...(row.status === 5 ? [{
label: (
<>
<CheckCircle size={16} className="mr-2" /> Rattacher
</>
),
onClick: () => openModalAssociationEleve(row.student),
}] : []),
...(row.status !== 6 ? [{
label: (
<>
<Trash2 size={16} className="mr-2 text-red-700" /> Archiver
</>
),
onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name),
}] : []),
]}
buttonClassName="text-gray-400 hover:text-gray-600" 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" 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"
/> />
) }, ), },
]; ];

View File

@ -83,7 +83,8 @@ export default function ParentHomePage() {
// Définir les colonnes du tableau // Définir les colonnes du tableau
const childrenColumns = [ 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', name: 'Statut',
transform: (row) => ( transform: (row) => (

View File

@ -21,7 +21,7 @@ import DraggableFileUpload from '@/components/DraggableFileUpload';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import FileStatusLabel from '@/components/FileStatusLabel'; import FileStatusLabel from '@/components/FileStatusLabel';
import logger from '@/utils/logger'; 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 FilesToSign from '@/components/Inscription/FilesToSign';
import FilesToUpload from '@/components/Inscription/FilesToUpload'; import FilesToUpload from '@/components/Inscription/FilesToUpload';
import { DocusealForm } from '@docuseal/react'; import { DocusealForm } from '@docuseal/react';
@ -70,6 +70,14 @@ export default function InscriptionFormShared({
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const isCurrentPageValid = () => {
if (currentPage === 1) {
const isValid = validateStudentInfo(formData);
return isValid;
}
return true;
};
// Chargement initial des données // Chargement initial des données
// Mettre à jour les données quand initialData change // Mettre à jour les données quand initialData change
useEffect(() => { useEffect(() => {
@ -178,12 +186,25 @@ export default function InscriptionFormShared({
...formData, ...formData,
guardians guardians
}, },
establishment: ESTABLISHMENT_ID, establishment: 1,
status:3 status:3
} }
onSubmit(data); 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 // Récupération des messages d'erreur
const getError = (field) => { const getError = (field) => {
return errors?.student?.[field]?.[0]; return errors?.student?.[field]?.[0];
@ -276,7 +297,18 @@ export default function InscriptionFormShared({
{/* Pages suivantes : Section Fichiers d'inscription */} {/* Pages suivantes : Section Fichiers d'inscription */}
{currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && ( {currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">{requiredFileTemplates[currentPage - 2].name}</h2> {/* Titre du document */}
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-800">
{requiredFileTemplates[currentPage - 2].name || "Document sans nom"}
</h2>
<p className="text-sm text-gray-500">
{requiredFileTemplates[currentPage - 2].description || "Aucune description disponible pour ce document."}
</p>
</div>
{/* Affichage du formulaire ou du document */}
{requiredFileTemplates[currentPage - 2].file === "" ? (
<DocusealForm <DocusealForm
id="docusealForm" id="docusealForm"
src={"https://docuseal.com/s/" + requiredFileTemplates[currentPage - 2].slug} src={"https://docuseal.com/s/" + requiredFileTemplates[currentPage - 2].slug}
@ -299,8 +331,18 @@ export default function InscriptionFormShared({
logger.error("error editing template : ", error); logger.error("error editing template : ", error);
}); });
}} }}
> />
</DocusealForm> ) : (
<iframe
src={`${BASE_URL}/${requiredFileTemplates[currentPage - 2].file}`}
title="Document Viewer"
className="w-full"
style={{
height: '75vh', // Ajuster la hauteur à 75% de la fenêtre
border: 'none',
}}
/>
)}
</div> </div>
)} )}
@ -316,11 +358,29 @@ export default function InscriptionFormShared({
{/* Boutons de contrôle */} {/* Boutons de contrôle */}
<div className="flex justify-end space-x-4"> <div className="flex justify-end space-x-4">
<Button
text="Sauvegarder"
onClick={handleSave}
className="px-4 py-2 rounded-md shadow-sm focus:outline-none bg-orange-500 text-white hover:bg-orange-600"
primary
name="Save"
/>
{currentPage > 1 && ( {currentPage > 1 && (
<Button text="Précédent" onClick={(e) => { e.preventDefault(); handlePreviousPage(); }} /> <Button text="Précédent" onClick={(e) => { e.preventDefault(); handlePreviousPage(); }} />
)} )}
{currentPage < requiredFileTemplates.length + 2 && ( {currentPage < requiredFileTemplates.length + 2 && (
<Button text="Suivant" onClick={(e) => { e.preventDefault(); handleNextPage(); }} /> <Button
text="Suivant"
onClick={(e) => { e.preventDefault(); handleNextPage(); }}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
!isCurrentPageValid()
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
disabled={!isCurrentPageValid()}
primary
name="Next"
/>
)} )}
{currentPage === requiredFileTemplates.length + 2 && ( {currentPage === requiredFileTemplates.length + 2 && (
<Button type="submit" text="Valider" primary /> <Button type="submit" text="Valider" primary />

View File

@ -56,8 +56,8 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
name="mailResponsable" name="mailResponsable"
type="email" type="email"
label={t('email')} label={t('email')}
value={item.email} value={item.associated_profile_email}
onChange={(event) => {onGuardiansChange(item.id, "email", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "associated_profile_email", event.target.value)}}
required required
errorMsg={getError(index, 'email')} errorMsg={getError(index, 'email')}
/> />

View File

@ -10,6 +10,28 @@ const levels = [
{ value:'4', label: 'GS - Grande Section'}, { value:'4', label: 'GS - Grande Section'},
]; ];
// Fonction de validation pour vérifier les champs requis
export function validateStudentInfo(formData) {
const requiredFields = [
'last_name',
'first_name',
'nationality',
'birth_date',
'birth_place',
'birth_postal_code',
'address',
'attending_physician',
'level',
];
const isValid = requiredFields.every((field) => {
const value = formData[field];
return typeof value === 'string' ? value.trim() !== '' : Boolean(value);
});
return isValid;
}
export default function StudentInfoForm({ formData, updateFormField, guardians, setGuardians, errors }) { export default function StudentInfoForm({ formData, updateFormField, guardians, setGuardians, errors }) {
const getError = (field) => { const getError = (field) => {
return errors?.student?.[field]?.[0]; return errors?.student?.[field]?.[0];

View File

@ -66,7 +66,7 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
const details = selectedGroups.flatMap(group => const details = selectedGroups.flatMap(group =>
group.registration_forms.flatMap(form => group.registration_forms.flatMap(form =>
form.guardians.map(guardian => ({ form.guardians.map(guardian => ({
email: guardian.email, email: guardian.associated_profile_email,
last_name: form.last_name, last_name: form.last_name,
first_name: form.first_name, first_name: form.first_name,
registration_form: form.student_id registration_form: form.student_id