mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
Compare commits
9 Commits
9dff32b388
...
05c68ebfaa
| Author | SHA1 | Date | |
|---|---|---|---|
| 05c68ebfaa | |||
| 195579e217 | |||
| ddcaba382e | |||
| a82483f3bd | |||
| 26d4b5633f | |||
| d66db1b019 | |||
| bd7dc2b0c2 | |||
| 176edc5c45 | |||
| 92c6a31740 |
@ -25,7 +25,7 @@ class ProfileRole(models.Model):
|
|||||||
profile = models.ForeignKey('Profile', on_delete=models.CASCADE, related_name='roles')
|
profile = models.ForeignKey('Profile', on_delete=models.CASCADE, related_name='roles')
|
||||||
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.Establishment', on_delete=models.CASCADE, related_name='profile_roles')
|
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='profile_roles')
|
||||||
is_active = models.BooleanField(default=False)
|
is_active = models.BooleanField(default=False, blank=True)
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from django.core.mail import send_mail, get_connection, EmailMultiAlternatives, EmailMessage
|
from django.core.mail import get_connection, EmailMultiAlternatives, EmailMessage
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -208,3 +208,21 @@ def isValid(message, fiche_inscription):
|
|||||||
mailReponsableAVerifier = responsable.mail
|
mailReponsableAVerifier = responsable.mail
|
||||||
|
|
||||||
return responsableMail == mailReponsableAVerifier and str(idMail) == str(fiche_inscription.eleve.id)
|
return responsableMail == mailReponsableAVerifier and str(idMail) == str(fiche_inscription.eleve.id)
|
||||||
|
|
||||||
|
def sendRegisterTeacher(recipients, establishment_id):
|
||||||
|
errorMessage = ''
|
||||||
|
try:
|
||||||
|
EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Bienvenue sur N3wt School (Enseignant)'
|
||||||
|
context = {
|
||||||
|
'BASE_URL': settings.BASE_URL,
|
||||||
|
'URL_DJANGO': settings.URL_DJANGO,
|
||||||
|
'email': recipients,
|
||||||
|
'establishment': establishment_id
|
||||||
|
}
|
||||||
|
connection = getConnection(establishment_id)
|
||||||
|
subject = EMAIL_INSCRIPTION_SUBJECT
|
||||||
|
html_message = render_to_string('emails/inscription_teacher.html', context)
|
||||||
|
sendMail(subject=subject, message=html_message, recipients=recipients, connection=connection)
|
||||||
|
except Exception as e:
|
||||||
|
errorMessage = str(e)
|
||||||
|
return errorMessage
|
||||||
@ -60,6 +60,7 @@ class TeacherSerializer(serializers.ModelSerializer):
|
|||||||
profile_role = serializers.PrimaryKeyRelatedField(queryset=ProfileRole.objects.all(), required=False)
|
profile_role = serializers.PrimaryKeyRelatedField(queryset=ProfileRole.objects.all(), required=False)
|
||||||
profile_role_data = ProfileRoleSerializer(write_only=True, required=False)
|
profile_role_data = ProfileRoleSerializer(write_only=True, required=False)
|
||||||
associated_profile_email = serializers.SerializerMethodField()
|
associated_profile_email = serializers.SerializerMethodField()
|
||||||
|
profile = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Teacher
|
model = Teacher
|
||||||
@ -155,6 +156,12 @@ class TeacherSerializer(serializers.ModelSerializer):
|
|||||||
return obj.profile_role.role_type
|
return obj.profile_role.role_type
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_profile(self, obj):
|
||||||
|
# Retourne l'id du profile associé via profile_role
|
||||||
|
if obj.profile_role and obj.profile_role.profile:
|
||||||
|
return obj.profile_role.profile.id
|
||||||
|
return None
|
||||||
|
|
||||||
class PlanningSerializer(serializers.ModelSerializer):
|
class PlanningSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Planning
|
model = Planning
|
||||||
|
|||||||
@ -35,6 +35,7 @@ from collections import defaultdict
|
|||||||
from Subscriptions.models import Student, StudentCompetency
|
from Subscriptions.models import Student, StudentCompetency
|
||||||
from Subscriptions.util import getCurrentSchoolYear
|
from Subscriptions.util import getCurrentSchoolYear
|
||||||
import logging
|
import logging
|
||||||
|
from N3wtSchool.mailManager import sendRegisterForm, sendRegisterTeacher
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -102,8 +103,17 @@ class TeacherListCreateView(APIView):
|
|||||||
teacher_serializer = TeacherSerializer(data=teacher_data)
|
teacher_serializer = TeacherSerializer(data=teacher_data)
|
||||||
|
|
||||||
if teacher_serializer.is_valid():
|
if teacher_serializer.is_valid():
|
||||||
teacher_serializer.save()
|
teacher_instance = teacher_serializer.save()
|
||||||
|
# Envoi du mail d'inscription enseignant uniquement à la création
|
||||||
|
email = None
|
||||||
|
establishment_id = None
|
||||||
|
if hasattr(teacher_instance, "profile_role") and teacher_instance.profile_role:
|
||||||
|
if hasattr(teacher_instance.profile_role, "profile") and teacher_instance.profile_role.profile:
|
||||||
|
email = teacher_instance.profile_role.profile.email
|
||||||
|
if hasattr(teacher_instance.profile_role, "establishment") and teacher_instance.profile_role.establishment:
|
||||||
|
establishment_id = teacher_instance.profile_role.establishment.id
|
||||||
|
if email and establishment_id:
|
||||||
|
sendRegisterTeacher(email, establishment_id)
|
||||||
return JsonResponse(teacher_serializer.data, safe=False)
|
return JsonResponse(teacher_serializer.data, safe=False)
|
||||||
|
|
||||||
return JsonResponse(teacher_serializer.errors, safe=False)
|
return JsonResponse(teacher_serializer.errors, safe=False)
|
||||||
@ -118,17 +128,43 @@ class TeacherDetailView(APIView):
|
|||||||
return JsonResponse(teacher_serializer.data, safe=False)
|
return JsonResponse(teacher_serializer.data, safe=False)
|
||||||
|
|
||||||
def put(self, request, id):
|
def put(self, request, id):
|
||||||
teacher_data=JSONParser().parse(request)
|
teacher_data = JSONParser().parse(request)
|
||||||
teacher = getObject(_objectName=Teacher, _columnName='id', _value=id)
|
teacher = getObject(_objectName=Teacher, _columnName='id', _value=id)
|
||||||
|
|
||||||
|
# Récupérer l'ancien profile avant modification
|
||||||
|
old_profile_role = getattr(teacher, 'profile_role', None)
|
||||||
|
old_profile = getattr(old_profile_role, 'profile', None) if old_profile_role else None
|
||||||
|
|
||||||
teacher_serializer = TeacherSerializer(teacher, data=teacher_data)
|
teacher_serializer = TeacherSerializer(teacher, data=teacher_data)
|
||||||
if teacher_serializer.is_valid():
|
if teacher_serializer.is_valid():
|
||||||
teacher_serializer.save()
|
teacher_serializer.save()
|
||||||
|
|
||||||
|
# Après modification, vérifier si l'ancien profile n'a plus de ProfileRole
|
||||||
|
if old_profile:
|
||||||
|
from Auth.models import ProfileRole # import local pour éviter les imports circulaires
|
||||||
|
if not ProfileRole.objects.filter(profile=old_profile).exists():
|
||||||
|
old_profile.delete()
|
||||||
|
|
||||||
return JsonResponse(teacher_serializer.data, safe=False)
|
return JsonResponse(teacher_serializer.data, safe=False)
|
||||||
|
|
||||||
return JsonResponse(teacher_serializer.errors, safe=False)
|
return JsonResponse(teacher_serializer.errors, safe=False)
|
||||||
|
|
||||||
def delete(self, request, id):
|
def delete(self, request, id):
|
||||||
return delete_object(Teacher, id, related_field='profile_role')
|
# Suppression du Teacher et du ProfileRole associé
|
||||||
|
teacher = getObject(_objectName=Teacher, _columnName='id', _value=id)
|
||||||
|
profile_role = getattr(teacher, 'profile_role', None)
|
||||||
|
profile = getattr(profile_role, 'profile', None) if profile_role else None
|
||||||
|
|
||||||
|
# Supprime le Teacher (ce qui supprime le ProfileRole via on_delete=models.CASCADE)
|
||||||
|
response = delete_object(Teacher, id, related_field='profile_role')
|
||||||
|
|
||||||
|
# Si un profile était associé, vérifier s'il reste des ProfileRole
|
||||||
|
if profile:
|
||||||
|
from Auth.models import ProfileRole # import local pour éviter les imports circulaires
|
||||||
|
if not ProfileRole.objects.filter(profile=profile).exists():
|
||||||
|
profile.delete()
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
@method_decorator(csrf_protect, name='dispatch')
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||||
|
|||||||
@ -21,6 +21,7 @@ from N3wtSchool import settings
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import pytz
|
import pytz
|
||||||
import Subscriptions.util as util
|
import Subscriptions.util as util
|
||||||
|
from N3wtSchool.mailManager import sendRegisterForm
|
||||||
|
|
||||||
class AbsenceManagementSerializer(serializers.ModelSerializer):
|
class AbsenceManagementSerializer(serializers.ModelSerializer):
|
||||||
student_name = serializers.SerializerMethodField()
|
student_name = serializers.SerializerMethodField()
|
||||||
@ -215,6 +216,14 @@ class StudentSerializer(serializers.ModelSerializer):
|
|||||||
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
|
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
|
||||||
profile_role_serializer.is_valid(raise_exception=True)
|
profile_role_serializer.is_valid(raise_exception=True)
|
||||||
profile_role = profile_role_serializer.save()
|
profile_role = profile_role_serializer.save()
|
||||||
|
# Envoi du mail d'inscription si un nouveau profil vient d'être créé
|
||||||
|
email = None
|
||||||
|
if profile_data and 'email' in profile_data:
|
||||||
|
email = profile_data['email']
|
||||||
|
elif profile_role and profile_role.profile:
|
||||||
|
email = profile_role.profile.email
|
||||||
|
if email:
|
||||||
|
sendRegisterForm(email, establishment_id)
|
||||||
elif profile_role:
|
elif profile_role:
|
||||||
# Récupérer un ProfileRole existant par son ID
|
# Récupérer un ProfileRole existant par son ID
|
||||||
profile_role = ProfileRole.objects.get(id=profile_role.id)
|
profile_role = ProfileRole.objects.get(id=profile_role.id)
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
<!-- Nouveau template pour l'inscription d'un enseignant -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Bienvenue sur N3wt School</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 120px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<!-- Utilisation d'un lien absolu pour le logo -->
|
||||||
|
<img src="{{URL_DJANGO}}/static/img/logo_min.svg" alt="Logo N3wt School" class="logo" style="display:block;margin:auto;" />
|
||||||
|
<h1>Bienvenue sur N3wt School</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Bonjour,</p>
|
||||||
|
<p>Votre compte enseignant a été créé sur la plateforme N3wt School.</p>
|
||||||
|
<p>Pour accéder à votre espace personnel, veuillez vous connecter à l'adresse suivante :<br>
|
||||||
|
<a href="{{BASE_URL}}/users/login">{{BASE_URL}}/users/login</a>
|
||||||
|
</p>
|
||||||
|
<p>Votre identifiant est : <b>{{ email }}</b></p>
|
||||||
|
<p>Si c'est votre première connexion, veuillez activer votre compte ici :<br>
|
||||||
|
<a href="{{BASE_URL}}/users/subscribe?establishment_id={{establishment}}">{{BASE_URL}}/users/subscribe</a>
|
||||||
|
</p>
|
||||||
|
<p>Nous vous souhaitons une excellente prise en main de l'outil.<br>
|
||||||
|
L'équipe N3wt School reste à votre disposition pour toute question.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Ce message est généré automatiquement, merci de ne pas y répondre.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -323,6 +323,27 @@ class RegisterFormWithIdView(APIView):
|
|||||||
registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf)
|
registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf)
|
||||||
registerForm.save()
|
registerForm.save()
|
||||||
|
|
||||||
|
# Envoi du mail d'inscription au second guardian si besoin
|
||||||
|
guardians = registerForm.student.guardians.all()
|
||||||
|
from Auth.models import Profile
|
||||||
|
from N3wtSchool.mailManager import sendRegisterForm
|
||||||
|
|
||||||
|
for guardian in guardians:
|
||||||
|
# Recherche de l'email dans le profil lié au guardian (si existant)
|
||||||
|
email = None
|
||||||
|
if hasattr(guardian, "profile_role") and guardian.profile_role and hasattr(guardian.profile_role, "profile") and guardian.profile_role.profile:
|
||||||
|
email = guardian.profile_role.profile.email
|
||||||
|
# Fallback sur le champ email direct (si jamais il existe)
|
||||||
|
if not email:
|
||||||
|
email = getattr(guardian, "email", None)
|
||||||
|
logger.debug(f"[RF_UNDER_REVIEW] Guardian id={guardian.id}, email={email}")
|
||||||
|
if email:
|
||||||
|
profile_exists = Profile.objects.filter(email=email).exists()
|
||||||
|
logger.debug(f"[RF_UNDER_REVIEW] Profile existe pour {email} ? {profile_exists}")
|
||||||
|
if not profile_exists:
|
||||||
|
logger.debug(f"[RF_UNDER_REVIEW] Envoi du mail d'inscription à {email} pour l'établissement {registerForm.establishment.pk}")
|
||||||
|
sendRegisterForm(email, registerForm.establishment.pk)
|
||||||
|
|
||||||
# Mise à jour de l'automate
|
# Mise à jour de l'automate
|
||||||
# Vérification de la présence du fichier SEPA
|
# Vérification de la présence du fichier SEPA
|
||||||
if registerForm.sepa_file:
|
if registerForm.sepa_file:
|
||||||
@ -332,6 +353,9 @@ class RegisterFormWithIdView(APIView):
|
|||||||
# Mise à jour de l'automate pour une signature classique
|
# Mise à jour de l'automate pour une signature classique
|
||||||
updateStateMachine(registerForm, 'EVENT_SIGNATURE')
|
updateStateMachine(registerForm, 'EVENT_SIGNATURE')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"[RF_UNDER_REVIEW] Exception: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
elif _status == RegistrationForm.RegistrationFormStatus.RF_SENT:
|
elif _status == RegistrationForm.RegistrationFormStatus.RF_SENT:
|
||||||
if registerForm.status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
|
if registerForm.status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import {
|
|||||||
Award,
|
Award,
|
||||||
Calendar,
|
Calendar,
|
||||||
Settings,
|
Settings,
|
||||||
LogOut,
|
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
@ -29,16 +28,14 @@ import {
|
|||||||
|
|
||||||
import { disconnect } from '@/app/actions/authAction';
|
import { disconnect } from '@/app/actions/authAction';
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||||
import { getGravatarUrl } from '@/utils/gravatar';
|
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
import { getRightStr, RIGHTS } from '@/utils/rights';
|
import { RIGHTS } from '@/utils/rights';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import ProfileSelector from '@/components/ProfileSelector';
|
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
const t = useTranslations('sidebar');
|
const t = useTranslations('sidebar');
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const { profileRole, establishments, user, clearContext } =
|
const { profileRole, establishments, clearContext } =
|
||||||
useEstablishment();
|
useEstablishment();
|
||||||
|
|
||||||
const sidebarItems = {
|
const sidebarItems = {
|
||||||
@ -97,45 +94,15 @@ export default function Layout({ children }) {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const currentPage = pathname.split('/').pop();
|
const currentPage = pathname.split('/').pop();
|
||||||
|
|
||||||
const headerTitle = sidebarItems[currentPage]?.name || t('dashboard');
|
|
||||||
|
|
||||||
const softwareName = 'N3WT School';
|
const softwareName = 'N3WT School';
|
||||||
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
|
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
|
||||||
setIsPopupVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDisconnect = () => {
|
const confirmDisconnect = () => {
|
||||||
setIsPopupVisible(false);
|
setIsPopupVisible(false);
|
||||||
disconnect();
|
disconnect();
|
||||||
clearContext();
|
clearContext();
|
||||||
};
|
};
|
||||||
|
|
||||||
const dropdownItems = [
|
|
||||||
{
|
|
||||||
type: 'info',
|
|
||||||
content: (
|
|
||||||
<div className="px-4 py-2">
|
|
||||||
<div className="font-medium">{user?.email || 'Utilisateur'}</div>
|
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
{getRightStr(profileRole) || ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator',
|
|
||||||
content: <hr className="my-2 border-gray-200" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
label: 'Déconnexion',
|
|
||||||
onClick: handleDisconnect,
|
|
||||||
icon: LogOut,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
setIsSidebarOpen(!isSidebarOpen);
|
setIsSidebarOpen(!isSidebarOpen);
|
||||||
};
|
};
|
||||||
@ -145,6 +112,15 @@ export default function Layout({ children }) {
|
|||||||
setIsSidebarOpen(false);
|
setIsSidebarOpen(false);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
// Filtrage dynamique des items de la sidebar selon le rôle
|
||||||
|
let sidebarItemsToDisplay = Object.values(sidebarItems);
|
||||||
|
if (profileRole === 0) {
|
||||||
|
// Si pas admin, on retire "directory" et "settings"
|
||||||
|
sidebarItemsToDisplay = sidebarItemsToDisplay.filter(
|
||||||
|
(item) => item.id !== 'directory' && item.id !== 'settings'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute requiredRight={[RIGHTS.ADMIN, RIGHTS.TEACHER]}>
|
<ProtectedRoute requiredRight={[RIGHTS.ADMIN, RIGHTS.TEACHER]}>
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
@ -156,7 +132,7 @@ export default function Layout({ children }) {
|
|||||||
<Sidebar
|
<Sidebar
|
||||||
establishments={establishments}
|
establishments={establishments}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
items={Object.values(sidebarItems)}
|
items={sidebarItemsToDisplay}
|
||||||
onCloseMobile={toggleSidebar}
|
onCloseMobile={toggleSidebar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export default function Page() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEstablishmentId) {
|
if (selectedEstablishmentId) {
|
||||||
@ -316,35 +316,39 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
...(profileRole !== 0
|
||||||
id: 'Fees',
|
? [
|
||||||
label: 'Tarifs',
|
{
|
||||||
content: (
|
id: 'Fees',
|
||||||
<div className="h-full overflow-y-auto p-4">
|
label: 'Tarifs',
|
||||||
<FeesManagement
|
content: (
|
||||||
registrationDiscounts={registrationDiscounts}
|
<div className="h-full overflow-y-auto p-4">
|
||||||
setRegistrationDiscounts={setRegistrationDiscounts}
|
<FeesManagement
|
||||||
tuitionDiscounts={tuitionDiscounts}
|
registrationDiscounts={registrationDiscounts}
|
||||||
setTuitionDiscounts={setTuitionDiscounts}
|
setRegistrationDiscounts={setRegistrationDiscounts}
|
||||||
registrationFees={registrationFees}
|
tuitionDiscounts={tuitionDiscounts}
|
||||||
setRegistrationFees={setRegistrationFees}
|
setTuitionDiscounts={setTuitionDiscounts}
|
||||||
tuitionFees={tuitionFees}
|
registrationFees={registrationFees}
|
||||||
setTuitionFees={setTuitionFees}
|
setRegistrationFees={setRegistrationFees}
|
||||||
registrationPaymentPlans={registrationPaymentPlans}
|
tuitionFees={tuitionFees}
|
||||||
setRegistrationPaymentPlans={setRegistrationPaymentPlans}
|
setTuitionFees={setTuitionFees}
|
||||||
tuitionPaymentPlans={tuitionPaymentPlans}
|
registrationPaymentPlans={registrationPaymentPlans}
|
||||||
setTuitionPaymentPlans={setTuitionPaymentPlans}
|
setRegistrationPaymentPlans={setRegistrationPaymentPlans}
|
||||||
registrationPaymentModes={registrationPaymentModes}
|
tuitionPaymentPlans={tuitionPaymentPlans}
|
||||||
setRegistrationPaymentModes={setRegistrationPaymentModes}
|
setTuitionPaymentPlans={setTuitionPaymentPlans}
|
||||||
tuitionPaymentModes={tuitionPaymentModes}
|
registrationPaymentModes={registrationPaymentModes}
|
||||||
setTuitionPaymentModes={setTuitionPaymentModes}
|
setRegistrationPaymentModes={setRegistrationPaymentModes}
|
||||||
handleCreate={handleCreate}
|
tuitionPaymentModes={tuitionPaymentModes}
|
||||||
handleEdit={handleEdit}
|
setTuitionPaymentModes={setTuitionPaymentModes}
|
||||||
handleDelete={handleDelete}
|
handleCreate={handleCreate}
|
||||||
/>
|
handleEdit={handleEdit}
|
||||||
</div>
|
handleDelete={handleDelete}
|
||||||
),
|
/>
|
||||||
},
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
id: 'Files',
|
id: 'Files',
|
||||||
label: 'Documents',
|
label: 'Documents',
|
||||||
@ -353,6 +357,7 @@ export default function Page() {
|
|||||||
<FilesGroupsManagement
|
<FilesGroupsManagement
|
||||||
csrfToken={csrfToken}
|
csrfToken={csrfToken}
|
||||||
selectedEstablishmentId={selectedEstablishmentId}
|
selectedEstablishmentId={selectedEstablishmentId}
|
||||||
|
profileRole={profileRole}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -83,12 +83,9 @@ export default function Page({ params: { locale } }) {
|
|||||||
const [totalHistorical, setTotalHistorical] = useState(0);
|
const [totalHistorical, setTotalHistorical] = useState(0);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10); // Définir le nombre d'éléments par page
|
const [itemsPerPage, setItemsPerPage] = useState(10); // Définir le nombre d'éléments par page
|
||||||
|
|
||||||
const [student, setStudent] = useState('');
|
|
||||||
const [classes, setClasses] = useState([]);
|
const [classes, setClasses] = useState([]);
|
||||||
const [reloadFetch, setReloadFetch] = useState(false);
|
const [reloadFetch, setReloadFetch] = useState(false);
|
||||||
|
|
||||||
const [isOpenAddGuardian, setIsOpenAddGuardian] = useState(false);
|
|
||||||
|
|
||||||
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
||||||
const [selectedRegisterForm, setSelectedRegisterForm] = useState([]);
|
const [selectedRegisterForm, setSelectedRegisterForm] = useState([]);
|
||||||
|
|
||||||
@ -101,7 +98,7 @@ export default function Page({ params: { locale } }) {
|
|||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
|
||||||
const openSepaUploadModal = (row) => {
|
const openSepaUploadModal = (row) => {
|
||||||
@ -801,15 +798,17 @@ export default function Page({ params: { locale } }) {
|
|||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
{profileRole !== 0 && (
|
||||||
onClick={() => {
|
<button
|
||||||
const url = `${FE_ADMIN_SUBSCRIPTIONS_CREATE_URL}`;
|
onClick={() => {
|
||||||
router.push(url);
|
const url = `${FE_ADMIN_SUBSCRIPTIONS_CREATE_URL}`;
|
||||||
}}
|
router.push(url);
|
||||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200 ml-4"
|
}}
|
||||||
>
|
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200 ml-4"
|
||||||
<Plus className="w-5 h-5" />
|
>
|
||||||
</button>
|
<Plus className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export default function InscriptionFormShared({
|
|||||||
const [parentFileTemplates, setParentFileTemplates] = useState([]);
|
const [parentFileTemplates, setParentFileTemplates] = useState([]);
|
||||||
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
||||||
const [formResponses, setFormResponses] = useState({});
|
const [formResponses, setFormResponses] = useState({});
|
||||||
const [currentPage, setCurrentPage] = useState(5);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
const [isPage1Valid, setIsPage1Valid] = useState(false);
|
const [isPage1Valid, setIsPage1Valid] = useState(false);
|
||||||
const [isPage2Valid, setIsPage2Valid] = useState(false);
|
const [isPage2Valid, setIsPage2Valid] = useState(false);
|
||||||
|
|||||||
@ -100,7 +100,7 @@ export default function ResponsableInputFields({
|
|||||||
profile_role_data: {
|
profile_role_data: {
|
||||||
establishment: selectedEstablishmentId,
|
establishment: selectedEstablishmentId,
|
||||||
role_type: 2,
|
role_type: 2,
|
||||||
is_active: true,
|
is_active: false,
|
||||||
profile_data: {
|
profile_data: {
|
||||||
email: '',
|
email: '',
|
||||||
password: 'Provisoire01!',
|
password: 'Provisoire01!',
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import CheckBox from '@/components/Form/CheckBox';
|
|||||||
import Button from '@/components/Form/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import {
|
import {
|
||||||
fetchEstablishmentCompetencies,
|
|
||||||
createEstablishmentCompetencies,
|
createEstablishmentCompetencies,
|
||||||
deleteEstablishmentCompetencies,
|
deleteEstablishmentCompetencies,
|
||||||
} from '@/app/actions/schoolAction';
|
} from '@/app/actions/schoolAction';
|
||||||
@ -44,7 +43,7 @@ export default function CompetenciesList({
|
|||||||
3: false,
|
3: false,
|
||||||
4: false,
|
4: false,
|
||||||
});
|
});
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
|
||||||
@ -280,17 +279,19 @@ export default function CompetenciesList({
|
|||||||
</div>
|
</div>
|
||||||
{/* Bouton submit centré en bas */}
|
{/* Bouton submit centré en bas */}
|
||||||
<div className="flex justify-center mb-2 mt-6">
|
<div className="flex justify-center mb-2 mt-6">
|
||||||
<Button
|
{profileRole !== 0 && (
|
||||||
text="Sauvegarder"
|
<Button
|
||||||
className={`px-6 py-2 rounded-md shadow ${
|
text="Sauvegarder"
|
||||||
!hasSelection
|
className={`px-6 py-2 rounded-md shadow ${
|
||||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
!hasSelection
|
||||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||||
}`}
|
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||||
onClick={handleSubmit}
|
}`}
|
||||||
primary
|
onClick={handleSubmit}
|
||||||
disabled={!hasSelection}
|
primary
|
||||||
/>
|
disabled={!hasSelection}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Légende en dessous du bouton, alignée à gauche */}
|
{/* Légende en dessous du bouton, alignée à gauche */}
|
||||||
<div className="flex flex-row items-center gap-4 mb-4">
|
<div className="flex flex-row items-center gap-4 mb-4">
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import React, {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { CheckCircle, Circle } from 'lucide-react';
|
import { CheckCircle, Circle } from 'lucide-react';
|
||||||
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
|
|
||||||
const TreeView = forwardRef(function TreeView(
|
const TreeView = forwardRef(function TreeView(
|
||||||
{ data, expandAll, onSelectionChange },
|
{ data, expandAll, onSelectionChange },
|
||||||
@ -72,6 +73,8 @@ const TreeView = forwardRef(function TreeView(
|
|||||||
clearSelection: () => setSelectedCompetencies({}),
|
clearSelection: () => setSelectedCompetencies({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const { profileRole } = useEstablishment();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{data.map((domaine) => (
|
{data.map((domaine) => (
|
||||||
@ -112,12 +115,18 @@ const TreeView = forwardRef(function TreeView(
|
|||||||
? 'text-emerald-600 font-semibold cursor-pointer'
|
? 'text-emerald-600 font-semibold cursor-pointer'
|
||||||
: 'text-gray-500 cursor-pointer hover:text-emerald-600'
|
: 'text-gray-500 cursor-pointer hover:text-emerald-600'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleCompetenceClick(competence)}
|
onClick={
|
||||||
|
profileRole !== 0
|
||||||
|
? () => handleCompetenceClick(competence)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
cursor:
|
cursor:
|
||||||
competence.state === 'required'
|
competence.state === 'required'
|
||||||
? 'default'
|
? 'default'
|
||||||
: 'pointer',
|
: profileRole !== 0
|
||||||
|
? 'pointer'
|
||||||
|
: 'default',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -130,9 +130,7 @@ const ClassesSection = ({
|
|||||||
const [removePopupVisible, setRemovePopupVisible] = useState(false);
|
const [removePopupVisible, setRemovePopupVisible] = useState(false);
|
||||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||||
const [detailsModalVisible, setDetailsModalVisible] = useState(false);
|
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
||||||
const [selectedClass, setSelectedClass] = useState(null);
|
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
|
||||||
const { addSchedule, reloadPlanning, reloadEvents } = usePlanning();
|
const { addSchedule, reloadPlanning, reloadEvents } = usePlanning();
|
||||||
const { getNiveauxLabels, allNiveaux } = useClasses();
|
const { getNiveauxLabels, allNiveaux } = useClasses();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -449,6 +447,25 @@ const ClassesSection = ({
|
|||||||
case 'MISE A JOUR':
|
case 'MISE A JOUR':
|
||||||
return classe.updated_date_formatted;
|
return classe.updated_date_formatted;
|
||||||
case 'ACTIONS':
|
case 'ACTIONS':
|
||||||
|
// Affichage des actions en mode affichage (hors édition/création)
|
||||||
|
if (profileRole === 0) {
|
||||||
|
// Si professeur, uniquement le bouton ZoomIn
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const url = `${FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL}?schoolClassId=${classe.id}`;
|
||||||
|
router.push(`${url}`);
|
||||||
|
}}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<ZoomIn className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Sinon, toutes les actions (admin)
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
<button
|
<button
|
||||||
@ -534,7 +551,7 @@ const ClassesSection = ({
|
|||||||
icon={Users}
|
icon={Users}
|
||||||
title="Liste des classes"
|
title="Liste des classes"
|
||||||
description="Gérez les classes de votre école"
|
description="Gérez les classes de votre école"
|
||||||
button={true}
|
button={profileRole !== 0}
|
||||||
onClick={handleAddClass}
|
onClick={handleAddClass}
|
||||||
/>
|
/>
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const SpecialitiesSection = ({
|
|||||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||||
|
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
||||||
|
|
||||||
// Récupération des messages d'erreur
|
// Récupération des messages d'erreur
|
||||||
const getError = (field) => {
|
const getError = (field) => {
|
||||||
@ -239,7 +239,7 @@ const SpecialitiesSection = ({
|
|||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'LIBELLE', label: 'Libellé' },
|
{ name: 'LIBELLE', label: 'Libellé' },
|
||||||
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||||
{ name: 'ACTIONS', label: 'Actions' },
|
...(profileRole !== 0 ? [{ name: 'ACTIONS', label: 'Actions' }] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -249,7 +249,7 @@ const SpecialitiesSection = ({
|
|||||||
icon={BookOpen}
|
icon={BookOpen}
|
||||||
title="Liste des spécialités"
|
title="Liste des spécialités"
|
||||||
description="Gérez les spécialités de votre école"
|
description="Gérez les spécialités de votre école"
|
||||||
button={true}
|
button={profileRole !== 0}
|
||||||
onClick={handleAddSpeciality}
|
onClick={handleAddSpeciality}
|
||||||
/>
|
/>
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import { Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react';
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { DndProvider, useDrop } from 'react-dnd';
|
||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import InputText from '@/components/Form/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||||
@ -128,7 +127,6 @@ const TeachersSection = ({
|
|||||||
handleEdit,
|
handleEdit,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
}) => {
|
}) => {
|
||||||
const csrfToken = useCsrfToken();
|
|
||||||
const [editingTeacher, setEditingTeacher] = useState(null);
|
const [editingTeacher, setEditingTeacher] = useState(null);
|
||||||
const [newTeacher, setNewTeacher] = useState(null);
|
const [newTeacher, setNewTeacher] = useState(null);
|
||||||
const [formData, setFormData] = useState({});
|
const [formData, setFormData] = useState({});
|
||||||
@ -140,40 +138,46 @@ const TeachersSection = ({
|
|||||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||||
|
|
||||||
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
|
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
||||||
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
|
|
||||||
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
|
|
||||||
|
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
// --- UTILS ---
|
||||||
|
|
||||||
|
// Retourne le profil existant pour un email
|
||||||
|
const getUsedProfileForEmail = (email) => {
|
||||||
|
// On cherche tous les profils dont l'email correspond
|
||||||
|
const matchingProfiles = profiles.filter(p => p.email === email);
|
||||||
|
|
||||||
|
// On retourne le premier profil correspondant (ou undefined)
|
||||||
|
const result = matchingProfiles.length > 0 ? matchingProfiles[0] : undefined;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Met à jour le formData et newTeacher si besoin
|
||||||
|
const updateFormData = (data) => {
|
||||||
|
setFormData(prev => ({ ...prev, ...data }));
|
||||||
|
if (newTeacher) setNewTeacher(prev => ({ ...prev, ...data }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Récupération des messages d'erreur pour un champ donné
|
||||||
|
const getError = (field) => {
|
||||||
|
return localErrors?.[field]?.[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- HANDLERS ---
|
||||||
|
|
||||||
const handleEmailChange = (e) => {
|
const handleEmailChange = (e) => {
|
||||||
const email = e.target.value;
|
const email = e.target.value;
|
||||||
|
const existingProfile = getUsedProfileForEmail(email);
|
||||||
|
|
||||||
// Vérifier si l'email correspond à un profil existant
|
if (existingProfile) {
|
||||||
const existingProfile = profiles.find((profile) => profile.email === email);
|
logger.info(`Adresse email déjà utilisée pour le profil ${existingProfile.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
setFormData((prevData) => ({
|
updateFormData({
|
||||||
...prevData,
|
|
||||||
associated_profile_email: email,
|
associated_profile_email: email,
|
||||||
existingProfileId: existingProfile ? existingProfile.id : null,
|
existingProfileId: existingProfile ? existingProfile.id : null,
|
||||||
}));
|
});
|
||||||
|
|
||||||
if (newTeacher) {
|
|
||||||
setNewTeacher((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
associated_profile_email: email,
|
|
||||||
existingProfileId: existingProfile ? existingProfile.id : null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelConfirmation = () => {
|
|
||||||
setConfirmPopupVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Récupération des messages d'erreur
|
|
||||||
const getError = (field) => {
|
|
||||||
return localErrors?.[field]?.[0];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddTeacher = () => {
|
const handleAddTeacher = () => {
|
||||||
@ -195,15 +199,15 @@ const TeachersSection = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveTeacher = (id) => {
|
const handleRemoveTeacher = (id) => {
|
||||||
|
logger.debug('[DELETE] Suppression teacher id:', id);
|
||||||
return handleDelete(id)
|
return handleDelete(id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setTeachers((prevTeachers) =>
|
setTeachers(prevTeachers =>
|
||||||
prevTeachers.filter((teacher) => teacher.id !== id)
|
prevTeachers.filter(teacher => teacher.id !== id)
|
||||||
);
|
);
|
||||||
|
logger.debug('[DELETE] Teacher supprimé:', id);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(logger.error);
|
||||||
logger.error(error);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveNewTeacher = () => {
|
const handleSaveNewTeacher = () => {
|
||||||
@ -234,16 +238,29 @@ const TeachersSection = ({
|
|||||||
|
|
||||||
handleCreate(data)
|
handleCreate(data)
|
||||||
.then((createdTeacher) => {
|
.then((createdTeacher) => {
|
||||||
|
// Recherche du profile associé dans profiles
|
||||||
|
let newProfileId = undefined;
|
||||||
|
let foundProfile = undefined;
|
||||||
|
if (
|
||||||
|
createdTeacher &&
|
||||||
|
createdTeacher.profile_role &&
|
||||||
|
createdTeacher.profile
|
||||||
|
) {
|
||||||
|
newProfileId = createdTeacher.profile;
|
||||||
|
foundProfile = profiles.find(p => p.id === newProfileId);
|
||||||
|
}
|
||||||
|
|
||||||
setTeachers([createdTeacher, ...teachers]);
|
setTeachers([createdTeacher, ...teachers]);
|
||||||
setNewTeacher(null);
|
setNewTeacher(null);
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
existingProfileId: newProfileId,
|
||||||
|
}));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('Error:', error.message);
|
logger.error('Error:', error.message);
|
||||||
if (error.details) {
|
if (error.details) setLocalErrors(error.details);
|
||||||
logger.error('Form errors:', error.details);
|
|
||||||
setLocalErrors(error.details);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||||
@ -252,51 +269,24 @@ const TeachersSection = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateTeacher = (id, updatedData) => {
|
const handleUpdateTeacher = (id, updatedData) => {
|
||||||
// Récupérer l'enseignant actuel à partir de la liste des enseignants
|
|
||||||
const currentTeacher = teachers.find((teacher) => teacher.id === id);
|
|
||||||
|
|
||||||
// Vérifier si l'email correspond à un profil existant
|
|
||||||
const existingProfile = profiles.find(
|
|
||||||
(profile) => profile.email === currentTeacher.associated_profile_email
|
|
||||||
);
|
|
||||||
|
|
||||||
// Vérifier si l'email a été modifié
|
|
||||||
const isEmailModified = currentTeacher
|
|
||||||
? currentTeacher.associated_profile_email !==
|
|
||||||
updatedData.associated_profile_email
|
|
||||||
: true;
|
|
||||||
|
|
||||||
// Mettre à jour existingProfileId en fonction de l'email
|
|
||||||
updatedData.existingProfileId = existingProfile ? existingProfile.id : null;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
updatedData.last_name &&
|
updatedData.last_name &&
|
||||||
updatedData.first_name &&
|
updatedData.first_name &&
|
||||||
updatedData.associated_profile_email
|
updatedData.associated_profile_email
|
||||||
) {
|
) {
|
||||||
const data = {
|
const profileRoleData = {
|
||||||
last_name: updatedData.last_name,
|
id: updatedData.profile_role,
|
||||||
first_name: updatedData.first_name,
|
establishment: selectedEstablishmentId,
|
||||||
profile_role_data: {
|
role_type: updatedData.role_type || 0,
|
||||||
id: updatedData.profile_role,
|
profile: updatedData.existingProfileId,
|
||||||
establishment: selectedEstablishmentId,
|
|
||||||
role_type: updatedData.role_type || 0,
|
|
||||||
is_active: true,
|
|
||||||
...(isEmailModified
|
|
||||||
? {
|
|
||||||
profile_data: {
|
|
||||||
id: updatedData.existingProfileId,
|
|
||||||
email: updatedData.associated_profile_email,
|
|
||||||
username: updatedData.associated_profile_email,
|
|
||||||
password: 'Provisoire01!',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: { profile: updatedData.existingProfileId }),
|
|
||||||
},
|
|
||||||
specialities: updatedData.specialities || [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleEdit(id, data)
|
handleEdit(id, {
|
||||||
|
last_name: updatedData.last_name,
|
||||||
|
first_name: updatedData.first_name,
|
||||||
|
profile_role_data: profileRoleData,
|
||||||
|
specialities: updatedData.specialities || [],
|
||||||
|
})
|
||||||
.then((updatedTeacher) => {
|
.then((updatedTeacher) => {
|
||||||
setTeachers((prevTeachers) =>
|
setTeachers((prevTeachers) =>
|
||||||
prevTeachers.map((teacher) =>
|
prevTeachers.map((teacher) =>
|
||||||
@ -308,10 +298,7 @@ const TeachersSection = ({
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('Error:', error.message);
|
logger.error('Error:', error.message);
|
||||||
if (error.details) {
|
if (error.details) setLocalErrors(error.details);
|
||||||
logger.error('Form errors:', error.details);
|
|
||||||
setLocalErrors(error.details);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||||
@ -321,45 +308,12 @@ const TeachersSection = ({
|
|||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value, type, checked } = e.target;
|
const { name, value, type, checked } = e.target;
|
||||||
let parsedValue = value;
|
let parsedValue = type === 'checkbox' ? (checked ? 1 : 0) : value;
|
||||||
|
updateFormData({ [name]: parsedValue });
|
||||||
if (type === 'checkbox') {
|
|
||||||
parsedValue = checked ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editingTeacher) {
|
|
||||||
setFormData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
[name]: parsedValue,
|
|
||||||
}));
|
|
||||||
} else if (newTeacher) {
|
|
||||||
setNewTeacher((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
[name]: parsedValue,
|
|
||||||
}));
|
|
||||||
setFormData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
[name]: parsedValue,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSpecialitiesChange = (selectedSpecialities) => {
|
const handleSpecialitiesChange = (selectedSpecialities) => {
|
||||||
if (editingTeacher) {
|
updateFormData({ specialities: selectedSpecialities });
|
||||||
setFormData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
specialities: selectedSpecialities,
|
|
||||||
}));
|
|
||||||
} else if (newTeacher) {
|
|
||||||
setNewTeacher((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
specialities: selectedSpecialities,
|
|
||||||
}));
|
|
||||||
setFormData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
specialities: selectedSpecialities,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditTeacher = (teacher) => {
|
const handleEditTeacher = (teacher) => {
|
||||||
@ -406,6 +360,7 @@ const TeachersSection = ({
|
|||||||
onChange={handleEmailChange}
|
onChange={handleEmailChange}
|
||||||
placeholder="Adresse email de l'enseignant"
|
placeholder="Adresse email de l'enseignant"
|
||||||
errorMsg={getError('email')}
|
errorMsg={getError('email')}
|
||||||
|
enable={!isEditing}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'SPECIALITES':
|
case 'SPECIALITES':
|
||||||
@ -563,7 +518,7 @@ const TeachersSection = ({
|
|||||||
{ name: 'SPECIALITES', label: 'Spécialités' },
|
{ name: 'SPECIALITES', label: 'Spécialités' },
|
||||||
{ name: 'ADMINISTRATEUR', label: 'Profil' },
|
{ name: 'ADMINISTRATEUR', label: 'Profil' },
|
||||||
{ name: 'MISE A JOUR', label: 'Mise à jour' },
|
{ name: 'MISE A JOUR', label: 'Mise à jour' },
|
||||||
{ name: 'ACTIONS', label: 'Actions' },
|
...(profileRole !== 0 ? [{ name: 'ACTIONS', label: 'Actions' }] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -573,7 +528,7 @@ const TeachersSection = ({
|
|||||||
icon={GraduationCap}
|
icon={GraduationCap}
|
||||||
title="Liste des enseignants.es"
|
title="Liste des enseignants.es"
|
||||||
description="Gérez les enseignants.es de votre école"
|
description="Gérez les enseignants.es de votre école"
|
||||||
button={true}
|
button={profileRole !== 0}
|
||||||
onClick={handleAddTeacher}
|
onClick={handleAddTeacher}
|
||||||
/>
|
/>
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
@ -192,8 +192,8 @@ function SimpleList({
|
|||||||
export default function FilesGroupsManagement({
|
export default function FilesGroupsManagement({
|
||||||
csrfToken,
|
csrfToken,
|
||||||
selectedEstablishmentId,
|
selectedEstablishmentId,
|
||||||
|
profileRole
|
||||||
}) {
|
}) {
|
||||||
const [showFilePreview, setShowFilePreview] = useState(false);
|
|
||||||
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
||||||
const [parentFiles, setParentFileMasters] = useState([]);
|
const [parentFiles, setParentFileMasters] = useState([]);
|
||||||
const [groups, setGroups] = useState([]);
|
const [groups, setGroups] = useState([]);
|
||||||
@ -211,7 +211,6 @@ export default function FilesGroupsManagement({
|
|||||||
const [selectedGroupId, setSelectedGroupId] = useState(null);
|
const [selectedGroupId, setSelectedGroupId] = useState(null);
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const [isFileUploadOpen, setIsFileUploadOpen] = useState(false);
|
|
||||||
const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false);
|
const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false);
|
||||||
const [editingParentFile, setEditingParentFile] = useState(null);
|
const [editingParentFile, setEditingParentFile] = useState(null);
|
||||||
|
|
||||||
@ -819,13 +818,15 @@ export default function FilesGroupsManagement({
|
|||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<SectionTitle title="Liste des dossiers d'inscriptions" />
|
<SectionTitle title="Liste des dossiers d'inscriptions" />
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<button
|
{profileRole !== 0 && (
|
||||||
className="flex items-center justify-center bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-2 rounded-lg shadow transition text-base font-semibold"
|
<button
|
||||||
onClick={() => setIsGroupModalOpen(true)}
|
className="flex items-center justify-center bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-2 rounded-lg shadow transition text-base font-semibold"
|
||||||
title="Créer un nouveau dossier"
|
onClick={() => setIsGroupModalOpen(true)}
|
||||||
>
|
title="Créer un nouveau dossier"
|
||||||
<Plus className="w-5 h-5" />
|
>
|
||||||
</button>
|
<Plus className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<SimpleList
|
<SimpleList
|
||||||
items={groups}
|
items={groups}
|
||||||
@ -865,52 +866,54 @@ export default function FilesGroupsManagement({
|
|||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-2/3">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<SectionTitle title="Liste des documents" />
|
<SectionTitle title="Liste des documents" />
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<DropdownMenu
|
{profileRole !== 0 && (
|
||||||
buttonContent={
|
<DropdownMenu
|
||||||
<span className="flex items-center">
|
buttonContent={
|
||||||
<Plus className="w-5 h-5" />
|
|
||||||
<ChevronDown className="w-4 h-4 ml-1" />
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
label: (
|
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Star className="w-5 h-5 mr-2 text-yellow-600" />
|
<Plus className="w-5 h-5" />
|
||||||
Formulaire personnalisé
|
<ChevronDown className="w-4 h-4 ml-1" />
|
||||||
</span>
|
</span>
|
||||||
),
|
}
|
||||||
onClick: () => handleDocDropdownSelect('formulaire'),
|
items={[
|
||||||
},
|
{
|
||||||
{
|
type: 'item',
|
||||||
type: 'item',
|
label: (
|
||||||
label: (
|
<span className="flex items-center">
|
||||||
<span className="flex items-center">
|
<Star className="w-5 h-5 mr-2 text-yellow-600" />
|
||||||
<FileText className="w-5 h-5 mr-2 text-gray-600" />
|
Formulaire personnalisé
|
||||||
Formulaire existant
|
</span>
|
||||||
</span>
|
),
|
||||||
),
|
onClick: () => handleDocDropdownSelect('formulaire'),
|
||||||
onClick: () => handleDocDropdownSelect('formulaire_existant'),
|
},
|
||||||
},
|
{
|
||||||
{
|
type: 'item',
|
||||||
type: 'item',
|
label: (
|
||||||
label: (
|
<span className="flex items-center">
|
||||||
<span className="flex items-center">
|
<FileText className="w-5 h-5 mr-2 text-gray-600" />
|
||||||
<Plus className="w-5 h-5 mr-2 text-orange-500" />
|
Formulaire existant
|
||||||
Pièce à fournir
|
</span>
|
||||||
</span>
|
),
|
||||||
),
|
onClick: () => handleDocDropdownSelect('formulaire_existant'),
|
||||||
onClick: () => handleDocDropdownSelect('parent'),
|
},
|
||||||
},
|
{
|
||||||
]}
|
type: 'item',
|
||||||
buttonClassName="flex items-center justify-center bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-2 rounded-lg shadow transition text-base font-semibold"
|
label: (
|
||||||
menuClassName="absolute right-0 mt-2 w-56 bg-white border border-gray-200 rounded shadow-lg z-20"
|
<span className="flex items-center">
|
||||||
dropdownOpen={isDocDropdownOpen}
|
<Plus className="w-5 h-5 mr-2 text-orange-500" />
|
||||||
setDropdownOpen={setIsDocDropdownOpen}
|
Pièce à fournir
|
||||||
/>
|
</span>
|
||||||
</div>
|
),
|
||||||
|
onClick: () => handleDocDropdownSelect('parent'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
buttonClassName="flex items-center justify-center bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-2 rounded-lg shadow transition text-base font-semibold"
|
||||||
|
menuClassName="absolute right-0 mt-2 w-56 bg-white border border-gray-200 rounded shadow-lg z-20"
|
||||||
|
dropdownOpen={isDocDropdownOpen}
|
||||||
|
setDropdownOpen={setIsDocDropdownOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{!selectedGroupId ? (
|
{!selectedGroupId ? (
|
||||||
<div className="flex items-center justify-center h-40 text-gray-400 text-lg italic border border-gray-200 rounded bg-white">
|
<div className="flex items-center justify-center h-40 text-gray-400 text-lg italic border border-gray-200 rounded bg-white">
|
||||||
Sélectionner un dossier d'inscription
|
Sélectionner un dossier d'inscription
|
||||||
|
|||||||
84
premier-pas.md
Normal file
84
premier-pas.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# 🧭 Premiers Pas avec N3WT-SCHOOL
|
||||||
|
|
||||||
|
Bienvenue dans **N3WT-SCHOOL** !
|
||||||
|
Ce guide rapide vous accompagne dans les premières étapes de configuration de votre instance afin de la rendre pleinement opérationnelle pour votre établissement.
|
||||||
|
|
||||||
|
> **ℹ️ Version bêta**
|
||||||
|
> N3WT-SCHOOL est actuellement en version bêta. Certaines fonctionnalités sont encore en cours de développement (par exemple : création d'une vue dédiée aux professeurs, génération automatique de factures, renforcement de la sécurité du site, etc).
|
||||||
|
> Il est donc possible que vous rencontriez des bugs ou des comportements inattendus. Merci de votre compréhension et de vos retours !
|
||||||
|
|
||||||
|
## ✅ Étapes à suivre :
|
||||||
|
|
||||||
|
1. **Configurer la signature électronique des documents via Docuseal**
|
||||||
|
2. **Activer l'envoi d'e-mails depuis la plateforme**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✍️ 1. Configuration de la signature électronique (Docuseal)
|
||||||
|
|
||||||
|
Pour permettre la signature électronique des documents administratifs (inscriptions, conventions, etc.), N3WT-SCHOOL s'appuie sur [**Docuseal**](https://docuseal.com), un service sécurisé de signature électronique.
|
||||||
|
|
||||||
|
### Étapes :
|
||||||
|
|
||||||
|
1. Créez un compte sur Docuseal :
|
||||||
|
👉 [https://docuseal.com/sign_up](https://docuseal.com/sign_up)
|
||||||
|
|
||||||
|
2. Une fois connecté, accédez à la section API :
|
||||||
|
👉 [https://console.docuseal.com/api](https://console.docuseal.com/api)
|
||||||
|
|
||||||
|
3. Copiez votre **X-Auth-Token** personnel.
|
||||||
|
Ce jeton permettra à N3WT-SCHOOL de se connecter à votre compte Docuseal.
|
||||||
|
|
||||||
|
4. **Envoyez votre X-Auth-Token à l'équipe N3WT-SCHOOL** pour qu'un administrateur puisse finaliser la configuration :
|
||||||
|
✉️ Contact : [contact@n3wtschool.com](mailto:contact@n3wtschool.com)
|
||||||
|
|
||||||
|
> ⚠️ Cette opération doit impérativement être réalisée par un administrateur N3WT-SCHOOL.
|
||||||
|
> Ne partagez pas ce token en dehors de ce cadre.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📧 2. Configuration de l'envoi d’e-mails
|
||||||
|
|
||||||
|
N3WT-SCHOOL assure la gestion des inscriptions de façon entièrement dématérialisée, avec l’envoi automatique d’e-mails à chaque étape clé du parcours (notifications aux étudiants, accusés de réception, transmission de documents, etc.).
|
||||||
|
Pour permettre le bon fonctionnement de ces envois automatiques, il est nécessaire que vous configuriez votre mot de passe dans les paramètres de messagerie (SMTP) de votre établissement dans le menu **Paramètres** de l’application.
|
||||||
|
|
||||||
|
### Informations requises :
|
||||||
|
|
||||||
|
- Hôte SMTP
|
||||||
|
- Port SMTP
|
||||||
|
- Type de sécurité (TLS / SSL)
|
||||||
|
- Adresse e-mail (utilisateur SMTP)
|
||||||
|
- Mot de passe ou **mot de passe applicatif**
|
||||||
|
|
||||||
|
La plupart des champs ont déjà été pré-remplis grâce aux informations fournies lors de votre inscription : un vrai gain de temps !
|
||||||
|
Il ne vous reste plus qu’à saisir votre mot de passe pour finaliser la configuration et profiter pleinement de l’envoi automatique d’e-mails.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Mot de passe applicatif (Gmail, Outlook, etc.)
|
||||||
|
|
||||||
|
Certains fournisseurs (notamment **Gmail**, **Yahoo**, **iCloud**) ne permettent pas d’utiliser directement votre mot de passe personnel pour des applications tierces.
|
||||||
|
Vous devez créer un **mot de passe applicatif**.
|
||||||
|
|
||||||
|
### Exemple : Créer un mot de passe applicatif avec Gmail
|
||||||
|
|
||||||
|
1. Connectez-vous à [votre compte Google](https://myaccount.google.com)
|
||||||
|
2. Allez dans **Sécurité > Validation en 2 étapes**
|
||||||
|
3. Activez la validation en 2 étapes si ce n’est pas déjà fait
|
||||||
|
4. Ensuite, allez dans **Mots de passe des applications**
|
||||||
|
5. Sélectionnez une application (ex. : "Autre (personnalisée)") et nommez-la "N3WT-SCHOOL"
|
||||||
|
6. Copiez le mot de passe généré et utilisez-le comme **mot de passe SMTP**
|
||||||
|
|
||||||
|
> 📎 Consultez l’aide officielle de Google :
|
||||||
|
> [Créer un mot de passe d’application – Google](https://support.google.com/accounts/answer/185833)
|
||||||
|
|
||||||
|
> ℹ️ Si vous rencontrez la moindre difficulté pour générer ou utiliser un mot de passe applicatif, n'hésitez pas à contacter l'équipe N3WT-SCHOOL : nous sommes à votre disposition pour vous accompagner dans cette démarche.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Vous êtes prêt·e !
|
||||||
|
|
||||||
|
Une fois ces deux configurations effectuées, votre instance N3WT-SCHOOL est prête à fonctionner pleinement.
|
||||||
|
Vous pourrez ensuite ajouter vos formations, étudiants, documents et automatiser toute votre gestion scolaire.
|
||||||
|
|
||||||
|
Merci de votre confiance et n’hésitez pas à nous faire part de vos retours pour améliorer la plateforme !
|
||||||
BIN
premier-pas.pdf
Normal file
BIN
premier-pas.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user