From 5851341235998647a4142bdf1996ddc9db21762d Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Thu, 1 May 2025 14:59:19 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Ajout=20de=20la=20photo=20pour=20le=20d?= =?UTF-8?q?ossier=20de=20l'=C3=A9l=C3=A8ve=20+=20correction=20sauvegarde?= =?UTF-8?q?=20des=20datas=20des=20responsables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Subscriptions/models.py | 20 ++++-- .../templates/pdfs/dossier_inscription.html | 20 +++++- .../views/register_form_views.py | 30 +++++--- Front-End/messages/en/subscriptions.json | 1 + Front-End/messages/fr/subscriptions.json | 1 + .../subscriptions/editInscription/page.js | 4 +- .../app/[locale]/admin/subscriptions/page.js | 39 ++++++++--- Front-End/src/components/FileUpload.js | 2 +- .../Inscription/InscriptionFormShared.js | 22 ++++-- .../Inscription/ResponsableInputFields.js | 44 ++++++++++-- .../components/Inscription/StudentInfoForm.js | 68 ++++++------------- .../Structure/Files/FileUploadDocuSeal.js | 22 ++++++ 12 files changed, 187 insertions(+), 86 deletions(-) diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 4933bd1..c3de2b3 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -48,6 +48,9 @@ class Sibling(models.Model): def __str__(self): return "SIBLING" +def registration_photo_upload_to(instance, filename): + return f"registration_files/dossier_rf_{instance.pk}/parent/{filename}" + class Student(models.Model): """ Représente l’élève inscrit ou en cours d’inscription. @@ -64,6 +67,11 @@ class Student(models.Model): MS = 3, _('MS - Moyenne Section') GS = 4, _('GS - Grande Section') + photo = models.FileField( + upload_to=registration_photo_upload_to, + null=True, + blank=True + ) last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") gender = models.IntegerField(choices=StudentGender, default=StudentGender.NONE, blank=True) @@ -239,12 +247,6 @@ class RegistrationForm(models.Model): # Appeler la méthode save originale super().save(*args, **kwargs) -def registration_school_file_upload_to(instance, filename): - return f"registration_files/dossier_rf_{instance.registration_form.pk}/school/{filename}" - -def registration_parent_file_upload_to(instance, filename): - return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}" - ############################################################# ####################### MASTER FILES ######################## ############################################################# @@ -269,6 +271,12 @@ class RegistrationParentFileMaster(models.Model): ####################### CLONE FILES ######################## ############################################################ +def registration_school_file_upload_to(instance, filename): + return f"registration_files/dossier_rf_{instance.registration_form.pk}/school/{filename}" + +def registration_parent_file_upload_to(instance, filename): + return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}" + ####### DocuSeal templates (par dossier d'inscription) ####### class RegistrationSchoolFileTemplate(models.Model): master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True) diff --git a/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html b/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html index f23d394..4b2c683 100644 --- a/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html +++ b/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html @@ -91,7 +91,25 @@ {% load myTemplateTag %}
-

{{ pdf_title }}

+
+

{{ pdf_title }}

+ {% if student.photo %} + Photo de l'élève + {% endif %} +
diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index 94ff256..83c5bd9 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -229,25 +229,37 @@ class RegisterFormWithIdView(APIView): """ Modifie un dossier d'inscription donné. """ - # 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}") + studentForm_data = request.data.get('data', '{}') + try: + data = json.loads(studentForm_data) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON format in 'data'"}, status=status.HTTP_400_BAD_REQUEST) - _status = studentForm_data.pop('status', 0) - 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) + # Extraire le fichier photo + photo_file = request.FILES.get('photo') + + # Ajouter la photo aux données de l'étudiant + if photo_file: + data['student']['photo'] = photo_file + + # Gérer le champ `_status` + _status = data.pop('status', 0) + _status = int(_status) # Récupérer le dossier d'inscription registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) if not registerForm: return JsonResponse({"error": "Dossier d'inscription introuvable"}, status=status.HTTP_404_NOT_FOUND) - studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data, partial=True) + studentForm_serializer = RegistrationFormSerializer(registerForm, data=data, partial=True) if studentForm_serializer.is_valid(): studentForm_serializer.save() + + # Sauvegarder la photo si elle est présente dans la requête + if photo_file: + student = registerForm.student + 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/messages/en/subscriptions.json b/Front-End/messages/en/subscriptions.json index 0f6b4b1..d9d745c 100644 --- a/Front-End/messages/en/subscriptions.json +++ b/Front-End/messages/en/subscriptions.json @@ -5,6 +5,7 @@ "pending": "Pending Registrations", "subscribed": "Subscribed", "archived": "Archived", + "photo": "Photo", "name": "Name", "class": "Class", "status": "Status", diff --git a/Front-End/messages/fr/subscriptions.json b/Front-End/messages/fr/subscriptions.json index d4670ea..81ea7ed 100644 --- a/Front-End/messages/fr/subscriptions.json +++ b/Front-End/messages/fr/subscriptions.json @@ -5,6 +5,7 @@ "pending": "Inscriptions en attente", "subscribed": "Inscrits", "archived": "Archivés", + "photo": "Photo", "name": "Nom", "class": "Classe", "status": "Statut", 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 a968c10..e7884b6 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js @@ -5,7 +5,7 @@ import InscriptionFormShared from '@/components/Inscription/InscriptionFormShare import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url'; import { useCsrfToken } from '@/context/CsrfContext'; import { useEstablishment } from '@/context/EstablishmentContext'; -import { editRegisterForm } from '@/app/actions/subscriptionAction'; +import { editRegisterFormWithBinaryFile } from '@/app/actions/subscriptionAction'; import logger from '@/utils/logger'; import Loader from '@/components/Loader'; @@ -21,7 +21,7 @@ export default function Page() { const handleSubmit = (data) => { setIsLoading(true); - editRegisterForm(studentId, data, csrfToken) + editRegisterFormWithBinaryFile(studentId, data, csrfToken) .then((result) => { setIsLoading(false); logger.debug('Success:', result); diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index d1ccff9..903a497 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -60,6 +60,7 @@ import { fetchProfiles } from '@/app/actions/authAction'; import { FE_ADMIN_SUBSCRIPTIONS_EDIT_URL, FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL, + BASE_URL, } from '@/utils/Url'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; @@ -743,17 +744,6 @@ export default function Page({ params: { locale } }) { const getActionsByStatus = (row) => { const actions = { 1: [ - { - icon: ( - - - - ), - onClick: () => - router.push( - `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}` - ), - }, { icon: ( @@ -865,6 +855,33 @@ export default function Page({ params: { locale } }) { }; const columns = [ + { + name: t('photo'), + transform: (row) => ( +
+ {row.student.photo ? ( + + {`${row.student.first_name} + + ) : ( +
+ + {row.student.first_name[0]} + {row.student.last_name[0]} + +
+ )} +
+ ), + }, { name: t('studentName'), transform: (row) => row.student.last_name }, { name: t('studentFistName'), transform: (row) => row.student.first_name }, { diff --git a/Front-End/src/components/FileUpload.js b/Front-End/src/components/FileUpload.js index 3894a31..5cc49bb 100644 --- a/Front-End/src/components/FileUpload.js +++ b/Front-End/src/components/FileUpload.js @@ -42,7 +42,7 @@ export default function FileUpload({ {/* Icône de cloud */} { diff --git a/Front-End/src/components/Inscription/ResponsableInputFields.js b/Front-End/src/components/Inscription/ResponsableInputFields.js index e7e5aff..a9f5b18 100644 --- a/Front-End/src/components/Inscription/ResponsableInputFields.js +++ b/Front-End/src/components/Inscription/ResponsableInputFields.js @@ -4,6 +4,7 @@ import React, { useEffect } from 'react'; import { useTranslations } from 'next-intl'; import { Trash2, Plus, Users } from 'lucide-react'; import SectionHeader from '@/components/SectionHeader'; +import { useEstablishment } from '@/context/EstablishmentContext'; export default function ResponsableInputFields({ guardians, @@ -12,6 +13,7 @@ export default function ResponsableInputFields({ setIsPageValid, }) { const t = useTranslations('ResponsableInputFields'); + const { selectedEstablishmentId } = useEstablishment(); useEffect(() => { const isValid = @@ -38,7 +40,7 @@ export default function ResponsableInputFields({ (field === 'first_name' && (!guardians[index].first_name || guardians[index].first_name.trim() === '')) || - (field === 'email' && + (field === 'associated_profile_email' && (!guardians[index].associated_profile_email || guardians[index].associated_profile_email.trim() === '')) || (field === 'birth_date' && @@ -56,16 +58,45 @@ export default function ResponsableInputFields({ }; const onGuardiansChange = (id, field, value) => { - const updatedGuardians = guardians.map((guardian) => - guardian.id === id ? { ...guardian, [field]: value } : guardian - ); + const updatedGuardians = guardians.map((guardian) => { + if (guardian.id === id) { + const updatedGuardian = { ...guardian, [field]: value }; + + // Synchroniser profile_data.email et profile_data.username avec associated_profile_email + if (field === 'associated_profile_email') { + updatedGuardian.profile_role_data.profile_data.email = value; + updatedGuardian.profile_role_data.profile_data.username = value; + } + + return updatedGuardian; + } + return guardian; + }); + setGuardians(updatedGuardians); }; const addGuardian = () => { setGuardians([ ...guardians, - { id: Date.now(), name: '', email: '', phone: '' }, + { + profile_role_data: { + establishment: selectedEstablishmentId, + role_type: 2, + is_active: false, + profile_data: { + email: '', + password: 'Provisoire01!', + username: '', + }, + }, + last_name: '', + first_name: '', + birth_date: '', + address: '', + phone: '', + profession: '', + }, ]); }; @@ -143,7 +174,8 @@ export default function ResponsableInputFields({ }} required errorMsg={ - getError(index, 'email') || getLocalError(index, 'email') + getError(index, 'associated_profile_email') || + getLocalError(index, 'associated_profile_email') } /> { + if (file) { + setFormData((prev) => ({ + ...prev, + photo: file, + })); + logger.debug('Photo sélectionnée :', file.name); + } + }; + // Affichage du loader pendant le chargement if (isLoading) return ; @@ -208,54 +220,18 @@ export default function StudentInfoForm({
- {/*
-

Responsables

- { - const updatedGuardians = guardians.map((resp) => - resp.id === id ? { ...resp, [field]: value } : resp - ); - setGuardians(updatedGuardians); - }} - addGuardian={(e) => { - e.preventDefault(); - setGuardians([...guardians, { id: Date.now() }]); - }} - deleteGuardian={(index) => { - const newArray = [...guardians]; - newArray.splice(index, 1); - setGuardians(newArray); - }} - errors={errors?.student?.guardians || []} + {/* Section pour l'upload des fichiers */} +
+ + handlePhotoUpload(file)} />
- - - - */} ); } diff --git a/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js b/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js index ba36e80..11f8c9b 100644 --- a/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js +++ b/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js @@ -10,6 +10,7 @@ import logger from '@/utils/logger'; import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect import { useCsrfToken } from '@/context/CsrfContext'; import { useEstablishment } from '@/context/EstablishmentContext'; +import Popup from '@/components/Popup'; export default function FileUploadDocuSeal({ handleCreateTemplateMaster, @@ -24,6 +25,9 @@ export default function FileUploadDocuSeal({ const [selectedGroups, setSelectedGroups] = useState([]); const [guardianDetails, setGuardianDetails] = useState([]); + const [popupVisible, setPopupVisible] = useState(false); + const [popupMessage, setPopupMessage] = useState(''); + const csrfToken = useCsrfToken(); const { selectedEstablishmentId } = useEstablishment(); @@ -84,6 +88,17 @@ export default function FileUploadDocuSeal({ }; const handleSubmit = (data) => { + // Vérifier si au moins un champ a la propriété "required" à true + const hasRequiredField = data.fields.some( + (field) => field.required === true + ); + + if (!hasRequiredField) { + setPopupMessage('Veuillez définir au moins un champ comme requis.'); + setPopupVisible(true); + return; + } + const is_required = data.fields.length > 0; if (fileToEdit) { logger.debug('Modification du template master:', templateMaster?.id); @@ -139,6 +154,13 @@ export default function FileUploadDocuSeal({ return (
+ setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> {/* Contenu principal */}
{/* Sélection des groupes */}