feat: Création d'un annuaire / mise à jour du subscribe

This commit is contained in:
N3WT DE COMPET
2025-03-14 19:51:35 +01:00
parent cd9c10a88a
commit 6bd5704983
22 changed files with 585 additions and 185 deletions

View File

@ -1,6 +1,8 @@
from rest_framework import serializers
from Auth.models import Profile, ProfileRole
from Establishment.models import Establishment
from Subscriptions.models import Guardian, RegistrationForm
from School.models import Teacher
class ProfileSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
@ -14,7 +16,7 @@ class ProfileSerializer(serializers.ModelSerializer):
def get_roles(self, obj):
roles = ProfileRole.objects.filter(profile=obj)
return [{'role_type': role.role_type, 'establishment': role.establishment.id, 'is_active': role.is_active} for role in roles]
return [{'role_type': role.role_type, 'establishment': role.establishment.id, 'establishment_name': role.establishment.name, 'is_active': role.is_active} for role in roles]
def create(self, validated_data):
user = Profile(
@ -48,10 +50,12 @@ class ProfileSerializer(serializers.ModelSerializer):
class ProfileRoleSerializer(serializers.ModelSerializer):
profile = serializers.PrimaryKeyRelatedField(queryset=Profile.objects.all(), required=False)
profile_data = ProfileSerializer(write_only=True, required=False)
associated_profile_email = serializers.SerializerMethodField()
associated_person = serializers.SerializerMethodField()
class Meta:
model = ProfileRole
fields = ['role_type', 'establishment', 'is_active', 'profile', 'profile_data']
fields = ['id', 'role_type', 'establishment', 'is_active', 'profile', 'profile_data', 'associated_profile_email', 'associated_person']
def create(self, validated_data):
profile_data = validated_data.pop('profile_data', None)
@ -83,3 +87,39 @@ class ProfileRoleSerializer(serializers.ModelSerializer):
instance.is_active = validated_data.get('is_active', instance.is_active)
instance.save()
return instance
def get_associated_profile_email(self, obj):
if obj.profile:
return obj.profile.email
return None
def get_associated_person(self, obj):
if obj.role_type == ProfileRole.RoleType.PROFIL_PARENT:
guardian = Guardian.objects.filter(profile_role=obj).first()
if guardian:
students = guardian.student_set.all()
students_list = []
for student in students:
registration_form = RegistrationForm.objects.filter(student=student).first()
registration_status = registration_form.status if registration_form else None
students_list.append({
"student_name": f"{student.last_name} {student.first_name}",
"registration_status": registration_status
})
return {
"guardian_name": f"{guardian.last_name} {guardian.first_name}",
"students": students_list
}
else:
teacher = Teacher.objects.filter(profile_role=obj).first()
if teacher:
classes = teacher.schoolclass_set.all()
classes_list = [{"id": classe.id, "name": classe.atmosphere_name} for classe in classes]
specialities = teacher.specialities.all()
specialities_list = [{"name": speciality.name, "color_code": speciality.color_code} for speciality in specialities]
return {
"teacher_name": f"{teacher.last_name} {teacher.first_name}",
"classes": classes_list,
"specialities": specialities_list
}
return None

View File

@ -200,7 +200,7 @@ class LoginView(APIView):
primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type, is_active=True).first()
if not primary_role:
return JsonResponse({"errorMessage": "Role not assigned to the user"}, status=status.HTTP_401_UNAUTHORIZED)
return JsonResponse({"errorMessage": "Profil inactif"}, status=status.HTTP_401_UNAUTHORIZED)
login(request, user)
user.save()
@ -305,10 +305,10 @@ class RefreshJWTView(APIView):
role_type = payload.get('role_type')
# Récupérer le rôle principal de l'utilisateur
primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type).first()
primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type, is_active=True).first()
if not primary_role:
return JsonResponse({'errorMessage': 'No role assigned to the user'}, status=400)
return JsonResponse({'errorMessage': 'Profil inactif'}, status=400)
# Générer un nouveau Access Token avec les informations complètes
new_access_payload = {
@ -383,10 +383,7 @@ class SubscribeView(APIView):
retourErreur = error.returnMessage[error.BAD_URL]
retour = ''
newProfilConnection = JSONParser().parse(request)
establishment_id = request.GET.get('establishment_id')
if not establishment_id:
return JsonResponse({'message': retour, 'errorMessage': 'establishment_id manquant', "errorFields": {}, "id": -1}, safe=False, status=status.HTTP_400_BAD_REQUEST)
establishment_id = newProfilConnection['establishment_id']
validatorSubscription = validator.ValidatorSubscription(data=newProfilConnection)
validationOk, errorFields = validatorSubscription.validate()
@ -398,7 +395,7 @@ class SubscribeView(APIView):
retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS]
else:
# Vérifier si le profil a déjà un rôle actif pour l'établissement donné
active_roles = ProfileRole.objects.filter(profile=profil, establishment_id=establishment_id, is_active=True)
active_roles = ProfileRole.objects.filter(profile=profil, establishment=establishment_id, is_active=True)
if active_roles.exists():
retourErreur = error.returnMessage[error.PROFIL_ACTIVE]
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": profil.id}, safe=False)
@ -408,11 +405,16 @@ class SubscribeView(APIView):
profil.full_clean()
profil.save()
# Utiliser le sérialiseur ProfileRoleSerializer pour créer ou mettre à jour le rôle
# Récupérer le ProfileRole existant pour l'établissement et le profil
profile_role = ProfileRole.objects.filter(profile=profil, establishment=establishment_id).first()
if profile_role:
profile_role.is_active = True
profile_role.save()
else:
# Si aucun ProfileRole n'existe, en créer un nouveau
role_data = {
'profile': profil.id,
'establishment_id': establishment_id,
'role_type': ProfileRole.RoleType.PROFIL_PARENT,
'establishment': establishment_id,
'is_active': True
}
role_serializer = ProfileRoleSerializer(data=role_data)
@ -536,7 +538,13 @@ class ProfileRoleView(APIView):
responses={200: ProfileRoleSerializer(many=True)}
)
def get(self, request):
establishment_id = request.GET.get('establishment_id', None)
if establishment_id is None:
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
profiles_roles_List = bdd.getAllObjects(_objectName=ProfileRole)
if profiles_roles_List:
profiles_roles_List = profiles_roles_List.filter(establishment=establishment_id).distinct()
profile_roles_serializer = ProfileRoleSerializer(profiles_roles_List, many=True)
return JsonResponse(profile_roles_serializer.data, safe=False)

View File

@ -103,20 +103,26 @@ class Command(BaseCommand):
# Créer entre 1 et 3 ProfileRole pour chaque profil
num_roles = random.randint(1, 3)
created_roles = set()
for _ in range(num_roles):
establishment = random.choice(self.establishments)
role_type = random.choice([ProfileRole.RoleType.PROFIL_ECOLE, ProfileRole.RoleType.PROFIL_ADMIN, ProfileRole.RoleType.PROFIL_PARENT])
# Vérifier si le rôle existe déjà pour cet établissement
if (establishment.id, role_type) in created_roles:
continue
profile_role_data = {
"profile": profile.id,
"establishment": establishment.id,
"role_type": role_type,
"is_active": True
"is_active": random.choice([True, False])
}
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
if profile_role_serializer.is_valid():
profile_role_serializer.save()
created_roles.add((establishment.id, role_type))
self.stdout.write(self.style.SUCCESS(f'ProfileRole for {profile.email} created successfully with role type {role_type}'))
else:
self.stdout.write(self.style.ERROR(f'Error in data for profile role: {profile_role_serializer.errors}'))
@ -129,7 +135,7 @@ class Command(BaseCommand):
for fee_data in fees_data:
establishment = random.choice(self.establishments)
print(f'establishment : {establishment}')
fee_data["name"] = f"{fee_data['name']} - {establishment.name}"
fee_data["name"] = fee_data['name']
fee_data["establishment"] = establishment.id
fee_data["type"] = random.choice([FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE])
@ -145,7 +151,7 @@ class Command(BaseCommand):
for discount_data in discounts_data:
establishment = random.choice(self.establishments)
discount_data["name"] = f"{discount_data['name']} - {establishment.name}"
discount_data["name"] = discount_data['name']
discount_data["establishment"] = establishment.id
discount_data["type"] = random.choice([FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE])
discount_data["discount_type"] = random.choice([DiscountType.CURRENCY, DiscountType.PERCENT])
@ -216,7 +222,7 @@ class Command(BaseCommand):
for speciality_data in specialities_data:
establishment = random.choice(self.establishments)
speciality_data["name"] = f"{speciality_data['name']} - {establishment.name}"
speciality_data["name"] = speciality_data['name']
speciality_data["establishment"] = establishment.id
serializer = SpecialitySerializer(data=speciality_data)
@ -260,7 +266,7 @@ class Command(BaseCommand):
# Générer des données fictives pour l'enseignant
teacher_data = {
"last_name": fake.last_name(),
"first_name": f"{fake.first_name()} - {profile_role.establishment.name}",
"first_name": fake.first_name(),
"profile_role": profile_role.id
}
@ -287,7 +293,7 @@ class Command(BaseCommand):
for index, class_data in enumerate(school_classes_data, start=1):
# Randomize establishment
establishment = random.choice(self.establishments)
class_data["atmosphere_name"] = f"Classe {index} - {establishment.name}"
class_data["atmosphere_name"] = f"Classe {index}"
class_data["establishment"] = establishment.id
# Randomize levels
@ -295,9 +301,12 @@ class Command(BaseCommand):
# Randomize teachers
establishment_teachers = list(Teacher.objects.filter(profile_role__establishment=establishment))
num_teachers = min(random.randint(1, 10), len(establishment_teachers))
if len(establishment_teachers) > 0:
num_teachers = min(2, len(establishment_teachers))
selected_teachers = random.sample(establishment_teachers, num_teachers)
teachers_ids = [teacher.id for teacher in selected_teachers]
else:
teachers_ids = []
# Use the serializer to create or update the school class
class_data["teachers"] = teachers_ids
@ -315,7 +324,7 @@ class Command(BaseCommand):
for establishment in self.establishments:
for i in range(1, 4): # Créer 3 groupes de fichiers par établissement
name = f"Fichiers d'inscription - {fake.word()} - {establishment.name}"
name = f"Fichiers d'inscription - {fake.word()}"
description = fake.sentence()
group_data = {
"name": name,
@ -342,8 +351,6 @@ class Command(BaseCommand):
used_profiles = set()
for _ in range(50):
establishment = random.choice(self.establishments)
# Récupérer un profil aléatoire qui n'a pas encore été utilisé
available_profiles = profiles_with_parent_role.exclude(id__in=used_profiles)
if not available_profiles.exists():
@ -354,7 +361,8 @@ class Command(BaseCommand):
used_profiles.add(profile.id)
# Récupérer le ProfileRole Parent associé au profil
profile_role = ProfileRole.objects.filter(profile=profile, role_type=ProfileRole.RoleType.PROFIL_PARENT).first()
profile_roles = ProfileRole.objects.filter(profile=profile, role_type=ProfileRole.RoleType.PROFIL_PARENT)
profile_role = random.choice(profile_roles)
# Générer des données fictives pour le guardian
guardian_data = {
@ -370,7 +378,7 @@ class Command(BaseCommand):
# Générer des données fictives pour l'étudiant
student_data = {
"last_name": fake.last_name(),
"first_name": f"{fake.first_name()} - {establishment.name}",
"first_name": fake.first_name(),
"address": fake.address(),
"birth_date": fake.date_of_birth(),
"birth_place": fake.city(),
@ -398,14 +406,14 @@ class Command(BaseCommand):
# Créer les données du formulaire d'inscription
register_form_data = {
"fileGroup": RegistrationFileGroup.objects.get(id=fake.random_int(min=1, max=file_group_count)),
"establishment": establishment,
"establishment": profile_role.establishment,
"status": fake.random_int(min=1, max=3)
}
# Créer ou mettre à jour le formulaire d'inscription
register_form, created = RegistrationForm.objects.get_or_create(
student=student,
establishment=establishment,
establishment=profile_role.establishment,
defaults=register_form_data
)

View File

@ -49,7 +49,9 @@ class TeacherSerializer(serializers.ModelSerializer):
if profile_role_data:
establishment_id = profile_role_data.pop('establishment').id
profile_id = profile_role_data.pop('profile').id
profile_role_data['establishment'] = establishment_id
profile_role_data['profile'] = profile_id
# Créer l'instance de ProfileRole
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
@ -95,7 +97,9 @@ class TeacherSerializer(serializers.ModelSerializer):
def get_role_type(self, obj):
profile_role = obj.profile_role
if profile_role:
return {'role_type': profile_role.role_type, 'establishment': profile_role.establishment.name}
return None
def get_specialities_details(self, obj):
return [{'id': speciality.id, 'name': speciality.name, 'color_code': speciality.color_code} for speciality in obj.specialities.all()]

View File

@ -23,7 +23,7 @@ def envoieReinitMotDePasse(recipients, code):
return errorMessage
def sendRegisterForm(recipients):
def sendRegisterForm(recipients, establishment_id):
errorMessage = ''
try:
print(f'{settings.EMAIL_HOST_USER}')
@ -31,7 +31,8 @@ def sendRegisterForm(recipients):
EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Dossier Inscription'
context = {
'BASE_URL': settings.BASE_URL,
'email': recipients
'email': recipients,
'establishment': establishment_id
}
subject = EMAIL_INSCRIPTION_SUBJECT

View File

@ -38,7 +38,7 @@
<div class="content">
<p>Bonjour,</p>
<p>Nous vous confirmons la réception de votre demande d'inscription, vous trouverez ci-joint le lien vers la page d'authentification : <a href="{{BASE_URL}}/users/login">{{BASE_URL}}/users/login</a></p>
<p>S'il s'agit de votre première connexion, veuillez procéder à l'activation de votre compte à cette url : <a href="{{BASE_URL}}/users/subscribe">{{BASE_URL}}/users/subscribe</a></p>
<p>S'il s'agit de votre première connexion, veuillez procéder à l'activation de votre compte à cette url : <a href="{{BASE_URL}}/users/subscribe?establishment_id={{establishment}}">{{BASE_URL}}/users/subscribe</a></p>
<p>votre identifiant est : {{ email }}</p>
<p>Merci de compléter votre dossier d'inscription en suivant les instructions fournies.</p>
<p>Cordialement,</p>

View File

@ -332,8 +332,8 @@ def send(request,id):
if register_form != None:
student = register_form.student
guardian = student.getMainGuardian()
email = guardian.email
errorMessage = mailer.sendRegisterForm(email)
email = guardian.profile_role.profile.email
errorMessage = mailer.sendRegisterForm(email, register_form.establishment.pk)
if errorMessage == '':
register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
updateStateMachine(register_form, 'envoiDI')

View File

@ -2,6 +2,7 @@
"dashboard": "Dashboard",
"subscriptions": "Subscriptions",
"structure": "Structure",
"directory": "Directory",
"events": "Events",
"grades": "Grades",
"settings": "Settings",

View File

@ -2,6 +2,7 @@
"dashboard": "Tableau de bord",
"subscriptions": "Inscriptions",
"structure": "Structure",
"directory": "Annuaire",
"events": "Evenements",
"grades": "Notes",
"settings": "Paramètres",

View File

@ -0,0 +1,66 @@
'use client'
import React, { useState, useEffect } from 'react';
import { fetchProfileRoles, updateProfileRoles, deleteProfileRoles } from '@/app/actions/authAction';
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([]);
const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment();
useEffect(() => {
if (selectedEstablishmentId) {
// Fetch data for profileRoles
handleProfiles();
}
}, [selectedEstablishmentId]);
const handleProfiles = () => {
fetchProfileRoles(selectedEstablishmentId)
.then(data => {
setProfileRoles(data);
})
.catch(error => logger.error('Error fetching profileRoles:', error));
};
const handleEdit = (profileRole) => {
const updatedData = { ...profileRole, is_active: !profileRole.is_active };
return updateProfileRoles(profileRole.id, updatedData, csrfToken)
.then(data => {
setProfileRoles(prevState => prevState.map(item => item.id === profileRole.id ? data : item));
return data;
})
.catch(error => {
logger.error('Error editing data:', error);
throw error;
});
};
const handleDelete = (id) => {
return deleteProfileRoles(id, csrfToken)
.then(() => {
setProfileRoles(prevState => prevState.filter(item => item.id !== id));
logger.debug("Profile deleted successfully:", id);
})
.catch(error => {
logger.error('Error deleting profile:', error);
throw error;
});
};
return (
<div className='p-8'>
<DjangoCSRFToken csrfToken={csrfToken} />
<div className="w-full p-4">
<ProfileDirectory profileRoles={profileRoles} handleActivateProfile={handleEdit} handleDeleteProfile={handleDelete} />
</div>
</div>
);
}

View File

@ -5,12 +5,13 @@ import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import {
LayoutDashboard,
FileText,
School,
Users,
Building,
Home,
Award,
Calendar,
Settings,
FileText,
LogOut,
Menu,
X
@ -22,6 +23,7 @@ import {
FE_ADMIN_HOME_URL,
FE_ADMIN_SUBSCRIPTIONS_URL,
FE_ADMIN_STRUCTURE_URL,
FE_ADMIN_DIRECTORY_URL,
FE_ADMIN_GRADES_URL,
FE_ADMIN_PLANNING_URL,
FE_ADMIN_SETTINGS_URL
@ -43,10 +45,11 @@ export default function Layout({
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const sidebarItems = {
"admin": { "id": "admin", "name": t('dashboard'), "url": FE_ADMIN_HOME_URL, "icon": Home },
"subscriptions": { "id": "subscriptions", "name": t('subscriptions'), "url": FE_ADMIN_SUBSCRIPTIONS_URL, "icon": Users },
"structure": { "id": "structure", "name": t('structure'), "url": FE_ADMIN_STRUCTURE_URL, "icon": Building },
"grades": { "id": "grades", "name": t('grades'), "url": FE_ADMIN_GRADES_URL, "icon": FileText },
"admin": { "id": "admin", "name": t('dashboard'), "url": FE_ADMIN_HOME_URL, "icon": LayoutDashboard },
"subscriptions": { "id": "subscriptions", "name": t('subscriptions'), "url": FE_ADMIN_SUBSCRIPTIONS_URL, "icon": FileText },
"structure": { "id": "structure", "name": t('structure'), "url": FE_ADMIN_STRUCTURE_URL, "icon": School },
"directory": { "id": "directory", "name": t('directory'), "url": FE_ADMIN_DIRECTORY_URL, "icon": Users },
"grades": { "id": "grades", "name": t('grades'), "url": FE_ADMIN_GRADES_URL, "icon": Award },
"planning": { "id": "planning", "name": t('events'), "url": FE_ADMIN_PLANNING_URL, "icon": Calendar },
"settings": { "id": "settings", "name": t('settings'), "url": FE_ADMIN_SETTINGS_URL, "icon": Settings }
};

View File

@ -6,7 +6,8 @@ import FeesManagement from '@/components/Structure/Tarification/FeesManagement';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { useCsrfToken } from '@/context/CsrfContext';
import { ClassesProvider } from '@/context/ClassesContext';
import { createDatas,
import {
createDatas,
updateDatas,
removeDatas,
fetchSpecialities,
@ -21,11 +22,10 @@ import { createDatas,
fetchTuitionPaymentPlans,
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes } from '@/app/actions/schoolAction';
import { fetchProfileRoles } from '@/app/actions/authAction';
import SidebarTabs from '@/components/SidebarTabs';
import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManagement';
import {
fetchRegistrationTemplateMaster
} from "@/app/actions/registerFileGroupAction";
import { fetchRegistrationTemplateMaster } from "@/app/actions/registerFileGroupAction";
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';

View File

@ -453,8 +453,8 @@ useEffect(()=>{
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[0].associated_profile_email },
{ name: t('phone'), transform: (row) => formatPhoneNumber(row.student.guardians[0].phone) },
{ 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) => (
<div className="flex justify-center items-center h-full">

View File

@ -13,8 +13,8 @@ import { User, KeySquare } from 'lucide-react'; // Importez directement les icô
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { useCsrfToken } from '@/context/CsrfContext';
import { subscribe } from '@/app/actions/authAction';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function Page() {
const searchParams = useSearchParams();
@ -30,55 +30,18 @@ export default function Page() {
const router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => {
if (useFakeData) {
setIsLoading(true);
// Simuler une réponse réussie
const data = {
errorFields: {},
errorMessage: ""
};
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
setIsLoading(false);
}
}, []);
const establishment_id = searchParams.get('establishment_id');
function isOK(data) {
return data.errorMessage === ""
}
function subscribeFormSubmit(formData) {
if (useFakeData) {
// Simuler une réponse réussie
const data = {
errorFields: {},
errorMessage: ""
};
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
if(isOK(data)){
setPopupMessage("Votre compte a été créé avec succès");
setPopupVisible(true);
} else {
if(data.errorMessage){
setErrorMessage(data.errorMessage);
}
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
}
} else {
const data ={
email: formData.get('login'),
password1: formData.get('password1'),
password2: formData.get('password2'),
establishment_id: establishment_id
}
subscribe(data,csrfToken).then(data => {
logger.debug('Success:', data);
@ -106,7 +69,6 @@ export default function Page() {
logger.debug(error);
});
}
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader

View File

@ -4,6 +4,7 @@ import {
BE_AUTH_REFRESH_JWT_URL,
BE_AUTH_REGISTER_URL,
BE_AUTH_PROFILES_URL,
BE_AUTH_PROFILES_ROLES_URL,
BE_AUTH_RESET_PASSWORD_URL,
BE_AUTH_NEW_PASSWORD_URL,
FE_USERS_LOGIN_URL,
@ -77,6 +78,41 @@ export const disconnect = () => {
signOut({ callbackUrl: FE_USERS_LOGIN_URL });
};
export const fetchProfileRoles = (establishment) => {
return fetch(`${BE_AUTH_PROFILES_ROLES_URL}?establishment_id=${establishment}`)
.then(requestResponseHandler)
};
export const updateProfileRoles = (id, data, csrfToken) => {
const request = new Request(
`${BE_AUTH_PROFILES_ROLES_URL}/${id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify(data),
}
);
return fetch(request).then(requestResponseHandler);
};
export const deleteProfileRoles = (id, csrfToken) => {
const request = new Request(
`${BE_AUTH_PROFILES_ROLES_URL}/${id}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken
},
credentials: 'include'
}
);
return fetch(request).then(requestResponseHandler);
};
export const createProfile = (data, csrfToken) => {
const request = new Request(
`${BE_AUTH_PROFILES_URL}`,

View File

@ -22,7 +22,7 @@ const CheckBoxList = ({
<div className={`mt-2 grid ${horizontal ? 'grid-cols-6 gap-2' : 'grid-cols-1 gap-4'}`}>
{items.map(item => (
<CheckBox
key={item.id}
key={`${fieldName}-${item.id}`}
item={item}
formData={formData}
handleChange={handleChange}

View File

@ -291,6 +291,24 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
</div>
{formData.responsableType === 'new' && (
<>
<InputTextIcon
name="guardianLastName"
type="text"
IconItem={User}
placeholder="Nom du responsable (optionnel)"
value={formData.guardianLastName}
onChange={handleChange}
className="w-full mt-4"
/>
<InputTextIcon
name="guardianFirstName"
type="text"
IconItem={User}
placeholder="Prénom du responsable (optionnel)"
value={formData.guardianFirstName}
onChange={handleChange}
className="w-full mt-4"
/>
<InputTextIcon
name="guardianEmail"
type="email"

View File

@ -0,0 +1,218 @@
import React, { useState } from 'react';
import { Trash2, Eye, EyeOff, ToggleLeft, ToggleRight } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import StatusLabel from '@/components/StatusLabel';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
const roleTypeToLabel = (roleType) => {
switch (roleType) {
case 0:
return 'ECOLE';
case 1:
return 'ADMIN';
case 2:
return 'PARENT';
default:
return 'UNKNOWN';
}
};
const roleTypeToBadgeClass = (roleType) => {
switch (roleType) {
case 0:
return 'bg-blue-100 text-blue-600';
case 1:
return 'bg-red-100 text-red-600';
case 2:
return 'bg-green-100 text-green-600';
default:
return 'bg-gray-100 text-gray-600';
}
};
const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeleteProfile }) => {
const parentProfiles = profileRoles.filter(profileRole => profileRole.role_type === 2);
const schoolAdminProfiles = profileRoles.filter(profileRole => profileRole.role_type !== 2);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
const [confirmPopupMessage, setConfirmPopupMessage] = useState("");
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
const handleConfirmActivateProfile = (profileRole) => {
setConfirmPopupMessage(`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`);
setConfirmPopupOnConfirm(() => () => {
handleActivateProfile(profileRole)
.then(() => {
setPopupMessage(`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`);
setPopupVisible(true);
})
.catch(error => {
setPopupMessage(`Erreur lors de la ${profileRole.is_active ? 'désactivation' : 'activation'} du profil.`);
setPopupVisible(true);
});
setConfirmPopupVisible(false);
});
setConfirmPopupVisible(true);
};
const handleConfirmDeleteProfile = (id) => {
setConfirmPopupMessage("Êtes-vous sûr de vouloir supprimer ce profil ?");
setConfirmPopupOnConfirm(() => () => {
handleDeleteProfile(id)
.then(() => {
setPopupMessage("Le profil a été supprimé avec succès.");
setPopupVisible(true);
})
.catch(error => {
setPopupMessage("Erreur lors de la suppression du profil.");
setPopupVisible(true);
});
setConfirmPopupVisible(false);
});
setConfirmPopupVisible(true);
};
const parentColumns = [
{ name: 'Identifiant', transform: (row) => row.associated_profile_email },
{ name: 'Rôle', transform: (row) => (
<span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}>
{roleTypeToLabel(row.role_type)}
</span>
)
},
{ name: 'Utilisateur', transform: (row) => row.associated_person?.guardian_name },
{ name: 'Elève(s) associé(s)', transform: (row) => (
<div className="flex flex-col justify-center space-y-2">
{row.associated_person?.students?.map(student => (
<span key={student.student_name} className="px-2 py-1 rounded-full text-gray-800">
{student.student_name}
</span>
))}
</div>
)
},
{ name: 'Etat du dossier d\'inscription', transform: (row) => (
<div className="flex flex-col justify-center items-center space-y-2">
{row.associated_person?.students?.map(student => (
<StatusLabel key={student.student_name} status={student.registration_status} showDropdown={false} />
))}
</div>
)
},
{
name: 'Actions',
transform: (row) => (
<div className="flex justify-center space-x-2">
<button
type="button"
className={row.is_active ? 'text-emerald-500 hover:text-emerald-700' : 'text-orange-500 hover:text-orange-700'}
onClick={() => handleConfirmActivateProfile(row)}
>
{row.is_active ? <ToggleRight className="w-5 h-5 " /> : <ToggleLeft className="w-5 h-5" />}
</button>
<button
type="button"
className="text-red-500 hover:text-red-700"
onClick={() => handleConfirmDeleteProfile(row.id)}
>
<Trash2 className="w-5 h-5" />
</button>
</div>
)
}
];
const schoolAdminColumns = [
{ name: 'Identifiant', transform: (row) => row.associated_profile_email },
{ name: 'Rôle', transform: (row) => (
<span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}>
{roleTypeToLabel(row.role_type)}
</span>
)
},
{ name: 'Utilisateur', transform: (row) => row.associated_person?.teacher_name },
{ name: 'Classe(s) associée(s)', transform: (row) => (
<div className="flex flex-wrap justify-center space-x-2">
{row.associated_person?.classes?.map(classe => (
<span key={classe.id} className="px-2 py-1 rounded-full bg-gray-200 text-gray-800">
{classe.name}
</span>
))}
</div>
)
},
{ name: 'Spécialités', transform: (row) => (
<div className="flex flex-wrap justify-center space-x-2">
{row.associated_person?.specialities?.map(speciality => (
<SpecialityItem speciality={speciality} isDraggable={false}/>
))}
</div>
)
},
{
name: 'Actions',
transform: (row) => (
<div className="flex justify-center space-x-2">
<button
type="button"
className={row.is_active ? 'text-emerald-500 hover:text-emerald-700' : 'text-orange-500 hover:text-orange-700'}
onClick={() => handleConfirmActivateProfile(row)}
>
{row.is_active ? <ToggleRight className="w-5 h-5 " /> : <ToggleLeft className="w-5 h-5" />}
</button>
<button
type="button"
className="text-red-500 hover:text-red-700"
onClick={() => handleConfirmDeleteProfile(row.id)}
>
<Trash2 className="w-5 h-5" />
</button>
</div>
)
}
];
return (
<div className="bg-white rounded-lg shadow-lg w-full p-6">
<div className="space-y-8">
<div className="max-h-128 overflow-y-auto border rounded p-4">
{parentProfiles.length === 0 ? (
<div>Aucun profil trouvé</div>
) : (
<Table
data={parentProfiles}
columns={parentColumns}
/>
)}
</div>
<div className="max-h-128 overflow-y-auto border rounded p-4">
{schoolAdminProfiles.length === 0 ? (
<div>Aucun profil trouvé</div>
) : (
<Table
data={schoolAdminProfiles}
columns={schoolAdminColumns}
/>
)}
</div>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={confirmPopupVisible}
message={confirmPopupMessage}
onConfirm={confirmPopupOnConfirm}
onCancel={() => setConfirmPopupVisible(false)}
/>
</div>
);
};
export default ProfileDirectory;

View File

@ -4,6 +4,8 @@ import { SessionProvider } from "next-auth/react"
import { CsrfProvider } from '@/context/CsrfContext'
import { NextIntlClientProvider } from 'next-intl'
import { EstablishmentProvider } from '@/context/EstablishmentContext';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
export default function Providers({ children, messages, locale, session }) {
@ -13,6 +15,7 @@ export default function Providers({ children, messages, locale, session }) {
}
return (
<SessionProvider session={session}>
<DndProvider backend={HTML5Backend}>
<CsrfProvider>
<EstablishmentProvider>
<NextIntlClientProvider messages={messages} locale={locale}>
@ -20,6 +23,7 @@ export default function Providers({ children, messages, locale, session }) {
</NextIntlClientProvider>
</EstablishmentProvider>
</CsrfProvider>
</DndProvider>
</SessionProvider>
)
}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Plus, Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react';
import { Plus, Edit3, Trash2, GraduationCap, Check, X, Hand, Search } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import ToggleSwitch from '@/components/ToggleSwitch';
@ -11,6 +11,8 @@ import InputText from '@/components/InputText';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import TeacherItem from './TeacherItem';
import logger from '@/utils/logger';
import { fetchProfiles } from '@/app/actions/authAction';
import { useEstablishment } from '@/context/EstablishmentContext';
const ItemTypes = {
SPECIALITY: 'speciality',
@ -103,14 +105,40 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
const [confirmPopupMessage, setConfirmPopupMessage] = useState("");
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
const { selectedEstablishmentId } = useEstablishment();
const handleSelectProfile = (profile) => {
setNewTeacher((prevData) => ({
...prevData,
selectedProfile: profile,
}));
setFormData((prevData) => ({
...prevData,
selectedProfile: profile
}));
setConfirmPopupMessage(`Vous êtes sur le point de rattacher l'enseignant ${newTeacher?.first_name} ${newTeacher?.last_name} au profil ${profile.email} ID = ${profile.id}.`);
setConfirmPopupOnConfirm(() => () => {
setConfirmPopupVisible(false);
});
setConfirmPopupVisible(true);
};
const handleCancelConfirmation = () => {
setConfirmPopupVisible(false);
};
// Récupération des messages d'erreur
const getError = (field) => {
return localErrors?.[field]?.[0];
};
const handleAddTeacher = () => {
setNewTeacher({ id: Date.now(), last_name: '', first_name: '', email: '', specialities: [], droit: 0 });
setFormData({ last_name: '', first_name: '', email: '', specialities: [], droit: 0 });
setNewTeacher({ id: Date.now(), last_name: '', first_name: '', selectedProfile: null, specialities: [], droit: 0 });
setFormData({ last_name: '', first_name: '', selectedProfile: null, specialities: [], droit: 0});
};
const handleRemoveTeacher = (id) => {
@ -124,21 +152,20 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
};
const handleSaveNewTeacher = () => {
if (formData.last_name && formData.first_name && formData.email) {
if (formData.last_name && formData.first_name && formData.selectedProfile) {
const data = {
email: formData.email,
password: 'Provisoire01!',
username: formData.email,
is_active: 1,
droit: formData.droit,
last_name: formData.last_name,
first_name: formData.first_name,
profile_role_data: {
role_type: formData.droit,
establishment: selectedEstablishmentId,
is_active: true,
profile: formData.selectedProfile.id
},
specialities: formData.specialities
};
createProfile(data, csrfToken)
.then(response => {
logger.debug('Success:', response);
if (response.id) {
let idProfil = response.id;
newTeacher.associated_profile = idProfil;
handleCreate(newTeacher)
handleCreate(data)
.then((createdTeacher) => {
setTeachers([createdTeacher, ...teachers]);
setNewTeacher(null);
@ -151,16 +178,6 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
setLocalErrors(error.details);
}
});
}
setLocalErrors({});
})
.catch((error) => {
logger.error('Error:', error.message);
if (error.details) {
logger.error('Form errors:', error.details);
setLocalErrors(error.details);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis et valides");
setPopupVisible(true);
@ -260,7 +277,6 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
<div className="flex justify-center space-x-2">
<InputText
name="last_name"
label="nom"
value={currentData.last_name}
onChange={handleChange}
placeholder="Nom de l'enseignant"
@ -268,7 +284,6 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
/>
<InputText
name="first_name"
label="prénom"
value={currentData.first_name}
onChange={handleChange}
placeholder="Prénom de l'enseignant"
@ -278,13 +293,23 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
);
case 'EMAIL':
return (
<InputText
name="email"
value={currentData.email}
onChange={handleChange}
placeholder="Email de l'enseignant"
errorMsg={getError('email')}
/>
<div className="flex items-center space-x-2">
{currentData.selectedProfile ? (
<span className="text-gray-900">
{currentData.selectedProfile.email}
</span>
) : (
<span className="text-gray-500 italic">
Rechercher un profil existant
</span>
)}
<button
type="button"
onClick={() => setDirectoryPopupVisible(true)}
>
<Search className="w-5 h-5 text-emerald-500 hover:text-emerald-700"/>
</button>
</div>
);
case 'SPECIALITES':
return (
@ -339,9 +364,9 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
</div>
);
case 'ADMINISTRATEUR':
if (teacher.associated_profile) {
const badgeClass = teacher.droit === 1 ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600';
const label = teacher.droit === 1 ? 'OUI' : 'NON';
if (teacher.associated_profile_email) {
const badgeClass = teacher.role_type.role_type === 1 ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600';
const label = teacher.role_type.role_type === 1 ? 'OUI' : 'NON';
return (
<div key={teacher.id} className="flex justify-center items-center space-x-2">
<span className={`px-3 py-1 rounded-full font-bold ${badgeClass}`}>

View File

@ -6,6 +6,7 @@ const localePrefixes = {
export function formatPhoneNumber(phoneString, fromFormat = 'XX-XX-XX-XX-XX', toFormat = 'LX-XX-XX-XX-XX', locale = "fr-FR") {
if (!phoneString) return;
// Extraire les chiffres du numéro de téléphone
const digits = phoneString.replace(/\D/g, '');

View File

@ -18,6 +18,7 @@ export const BE_AUTH_LOGIN_URL = `${BASE_URL}/Auth/login`
export const BE_AUTH_REFRESH_JWT_URL = `${BASE_URL}/Auth/refreshJWT`
export const BE_AUTH_LOGOUT_URL = `${BASE_URL}/Auth/logout`
export const BE_AUTH_PROFILES_URL = `${BASE_URL}/Auth/profiles`
export const BE_AUTH_PROFILES_ROLES_URL = `${BASE_URL}/Auth/profileRoles`
export const BE_AUTH_CSRF_URL = `${BASE_URL}/Auth/csrf`
export const BE_AUTH_INFO_SESSION = `${BASE_URL}/Auth/infoSession`
@ -79,6 +80,9 @@ export const FE_ADMIN_CLASSES_URL = `/admin/classes`
//ADMIN/STRUCTURE URL
export const FE_ADMIN_STRUCTURE_URL = `/admin/structure`
//ADMIN/DIRECTORY URL
export const FE_ADMIN_DIRECTORY_URL = `/admin/directory`
//ADMIN/GRADES URL
export const FE_ADMIN_GRADES_URL = `/admin/grades`