feat: Ajout d'une fonction de dissociation entre un responsable et un

élève
This commit is contained in:
N3WT DE COMPET
2025-03-20 20:28:12 +01:00
parent fb73f9e9a8
commit 3bcc620ee1
9 changed files with 148 additions and 40 deletions

View File

@ -120,6 +120,7 @@ class ProfileRoleSerializer(serializers.ModelSerializer):
registration_form = RegistrationForm.objects.filter(student=student).first()
registration_status = registration_form.status if registration_form else None
students_list.append({
"id": f"{student.id}",
"student_name": f"{student.last_name} {student.first_name}",
"registration_status": registration_status
})

View File

@ -66,7 +66,8 @@ class TeacherSerializer(serializers.ModelSerializer):
def create(self, validated_data):
specialities_data = validated_data.pop('specialities', None)
associated_profile_email = validated_data.pop('associated_profile_email')
establishment_id = validated_data.pop('establishment')
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(

View File

@ -5,7 +5,7 @@ from . import views
# RF
from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archive
# SubClasses
from .views import StudentView, GuardianView, ChildrenListView, StudentListView
from .views import StudentView, GuardianView, ChildrenListView, StudentListView, DissociateGuardianView
# Files
from .views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
@ -38,4 +38,7 @@ urlpatterns = [
re_path(r'^registrationTemplates/(?P<id>[0-9]+)$', RegistrationTemplateSimpleView.as_view(), name='registrationTemplates'),
re_path(r'^registrationTemplates$', RegistrationTemplateView.as_view(), name="registrationTemplates"),
re_path(r'^students/(?P<student_id>[0-9]+)/guardians/(?P<guardian_id>[0-9]+)/dissociate', DissociateGuardianView.as_view(), name='dissociate-guardian'),
]

View File

@ -2,7 +2,7 @@ from .register_form_views import RegisterFormView, RegisterFormWithIdView, send,
from .registration_file_views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .student_views import StudentView, StudentListView, ChildrenListView
from .guardian_views import GuardianView
from .guardian_views import GuardianView, DissociateGuardianView
__all__ = [
'RegisterFormView',
@ -22,4 +22,5 @@ __all__ = [
'StudentListView',
'ChildrenListView',
'GuardianView',
'DissociateGuardianView'
]

View File

@ -1,9 +1,10 @@
from django.http.response import JsonResponse
from rest_framework import status
from rest_framework.views import APIView
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from Subscriptions.models import Guardian
from Subscriptions.models import Guardian, Student
from N3wtSchool import bdd
class GuardianView(APIView):
@ -32,3 +33,43 @@ class GuardianView(APIView):
def get(self, request):
lastGuardian = bdd.getLastId(Guardian)
return JsonResponse({"lastid":lastGuardian}, safe=False)
class DissociateGuardianView(APIView):
"""
Vue pour dissocier un Guardian d'un Student.
"""
def put(self, request, student_id, guardian_id):
try:
# Récupérer l'étudiant
student = Student.objects.get(id=student_id)
# Récupérer le guardian
guardian = Guardian.objects.get(id=guardian_id)
# Supprimer la relation entre le student et le guardian
student.guardians.remove(guardian)
# Vérifier si le guardian n'est plus associé à aucun élève
if guardian.student_set.count() == 0: # Utilise la relation ManyToMany inverse
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}."},
status=status.HTTP_200_OK
)
except Student.DoesNotExist:
return JsonResponse(
{"error": "Étudiant non trouvé."},
status=status.HTTP_404_NOT_FOUND
)
except Guardian.DoesNotExist:
return JsonResponse(
{"error": "Guardian non trouvé."},
status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
return JsonResponse(
{"error": f"Une erreur est survenue : {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)

View File

@ -1,12 +1,12 @@
'use client'
import React, { useState, useEffect } from 'react';
import { fetchProfileRoles, updateProfileRoles, deleteProfileRoles } from '@/app/actions/authAction';
import { dissociateGuardian } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { useCsrfToken } from '@/context/CsrfContext';
import ProfileDirectory from '@/components/ProfileDirectory';
import { BE_AUTH_PROFILES_ROLES_URL } from '@/utils/Url';
export default function Page() {
const [profileRoles, setProfileRoles] = useState([]);
@ -54,12 +54,23 @@ export default function Page() {
});
};
const handleDissociate = (studentId, guardianId) => {
return dissociateGuardian(studentId, guardianId)
.then(() => {
logger.debug("Guardian dissociated successfully:", guardianId);
})
.catch(error => {
logger.error('Error dissociating guardian:', error);
throw error;
});
};
return (
<div className='p-8'>
<DjangoCSRFToken csrfToken={csrfToken} />
<div className="w-full p-4">
<ProfileDirectory profileRoles={profileRoles} handleActivateProfile={handleEdit} handleDeleteProfile={handleDelete} />
<ProfileDirectory profileRoles={profileRoles} handleActivateProfile={handleEdit} handleDeleteProfile={handleDelete} handleDissociateGuardian={handleDissociate} />
</div>
</div>
);

View File

@ -151,4 +151,18 @@ export const fetchTemplatesFromRegistrationFiles = async (id) => {
throw new Error('Erreur lors de la récupération des fichiers associés au groupe');
}
return response.json();
}
}
export const dissociateGuardian = async (studentId, guardianId) => {
const response = await fetch(`${BE_SUBSCRIPTION_STUDENTS_URL}/${studentId}/guardians/${guardianId}/dissociate`, {
credentials: 'include',
method: 'PUT',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error('Erreur lors de la dissociation.');
}
return response.json();
};

View File

@ -113,22 +113,13 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
if (existingProfile) {
// Vérifier si le profil a un rôle de type PARENT
const parentRole = existingProfile.roles.find(role => role.role_type === 2);
if (parentRole) {
console.log('Profil parent associé trouvé !', existingProfile);
setFormData((prevData) => ({
console.log('Profil associé trouvé !', existingProfile);
setFormData((prevData) => ({
...prevData,
guardianEmail: existingProfile.email, // Mettre à jour le champ guardianEmail avec l'email du profil
isExistingParentProfile: true, // Indiquer que le profil est un parent existant
existingProfileId: existingProfile.id
}));
} else {
console.log('Le profil existe mais n\'est pas de type PARENT.');
setFormData((prevData) => ({
...prevData,
isExistingParentProfile: false, // Réinitialiser si le profil n'est pas de type PARENT
}));
}
}
}

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Trash2, Eye, EyeOff, ToggleLeft, ToggleRight, Info } from 'lucide-react';
import { Trash2, Eye, EyeOff, ToggleLeft, ToggleRight, Info, XCircle } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import StatusLabel from '@/components/StatusLabel';
@ -32,7 +32,7 @@ const roleTypeToBadgeClass = (roleType) => {
}
};
const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeleteProfile }) => {
const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeleteProfile, handleDissociateGuardian }) => {
const parentProfiles = profileRoles.filter(profileRole => profileRole.role_type === 2);
const schoolAdminProfiles = profileRoles.filter(profileRole => profileRole.role_type !== 2);
@ -41,6 +41,15 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
const [confirmPopupMessage, setConfirmPopupMessage] = useState("");
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
const [visibleTooltipId, setVisibleTooltipId] = useState(null);
const handleTooltipVisibility = (id) => {
setVisibleTooltipId(id); // Définir l'ID de la ligne pour laquelle la tooltip est visible
};
const handleTooltipHide = () => {
setVisibleTooltipId(null); // Cacher toutes les tooltips
}
const handleConfirmActivateProfile = (profileRole) => {
setConfirmPopupMessage(`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`);
@ -76,6 +85,26 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
setConfirmPopupVisible(true);
};
const handleConfirmDissociateGuardian = (profileRole, student) => {
setVisibleTooltipId(null);
setConfirmPopupMessage(
`Vous êtes sur le point de dissocier le responsable ${profileRole.associated_person?.guardian_name} de l'élève ${student.student_name}. Êtes-vous sûr de vouloir poursuivre cette opération ?`
);
setConfirmPopupOnConfirm(() => () => {
handleDissociateGuardian(student.id, profileRole.associated_person?.id)
.then(() => {
setPopupMessage("Le responsable a été dissocié avec succès.");
setPopupVisible(true);
})
.catch(error => {
setPopupMessage("Erreur lors de la dissociation du responsable.");
setPopupVisible(true);
});
setConfirmPopupVisible(false);
});
setConfirmPopupVisible(true);
};
const parentColumns = [
{ name: 'Identifiant', transform: (row) => row.associated_profile_email },
{ name: 'Rôle', transform: (row) => (
@ -84,31 +113,47 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
</span>
)
},
{ name: 'Utilisateur', transform: (row) => (
<div className="flex items-center justify-center space-x-2">
{
name: 'Utilisateur',
transform: (row) => (
<div className="flex items-center justify-center space-x-2 relative">
<span>{row.associated_person?.guardian_name}</span>
{row.associated_person && (
<Tooltip content={
<div>
<div className="mb-2">
<strong>Elève(s) associé(s):</strong>
<div className="flex flex-col justify-center space-y-2">
{row.associated_person?.students?.map(student => (
<div key={student.student_name} className="flex justify-between items-center">
<span className="px-2 py-1 rounded-full text-gray-800">
{student.student_name}
</span>
<StatusLabel status={student.registration_status} showDropdown={false} />
</div>
))}
</div>
</div>
</div>
}>
<div
className="relative group"
onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne
onMouseLeave={handleTooltipHide} // Cacher la tooltip
>
<button className="text-blue-500 hover:text-blue-700">
<Info className="w-5 h-5" />
</button>
</Tooltip>
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
<div className="absolute z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg left-0 -translate-x-1/2 top-full">
<div className="mb-2">
<strong>Elève(s) associé(s):</strong>
<div className="flex flex-col justify-center space-y-2 mt-4">
{row.associated_person?.students?.map(student => (
<div key={student.student_name} className="flex justify-between items-center">
<span className="px-2 py-1 rounded-full text-gray-800 whitespace-nowrap inline-block min-w-0 max-w-fit">
{student.student_name}
</span>
<div className="flex items-center space-x-2">
<StatusLabel status={student.registration_status} showDropdown={false} />
<button
className="text-red-500 hover:text-red-700 flex items-center space-x-1"
onClick={() => handleConfirmDissociateGuardian(row, student)}
>
<XCircle className="w-5 h-5" />
<span className="text-sm">Dissocier</span>
</button>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
)}
</div>
)