diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index d72fed4..4b793b7 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -27,7 +27,7 @@ class Guardian(models.Model): """ last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") - birth_date = models.CharField(max_length=200, default="", blank=True) + birth_date = models.DateField(null=True, blank=True) address = models.CharField(max_length=200, default="", blank=True) phone = models.CharField(max_length=200, default="", blank=True) profession = models.CharField(max_length=200, default="", blank=True) @@ -43,7 +43,7 @@ class Sibling(models.Model): id = models.AutoField(primary_key=True) last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") - birth_date = models.CharField(max_length=200, default="", blank=True) + birth_date = models.DateField(null=True, blank=True) def __str__(self): return "SIBLING" diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index 4348a49..04ed9aa 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -158,15 +158,33 @@ class StudentSerializer(serializers.ModelSerializer): guardians_ids.append(guardian_instance.id) return guardians_ids - def create_or_update_siblings(self, siblings_data): - siblings_ids = [] + def create_or_update_siblings(self, siblings_data, student_instance): + """ + Crée ou met à jour les frères et sœurs associés à un étudiant. + Supprime les frères et sœurs qui ne sont plus présents dans siblings_data. + """ + + # Si siblings_data est vide, supprimer tous les frères et sœurs associés + if not siblings_data: + student_instance.siblings.clear() # Supprime toutes les relations + return [] + + # Récupérer les IDs des frères et sœurs existants + existing_sibling_ids = set(student_instance.siblings.values_list('id', flat=True)) + + # Créer ou mettre à jour les frères et sœurs + updated_sibling_ids = [] for sibling_data in siblings_data: sibling_instance, created = Sibling.objects.update_or_create( id=sibling_data.get('id'), defaults=sibling_data ) - siblings_ids.append(sibling_instance.id) - return siblings_ids + updated_sibling_ids.append(sibling_instance.id) + + # Supprimer les frères et sœurs qui ne sont plus dans siblings_data + siblings_to_delete = existing_sibling_ids - set(updated_sibling_ids) + Sibling.objects.filter(id__in=siblings_to_delete).delete() + return updated_sibling_ids def create_or_update_languages(self, languages_data): languages_ids = [] @@ -195,8 +213,10 @@ class StudentSerializer(serializers.ModelSerializer): languages_data = validated_data.pop('spoken_languages', []) if guardians_data: instance.guardians.set(self.create_or_update_guardians(guardians_data)) - if siblings_data: - instance.siblings.set(self.create_or_update_siblings(siblings_data)) + + sibling_ids = self.create_or_update_siblings(siblings_data, instance) + instance.siblings.set(sibling_ids) + if languages_data: instance.spoken_languages.set(self.create_or_update_languages(languages_data)) diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index 3f2406b..68b02a8 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -146,7 +146,6 @@ def rfToPDF(registerForm, filename): # Vérifier si le fichier existe et le supprimer if os.path.exists(existing_file_path): - print(f'exist ! REMOVE') os.remove(existing_file_path) registerForm.registration_file.delete(save=False) else: diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index 36cdecb..3f3cad3 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -266,6 +266,23 @@ class RegisterFormWithIdView(APIView): # Sauvegarder la photo si elle est présente dans la requête if photo_file: student = registerForm.student + + # Vérifier si une photo existante est déjà associée à l'étudiant + if student.photo and student.photo.name: + # Construire le chemin complet du fichier existant + if os.path.isabs(student.photo.name): + existing_file_path = student.photo.name + else: + existing_file_path = os.path.join(settings.MEDIA_ROOT, student.photo.name.lstrip('/')) + + # Vérifier si le fichier existe et le supprimer + if os.path.exists(existing_file_path): + os.remove(existing_file_path) + student.photo.delete(save=False) + else: + print(f'File does not exist: {existing_file_path}') + + # Sauvegarder la nouvelle photo student.photo.save(photo_file.name, photo_file, save=True) else: return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) diff --git a/Front-End/src/components/FileUpload.js b/Front-End/src/components/FileUpload.js index f809323..665eeec 100644 --- a/Front-End/src/components/FileUpload.js +++ b/Front-End/src/components/FileUpload.js @@ -1,6 +1,7 @@ import React, { useState, useRef } from 'react'; import { CloudUpload } from 'lucide-react'; import logger from '@/utils/logger'; +import { BASE_URL } from '@/utils/Url'; export default function FileUpload({ selectionMessage, @@ -69,9 +70,18 @@ export default function FileUpload({

- {typeof existingFile === 'string' - ? existingFile.split('/').pop() - : existingFile?.name || 'Fichier inconnu'} + {typeof existingFile === 'string' ? ( + + {existingFile.split('/').pop()} + + ) : ( + existingFile?.name || 'Fichier inconnu' + )}

diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index 914b626..199dd39 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -23,6 +23,7 @@ import FilesToUpload from '@/components/Inscription/FilesToUpload'; import { DocusealForm } from '@docuseal/react'; import StudentInfoForm from '@/components/Inscription/StudentInfoForm'; import ResponsableInputFields from '@/components/Inscription/ResponsableInputFields'; +import SiblingInputFields from '@/components/Inscription/SiblingInputFields'; import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector'; import ProgressStep from '@/components/ProgressStep'; import { CheckCircle, Hourglass } from 'lucide-react'; @@ -58,6 +59,7 @@ export default function InscriptionFormShared({ photo: '', }); const [guardians, setGuardians] = useState([]); + const [siblings, setSiblings] = useState([]); const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]); const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]); @@ -75,6 +77,7 @@ export default function InscriptionFormShared({ const [isPage3Valid, setIsPage3Valid] = useState(false); const [isPage4Valid, setIsPage4Valid] = useState(false); const [isPage5Valid, setIsPage5Valid] = useState(false); + const [isPage6Valid, setIsPage6Valid] = useState(false); const [hasInteracted, setHasInteracted] = useState(false); @@ -103,7 +106,7 @@ export default function InscriptionFormShared({ ); // Mettre à jour isPage4Valid en fonction de cette condition - setIsPage4Valid(allSigned); + setIsPage5Valid(allSigned); if (allSigned) { setCurrentTemplateIndex(0); @@ -116,8 +119,8 @@ export default function InscriptionFormShared({ (template) => template.file !== null ); - // Mettre à jour isPage5Valid en fonction de cette condition - setIsPage5Valid(allUploaded); + // Mettre à jour isPage6Valid en fonction de cette condition + setIsPage6Valid(allUploaded); }, [parentFileTemplates]); const handleTemplateSigned = (index) => { @@ -333,7 +336,7 @@ export default function InscriptionFormShared({ .then((response) => { logger.debug('Fichier supprimé avec succès dans la base :', response); - setIsPage5Valid(false); + setIsPage6Valid(false); // Mettre à jour l'état local pour refléter la suppression setUploadedFiles((prev) => @@ -374,6 +377,7 @@ export default function InscriptionFormShared({ student: { ...formData, guardians: guardians, + siblings: siblings, }, establishment: selectedEstablishmentId, status: isSepaPayment ? 8 : 3, @@ -383,8 +387,6 @@ export default function InscriptionFormShared({ registration_payment_plan: formData.registration_payment_plan, }; - console.log('jsonData : ', jsonData); - // Créer un objet FormData const formDataToSend = new FormData(); @@ -411,14 +413,16 @@ export default function InscriptionFormShared({ const stepTitles = { 1: 'Elève', 2: 'Responsables légaux', - 3: 'Modalités de paiement', - 4: 'Formulaires à signer', - 5: 'Pièces à fournir', + 3: 'Frères et soeurs', + 4: 'Modalités de paiement', + 5: 'Formulaires à signer', + 6: 'Pièces à fournir', }; const steps = [ 'Élève', 'Responsable', + 'Fratrie', 'Paiement', 'Formulaires', 'Documents parent', @@ -436,6 +440,8 @@ export default function InscriptionFormShared({ return isPage4Valid; case 5: return isPage5Valid; + case 6: + return isPage6Valid; default: return false; } @@ -464,6 +470,7 @@ export default function InscriptionFormShared({ setFormData={setFormData} guardians={guardians} setGuardians={setGuardians} + setSiblings={setSiblings} errors={errors} setIsPageValid={setIsPage1Valid} hasInteracted={hasInteracted} @@ -481,8 +488,19 @@ export default function InscriptionFormShared({ /> )} - {/* Page 3 : Informations sur les modalités de paiement */} + {/* Étape 3 : Frères et Sœurs */} {currentPage === 3 && ( + + )} + + {/* Page 4 : Informations sur les modalités de paiement */} + {currentPage === 4 && ( <> )} - {/* Pages suivantes : Section Fichiers d'inscription */} - {currentPage === 4 && ( + {/* Page 5 : Section Fichiers d'inscription */} + {currentPage === 5 && (
{/* Liste des états de signature */}
@@ -613,7 +631,8 @@ export default function InscriptionFormShared({ (currentPage === 1 && !isPage1Valid) || (currentPage === 2 && !isPage2Valid) || (currentPage === 3 && !isPage3Valid) || - (currentPage === 4 && !isPage4Valid) + (currentPage === 4 && !isPage4Valid) || + (currentPage === 5 && !isPage5Valid) ? 'bg-gray-300 text-gray-700 cursor-not-allowed' : 'bg-emerald-500 text-white hover:bg-emerald-600' }`} @@ -621,7 +640,8 @@ export default function InscriptionFormShared({ (currentPage === 1 && !isPage1Valid) || (currentPage === 2 && !isPage2Valid) || (currentPage === 3 && !isPage3Valid) || - (currentPage === 4 && !isPage4Valid) + (currentPage === 4 && !isPage4Valid) || + (currentPage === 5 && !isPage5Valid) } primary name="Next" @@ -633,11 +653,11 @@ export default function InscriptionFormShared({ text="Valider" primary className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${ - currentPage === 5 && !isPage5Valid + currentPage === 6 && !isPage6Valid ? 'bg-gray-300 text-gray-700 cursor-not-allowed' : 'bg-emerald-500 text-white hover:bg-emerald-600' }`} - disabled={currentPage === 5 && !isPage5Valid} + disabled={currentPage === 6 && !isPage6Valid} /> )}
diff --git a/Front-End/src/components/Inscription/SiblingInputFields.js b/Front-End/src/components/Inscription/SiblingInputFields.js new file mode 100644 index 0000000..0d9aff4 --- /dev/null +++ b/Front-End/src/components/Inscription/SiblingInputFields.js @@ -0,0 +1,157 @@ +import InputText from '@/components/InputText'; +import React, { useEffect } from 'react'; +import { Trash2, Plus, Users } from 'lucide-react'; +import SectionHeader from '@/components/SectionHeader'; + +export default function SiblingInputFields({ + siblings, + setSiblings, + setFormData, + errors, + setIsPageValid, +}) { + useEffect(() => { + // Si siblings est null ou undefined, le valoriser à un tableau vide + if (!siblings || siblings.length === 0) { + setSiblings([]); + } + + // Synchroniser siblings avec formData + setFormData((prevFormData) => ({ + ...prevFormData, + siblings: siblings, // Mettre à jour siblings dans formData + })); + + const isValid = siblings.every((sibling, index) => { + return !Object.keys(sibling).some( + (field) => getLocalError(index, field) !== '' + ); + }); + + setIsPageValid(isValid); + }, [siblings, setSiblings, setFormData, setIsPageValid]); + + const getError = (index, field) => { + return errors[index]?.[field]?.[0]; + }; + + const getLocalError = (index, field) => { + if ( + (field === 'last_name' && + (!siblings[index].last_name || + siblings[index].last_name.trim() === '')) || + (field === 'first_name' && + (!siblings[index].first_name || + siblings[index].first_name.trim() === '')) || + (field === 'birth_date' && + (!siblings[index].birth_date || + siblings[index].birth_date.trim() === '')) + ) { + return 'Champs requis'; + } + return ''; + }; + + const onSiblingsChange = (id, field, value) => { + const updatedSiblings = siblings.map((sibling) => { + if (sibling.id === id) { + return { ...sibling, [field]: value }; + } + return sibling; + }); + + setSiblings(updatedSiblings); + }; + + const addSibling = () => { + setSiblings([ + ...siblings, + { + last_name: '', + first_name: '', + birth_date: '', + }, + ]); + }; + + const deleteSibling = (index) => { + const updatedSiblings = siblings.filter((_, i) => i !== index); + setSiblings(updatedSiblings); + }; + + return ( +
+ + {siblings.map((item, index) => ( +
+
+

Frère/Sœur {index + 1}

+ deleteSibling(index)} + /> +
+ +
+ { + onSiblingsChange(item.id, 'last_name', event.target.value); + }} + errorMsg={ + getError(index, 'last_name') || + getLocalError(index, 'last_name') + } + required + /> + { + onSiblingsChange(item.id, 'first_name', event.target.value); + }} + errorMsg={ + getError(index, 'first_name') || + getLocalError(index, 'first_name') + } + required + /> +
+ +
+ { + onSiblingsChange(item.id, 'birth_date', event.target.value); + }} + errorMsg={ + getError(index, 'birth_date') || + getLocalError(index, 'birth_date') + } + required + /> +
+
+ ))} + +
+ +
+
+ ); +} diff --git a/Front-End/src/components/Inscription/StudentInfoForm.js b/Front-End/src/components/Inscription/StudentInfoForm.js index 8f5694e..459ee5b 100644 --- a/Front-End/src/components/Inscription/StudentInfoForm.js +++ b/Front-End/src/components/Inscription/StudentInfoForm.js @@ -7,6 +7,7 @@ import logger from '@/utils/logger'; import SectionHeader from '@/components/SectionHeader'; import { User } from 'lucide-react'; import FileUpload from '@/components/FileUpload'; +import { BASE_URL } from '@/utils/Url'; const levels = [ { value: '1', label: 'TPS - Très Petite Section' }, @@ -25,6 +26,7 @@ export default function StudentInfoForm({ formData, setFormData, setGuardians, + setSiblings, errors, setIsPageValid, hasInteracted, @@ -37,9 +39,11 @@ export default function StudentInfoForm({ fetchRegisterForm(studentId).then((data) => { logger.debug(data); + const photoPath = data?.student?.photo || null; + setFormData({ id: data?.student?.id || '', - photo: data?.student?.photo || null, + photo: photoPath, last_name: data?.student?.last_name || '', first_name: data?.student?.first_name || '', address: data?.student?.address || '', @@ -56,7 +60,29 @@ export default function StudentInfoForm({ totalRegistrationFees: data?.totalRegistrationFees, totalTuitionFees: data?.totalTuitionFees, }); + setGuardians(data?.student?.guardians || []); + setSiblings(data?.student?.siblings || []); + + // Convertir la photo en fichier binaire si elle est un chemin ou une URL + if (photoPath && typeof photoPath === 'string') { + fetch(`${BASE_URL}${photoPath}`) + .then((response) => { + if (!response.ok) { + throw new Error('Erreur lors de la récupération de la photo.'); + } + return response.blob(); + }) + .then((blob) => { + const file = new File([blob], photoPath.split('/').pop(), { + type: blob.type, + }); + handlePhotoUpload(file); // Utiliser handlePhotoUpload pour valoriser la photo + }) + .catch((error) => { + logger.error('Erreur lors de la conversion de la photo :', error); + }); + } }); setIsLoading(false);