feat: Ajout de la possibilité de supprimer une association

guardian/student + ajout de la possibilité de créer un guardian pour un
student + tri chrologique
This commit is contained in:
N3WT DE COMPET
2025-03-22 12:28:12 +01:00
parent 43ed495a9a
commit c9350a796b
12 changed files with 326 additions and 93 deletions

View File

@ -27,6 +27,7 @@ class ProfileRole(models.Model):
role_type = models.IntegerField(choices=RoleType.choices, default=RoleType.PROFIL_UNDEFINED) role_type = models.IntegerField(choices=RoleType.choices, default=RoleType.PROFIL_UNDEFINED)
establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='profile_roles') establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='profile_roles')
is_active = models.BooleanField(default=False) is_active = models.BooleanField(default=False)
updated_date = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):
return f"{self.profile.email} - {self.get_role_type_display()} - {self.establishment.name}" return f"{self.profile.email} - {self.get_role_type_display()} - {self.establishment.name}"

View File

@ -3,6 +3,9 @@ from Auth.models import Profile, ProfileRole
from Establishment.models import Establishment from Establishment.models import Establishment
from Subscriptions.models import Guardian, RegistrationForm from Subscriptions.models import Guardian, RegistrationForm
from School.models import Teacher from School.models import Teacher
from N3wtSchool import settings
from django.utils import timezone
import pytz
class ProfileSerializer(serializers.ModelSerializer): class ProfileSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) id = serializers.IntegerField(required=False)
@ -69,10 +72,11 @@ class ProfileRoleSerializer(serializers.ModelSerializer):
profile_data = ProfileSerializer(write_only=True, required=False) profile_data = ProfileSerializer(write_only=True, required=False)
associated_profile_email = serializers.SerializerMethodField() associated_profile_email = serializers.SerializerMethodField()
associated_person = serializers.SerializerMethodField() associated_person = serializers.SerializerMethodField()
updated_date_formatted = serializers.SerializerMethodField()
class Meta: class Meta:
model = ProfileRole 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): def create(self, validated_data):
profile_data = validated_data.pop('profile_data', None) profile_data = validated_data.pop('profile_data', None)
@ -142,4 +146,11 @@ class ProfileRoleSerializer(serializers.ModelSerializer):
"classes": classes_list, "classes": classes_list,
"specialities": specialities_list "specialities": specialities_list
} }
return None 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")

View File

@ -538,7 +538,7 @@ class ProfileRoleView(APIView):
profiles_roles_List = bdd.getAllObjects(_objectName=ProfileRole) profiles_roles_List = bdd.getAllObjects(_objectName=ProfileRole)
if profiles_roles_List: 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) profile_roles_serializer = ProfileRoleSerializer(profiles_roles_List, many=True)
return JsonResponse(profile_roles_serializer.data, safe=False) return JsonResponse(profile_roles_serializer.data, safe=False)

View File

@ -67,7 +67,6 @@ class TeacherSerializer(serializers.ModelSerializer):
specialities_data = validated_data.pop('specialities', None) specialities_data = validated_data.pop('specialities', None)
associated_profile_email = validated_data.pop('associated_profile_email') associated_profile_email = validated_data.pop('associated_profile_email')
establishment_id = validated_data.get('establishment') establishment_id = validated_data.get('establishment')
print(f'debug : {validated_data}')
role_type = validated_data.pop('role_type') role_type = validated_data.pop('role_type')
profile, created = Profile.objects.get_or_create( profile, created = Profile.objects.get_or_create(

View File

@ -26,7 +26,6 @@ def getStateMachineObjectState(etat):
def updateStateMachine(rf, transition) : def updateStateMachine(rf, transition) :
automateModel = load_config('Subscriptions/Configuration/automate.json') automateModel = load_config('Subscriptions/Configuration/automate.json')
state_machine = getStateMachineObject(rf.status) state_machine = getStateMachineObject(rf.status)
print(f'etat DI : {state_machine.state}')
if state_machine.trigger(transition, automateModel): if state_machine.trigger(transition, automateModel):
rf.status = state_machine.state rf.status = state_machine.state
rf.save() rf.save()

View File

@ -29,7 +29,7 @@ class Guardian(models.Model):
address = models.CharField(max_length=200, default="", blank=True) address = models.CharField(max_length=200, default="", blank=True)
phone = 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) 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): def __str__(self):
return self.last_name + "_" + self.first_name return self.last_name + "_" + self.first_name

View File

@ -5,6 +5,7 @@ from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from Subscriptions.models import Guardian, Student from Subscriptions.models import Guardian, Student
from Auth.models import ProfileRole
from N3wtSchool import bdd from N3wtSchool import bdd
class GuardianView(APIView): class GuardianView(APIView):
@ -50,12 +51,34 @@ class DissociateGuardianView(APIView):
# Supprimer la relation entre le student et le guardian # Supprimer la relation entre le student et le guardian
student.guardians.remove(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 # Vérifier si le guardian n'est plus associé à aucun élève
if guardian.student_set.count() == 0: # Utilise la relation ManyToMany inverse 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() guardian.delete()
return JsonResponse( 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 status=status.HTTP_200_OK
) )
except Student.DoesNotExist: except Student.DoesNotExist:

View File

@ -99,8 +99,7 @@ class RegisterFormView(APIView):
registerForms_List = None registerForms_List = None
if registerForms_List: if registerForms_List:
print(f'filtrate sur lestablishment : {establishment_id}') registerForms_List = registerForms_List.filter(establishment=establishment_id).order_by('-last_update')
registerForms_List = registerForms_List.filter(establishment=establishment_id)
if not registerForms_List: if not registerForms_List:
return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False) return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False)

View File

@ -10,6 +10,7 @@ import ProfileDirectory from '@/components/ProfileDirectory';
export default function Page() { export default function Page() {
const [profileRoles, setProfileRoles] = useState([]); const [profileRoles, setProfileRoles] = useState([]);
const [reloadFetch, setReloadFetch] = useState(false);
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment(); const { selectedEstablishmentId } = useEstablishment();
@ -19,7 +20,7 @@ export default function Page() {
// Fetch data for profileRoles // Fetch data for profileRoles
handleProfiles(); handleProfiles();
} }
}, [selectedEstablishmentId]); }, [selectedEstablishmentId, reloadFetch]);
const handleProfiles = () => { const handleProfiles = () => {
fetchProfileRoles(selectedEstablishmentId) fetchProfileRoles(selectedEstablishmentId)
@ -27,6 +28,7 @@ export default function Page() {
setProfileRoles(data); setProfileRoles(data);
}) })
.catch(error => logger.error('Error fetching profileRoles:', error)); .catch(error => logger.error('Error fetching profileRoles:', error));
setReloadFetch(false);
}; };
const handleEdit = (profileRole) => { const handleEdit = (profileRole) => {
@ -56,8 +58,37 @@ export default function Page() {
const handleDissociate = (studentId, guardianId) => { const handleDissociate = (studentId, guardianId) => {
return dissociateGuardian(studentId, guardianId) return dissociateGuardian(studentId, guardianId)
.then(() => { .then((response) => {
logger.debug("Guardian dissociated successfully:", guardianId); 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 => { .catch(error => {
logger.error('Error dissociating guardian:', error); logger.error('Error dissociating guardian:', error);

View File

@ -15,7 +15,6 @@ import { MoreVertical, Send, Edit, Trash2, FileText, CheckCircle, Plus } from '
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import InscriptionForm from '@/components/Inscription/InscriptionForm' import InscriptionForm from '@/components/Inscription/InscriptionForm'
import AffectationClasseForm from '@/components/AffectationClasseForm' import AffectationClasseForm from '@/components/AffectationClasseForm'
import { getSession } from 'next-auth/react';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { import {
@ -87,6 +86,7 @@ export default function Page({ params: { locale } }) {
const [tuitionFees, setTuitionFees] = useState([]); const [tuitionFees, setTuitionFees] = useState([]);
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
const [profiles, setProfiles] = useState([]); const [profiles, setProfiles] = useState([]);
const [isOpenAddGuardian, setIsOpenAddGuardian] = useState(false);
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment(); 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) => { const openModalAssociationEleve = (eleveSelected) => {
setIsOpenAffectationClasse(true); setIsOpenAffectationClasse(true);
setStudent(eleveSelected); 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 = [ const columns = [
{ name: t('studentName'), transform: (row) => row.student.last_name }, { name: t('studentName'), transform: (row) => row.student.last_name },
{ name: t('studentFistName'), transform: (row) => row.student.first_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
: (
<div className="flex justify-center h-full">
<button
className="flex items-center gap-2 text-blue-600 font-semibold hover:text-blue-800 transition duration-200 underline decoration-blue-600 hover:decoration-blue-800"
onClick={() => handleOpenAddGuardian(row.student)}
>
<span className="px-3 py-1 bg-blue-100 rounded-full hover:bg-blue-200 transition duration-200">
Ajouter un responsable
</span>
</button>
</div>
)
)
},
{ name: t('phone'), transform: (row) => formatPhoneNumber(row.student.guardians[0]?.phone) }, { name: t('phone'), transform: (row) => formatPhoneNumber(row.student.guardians[0]?.phone) },
{ name: t('lastUpdateDate'), transform: (row) => row.formatted_last_update}, { name: t('lastUpdateDate'), transform: (row) => row.formatted_last_update},
{ name: t('registrationFileStatus'), transform: (row) => ( { name: t('registrationFileStatus'), transform: (row) => (
@ -742,13 +835,29 @@ const columnsSubscribed = [
title="Affectation à une classe" title="Affectation à une classe"
ContentComponent={() => ( ContentComponent={() => (
<AffectationClasseForm <AffectationClasseForm
student={student} students={students}
onSubmit={affectationClassFormSubmitHandler} onSubmit={affectationClassFormSubmitHandler}
classes={classes} classes={classes}
/> />
)} )}
/> />
)} )}
{isOpenAddGuardian && (
<Modal
isOpen={isOpenAddGuardian}
setIsOpen={setIsOpenAddGuardian}
title="Ajouter un responsable"
ContentComponent={() => (
<InscriptionForm
students={students}
profiles={profiles}
onSubmit={updateRF}
currentStep={2}
showOnlyStep2={true}
/>
)}
/>
)}
</div> </div>
); );
} }

View File

@ -11,8 +11,19 @@ import ProgressStep from '@/components/ProgressStep';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, profiles, onSubmit, currentStep, groups }) => { const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, profiles, onSubmit, currentStep, groups, showOnlyStep2 = false }) => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState(() => {
if (showOnlyStep2) {
return {
guardianLastName: '',
guardianFirstName: '',
guardianEmail: '',
guardianPhone: '',
selectedGuardians: [],
responsableType: 'new',
};
}
return {
studentLastName: '', studentLastName: '',
studentFirstName: '', studentFirstName: '',
guardianLastName: '', guardianLastName: '',
@ -27,6 +38,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
selectedTuitionDiscounts: [], selectedTuitionDiscounts: [],
selectedTuitionFees: [], selectedTuitionFees: [],
selectedFileGroup: null // Ajout du groupe de fichiers sélectionné selectedFileGroup: null // Ajout du groupe de fichiers sélectionné
};
}); });
const [step, setStep] = useState(currentStep || 1); const [step, setStep] = useState(currentStep || 1);
@ -55,8 +67,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
formData.selectedGuardians.length > 0 || formData.selectedGuardians.length > 0 ||
(!formData.emailError && formData.guardianEmail.length > 0 && filteredStudents.length === 0) (!formData.emailError && formData.guardianEmail.length > 0 && filteredStudents.length === 0)
); );
const isStep3Valid = formData.selectedRegistrationFees.length > 0; const isStep3Valid = formData.selectedRegistrationFees?.length > 0;
const isStep4Valid = formData.selectedTuitionFees.length > 0; const isStep4Valid = formData.selectedTuitionFees?.length > 0;
const isStep5Valid = formData.selectedFileGroup !== null; const isStep5Valid = formData.selectedFileGroup !== null;
const isStep6Valid = isStep1Valid && isStep2Valid && isStep3Valid && isStep4Valid && isStep5Valid; const isStep6Valid = isStep1Valid && isStep2Valid && isStep3Valid && isStep4Valid && isStep5Valid;
@ -80,12 +92,14 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
}; };
useEffect(() => { useEffect(() => {
if (!showOnlyStep2) {
// Calcul du montant total des frais d'inscription lors de l'initialisation // Calcul du montant total des frais d'inscription lors de l'initialisation
const initialTotalRegistrationAmount = calculateFinalRegistrationAmount( const initialTotalRegistrationAmount = calculateFinalRegistrationAmount(
registrationFees.map(fee => fee.id), registrationFees.map(fee => fee.id),
[] []
); );
setTotalRegistrationAmount(initialTotalRegistrationAmount); setTotalRegistrationAmount(initialTotalRegistrationAmount);
}
}, [registrationDiscounts, registrationFees]); }, [registrationDiscounts, registrationFees]);
@ -123,15 +137,15 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
} }
} }
if (step < steps.length) { if (!showOnlyStep2 && step < steps.length) {
setStep(step + 1); setStep(step + 1);
} }
}; };
const prevStep = () => { const prevStep = () => {
if (step > 1) { if (!showOnlyStep2 && step > 1) {
setStep(step - 1); setStep(step - 1);
} }
}; };
const handleEleveSelection = (student) => { const handleEleveSelection = (student) => {
@ -270,6 +284,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
return ( return (
<div className="space-y-4 mt-6"> <div className="space-y-4 mt-6">
{!showOnlyStep2 && (
<ProgressStep <ProgressStep
steps={steps} steps={steps}
stepTitles={stepTitles} stepTitles={stepTitles}
@ -277,6 +292,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
setStep={setStep} setStep={setStep}
isStepValid={isStepValid} isStepValid={isStepValid}
/> />
)}
{step === 1 && ( {step === 1 && (
<div className="mt-6"> <div className="mt-6">
@ -308,7 +324,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
name="guardianLastName" name="guardianLastName"
type="text" type="text"
IconItem={User} IconItem={User}
placeholder="Nom du responsable (optionnel)" placeholder="Nom du responsable"
value={formData.guardianLastName} value={formData.guardianLastName}
onChange={handleChange} onChange={handleChange}
className="w-full mt-4" className="w-full mt-4"
@ -317,7 +333,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
name="guardianFirstName" name="guardianFirstName"
type="text" type="text"
IconItem={User} IconItem={User}
placeholder="Prénom du responsable (optionnel)" placeholder="Prénom du responsable"
value={formData.guardianFirstName} value={formData.guardianFirstName}
onChange={handleChange} onChange={handleChange}
className="w-full mt-4" className="w-full mt-4"
@ -663,44 +679,83 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
)} )}
<div className="flex justify-end mt-4 space-x-4"> <div className="flex justify-end mt-4 space-x-4">
{step > 1 && ( {showOnlyStep2 ? (
<Button text="Précédent" <>
onClick={prevStep} <Button
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md shadow-sm hover:bg-gray-400 focus:outline-none" text="Valider"
secondary onClick={submit}
name="Previous" /> className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
)} (
{step < steps.length ? ( (step === 1 && !isStep1Valid) ||
<Button text="Suivant" (step === 2 && !isStep2Valid) ||
onClick={nextStep} (step === 3 && !isStep3Valid) ||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${ (step === 4 && !isStep4Valid) ||
( (step === 5 && !isStep5Valid)
(step === 1 && !isStep1Valid) || )
(step === 2 && !isStep2Valid) || ? "bg-gray-300 text-gray-700 cursor-not-allowed"
(step === 3 && !isStep3Valid) || : "bg-emerald-500 text-white hover:bg-emerald-600"
(step === 4 && !isStep4Valid) || }`}
(step === 5 && !isStep5Valid) disabled={
) (
? "bg-gray-300 text-gray-700 cursor-not-allowed" (step === 1 && !isStep1Valid) ||
: "bg-emerald-500 text-white hover:bg-emerald-600" (step === 2 && !isStep2Valid) ||
}`} (step === 3 && !isStep3Valid) ||
disabled={ (step === 4 && !isStep4Valid) ||
( (step === 5 && !isStep5Valid)
(step === 1 && !isStep1Valid) || )
(step === 2 && !isStep2Valid) || }
(step === 3 && !isStep3Valid) || primary
(step === 4 && !isStep4Valid) || name="Validate"
(step === 5 && !isStep5Valid) />
) </>
}
primary
name="Next" />
) : ( ) : (
<Button text="Valider" <>
onClick={submit} {step > 1 && (
className="px-4 py-2 bg-emerald-500 text-white rounded-md shadow-sm hover:bg-emerald-600 focus:outline-none" <Button
primary text="Précédent"
name="Create" /> onClick={prevStep}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md shadow-sm hover:bg-gray-400 focus:outline-none"
secondary
name="Previous"
/>
)}
{step < steps.length ? (
<Button
text="Suivant"
onClick={nextStep}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(
(step === 1 && !isStep1Valid) ||
(step === 2 && !isStep2Valid) ||
(step === 3 && !isStep3Valid) ||
(step === 4 && !isStep4Valid) ||
(step === 5 && !isStep5Valid)
)
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
disabled={
(
(step === 1 && !isStep1Valid) ||
(step === 2 && !isStep2Valid) ||
(step === 3 && !isStep3Valid) ||
(step === 4 && !isStep4Valid) ||
(step === 5 && !isStep5Valid)
)
}
primary
name="Next"
/>
) : (
<Button
text="Valider"
onClick={submit}
className="px-4 py-2 bg-emerald-500 text-white rounded-md shadow-sm hover:bg-emerald-600 focus:outline-none"
primary
name="Create"
/>
)}
</>
)} )}
</div> </div>

View File

@ -107,6 +107,7 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
const parentColumns = [ const parentColumns = [
{ name: 'Identifiant', transform: (row) => row.associated_profile_email }, { name: 'Identifiant', transform: (row) => row.associated_profile_email },
{ name: 'Mise à jour', transform: (row) => row.updated_date_formatted },
{ name: 'Rôle', transform: (row) => ( { name: 'Rôle', transform: (row) => (
<span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}> <span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}>
{roleTypeToLabel(row.role_type)} {roleTypeToLabel(row.role_type)}
@ -120,40 +121,44 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
<span>{row.associated_person?.guardian_name}</span> <span>{row.associated_person?.guardian_name}</span>
{row.associated_person && ( {row.associated_person && (
<div <div
className="relative group" className="relative group"
onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne
onMouseLeave={handleTooltipHide} // Cacher la tooltip onMouseLeave={handleTooltipHide} // Cacher la tooltip
> >
<button className="text-blue-500 hover:text-blue-700"> <button className="relative text-blue-500 hover:text-blue-700 flex items-center justify-center">
<Info className="w-5 h-5" /> <div className="w-6 h-6 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center font-bold">
</button> {row.associated_person?.students?.length || 0}
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond </div>
<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"> </button>
<div className="mb-2"> {visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
<strong>Elève(s) associé(s):</strong> <div
<div className="flex flex-col justify-center space-y-2 mt-4"> className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2"
{row.associated_person?.students?.map(student => ( >
<div key={student.student_name} className="flex justify-between items-center"> <div className="mb-2">
<span className="px-2 py-1 rounded-full text-gray-800 whitespace-nowrap inline-block min-w-0 max-w-fit"> <strong>Elève(s) associé(s):</strong>
{student.student_name} <div className="flex flex-col justify-center space-y-2 mt-4">
</span> {row.associated_person?.students?.map(student => (
<div className="flex items-center space-x-2"> <div key={student.student_name} className="flex justify-between items-center">
<StatusLabel status={student.registration_status} showDropdown={false} /> <span className="px-2 py-1 rounded-full text-gray-800 whitespace-nowrap inline-block min-w-0 max-w-fit">
<button {student.student_name}
className="text-red-500 hover:text-red-700 flex items-center space-x-1" </span>
onClick={() => handleConfirmDissociateGuardian(row, student)} <div className="flex items-center space-x-2">
> <StatusLabel status={student.registration_status} showDropdown={false} />
<XCircle className="w-5 h-5" /> <button
<span className="text-sm">Dissocier</span> className="text-red-500 hover:text-red-700 flex items-center space-x-1"
</button> onClick={() => handleConfirmDissociateGuardian(row, student)}
</div> >
<XCircle className="w-5 h-5" />
<span className="text-sm">Dissocier</span>
</button>
</div> </div>
))} </div>
</div> ))}
</div> </div>
</div> </div>
)} </div>
</div> )}
</div>
)} )}
</div> </div>
) )
@ -183,6 +188,7 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
const schoolAdminColumns = [ const schoolAdminColumns = [
{ name: 'Identifiant', transform: (row) => row.associated_profile_email }, { name: 'Identifiant', transform: (row) => row.associated_profile_email },
{ name: 'Mise à jour', transform: (row) => row.updated_date_formatted },
{ name: 'Rôle', transform: (row) => ( { name: 'Rôle', transform: (row) => (
<span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}> <span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}>
{roleTypeToLabel(row.role_type)} {roleTypeToLabel(row.role_type)}