From c9350a796b65ea4eef0e38390ab9fb1d88196210 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sat, 22 Mar 2025 12:28:12 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Ajout=20de=20la=20possibilit=C3=A9=20de?= =?UTF-8?q?=20supprimer=20une=20association=20guardian/student=20+=20ajout?= =?UTF-8?q?=20de=20la=20possibilit=C3=A9=20de=20cr=C3=A9er=20un=20guardian?= =?UTF-8?q?=20pour=20un=20student=20+=20tri=20chrologique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Auth/models.py | 1 + Back-End/Auth/serializers.py | 15 +- Back-End/Auth/views.py | 2 +- Back-End/School/serializers.py | 1 - Back-End/Subscriptions/automate.py | 1 - Back-End/Subscriptions/models.py | 2 +- .../Subscriptions/views/guardian_views.py | 25 ++- .../views/register_form_views.py | 3 +- .../src/app/[locale]/admin/directory/page.js | 35 +++- .../app/[locale]/admin/subscriptions/page.js | 115 ++++++++++++- .../components/Inscription/InscriptionForm.js | 151 ++++++++++++------ Front-End/src/components/ProfileDirectory.js | 68 ++++---- 12 files changed, 326 insertions(+), 93 deletions(-) diff --git a/Back-End/Auth/models.py b/Back-End/Auth/models.py index cbbceac..f148dd7 100644 --- a/Back-End/Auth/models.py +++ b/Back-End/Auth/models.py @@ -27,6 +27,7 @@ class ProfileRole(models.Model): role_type = models.IntegerField(choices=RoleType.choices, default=RoleType.PROFIL_UNDEFINED) establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='profile_roles') is_active = models.BooleanField(default=False) + updated_date = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.profile.email} - {self.get_role_type_display()} - {self.establishment.name}" \ No newline at end of file diff --git a/Back-End/Auth/serializers.py b/Back-End/Auth/serializers.py index c4c4d7e..88cc2ee 100644 --- a/Back-End/Auth/serializers.py +++ b/Back-End/Auth/serializers.py @@ -3,6 +3,9 @@ from Auth.models import Profile, ProfileRole from Establishment.models import Establishment from Subscriptions.models import Guardian, RegistrationForm from School.models import Teacher +from N3wtSchool import settings +from django.utils import timezone +import pytz class ProfileSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) @@ -69,10 +72,11 @@ class ProfileRoleSerializer(serializers.ModelSerializer): profile_data = ProfileSerializer(write_only=True, required=False) associated_profile_email = serializers.SerializerMethodField() associated_person = serializers.SerializerMethodField() + updated_date_formatted = serializers.SerializerMethodField() class Meta: model = ProfileRole - fields = ['id', 'role_type', 'establishment', 'is_active', 'profile', 'profile_data', 'associated_profile_email', 'associated_person'] + fields = ['id', 'role_type', 'establishment', 'is_active', 'profile', 'profile_data', 'associated_profile_email', 'associated_person', 'updated_date_formatted'] def create(self, validated_data): profile_data = validated_data.pop('profile_data', None) @@ -142,4 +146,11 @@ class ProfileRoleSerializer(serializers.ModelSerializer): "classes": classes_list, "specialities": specialities_list } - return None \ No newline at end of file + return None + + def get_updated_date_formatted(self, obj): + utc_time = timezone.localtime(obj.updated_date) + local_tz = pytz.timezone(settings.TZ_APPLI) + local_time = utc_time.astimezone(local_tz) + + return local_time.strftime("%d-%m-%Y %H:%M") \ No newline at end of file diff --git a/Back-End/Auth/views.py b/Back-End/Auth/views.py index a1eb820..8955b40 100644 --- a/Back-End/Auth/views.py +++ b/Back-End/Auth/views.py @@ -538,7 +538,7 @@ class ProfileRoleView(APIView): profiles_roles_List = bdd.getAllObjects(_objectName=ProfileRole) if profiles_roles_List: - profiles_roles_List = profiles_roles_List.filter(establishment=establishment_id).distinct() + profiles_roles_List = profiles_roles_List.filter(establishment=establishment_id).distinct().order_by('-updated_date') profile_roles_serializer = ProfileRoleSerializer(profiles_roles_List, many=True) return JsonResponse(profile_roles_serializer.data, safe=False) diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index 04231a2..e706560 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -67,7 +67,6 @@ class TeacherSerializer(serializers.ModelSerializer): specialities_data = validated_data.pop('specialities', None) associated_profile_email = validated_data.pop('associated_profile_email') establishment_id = validated_data.get('establishment') - print(f'debug : {validated_data}') role_type = validated_data.pop('role_type') profile, created = Profile.objects.get_or_create( diff --git a/Back-End/Subscriptions/automate.py b/Back-End/Subscriptions/automate.py index 6f98e79..5b8180f 100644 --- a/Back-End/Subscriptions/automate.py +++ b/Back-End/Subscriptions/automate.py @@ -26,7 +26,6 @@ def getStateMachineObjectState(etat): def updateStateMachine(rf, transition) : automateModel = load_config('Subscriptions/Configuration/automate.json') state_machine = getStateMachineObject(rf.status) - print(f'etat DI : {state_machine.state}') if state_machine.trigger(transition, automateModel): rf.status = state_machine.state rf.save() diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 4f70800..d1ffd38 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -29,7 +29,7 @@ class Guardian(models.Model): 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) - profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='guardian_profile', null=True, blank=True) + profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='guardian_profile', blank=True) def __str__(self): return self.last_name + "_" + self.first_name diff --git a/Back-End/Subscriptions/views/guardian_views.py b/Back-End/Subscriptions/views/guardian_views.py index 4d0244b..221c2fd 100644 --- a/Back-End/Subscriptions/views/guardian_views.py +++ b/Back-End/Subscriptions/views/guardian_views.py @@ -5,6 +5,7 @@ from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from Subscriptions.models import Guardian, Student +from Auth.models import ProfileRole from N3wtSchool import bdd class GuardianView(APIView): @@ -50,12 +51,34 @@ class DissociateGuardianView(APIView): # Supprimer la relation entre le student et le guardian student.guardians.remove(guardian) + if guardian.profile_role: + guardian.profile_role.save() + + isGuardianDeleted = False # Vérifier si le guardian n'est plus associé à aucun élève if guardian.student_set.count() == 0: # Utilise la relation ManyToMany inverse + print(f'Le guardian {guardian} n\'est plus rattaché à aucun élève : on le supprime') + isGuardianDeleted = True + + # Vérifier si le guardian a un ProfileRole associé + if guardian.profile_role: + print(f'Suppression du ProfileRole associé au guardian {guardian}') + guardian.profile_role.delete() + + # Vérifier si le Profile n'a plus de ProfileRole associés + profile = guardian.profile_role.profile + if not ProfileRole.objects.filter(profile=profile).exists(): + print(f'Le profile {profile} n\'a plus de rôle associé : on le supprime') + profile.delete() + + # Supprimer le guardian guardian.delete() 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}.", + "isGuardianDeleted": isGuardianDeleted + }, status=status.HTTP_200_OK ) except Student.DoesNotExist: diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index 1cecb01..347aa33 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -99,8 +99,7 @@ class RegisterFormView(APIView): registerForms_List = None if registerForms_List: - print(f'filtrate sur lestablishment : {establishment_id}') - registerForms_List = registerForms_List.filter(establishment=establishment_id) + registerForms_List = registerForms_List.filter(establishment=establishment_id).order_by('-last_update') if not registerForms_List: return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False) diff --git a/Front-End/src/app/[locale]/admin/directory/page.js b/Front-End/src/app/[locale]/admin/directory/page.js index ebbea53..38090a3 100644 --- a/Front-End/src/app/[locale]/admin/directory/page.js +++ b/Front-End/src/app/[locale]/admin/directory/page.js @@ -10,6 +10,7 @@ import ProfileDirectory from '@/components/ProfileDirectory'; export default function Page() { const [profileRoles, setProfileRoles] = useState([]); + const [reloadFetch, setReloadFetch] = useState(false); const csrfToken = useCsrfToken(); const { selectedEstablishmentId } = useEstablishment(); @@ -19,7 +20,7 @@ export default function Page() { // Fetch data for profileRoles handleProfiles(); } - }, [selectedEstablishmentId]); + }, [selectedEstablishmentId, reloadFetch]); const handleProfiles = () => { fetchProfileRoles(selectedEstablishmentId) @@ -27,6 +28,7 @@ export default function Page() { setProfileRoles(data); }) .catch(error => logger.error('Error fetching profileRoles:', error)); + setReloadFetch(false); }; const handleEdit = (profileRole) => { @@ -56,8 +58,37 @@ export default function Page() { const handleDissociate = (studentId, guardianId) => { return dissociateGuardian(studentId, guardianId) - .then(() => { + .then((response) => { logger.debug("Guardian dissociated successfully:", guardianId); + + // Vérifier si le Guardian a été supprimé + const isGuardianDeleted = response?.isGuardianDeleted; + + // Mettre à jour le modèle profileRoles + setProfileRoles(prevState => + prevState.map(profileRole => { + if (profileRole.associated_person?.id === guardianId) { + if (isGuardianDeleted) { + // Si le Guardian est supprimé, retirer le profileRole + return null; + } else { + // Si le Guardian n'est pas supprimé, mettre à jour les élèves associés + const updatedStudents = profileRole.associated_person.students.filter( + student => student.id !== studentId + ); + return { + ...profileRole, + associated_person: { + ...profileRole.associated_person, + students: updatedStudents, // Mettre à jour les élèves associés + }, + }; + } + } + setReloadFetch(true); + return profileRole; // Conserver les autres profileRoles + }).filter(Boolean) // Supprimer les entrées nulles + ); }) .catch(error => { logger.error('Error dissociating guardian:', error); diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 973e3d8..45218ff 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -15,7 +15,6 @@ import { MoreVertical, Send, Edit, Trash2, FileText, CheckCircle, Plus } from ' import Modal from '@/components/Modal'; import InscriptionForm from '@/components/Inscription/InscriptionForm' import AffectationClasseForm from '@/components/AffectationClasseForm' -import { getSession } from 'next-auth/react'; import { useEstablishment } from '@/context/EstablishmentContext'; import { @@ -87,6 +86,7 @@ export default function Page({ params: { locale } }) { const [tuitionFees, setTuitionFees] = useState([]); const [groups, setGroups] = useState([]); const [profiles, setProfiles] = useState([]); + const [isOpenAddGuardian, setIsOpenAddGuardian] = useState(false); const csrfToken = useCsrfToken(); const { selectedEstablishmentId } = useEstablishment(); @@ -100,6 +100,15 @@ export default function Page({ params: { locale } }) { } + const handleOpenAddGuardian = (eleveSelected) => { + setIsOpenAddGuardian(true); + setStudent(eleveSelected); + }; + + const handleCloseAddGuardian = () => { + setIsOpenAddGuardian(false); + }; + const openModalAssociationEleve = (eleveSelected) => { setIsOpenAffectationClasse(true); setStudent(eleveSelected); @@ -480,10 +489,94 @@ useEffect(()=>{ }); } + const updateRF = (updatedData) => { + logger.debug('updateRF updatedData:', updatedData); + + const data = { + student: { + guardians: updatedData.selectedGuardians.length !== 0 + ? updatedData.selectedGuardians.map(guardianId => ({ id: guardianId })) + : (() => { + if (updatedData.isExistingParentProfile) { + return [{ + profile_role_data: { + establishment: selectedEstablishmentId, + role_type: 2, + is_active: false, + profile: updatedData.existingProfileId, // Associer au profil existant + }, + last_name: updatedData.guardianLastName, + first_name: updatedData.guardianFirstName, + birth_date: updatedData.guardianBirthDate, + address: updatedData.guardianAddress, + phone: updatedData.guardianPhone, + profession: updatedData.guardianProfession + }]; + } + + // Si aucun profil existant n'est trouvé, créer un nouveau profil + return [{ + profile_role_data: { + establishment: selectedEstablishmentId, + role_type: 2, + is_active: false, + profile_data: { + email: updatedData.guardianEmail, + password: 'Provisoire01!', + username: updatedData.guardianEmail, + } + }, + last_name: updatedData.guardianLastName, + first_name: updatedData.guardianFirstName, + birth_date: updatedData.guardianBirthDate, + address: updatedData.guardianAddress, + phone: updatedData.guardianPhone, + profession: updatedData.guardianProfession + }]; + })(), + }, + establishment: selectedEstablishmentId + }; + + editRegisterForm(student.id, data, csrfToken) + .then(data => { + // Mise à jour immédiate des données + setRegistrationFormsDataPending(prevState => [...(prevState || []), data]); + setTotalPending(prev => prev + 1); + if (updatedData.autoMail) { + sendConfirmRegisterForm(data.student.id, updatedData.studentLastName, updatedData.studentFirstName); + } + handleCloseAddGuardian(); + // Forcer le rechargement complet des données + setReloadFetch(true); + }) + .catch(error => { + logger.error('Error during updating registration form:', error); + }); + } + const columns = [ { name: t('studentName'), transform: (row) => row.student.last_name }, { name: t('studentFistName'), transform: (row) => row.student.first_name }, - { name: t('mainContactMail'), transform: (row) => (row.student.guardians && row.student.guardians.length > 0) ? row.student.guardians[0].associated_profile_email : '' }, + { + name: t('mainContactMail'), + transform: (row) => ( + row.student.guardians && row.student.guardians.length > 0 + ? row.student.guardians[0].associated_profile_email + : ( +
+ +
+ ) + ) + }, { name: t('phone'), transform: (row) => formatPhoneNumber(row.student.guardians[0]?.phone) }, { name: t('lastUpdateDate'), transform: (row) => row.formatted_last_update}, { name: t('registrationFileStatus'), transform: (row) => ( @@ -742,13 +835,29 @@ const columnsSubscribed = [ title="Affectation à une classe" ContentComponent={() => ( )} /> )} + {isOpenAddGuardian && ( + ( + + )} + /> + )} ); } diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index 721dbbe..6dd799b 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -11,8 +11,19 @@ import ProgressStep from '@/components/ProgressStep'; import logger from '@/utils/logger'; import Popup from '@/components/Popup'; -const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, profiles, onSubmit, currentStep, groups }) => { - const [formData, setFormData] = useState({ +const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, profiles, onSubmit, currentStep, groups, showOnlyStep2 = false }) => { + const [formData, setFormData] = useState(() => { + if (showOnlyStep2) { + return { + guardianLastName: '', + guardianFirstName: '', + guardianEmail: '', + guardianPhone: '', + selectedGuardians: [], + responsableType: 'new', + }; + } + return { studentLastName: '', studentFirstName: '', guardianLastName: '', @@ -27,6 +38,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r selectedTuitionDiscounts: [], selectedTuitionFees: [], selectedFileGroup: null // Ajout du groupe de fichiers sélectionné + }; }); const [step, setStep] = useState(currentStep || 1); @@ -55,8 +67,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r formData.selectedGuardians.length > 0 || (!formData.emailError && formData.guardianEmail.length > 0 && filteredStudents.length === 0) ); - const isStep3Valid = formData.selectedRegistrationFees.length > 0; - const isStep4Valid = formData.selectedTuitionFees.length > 0; + const isStep3Valid = formData.selectedRegistrationFees?.length > 0; + const isStep4Valid = formData.selectedTuitionFees?.length > 0; const isStep5Valid = formData.selectedFileGroup !== null; const isStep6Valid = isStep1Valid && isStep2Valid && isStep3Valid && isStep4Valid && isStep5Valid; @@ -80,12 +92,14 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r }; useEffect(() => { + if (!showOnlyStep2) { // Calcul du montant total des frais d'inscription lors de l'initialisation const initialTotalRegistrationAmount = calculateFinalRegistrationAmount( registrationFees.map(fee => fee.id), [] ); setTotalRegistrationAmount(initialTotalRegistrationAmount); + } }, [registrationDiscounts, registrationFees]); @@ -123,15 +137,15 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r } } - if (step < steps.length) { - setStep(step + 1); + if (!showOnlyStep2 && step < steps.length) { + setStep(step + 1); } }; const prevStep = () => { - if (step > 1) { - setStep(step - 1); - } + if (!showOnlyStep2 && step > 1) { + setStep(step - 1); + } }; const handleEleveSelection = (student) => { @@ -270,6 +284,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r return (
+ {!showOnlyStep2 && ( + )} {step === 1 && (
@@ -308,7 +324,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r name="guardianLastName" type="text" IconItem={User} - placeholder="Nom du responsable (optionnel)" + placeholder="Nom du responsable" value={formData.guardianLastName} onChange={handleChange} className="w-full mt-4" @@ -317,7 +333,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r name="guardianFirstName" type="text" IconItem={User} - placeholder="Prénom du responsable (optionnel)" + placeholder="Prénom du responsable" value={formData.guardianFirstName} onChange={handleChange} className="w-full mt-4" @@ -663,44 +679,83 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r )}
- {step > 1 && ( -
diff --git a/Front-End/src/components/ProfileDirectory.js b/Front-End/src/components/ProfileDirectory.js index 690e144..32f1599 100644 --- a/Front-End/src/components/ProfileDirectory.js +++ b/Front-End/src/components/ProfileDirectory.js @@ -107,6 +107,7 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro const parentColumns = [ { name: 'Identifiant', transform: (row) => row.associated_profile_email }, + { name: 'Mise à jour', transform: (row) => row.updated_date_formatted }, { name: 'Rôle', transform: (row) => ( {roleTypeToLabel(row.role_type)} @@ -120,40 +121,44 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro {row.associated_person?.guardian_name} {row.associated_person && (
handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne - onMouseLeave={handleTooltipHide} // Cacher la tooltip - > - - {visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond -
-
- Elève(s) associé(s): -
- {row.associated_person?.students?.map(student => ( -
- - {student.student_name} - -
- - -
+ className="relative group" + onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne + onMouseLeave={handleTooltipHide} // Cacher la tooltip + > + + {visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond +
+
+ Elève(s) associé(s): +
+ {row.associated_person?.students?.map(student => ( +
+ + {student.student_name} + +
+ +
- ))} -
+
+ ))}
- )} -
+
+ )} +
)}
) @@ -183,6 +188,7 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro const schoolAdminColumns = [ { name: 'Identifiant', transform: (row) => row.associated_profile_email }, + { name: 'Mise à jour', transform: (row) => row.updated_date_formatted }, { name: 'Rôle', transform: (row) => ( {roleTypeToLabel(row.role_type)}