From 0f49236965f575f6af17837b9860fa4481227785 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sat, 3 May 2025 21:37:41 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Validation=20du=20dossier=20d'inscripti?= =?UTF-8?q?on=20en=20affectant=20l'=C3=A9l=C3=A8ve=20=C3=A0=20une=20classe?= =?UTF-8?q?=20de=20son=20niveau=20/=20cr=C3=A9ation=20d'une=20fen=C3=AAtre?= =?UTF-8?q?=20de=20visualisation=20d'une=20classe=20(en=20cours)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/School/serializers.py | 7 +- Back-End/Subscriptions/serializers.py | 2 +- .../structure/SchoolClassManagement/page.js | 214 ++++++++++++++++++ .../app/[locale]/admin/subscriptions/page.js | 36 +-- .../validateSubscription/page.js | 37 ++- Front-End/src/app/actions/schoolAction.js | 6 + .../src/components/AffectationClasseForm.js | 5 +- .../Inscription/ValidateSubscription.js | 197 +++++++++++----- Front-End/src/components/Providers.js | 9 +- .../Structure/Configuration/ClassesSection.js | 23 +- .../TeachersSelectionConfiguration.js | 60 ----- Front-End/src/context/ClassesContext.js | 6 + Front-End/src/utils/Url.js | 1 + 13 files changed, 426 insertions(+), 177 deletions(-) create mode 100644 Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js delete mode 100644 Front-End/src/components/Structure/Configuration/TeachersSelectionConfiguration.js diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index ee25db1..b348ec2 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -142,12 +142,17 @@ class PlanningSerializer(serializers.ModelSerializer): internal_value['schedule'] = data.get('schedule', {}) return internal_value +class StudentDetailSerializer(serializers.ModelSerializer): + class Meta: + model = Student + fields = ['id', 'last_name', 'first_name', 'photo', 'level'] + class SchoolClassSerializer(serializers.ModelSerializer): updated_date_formatted = serializers.SerializerMethodField() teachers = serializers.PrimaryKeyRelatedField(queryset=Teacher.objects.all(), many=True, required=False) establishment = serializers.PrimaryKeyRelatedField(queryset=Establishment.objects.all(), required=False) teachers_details = serializers.SerializerMethodField() - students = serializers.PrimaryKeyRelatedField(queryset=Student.objects.all(), many=True, required=False) + students = StudentDetailSerializer(many=True, read_only=True) class Meta: model = SchoolClass diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index 1f472f5..a264891 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -236,7 +236,7 @@ class StudentSerializer(serializers.ModelSerializer): return obj.formatted_birth_date def get_associated_class_name(self, obj): - return obj.associated_class.atmosphereName if obj.associated_class else None + return obj.associated_class.atmosphere_name if obj.associated_class else None class RegistrationFormSerializer(serializers.ModelSerializer): student = StudentSerializer(many=False, required=False) diff --git a/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js b/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js new file mode 100644 index 0000000..7d1f4d1 --- /dev/null +++ b/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js @@ -0,0 +1,214 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Plus, Users, Layers } from 'lucide-react'; +import Table from '@/components/Table'; +import MultiSelect from '@/components/MultiSelect'; +import InputText from '@/components/InputText'; +import Popup from '@/components/Popup'; +import { fetchClasse } from '@/app/actions/schoolAction'; +import { useSearchParams } from 'next/navigation'; +import logger from '@/utils/logger'; +import { useClasses } from '@/context/ClassesContext'; +import { BASE_URL } from '@/utils/Url'; + +export default function Page() { + const searchParams = useSearchParams(); + const schoolClassId = searchParams.get('schoolClassId'); + const [classe, setClasse] = useState([]); + const { getNiveauxLabels, getNiveauLabel } = useClasses(); + + const [students, setStudents] = useState([]); + const [groups, setGroups] = useState([]); + const [newGroup, setNewGroup] = useState({ + name: '', + level: null, + students: [], + }); + const [popupVisible, setPopupVisible] = useState(false); + const [popupMessage, setPopupMessage] = useState(''); + + const handleCreateGroup = () => { + if (!newGroup.name || !newGroup.level || newGroup.students.length === 0) { + setPopupMessage( + 'Tous les champs doivent être remplis pour créer un groupe.' + ); + setPopupVisible(true); + return; + } + + const updatedGroups = [...groups, newGroup]; + setGroups(updatedGroups); + setNewGroup({ name: '', level: null, students: [] }); + }; + + const requestErrorHandler = (err) => { + logger.error('Error fetching data:', err); + }; + + useEffect(() => { + fetchClasse(schoolClassId) + .then((classeData) => { + logger.debug('Classes récupérées :', classeData); + setClasse(classeData); + }) + .catch(requestErrorHandler); + }, []); + + return ( +
+

{classe?.atmosphere_name}

+ + {/* Section Niveaux */} +
+

+ + Niveaux +

+
+ {classe?.levels?.length > 0 ? ( + getNiveauxLabels(classe.levels).map((label, index) => ( + + {label} + + )) + ) : ( + Aucun niveau associé + )} +
+
+ + {/* Section Enseignants */} +
+

+ + Enseignants +

+
+ {classe?.teachers_details?.map((teacher) => ( + + {teacher.last_name} {teacher.first_name} + + ))} +
+
+ + {/* Section Élèves */} +
+

+ + Eleves +

+ ( +
+ {row.photo ? ( + + {`${row.first_name} + + ) : ( +
+ + {row.first_name[0]} + {row.last_name[0]} + +
+ )} +
+ ), + }, + { name: 'Nom', transform: (row) => row.last_name }, + { name: 'Prénom', transform: (row) => row.first_name }, + { name: 'Niveau', transform: (row) => getNiveauLabel(row.level) }, + ]} + data={classe.students} + /> + + + {/* Section Groupes */} + {/*
+

+ + groups +

+
+ {groups.map((group, index) => ( +
+

{group.name}

+

level: {group.level.name}

+
+ {group.students.map((student) => ( + + {student.last_name} {student.first_name} + + ))} +
+
+ ))} +
*/} + + {/* Formulaire de création de groupe */} + {/*
+

createGroup

+
+ setNewGroup({ ...newGroup, name: e.target.value })} + placeholder='groupName' + /> + setNewGroup({ ...newGroup, level: selected[0] })} + /> + setNewGroup({ ...newGroup, students: selected })} + /> + +
+
+
*/} + + {/* Popup */} + setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + ); +} diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 632d648..299543d 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -4,13 +4,13 @@ import Table from '@/components/Table'; import Tab from '@/components/Tab'; import { useTranslations } from 'next-intl'; import StatusLabel from '@/components/StatusLabel'; -import { Search } from 'lucide-react'; import Popup from '@/components/Popup'; import Loader from '@/components/Loader'; import AlertWithModal from '@/components/AlertWithModal'; import { useRouter } from 'next/navigation'; import DropdownMenu from '@/components/DropdownMenu'; import { + Search, MoreVertical, Send, Edit, @@ -22,7 +22,6 @@ import { } from 'lucide-react'; import Modal from '@/components/Modal'; import InscriptionForm from '@/components/Inscription/InscriptionForm'; -import AffectationClasseForm from '@/components/AffectationClasseForm'; import { useEstablishment } from '@/context/EstablishmentContext'; import { @@ -94,7 +93,6 @@ export default function Page({ params: { locale } }) { const [schoolFileMasters, setSchoolFileMasters] = useState([]); const [parentFileMasters, setParentFileMasters] = useState([]); const [isOpen, setIsOpen] = useState(false); - const [isOpenAffectationClasse, setIsOpenAffectationClasse] = useState(false); const [student, setStudent] = useState(''); const [classes, setClasses] = useState([]); const [students, setEleves] = useState([]); @@ -142,11 +140,6 @@ export default function Page({ params: { locale } }) { setIsOpen(false); }; - const openModalAssociationEleve = (eleveSelected) => { - setIsOpenAffectationClasse(true); - setStudent(eleveSelected); - }; - const openFilesModal = (row) => { setSelectedRegisterForm(row || []); setIsFilesModalOpen(true); @@ -453,17 +446,6 @@ export default function Page({ params: { locale } }) { setConfirmPopupVisible(true); }; - const affectationClassFormSubmitHandler = (formdata) => { - editRegisterForm(student.id, formData, csrfToken) - .then((data) => { - logger.debug('Success:', data); - setReloadFetch(true); - }) - .catch((error) => { - logger.error('Error :', error); - }); - }; - const updateStatusAction = (id, newStatus) => { logger.debug( `Mise à jour du statut du dossier d'inscription avec l'ID : ${id} vers le statut : ${newStatus}` @@ -785,7 +767,7 @@ export default function Page({ params: { locale } }) { ), onClick: () => { - const url = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&sepa_file=${row.sepa_file}&student_file=${row.registration_file}`; + const url = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&level=${row.student.level}&sepa_file=${row.sepa_file}&student_file=${row.registration_file}`; router.push(`${url}`); }, }, @@ -1141,20 +1123,6 @@ export default function Page({ params: { locale } }) { )} /> )} - {isOpenAffectationClasse && ( - ( - - )} - /> - )} {isSepaUploadModalOpen && ( { + logger.error('Error fetching data:', err); + }; + + useEffect(() => { + if (selectedEstablishmentId) { + fetchClasses(selectedEstablishmentId) + .then((classesData) => { + logger.debug('Classes récupérées :', classesData); + + // Filtrer les classes par niveau + const filteredClasses = classesData.filter( + (classe) => classe.levels.includes(parseInt(level, 10)) // Vérifier si le niveau de l'étudiant est dans les niveaux de la classe + ); + + setClasses(filteredClasses); // Mettre à jour les classes filtrées + }) + .catch(requestErrorHandler); + } + }, [selectedEstablishmentId]); const handleAcceptRF = (data) => { const formData = new FormData(); @@ -41,7 +71,7 @@ export default function Page() { }); }; - if (isLoading) { + if (isLoading || classes.length === 0) { return ; } @@ -53,6 +83,7 @@ export default function Page() { sepa_file={sepa_file} student_file={student_file} onAccept={handleAcceptRF} + classes={classes} /> ); } diff --git a/Front-End/src/app/actions/schoolAction.js b/Front-End/src/app/actions/schoolAction.js index fd1fa2d..37d562d 100644 --- a/Front-End/src/app/actions/schoolAction.js +++ b/Front-End/src/app/actions/schoolAction.js @@ -39,6 +39,12 @@ export const fetchClasses = (establishment) => { ).then(requestResponseHandler); }; +export const fetchClasse = (id) => { + return fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}/${id}`).then( + requestResponseHandler + ); +}; + export const fetchSchedules = () => { return fetch(`${BE_SCHOOL_PLANNINGS_URL}`).then(requestResponseHandler); }; diff --git a/Front-End/src/components/AffectationClasseForm.js b/Front-End/src/components/AffectationClasseForm.js index 6bd5278..c88f90c 100644 --- a/Front-End/src/components/AffectationClasseForm.js +++ b/Front-End/src/components/AffectationClasseForm.js @@ -14,12 +14,13 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => { }; const handleSubmit = () => { - onSubmit({ + console.log(formData); + /*onSubmit({ eleve: { ...formData, }, etat: 5, - }); + });*/ }; return ( diff --git a/Front-End/src/components/Inscription/ValidateSubscription.js b/Front-End/src/components/Inscription/ValidateSubscription.js index 97e46e3..42a207a 100644 --- a/Front-End/src/components/Inscription/ValidateSubscription.js +++ b/Front-End/src/components/Inscription/ValidateSubscription.js @@ -1,6 +1,7 @@ 'use client'; import React, { useState, useEffect } from 'react'; import ToggleSwitch from '@/components/ToggleSwitch'; +import SelectChoice from '@/components/SelectChoice'; import { BASE_URL } from '@/utils/Url'; import { fetchSchoolFileTemplatesFromRegistrationFiles, @@ -9,6 +10,7 @@ import { import logger from '@/utils/logger'; import { School, CheckCircle } from 'lucide-react'; import SectionHeader from '@/components/SectionHeader'; +import Button from '@/components/Button'; export default function ValidateSubscription({ studentId, @@ -17,11 +19,34 @@ export default function ValidateSubscription({ sepa_file, student_file, onAccept, + classes, }) { const [schoolFileTemplates, setSchoolFileTemplates] = useState([]); const [parentFileTemplates, setParentFileTemplates] = useState([]); const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0); const [mergeDocuments, setMergeDocuments] = useState(false); + const [isPageValid, setIsPageValid] = useState(false); + + const [formData, setFormData] = useState({ + associated_class: null, + }); + + useEffect(() => { + if (classes.length > 0) { + // Si l'étudiant a déjà une classe associée, initialisez formData avec cette classe + const initialClass = classes.find( + (classe) => classe.id === formData.associated_class + ); + setFormData({ + associated_class: initialClass ? initialClass.id : null, + }); + } + }, [classes]); + + // Mettre à jour isPageValid en fonction de la sélection de la classe + useEffect(() => { + setIsPageValid(!!formData.associated_class); // true si une classe est sélectionnée, sinon false + }, [formData.associated_class]); useEffect(() => { // Récupérer les fichiers schoolFileTemplates @@ -51,20 +76,32 @@ export default function ValidateSubscription({ ); }, [studentId]); - const handleAccept = () => { - const fusionParam = mergeDocuments ? 'true' : 'false'; - - const data = { - status: 5, - fusionParam: fusionParam, - }; - - onAccept(data); + const handleToggleMergeDocuments = () => { + setMergeDocuments((prevState) => !prevState); }; - const handleToggleMergeDocuments = () => { - // Inverser l'état de mergeDocuments - setMergeDocuments((prevState) => !prevState); + const handleAssignClass = () => { + const fusionParam = mergeDocuments ? 'true' : 'false'; + if (formData.associated_class) { + const data = { + student: { + associated_class: formData.associated_class, + }, + status: 5, + fusionParam: fusionParam, + }; + + onAccept(data); + } else { + logger.warn('Aucune classe sélectionnée.'); + } + }; + + const onChange = (field, value) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); }; const allTemplates = [ @@ -89,6 +126,7 @@ export default function ValidateSubscription({ ] : []), ]; + console.log(allTemplates); return (
@@ -101,53 +139,7 @@ export default function ValidateSubscription({ {/* Contenu principal */}
- {/* Liste des documents */} -
-

- Liste des documents -

-
    - {allTemplates.map((template, index) => ( -
  • setCurrentTemplateIndex(index)} // Mettre à jour l'index du template actuel - > - - {template.file !== null ? ( - - ) : ( - - )} - - {template.name} -
  • - ))} -
- - {/* ToggleSwitch et bouton de validation */} -
- - -
-
- - {/* Affichage du fichier actuel */} + {/* Colonne gauche : Affichage du fichier actuel */}
{currentTemplateIndex < allTemplates.length && (
@@ -158,7 +150,6 @@ export default function ValidateSubscription({ {allTemplates[currentTemplateIndex].description || 'Aucune description disponible pour ce document.'}

-