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 + : ( +