From 427b6c758892469d07579159511e7ce1ceed20d0 Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Sun, 12 Jan 2025 14:37:49 +0100 Subject: [PATCH 001/249] refactor: refactoring du FRONT page subscribe --- Front-End/src/app/[locale]/admin/layout.js | 26 +- Front-End/src/app/[locale]/admin/page.js | 4 +- .../src/app/[locale]/admin/structure/page.js | 28 +- .../subscriptions/components/FileUpload.js | 8 +- .../subscriptions/editInscription/page.js | 18 +- .../app/[locale]/admin/subscriptions/page.js | 530 +++++++----------- .../[locale]/parents/editInscription/page.js | 26 +- Front-End/src/app/[locale]/parents/layout.js | 12 +- Front-End/src/app/[locale]/parents/page.js | 6 +- .../src/app/[locale]/users/login/page.js | 14 +- .../app/[locale]/users/password/new/page.js | 6 +- .../app/[locale]/users/password/reset/page.js | 12 +- .../src/app/[locale]/users/subscribe/page.js | 12 +- Front-End/src/app/lib/actions.js | 39 -- Front-End/src/app/lib/authAction.js | 67 +++ Front-End/src/app/lib/schoolAction.js | 9 + Front-End/src/app/lib/subscriptionAction.js | 123 ++++ .../components/Inscription/InscriptionForm.js | 110 ++-- .../Inscription/InscriptionFormShared.js | 32 +- .../Configuration/StructureManagement.js | 36 +- .../Configuration/TeachersSection.js | 68 +-- .../Structure/Planning/ScheduleManagement.js | 28 +- .../Planning/SpecialityEventModal.js | 14 +- Front-End/src/hooks/useCsrfToken.js | 6 +- Front-End/src/utils/Url.js | 89 ++- 25 files changed, 671 insertions(+), 652 deletions(-) delete mode 100644 Front-End/src/app/lib/actions.js create mode 100644 Front-End/src/app/lib/authAction.js create mode 100644 Front-End/src/app/lib/schoolAction.js create mode 100644 Front-End/src/app/lib/subscriptionAction.js diff --git a/Front-End/src/app/[locale]/admin/layout.js b/Front-End/src/app/[locale]/admin/layout.js index b30edd1..c35aac9 100644 --- a/Front-End/src/app/[locale]/admin/layout.js +++ b/Front-End/src/app/[locale]/admin/layout.js @@ -16,15 +16,15 @@ import { import DropdownMenu from '@/components/DropdownMenu'; import Logo from '@/components/Logo'; import { - FR_ADMIN_HOME_URL, - FR_ADMIN_SUBSCRIPTIONS_URL, - FR_ADMIN_STRUCTURE_URL, - FR_ADMIN_GRADES_URL, - FR_ADMIN_PLANNING_URL, - FR_ADMIN_SETTINGS_URL + FE_ADMIN_HOME_URL, + FE_ADMIN_SUBSCRIPTIONS_URL, + FE_ADMIN_STRUCTURE_URL, + FE_ADMIN_GRADES_URL, + FE_ADMIN_PLANNING_URL, + FE_ADMIN_SETTINGS_URL } from '@/utils/Url'; -import { disconnect } from '@/app/lib/actions'; +import { disconnect } from '@/app/lib/authAction'; export default function Layout({ children, @@ -32,12 +32,12 @@ export default function Layout({ const t = useTranslations('sidebar'); const sidebarItems = { - "admin": { "id": "admin", "name": t('dashboard'), "url": FR_ADMIN_HOME_URL, "icon": Home }, - "subscriptions": { "id": "subscriptions", "name": t('subscriptions'), "url": FR_ADMIN_SUBSCRIPTIONS_URL, "icon": Users }, - "structure": { "id": "structure", "name": t('structure'), "url": FR_ADMIN_STRUCTURE_URL, "icon": Building }, - "grades": { "id": "grades", "name": t('grades'), "url": FR_ADMIN_GRADES_URL, "icon": FileText }, - "planning": { "id": "planning", "name": t('planning'), "url": FR_ADMIN_PLANNING_URL, "icon": Calendar }, - "settings": { "id": "settings", "name": t('settings'), "url": FR_ADMIN_SETTINGS_URL, "icon": Settings } + "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 }, + "planning": { "id": "planning", "name": t('planning'), "url": FE_ADMIN_PLANNING_URL, "icon": Calendar }, + "settings": { "id": "settings", "name": t('settings'), "url": FE_ADMIN_SETTINGS_URL, "icon": Settings } }; const pathname = usePathname(); diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index 85e18fa..0a1f2f3 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; import { useTranslations } from 'next-intl'; import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react'; import Loader from '@/components/Loader'; -import { BK_GESTIONENSEIGNANTS_CLASSES_URL } from '@/utils/Url'; +import { BE_SCHOOL_SCHOOLCLASSES_URL } from '@/utils/Url'; import ClasseDetails from '@/components/ClasseDetails'; // Composant StatCard pour afficher une statistique @@ -59,7 +59,7 @@ export default function DashboardPage() { const [classes, setClasses] = useState([]); const fetchClasses = () => { - fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`) + fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}`) .then(response => response.json()) .then(data => { setClasses(data); diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 06ec2f1..7ae7b2f 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -4,10 +4,10 @@ import { School, Calendar } from 'lucide-react'; import TabsStructure from '@/components/Structure/Configuration/TabsStructure'; import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement' import StructureManagement from '@/components/Structure/Configuration/StructureManagement' -import { BK_GESTIONENSEIGNANTS_SPECIALITES_URL, - BK_GESTIONENSEIGNANTS_CLASSES_URL, - BK_GESTIONENSEIGNANTS_TEACHERS_URL, - BK_GESTIONENSEIGNANTS_PLANNINGS_URL } from '@/utils/Url'; +import { BE_SCHOOL_SPECIALITIES_URL, + BE_SCHOOL_SCHOOLCLASSES_URL, + BE_SCHOOL_TEACHERS_URL, + BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url'; import DjangoCSRFToken from '@/components/DjangoCSRFToken' import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; @@ -34,13 +34,13 @@ export default function Page() { // Fetch data for classes fetchClasses(); - + // Fetch data for schedules fetchSchedules(); }, []); const fetchSpecialities = () => { - fetch(`${BK_GESTIONENSEIGNANTS_SPECIALITES_URL}`) + fetch(`${BE_SCHOOL_SPECIALITIES_URL}`) .then(response => response.json()) .then(data => { setSpecialities(data); @@ -51,7 +51,7 @@ export default function Page() { }; const fetchTeachers = () => { - fetch(`${BK_GESTIONENSEIGNANTS_TEACHERS_URL}`) + fetch(`${BE_SCHOOL_TEACHERS_URL}`) .then(response => response.json()) .then(data => { setTeachers(data); @@ -62,7 +62,7 @@ export default function Page() { }; const fetchClasses = () => { - fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`) + fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}`) .then(response => response.json()) .then(data => { setClasses(data); @@ -73,7 +73,7 @@ export default function Page() { }; const fetchSchedules = () => { - fetch(`${BK_GESTIONENSEIGNANTS_PLANNINGS_URL}`) + fetch(`${BE_SCHOOL_PLANNINGS_URL}`) .then(response => response.json()) .then(data => { setSchedules(data); @@ -141,13 +141,13 @@ export default function Page() { console.error('Erreur :', error); }); }; - + const handleDelete = (url, id, setDatas) => { fetch(`${url}/${id}`, { method:'DELETE', headers: { - 'Content-Type':'application/json', + 'Content-Type':'application/json', 'X-CSRFToken': csrfToken }, credentials: 'include' @@ -172,12 +172,12 @@ export default function Page() { {activeTab === 'Configuration' && ( <> - + /> )} diff --git a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js b/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js index beab467..db1c60b 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js @@ -34,11 +34,11 @@ export default function FileUpload({ onFileUpload }) { }; const handleUpload = () => { - if (file) { + onFileUpload(file, fileName); setFile(null); setFileName(''); - } + }; return ( @@ -66,8 +66,8 @@ export default function FileUpload({ onFileUpload }) { /> diff --git a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js index 1149b3b..4f2eaa5 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js @@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react'; import { useSearchParams } from 'next/navigation'; import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared'; -import { FR_ADMIN_SUBSCRIPTIONS_URL, - BK_GESTIONINSCRIPTION_ELEVE_URL, - BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL } from '@/utils/Url'; +import { FE_ADMIN_SUBSCRIPTIONS_URL, + BE_SUBSCRIPTION_STUDENT_URL, + BE_SUBSCRIPTION_REGISTERFORM_URL } from '@/utils/Url'; import useCsrfToken from '@/hooks/useCsrfToken'; import { mockStudent } from '@/data/mockStudent'; @@ -13,7 +13,7 @@ const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; export default function Page() { const searchParams = useSearchParams(); const idProfil = searchParams.get('id'); - const idEleve = searchParams.get('idEleve'); // Changé de codeDI à idEleve + const studentId = searchParams.get('studentId'); // Changé de codeDI à studentId const [initialData, setInitialData] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -24,7 +24,7 @@ export default function Page() { setInitialData(mockStudent); setIsLoading(false); } else { - fetch(`${BK_GESTIONINSCRIPTION_ELEVE_URL}/${idEleve}`) // Utilisation de idEleve au lieu de codeDI + fetch(`${BE_SUBSCRIPTION_STUDENT_URL}/${studentId}`) // Utilisation de studentId au lieu de codeDI .then(response => response.json()) .then(data => { console.log('Fetched data:', data); // Pour le débogage @@ -49,7 +49,7 @@ export default function Page() { setIsLoading(false); }); } - }, [idEleve]); // Dépendance changée à idEleve + }, [studentId]); // Dépendance changée à studentId const handleSubmit = async (data) => { if (useFakeData) { @@ -58,7 +58,7 @@ export default function Page() { } try { - const response = await fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${idEleve}`, { // Utilisation de idEleve + const response = await fetch(`${BE_SUBSCRIPTION_REGISTERFORM_URL}/${studentId}`, { // Utilisation de studentId method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -72,7 +72,7 @@ export default function Page() { const result = await response.json(); console.log('Success:', result); // Redirection après succès - window.location.href = FR_ADMIN_SUBSCRIPTIONS_URL; + window.location.href = FE_ADMIN_SUBSCRIPTIONS_URL; } catch (error) { console.error('Error:', error); alert('Une erreur est survenue lors de la mise à jour des données'); @@ -84,7 +84,7 @@ export default function Page() { initialData={initialData} csrfToken={csrfToken} onSubmit={handleSubmit} - cancelUrl={FR_ADMIN_SUBSCRIPTIONS_URL} + cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL} isLoading={isLoading} /> ); diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index cc94eec..67e3a13 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -9,25 +9,33 @@ import { Search } from 'lucide-react'; import Popup from '@/components/Popup'; import Loader from '@/components/Loader'; import AlertWithModal from '@/components/AlertWithModal'; -import Button from '@/components/Button'; import DropdownMenu from "@/components/DropdownMenu"; -import { swapFormatDate } from '@/utils/Date'; import { formatPhoneNumber } from '@/utils/Telephone'; -import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus, CheckCircle, Plus, Download } from 'lucide-react'; +import { MoreVertical, Send, Edit, Trash2, FileText, CheckCircle, Plus, Download } from 'lucide-react'; import Modal from '@/components/Modal'; import InscriptionForm from '@/components/Inscription/InscriptionForm' import AffectationClasseForm from '@/components/AffectationClasseForm' import FileUpload from './components/FileUpload'; -import { BASE_URL, BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL, - BK_GESTIONINSCRIPTION_SEND_URL, - FR_ADMIN_SUBSCRIPTIONS_EDIT_URL, - BK_GESTIONINSCRIPTION_ARCHIVE_URL, - BK_GESTIONENSEIGNANTS_CLASSES_URL, - BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL, - BK_GESTIONINSCRIPTION_FICHERSINSCRIPTION_URL , - BK_GESTIONINSCRIPTION_ELEVES_URL, - BK_PROFILE_URL } from '@/utils/Url'; +import { + PENDING, + SUBSCRIBED, + ARCHIVED, + fetchRegisterForm, + createRegisterForm, + sendRegisterForm, + archiveRegisterForm, + fetchRegisterFormFileTemplate, + deleteRegisterFormFileTemplate, + fetchStudents, + editRegisterForm } from "@/app/lib/subscriptionAction" + +import { fetchClasses } from '@/app/lib/schoolAction'; +import { createProfile } from '@/app/lib/authAction'; + +import { + BASE_URL, + FE_ADMIN_SUBSCRIPTIONS_EDIT_URL } from '@/utils/Url'; import DjangoCSRFToken from '@/components/DjangoCSRFToken' import useCsrfToken from '@/hooks/useCsrfToken'; @@ -56,14 +64,13 @@ export default function Page({ params: { locale } }) { const [itemsPerPage, setItemsPerPage] = useState(5); // Définir le nombre d'éléments par page const [fichiers, setFichiers] = useState([]); - const [nomFichier, setNomFichier] = useState(''); - const [fichier, setFichier] = useState(null); + const [isOpen, setIsOpen] = useState(false); const [isOpenAffectationClasse, setIsOpenAffectationClasse] = useState(false); - const [eleve, setEleve] = useState(''); + const [student, setStudent] = useState(''); const [classes, setClasses] = useState([]); - const [eleves, setEleves] = useState([]); + const [students, setEleves] = useState([]); const csrfToken = useCsrfToken(); @@ -77,152 +84,90 @@ export default function Page({ params: { locale } }) { const openModalAssociationEleve = (eleveSelected) => { setIsOpenAffectationClasse(true); - setEleve(eleveSelected); + setStudent(eleveSelected); } - // Modifier la fonction fetchData pour inclure le terme de recherche - const fetchData = (page, pageSize, search = '') => { - const url = `${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/pending?page=${page}&page_size=${pageSize}&search=${search}`; - fetch(url, { - headers: { - 'Content-Type': 'application/json', - }, - }).then(response => response.json()) - .then(data => { - setIsLoading(false); - if (data) { - const { fichesInscriptions, count } = data; - if (ficheInscriptions) { - setFichesInscriptionsDataEnCours(fichesInscriptions); - } - const calculatedTotalPages = count === 0 ? 1 : Math.ceil(count / pageSize); - setTotalPending(count); - setTotalPages(calculatedTotalPages); - } - console.log('Success PENDING:', data); - }) - .catch(error => { - console.error('Error fetching data:', error); - setIsLoading(false); - }); - }; - const fetchDataSubscribed = () => { - fetch(`${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/subscribed`, { - headers: { - 'Content-Type': 'application/json', - }, - }).then(response => response.json()) - .then(data => { - setIsLoading(false); - if (data) { - const { fichesInscriptions, count } = data; - setTotalSubscribed(count); - if (fichesInscriptions) { - setFichesInscriptionsDataInscrits(fichesInscriptions); - } - } - console.log('Success SUBSCRIBED:', data); - }) - .catch(error => { - console.error('Error fetching data:', error); - setIsLoading(false); - }); - }; + const requestErrorHandler = (err)=>{ + setIsLoading(false); + } - const fetchDataArchived = () => { - fetch(`${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/archived`, { - headers: { - 'Content-Type': 'application/json', - }, - }).then(response => response.json()) - .then(data => { - setIsLoading(false); - if (data) { - const { fichesInscriptions, count } = data; - setTotalArchives(count); - if (fichesInscriptions) { - setFichesInscriptionsDataArchivees(fichesInscriptions); - } - } - console.log('Success ARCHIVED:', data); - }) - .catch(error => { - console.error('Error fetching data:', error); - setIsLoading(false); - }); - }; + const registerFormPendingDataHandler = (data) => { + setIsLoading(false); + if (data) { + const { registerForms, count } = data; + if (registerForms) { + setFichesInscriptionsDataEnCours(registerForms); + } + const calculatedTotalPages = count === 0 ? 1 : Math.ceil(count / pageSize); + setTotalPending(count); + setTotalPages(calculatedTotalPages); + } + } - const fetchClasses = () => { - fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`) - .then(response => response.json()) - .then(data => { + const registerFormSubscribedDataHandler = (data) => { + setIsLoading(false); + if (data) { + const { registerForms, count } = data; + setTotalSubscribed(count); + if (registerForms) { + setFichesInscriptionsDataInscrits(registerForms); + } + } + } + +const registerFormArchivedDataHandler = (data) => { + setIsLoading(false); + if (data) { + const { registerForms, count } = data; + setTotalArchives(count); + if (registerForms) { + setFichesInscriptionsDataArchivees(registerForms); + } + } +} + + + + + useEffect(() => { + fetchRegisterFormFileTemplate() + .then((data)=> {setFichiers(data)}) + .catch((err)=>{ err = err.message; console.log(err);}); + }, []); + + useEffect(() => { + fetchClasses() + .then(data => { setClasses(data); console.log("Success CLASSES : ", data) }) .catch(error => { console.error('Error fetching classes:', error); }); - }; - - const fetchStudents = () => { - const request = new Request( - `${BK_GESTIONINSCRIPTION_ELEVES_URL}`, - { - method:'GET', - headers: { - 'Content-Type':'application/json' - }, - } - ); - fetch(request).then(response => response.json()) - .then(data => { - console.log('Success STUDENTS:', data); - setEleves(data); - }) - .catch(error => { - console.error('Error fetching data:', error); - error = error.message; - console.log(error); + fetchStudents() + .then(data => { + console.log('Success STUDENTS:', data); + setEleves(data); + }) + .catch(error => { + console.error('Error fetching data:', error); + error = error.message; + console.log(error); }); - }; - - useEffect(() => { - const fetchFichiers = () => { - const request = new Request( - `${BK_GESTIONINSCRIPTION_FICHERSINSCRIPTION_URL}`, - { - method:'GET', - headers: { - 'Content-Type':'application/json' - }, - } - ); - fetch(request).then(response => response.json()) - .then(data => { - console.log('Success FILES:', data); - setFichiers(data); - }) - .catch(error => { - console.error('Error fetching data:', error); - error = error.message; - console.log(error); - }); - - }; - - fetchFichiers(); - }, []); - useEffect(() => { - fetchClasses(); - fetchStudents(); }, [fichesInscriptionsDataEnCours]); useEffect(() => { const fetchDataAndSetState = () => { if (!useFakeData) { - fetchData(currentPage, itemsPerPage, searchTerm); - fetchDataSubscribed(); - fetchDataArchived(); + fetchRegisterForm(PENDING, currentPage, itemsPerPage, searchTerm) + .then(registerFormPendingDataHandler) + .catch(requestErrorHandler) + fetchRegisterForm(SUBSCRIBED) + .then(registerFormSubscribedDataHandler) + .catch(requestErrorHandler) + fetchRegisterForm(ARCHIVED) + .then(registerFormArchivedDataHandler) + .catch(requestErrorHandler) } else { setTimeout(() => { setFichesInscriptionsDataEnCours(mockFicheInscription); @@ -239,7 +184,9 @@ export default function Page({ params: { locale } }) { // Modifier le useEffect pour la recherche useEffect(() => { const timeoutId = setTimeout(() => { - fetchData(currentPage, itemsPerPage, searchTerm); + fetchRegisterForm(PENDING, currentPage, itemsPerPage, searchTerm) + .then(registerFormPendingDataHandler) + .catch(requestErrorHandler) }, 500); // Debounce la recherche return () => clearTimeout(timeoutId); @@ -250,13 +197,7 @@ export default function Page({ params: { locale } }) { visible: true, message: `Attentions ! \nVous êtes sur le point d'archiver le dossier d'inscription de ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`, onConfirm: () => { - const url = `${BK_GESTIONINSCRIPTION_ARCHIVE_URL}/${id}`; - fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }).then(response => response.json()) + archiveRegisterForm(id) .then(data => { console.log('Success:', data); setFicheInscriptions(ficheInscriptions.filter(fiche => fiche.id !== id)); @@ -271,18 +212,12 @@ export default function Page({ params: { locale } }) { }); }; - const sendConfirmFicheInscription = (id, nom, prenom) => { + const sendConfirmRegisterForm = (id, nom, prenom) => { setPopup({ visible: true, message: `Avertissement ! \nVous êtes sur le point d'envoyer un dossier d'inscription à ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`, onConfirm: () => { - const url = `${BK_GESTIONINSCRIPTION_SEND_URL}/${id}`; - fetch(url, { - headers: { - 'Content-Type': 'application/json', - }, - }).then(response => response.json()) - .then(data => { + sendRegisterForm(id).then(data => { console.log('Success:', data); setMailSent(true); }) @@ -292,15 +227,19 @@ export default function Page({ params: { locale } }) { } }); }; - + const affectationClassFormSubmitHandler = (formdata)=> { + editRegisterForm(student.id,formData, csrfToken) + .then(data => { + console.log('Success:', data); + }) + .catch(error => { + console.error('Error :', error); + }); + } const updateStatusAction = (id, newStatus) => { console.log('Edit fiche inscription with id:', id); }; - const handleLetterClick = (letter) => { - setFilter(letter); - }; - const handleSearchChange = (event) => { setSearchTerm(event.target.value); }; @@ -310,28 +249,20 @@ export default function Page({ params: { locale } }) { fetchData(newPage, itemsPerPage); // Appeler fetchData directement ici }; - const createDI = (updatedData) => { - if (updatedData.selectedResponsables.length !== 0) { - const selectedResponsablesIds = updatedData.selectedResponsables.map(responsableId => responsableId) + const createRF = (updatedData) => { + console.log("updateDATA",updatedData); + if (updatedData.selectedGuardians.length !== 0) { + const selectedGuardiansIds = updatedData.selectedGuardians.map(guardianId => guardianId) const data = { - eleve: { - nom: updatedData.eleveNom, - prenom: updatedData.elevePrenom, + student: { + last_name: updatedData.studentLastName, + first_name: updatedData.studentFirstName, }, - idResponsables: selectedResponsablesIds + idGuardians: selectedGuardiansIds }; - const url = `${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}`; - fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken - }, - body: JSON.stringify(data), - credentials: 'include' - }) + createRegisterForm(data,csrfToken) .then(response => response.json()) .then(data => { console.log('Success:', data); @@ -343,7 +274,7 @@ export default function Page({ params: { locale } }) { }); setTotalPending(totalPending+1); if (updatedData.autoMail) { - sendConfirmFicheInscription(data.eleve.id, updatedData.eleveNom, updatedData.elevePrenom); + sendConfirmRegisterForm(data.student.id, updatedData.studentLastName, updatedData.studentFirstName); } }) .catch((error) => { @@ -353,55 +284,35 @@ export default function Page({ params: { locale } }) { else { // Création d'un profil associé à l'adresse mail du responsable saisie // Le profil est inactif - const request = new Request( - `${BK_PROFILE_URL}`, - { - method:'POST', - headers: { - 'Content-Type':'application/json', - 'X-CSRFToken': csrfToken - }, - credentials: 'include', - body: JSON.stringify( { - email: updatedData.responsableEmail, - password: 'Provisoire01!', - username: updatedData.responsableEmail, - is_active: 0, // On rend le profil inactif : impossible de s'y connecter dans la fenêtre du login tant qu'il ne s'est pas inscrit - droit:2 // Profil PARENT - }), - } - ); - fetch(request).then(response => response.json()) + const data = { + email: updatedData.guardianEmail, + password: 'Provisoire01!', + username: updatedData.guardianEmail, + is_active: 0, // On rend le profil inactif : impossible de s'y connecter dans la fenêtre du login tant qu'il ne s'est pas inscrit + droit:2 // Profil PARENT + } + createProfile(data,csrfToken) .then(response => { console.log('Success:', response); if (response.id) { - let idProfil = response.id; + let idProfile = response.id; const data = { - eleve: { - nom: updatedData.eleveNom, - prenom: updatedData.elevePrenom, - responsables: [ + student: { + last_name: updatedData.studentLastName, + first_name: updatedData.studentFirstName, + guardians: [ { - mail: updatedData.responsableEmail, - telephone: updatedData.responsableTel, - profilAssocie: idProfil // Association entre le reponsable de l'élève et le profil créé par défaut précédemment + email: updatedData.guardianEmail, + phone: updatedData.guardianPhone, + associated_profile: idProfile // Association entre le responsable de l'élève et le profil créé par défaut précédemment } ], - freres: [] + sibling: [] } }; - const url = `${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}`; - fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken - }, - body: JSON.stringify(data), - credentials: 'include' - }) - .then(response => response.json()) + + createRegisterForm(data,csrfToken) .then(data => { console.log('Success:', data); setFichesInscriptionsDataEnCours(prevState => { @@ -412,7 +323,7 @@ export default function Page({ params: { locale } }) { }); setTotalPending(totalPending+1); if (updatedData.autoMail) { - sendConfirmFicheInscription(data.eleve.id, updatedData.eleveNom, updatedData.elevePrenom); + sendConfirmRegisterForm(data.student.id, updatedData.studentLastName, updatedData.studentFirstName); } }) .catch((error) => { @@ -429,42 +340,26 @@ export default function Page({ params: { locale } }) { closeModal(); } - const validateAndAssociate = (updatedData) => { - fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${eleve.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken - }, - body: JSON.stringify(updatedData), - credentials: 'include' - }) - .then(response => response.json()) - .then(data => { - console.log('Succès :', data); - }) - .catch(error => { - console.error('Erreur :', error); - }); - } + const columns = [ - { name: t('studentName'), transform: (row) => row.eleve.nom }, - { name: t('studentFistName'), transform: (row) => row.eleve.prenom }, - { name: t('mainContactMail'), transform: (row) => row.eleve.responsables[0].mail }, - { name: t('phone'), transform: (row) => formatPhoneNumber(row.eleve.responsables[0].telephone) }, - { name: t('lastUpdateDate'), transform: (row) => row.dateMAJ_formattee}, + { 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].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) => (
- updateStatusAction(row.eleve.id, newStatus)} showDropdown={false} /> + updateStatusAction(row.student.id, newStatus)} showDropdown={false} />
) }, - { name: t('files'), transform: (row) => ( + { name: t('files'), transform: (row) => + (row.registerForms != null) &&( ) }, @@ -472,53 +367,53 @@ const columns = [ } items={[ - ...(row.etat === 1 ? [{ + ...(row.status === 1 ? [{ label: ( <> Envoyer ), - onClick: () => sendConfirmFicheInscription(row.eleve.id, row.eleve.nom, row.eleve.prenom), + onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name), }] : []), - ...(row.etat === 1 ? [{ + ...(row.status === 1 ? [{ label: ( <> Modifier ), - onClick: () => window.location.href = `${FR_ADMIN_SUBSCRIPTIONS_EDIT_URL}?idEleve=${row.eleve.id}&id=1`, + onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`, }] : []), - ...(row.etat === 2 ? [{ + ...(row.status === 2 ? [{ label: ( <> Modifier ), - onClick: () => window.location.href = `${FR_ADMIN_SUBSCRIPTIONS_EDIT_URL}?idEleve=${row.eleve.id}&id=1`, + onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`, }] : []), - ...(row.etat === 3 ? [{ + ...(row.status === 3 ? [{ label: ( <> Valider ), - onClick: () => openModalAssociationEleve(row.eleve), + onClick: () => openModalAssociationEleve(row.student), }] : []), - ...(row.etat === 5 ? [{ + ...(row.status === 5 ? [{ label: ( <> Rattacher ), - onClick: () => openModalAssociationEleve(row.eleve), + onClick: () => openModalAssociationEleve(row.student), }] : []), - ...(row.etat !== 6 ? [{ + ...(row.status !== 6 ? [{ label: ( <> Archiver ), - onClick: () => archiveFicheInscription(row.eleve.id, row.eleve.nom, row.eleve.prenom), + onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name), }] : []), ]} buttonClassName="text-gray-400 hover:text-gray-600" @@ -529,22 +424,23 @@ const columns = [ ]; const columnsSubscribed = [ - { name: t('studentName'), transform: (row) => row.eleve.nom }, - { name: t('studentFistName'), transform: (row) => row.eleve.prenom }, - { name: t('lastUpdateDate'), transform: (row) => row.dateMAJ_formattee}, - { name: t('class'), transform: (row) => row.eleve.classeAssocieeName}, - { name: t('registrationFileStatus'), transform: (row) => ( -
- updateStatusAction(row.eleve.id, newStatus)} showDropdown={false} /> -
- ) + { name: t('studentName'), transform: (row) => row.student.last_name }, + { name: t('studentFistName'), transform: (row) => row.student.first_name }, + { name: t('lastUpdateDate'), transform: (row) => row.updated_date_formated}, + { name: t('class'), transform: (row) => row.student.first_name}, + { name: t('registrationFileStatus'), transform: (row) => + ( +
+ updateStatusAction(row.student.id, newStatus)} showDropdown={false} /> +
+ ) }, { name: t('files'), transform: (row) => - ( + (row.registerForm != null) &&( ) }, @@ -557,14 +453,14 @@ const columnsSubscribed = [ Rattacher ), - onClick: () => openModalAssociationEleve(row.eleve) + onClick: () => openModalAssociationEleve(row.student) }, { label: ( <> Archiver ), - onClick: () => archiveFicheInscription(row.eleve.id, row.eleve.nom, row.eleve.prenom), + onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name), } ]} buttonClassName="text-gray-400 hover:text-gray-600" @@ -575,13 +471,7 @@ const columnsSubscribed = [ ]; const handleFileDelete = (fileId) => { - fetch(`${BK_GESTIONINSCRIPTION_FICHERSINSCRIPTION_URL}/${fileId}`, { - method: 'DELETE', - headers: { - 'X-CSRFToken': csrfToken, - }, - credentials: 'include', - }) + deleteRegisterFormFileTemplate(fileId,csrfToken) .then(response => { if (response.ok) { setFichiers(fichiers.filter(fichier => fichier.id !== fileId)); @@ -598,12 +488,15 @@ const handleFileDelete = (fileId) => { const columnsFiles = [ { name: 'Nom du fichier', transform: (row) => row.name }, - { name: 'Date de création', transform: (row) => row.date_ajout }, + { name: 'Date de création', transform: (row) => row.last_update }, { name: 'Actions', transform: (row) => (
+ { + row.file && ( - + ) + } @@ -611,51 +504,19 @@ const columnsFiles = [ ) }, ]; -const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); -const [uploadFile, setUploadFile] = useState(null); -const [uploadFileName, setUploadFileName] = useState(''); -const [fileName, setFileName] = useState(''); - -const openUploadModal = () => { - setIsUploadModalOpen(true); -}; - -const closeUploadModal = () => { - setIsUploadModalOpen(false); - setUploadFile(null); - setUploadFileName(''); - setFileName(''); -}; - -const handleFileChange = (event) => { - const file = event.target.files[0]; - setUploadFile(file); - setUploadFileName(file ? file.name : ''); -}; - -const handleFileNameChange = (event) => { - setFileName(event.target.value); -}; - const handleFileUpload = (file, fileName) => { - if (!file || !fileName) { - alert('Veuillez sélectionner un fichier et entrer un nom de fichier.'); + if ( !fileName) { + alert('Veuillez entrer un nom de fichier.'); return; } - const formData = new FormData(); - formData.append('file', file); - formData.append('name', fileName); - fetch(`${BK_GESTIONINSCRIPTION_FICHERSINSCRIPTION_URL}`, { - method: 'POST', - body: formData, - headers: { - 'X-CSRFToken': csrfToken, - }, - credentials: 'include', - }) - .then(response => response.json()) + const formData = new FormData(); + if(file){ + formData.append('fichier', file); + } + formData.append('nom', fileName); + createRegisterFormFileTemplate(formData,csrfToken) .then(data => { console.log('Success:', data); setFichiers([...fichiers, data]); @@ -808,8 +669,8 @@ const handleFileUpload = (file, fileName) => { title={"Création d'un nouveau dossier d'inscription"} size='sm:w-1/4' ContentComponent={() => ( - )} /> @@ -821,24 +682,13 @@ const handleFileUpload = (file, fileName) => { title="Affectation à une classe" ContentComponent={() => ( )} /> )} - {isUploadModalOpen && ( - ( - - )} - /> - )}
); } diff --git a/Front-End/src/app/[locale]/parents/editInscription/page.js b/Front-End/src/app/[locale]/parents/editInscription/page.js index f817e7b..08b2d4a 100644 --- a/Front-End/src/app/[locale]/parents/editInscription/page.js +++ b/Front-End/src/app/[locale]/parents/editInscription/page.js @@ -3,10 +3,10 @@ import React, { useState, useEffect } from 'react'; import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared'; import { useSearchParams, redirect, useRouter } from 'next/navigation'; import useCsrfToken from '@/hooks/useCsrfToken'; -import { FR_PARENTS_HOME_URL, - BK_GESTIONINSCRIPTION_ELEVE_URL, - BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL, - BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL } from '@/utils/Url'; +import { FE_PARENTS_HOME_URL, + BE_SUBSCRIPTION_STUDENT_URL, + BE_SUBSCRIPTION_REGISTERFORM_URL, + BE_SUBSCRIPTION_LAST_GUARDIAN_URL } from '@/utils/Url'; import { mockStudent } from '@/data/mockStudent'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; @@ -14,7 +14,7 @@ const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; export default function Page() { const searchParams = useSearchParams(); const idProfil = searchParams.get('id'); - const idEleve = searchParams.get('idEleve'); + const studentId = searchParams.get('studentId'); const router = useRouter(); const [initialData, setInitialData] = useState(null); @@ -24,8 +24,8 @@ export default function Page() { const [lastIdResponsable, setLastIdResponsable] = useState(1); useEffect(() => { - if (!idEleve || !idProfil) { - console.error('Missing idEleve or idProfil'); + if (!studentId || !idProfil) { + console.error('Missing studentId or idProfil'); return; } @@ -36,9 +36,9 @@ export default function Page() { } else { Promise.all([ // Fetch eleve data - fetch(`${BK_GESTIONINSCRIPTION_ELEVE_URL}/${idEleve}`), + fetch(`${BE_SUBSCRIPTION_STUDENT_URL}/${studentId}`), // Fetch last responsable ID - fetch(BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL) + fetch(BE_SUBSCRIPTION_LAST_GUARDIAN_URL) ]) .then(async ([eleveResponse, responsableResponse]) => { const eleveData = await eleveResponse.json(); @@ -74,7 +74,7 @@ export default function Page() { setIsLoading(false); }); } - }, [idEleve, idProfil]); + }, [studentId, idProfil]); const handleSubmit = async (data) => { if (useFakeData) { @@ -83,7 +83,7 @@ export default function Page() { } try { - const response = await fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${idEleve}`, { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTERFORM_URL}/${studentId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -95,7 +95,7 @@ export default function Page() { const result = await response.json(); console.log('Success:', result); - router.push(FR_PARENTS_HOME_URL); + router.push(FE_PARENTS_HOME_URL); } catch (error) { console.error('Error:', error); } @@ -106,7 +106,7 @@ export default function Page() { initialData={initialData} csrfToken={csrfToken} onSubmit={handleSubmit} - cancelUrl={FR_PARENTS_HOME_URL} + cancelUrl={FE_PARENTS_HOME_URL} isLoading={isLoading} /> ); diff --git a/Front-End/src/app/[locale]/parents/layout.js b/Front-End/src/app/[locale]/parents/layout.js index a78766d..29bdc94 100644 --- a/Front-End/src/app/[locale]/parents/layout.js +++ b/Front-End/src/app/[locale]/parents/layout.js @@ -5,7 +5,7 @@ import DropdownMenu from '@/components/DropdownMenu'; import { useRouter } from 'next/navigation'; // Ajout de l'importation import { Bell, User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo -import { FR_PARENTS_HOME_URL,FR_PARENTS_MESSAGERIE_URL,FR_PARENTS_SETTINGS_URL, BK_GESTIONINSCRIPTION_MESSAGES_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent +import { FE_PARENTS_HOME_URL,FE_PARENTS_MESSAGERIE_URL,FE_PARENTS_SETTINGS_URL, BE_GESTIONINSCRIPTION_MESSAGES_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent import useLocalStorage from '@/hooks/useLocalStorage'; export default function Layout({ @@ -19,7 +19,7 @@ export default function Layout({ useEffect(() => { setUserId(userId); - fetch(`${BK_GESTIONINSCRIPTION_MESSAGES_URL}/${userId}`, { + fetch(`${BE_GESTIONINSCRIPTION_MESSAGES_URL}/${userId}`, { headers: { 'Content-Type': 'application/json', }, @@ -33,7 +33,7 @@ export default function Layout({ .catch(error => { console.error('Error fetching data:', error); }); - + }, []); return ( @@ -49,7 +49,7 @@ export default function Layout({
@@ -58,7 +58,7 @@ export default function Layout({
diff --git a/Front-End/src/app/[locale]/users/password/new/page.js b/Front-End/src/app/[locale]/users/password/new/page.js index c8aa160..07099f4 100644 --- a/Front-End/src/app/[locale]/users/password/new/page.js +++ b/Front-End/src/app/[locale]/users/password/new/page.js @@ -9,7 +9,7 @@ import Loader from '@/components/Loader'; // Importez le composant Loader import Button from '@/components/Button'; // Importez le composant Button import Popup from '@/components/Popup'; // Importez le composant Popup import { User } from 'lucide-react'; // Importez directement les icônes nécessaires -import { BK_NEW_PASSWORD_URL,FR_USERS_LOGIN_URL } from '@/utils/Url'; +import { BE_AUTH_NEW_PASSWORD_URL,FE_USERS_LOGIN_URL } from '@/utils/Url'; import useCsrfToken from '@/hooks/useCsrfToken'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; @@ -35,7 +35,7 @@ export default function Page() { }, 1000); // Simule un délai de traitement } else { const request = new Request( - `${BK_NEW_PASSWORD_URL}`, + `${BE_AUTH_NEW_PASSWORD_URL}`, { method: 'POST', headers: { @@ -93,7 +93,7 @@ export default function Page() {
-
{ setPopupVisible(false); - router.push(`${FR_USERS_LOGIN_URL}`); + router.push(`${FE_USERS_LOGIN_URL}`); }} onCancel={() => setPopupVisible(false)} /> @@ -136,7 +136,7 @@ export default function Page() {
-
diff --git a/Front-End/src/app/[locale]/users/subscribe/page.js b/Front-End/src/app/[locale]/users/subscribe/page.js index f5b3634..b724a0b 100644 --- a/Front-End/src/app/[locale]/users/subscribe/page.js +++ b/Front-End/src/app/[locale]/users/subscribe/page.js @@ -10,7 +10,7 @@ import Loader from '@/components/Loader'; // Importez le composant Loader import Button from '@/components/Button'; // Importez le composant Button import Popup from '@/components/Popup'; // Importez le composant Popup import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires -import { BK_REGISTER_URL, FR_USERS_LOGIN_URL } from '@/utils/Url'; +import { BE_AUTH_REGISTER_URL, FE_USERS_LOGIN_URL } from '@/utils/Url'; import useCsrfToken from '@/hooks/useCsrfToken'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; @@ -41,7 +41,7 @@ export default function Page() { setErrorMessage("") setIsLoading(false); } else { - const url= `${BK_REGISTER_URL}`; + const url= `${BE_AUTH_REGISTER_URL}`; fetch(url, { headers: { 'Content-Type': 'application/json', @@ -100,11 +100,11 @@ export default function Page() { } } else { const request = new Request( - `${BK_REGISTER_URL}`, + `${BE_AUTH_REGISTER_URL}`, { method:'POST', headers: { - 'Content-Type':'application/json', + 'Content-Type':'application/json', 'X-CSRFToken': csrfToken }, credentials: 'include', @@ -164,14 +164,14 @@ export default function Page() {
-
+
{ setPopupVisible(false); - router.push(`${FR_USERS_LOGIN_URL}`); + router.push(`${FE_USERS_LOGIN_URL}`); }} onCancel={() => setPopupVisible(false)} /> diff --git a/Front-End/src/app/lib/actions.js b/Front-End/src/app/lib/actions.js deleted file mode 100644 index 5ee83b0..0000000 --- a/Front-End/src/app/lib/actions.js +++ /dev/null @@ -1,39 +0,0 @@ - -import { - BK_LOGIN_URL, - FR_USERS_LOGIN_URL , - FR_ADMIN_HOME_URL, - FR_ADMIN_SUBSCRIPTIONS_URL, - FR_ADMIN_CLASSES_URL, - FR_ADMIN_GRADES_URL, - FR_ADMIN_PLANNING_URL, - FR_ADMIN_TEACHERS_URL, - FR_ADMIN_SETTINGS_URL -} from '@/utils/Url'; - -import {mockUser} from "@/data/mockUsersData"; - -const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; - - -/** - * Disconnects the user after confirming the action. - * If `NEXT_PUBLIC_USE_FAKE_DATA` environment variable is set to 'true', it will log a fake disconnect and redirect to the login URL. - * Otherwise, it will send a PUT request to the backend to update the user profile and then redirect to the login URL. - * - * @function - * @name disconnect - * @returns {void} - */ -export function disconnect () { - if (confirm("\nÊtes-vous sûr(e) de vouloir vous déconnecter ?")) { - - if (useFakeData) { - console.log('Fake disconnect:', mockUser); - router.push(`${FR_USERS_LOGIN_URL}`); - } else { - console.log('Fake disconnect:', mockUser); - router.push(`${FR_USERS_LOGIN_URL}`); - } - } - }; diff --git a/Front-End/src/app/lib/authAction.js b/Front-End/src/app/lib/authAction.js new file mode 100644 index 0000000..bce3624 --- /dev/null +++ b/Front-End/src/app/lib/authAction.js @@ -0,0 +1,67 @@ + +import { + BE_AUTH_LOGIN_URL, + BE_AUTH_PROFILE_URL, + FE_USERS_LOGIN_URL , +} from '@/utils/Url'; + +import {mockUser} from "@/data/mockUsersData"; + +const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; + + +/** + * Disconnects the user after confirming the action. + * If `NEXT_PUBLIC_USE_FAKE_DATA` environment variable is set to 'true', it will log a fake disconnect and redirect to the login URL. + * Otherwise, it will send a PUT request to the backend to update the user profile and then redirect to the login URL. + * + * @function + * @name disconnect + * @returns {void} + */ +export const disconnect = () => { + if (confirm("\nÊtes-vous sûr(e) de vouloir vous déconnecter ?")) { + + if (useFakeData) { + console.log('Fake disconnect:', mockUser); + router.push(`${FE_USERS_LOGIN_URL}`); + } else { + console.log('Fake disconnect:', mockUser); + router.push(`${FE_USERS_LOGIN_URL}`); + } + } + }; + + +export const createProfile = (data,csrfToken) => { +const request = new Request( + `${BE_AUTH_PROFILE_URL}`, + { + method:'POST', + headers: { + 'Content-Type':'application/json', + 'X-CSRFToken': csrfToken + }, + credentials: 'include', + body: JSON.stringify(data), + } + ); + return fetch(request).then(response => response.json()) +} + + +export const updateProfile = (id, data, csrfToken) => { + const request = new Request( + `${BE_AUTH_PROFILE_URL}/${id}`, + { + method:'PUT', + headers: { + 'Content-Type':'application/json', + 'X-CSRFToken': csrfToken + }, + credentials: 'include', + body: JSON.stringify(data), + } + ); + return fetch(request).then(response => response.json()) +} \ No newline at end of file diff --git a/Front-End/src/app/lib/schoolAction.js b/Front-End/src/app/lib/schoolAction.js new file mode 100644 index 0000000..fceb666 --- /dev/null +++ b/Front-End/src/app/lib/schoolAction.js @@ -0,0 +1,9 @@ +import { + BE_SCHOOL_SCHOOLCLASSES_URL +} from '@/utils/Url'; + + export const fetchClasses = () => { + return fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}`) + .then(response => response.json()) + + }; \ No newline at end of file diff --git a/Front-End/src/app/lib/subscriptionAction.js b/Front-End/src/app/lib/subscriptionAction.js new file mode 100644 index 0000000..b6fc8bb --- /dev/null +++ b/Front-End/src/app/lib/subscriptionAction.js @@ -0,0 +1,123 @@ +import { + BE_SUBSCRIPTION_STUDENTS_URL, + BE_SUBSCRIPTION_ARCHIVE_URL, + BE_SUBSCRIPTION_SEND_URL, + BE_SUBSCRIPTION_REGISTERFORM_URL, + BE_SUBSCRIPTION_REGISTERFORMS_URL, + BE_SUBSCRIPTION_REGISTERFORMFILE_TEMPLATE_URL +} from '@/utils/Url'; + +export const PENDING = 'pending'; +export const SUBSCRIBED = 'subscribed'; +export const ARCHIVED = 'archived'; + + +export const fetchRegisterForm = (type=PENDING, page='', pageSize='', search = '') => { + let url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${type}`; + if (page !== '' && pageSize !== '') { + url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${type}?page=${page}&search=${search}`; + } + return fetch(url, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(response => response.json()) + }; + +export const editRegisterForm=(id, data, csrfToken)=>{ + + return fetch(`${BE_SUBSCRIPTION_REGISTERFORM_URL}/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(data), + credentials: 'include' + }) + .then(response => response.json()) + +}; + +export const createRegisterForm=(data, csrfToken)=>{ + const url = `${BE_SUBSCRIPTION_REGISTERFORM_URL}`; + return fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(data), + credentials: 'include' + }) + .then(response => response.json()) +} + +export const archiveRegisterForm = (id) => { + const url = `${BE_SUBSCRIPTION_ARCHIVE_URL}/${id}`; + return fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }).then(response => response.json()) +} + +export const sendRegisterForm = (id) => { + const url = `${BE_SUBSCRIPTION_SEND_URL}/${id}`; + return fetch(url, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(response => response.json()) + +} + +export const fetchRegisterFormFileTemplate = () => { + const request = new Request( + `${BE_SUBSCRIPTION_REGISTERFORMFILE_TEMPLATE_URL}`, + { + method:'GET', + headers: { + 'Content-Type':'application/json' + }, + } + ); + return fetch(request).then(response => response.json()) +}; + +export const createRegisterFormFileTemplate = (data,csrfToken) => { + + fetch(`${BE_SUBSCRIPTION_REGISTERFORMFILE_TEMPLATE_URL}`, { + method: 'POST', + body: data, + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include', + }) + .then(response => response.json()) +} + +export const deleteRegisterFormFileTemplate = (fileId,csrfToken) => { + return fetch(`${BE_SUBSCRIPTION_REGISTERFORMFILE_TEMPLATE_URL}/${fileId}`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include', + }) +} +export const fetchStudents = () => { + const request = new Request( + `${BE_SUBSCRIPTION_STUDENTS_URL}`, + { + method:'GET', + headers: { + 'Content-Type':'application/json' + }, + } + ); + return fetch(request).then(response => response.json()) + +}; diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index 79d8edb..9414a19 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -4,20 +4,20 @@ import InputTextIcon from '@/components/InputTextIcon'; import ToggleSwitch from '@/components/ToggleSwitch'; import Button from '@/components/Button'; -const InscriptionForm = ( { eleves, onSubmit }) => { +const InscriptionForm = ( { students, onSubmit }) => { const [formData, setFormData] = useState({ - eleveNom: '', - elevePrenom: '', - responsableEmail: '', - responsableTel: '', - selectedResponsables: [], + studentLastName: '', + studentFirstName: '', + guardianEmail: '', + guardianPhone: '', + selectedGuardians: [], responsableType: 'new', autoMail: false }); const [step, setStep] = useState(1); - const [selectedEleve, setSelectedEleve] = useState(''); - const [existingResponsables, setExistingResponsables] = useState([]); + const [selectedStudent, setSelectedEleve] = useState(''); + const [existingGuardians, setExistingGuardians] = useState([]); const maxStep = 4 const handleToggleChange = () => { @@ -44,21 +44,21 @@ const InscriptionForm = ( { eleves, onSubmit }) => { } }; - const handleEleveSelection = (eleve) => { - setSelectedEleve(eleve); + const handleEleveSelection = (student) => { + setSelectedEleve(student); setFormData((prevData) => ({ ...prevData, - selectedResponsables: [] + selectedGuardians: [] })); - setExistingResponsables(eleve.responsables); + setExistingGuardians(student.guardians); }; - const handleResponsableSelection = (responsableId) => { + const handleResponsableSelection = (guardianId) => { setFormData((prevData) => { - const selectedResponsables = prevData.selectedResponsables.includes(responsableId) - ? prevData.selectedResponsables.filter(id => id !== responsableId) - : [...prevData.selectedResponsables, responsableId]; - return { ...prevData, selectedResponsables }; + const selectedGuardians = prevData.selectedGuardians.includes(guardianId) + ? prevData.selectedGuardians.filter(id => id !== guardianId) + : [...prevData.selectedGuardians, guardianId]; + return { ...prevData, selectedGuardians }; }); }; @@ -72,20 +72,20 @@ const InscriptionForm = ( { eleves, onSubmit }) => {

Nouvel élève

@@ -121,11 +121,11 @@ const InscriptionForm = ( { eleves, onSubmit }) => {
{formData.responsableType === 'new' && ( @@ -142,33 +142,33 @@ const InscriptionForm = ( { eleves, onSubmit }) => { - {eleves.map((eleve, index) => ( + {students.map((student, index) => ( handleEleveSelection(eleve)} + key={student.id} + className={`cursor-pointer ${selectedStudent && selectedStudent.id === student.id ? 'bg-emerald-600 text-white' : index % 2 === 0 ? 'bg-emerald-100' : ''}`} + onClick={() => handleEleveSelection(student)} > - {eleve.nom} - {eleve.prenom} + {student.last_name} + {student.first_name} ))} - {selectedEleve && ( + {selectedStudent && (
-

Responsables associés à {selectedEleve.nom} {selectedEleve.prenom} :

- {existingResponsables.map((responsable) => ( -
+

Responsables associés à {selectedStudent.last_name} {selectedStudent.first_name} :

+ {existingGuardians.map((guardian) => ( +
@@ -184,11 +184,11 @@ const InscriptionForm = ( { eleves, onSubmit }) => {

Téléphone (optionnel)

@@ -210,8 +210,8 @@ const InscriptionForm = ( { eleves, onSubmit }) => { - {formData.eleveNom} - {formData.elevePrenom} + {formData.studentLastName} + {formData.studentFirstName} @@ -228,15 +228,15 @@ const InscriptionForm = ( { eleves, onSubmit }) => { - {formData.responsableEmail} - {formData.responsableTel} + {formData.guardianEmail} + {formData.guardianPhone} )} - {formData.responsableType === 'existing' && selectedEleve && ( + {formData.responsableType === 'existing' && selectedStudent && (
-

Associé(s) à : {selectedEleve.nom} {selectedEleve.prenom}

+

Associé(s) à : {selectedStudent.nom} {selectedStudent.prenom}

@@ -246,11 +246,11 @@ const InscriptionForm = ( { eleves, onSubmit }) => { - {existingResponsables.filter(responsable => formData.selectedResponsables.includes(responsable.id)).map((responsable) => ( - - - - + {existingGuardians.filter(guardian => formData.selectedGuardians.includes(guardian.id)).map((guardian) => ( + + + + ))} @@ -281,15 +281,15 @@ const InscriptionForm = ( { eleves, onSubmit }) => {
{responsable.nom}{responsable.prenom}{responsable.mail}
{guardian.last_name}{guardian.first_name}{guardian.email}
{}} + /> + + + {/* Boutons de contrôle */}
@@ -48,14 +48,14 @@ export default function ResponsableInputFields({responsables, onResponsablesChan name="mailResponsable" type="email" label={t('email')} - value={item.mail} - onChange={(event) => {onResponsablesChange(item.id, "mail", event.target.value)}} + value={item.email} + onChange={(event) => {onGuardiansChange(item.id, "email", event.target.value)}} /> {onResponsablesChange(item.id, "telephone", event)}} + value={item.phone} + onChange={(event) => {onGuardiansChange(item.id, "phone", event)}} /> @@ -64,15 +64,15 @@ export default function ResponsableInputFields({responsables, onResponsablesChan name="dateNaissanceResponsable" type="date" label={t('birthdate')} - value={item.dateNaissance} - onChange={(event) => {onResponsablesChange(item.id, "dateNaissance", event.target.value)}} + value={item.birth_date} + onChange={(event) => {onGuardiansChange(item.id, "birth_date", event.target.value)}} /> {onResponsablesChange(item.id, "profession", event.target.value)}} + onChange={(event) => {onGuardiansChange(item.id, "profession", event.target.value)}} /> @@ -81,8 +81,8 @@ export default function ResponsableInputFields({responsables, onResponsablesChan name="adresseResponsable" type="text" label={t('address')} - value={item.adresse} - onChange={(event) => {onResponsablesChange(item.id, "adresse", event.target.value)}} + value={item.address} + onChange={(event) => {onGuardiansChange(item.id, "address", event.target.value)}} /> @@ -91,7 +91,7 @@ export default function ResponsableInputFields({responsables, onResponsablesChan
diff --git a/Front-End/src/components/Structure/Configuration/ClassForm.js b/Front-End/src/components/Structure/Configuration/ClassForm.js index f7e5729..8088b1a 100644 --- a/Front-End/src/components/Structure/Configuration/ClassForm.js +++ b/Front-End/src/components/Structure/Configuration/ClassForm.js @@ -13,7 +13,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { const { formData, setFormData } = useClasseForm(); const { getNiveauNameById, schoolYears, getNiveauxLabels, getNiveauxTabs, generateAgeToNiveaux, niveauxPremierCycle, niveauxSecondCycle, niveauxTroisiemeCycle, typeEmploiDuTemps, updatePlannings } = useClasses(); - const [selectedTeachers, setSelectedTeachers] = useState(formData.enseignants_ids); + const [selectedTeachers, setSelectedTeachers] = useState(formData.teachers); const handleTeacherSelection = (teacher) => { setSelectedTeachers(prevState => @@ -23,21 +23,21 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { ); setFormData(prevState => ({ ...prevState, - enseignants_ids: prevState.enseignants_ids.includes(teacher.id) - ? prevState.enseignants_ids.filter(id => id !== teacher.id) - : [...prevState.enseignants_ids, teacher.id] + teachers: prevState.teachers.includes(teacher.id) + ? prevState.teachers.filter(id => id !== teacher.id) + : [...prevState.teachers, teacher.id] })); }; const handleTimeChange = (e, index) => { const { value } = e.target; setFormData(prevState => { - const updatedTimes = [...prevState.plage_horaire]; + const updatedTimes = [...prevState.time_range]; updatedTimes[index] = value; const updatedFormData = { ...prevState, - plage_horaire: updatedTimes, + time_range: updatedTimes, }; const existingPlannings = prevState.plannings || []; @@ -53,12 +53,12 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { setFormData((prevState) => { const updatedJoursOuverture = checked - ? [...prevState.jours_ouverture, dayId] - : prevState.jours_ouverture.filter((id) => id !== dayId); + ? [...prevState.opening_days, dayId] + : prevState.opening_days.filter((id) => id !== dayId); const updatedFormData = { ...prevState, - jours_ouverture: updatedJoursOuverture, + opening_days: updatedJoursOuverture, }; const existingPlannings = prevState.plannings || []; @@ -82,7 +82,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { ? [...(prevState[name] || []), parseInt(value)] : (prevState[name] || []).filter(v => v !== parseInt(value)); newState[name] = newValues; - } else if (name === 'tranche_age') { + } else if (name === 'age_range') { const [minAgeStr, maxAgeStr] = value.split('-'); const minAge = minAgeStr ? parseInt(minAgeStr) : null; const maxAge = minAgeStr ? parseInt(maxAgeStr) : null; @@ -92,7 +92,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { newState = { ...prevState, [name]: value, - niveaux: selectedNiveaux.length > 0 ? selectedNiveaux : [], + levels: selectedNiveaux.length > 0 ? selectedNiveaux : [], }; } else if (type === 'radio') { newState[name] = parseInt(value, 10); @@ -111,7 +111,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { onSubmit(formData); }; - const [minAge, maxAge] = formData.tranche_age.length === 2 ? formData.tranche_age : [null, null]; + const [minAge, maxAge] = formData.age_range.length === 2 ? formData.age_range : [null, null]; const selectedAgeGroup = generateAgeToNiveaux(minAge, maxAge); return ( @@ -136,11 +136,11 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
@@ -156,7 +156,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { items={niveauxPremierCycle} formData={formData} handleChange={handleChange} - fieldName="niveaux" + fieldName="levels" labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))} className="w-full" /> @@ -164,7 +164,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { items={niveauxSecondCycle} formData={formData} handleChange={handleChange} - fieldName="niveaux" + fieldName="levels" labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))} className="w-full" /> @@ -172,7 +172,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { items={niveauxTroisiemeCycle} formData={formData} handleChange={handleChange} - fieldName="niveaux" + fieldName="levels" labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))} className="w-full" /> @@ -186,11 +186,11 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
@@ -202,9 +202,9 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
{ text={`${isNew ? "Créer" : "Modifier"}`} onClick={handleSubmit} className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${ - (formData.niveaux.length === 0 || !formData.annee_scolaire || !formData.nombre_eleves || formData.enseignants_ids.length === 0) + (formData.levels.length === 0 || !formData.school_year || !formData.number_of_students || formData.teachers.length === 0) ? "bg-gray-300 text-gray-700 cursor-not-allowed" : "bg-emerald-500 text-white hover:bg-emerald-600" }`} primary - disabled={(formData.niveaux.length === 0 || !formData.annee_scolaire || !formData.nombre_eleves || formData.enseignants_ids.length === 0)} + disabled={(formData.levels.length === 0 || !formData.school_year || !formData.number_of_students || formData.teachers.length === 0)} type="submit" name="Create" /> diff --git a/Front-End/src/components/Structure/Configuration/ClassesSection.js b/Front-End/src/components/Structure/Configuration/ClassesSection.js index 3baae8a..05adaea 100644 --- a/Front-End/src/components/Structure/Configuration/ClassesSection.js +++ b/Front-End/src/components/Structure/Configuration/ClassesSection.js @@ -11,7 +11,7 @@ import { ClasseFormProvider } from '@/context/ClasseFormContext'; import { useClasses } from '@/context/ClassesContext'; -const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleEdit, handleDelete }) => { +const ClassesSection = ({ classes, teachers, handleCreate, handleEdit, handleDelete }) => { const { getNiveauxLabels } = useClasses(); const [isOpen, setIsOpen] = useState(false); @@ -67,8 +67,8 @@ const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleE { name: 'AMBIANCE', transform: (row) => { - const ambiance = row.nom_ambiance ? row.nom_ambiance : ''; - const trancheAge = row.tranche_age ? `${row.tranche_age} ans` : ''; + const ambiance = row.atmosphere_name ? row.atmosphere_name : ''; + const trancheAge = row.age_range ? `${row.age_range} ans` : ''; if (ambiance && trancheAge) { return `${ambiance} (${trancheAge})`; @@ -84,11 +84,11 @@ const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleE { name: 'NIVEAUX', transform: (row) => { - const niveauxLabels = Array.isArray(row.niveaux) ? getNiveauxLabels(row.niveaux) : []; + const levelLabels = Array.isArray(row.levels) ? getNiveauxLabels(row.levels) : []; return (
- {niveauxLabels.length > 0 - ? niveauxLabels.map((label, index) => ( + {levelLabels.length > 0 + ? levelLabels.map((label, index) => ( )) : 'Aucun niveau'} @@ -96,19 +96,19 @@ const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleE ); } }, - { name: 'CAPACITÉ MAX', transform: (row) => row.nombre_eleves }, - { name: 'ANNÉE SCOLAIRE', transform: (row) => row.annee_scolaire }, + { name: 'CAPACITÉ MAX', transform: (row) => row.number_of_students }, + { name: 'ANNÉE SCOLAIRE', transform: (row) => row.school_year }, { name: 'ENSEIGNANTS', transform: (row) => (
- {row.enseignants.map((teacher, index) => ( - + {row.teachers_details.map((teacher, index) => ( + ))}
) }, - { name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee }, + { name: 'DATE DE CREATION', transform: (row) => row.updated_date_formatted }, { name: 'ACTIONS', transform: (row) => ( {editingClass ? ( <> - {editingClass.nom_ambiance} - {editingClass.tranche_age[0]} à {editingClass.tranche_age[1]} ans + {editingClass.nom_ambiance} - {editingClass.age_range[0]} à {editingClass.age_range[1]} ans ) : ''}
diff --git a/Front-End/src/components/Structure/Configuration/PlanningConfiguration.js b/Front-End/src/components/Structure/Configuration/PlanningConfiguration.js index 8af7942..8582362 100644 --- a/Front-End/src/components/Structure/Configuration/PlanningConfiguration.js +++ b/Front-End/src/components/Structure/Configuration/PlanningConfiguration.js @@ -15,7 +15,7 @@ const PlanningConfiguration = ({ formData, handleChange, handleTimeChange, handl ]; const isLabelAttenuated = (item) => { - return !formData.jours_ouverture.includes(parseInt(item.id)); + return !formData.opening_days.includes(parseInt(item.id)); }; return ( @@ -36,8 +36,8 @@ const PlanningConfiguration = ({ formData, handleChange, handleTimeChange, handl {/* Plage horaire */}
handleTimeChange(e, 0)} onEndChange={(e) => handleTimeChange(e, 1)} /> @@ -47,7 +47,7 @@ const PlanningConfiguration = ({ formData, handleChange, handleTimeChange, handl items={daysOfWeek} formData={formData} handleChange={handleJoursChange} - fieldName="jours_ouverture" + fieldName="opening_days" horizontal={true} labelAttenuated={isLabelAttenuated} /> diff --git a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js index 2cae280..8ec7b45 100644 --- a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js +++ b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js @@ -52,14 +52,14 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel transform: (row) => (
- {row.nom.toUpperCase()} + {row.name.toUpperCase()}
) }, - { name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee }, + { name: 'DATE DE CREATION', transform: (row) => row.updated_date_formatted }, { name: 'ACTIONS', transform: (row) => ( } diff --git a/Front-End/src/components/Structure/Configuration/SpecialityForm.js b/Front-End/src/components/Structure/Configuration/SpecialityForm.js index 5a767a2..39293eb 100644 --- a/Front-End/src/components/Structure/Configuration/SpecialityForm.js +++ b/Front-End/src/components/Structure/Configuration/SpecialityForm.js @@ -27,10 +27,10 @@ const SpecialityForm = ({ onSubmit, isNew }) => {
@@ -38,10 +38,10 @@ const SpecialityForm = ({ onSubmit, isNew }) => {
@@ -50,12 +50,12 @@ const SpecialityForm = ({ onSubmit, isNew }) => {
diff --git a/Front-End/src/components/Structure/Configuration/StructureManagement.js b/Front-End/src/components/Structure/Configuration/StructureManagement.js index 56d56ce..f1449e0 100644 --- a/Front-End/src/components/Structure/Configuration/StructureManagement.js +++ b/Front-End/src/components/Structure/Configuration/StructureManagement.js @@ -30,7 +30,6 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach handleCreate(`${BE_SCHOOL_SCHOOLCLASS_URL}`, newData, setClasses)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, updatedData, setClasses)} diff --git a/Front-End/src/components/Structure/Configuration/TeacherForm.js b/Front-End/src/components/Structure/Configuration/TeacherForm.js index 9d3d9d5..72d1510 100644 --- a/Front-End/src/components/Structure/Configuration/TeacherForm.js +++ b/Front-End/src/components/Structure/Configuration/TeacherForm.js @@ -10,7 +10,7 @@ const TeacherForm = ({ onSubmit, isNew, specialities }) => { const { formData, setFormData } = useTeacherForm(); const handleToggleChange = () => { - setFormData({ ...formData, droit: 1-formData.droit }); + setFormData({ ...formData, droit: 1-formData.droit.id }); }; const handleChange = (e) => { @@ -39,45 +39,45 @@ const TeacherForm = ({ onSubmit, isNew, specialities }) => { onSubmit(formData, isNew); }; - const getSpecialityLabel = (speciality) => { - return `${speciality.nom}`; + const getSpecialityLabel = (speciality) => { + return `${speciality.name}`; }; const isLabelAttenuated = (item) => { - return !formData.specialites_ids.includes(parseInt(item.id)); + return !formData.specialities.includes(parseInt(item.id)); }; return (
@@ -87,7 +87,7 @@ const TeacherForm = ({ onSubmit, isNew, specialities }) => { items={specialities} formData={formData} handleChange={handleChange} - fieldName="specialites_ids" + fieldName="specialities" label="Spécialités" icon={BookOpen} className="w-full mt-4" @@ -98,7 +98,7 @@ const TeacherForm = ({ onSubmit, isNew, specialities }) => {
@@ -106,12 +106,12 @@ const TeacherForm = ({ onSubmit, isNew, specialities }) => {
diff --git a/Front-End/src/components/Structure/Configuration/TeachersSection.js b/Front-End/src/components/Structure/Configuration/TeachersSection.js index e38194a..510cfb9 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSection.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSection.js @@ -4,12 +4,11 @@ import Table from '@/components/Table'; import DropdownMenu from '@/components/DropdownMenu'; import Modal from '@/components/Modal'; import TeacherForm from '@/components/Structure/Configuration/TeacherForm'; - import useCsrfToken from '@/hooks/useCsrfToken'; import { TeacherFormProvider } from '@/context/TeacherFormContext'; import { createProfile, updateProfile } from '@/app/lib/authAction'; -const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, specialities }) => { +const TeachersSection = ({ teachers, specialities , handleCreate, handleEdit, handleDelete}) => { const [isOpen, setIsOpen] = useState(false); const [editingTeacher, setEditingTeacher] = useState(null); @@ -30,12 +29,12 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe if (editingTeacher) { // Modification du profil const data = { - email: updatedData.mail, - username: updatedData.mail, + email: updatedData.email, + username: updatedData.email, droit:updatedData.droit } - updateProfile(updatedData.profilAssocie_id,data,csrfToken) + updateProfile(updatedData.associated_profile,data,csrfToken) .then(response => { console.log('Success:', response); console.log('UpdateData:', updatedData); @@ -50,9 +49,9 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe // Création d'un profil associé à l'adresse mail du responsable saisie // Le profil est inactif const data = { - email: updatedData.mail, + email: updatedData.email, password: 'Provisoire01!', - username: updatedData.mail, + username: updatedData.email, is_active: 1, // On rend le profil actif : on considère qu'au moment de la configuration de l'école un abonnement a été souscrit droit:updatedData.droit } @@ -62,7 +61,7 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe console.log('UpdateData:', updatedData); if (response.id) { let idProfil = response.id; - updatedData.profilAssocie_id = idProfil; + updatedData.associated_profile = idProfil; handleCreate(updatedData); } }) @@ -92,21 +91,21 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
row.nom }, - { name: 'PRENOM', transform: (row) => row.prenom }, - { name: 'MAIL', transform: (row) => row.mail }, + { name: 'NOM', transform: (row) => row.last_name }, + { name: 'PRENOM', transform: (row) => row.first_name }, + { name: 'MAIL', transform: (row) => row.email }, { name: 'SPÉCIALITÉS', transform: (row) => (
- {row.specialites.map(specialite => ( + {row.specialities_details.map((speciality,index) => ( - {specialite.nom} + {speciality.name} ))}
@@ -115,12 +114,12 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe { name: 'TYPE PROFIL', transform: (row) => { - if (row.profilAssocie) { - const badgeClass = row.DroitLabel === 'ECOLE' ? 'bg-blue-100 text-blue-600' : 'bg-red-100 text-red-600'; + if (row.associated_profile) { + const badgeClass = row.droit.label === 'ECOLE' ? 'bg-blue-100 text-blue-600' : 'bg-red-100 text-red-600'; return (
- {row.DroitLabel} + {row.droit.label}
); @@ -129,7 +128,7 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe } } }, - { name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee }, + { name: 'DATE DE CREATION', transform: (row) => row.updated_date_formatted }, { name: 'ACTIONS', transform: (row) => ( } diff --git a/Front-End/src/components/Structure/Configuration/TeachersSelectionConfiguration.js b/Front-End/src/components/Structure/Configuration/TeachersSelectionConfiguration.js index c749939..05969a5 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSelectionConfiguration.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSelectionConfiguration.js @@ -5,34 +5,34 @@ const TeachersSelectionConfiguration = ({ formData, teachers, handleTeacherSelec return (
- +
row.nom, + transform: (row) => row.last_name, }, { name: 'Prénom', - transform: (row) => row.prenom, - }, - { - name: 'Spécialités', - transform: (row) => ( -
- {row.specialites.map(specialite => ( - -
- {specialite.nom} -
- ))} -
- ), + transform: (row) => row.first_name, }, + // { + // name: 'Spécialités', + // transform: (row) => ( + //
+ // {row.specialites.map(specialite => ( + // + //
+ // {specialite.nom} + //
+ // ))} + //
+ // ), + // }, ]} data={teachers} onRowClick={handleTeacherSelection} diff --git a/Front-End/src/components/Structure/Planning/ClassesInformation.js b/Front-End/src/components/Structure/Planning/ClassesInformation.js index 5e90e03..05df701 100644 --- a/Front-End/src/components/Structure/Planning/ClassesInformation.js +++ b/Front-End/src/components/Structure/Planning/ClassesInformation.js @@ -9,11 +9,11 @@ const ClassesInformation = ({ selectedClass, isPastYear }) => { return (
-

{selectedClass.tranche_age} ans

+

{selectedClass.age_range} ans

- {selectedClass.enseignants.map((teacher) => ( + {selectedClass.teachers.map((teacher) => (
diff --git a/Front-End/src/components/Structure/Planning/ClassesList.js b/Front-End/src/components/Structure/Planning/ClassesList.js index dd9fd08..1cf7b5e 100644 --- a/Front-End/src/components/Structure/Planning/ClassesList.js +++ b/Front-End/src/components/Structure/Planning/ClassesList.js @@ -7,13 +7,13 @@ const ClassesList = ({ classes, onClassSelect, selectedClassId }) => { const currentSchoolYearStart = currentMonth >= 8 ? currentYear : currentYear - 1; const handleClassClick = (classe) => { - console.log(`Classe sélectionnée: ${classe.nom_ambiance}, Année scolaire: ${classe.annee_scolaire}`); + console.log(`Classe sélectionnée: ${classe.nom_ambiance}, Année scolaire: ${classe.school_year}`); onClassSelect(classe); }; const categorizedClasses = classes.reduce((acc, classe) => { - const { annee_scolaire } = classe; - const [startYear] = annee_scolaire.split('-').map(Number); + const { school_year } = classe; + const [startYear] = school_year.split('-').map(Number); const category = startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes'; if (!acc[category]) { @@ -45,7 +45,7 @@ const ClassesList = ({ classes, onClassSelect, selectedClassId }) => { style={{ maxWidth: '400px' }} >
{classe.nom_ambiance}
-
{classe.annee_scolaire}
+
{classe.school_year}
))}
@@ -63,7 +63,7 @@ const ClassesList = ({ classes, onClassSelect, selectedClassId }) => { style={{ maxWidth: '400px' }} >
{classe.nom_ambiance}
-
{classe.annee_scolaire}
+
{classe.school_year}
))}
diff --git a/Front-End/src/components/Structure/Planning/PlanningClassView.js b/Front-End/src/components/Structure/Planning/PlanningClassView.js index ed561ea..63ef1b0 100644 --- a/Front-End/src/components/Structure/Planning/PlanningClassView.js +++ b/Front-End/src/components/Structure/Planning/PlanningClassView.js @@ -79,7 +79,7 @@ const PlanningClassView = ({ schedule, onDrop, selectedLevel, handleUpdatePlanni const renderTimeSlots = () => { const timeSlots = []; - for (let hour = parseInt(formData.plage_horaire[0], 10); hour <= parseInt(formData.plage_horaire[1], 10); hour++) { + for (let hour = parseInt(formData.time_range[0], 10); hour <= parseInt(formData.time_range[1], 10); hour++) { const hourString = hour.toString().padStart(2, '0'); timeSlots.push( diff --git a/Front-End/src/components/Structure/Planning/ScheduleManagement.js b/Front-End/src/components/Structure/Planning/ScheduleManagement.js index eafeb45..f90c666 100644 --- a/Front-End/src/components/Structure/Planning/ScheduleManagement.js +++ b/Front-End/src/components/Structure/Planning/ScheduleManagement.js @@ -22,7 +22,7 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => { const [schedule, setSchedule] = useState(null); const { getNiveauxTabs } = useClasses(); - const niveauxLabels = Array.isArray(selectedClass?.niveaux) ? getNiveauxTabs(selectedClass.niveaux) : []; + const niveauxLabels = Array.isArray(selectedClass?.levels) ? getNiveauxTabs(selectedClass.levels) : []; const [isModalOpen, setIsModalOpen] = useState(false); const handleOpenModal = () => setIsModalOpen(true); @@ -94,8 +94,8 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => { }; const categorizedClasses = classes.reduce((acc, classe) => { - const { annee_scolaire } = classe; - const [startYear] = annee_scolaire.split('-').map(Number); + const { school_year } = classe; + const [startYear] = school_year.split('-').map(Number); const category = startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes'; if (!acc[category]) { @@ -153,7 +153,7 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => { Spécialités
- + diff --git a/Front-End/src/components/Structure/Planning/SpecialityEventModal.js b/Front-End/src/components/Structure/Planning/SpecialityEventModal.js index 913d622..bd4b74e 100644 --- a/Front-End/src/components/Structure/Planning/SpecialityEventModal.js +++ b/Front-End/src/components/Structure/Planning/SpecialityEventModal.js @@ -68,7 +68,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha } // Transformer eventData pour correspondre au format du planning - const selectedTeacherData = formData.enseignants.find(teacher => teacher.id === parseInt(eventData.teacherId, 10)); + const selectedTeacherData = formData.teachers.find(teacher => teacher.id === parseInt(eventData.teacherId, 10)); const newCourse = { color: '#FF0000', // Vous pouvez définir la couleur de manière dynamique si nécessaire teachers: selectedTeacherData ? [`${selectedTeacherData.nom} ${selectedTeacherData.prenom}`] : [], @@ -121,10 +121,10 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha }; const filteredTeachers = selectedSpeciality - ? formData.enseignants.filter(teacher => - teacher.specialites_ids.includes(parseInt(selectedSpeciality, 10)) + ? formData.teachers.filter(teacher => + teacher.specialites.includes(parseInt(selectedSpeciality, 10)) ) - : formData.enseignants; + : formData.teachers; const handleSpecialityChange = (e) => { const specialityId = e.target.value; @@ -164,7 +164,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha selected={selectedSpeciality} choices={[ { value: '', label: 'Sélectionner une spécialité' }, - ...groupSpecialitiesBySubject(formData.enseignants).map((speciality) => ({ + ...groupSpecialitiesBySubject(formData.teachers).map((speciality) => ({ value: speciality.id, label: speciality.nom })) diff --git a/Front-End/src/components/ToggleSwitch.js b/Front-End/src/components/ToggleSwitch.js index f13147b..9084bff 100644 --- a/Front-End/src/components/ToggleSwitch.js +++ b/Front-End/src/components/ToggleSwitch.js @@ -20,7 +20,7 @@ const ToggleSwitch = ({ label, checked, onChange }) => { id="toggle" checked={checked} onChange={handleChange} - className="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer border-emerald-500 checked:right-0 checked:border-emerald-500 checked:bg-emerald-500 hover:border-emerald-500 hover:bg-emerald-500 focus:outline-none focus:ring-0" + className="hover:text-emerald-500 absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer border-emerald-500 checked:right-0 checked:border-emerald-500 checked:bg-emerald-500 hover:border-emerald-500 hover:bg-emerald-500 focus:outline-none focus:ring-0" ref={inputRef} // Reference to the input element />
- - ); -} \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index 0a1f2f3..c48ed4b 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -4,8 +4,8 @@ import React, { useState, useEffect } from 'react'; import { useTranslations } from 'next-intl'; import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react'; import Loader from '@/components/Loader'; -import { BE_SCHOOL_SCHOOLCLASSES_URL } from '@/utils/Url'; import ClasseDetails from '@/components/ClasseDetails'; +import { fetchClasses } from '@/app/lib/schoolAction'; // Composant StatCard pour afficher une statistique const StatCard = ({ title, value, icon, change, color = "blue" }) => ( @@ -58,20 +58,15 @@ export default function DashboardPage() { const [classes, setClasses] = useState([]); - const fetchClasses = () => { - fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}`) - .then(response => response.json()) - .then(data => { + + useEffect(() => { + // Fetch data for classes + fetchClasses().then(data => { setClasses(data); }) .catch(error => { console.error('Error fetching classes:', error); }); - }; - - useEffect(() => { - // Fetch data for classes - fetchClasses(); // Simulation de chargement des données setTimeout(() => { diff --git a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js index 8de7262..9f7cab6 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js @@ -1,6 +1,6 @@ 'use client' import React, { useState, useEffect } from 'react'; -import { useSearchParams } from 'next/navigation'; +import { useSearchParams, useRouter } from 'next/navigation'; import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared'; import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url'; import useCsrfToken from '@/hooks/useCsrfToken'; @@ -10,6 +10,7 @@ import { editRegisterForm, fetchRegisterForm } from '@/app/lib/subscriptionActio const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; export default function Page() { + const router = useRouter(); const searchParams = useSearchParams(); const idProfil = searchParams.get('id'); const studentId = searchParams.get('studentId'); // Changé de codeDI à studentId @@ -21,50 +22,44 @@ export default function Page() { useEffect(() => { if (useFakeData) { setInitialData(mockStudent); - setIsLoading(false); } else { fetchRegisterForm(studentId) .then(data => { console.log('Fetched data:', data); // Pour le débogage const formattedData = { - id: data.id, - last_name: data.last_name, - first_name: data.first_name, - address: data.address, - birth_date: data.birth_date, - birth_place: data.birth_place, - birth_postal_code: data.birth_postal_code, - nationality: data.nationality, - attending_physician: data.attending_physician, - level: data.level, + ...data, guardians: data.guardians || [] }; setInitialData(formattedData); - setIsLoading(false); }) .catch(error => { console.error('Error fetching student data:', error); - setIsLoading(false); }); } + setIsLoading(false); }, [studentId]); // Dépendance changée à studentId - const handleSubmit = async (data) => { + const handleSubmit = (data) => { if (useFakeData) { console.log('Fake submit:', data); return; } - try { - const result = await editRegisterForm(studentId, data, csrfToken); - // Utilisation de studentId + editRegisterForm(studentId, data, csrfToken) + + .then((result) => { console.log('Success:', result); - // Redirection après succès - window.location.href = FE_ADMIN_SUBSCRIPTIONS_URL; - } catch (error) { - console.error('Error:', error); + router.push(FE_ADMIN_SUBSCRIPTIONS_URL); + }) + .catch((error) => { + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + // Handle form errors (e.g., display them to the user) + } alert('Une erreur est survenue lors de la mise à jour des données'); - } + }); + }; return ( diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index cd8aa87..10a865c 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -27,6 +27,7 @@ import { archiveRegisterForm, fetchRegisterFormFileTemplate, deleteRegisterFormFileTemplate, + createRegistrationFormFileTemplate, fetchStudents, editRegisterForm } from "@/app/lib/subscriptionAction" @@ -44,16 +45,14 @@ const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; export default function Page({ params: { locale } }) { const t = useTranslations('subscriptions'); - const [ficheInscriptions, setFicheInscriptions] = useState([]); - const [fichesInscriptionsDataEnCours, setFichesInscriptionsDataEnCours] = useState([]); - const [fichesInscriptionsDataInscrits, setFichesInscriptionsDataInscrits] = useState([]); - const [fichesInscriptionsDataArchivees, setFichesInscriptionsDataArchivees] = useState([]); + const [registrationForms, setRegistrationForms] = useState([]); + const [registrationFormsDataPending, setRegistrationFormsDataPending] = useState([]); + const [registrationFormsDataSubscribed, setRegistrationFormsDataSubscribed] = useState([]); + const [registrationFormsDataArchived, setRegistrationFormsDataArchived] = useState([]); // const [filter, setFilter] = useState('*'); const [searchTerm, setSearchTerm] = useState(''); const [alertPage, setAlertPage] = useState(false); - const [mailSent, setMailSent] = useState(false); - const [ficheArchivee, setFicheArchivee] = useState(false); - const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState(false); const [popup, setPopup] = useState({ visible: false, message: '', onConfirm: null }); const [activeTab, setActiveTab] = useState('pending'); const [currentPage, setCurrentPage] = useState(1); @@ -64,13 +63,12 @@ export default function Page({ params: { locale } }) { const [itemsPerPage, setItemsPerPage] = useState(5); // Définir le nombre d'éléments par page const [fichiers, setFichiers] = useState([]); - - const [isOpen, setIsOpen] = useState(false); const [isOpenAffectationClasse, setIsOpenAffectationClasse] = useState(false); const [student, setStudent] = useState(''); const [classes, setClasses] = useState([]); const [students, setEleves] = useState([]); + const [reloadFetch, setReloadFetch] = useState(false); const csrfToken = useCsrfToken(); @@ -80,6 +78,7 @@ export default function Page({ params: { locale } }) { const closeModal = () => { setIsOpen(false); + } const openModalAssociationEleve = (eleveSelected) => { @@ -88,58 +87,72 @@ export default function Page({ params: { locale } }) { } const requestErrorHandler = (err)=>{ - setIsLoading(false); + console.error('Error fetching data:', err); } + /** + * Handles the pending data for the registration form. + * + * @param {Object} data - The data object containing registration forms and count. + * @param {Array} data.registerForms - The array of registration forms. + * @param {number} data.count - The total count of registration forms. + */ const registerFormPendingDataHandler = (data) => { - setIsLoading(false); if (data) { - const { registerForms, count } = data; + const { registerForms, count, page_size } = data; if (registerForms) { - setFichesInscriptionsDataEnCours(registerForms); + setRegistrationFormsDataPending(registerForms); } - const calculatedTotalPages = count === 0 ? 1 : Math.ceil(count / pageSize); + const calculatedTotalPages = count === 0 ? 1 : Math.ceil(count / page_size); setTotalPending(count); setTotalPages(calculatedTotalPages); } } + + + /** + * Handles the data received from the subscription registration form. + * + * @param {Object} data - The data object received from the subscription registration form. + * @param {Array} data.registerForms - An array of registration forms. + * @param {number} data.count - The total count of subscribed forms. + */ const registerFormSubscribedDataHandler = (data) => { - setIsLoading(false); if (data) { - const { registerForms, count } = data; + const { registerForms, count, page_size } = data; setTotalSubscribed(count); if (registerForms) { - setFichesInscriptionsDataInscrits(registerForms); + setRegistrationFormsDataSubscribed(registerForms); } } } + +/** + * Handles the archived data for the register form. + * + * @param {Object} data - The data object containing archived register forms and count. + * @param {Array} data.registerForms - The array of archived register forms. + * @param {number} data.count - The total count of archived register forms. + */ const registerFormArchivedDataHandler = (data) => { - setIsLoading(false); if (data) { - const { registerForms, count } = data; + const { registerForms, count, page_size } = data; + setTotalArchives(count); if (registerForms) { - setFichesInscriptionsDataArchivees(registerForms); + setRegistrationFormsDataArchived(registerForms); } } } - - - - - useEffect(() => { - fetchRegisterFormFileTemplate() - .then((data)=> {setFichiers(data)}) - .catch((err)=>{ err = err.message; console.log(err);}); - }, []); +// TODO: revoir le système de pagination et de UseEffect useEffect(() => { fetchClasses() .then(data => { setClasses(data); - console.log("Success CLASSES : ", data) + console.log('Success Classes:', data); }) .catch(error => { console.error('Error fetching classes:', error); @@ -154,10 +167,13 @@ const registerFormArchivedDataHandler = (data) => { error = error.message; console.log(error); }); - }, [fichesInscriptionsDataEnCours]); + }, [registrationFormsDataPending]); + useEffect(() => { const fetchDataAndSetState = () => { + + setIsLoading(true); if (!useFakeData) { fetchRegisterForms(PENDING, currentPage, itemsPerPage, searchTerm) .then(registerFormPendingDataHandler) @@ -168,30 +184,73 @@ const registerFormArchivedDataHandler = (data) => { fetchRegisterForms(ARCHIVED) .then(registerFormArchivedDataHandler) .catch(requestErrorHandler) + fetchRegisterFormFileTemplate() + .then((data)=> {setFichiers(data)}) + .catch((err)=>{ err = err.message; console.log(err);}); } else { setTimeout(() => { - setFichesInscriptionsDataEnCours(mockFicheInscription); - setIsLoading(false); + setRegistrationFormsDataPending(mockFicheInscription); }, 1000); } - setFicheArchivee(false); - setMailSent(false); + setIsLoading(false); + setReloadFetch(false); }; + fetchDataAndSetState(); - }, [mailSent, ficheArchivee, currentPage, itemsPerPage]); + }, [reloadFetch, currentPage]); - // Modifier le useEffect pour la recherche - useEffect(() => { - const timeoutId = setTimeout(() => { - fetchRegisterForms(PENDING, currentPage, itemsPerPage, searchTerm) - .then(registerFormPendingDataHandler) - .catch(requestErrorHandler) +useEffect(() => { + const fetchDataAndSetState = () => { + + setIsLoading(true); + if (!useFakeData) { + fetchRegisterForms(PENDING, currentPage, itemsPerPage, searchTerm) + .then(registerFormPendingDataHandler) + .catch(requestErrorHandler) + fetchRegisterForms(SUBSCRIBED) + .then(registerFormSubscribedDataHandler) + .catch(requestErrorHandler) + fetchRegisterForms(ARCHIVED) + .then(registerFormArchivedDataHandler) + .catch(requestErrorHandler) + fetchRegisterFormFileTemplate() + .then((data)=> {setFichiers(data)}) + .catch((err)=>{ err = err.message; console.log(err);}); + } else { + setTimeout(() => { + setRegistrationFormsDataPending(mockFicheInscription); + }, 1000); + } + setIsLoading(false); + setReloadFetch(false); + }; + +const timeoutId = setTimeout(() => { + fetchDataAndSetState(); }, 500); // Debounce la recherche - return () => clearTimeout(timeoutId); - }, [searchTerm, currentPage, itemsPerPage]); +}, [searchTerm]); +/** + * UseEffect to update page count of tab + */ +useEffect(()=>{ + if (activeTab === 'pending') { + setTotalPages(Math.ceil(totalPending / itemsPerPage)); + } else if (activeTab === 'subscribed') { + setTotalPages(Math.ceil(totalSubscribed / itemsPerPage)); + } else if (activeTab === 'archived') { + setTotalPages(Math.ceil(totalArchives / itemsPerPage)); + } +},[currentPage]); + /** + * Archives a registration form after user confirmation. + * + * @param {number} id - The ID of the registration form to be archived. + * @param {string} nom - The last name of the person whose registration form is being archived. + * @param {string} prenom - The first name of the person whose registration form is being archived. + */ const archiveFicheInscription = (id, nom, prenom) => { setPopup({ visible: true, @@ -200,8 +259,8 @@ const registerFormArchivedDataHandler = (data) => { archiveRegisterForm(id) .then(data => { console.log('Success:', data); - setFicheInscriptions(ficheInscriptions.filter(fiche => fiche.id !== id)); - setFicheArchivee(true); + setRegistrationForms(registrationForms.filter(fiche => fiche.id !== id)); + setReloadFetch(true); alert("Le dossier d'inscription a été correctement archivé"); }) .catch(error => { @@ -219,7 +278,7 @@ const registerFormArchivedDataHandler = (data) => { onConfirm: () => { sendRegisterForm(id).then(data => { console.log('Success:', data); - setMailSent(true); + setReloadFetch(true); }) .catch(error => { console.error('Error fetching data:', error); @@ -227,15 +286,18 @@ const registerFormArchivedDataHandler = (data) => { } }); }; + const affectationClassFormSubmitHandler = (formdata)=> { editRegisterForm(student.id,formData, csrfToken) .then(data => { console.log('Success:', data); + setReloadFetch(true); }) .catch(error => { console.error('Error :', error); }); } + const updateStatusAction = (id, newStatus) => { console.log('Edit fiche inscription with id:', id); }; @@ -246,11 +308,11 @@ const registerFormArchivedDataHandler = (data) => { const handlePageChange = (newPage) => { setCurrentPage(newPage); - fetchData(newPage, itemsPerPage); // Appeler fetchData directement ici }; const createRF = (updatedData) => { - console.log("updateDATA",updatedData); + console.log('createRF updatedData:', updatedData); + if (updatedData.selectedGuardians.length !== 0) { const selectedGuardiansIds = updatedData.selectedGuardians.map(guardianId => guardianId) @@ -263,10 +325,9 @@ const registerFormArchivedDataHandler = (data) => { }; createRegisterForm(data,csrfToken) - .then(response => response.json()) .then(data => { console.log('Success:', data); - setFichesInscriptionsDataEnCours(prevState => { + setRegistrationFormsDataPending(prevState => { if (prevState) { return [...prevState, data]; } @@ -291,7 +352,7 @@ const registerFormArchivedDataHandler = (data) => { is_active: 0, // On rend le profil inactif : impossible de s'y connecter dans la fenêtre du login tant qu'il ne s'est pas inscrit droit:2 // Profil PARENT } - createProfile(data,csrfToken) + createProfile(data,csrfToken) .then(response => { console.log('Success:', response); if (response.id) { @@ -315,7 +376,7 @@ const registerFormArchivedDataHandler = (data) => { createRegisterForm(data,csrfToken) .then(data => { console.log('Success:', data); - setFichesInscriptionsDataEnCours(prevState => { + setRegistrationFormsDataPending(prevState => { if (prevState && prevState.length > 0) { return [...prevState, data]; } @@ -338,6 +399,7 @@ const registerFormArchivedDataHandler = (data) => { }); } closeModal(); + setReloadFetch(true); } @@ -349,10 +411,10 @@ const columns = [ { 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) => ( -
- updateStatusAction(row.student.id, newStatus)} showDropdown={false} /> -
- ) +
+ updateStatusAction(row.student.id, newStatus)} showDropdown={false} /> +
+ ) }, { name: t('files'), transform: (row) => (row.registerForms != null) &&( @@ -431,7 +493,7 @@ const columnsSubscribed = [ { name: t('registrationFileStatus'), transform: (row) => (
- updateStatusAction(row.student.id, newStatus)} showDropdown={false} /> + updateStatusAction(row.student.id, newStatus)} showDropdown={false} />
) }, @@ -513,10 +575,10 @@ const handleFileUpload = (file, fileName) => { const formData = new FormData(); if(file){ - formData.append('fichier', file); + formData.append('file', file); } - formData.append('nom', fileName); - createRegisterFormFileTemplate(formData,csrfToken) + formData.append('name', fileName); + createRegistrationFormFileTemplate(formData,csrfToken) .then(data => { console.log('Success:', data); setFichiers([...fichiers, data]); @@ -530,7 +592,7 @@ const handleFileUpload = (file, fileName) => { if (isLoading) { return ; } else { - if (ficheInscriptions.length === 0 && fichesInscriptionsDataArchivees.length === 0 && alertPage) { + if (registrationForms.length === 0 && registrationFormsDataArchived.length === 0 && alertPage) { return (
{ key={`${currentPage}-${searchTerm}`} data={ activeTab === 'pending' - ? fichesInscriptionsDataEnCours + ? registrationFormsDataPending : activeTab === 'subscribed' - ? fichesInscriptionsDataInscrits - : fichesInscriptionsDataArchivees + ? registrationFormsDataSubscribed + : registrationFormsDataArchived } columns={ activeTab === 'subscribed' diff --git a/Front-End/src/app/[locale]/admin/teachers/page.js b/Front-End/src/app/[locale]/admin/teachers/page.js deleted file mode 100644 index 0f3b478..0000000 --- a/Front-End/src/app/[locale]/admin/teachers/page.js +++ /dev/null @@ -1,23 +0,0 @@ -'use client' -import React, { useState, useEffect } from 'react'; -import Button from '@/components/Button'; -import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus } from 'lucide-react'; -import Modal from '@/components/Modal'; - -export default function Page() { - - const [isOpen, setIsOpen] = useState(false); - - const openModal = () => { - setIsOpen(true); - } - - -return ( -
- -
-); -} \ No newline at end of file diff --git a/Front-End/src/app/[locale]/parents/editInscription/page.js b/Front-End/src/app/[locale]/parents/editInscription/page.js index 08b2d4a..7e72781 100644 --- a/Front-End/src/app/[locale]/parents/editInscription/page.js +++ b/Front-End/src/app/[locale]/parents/editInscription/page.js @@ -3,11 +3,9 @@ import React, { useState, useEffect } from 'react'; import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared'; import { useSearchParams, redirect, useRouter } from 'next/navigation'; import useCsrfToken from '@/hooks/useCsrfToken'; -import { FE_PARENTS_HOME_URL, - BE_SUBSCRIPTION_STUDENT_URL, - BE_SUBSCRIPTION_REGISTERFORM_URL, - BE_SUBSCRIPTION_LAST_GUARDIAN_URL } from '@/utils/Url'; +import { FE_PARENTS_HOME_URL} from '@/utils/Url'; import { mockStudent } from '@/data/mockStudent'; +import { fetchLastGuardian, fetchRegisterForm } from '@/app/lib/subscriptionAction'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; @@ -21,7 +19,7 @@ export default function Page() { const [isLoading, setIsLoading] = useState(true); const csrfToken = useCsrfToken(); const [currentProfil, setCurrentProfil] = useState(""); - const [lastIdResponsable, setLastIdResponsable] = useState(1); + const [lastGuardianId, setLastGuardianId] = useState(1); useEffect(() => { if (!studentId || !idProfil) { @@ -31,37 +29,25 @@ export default function Page() { if (useFakeData) { setInitialData(mockStudent); - setLastIdResponsable(999); + setLastGuardianId(999); setIsLoading(false); } else { Promise.all([ // Fetch eleve data - fetch(`${BE_SUBSCRIPTION_STUDENT_URL}/${studentId}`), - // Fetch last responsable ID - fetch(BE_SUBSCRIPTION_LAST_GUARDIAN_URL) + fetchRegisterForm(studentId), + // Fetch last guardian ID + fetchLastGuardian() ]) - .then(async ([eleveResponse, responsableResponse]) => { - const eleveData = await eleveResponse.json(); - const responsableData = await responsableResponse.json(); - - const formattedData = { - id: eleveData.id, - nom: eleveData.nom, - prenom: eleveData.prenom, - adresse: eleveData.adresse, - dateNaissance: eleveData.dateNaissance, - lieuNaissance: eleveData.lieuNaissance, - codePostalNaissance: eleveData.codePostalNaissance, - nationalite: eleveData.nationalite, - medecinTraitant: eleveData.medecinTraitant, - niveau: eleveData.niveau, - responsables: eleveData.responsables || [] + .then(async ([studentData, guardianData]) => { + const formattedData = { + ...studentData, + guardians: studentData.guardians || [] }; setInitialData(formattedData); - setLastIdResponsable(responsableData.lastid); + setLastGuardianId(guardianData.lastid); - let profils = eleveData.profils; + let profils = studentData.profils; const currentProf = profils.find(profil => profil.id === idProfil); if (currentProf) { setCurrentProfil(currentProf); @@ -83,17 +69,8 @@ export default function Page() { } try { - const response = await fetch(`${BE_SUBSCRIPTION_REGISTERFORM_URL}/${studentId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken - }, - credentials: 'include', - body: JSON.stringify(data), - }); - const result = await response.json(); + const result = await editRegisterForm(studentId, data, csrfToken); console.log('Success:', result); router.push(FE_PARENTS_HOME_URL); } catch (error) { diff --git a/Front-End/src/app/[locale]/parents/page.js b/Front-End/src/app/[locale]/parents/page.js index 6c19b54..695ed33 100644 --- a/Front-End/src/app/[locale]/parents/page.js +++ b/Front-End/src/app/[locale]/parents/page.js @@ -67,7 +67,7 @@ export default function ParentHomePage() {

{child.student.last_name} {child.student.first_name}

handleEdit(child.student.id)} />
- + ))} diff --git a/Front-End/src/app/lib/authAction.js b/Front-End/src/app/lib/authAction.js index 9cbc41f..f84b6af 100644 --- a/Front-End/src/app/lib/authAction.js +++ b/Front-End/src/app/lib/authAction.js @@ -12,6 +12,20 @@ import {mockUser} from "@/data/mockUsersData"; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; + +const requestResponseHandler = async (response) => { + + const body = await response.json(); + if (response.ok) { + return body; + } + // Throw an error with the JSON body containing the form errors + const error = new Error('Form submission error'); + error.details = body; + throw error; +} + + export const login = (data, csrfToken) => { const request = new Request( `${BE_AUTH_LOGIN_URL}`, @@ -25,7 +39,7 @@ export const login = (data, csrfToken) => { credentials: 'include', } ); - return fetch(request).then(response => response.json()) + return fetch(request).then(requestResponseHandler) } /** @@ -64,7 +78,7 @@ const request = new Request( body: JSON.stringify(data), } ); - return fetch(request).then(response => response.json()) + return fetch(request).then(requestResponseHandler) } export const updateProfile = (id, data, csrfToken) => { @@ -80,7 +94,7 @@ export const updateProfile = (id, data, csrfToken) => { body: JSON.stringify(data), } ); - return fetch(request).then(response => response.json()) + return fetch(request).then(requestResponseHandler) } export const sendNewPassword = (data, csrfToken) => { @@ -97,7 +111,7 @@ export const sendNewPassword = (data, csrfToken) => { body: JSON.stringify(data), } ); - return fetch(request).then(response => response.json()) + return fetch(request).then(requestResponseHandler) } export const subscribe = (data,csrfToken) =>{ @@ -113,7 +127,7 @@ export const subscribe = (data,csrfToken) =>{ body: JSON.stringify( data), } ); - return fetch(request).then(response => response.json()) + return fetch(request).then(requestResponseHandler) } export const resetPassword = (uuid, data, csrfToken) => { @@ -129,7 +143,7 @@ export const resetPassword = (uuid, data, csrfToken) => { body: JSON.stringify(data), } ); - return fetch(request).then(response => response.json()) + return fetch(request).then(requestResponseHandler) } export const getResetPassword = (uuid) => { @@ -138,5 +152,5 @@ export const getResetPassword = (uuid) => { headers: { 'Content-Type': 'application/json', }, - }).then(response => response.json()) + }).then(requestResponseHandler) } \ No newline at end of file diff --git a/Front-End/src/app/lib/schoolAction.js b/Front-End/src/app/lib/schoolAction.js index 29080e1..6ee39ba 100644 --- a/Front-End/src/app/lib/schoolAction.js +++ b/Front-End/src/app/lib/schoolAction.js @@ -5,22 +5,35 @@ import { BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url'; +const requestResponseHandler = async (response) => { + + const body = await response.json(); + if (response.ok) { + return body; + } + // Throw an error with the JSON body containing the form errors + const error = new Error('Form submission error'); + error.details = body; + throw error; +} + + export const fetchSpecialities = () => { return fetch(`${BE_SCHOOL_SPECIALITIES_URL}`) - .then(response => response.json()) + .then(requestResponseHandler) }; export const fetchTeachers = () => { return fetch(`${BE_SCHOOL_TEACHERS_URL}`) - .then(response => response.json()) + .then(requestResponseHandler) }; export const fetchClasses = () => { return fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}`) - .then(response => response.json()) + .then(requestResponseHandler) }; export const fetchSchedules = () => { return fetch(`${BE_SCHOOL_PLANNINGS_URL}`) - .then(response => response.json()) + .then(requestResponseHandler) }; \ No newline at end of file diff --git a/Front-End/src/app/lib/subscriptionAction.js b/Front-End/src/app/lib/subscriptionAction.js index 2066e59..7adabb5 100644 --- a/Front-End/src/app/lib/subscriptionAction.js +++ b/Front-End/src/app/lib/subscriptionAction.js @@ -6,7 +6,8 @@ import { BE_SUBSCRIPTION_CHILDRENS_URL, BE_SUBSCRIPTION_REGISTERFORM_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL, - BE_SUBSCRIPTION_REGISTERFORMFILE_TEMPLATE_URL + BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL, + BE_SUBSCRIPTION_LAST_GUARDIAN_URL } from '@/utils/Url'; export const PENDING = 'pending'; @@ -14,6 +15,18 @@ export const SUBSCRIBED = 'subscribed'; export const ARCHIVED = 'archived'; +const requestResponseHandler = async (response) => { + + const body = await response.json(); + if (response.ok) { + return body; + } + // Throw an error with the JSON body containing the form errors + const error = new Error('Form submission error'); + error.details = body; + throw error; +} + export const fetchRegisterForms = (type=PENDING, page='', pageSize='', search = '') => { let url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${type}`; if (page !== '' && pageSize !== '') { @@ -23,12 +36,16 @@ export const fetchRegisterForms = (type=PENDING, page='', pageSize='', search = headers: { 'Content-Type': 'application/json', }, - }).then(response => response.json()) + }).then(requestResponseHandler) }; export const fetchRegisterForm = (id) =>{ - return fetch(`${BE_SUBSCRIPTION_STUDENT_URL}/${id}`) // Utilisation de studentId au lieu de codeDI - .then(response => response.json()) + return fetch(`${BE_SUBSCRIPTION_STUDENT_URL}/${id}`) // Utilisation de studentId au lieu de codeDI + .then(requestResponseHandler) +} +export const fetchLastGuardian = () =>{ + return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_URL}`) + .then(requestResponseHandler) } export const editRegisterForm=(id, data, csrfToken)=>{ @@ -42,7 +59,7 @@ export const editRegisterForm=(id, data, csrfToken)=>{ body: JSON.stringify(data), credentials: 'include' }) - .then(response => response.json()) + .then(requestResponseHandler) }; @@ -57,7 +74,7 @@ export const createRegisterForm=(data, csrfToken)=>{ body: JSON.stringify(data), credentials: 'include' }) - .then(response => response.json()) + .then(requestResponseHandler) } export const archiveRegisterForm = (id) => { @@ -67,7 +84,7 @@ export const archiveRegisterForm = (id) => { headers: { 'Content-Type': 'application/json', }, - }).then(response => response.json()) + }).then(requestResponseHandler) } export const sendRegisterForm = (id) => { @@ -76,13 +93,13 @@ export const sendRegisterForm = (id) => { headers: { 'Content-Type': 'application/json', }, - }).then(response => response.json()) + }).then(requestResponseHandler) } export const fetchRegisterFormFileTemplate = () => { const request = new Request( - `${BE_SUBSCRIPTION_REGISTERFORMFILE_TEMPLATE_URL}`, + `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, { method:'GET', headers: { @@ -90,12 +107,12 @@ export const fetchRegisterFormFileTemplate = () => { }, } ); - return fetch(request).then(response => response.json()) + return fetch(request).then(requestResponseHandler) }; -export const createRegisterFormFileTemplate = (data,csrfToken) => { +export const createRegistrationFormFileTemplate = (data,csrfToken) => { - fetch(`${BE_SUBSCRIPTION_REGISTERFORMFILE_TEMPLATE_URL}`, { + return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, { method: 'POST', body: data, headers: { @@ -103,11 +120,11 @@ export const createRegisterFormFileTemplate = (data,csrfToken) => { }, credentials: 'include', }) - .then(response => response.json()) + .then(requestResponseHandler) } export const deleteRegisterFormFileTemplate = (fileId,csrfToken) => { - return fetch(`${BE_SUBSCRIPTION_REGISTERFORMFILE_TEMPLATE_URL}/${fileId}`, { + return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}/${fileId}`, { method: 'DELETE', headers: { 'X-CSRFToken': csrfToken, @@ -125,7 +142,7 @@ export const fetchStudents = () => { }, } ); - return fetch(request).then(response => response.json()) + return fetch(request).then(requestResponseHandler) }; @@ -139,5 +156,5 @@ export const fetchChildren = (id) =>{ }, } ); - return fetch(request).then(response => response.json()) + return fetch(request).then(requestResponseHandler) } \ No newline at end of file diff --git a/Front-End/src/components/StatusLabel.js b/Front-End/src/components/StatusLabel.js index 31e3e33..142c0e2 100644 --- a/Front-End/src/components/StatusLabel.js +++ b/Front-End/src/components/StatusLabel.js @@ -2,7 +2,7 @@ import { useState } from 'react'; import { ChevronUp } from 'lucide-react'; import DropdownMenu from './DropdownMenu'; -const StatusLabel = ({ etat, onChange, showDropdown = true }) => { +const StatusLabel = ({ status, onChange, showDropdown = true }) => { const [dropdownOpen, setDropdownOpen] = useState(false); const statusOptions = [ { value: 1, label: 'Créé' }, @@ -13,7 +13,7 @@ const StatusLabel = ({ etat, onChange, showDropdown = true }) => { { value: 6, label: 'Archivé' }, ]; - const currentStatus = statusOptions.find(option => option.value === etat); + const currentStatus = statusOptions.find(option => option.value === status); return ( <> {showDropdown ? ( @@ -29,12 +29,12 @@ const StatusLabel = ({ etat, onChange, showDropdown = true }) => { onClick: () => onChange(option.value), }))} buttonClassName={`w-[150px] flex items-center justify-center gap-2 px-2 py-2 rounded-md text-sm text-center font-medium ${ - etat === 1 && 'bg-blue-50 text-blue-600' || - etat === 2 && 'bg-orange-50 text-orange-600' || - etat === 3 && 'bg-purple-50 text-purple-600' || - etat === 4 && 'bg-red-50 text-red-600' || - etat === 5 && 'bg-green-50 text-green-600' || - etat === 6 && 'bg-red-50 text-red-600' + status === 1 && 'bg-blue-50 text-blue-600' || + status === 2 && 'bg-orange-50 text-orange-600' || + status === 3 && 'bg-purple-50 text-purple-600' || + status === 4 && 'bg-red-50 text-red-600' || + status === 5 && 'bg-green-50 text-green-600' || + status === 6 && 'bg-red-50 text-red-600' }`} menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10" dropdownOpen={dropdownOpen} @@ -42,12 +42,12 @@ const StatusLabel = ({ etat, onChange, showDropdown = true }) => { /> ) : (
{currentStatus ? currentStatus.label : 'Statut inconnu'}
diff --git a/Front-End/src/instrumentation.js b/Front-End/src/instrumentation.js new file mode 100644 index 0000000..da8332b --- /dev/null +++ b/Front-End/src/instrumentation.js @@ -0,0 +1,7 @@ +export async function register() { + + if (process.env.NEXT_RUNTIME === 'nodejs') { + await require('pino') + await require('next-logger') + } +} diff --git a/Front-End/src/middleware.js b/Front-End/src/middleware.js index add34ad..3819688 100644 --- a/Front-End/src/middleware.js +++ b/Front-End/src/middleware.js @@ -1,7 +1,6 @@ import { NextResponse } from 'next/server'; import createMiddleware from 'next-intl/middleware'; import { routing } from '@/i18n/routing'; - const middleware = createMiddleware(routing); export default function handler(req) { diff --git a/Front-End/src/next-logger.config.js b/Front-End/src/next-logger.config.js new file mode 100644 index 0000000..f8b4510 --- /dev/null +++ b/Front-End/src/next-logger.config.js @@ -0,0 +1,28 @@ +const pino = require('pino') + +const PinoLevelToSeverityLookup = { + trace: 'DEBUG', + debug: 'DEBUG', + info: 'INFO', + warn: 'WARNING', + error: 'ERROR', + fatal: 'CRITICAL', +}; + +const logger = defaultConfig => + pino({ + ...defaultConfig, + messageKey: 'message', + formatters: { + level(label, number) { + return { + severity: PinoLevelToSeverityLookup[label] || PinoLevelToSeverityLookup['info'], + level: number, + } + },}, + mixin: () => ({ name: 'custom-pino-instance' }), + }) + +module.exports = { + logger, +} \ No newline at end of file diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index dd3308e..b99268e 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -22,7 +22,7 @@ export const BE_SUBSCRIPTION_SEND_URL = `${BASE_URL}/Subscriptions/send` export const BE_SUBSCRIPTION_ARCHIVE_URL = `${BASE_URL}/Subscriptions/archive` export const BE_SUBSCRIPTION_REGISTERFORM_URL = `${BASE_URL}/Subscriptions/registerForm` export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms` -export const BE_SUBSCRIPTION_REGISTERFORMFILE_TEMPLATE_URL = `${BASE_URL}/Subscriptions/registerFormFileTemplate` +export const BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL = `${BASE_URL}/Subscriptions/registrationFileTemplates` export const BE_SUBSCRIPTION_LAST_GUARDIAN_URL = `${BASE_URL}/Subscriptions/lastGuardian` //GESTION ENSEIGNANT diff --git a/package-lock.json b/package-lock.json index 4a26b3d..6fe1f10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,10 @@ "": { "name": "n3wt-school", "version": "0.0.1", + "dependencies": { + "next-logger": "^5.0.1", + "winston": "^3.17.0" + }, "devDependencies": { "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", @@ -37,6 +41,14 @@ "node": ">=6.9.0" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@commitlint/cli": { "version": "19.5.0", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.5.0.tgz", @@ -574,6 +586,26 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@hutson/parse-repository-url": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", @@ -583,6 +615,516 @@ "node": ">=6.9.0" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz", + "integrity": "sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==", + "peer": true + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.4.tgz", + "integrity": "sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.4.tgz", + "integrity": "sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.4.tgz", + "integrity": "sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.4.tgz", + "integrity": "sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.4.tgz", + "integrity": "sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.4.tgz", + "integrity": "sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.4.tgz", + "integrity": "sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.4.tgz", + "integrity": "sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "peer": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -613,6 +1155,11 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, "node_modules/add-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", @@ -677,6 +1224,11 @@ "node": ">=0.10.0" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -699,6 +1251,18 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "peer": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -734,6 +1298,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -748,6 +1332,12 @@ "node": ">=4" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "peer": true + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -759,11 +1349,19 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -771,8 +1369,25 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "node_modules/compare-func": { "version": "2.0.0", @@ -1194,6 +1809,16 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -1295,6 +1920,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -1343,6 +1973,11 @@ "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", "dev": true }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -1374,6 +2009,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1671,8 +2311,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -1728,6 +2367,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", @@ -1831,6 +2481,22 @@ "node": ">=0.10.0" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -1942,6 +2608,22 @@ "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", "dev": true }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2191,12 +2873,110 @@ "node": ">=0.10.0" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/next": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz", + "integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==", + "peer": true, + "dependencies": { + "@next/env": "15.1.4", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.1.4", + "@next/swc-darwin-x64": "15.1.4", + "@next/swc-linux-arm64-gnu": "15.1.4", + "@next/swc-linux-arm64-musl": "15.1.4", + "@next/swc-linux-x64-gnu": "15.1.4", + "@next/swc-linux-x64-musl": "15.1.4", + "@next/swc-win32-arm64-msvc": "15.1.4", + "@next/swc-win32-x64-msvc": "15.1.4", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-logger": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/next-logger/-/next-logger-5.0.1.tgz", + "integrity": "sha512-zWTPtS0YwTB+4iSK4VxUVtCYt+zg8+Sx2Tjbtgmpd4SXsFnWdmCbXAeFZFKtEH8yNlucLCUaj0xqposMQ9rKRg==", + "dependencies": { + "lilconfig": "^3.1.2" + }, + "peerDependencies": { + "next": ">=9.0.0", + "pino": "^8.0.0 || ^9.0.0", + "winston": "^3.0.0" + }, + "peerDependenciesMeta": { + "pino": { + "optional": true + }, + "winston": { + "optional": true + } + } + }, "node_modules/normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", @@ -2212,6 +2992,14 @@ "node": ">=10" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -2315,8 +3103,7 @@ "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/pify": { "version": "2.3.0", @@ -2327,6 +3114,34 @@ "node": ">=0.10.0" } }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2353,6 +3168,27 @@ "node": ">=8" } }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "peer": true, + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -2478,7 +3314,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -2549,7 +3384,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -2565,11 +3399,25 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "peer": true + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, + "devOptional": true, "bin": { "semver": "bin/semver.js" }, @@ -2577,6 +3425,93 @@ "node": ">=10" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/sharp/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/sharp/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true, + "peer": true + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2586,6 +3521,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -2639,6 +3583,14 @@ "readable-stream": "^3.0.0" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, "node_modules/standard-version": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-9.5.0.tgz", @@ -2667,11 +3619,19 @@ "node": ">=10" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -2730,6 +3690,29 @@ "node": ">=8" } }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "peer": true, + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2763,6 +3746,11 @@ "node": ">=0.10" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -2793,6 +3781,20 @@ "node": ">=8" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "peer": true + }, "node_modules/type-fest": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", @@ -2859,8 +3861,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -2872,6 +3873,40 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -3010,6 +4045,11 @@ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true }, + "@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==" + }, "@commitlint/cli": { "version": "19.5.0", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.5.0.tgz", @@ -3398,12 +4438,269 @@ } } }, + "@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "optional": true, + "peer": true, + "requires": { + "tslib": "^2.4.0" + } + }, "@hutson/parse-repository-url": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", "dev": true }, + "@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "optional": true, + "peer": true, + "requires": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "optional": true, + "peer": true, + "requires": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "optional": true, + "peer": true + }, + "@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "optional": true, + "peer": true + }, + "@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "optional": true, + "peer": true + }, + "@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "optional": true, + "peer": true + }, + "@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "optional": true, + "peer": true + }, + "@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "optional": true, + "peer": true + }, + "@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "optional": true, + "peer": true + }, + "@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "optional": true, + "peer": true + }, + "@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "optional": true, + "peer": true, + "requires": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "optional": true, + "peer": true, + "requires": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "optional": true, + "peer": true, + "requires": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "optional": true, + "peer": true, + "requires": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "optional": true, + "peer": true, + "requires": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "optional": true, + "peer": true, + "requires": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "optional": true, + "peer": true, + "requires": { + "@emnapi/runtime": "^1.2.0" + } + }, + "@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "optional": true, + "peer": true + }, + "@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "optional": true, + "peer": true + }, + "@next/env": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz", + "integrity": "sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==", + "peer": true + }, + "@next/swc-darwin-arm64": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.4.tgz", + "integrity": "sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==", + "optional": true, + "peer": true + }, + "@next/swc-darwin-x64": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.4.tgz", + "integrity": "sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==", + "optional": true, + "peer": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.4.tgz", + "integrity": "sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==", + "optional": true, + "peer": true + }, + "@next/swc-linux-arm64-musl": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.4.tgz", + "integrity": "sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==", + "optional": true, + "peer": true + }, + "@next/swc-linux-x64-gnu": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.4.tgz", + "integrity": "sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==", + "optional": true, + "peer": true + }, + "@next/swc-linux-x64-musl": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.4.tgz", + "integrity": "sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==", + "optional": true, + "peer": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.4.tgz", + "integrity": "sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==", + "optional": true, + "peer": true + }, + "@next/swc-win32-x64-msvc": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.4.tgz", + "integrity": "sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==", + "optional": true, + "peer": true + }, + "@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "peer": true + }, + "@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "peer": true, + "requires": { + "tslib": "^2.8.0" + } + }, "@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -3434,6 +4731,11 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, "add-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", @@ -3485,6 +4787,11 @@ "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true }, + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3507,6 +4814,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "peer": true, + "requires": { + "streamsearch": "^1.1.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3530,6 +4846,12 @@ "quick-lru": "^4.0.1" } }, + "caniuse-lite": { + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "peer": true + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -3541,6 +4863,12 @@ "supports-color": "^5.3.0" } }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "peer": true + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -3552,11 +4880,19 @@ "wrap-ansi": "^7.0.0" } }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -3564,8 +4900,25 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "requires": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "compare-func": { "version": "2.0.0", @@ -3881,6 +5234,13 @@ "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true }, + "detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "peer": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3957,6 +5317,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3996,6 +5361,11 @@ "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", "dev": true }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -4015,6 +5385,11 @@ "path-exists": "^4.0.0" } }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4236,8 +5611,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", @@ -4278,6 +5652,11 @@ "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, "is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", @@ -4360,6 +5739,16 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==" + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4461,6 +5850,19 @@ "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", "dev": true }, + "logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "requires": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4647,12 +6049,55 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "peer": true + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "next": { + "version": "15.1.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz", + "integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==", + "peer": true, + "requires": { + "@next/env": "15.1.4", + "@next/swc-darwin-arm64": "15.1.4", + "@next/swc-darwin-x64": "15.1.4", + "@next/swc-linux-arm64-gnu": "15.1.4", + "@next/swc-linux-arm64-musl": "15.1.4", + "@next/swc-linux-x64-gnu": "15.1.4", + "@next/swc-linux-x64-musl": "15.1.4", + "@next/swc-win32-arm64-msvc": "15.1.4", + "@next/swc-win32-x64-msvc": "15.1.4", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "sharp": "^0.33.5", + "styled-jsx": "5.1.6" + } + }, + "next-logger": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/next-logger/-/next-logger-5.0.1.tgz", + "integrity": "sha512-zWTPtS0YwTB+4iSK4VxUVtCYt+zg8+Sx2Tjbtgmpd4SXsFnWdmCbXAeFZFKtEH8yNlucLCUaj0xqposMQ9rKRg==", + "requires": { + "lilconfig": "^3.1.2" + } + }, "normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", @@ -4665,6 +6110,14 @@ "validate-npm-package-license": "^3.0.1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4740,8 +6193,7 @@ "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "pify": { "version": "2.3.0", @@ -4749,6 +6201,17 @@ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "peer": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -4767,6 +6230,21 @@ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, + "react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "peer": true + }, + "react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "peer": true, + "requires": { + "scheduler": "^0.25.0" + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -4869,7 +6347,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4918,14 +6395,100 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" + }, + "scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "peer": true }, "semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true + "devOptional": true + }, + "sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "optional": true, + "peer": true, + "requires": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5", + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "dependencies": { + "color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "optional": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "peer": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true, + "peer": true + } + } + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } }, "source-map": { "version": "0.6.1", @@ -4933,6 +6496,12 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "peer": true + }, "spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -4983,6 +6552,11 @@ "readable-stream": "^3.0.0" } }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" + }, "standard-version": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-9.5.0.tgz", @@ -5005,11 +6579,16 @@ "yargs": "^16.0.0" } }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "peer": true + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "requires": { "safe-buffer": "~5.2.0" } @@ -5055,6 +6634,15 @@ "min-indent": "^1.0.0" } }, + "styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "peer": true, + "requires": { + "client-only": "0.0.1" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -5076,6 +6664,11 @@ "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", "dev": true }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -5103,6 +6696,17 @@ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, + "triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==" + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "peer": true + }, "type-fest": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", @@ -5144,8 +6748,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "validate-npm-package-license": { "version": "3.0.4", @@ -5157,6 +6760,34 @@ "spdx-expression-parse": "^3.0.0" } }, + "winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "requires": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + } + }, + "winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "requires": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + } + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", From 5a0e65bb752a80781517394d7b2a673788f7595e Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sun, 19 Jan 2025 21:00:58 +0100 Subject: [PATCH 006/249] =?UTF-8?q?feat:=20Ajout=20de=20la=20configuration?= =?UTF-8?q?=20des=20tarifs=20de=20l'=C3=A9cole=20[#18]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Dockerfile | 1 + Back-End/N3wtSchool/bdd.py | 3 +- Back-End/School/models.py | 58 +++- Back-End/School/serializers.py | 57 +++- Back-End/School/urls.py | 29 +- Back-End/School/views.py | 197 ++++++++++-- Back-End/Subscriptions/models.py | 87 +++++- Back-End/Subscriptions/serializers.py | 20 +- Back-End/Subscriptions/urls.py | 26 +- Back-End/Subscriptions/util.py | 57 +++- Back-End/Subscriptions/views.py | 227 ++++++++++++-- Back-End/start.py | 12 +- .../src/app/[locale]/admin/structure/page.js | 208 ++++++++----- .../components/DraggableFileUpload.js | 50 +++ .../subscriptions/components/FileUpload.js | 92 +++--- .../subscriptions/editInscription/page.js | 5 +- .../app/[locale]/admin/subscriptions/page.js | 84 ++++- Front-End/src/app/[locale]/parents/layout.js | 19 +- Front-End/src/app/lib/messagerieAction.js | 24 ++ Front-End/src/app/lib/schoolAction.js | 22 +- Front-End/src/app/lib/subscriptionAction.js | 42 ++- Front-End/src/components/InputColorIcon.js | 6 +- Front-End/src/components/InputPhone.js | 6 +- Front-End/src/components/InputText.js | 8 +- Front-End/src/components/InputTextIcon.js | 7 +- .../Inscription/InscriptionFormShared.js | 114 ++++++- .../Inscription/ResponsableInputFields.js | 17 +- Front-End/src/components/Popup.js | 12 +- Front-End/src/components/ProtectedRoute.js | 21 ++ Front-End/src/components/SelectChoice.js | 32 +- Front-End/src/components/SidebarTabs.js | 30 ++ .../Structure/Configuration/ClassForm.js | 46 +-- .../Structure/Configuration/ClassesSection.js | 9 +- .../Configuration/DiscountsSection.js | 188 ++++++++++++ .../Structure/Configuration/FeesManagement.js | 43 +++ .../Structure/Configuration/FeesSection.js | 190 ++++++++++++ .../Configuration/SpecialitiesSection.js | 11 +- .../Configuration/StructureManagement.js | 59 ++-- .../Configuration/TeachersSection.js | 9 +- .../Configuration/TuitionFeesSection.js | 286 ++++++++++++++++++ .../Planning/SpecialityEventModal.js | 4 +- Front-End/src/components/ToggleSwitch.js | 12 +- Front-End/src/context/ClassesContext.js | 1 - Front-End/src/context/TuitionFeesContext.js | 24 ++ Front-End/src/utils/Url.js | 10 +- 45 files changed, 2089 insertions(+), 376 deletions(-) create mode 100644 Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js create mode 100644 Front-End/src/app/lib/messagerieAction.js create mode 100644 Front-End/src/components/ProtectedRoute.js create mode 100644 Front-End/src/components/SidebarTabs.js create mode 100644 Front-End/src/components/Structure/Configuration/DiscountsSection.js create mode 100644 Front-End/src/components/Structure/Configuration/FeesManagement.js create mode 100644 Front-End/src/components/Structure/Configuration/FeesSection.js create mode 100644 Front-End/src/components/Structure/Configuration/TuitionFeesSection.js create mode 100644 Front-End/src/context/TuitionFeesContext.js diff --git a/Back-End/Dockerfile b/Back-End/Dockerfile index 7f46014..afa5421 100644 --- a/Back-End/Dockerfile +++ b/Back-End/Dockerfile @@ -7,6 +7,7 @@ FROM python:3.12.7 # Allows docker to cache installed dependencies between builds COPY requirements.txt requirements.txt RUN pip install -r requirements.txt +RUN pip install pymupdf # Mounts the application code to the image COPY . . diff --git a/Back-End/N3wtSchool/bdd.py b/Back-End/N3wtSchool/bdd.py index 1fe62ef..6c4b58b 100644 --- a/Back-End/N3wtSchool/bdd.py +++ b/Back-End/N3wtSchool/bdd.py @@ -92,6 +92,7 @@ def searchObjects(_objectName, _searchTerm=None, _excludeStates=None): def delete_object(model_class, object_id, related_field=None): try: obj = model_class.objects.get(id=object_id) + if related_field and hasattr(obj, related_field): related_obj = getattr(obj, related_field) if related_obj: @@ -103,5 +104,3 @@ def delete_object(model_class, object_id, related_field=None): return JsonResponse({'error': f'L\'objet {model_class.__name__} n\'existe pas avec cet ID'}, status=404, safe=False) except Exception as e: return JsonResponse({'error': f'Une erreur est survenue : {str(e)}'}, status=500, safe=False) - - diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 31d2a80..a992eac 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -3,6 +3,9 @@ from Auth.models import Profile from django.db.models import JSONField from django.dispatch import receiver from django.contrib.postgres.fields import ArrayField +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError + LEVEL_CHOICES = [ (1, 'Très Petite Section (TPS)'), @@ -47,7 +50,7 @@ class SchoolClass(models.Model): number_of_students = models.PositiveIntegerField(blank=True) teaching_language = models.CharField(max_length=255, blank=True) school_year = models.CharField(max_length=9, blank=True) - updated_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) teachers = models.ManyToManyField(Teacher, blank=True) levels = ArrayField(models.IntegerField(choices=LEVEL_CHOICES), default=list) type = models.IntegerField(choices=PLANNING_TYPE_CHOICES, default=1) @@ -64,3 +67,56 @@ class Planning(models.Model): def __str__(self): return f'Planning for {self.level} of {self.school_class.atmosphere_name}' + +class Discount(models.Model): + name = models.CharField(max_length=255, unique=True) + amount = models.DecimalField(max_digits=10, decimal_places=2) + description = models.TextField(blank=True) + + def __str__(self): + return self.name + +class Fee(models.Model): + name = models.CharField(max_length=255, unique=True) + amount = models.DecimalField(max_digits=10, decimal_places=2) + description = models.TextField(blank=True) + + def __str__(self): + return self.name + +class TuitionFee(models.Model): + class PaymentOptions(models.IntegerChoices): + SINGLE_PAYMENT = 0, _('Paiement en une seule fois') + FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois') + TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois') + + name = models.CharField(max_length=255, unique=True) + description = models.TextField(blank=True) + base_amount = models.DecimalField(max_digits=10, decimal_places=2) + currency = models.CharField(max_length=3, default='EUR') + discounts = models.ManyToManyField('Discount', blank=True) + validity_start_date = models.DateField() + validity_end_date = models.DateField() + payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT) + is_active = models.BooleanField(default=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + def clean(self): + if self.validity_end_date <= self.validity_start_date: + raise ValidationError(_('La date de fin de validité doit être après la date de début de validité.')) + + def calculate_final_amount(self): + amount = self.base_amount + + # Apply fees (supplements and taxes) + # for fee in self.fees.all(): + # amount += fee.amount + + # Apply discounts + for discount in self.discounts.all(): + amount -= discount.amount + + return amount diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index d3bae13..2248861 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES +from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, TuitionFee, Fee from Subscriptions.models import RegistrationForm from Subscriptions.serializers import StudentSerializer from Auth.serializers import ProfileSerializer @@ -172,4 +172,57 @@ class SchoolClassSerializer(serializers.ModelSerializer): utc_time = timezone.localtime(obj.updated_date) local_tz = pytz.timezone(settings.TZ_APPLI) local_time = utc_time.astimezone(local_tz) - return local_time.strftime("%d-%m-%Y %H:%M") \ No newline at end of file + return local_time.strftime("%d-%m-%Y %H:%M") + +class DiscountSerializer(serializers.ModelSerializer): + class Meta: + model = Discount + fields = '__all__' + +class FeeSerializer(serializers.ModelSerializer): + class Meta: + model = Fee + fields = '__all__' + +class TuitionFeeSerializer(serializers.ModelSerializer): + discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) + final_amount = serializers.SerializerMethodField() + + class Meta: + model = TuitionFee + fields = '__all__' + + def get_final_amount(self, obj): + return obj.calculate_final_amount() + + def create(self, validated_data): + discounts_data = validated_data.pop('discounts', []) + + # Create the TuitionFee instance + tuition_fee = TuitionFee.objects.create(**validated_data) + + # Add discounts if provided + for discount in discounts_data: + tuition_fee.discounts.add(discount) + + return tuition_fee + + def update(self, instance, validated_data): + discounts_data = validated_data.pop('discounts', []) + + # Update the TuitionFee instance + instance.name = validated_data.get('name', instance.name) + instance.description = validated_data.get('description', instance.description) + instance.base_amount = validated_data.get('base_amount', instance.base_amount) + instance.currency = validated_data.get('currency', instance.currency) + instance.validity_start_date = validated_data.get('validity_start_date', instance.validity_start_date) + instance.validity_end_date = validated_data.get('validity_end_date', instance.validity_end_date) + instance.payment_option = validated_data.get('payment_option', instance.payment_option) + instance.is_active = validated_data.get('is_active', instance.is_active) + instance.save() + + # Update discounts if provided + if discounts_data: + instance.discounts.set(discounts_data) + + return instance \ No newline at end of file diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index 77897c3..4ccae46 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -1,6 +1,21 @@ from django.urls import path, re_path -from School.views import TeachersView, TeacherView, SpecialitiesView, SpecialityView, ClassesView, ClasseView, PlanningsView, PlanningView +from School.views import ( + TeachersView, + TeacherView, + SpecialitiesView, + SpecialityView, + ClassesView, + ClasseView, + PlanningsView, + PlanningView, + FeesView, + FeeView, + TuitionFeesView, + TuitionFeeView, + DiscountsView, + DiscountView, +) urlpatterns = [ re_path(r'^specialities$', SpecialitiesView.as_view(), name="specialities"), @@ -18,4 +33,16 @@ urlpatterns = [ re_path(r'^plannings$', PlanningsView.as_view(), name="plannings"), re_path(r'^planning$', PlanningView.as_view(), name="planning"), re_path(r'^planning/([0-9]+)$', PlanningView.as_view(), name="planning"), + + re_path(r'^fees$', FeesView.as_view(), name="fees"), + re_path(r'^fee$', FeeView.as_view(), name="fee"), + re_path(r'^fee/([0-9]+)$', FeeView.as_view(), name="fee"), + + re_path(r'^tuitionFees$', TuitionFeesView.as_view(), name="tuitionFees"), + re_path(r'^tuitionFee$', TuitionFeeView.as_view(), name="tuitionFee"), + re_path(r'^tuitionFee/([0-9]+)$', TuitionFeeView.as_view(), name="tuitionFee"), + + re_path(r'^discounts$', DiscountsView.as_view(), name="discounts"), + re_path(r'^discount$', DiscountView.as_view(), name="discount"), + re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"), ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index 7ba4984..e06e944 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -5,46 +5,41 @@ from rest_framework.parsers import JSONParser from rest_framework.views import APIView from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist -from .models import Teacher, Speciality, SchoolClass, Planning -from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer +from .models import Teacher, Speciality, SchoolClass, Planning, Discount, TuitionFee, Fee +from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, TuitionFeeSerializer, FeeSerializer from N3wtSchool import bdd +from N3wtSchool.bdd import delete_object, getAllObjects, getObject @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class SpecialitiesView(APIView): def get(self, request): - specialitiesList=bdd.getAllObjects(Speciality) - specialities_serializer=SpecialitySerializer(specialitiesList, many=True) - + specialitiesList = getAllObjects(Speciality) + specialities_serializer = SpecialitySerializer(specialitiesList, many=True) return JsonResponse(specialities_serializer.data, safe=False) def post(self, request): - specialities_data=JSONParser().parse(request) + specialities_data = JSONParser().parse(request) all_valid = True for speciality_data in specialities_data: speciality_serializer = SpecialitySerializer(data=speciality_data) - if speciality_serializer.is_valid(): speciality_serializer.save() else: all_valid = False break if all_valid: - specialitiesList = bdd.getAllObjects(Speciality) + specialitiesList = getAllObjects(Speciality) specialities_serializer = SpecialitySerializer(specialitiesList, many=True) - return JsonResponse(specialities_serializer.data, safe=False) - return JsonResponse(speciality_serializer.errors, safe=False) - @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class SpecialityView(APIView): - def get (self, request, _id): - speciality = bdd.getObject(_objectName=Speciality, _columnName='id', _value=_id) - speciality_serializer=SpecialitySerializer(speciality) - + def get(self, request, _id): + speciality = getObject(_objectName=Speciality, _columnName='id', _value=_id) + speciality_serializer = SpecialitySerializer(speciality) return JsonResponse(speciality_serializer.data, safe=False) def post(self, request): @@ -59,7 +54,7 @@ class SpecialityView(APIView): def put(self, request, _id): speciality_data=JSONParser().parse(request) - speciality = bdd.getObject(_objectName=Speciality, _columnName='id', _value=_id) + speciality = getObject(_objectName=Speciality, _columnName='id', _value=_id) speciality_serializer = SpecialitySerializer(speciality, data=speciality_data) if speciality_serializer.is_valid(): speciality_serializer.save() @@ -68,11 +63,62 @@ class SpecialityView(APIView): return JsonResponse(speciality_serializer.errors, safe=False) def delete(self, request, _id): - return bdd.delete_object(Speciality, _id) + return delete_object(Speciality, _id) + +# Vues pour les réductions (Discount) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountsView(APIView): + def get(self, request): + discountsList = Discount.objects.all() + discounts_serializer = DiscountSerializer(discountsList, many=True) + return JsonResponse(discounts_serializer.data, safe=False) + + def post(self, request): + discount_data = JSONParser().parse(request) + discount_serializer = DiscountSerializer(data=discount_data) + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False, status=201) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountView(APIView): + def get(self, request, _id): + try: + discount = Discount.objects.get(id=_id) + discount_serializer = DiscountSerializer(discount) + return JsonResponse(discount_serializer.data, safe=False) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + discount_data = JSONParser().parse(request) + discount_serializer = DiscountSerializer(data=discount_data) + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False, status=201) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + discount_data = JSONParser().parse(request) + try: + discount = Discount.objects.get(id=_id) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + discount_serializer = DiscountSerializer(discount, data=discount_data, partial=True) # Utilisation de partial=True + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(Discount, _id) class TeachersView(APIView): def get(self, request): - teachersList=bdd.getAllObjects(Teacher) + teachersList=getAllObjects(Teacher) teachers_serializer=TeacherSerializer(teachersList, many=True) return JsonResponse(teachers_serializer.data, safe=False) @@ -81,7 +127,7 @@ class TeachersView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class TeacherView(APIView): def get (self, request, _id): - teacher = bdd.getObject(_objectName=Teacher, _columnName='id', _value=_id) + teacher = getObject(_objectName=Teacher, _columnName='id', _value=_id) teacher_serializer=TeacherSerializer(teacher) return JsonResponse(teacher_serializer.data, safe=False) @@ -99,7 +145,7 @@ class TeacherView(APIView): def put(self, request, _id): teacher_data=JSONParser().parse(request) - teacher = bdd.getObject(_objectName=Teacher, _columnName='id', _value=_id) + teacher = getObject(_objectName=Teacher, _columnName='id', _value=_id) teacher_serializer = TeacherSerializer(teacher, data=teacher_data) if teacher_serializer.is_valid(): teacher_serializer.save() @@ -108,13 +154,13 @@ class TeacherView(APIView): return JsonResponse(teacher_serializer.errors, safe=False) def delete(self, request, _id): - return bdd.delete_object(Teacher, _id, related_field='associated_profile') + return delete_object(Teacher, _id, related_field='associated_profile') @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class ClassesView(APIView): def get(self, request): - classesList=bdd.getAllObjects(SchoolClass) + classesList=getAllObjects(SchoolClass) classes_serializer=SchoolClassSerializer(classesList, many=True) return JsonResponse(classes_serializer.data, safe=False) @@ -131,7 +177,7 @@ class ClassesView(APIView): break if all_valid: - classesList = bdd.getAllObjects(SchoolClass) + classesList = getAllObjects(SchoolClass) classes_serializer = SchoolClassSerializer(classesList, many=True) return JsonResponse(classes_serializer.data, safe=False) @@ -142,7 +188,7 @@ class ClassesView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class ClasseView(APIView): def get (self, request, _id): - schoolClass = bdd.getObject(_objectName=SchoolClass, _columnName='id', _value=_id) + schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=_id) classe_serializer=SchoolClassSerializer(schoolClass) return JsonResponse(classe_serializer.data, safe=False) @@ -159,7 +205,7 @@ class ClasseView(APIView): def put(self, request, _id): classe_data=JSONParser().parse(request) - schoolClass = bdd.getObject(_objectName=SchoolClass, _columnName='id', _value=_id) + schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=_id) classe_serializer = SchoolClassSerializer(schoolClass, data=classe_data) if classe_serializer.is_valid(): classe_serializer.save() @@ -168,14 +214,14 @@ class ClasseView(APIView): return JsonResponse(classe_serializer.errors, safe=False) def delete(self, request, _id): - return bdd.delete_object(SchoolClass, _id) + return delete_object(SchoolClass, _id) @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class PlanningsView(APIView): def get(self, request): - schedulesList=bdd.getAllObjects(Planning) + schedulesList=getAllObjects(Planning) schedules_serializer=PlanningSerializer(schedulesList, many=True) return JsonResponse(schedules_serializer.data, safe=False) @@ -183,7 +229,7 @@ class PlanningsView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class PlanningView(APIView): def get (self, request, _id): - planning = bdd.getObject(_objectName=Planning, _columnName='classe__id', _value=_id) + planning = getObject(_objectName=Planning, _columnName='classe__id', _value=_id) planning_serializer=PlanningSerializer(planning) return JsonResponse(planning_serializer.data, safe=False) @@ -215,3 +261,98 @@ class PlanningView(APIView): return JsonResponse(planning_serializer.data, safe=False) return JsonResponse(planning_serializer.errors, safe=False) + + +# Vues pour les frais (Fee) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class FeesView(APIView): + def get(self, request): + feesList = Fee.objects.all() + fees_serializer = FeeSerializer(feesList, many=True) + return JsonResponse(fees_serializer.data, safe=False) + + def post(self, request): + fee_data = JSONParser().parse(request) + fee_serializer = FeeSerializer(data=fee_data) + if fee_serializer.is_valid(): + fee_serializer.save() + return JsonResponse(fee_serializer.data, safe=False, status=201) + return JsonResponse(fee_serializer.errors, safe=False, status=400) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class FeeView(APIView): + def get(self, request, _id): + try: + fee = Fee.objects.get(id=_id) + fee_serializer = FeeSerializer(fee) + return JsonResponse(fee_serializer.data, safe=False) + except Fee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + fee_data = JSONParser().parse(request) + fee_serializer = FeeSerializer(data=fee_data) + if fee_serializer.is_valid(): + fee_serializer.save() + return JsonResponse(fee_serializer.data, safe=False, status=201) + return JsonResponse(fee_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + fee_data = JSONParser().parse(request) + try: + fee = Fee.objects.get(id=_id) + except Fee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + fee_serializer = FeeSerializer(fee, data=fee_data, partial=True) # Utilisation de partial=True + if fee_serializer.is_valid(): + fee_serializer.save() + return JsonResponse(fee_serializer.data, safe=False) + return JsonResponse(fee_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(Fee, _id) + +# Vues pour les frais de scolarité (TuitionFee) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class TuitionFeesView(APIView): + def get(self, request): + tuitionFeesList = TuitionFee.objects.all() + tuitionFees_serializer = TuitionFeeSerializer(tuitionFeesList, many=True) + return JsonResponse(tuitionFees_serializer.data, safe=False) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class TuitionFeeView(APIView): + def get(self, request, _id): + try: + tuitionFee = TuitionFee.objects.get(id=_id) + tuitionFee_serializer = TuitionFeeSerializer(tuitionFee) + return JsonResponse(tuitionFee_serializer.data, safe=False) + except TuitionFee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + tuitionFee_data = JSONParser().parse(request) + tuitionFee_serializer = TuitionFeeSerializer(data=tuitionFee_data) + if tuitionFee_serializer.is_valid(): + tuitionFee_serializer.save() + return JsonResponse(tuitionFee_serializer.data, safe=False, status=201) + return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + tuitionFee_data = JSONParser().parse(request) + try: + tuitionFee = TuitionFee.objects.get(id=_id) + except TuitionFee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + tuitionFee_serializer = TuitionFeeSerializer(tuitionFee, data=tuitionFee_data, partial=True) # Utilisation de partial=True + if tuitionFee_serializer.is_valid(): + tuitionFee_serializer.save() + return JsonResponse(tuitionFee_serializer.data, safe=False) + return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(TuitionFee, _id) \ No newline at end of file diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 766577d..b183107 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -9,6 +9,9 @@ from School.models import SchoolClass from datetime import datetime class RegistrationFee(models.Model): + """ + Représente un tarif ou frais d’inscription avec différentes options de paiement. + """ class PaymentOptions(models.IntegerChoices): SINGLE_PAYMENT = 0, _('Paiement en une seule fois') MONTHLY_PAYMENT = 1, _('Paiement mensuel') @@ -27,6 +30,9 @@ class RegistrationFee(models.Model): return self.name class Language(models.Model): + """ + Représente une langue parlée par l’élève. + """ id = models.AutoField(primary_key=True) label = models.CharField(max_length=200, default="") @@ -34,6 +40,9 @@ class Language(models.Model): return "LANGUAGE" class Guardian(models.Model): + """ + Représente un responsable légal (parent/tuteur) d’un élève. + """ last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") birth_date = models.CharField(max_length=200, default="", blank=True) @@ -47,6 +56,9 @@ class Guardian(models.Model): return self.last_name + "_" + self.first_name class Sibling(models.Model): + """ + Représente un frère ou une sœur d’un élève. + """ id = models.AutoField(primary_key=True) last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") @@ -56,7 +68,9 @@ class Sibling(models.Model): return "SIBLING" class Student(models.Model): - + """ + Représente l’élève inscrit ou en cours d’inscription. + """ class StudentGender(models.IntegerChoices): NONE = 0, _('Sélection du genre') MALE = 1, _('Garçon') @@ -95,6 +109,9 @@ class Student(models.Model): # Many-to-Many Relationship siblings = models.ManyToManyField(Sibling, blank=True) + # Many-to-Many Relationship + registration_files = models.ManyToManyField('RegistrationFile', blank=True, related_name='students') + # Many-to-Many Relationship spoken_languages = models.ManyToManyField(Language, blank=True) @@ -105,21 +122,39 @@ class Student(models.Model): return self.last_name + "_" + self.first_name def getSpokenLanguages(self): + """ + Retourne la liste des langues parlées par l’élève. + """ return self.spoken_languages.all() def getMainGuardian(self): + """ + Retourne le responsable légal principal de l’élève. + """ return self.guardians.all()[0] def getGuardians(self): + """ + Retourne tous les responsables légaux de l’élève. + """ return self.guardians.all() def getProfiles(self): + """ + Retourne les profils utilisateurs liés à l’élève. + """ return self.profiles.all() def getSiblings(self): + """ + Retourne les frères et sœurs de l’élève. + """ return self.siblings.all() def getNumberOfSiblings(self): + """ + Retourne le nombre de frères et sœurs. + """ return self.siblings.count() @property @@ -148,7 +183,9 @@ class Student(models.Model): return None class RegistrationForm(models.Model): - + """ + Gère le dossier d’inscription lié à un élève donné. + """ class RegistrationFormStatus(models.IntegerChoices): RF_ABSENT = 0, _('Pas de dossier d\'inscription') RF_CREATED = 1, _('Dossier d\'inscription créé') @@ -171,9 +208,53 @@ class RegistrationForm(models.Model): return "RF_" + self.student.last_name + "_" + self.student.first_name class RegistrationFileTemplate(models.Model): + """ + Modèle pour stocker les fichiers "templates" d’inscription. + """ name = models.CharField(max_length=255) - file = models.FileField(upload_to='registration_files/') + file = models.FileField(upload_to='templates_files/', blank=True, null=True) + order = models.PositiveIntegerField(default=0) # Ajout du champ order date_added = models.DateTimeField(auto_now_add=True) + is_required = models.BooleanField(default=False) + + @property + def formatted_date_added(self): + if self.date_added: + return self.date_added.strftime('%d-%m-%Y') + return None def __str__(self): return self.name + +def registration_file_upload_to(instance, filename): + return f"registration_files/dossier_rf_{instance.register_form.pk}/{filename}" + +class RegistrationFile(models.Model): + """ + Fichier lié à un dossier d’inscription particulier. + """ + name = models.CharField(max_length=255) + file = models.FileField(upload_to=registration_file_upload_to) + date_added = models.DateTimeField(auto_now_add=True) + template = models.OneToOneField(RegistrationFileTemplate, on_delete=models.CASCADE) + register_form = models.ForeignKey('RegistrationForm', on_delete=models.CASCADE, related_name='registration_files') + + @property + def formatted_date_added(self): + if self.date_added: + return self.date_added.strftime('%d-%m-%Y') + return None + + def __str__(self): + return self.name + + @staticmethod + def get_files_from_rf(register_form_id): + """ + Récupère tous les fichiers liés à un dossier d’inscription donné. + """ + registration_files = RegistrationFile.objects.filter(register_form_id=register_form_id).order_by('template__order') + filenames = [] + for reg_file in registration_files: + filenames.append(reg_file.file.path) + return filenames diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index 99538d7..b4a996c 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import RegistrationFileTemplate, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee +from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee from School.models import SchoolClass from Auth.models import Profile from Auth.serializers import ProfileSerializer @@ -10,17 +10,22 @@ from django.utils import timezone import pytz from datetime import datetime -class RegistrationFileTemplateSerializer(serializers.ModelSerializer): - class Meta: - model = RegistrationFileTemplate - fields = '__all__' - class RegistrationFeeSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) class Meta: model = RegistrationFee fields = '__all__' +class RegistrationFileSerializer(serializers.ModelSerializer): + class Meta: + model = RegistrationFile + fields = '__all__' + +class RegistrationFileTemplateSerializer(serializers.ModelSerializer): + class Meta: + model = RegistrationFileTemplate + fields = '__all__' + class LanguageSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) class Meta: @@ -47,6 +52,7 @@ class GuardianSerializer(serializers.ModelSerializer): class StudentSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) guardians = GuardianSerializer(many=True, required=False) siblings = SiblingSerializer(many=True, required=False) @@ -126,7 +132,7 @@ class RegistrationFormSerializer(serializers.ModelSerializer): registration_file = serializers.FileField(required=False) status_label = serializers.SerializerMethodField() formatted_last_update = serializers.SerializerMethodField() - + registration_files = RegistrationFileSerializer(many=True, required=False) class Meta: model = RegistrationForm fields = '__all__' diff --git a/Back-End/Subscriptions/urls.py b/Back-End/Subscriptions/urls.py index 53647fa..0e7fbf8 100644 --- a/Back-End/Subscriptions/urls.py +++ b/Back-End/Subscriptions/urls.py @@ -1,36 +1,44 @@ from django.urls import path, re_path from . import views -from Subscriptions.views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView +from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView, RegistrationFileView urlpatterns = [ - re_path(r'^registerForms/([a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"), + re_path(r'^registerForms/(?P<_filter>[a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"), + re_path(r'^registerForm$', RegisterFormView.as_view(), name="registerForm"), - re_path(r'^registerForm/([0-9]+)$', RegisterFormView.as_view(), name="registerForm"), + re_path(r'^registerForm/(?P<_id>[0-9]+)$', RegisterFormView.as_view(), name="registerForm"), # Page de formulaire d'inscription - ELEVE - re_path(r'^student/([0-9]+)$', StudentView.as_view(), name="students"), + re_path(r'^student/(?P<_id>[0-9]+)$', StudentView.as_view(), name="students"), # Page de formulaire d'inscription - RESPONSABLE re_path(r'^lastGuardian$', GuardianView.as_view(), name="lastGuardian"), # Envoi d'un dossier d'inscription - re_path(r'^send/([0-9]+)$', views.send, name="send"), + re_path(r'^send/(?P<_id>[0-9]+)$', views.send, name="send"), # Archivage d'un dossier d'inscription - re_path(r'^archive/([0-9]+)$', views.archive, name="archive"), + re_path(r'^archive/(?P<_id>[0-9]+)$', views.archive, name="archive"), # Envoi d'une relance de dossier d'inscription - re_path(r'^sendRelance/([0-9]+)$', views.relance, name="sendRelance"), + re_path(r'^sendRelance/(?P<_id>[0-9]+)$', views.relance, name="sendRelance"), # Page PARENT - Liste des children - re_path(r'^children/([0-9]+)$', ChildrenListView.as_view(), name="children"), + re_path(r'^children/(?P<_id>[0-9]+)$', ChildrenListView.as_view(), name="children"), # Page INSCRIPTION - Liste des élèves re_path(r'^students$', StudentListView.as_view(), name="students"), # Frais d'inscription re_path(r'^registrationFees$', RegistrationFeeView.as_view(), name="registrationFees"), + + # modèles de fichiers d'inscription re_path(r'^registrationFileTemplates$', RegistrationFileTemplateView.as_view(), name='registrationFileTemplates'), - re_path(r'^registrationFileTemplates/([0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"), + re_path(r'^registrationFileTemplates/(?P<_id>[0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"), + + # fichiers d'inscription + re_path(r'^registrationFiles/(?P<_id>[0-9]+)$', RegistrationFileView.as_view(), name='registrationFiles'), + re_path(r'^registrationFiles', RegistrationFileView.as_view(), name="registrationFiles"), + ] \ No newline at end of file diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index 9ae17a9..e76d06b 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -16,52 +16,95 @@ from enum import Enum import random import string from rest_framework.parsers import JSONParser +import pymupdf def recupereListeFichesInscription(): + """ + Retourne la liste complète des fiches d’inscription. + """ context = { "ficheInscriptions_list": bdd.getAllObjects(RegistrationForm), } return context def recupereListeFichesInscriptionEnAttenteSEPA(): - + """ + Retourne les fiches d’inscription avec paiement SEPA en attente. + """ ficheInscriptionsSEPA_list = RegistrationForm.objects.filter(modePaiement="Prélèvement SEPA").filter(etat=RegistrationForm.RegistrationFormStatus['SEPA_ENVOYE']) return ficheInscriptionsSEPA_list def _now(): + """ + Retourne la date et l’heure en cours, avec fuseau. + """ return datetime.now(ZoneInfo(settings.TZ_APPLI)) def convertToStr(dateValue, dateFormat): + """ + Convertit un objet datetime en chaîne selon un format donné. + """ return dateValue.strftime(dateFormat) def convertToDate(date_time): + """ + Convertit une chaîne en objet datetime selon le format '%d-%m-%Y %H:%M'. + """ format = '%d-%m-%Y %H:%M' datetime_str = datetime.strptime(date_time, format) return datetime_str def convertTelephone(telephoneValue, separator='-'): + """ + Reformate un numéro de téléphone en y insérant un séparateur donné. + """ return f"{telephoneValue[:2]}{separator}{telephoneValue[2:4]}{separator}{telephoneValue[4:6]}{separator}{telephoneValue[6:8]}{separator}{telephoneValue[8:10]}" def genereRandomCode(length): + """ + Génère un code aléatoire de longueur spécifiée. + """ return ''.join(random.choice(string.ascii_letters) for i in range(length)) def calculeDatePeremption(_start, nbDays): + """ + Calcule la date de fin à partir d’un point de départ et d’un nombre de jours. + """ return convertToStr(_start + timedelta(days=nbDays), settings.DATE_FORMAT) # Fonction permettant de retourner la valeur du QueryDict # QueryDict [ index ] -> Dernière valeur d'une liste # dict (QueryDict [ index ]) -> Toutes les valeurs de la liste def _(liste): + """ + Retourne la première valeur d’une liste extraite d’un QueryDict. + """ return liste[0] def getArgFromRequest(_argument, _request): + """ + Extrait la valeur d’un argument depuis la requête (JSON). + """ resultat = None data=JSONParser().parse(_request) resultat = data[_argument] return resultat -def rfToPDF(registerForm): +def merge_files_pdf(filenames, output_filename): + """ + Insère plusieurs fichiers PDF dans un seul document de sortie. + """ + merger = pymupdf.open() + for filename in filenames: + merger.insert_file(filename) + merger.save(output_filename) + merger.close() + +def rfToPDF(registerForm,filename): + """ + Génère le PDF d’un dossier d’inscription et l’associe au RegistrationForm. + """ # Ajout du fichier d'inscriptions data = { 'pdf_title': "Dossier d'inscription de %s"%registerForm.student.first_name, @@ -69,14 +112,12 @@ def rfToPDF(registerForm): 'signatureTime': convertToStr(_now(), '%H:%M'), 'student':registerForm.student, } - + PDFFileName = filename pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) - - PDFFileName = "Dossier_Inscription_%s_%s.pdf"%(registerForm.student.last_name, registerForm.student.first_name) - pathFichier = Path(settings.DOCUMENT_DIR + "/" + PDFFileName) + pathFichier = Path(filename) if os.path.exists(str(pathFichier)): print(f'File exists : {str(pathFichier)}') os.remove(str(pathFichier)) - receipt_file = BytesIO(pdf.content) - registerForm.fichierInscription = File(receipt_file, PDFFileName) \ No newline at end of file + registerForm.fichierInscription = File(receipt_file, PDFFileName) + registerForm.fichierInscription.save() \ No newline at end of file diff --git a/Back-End/Subscriptions/views.py b/Back-End/Subscriptions/views.py index 47a085d..5a29bd8 100644 --- a/Back-End/Subscriptions/views.py +++ b/Back-End/Subscriptions/views.py @@ -10,6 +10,8 @@ from rest_framework.parsers import JSONParser,MultiPartParser, FormParser from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi import json from pathlib import Path @@ -18,18 +20,22 @@ from io import BytesIO import Subscriptions.mailManager as mailer import Subscriptions.util as util -from Subscriptions.serializers import RegistrationFormSerializer, RegistrationFileTemplateSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer -from Subscriptions.pagination import CustomPagination -from Subscriptions.signals import clear_cache -from .models import Student, Guardian, RegistrationForm, RegistrationFee, RegistrationFileTemplate from Subscriptions.automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine +from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer +from .pagination import CustomPagination +from .signals import clear_cache +from .models import Student, Guardian, RegistrationForm, RegistrationFee, RegistrationFileTemplate, RegistrationFile +from .automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine from Auth.models import Profile from N3wtSchool import settings, renderers, bdd class RegisterFormListView(APIView): + """ + Gère la liste des dossiers d’inscription, lecture et création. + """ pagination_class = CustomPagination def get_register_form(self, _filter, search=None): @@ -47,8 +53,18 @@ class RegisterFormListView(APIView): return bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_VALIDATED) return None + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter('_filter', openapi.IN_PATH, description="filtre", type=openapi.TYPE_STRING, enum=['pending', 'archived', 'subscribed'], required=True), + openapi.Parameter('search', openapi.IN_QUERY, description="search", type=openapi.TYPE_STRING, required=False), + openapi.Parameter('page_size', openapi.IN_QUERY, description="limite de page lors de la pagination", type=openapi.TYPE_INTEGER, required=False), + ], + responses={200: RegistrationFormSerializer(many=True)} + ) def get(self, request, _filter): - + """ + Récupère les fiches d'inscriptions en fonction du filtre passé. + """ # Récupération des paramètres search = request.GET.get('search', '').strip() page_size = request.GET.get('page_size', None) @@ -84,6 +100,11 @@ class RegisterFormListView(APIView): return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False) + @swagger_auto_schema( + manual_parameters=[ + ], + responses={200: RegistrationFormSerializer(many=True)} + ) def post(self, request): studentFormList_serializer=JSONParser().parse(request) for studentForm_data in studentFormList_serializer: @@ -104,14 +125,23 @@ class RegisterFormListView(APIView): @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class RegisterFormView(APIView): + """ + Gère la lecture, création, modification et suppression d’un dossier d’inscription. + """ pagination_class = CustomPagination def get(self, request, _id): + """ + Récupère un dossier d'inscription donné. + """ registerForm=bdd.getObject(RegistrationForm, "student__id", _id) registerForm_serializer=RegistrationFormSerializer(registerForm) return JsonResponse(registerForm_serializer.data, safe=False) def post(self, request): + """ + Crée un dossier d'inscription. + """ studentForm_data=JSONParser().parse(request) # Ajout de la date de mise à jour studentForm_data["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') @@ -137,21 +167,33 @@ class RegisterFormView(APIView): return JsonResponse(studentForm_serializer.data, safe=False) - return JsonResponse(studentForm_serializer.errors, safe=False, status=400) + return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - def put(self, request, id): + def put(self, request, _id): + """ + Modifie un dossier d'inscription donné. + """ studentForm_data=JSONParser().parse(request) - status = studentForm_data.pop('status', 0) + _status = studentForm_data.pop('status', 0) studentForm_data["last_update"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M')) - registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - if status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: + if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: # Le parent a complété le dossier d'inscription, il est soumis à validation par l'école json.dumps(studentForm_data) - util.rfToPDF(registerForm) + #Génération de la fiche d'inscription au format PDF + PDFFileName = "rf_%s_%s.pdf"%(registerForm.student.last_name, registerForm.student.first_name) + path = Path(f"registration_files/dossier_rf_{registerForm.pk}/{PDFFileName}") + registerForm.fichierInscription = util.rfToPDF(registerForm, path) + # Récupération des fichiers d'inscription + fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) + fileNames.insert(0,path) + # Création du fichier PDF Fusionné avec le dossier complet + output_path = f"registration_files/dossier_rf_{registerForm.pk}/dossier_{registerForm.pk}.pdf" + util.merge_files_pdf(fileNames, output_path) # Mise à jour de l'automate updateStateMachine(registerForm, 'saisiDI') - elif status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: + elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: # L'école a validé le dossier d'inscription # Mise à jour de l'automate updateStateMachine(registerForm, 'valideDI') @@ -162,34 +204,49 @@ class RegisterFormView(APIView): studentForm_serializer.save() return JsonResponse(studentForm_serializer.data, safe=False) - return JsonResponse(studentForm_serializer.errors, safe=False, status=400) + return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, id): + """ + Supprime un dossier d'inscription donné. + """ register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) if register_form != None: student = register_form.student student.guardians.clear() student.profiles.clear() + student.registration_files.clear() student.delete() clear_cache() return JsonResponse("La suppression du dossier a été effectuée avec succès", safe=False) - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) class StudentView(APIView): + """ + Gère la lecture d’un élève donné. + """ def get(self, request, _id): student = bdd.getObject(_objectName=Student, _columnName='id', _value=_id) + if student is None: + return JsonResponse({"errorMessage":'Aucun élève trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) student_serializer = StudentSerializer(student) return JsonResponse(student_serializer.data, safe=False) class GuardianView(APIView): + """ + Récupère le dernier ID de responsable légal créé. + """ def get(self, request): lastGuardian = bdd.getLastId(Guardian) return JsonResponse({"lastid":lastGuardian}, safe=False) -def send(request, id): - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) +def send(request, _id): + """ + Envoie le dossier d’inscription par e-mail. + """ + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) if register_form != None: student = register_form.student guardian = student.getMainGuardian() @@ -199,24 +256,31 @@ def send(request, id): register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') # Mise à jour de l'automate updateStateMachine(register_form, 'envoiDI') + return JsonResponse({"message": f"Le dossier d'inscription a bien été envoyé à l'addresse {email}"}, safe=False) - return JsonResponse({"errorMessage":errorMessage}, safe=False, status=400) + return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) -def archive(request, id): - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) +def archive(request, _id): + """ + Archive le dossier d’inscription visé. + """ + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) if register_form != None: register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') # Mise à jour de l'automate updateStateMachine(register_form, 'archiveDI') - return JsonResponse({"errorMessage":''}, safe=False, status=400) + return JsonResponse({"errorMessage":''}, safe=False, status=status.HTTP_400_BAD_REQUEST) - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) -def relance(request, id): - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) +def relance(request, _id): + """ + Relance un dossier d’inscription par e-mail. + """ + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) if register_form != None: student = register_form.student guardian = student.getMainGuardian() @@ -227,12 +291,15 @@ def relance(request, id): register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') register_form.save() - return JsonResponse({"errorMessage":errorMessage}, safe=False, status=400) + return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) # API utilisée pour la vue parent class ChildrenListView(APIView): + """ + Pour la vue parent : liste les élèves rattachés à un profil donné. + """ # Récupération des élèves d'un parent # idProfile : identifiant du profil connecté rattaché aux fiches d'élèves def get(self, request, _idProfile): @@ -242,6 +309,9 @@ class ChildrenListView(APIView): # API utilisée pour la vue de création d'un DI class StudentListView(APIView): + """ + Pour la vue de création d’un dossier d’inscription : liste les élèves disponibles. + """ # Récupération de la liste des élèves inscrits ou en cours d'inscriptions def get(self, request): students = bdd.getAllObjects(_objectName=Student) @@ -250,20 +320,52 @@ class StudentListView(APIView): # API utilisée pour la vue de personnalisation des frais d'inscription pour la structure class RegistrationFeeView(APIView): + """ + Liste les frais d’inscription. + """ def get(self, request): tarifs = bdd.getAllObjects(RegistrationFee) tarifs_serializer = RegistrationFeeSerializer(tarifs, many=True) return JsonResponse(tarifs_serializer.data, safe=False) class RegistrationFileTemplateView(APIView): + """ + Gère les fichiers templates pour les dossiers d’inscription. + """ parser_classes = (MultiPartParser, FormParser) - def get(self, request): - fichiers = RegistrationFileTemplate.objects.all() - serializer = RegistrationFileTemplateSerializer(fichiers, many=True) - return Response(serializer.data) + def get(self, request, _id=None): + """ + Récupère les fichiers templates pour les dossiers d’inscription. + """ + if _id is None: + files = RegistrationFileTemplate.objects.all() + serializer = RegistrationFileTemplateSerializer(files, many=True) + return Response(serializer.data) + else : + registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) + if registationFileTemplate is None: + return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileTemplateSerializer(registationFileTemplate) + return JsonResponse(serializer.data, safe=False) + + def put(self, request, _id): + """ + Met à jour un fichier template existant. + """ + registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) + if registationFileTemplate is None: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileTemplateSerializer(registationFileTemplate,data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def post(self, request): + """ + Crée un fichier template pour les dossiers d’inscription. + """ serializer = RegistrationFileTemplateSerializer(data=request.data) if serializer.is_valid(): serializer.save() @@ -271,10 +373,71 @@ class RegistrationFileTemplateView(APIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, _id): + """ + Supprime un fichier template existant. + """ registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) if registrationFileTemplate is not None: registrationFileTemplate.file.delete() # Supprimer le fichier uploadé registrationFileTemplate.delete() - return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False) + return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) else: - return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=400) + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +class RegistrationFileView(APIView): + """ + Gère la création, mise à jour et suppression de fichiers liés à un dossier d’inscription. + """ + parser_classes = (MultiPartParser, FormParser) + + def get(self, request, _id=None): + """ + Récupère les fichiers liés à un dossier d’inscription donné. + """ + if (_id is None): + files = RegistrationFile.objects.all() + serializer = RegistrationFileSerializer(files, many=True) + return Response(serializer.data) + else: + registationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id) + if registationFile is None: + return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileSerializer(registationFile) + return JsonResponse(serializer.data, safe=False) + + def post(self, request): + """ + Crée un RegistrationFile pour le RegistrationForm associé. + """ + serializer = RegistrationFileSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + def put(self, request, fileId): + """ + Met à jour un RegistrationFile existant. + """ + registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=fileId) + if registrationFile is None: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileSerializer(registrationFile, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response({'message': 'Fichier mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, _id): + """ + Supprime un RegistrationFile existant. + """ + registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id) + if registrationFile is not None: + registrationFile.file.delete() # Supprimer le fichier uploadé + registrationFile.delete() + return JsonResponse({'message': 'La suppression du fichier a été effectuée avec succès'}, safe=False) + else: + return JsonResponse({'erreur': 'Le fichier n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + diff --git a/Back-End/start.py b/Back-End/start.py index 0b0aff0..be242c4 100644 --- a/Back-End/start.py +++ b/Back-End/start.py @@ -13,12 +13,12 @@ def run_command(command): commands = [ ["python", "manage.py", "collectstatic", "--noinput"], ["python", "manage.py", "flush", "--noinput"], - ["python", "manage.py", "makemigrations", "Subscriptions"], - ["python", "manage.py", "makemigrations", "GestionNotification"], - ["python", "manage.py", "makemigrations", "GestionMessagerie"], - ["python", "manage.py", "makemigrations", "Auth"], - ["python", "manage.py", "makemigrations", "School"], - ["python", "manage.py", "migrate"] + ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"], + ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"], + ["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"], + ["python", "manage.py", "makemigrations", "Auth", "--noinput"], + ["python", "manage.py", "makemigrations", "School", "--noinput"], + ["python", "manage.py", "migrate", "--noinput"] ] for command in commands: diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 365a30f..2f111a2 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -1,28 +1,22 @@ 'use client' import React, { useState, useEffect } from 'react'; -import { School, Calendar } from 'lucide-react'; -import TabsStructure from '@/components/Structure/Configuration/TabsStructure'; -import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement' -import StructureManagement from '@/components/Structure/Configuration/StructureManagement' -import { BE_SCHOOL_SPECIALITIES_URL, - BE_SCHOOL_SCHOOLCLASSES_URL, - BE_SCHOOL_TEACHERS_URL, - BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url'; -import DjangoCSRFToken from '@/components/DjangoCSRFToken' +import { School, Calendar, DollarSign } from 'lucide-react'; // Import de l'icône DollarSign +import StructureManagement from '@/components/Structure/Configuration/StructureManagement'; +import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'; +import FeesManagement from '@/components/Structure/Configuration/FeesManagement'; +import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; -import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules } from '@/app/lib/schoolAction'; +import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchDiscounts, fetchFees, fetchTuitionFees } from '@/app/lib/schoolAction'; +import SidebarTabs from '@/components/SidebarTabs'; export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); - const [schedules, setSchedules] = useState([]); - const [activeTab, setActiveTab] = useState('Configuration'); - const tabs = [ - { id: 'Configuration', title: "Configuration de l'école", icon: School }, - { id: 'Schedule', title: "Gestion de l'emploi du temps", icon: Calendar }, - ]; + const [fees, setFees] = useState([]); + const [discounts, setDiscounts] = useState([]); + const [tuitionFees, setTuitionFees] = useState([]); const csrfToken = useCsrfToken(); @@ -38,6 +32,15 @@ export default function Page() { // Fetch data for schedules handleSchedules(); + + // Fetch data for fees + handleFees(); + + // Fetch data for discounts + handleDiscounts(); + + // Fetch data for TuitionFee + handleTuitionFees(); }, []); const handleSpecialities = () => { @@ -45,9 +48,7 @@ export default function Page() { .then(data => { setSpecialities(data); }) - .catch(error => { - console.error('Error fetching specialities:', error); - }); + .catch(error => console.error('Error fetching specialities:', error)); }; const handleTeachers = () => { @@ -55,9 +56,7 @@ export default function Page() { .then(data => { setTeachers(data); }) - .catch(error => { - console.error('Error fetching teachers:', error); - }); + .catch(error => console.error('Error fetching teachers:', error)); }; const handleClasses = () => { @@ -65,9 +64,7 @@ export default function Page() { .then(data => { setClasses(data); }) - .catch(error => { - console.error('Error fetching classes:', error); - }); + .catch(error => console.error('Error fetching classes:', error)); }; const handleSchedules = () => { @@ -75,13 +72,35 @@ export default function Page() { .then(data => { setSchedules(data); }) - .catch(error => { - console.error('Error fetching classes:', error); - }); + .catch(error => console.error('Error fetching schedules:', error)); }; - const handleCreate = (url, newData, setDatas) => { - fetch(url, { + const handleFees = () => { + fetchFees() + .then(data => { + setFees(data); + }) + .catch(error => console.error('Error fetching fees:', error)); + }; + + const handleDiscounts = () => { + fetchDiscounts() + .then(data => { + setDiscounts(data); + }) + .catch(error => console.error('Error fetching discounts:', error)); + }; + + const handleTuitionFees = () => { + fetchTuitionFees() + .then(data => { + setTuitionFees(data); + }) + .catch(error => console.error('Error fetching tuition fees', error)); + }; + + const handleCreate = (url, newData, setDatas, setErrors) => { + return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -90,18 +109,28 @@ export default function Page() { body: JSON.stringify(newData), credentials: 'include' }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + return response.json().then(errorData => { + throw errorData; + }); + } + return response.json(); + }) .then(data => { - console.log('Succes :', data); setDatas(prevState => [...prevState, data]); + setErrors({}); + return data; }) .catch(error => { - console.error('Erreur :', error); + setErrors(error); + console.error('Error creating data:', error); + throw error; }); }; - const handleEdit = (url, id, updatedData, setDatas) => { - fetch(`${url}/${id}`, { + const handleEdit = (url, id, updatedData, setDatas, setErrors) => { + return fetch(`${url}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -110,15 +139,41 @@ export default function Page() { body: JSON.stringify(updatedData), credentials: 'include' }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + return response.json().then(errorData => { + throw errorData; + }); + } + return response.json(); + }) .then(data => { setDatas(prevState => prevState.map(item => item.id === id ? data : item)); + setErrors({}); + return data; }) .catch(error => { - console.error('Erreur :', error); + setErrors(error); + console.error('Error editing data:', error); + throw error; }); }; + const handleDelete = (url, id, setDatas) => { + fetch(`${url}/${id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + credentials: 'include' + }) + .then(response => response.json()) + .then(data => { + setDatas(prevState => prevState.filter(item => item.id !== id)); + }) + .catch(error => console.error('Error deleting data:', error)); + }; const handleUpdatePlanning = (url, planningId, updatedData) => { fetch(`${url}/${planningId}`, { method: 'PUT', @@ -139,35 +194,11 @@ export default function Page() { }); }; - const handleDelete = (url, id, setDatas) => { - fetch(`${url}/${id}`, { - method:'DELETE', - headers: { - 'Content-Type':'application/json', - 'X-CSRFToken': csrfToken - }, - credentials: 'include' - }) - .then(response => response.json()) - .then(data => { - console.log('Success:', data); - setDatas(prevState => prevState.filter(item => item.id !== id)); - }) - .catch(error => { - console.error('Error fetching data:', error); - error = error.errorMessage; - console.log(error); - }); - }; - - return ( -
- - - - - {activeTab === 'Configuration' && ( - <> + const tabs = [ + { + id: 'Configuration', + label: "Configuration de l'école", + content: ( - - )} - - {activeTab === 'Schedule' && ( + handleDelete={handleDelete} + /> + ) + }, + { + id: 'Schedule', + label: "Gestion de l'emploi du temps", + content: ( - )} + ) + }, + { + id: 'Fees', + label: 'Tarifications', + content: ( + + ) + } + ]; + + return ( +
+ + +
+ +
+
); -}; +} diff --git a/Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js b/Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js new file mode 100644 index 0000000..38a4bbb --- /dev/null +++ b/Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import { Upload } from 'lucide-react'; + +export default function DraggableFileUpload({ fileName, onFileSelect }) { + const [dragActive, setDragActive] = useState(false); + + + const handleDragOver = (event) => { + event.preventDefault(); + setDragActive(true); + }; + + const handleDragLeave = () => { + setDragActive(false); + }; + + const handleFileChosen = (selectedFile) => { + onFileSelect && onFileSelect(selectedFile); + }; + + const handleDrop = (event) => { + event.preventDefault(); + setDragActive(false); + const droppedFile = event.dataTransfer.files[0]; + handleFileChosen(droppedFile); + }; + + const handleFileChange = (event) => { + const selectedFile = event.target.files[0]; + handleFileChosen(selectedFile); + }; + + return ( +
+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js b/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js index db1c60b..92cd485 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js @@ -1,61 +1,47 @@ -import React, { useState } from 'react'; -import { Upload } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch +import DraggableFileUpload from './DraggableFileUpload'; -export default function FileUpload({ onFileUpload }) { - const [dragActive, setDragActive] = useState(false); +export default function FileUpload({ onFileUpload, fileToEdit = null }) { const [fileName, setFileName] = useState(''); const [file, setFile] = useState(null); + const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired + const [order, setOrder] = useState(0); - const handleDragOver = (event) => { - event.preventDefault(); - setDragActive(true); - }; - - const handleDragLeave = () => { - setDragActive(false); - }; - - const handleDrop = (event) => { - event.preventDefault(); - setDragActive(false); - const droppedFile = event.dataTransfer.files[0]; - setFile(droppedFile); - setFileName(droppedFile.name.replace(/\.[^/.]+$/, "")); - }; - - const handleFileChange = (event) => { - const selectedFile = event.target.files[0]; - setFile(selectedFile); - setFileName(selectedFile.name.replace(/\.[^/.]+$/, "")); - }; + useEffect(() => { + if (fileToEdit) { + setFileName(fileToEdit.name || ''); + setIsRequired(fileToEdit.is_required || false); + setOrder(fileToEdit.fusion_order || 0); + } + }, [fileToEdit]); const handleFileNameChange = (event) => { setFileName(event.target.value); }; const handleUpload = () => { - - onFileUpload(file, fileName); - setFile(null); - setFileName(''); - + onFileUpload({ + file, + name: fileName, + is_required: isRequired, + order: parseInt(order, 10), + }); + setFile(null); + setFileName(''); + setIsRequired(false); + setOrder(0); }; return (
-
- - -
+ { + setFile(selectedFile); + setFileName(selectedFile.name.replace(/\.[^/.]+$/, "")); + }} + />
+ setOrder(e.target.value)} + placeholder="Ordre de fusion" + className="p-2 border border-gray-200 rounded-md ml-2 w-20" + />
+
+ setIsRequired(!isRequired)} + /> +
); } \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js index 9f7cab6..35d0520 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js @@ -17,6 +17,7 @@ export default function Page() { const [initialData, setInitialData] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [formErrors, setFormErrors] = useState({}); const csrfToken = useCsrfToken(); useEffect(() => { @@ -55,9 +56,8 @@ export default function Page() { console.error('Error:', error.message); if (error.details) { console.error('Form errors:', error.details); - // Handle form errors (e.g., display them to the user) + setFormErrors(error.details); } - alert('Une erreur est survenue lors de la mise à jour des données'); }); }; @@ -69,6 +69,7 @@ export default function Page() { onSubmit={handleSubmit} cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL} isLoading={isLoading} + errors={formErrors} /> ); } \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 10a865c..5ddb2da 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -28,6 +28,7 @@ import { fetchRegisterFormFileTemplate, deleteRegisterFormFileTemplate, createRegistrationFormFileTemplate, + editRegistrationFormFileTemplate, fetchStudents, editRegisterForm } from "@/app/lib/subscriptionAction" @@ -40,6 +41,7 @@ import { import DjangoCSRFToken from '@/components/DjangoCSRFToken' import useCsrfToken from '@/hooks/useCsrfToken'; +import { formatDate } from '@/utils/Date'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; @@ -69,6 +71,9 @@ export default function Page({ params: { locale } }) { const [classes, setClasses] = useState([]); const [students, setEleves] = useState([]); const [reloadFetch, setReloadFetch] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [fileToEdit, setFileToEdit] = useState(null); const csrfToken = useCsrfToken(); @@ -185,7 +190,11 @@ const registerFormArchivedDataHandler = (data) => { .then(registerFormArchivedDataHandler) .catch(requestErrorHandler) fetchRegisterFormFileTemplate() - .then((data)=> {setFichiers(data)}) + .then((data)=> { + console.log(data); + + setFichiers(data) + }) .catch((err)=>{ err = err.message; console.log(err);}); } else { setTimeout(() => { @@ -548,9 +557,17 @@ const handleFileDelete = (fileId) => { }); }; +const handleFileEdit = (file) => { + setIsEditing(true); + setFileToEdit(file); + setIsModalOpen(true); +}; + const columnsFiles = [ { name: 'Nom du fichier', transform: (row) => row.name }, - { name: 'Date de création', transform: (row) => row.last_update }, + { name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") }, + { name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' }, + { name: 'Ordre de fusion', transform: (row) => row.order }, { name: 'Actions', transform: (row) => (
{ @@ -559,6 +576,9 @@ const columnsFiles = [ ) } + @@ -566,27 +586,43 @@ const columnsFiles = [ ) }, ]; -const handleFileUpload = (file, fileName) => { - if ( !fileName) { +const handleFileUpload = ({file, name, is_required, order}) => { + if (!name) { alert('Veuillez entrer un nom de fichier.'); return; } - const formData = new FormData(); if(file){ formData.append('file', file); } - formData.append('name', fileName); - createRegistrationFormFileTemplate(formData,csrfToken) - .then(data => { - console.log('Success:', data); - setFichiers([...fichiers, data]); - closeUploadModal(); - }) - .catch(error => { - console.error('Error uploading file:', error); - }); + formData.append('name', name); + formData.append('is_required', is_required); + formData.append('order', order); + + if (isEditing && fileToEdit) { + editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken) + .then(data => { + setFichiers(prevFichiers => + prevFichiers.map(f => f.id === fileToEdit.id ? data : f) + ); + setIsModalOpen(false); + setFileToEdit(null); + setIsEditing(false); + }) + .catch(error => { + console.error('Error editing file:', error); + }); + } else { + createRegistrationFormFileTemplate(formData, csrfToken) + .then(data => { + setFichiers([...fichiers, data]); + setIsModalOpen(false); + }) + .catch(error => { + console.error('Error uploading file:', error); + }); + } }; if (isLoading) { @@ -699,7 +735,23 @@ const handleFileUpload = (file, fileName) => { {/*SI STATE == subscribeFiles */} {activeTab === 'subscribeFiles' && (
- + + ( + + )} + />
{ - setUserId(userId); - fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${userId}`, { - headers: { - 'Content-Type': 'application/json', - }, - }).then(response => response.json()) + setUserId(userId) + fetchMessages(userId) .then(data => { if (data) { setMessages(data); @@ -33,11 +31,10 @@ export default function Layout({ .catch(error => { console.error('Error fetching data:', error); }); - }, []); return ( - <> +
{/* Entête */}
@@ -85,7 +82,7 @@ export default function Layout({ {children}
- +
); } diff --git a/Front-End/src/app/lib/messagerieAction.js b/Front-End/src/app/lib/messagerieAction.js new file mode 100644 index 0000000..d7623fc --- /dev/null +++ b/Front-End/src/app/lib/messagerieAction.js @@ -0,0 +1,24 @@ +import { +BE_GESTIONMESSAGERIE_MESSAGES_URL +} from '@/utils/Url'; + +const requestResponseHandler = async (response) => { + + const body = await response.json(); + if (response.ok) { + return body; + } + // Throw an error with the JSON body containing the form errors + const error = new Error('Form submission error'); + error.details = body; + throw error; +} + + +export const fetchMessages = (id) =>{ + return fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${id}`, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(requestResponseHandler) +} \ No newline at end of file diff --git a/Front-End/src/app/lib/schoolAction.js b/Front-End/src/app/lib/schoolAction.js index 6ee39ba..9675da9 100644 --- a/Front-End/src/app/lib/schoolAction.js +++ b/Front-End/src/app/lib/schoolAction.js @@ -2,7 +2,10 @@ import { BE_SCHOOL_SPECIALITIES_URL, BE_SCHOOL_TEACHERS_URL, BE_SCHOOL_SCHOOLCLASSES_URL, - BE_SCHOOL_PLANNINGS_URL + BE_SCHOOL_PLANNINGS_URL, + BE_SCHOOL_FEES_URL, + BE_SCHOOL_DISCOUNTS_URL, + BE_SCHOOL_TUITION_FEES_URL } from '@/utils/Url'; const requestResponseHandler = async (response) => { @@ -36,4 +39,19 @@ export const fetchClasses = () => { export const fetchSchedules = () => { return fetch(`${BE_SCHOOL_PLANNINGS_URL}`) .then(requestResponseHandler) -}; \ No newline at end of file +}; + +export const fetchDiscounts = () => { + return fetch(`${BE_SCHOOL_DISCOUNTS_URL}`) + .then(requestResponseHandler) +}; + +export const fetchFees = () => { + return fetch(`${BE_SCHOOL_FEES_URL}`) + .then(requestResponseHandler) +}; + +export const fetchTuitionFees = () => { + return fetch(`${BE_SCHOOL_TUITION_FEES_URL}`) + .then(requestResponseHandler) +}; diff --git a/Front-End/src/app/lib/subscriptionAction.js b/Front-End/src/app/lib/subscriptionAction.js index 7adabb5..39ff411 100644 --- a/Front-End/src/app/lib/subscriptionAction.js +++ b/Front-End/src/app/lib/subscriptionAction.js @@ -7,7 +7,8 @@ import { BE_SUBSCRIPTION_REGISTERFORM_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL, BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL, - BE_SUBSCRIPTION_LAST_GUARDIAN_URL + BE_SUBSCRIPTION_LAST_GUARDIAN_URL, + BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL } from '@/utils/Url'; export const PENDING = 'pending'; @@ -110,6 +111,32 @@ export const fetchRegisterFormFileTemplate = () => { return fetch(request).then(requestResponseHandler) }; +export const fetchRegisterFormFile = (id) => { + const request = new Request( + `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${id}`, + { + method:'GET', + headers: { + 'Content-Type':'application/json' + }, + } + ); + return fetch(request).then(requestResponseHandler) +}; + +export const createRegistrationFormFile = (data,csrfToken) => { + + return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}`, { + method: 'POST', + body: data, + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include', + }) + .then(requestResponseHandler) +} + export const createRegistrationFormFileTemplate = (data,csrfToken) => { return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, { @@ -132,6 +159,19 @@ export const deleteRegisterFormFileTemplate = (fileId,csrfToken) => { credentials: 'include', }) } + +export const editRegistrationFormFileTemplate = (fileId, data, csrfToken) => { + return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}/${fileId}`, { + method: 'PUT', + body: data, + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include', + }) + .then(requestResponseHandler) +} + export const fetchStudents = () => { const request = new Request( `${BE_SUBSCRIPTION_STUDENTS_URL}`, diff --git a/Front-End/src/components/InputColorIcon.js b/Front-End/src/components/InputColorIcon.js index 1aa8243..7120dec 100644 --- a/Front-End/src/components/InputColorIcon.js +++ b/Front-End/src/components/InputColorIcon.js @@ -6,8 +6,8 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) = <>
-
- +
+
{errorMsg &&

{errorMsg}

} diff --git a/Front-End/src/components/InputPhone.js b/Front-End/src/components/InputPhone.js index 26e4dfb..4f11a1a 100644 --- a/Front-End/src/components/InputPhone.js +++ b/Front-End/src/components/InputPhone.js @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { isValidPhoneNumber } from 'react-phone-number-input'; + export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className }) { const inputRef = useRef(null); @@ -19,12 +19,12 @@ export default function InputPhone({ name, label, value, onChange, errorMsg, pla <>
-
+
- +
{errorMsg &&

{errorMsg}

} diff --git a/Front-End/src/components/InputTextIcon.js b/Front-End/src/components/InputTextIcon.js index 1f792b8..8b60ba2 100644 --- a/Front-End/src/components/InputTextIcon.js +++ b/Front-End/src/components/InputTextIcon.js @@ -1,11 +1,10 @@ export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) { - return ( <>
-
- +
+ {IconItem && }
{errorMsg &&

{errorMsg}

} diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index 3d2a0f8..051f749 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -5,8 +5,12 @@ import ResponsableInputFields from '@/components/Inscription/ResponsableInputFie import Loader from '@/components/Loader'; import Button from '@/components/Button'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; -import FileUpload from '@/app/[locale]/admin/subscriptions/components/FileUpload'; import Table from '@/components/Table'; +import { fetchRegisterFormFileTemplate, createRegistrationFormFile } from '@/app/lib/subscriptionAction'; +import { Download, Upload } from 'lucide-react'; +import { BASE_URL } from '@/utils/Url'; +import DraggableFileUpload from '@/app/[locale]/admin/subscriptions/components/DraggableFileUpload'; +import Modal from '@/components/Modal'; const levels = [ { value:'1', label: 'TPS - Très Petite Section'}, @@ -20,7 +24,8 @@ export default function InscriptionFormShared({ csrfToken, onSubmit, cancelUrl, - isLoading = false + isLoading = false, + errors = {} // Nouvelle prop pour les erreurs }) { const [formData, setFormData] = useState(() => ({ @@ -41,6 +46,11 @@ export default function InscriptionFormShared({ ); const [uploadedFiles, setUploadedFiles] = useState([]); + const [fileTemplates, setFileTemplates] = useState([]); + const [fileName, setFileName] = useState(""); + const [file, setFile] = useState(""); + const [showUploadModal, setShowUploadModal] = useState(false); + const [currentTemplateId, setCurrentTemplateId] = useState(null); // Mettre à jour les données quand initialData change useEffect(() => { @@ -58,6 +68,9 @@ export default function InscriptionFormShared({ level: initialData.level || '' }); setGuardians(initialData.guardians || []); + fetchRegisterFormFileTemplate().then((data) => { + setFileTemplates(data); + }); } }, [initialData]); @@ -65,8 +78,22 @@ export default function InscriptionFormShared({ setFormData(prev => ({...prev, [field]: value})); }; - const handleFileUpload = (file, fileName) => { - setUploadedFiles([...uploadedFiles, { file, fileName }]); + const handleFileUpload = async (file, fileName) => { + const data = new FormData(); + data.append('file', file); + data.append('name',fileName); + data.append('template', currentTemplateId); + data.append('register_form', formData.id); + + try { + await createRegistrationFormFile(data, csrfToken); + // Optionnellement, rafraîchir la liste des fichiers + fetchRegisterFormFileTemplate().then((data) => { + setFileTemplates(data); + }); + } catch (error) { + console.error('Error uploading file:', error); + } }; const handleSubmit = (e) => { @@ -80,12 +107,31 @@ export default function InscriptionFormShared({ onSubmit(data); }; + const getError = (field) => { + return errors?.student?.[field]?.[0]; + }; + + const getGuardianError = (index, field) => { + return errors?.student?.guardians?.[index]?.[field]?.[0]; + }; + const columns = [ - { name: 'Nom du fichier', transform: (row) => row.last_name }, + { name: 'Nom du fichier', transform: (row) => row.name }, + { name: 'Fichier à Remplir', transform: (row) => row.is_required ? 'Oui' : 'Non' }, + { name: 'Fichier de référence', transform: (row) => row.file && }, { name: 'Actions', transform: (row) => ( - - Télécharger - +
+ {row.is_required && + + } +
) }, ]; @@ -105,12 +151,14 @@ export default function InscriptionFormShared({ value={formData.last_name} onChange={(e) => updateFormField('last_name', e.target.value)} required + errorMsg={getError('last_name')} /> updateFormField('first_name', e.target.value)} + errorMsg={getError('first_name')} required /> updateFormField('birth_date', e.target.value)} required + errorMsg={getError('birth_date')} /> updateFormField('birth_place', e.target.value)} + errorMsg={getError('birth_place')} /> updateFormField('birth_postal_code', e.target.value)} + required + errorMsg={getError('birth_postal_code')} />
updateFormField('address', e.target.value)} + errorMsg={getError('address')} />
updateFormField('attending_physician', e.target.value)} + errorMsg={getError('attending_physician')} /> updateFormField('level', e.target.value)} choices={levels} required + errorMsg={getError('level')} />
@@ -184,6 +240,7 @@ export default function InscriptionFormShared({ newArray.splice(index, 1); setGuardians(newArray); }} + errors={errors?.student?.guardians || []} />
@@ -191,14 +248,13 @@ export default function InscriptionFormShared({

Fichiers à remplir

{}} /> - {/* Boutons de contrôle */} @@ -207,6 +263,44 @@ export default function InscriptionFormShared({ - +
+ {!uniqueConfirmButton && ( + + )} +
, diff --git a/Front-End/src/components/ProtectedRoute.js b/Front-End/src/components/ProtectedRoute.js new file mode 100644 index 0000000..61c7671 --- /dev/null +++ b/Front-End/src/components/ProtectedRoute.js @@ -0,0 +1,21 @@ +import React, { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import useLocalStorage from '@/hooks/useLocalStorage'; +import { FE_USERS_LOGIN_URL } from '@/utils/Url'; + +const ProtectedRoute = ({ children }) => { + const router = useRouter(); + const [userId] = useLocalStorage("userId", ''); + + useEffect(() => { + if (!userId) { + // Rediriger vers la page de login si l'utilisateur n'est pas connecté + router.push(FE_USERS_LOGIN_URL); + } + }, [userId, router]); + + // Afficher les enfants seulement si l'utilisateur est connecté + return userId ? children : null; +}; + +export default ProtectedRoute; \ No newline at end of file diff --git a/Front-End/src/components/SelectChoice.js b/Front-End/src/components/SelectChoice.js index 4040ea1..6e3fbd7 100644 --- a/Front-End/src/components/SelectChoice.js +++ b/Front-End/src/components/SelectChoice.js @@ -1,34 +1,36 @@ -export default function SelectChoice({ type, name, label, choices, callback, selected, error, IconItem, disabled = false }) { +export default function SelectChoice({ type, name, label,required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) { return ( <>
- -
- - {IconItem && } - + +
+ {IconItem && + + {} + + }
- {error &&

{error}

} + {errorMsg &&

{errorMsg}

}
); -} - - \ No newline at end of file +} \ No newline at end of file diff --git a/Front-End/src/components/SidebarTabs.js b/Front-End/src/components/SidebarTabs.js new file mode 100644 index 0000000..ada4f20 --- /dev/null +++ b/Front-End/src/components/SidebarTabs.js @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; + +const SidebarTabs = ({ tabs }) => { + const [activeTab, setActiveTab] = useState(tabs[0].id); + + return ( +
+
+ {tabs.map(tab => ( + + ))} +
+
+ {tabs.map(tab => ( +
+ {tab.content} +
+ ))} +
+
+ ); +}; + +export default SidebarTabs; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/ClassForm.js b/Front-End/src/components/Structure/Configuration/ClassForm.js index 1d333fe..01cf73e 100644 --- a/Front-End/src/components/Structure/Configuration/ClassForm.js +++ b/Front-End/src/components/Structure/Configuration/ClassForm.js @@ -34,15 +34,15 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { setFormData(prevState => { const updatedTimes = [...prevState.time_range]; updatedTimes[index] = value; - + const updatedFormData = { ...prevState, time_range: updatedTimes, }; - + const existingPlannings = prevState.plannings || []; updatedFormData.plannings = updatePlannings(updatedFormData, existingPlannings); - + return updatedFormData; }); }; @@ -50,24 +50,24 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { const handleJoursChange = (e) => { const { value, checked } = e.target; const dayId = parseInt(value, 10); - + setFormData((prevState) => { const updatedJoursOuverture = checked ? [...prevState.opening_days, dayId] : prevState.opening_days.filter((id) => id !== dayId); - + const updatedFormData = { ...prevState, opening_days: updatedJoursOuverture, }; - + const existingPlannings = prevState.plannings || []; updatedFormData.plannings = updatePlannings(updatedFormData, existingPlannings); - + return updatedFormData; }); }; - + const handleChange = (e) => { e.preventDefault(); const { name, value, type, checked } = e.target; @@ -78,8 +78,8 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { let newState = { ...prevState }; if (type === 'checkbox') { - const newValues = checked - ? [...(prevState[name] || []), parseInt(value)] + const newValues = checked + ? [...(prevState[name] || []), parseInt(value)] : (prevState[name] || []).filter(v => v !== parseInt(value)); newState[name] = newValues; } else if (name === 'age_range') { @@ -117,14 +117,14 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { return (
- +
{/* Section Ambiance */}
- { />
- {
- {
- {
{/* Section Enseignants */} - {/* Section Emploi du temps */} -
diff --git a/Front-End/src/components/Structure/Configuration/ClassesSection.js b/Front-End/src/components/Structure/Configuration/ClassesSection.js index 8b4cba3..d2f8d07 100644 --- a/Front-End/src/components/Structure/Configuration/ClassesSection.js +++ b/Front-End/src/components/Structure/Configuration/ClassesSection.js @@ -1,4 +1,4 @@ -import { Users, Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react'; +import { Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react'; import { useState } from 'react'; import Table from '@/components/Table'; import DropdownMenu from '@/components/DropdownMenu'; @@ -49,11 +49,8 @@ const ClassesSection = ({ classes, teachers, handleCreate, handleEdit, handleDel return (
-
-

- - Classes -

+
+

Gestion des classes

+ +
+ ); + default: + return null; + } + } else { + switch (column) { + case 'LIBELLE': + return discount.name; + case 'MONTANT': + return discount.amount + ' €'; + case 'DESCRIPTION': + return discount.description; + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } + }; + + return ( +
+
+

Réductions

+ +
+
+ setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + ); +}; + +export default DiscountsSection; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/FeesManagement.js b/Front-End/src/components/Structure/Configuration/FeesManagement.js new file mode 100644 index 0000000..ef4ef98 --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/FeesManagement.js @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; +import FeesSection from './FeesSection'; +import DiscountsSection from './DiscountsSection'; +import TuitionFeesSection from './TuitionFeesSection'; +import { TuitionFeesProvider } from '@/context/TuitionFeesContext'; +import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL, BE_SCHOOL_TUITION_FEE_URL } from '@/utils/Url'; + +const FeesManagement = ({ fees, setFees, discounts, setDiscounts, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { + const [errors, setErrors] = useState({}); + + return ( + +
+
+ handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees, setErrors)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees, setErrors)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)} + errors + /> +
+
+ handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts, setErrors)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts, setErrors)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)} + /> +
+
+ handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees, setErrors)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees, setErrors)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)} + /> +
+
+
+ ); +}; + +export default FeesManagement; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/FeesSection.js b/Front-End/src/components/Structure/Configuration/FeesSection.js new file mode 100644 index 0000000..60f494c --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/FeesSection.js @@ -0,0 +1,190 @@ +import React, { useState } from 'react'; +import { Plus, Trash, Edit3, Check, X } from 'lucide-react'; +import Table from '@/components/Table'; +import InputTextIcon from '@/components/InputTextIcon'; +import Popup from '@/components/Popup'; + +const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) => { + const [editingFee, setEditingFee] = useState(null); + const [newFee, setNewFee] = useState(null); + const [formData, setFormData] = useState({}); + const [localErrors, setLocalErrors] = useState({}); + const [popupVisible, setPopupVisible] = useState(false); + const [popupMessage, setPopupMessage] = useState(""); + + const handleAddFee = () => { + setNewFee({ id: Date.now(), name: '', amount: '', description: '' }); + }; + + const handleRemoveFee = (id) => { + handleDelete(id); + }; + + const handleSaveNewFee = () => { + if (newFee.name && newFee.amount) { + handleCreate(newFee) + .then(() => { + setNewFee(null); + setLocalErrors({}); + }) + .catch(error => { + if (error && typeof error === 'object') { + setLocalErrors(error); + } else { + console.error(error); + } + }); + } else { + setPopupMessage("Tous les champs doivent être remplis"); + setPopupVisible(true); + } + }; + + const handleUpdateFee = (id, updatedFee) => { + if (updatedFee.name && updatedFee.amount) { + handleEdit(id, updatedFee) + .then(() => { + setEditingFee(null); + setLocalErrors({}); + }) + .catch(error => { + if (error && typeof error === 'object') { + setLocalErrors(error); + } else { + console.error(error); + } + }); + } else { + setPopupMessage("Tous les champs doivent être remplis"); + setPopupVisible(true); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + if (editingFee) { + setFormData((prevData) => ({ + ...prevData, + [name]: value, + })); + } else if (newFee) { + setNewFee((prevData) => ({ + ...prevData, + [name]: value, + })); + } + }; + + const renderInputField = (field, value, onChange, placeholder) => ( +
+ +
+ ); + + const renderFeeCell = (fee, column) => { + const isEditing = editingFee === fee.id; + const isCreating = newFee && newFee.id === fee.id; + const currentData = isEditing ? formData : newFee; + + if (isEditing || isCreating) { + switch (column) { + case 'LIBELLE': + return renderInputField('name', currentData.name, handleChange, 'Libellé du frais'); + case 'MONTANT': + return renderInputField('amount', currentData.amount, handleChange, 'Montant'); + case 'DESCRIPTION': + return renderInputField('description', currentData.description, handleChange, 'Description'); + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } else { + switch (column) { + case 'LIBELLE': + return fee.name; + case 'MONTANT': + return fee.amount + ' €'; + case 'DESCRIPTION': + return fee.description; + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } + }; + + return ( + <> +
+
+

Frais d'inscription

+ +
+
+ + setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + ); +}; + +export default FeesSection; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js index 8ec7b45..b4149bc 100644 --- a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js +++ b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js @@ -1,4 +1,4 @@ -import { BookOpen, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react'; +import { Trash2, MoreVertical, Edit3, Plus } from 'lucide-react'; import { useState } from 'react'; import Table from '@/components/Table'; import DropdownMenu from '@/components/DropdownMenu'; @@ -33,10 +33,7 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel return (
-

- - Spécialités -

+

Gestion des spécialités

+ +
+ ); + default: + return null; + } + } else { + switch (column) { + case 'NOM': + return tuitionFee.name; + case 'MONTANT DE BASE': + return tuitionFee.base_amount + ' €'; + case 'DESCRIPTION': + return tuitionFee.description; + case 'DATE DE DEBUT': + return tuitionFee.validity_start_date; + case 'DATE DE FIN': + return tuitionFee.validity_end_date; + case 'OPTIONS DE PAIEMENT': + return paymentOptions.find(option => option.value === tuitionFee.payment_option)?.label || ''; + case 'REMISES': + const discountNames = tuitionFee.discounts + .map(discountId => discounts.find(discount => discount.id === discountId)?.name) + .filter(name => name) + .join(', '); + return discountNames; + case 'MONTANT FINAL': + return calculateFinalAmount(tuitionFee.base_amount, tuitionFee.discounts) + ' €'; + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } + }; + + return ( +
+
+

Frais de scolarité

+ +
+
+ setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + ); +}; + +export default TuitionFeesSection; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Planning/SpecialityEventModal.js b/Front-End/src/components/Structure/Planning/SpecialityEventModal.js index bd4b74e..3c93d1b 100644 --- a/Front-End/src/components/Structure/Planning/SpecialityEventModal.js +++ b/Front-End/src/components/Structure/Planning/SpecialityEventModal.js @@ -160,7 +160,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
{
- -
diff --git a/Front-End/src/context/ClassesContext.js b/Front-End/src/context/ClassesContext.js index 7cc291c..21b86b0 100644 --- a/Front-End/src/context/ClassesContext.js +++ b/Front-End/src/context/ClassesContext.js @@ -9,7 +9,6 @@ export const ClassesProvider = ({ children }) => { const currentYear = new Date().getFullYear(); const schoolYears = [ - { value: '', label: 'Sélectionner une période' }, { value: `${currentYear - 1}-${currentYear}`, label: `${currentYear - 1}-${currentYear}` }, { value: `${currentYear}-${currentYear + 1}`, label: `${currentYear}-${currentYear + 1}` }, { value: `${currentYear + 1}-${currentYear + 2}`, label: `${currentYear + 1}-${currentYear + 2}` }, diff --git a/Front-End/src/context/TuitionFeesContext.js b/Front-End/src/context/TuitionFeesContext.js new file mode 100644 index 0000000..dd356e3 --- /dev/null +++ b/Front-End/src/context/TuitionFeesContext.js @@ -0,0 +1,24 @@ +import React, { createContext, useState, useEffect, useContext } from 'react'; +import { fetchTuitionFees, fetchFees, fetchDiscounts } from '@/app/lib/schoolAction'; + +const TuitionFeesContext = createContext(); + +export const useTuitionFees = () => useContext(TuitionFeesContext); + +export const TuitionFeesProvider = ({ children }) => { + const [tuitionFees, setTuitionFees] = useState([]); + const [fees, setFees] = useState([]); + const [discounts, setDiscounts] = useState([]); + + useEffect(() => { + fetchTuitionFees().then(data => setTuitionFees(data)); + fetchFees().then(data => setFees(data)); + fetchDiscounts().then(data => setDiscounts(data)); + }, []); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index b99268e..82c3e20 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -23,6 +23,7 @@ export const BE_SUBSCRIPTION_ARCHIVE_URL = `${BASE_URL}/Subscriptions/archive` export const BE_SUBSCRIPTION_REGISTERFORM_URL = `${BASE_URL}/Subscriptions/registerForm` export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms` export const BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL = `${BASE_URL}/Subscriptions/registrationFileTemplates` +export const BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL = `${BASE_URL}/Subscriptions/registrationFiles` export const BE_SUBSCRIPTION_LAST_GUARDIAN_URL = `${BASE_URL}/Subscriptions/lastGuardian` //GESTION ENSEIGNANT @@ -34,6 +35,12 @@ export const BE_SCHOOL_TEACHER_URL = `${BASE_URL}/School/teacher` export const BE_SCHOOL_TEACHERS_URL = `${BASE_URL}/School/teachers` export const BE_SCHOOL_PLANNING_URL = `${BASE_URL}/School/planning` export const BE_SCHOOL_PLANNINGS_URL = `${BASE_URL}/School/plannings` +export const BE_SCHOOL_FEE_URL = `${BASE_URL}/School/fee`; +export const BE_SCHOOL_FEES_URL = `${BASE_URL}/School/fees`; +export const BE_SCHOOL_DISCOUNT_URL = `${BASE_URL}/School/discount`; +export const BE_SCHOOL_DISCOUNTS_URL = `${BASE_URL}/School/discounts`; +export const BE_SCHOOL_TUITION_FEE_URL = `${BASE_URL}/School/tuitionFee`; +export const BE_SCHOOL_TUITION_FEES_URL = `${BASE_URL}/School/tuitionFees`; // GESTION MESSAGERIE export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie` @@ -47,8 +54,7 @@ export const FE_USERS_SUBSCRIBE_URL = `/users/subscribe` export const FE_USERS_RESET_PASSWORD_URL = `/users/password/reset` export const FE_USERS_NEW_PASSWORD_URL = `/users/password/new` - -//ADMIN +// ADMIN export const FE_ADMIN_HOME_URL = `/admin` // ADMIN/SUBSCRIPTIONS URL From 8d1a41e2693c3704b68e8d75bd32c4a89a6389e5 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Mon, 20 Jan 2025 20:42:51 +0100 Subject: [PATCH 007/249] =?UTF-8?q?feat:=20Mise=20=C3=A0=20jour=20du=20mod?= =?UTF-8?q?=C3=A8le=20(possibilit=C3=A9=20d'associer=20une=20r=C3=A9ducito?= =?UTF-8?q?n=20=C3=A0=20un=20frais=20d'inscription=20[#18]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/School/models.py | 34 +++--- Back-End/School/serializers.py | 44 ++++++-- .../src/app/[locale]/admin/structure/page.js | 26 +++-- Front-End/src/components/InputTextIcon.js | 2 +- .../Configuration/DiscountsSection.js | 4 +- .../Structure/Configuration/FeesManagement.js | 61 +++++------ .../Structure/Configuration/FeesSection.js | 101 +++++++++++++++--- .../Configuration/TuitionFeesSection.js | 79 +++++++------- Front-End/src/context/TuitionFeesContext.js | 24 ----- 9 files changed, 224 insertions(+), 151 deletions(-) delete mode 100644 Front-End/src/context/TuitionFeesContext.js diff --git a/Back-End/School/models.py b/Back-End/School/models.py index a992eac..25967b2 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -68,6 +68,11 @@ class Planning(models.Model): def __str__(self): return f'Planning for {self.level} of {self.school_class.atmosphere_name}' +class PaymentOptions(models.IntegerChoices): + SINGLE_PAYMENT = 0, _('Paiement en une seule fois') + FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois') + TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois') + class Discount(models.Model): name = models.CharField(max_length=255, unique=True) amount = models.DecimalField(max_digits=10, decimal_places=2) @@ -78,25 +83,23 @@ class Discount(models.Model): class Fee(models.Model): name = models.CharField(max_length=255, unique=True) - amount = models.DecimalField(max_digits=10, decimal_places=2) description = models.TextField(blank=True) + base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) + currency = models.CharField(max_length=3, default='EUR') + discounts = models.ManyToManyField('Discount', blank=True) + payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT) + is_active = models.BooleanField(default=True) + updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name class TuitionFee(models.Model): - class PaymentOptions(models.IntegerChoices): - SINGLE_PAYMENT = 0, _('Paiement en une seule fois') - FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois') - TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois') - name = models.CharField(max_length=255, unique=True) description = models.TextField(blank=True) - base_amount = models.DecimalField(max_digits=10, decimal_places=2) + base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) currency = models.CharField(max_length=3, default='EUR') discounts = models.ManyToManyField('Discount', blank=True) - validity_start_date = models.DateField() - validity_end_date = models.DateField() payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT) is_active = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) @@ -107,16 +110,3 @@ class TuitionFee(models.Model): def clean(self): if self.validity_end_date <= self.validity_start_date: raise ValidationError(_('La date de fin de validité doit être après la date de début de validité.')) - - def calculate_final_amount(self): - amount = self.base_amount - - # Apply fees (supplements and taxes) - # for fee in self.fees.all(): - # amount += fee.amount - - # Apply discounts - for discount in self.discounts.all(): - amount -= discount.amount - - return amount diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index 2248861..08b5588 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -180,21 +180,48 @@ class DiscountSerializer(serializers.ModelSerializer): fields = '__all__' class FeeSerializer(serializers.ModelSerializer): + discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) + class Meta: model = Fee fields = '__all__' + def create(self, validated_data): + discounts_data = validated_data.pop('discounts', []) + + # Create the Fee instance + fee = Fee.objects.create(**validated_data) + + # Add discounts if provided + fee.discounts.set(discounts_data) + + return fee + + def update(self, instance, validated_data): + discounts_data = validated_data.pop('discounts', []) + + # Update the Fee instance + instance.name = validated_data.get('name', instance.name) + instance.description = validated_data.get('description', instance.description) + instance.base_amount = validated_data.get('base_amount', instance.base_amount) + instance.currency = validated_data.get('currency', instance.currency) + instance.payment_option = validated_data.get('payment_option', instance.payment_option) + instance.is_active = validated_data.get('is_active', instance.is_active) + instance.updated_at = validated_data.get('updated_at', instance.updated_at) + instance.save() + + # Update discounts if provided + instance.discounts.set(discounts_data) + + return instance + class TuitionFeeSerializer(serializers.ModelSerializer): discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) - final_amount = serializers.SerializerMethodField() class Meta: model = TuitionFee fields = '__all__' - def get_final_amount(self, obj): - return obj.calculate_final_amount() - def create(self, validated_data): discounts_data = validated_data.pop('discounts', []) @@ -202,8 +229,7 @@ class TuitionFeeSerializer(serializers.ModelSerializer): tuition_fee = TuitionFee.objects.create(**validated_data) # Add discounts if provided - for discount in discounts_data: - tuition_fee.discounts.add(discount) + tuition_fee.discounts.set(discounts_data) return tuition_fee @@ -215,14 +241,12 @@ class TuitionFeeSerializer(serializers.ModelSerializer): instance.description = validated_data.get('description', instance.description) instance.base_amount = validated_data.get('base_amount', instance.base_amount) instance.currency = validated_data.get('currency', instance.currency) - instance.validity_start_date = validated_data.get('validity_start_date', instance.validity_start_date) - instance.validity_end_date = validated_data.get('validity_end_date', instance.validity_end_date) instance.payment_option = validated_data.get('payment_option', instance.payment_option) instance.is_active = validated_data.get('is_active', instance.is_active) + instance.updated_at = validated_data.get('updated_at', instance.updated_at) instance.save() # Update discounts if provided - if discounts_data: - instance.discounts.set(discounts_data) + instance.discounts.set(discounts_data) return instance \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 2f111a2..4300560 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -99,7 +99,7 @@ export default function Page() { .catch(error => console.error('Error fetching tuition fees', error)); }; - const handleCreate = (url, newData, setDatas, setErrors) => { + const handleCreate = (url, newData, setDatas) => { return fetch(url, { method: 'POST', headers: { @@ -119,17 +119,15 @@ export default function Page() { }) .then(data => { setDatas(prevState => [...prevState, data]); - setErrors({}); return data; }) .catch(error => { - setErrors(error); console.error('Error creating data:', error); throw error; }); }; - const handleEdit = (url, id, updatedData, setDatas, setErrors) => { + const handleEdit = (url, id, updatedData, setDatas) => { return fetch(`${url}/${id}`, { method: 'PUT', headers: { @@ -149,18 +147,16 @@ export default function Page() { }) .then(data => { setDatas(prevState => prevState.map(item => item.id === id ? data : item)); - setErrors({}); return data; }) .catch(error => { - setErrors(error); console.error('Error editing data:', error); throw error; }); }; const handleDelete = (url, id, setDatas) => { - fetch(`${url}/${id}`, { + return fetch(`${url}/${id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', @@ -168,12 +164,24 @@ export default function Page() { }, credentials: 'include' }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + return response.json().then(errorData => { + throw errorData; + }); + } + return response.json(); + }) .then(data => { setDatas(prevState => prevState.filter(item => item.id !== id)); + return data; }) - .catch(error => console.error('Error deleting data:', error)); + .catch(error => { + console.error('Error deleting data:', error); + throw error; + }); }; + const handleUpdatePlanning = (url, planningId, updatedData) => { fetch(`${url}/${planningId}`, { method: 'PUT', diff --git a/Front-End/src/components/InputTextIcon.js b/Front-End/src/components/InputTextIcon.js index 8b60ba2..25aeb94 100644 --- a/Front-End/src/components/InputTextIcon.js +++ b/Front-End/src/components/InputTextIcon.js @@ -1,7 +1,7 @@ export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) { return ( <> -
+
diff --git a/Front-End/src/components/Structure/Configuration/DiscountsSection.js b/Front-End/src/components/Structure/Configuration/DiscountsSection.js index 57cbcdc..36d2271 100644 --- a/Front-End/src/components/Structure/Configuration/DiscountsSection.js +++ b/Front-End/src/components/Structure/Configuration/DiscountsSection.js @@ -4,7 +4,7 @@ import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; -const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete, errors }) => { +const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete }) => { const [editingDiscount, setEditingDiscount] = useState(null); const [newDiscount, setNewDiscount] = useState(null); const [formData, setFormData] = useState({}); @@ -17,7 +17,7 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete, e }; const handleRemoveDiscount = (id) => { - handleDelete(id); + handleDelete(id) }; const handleSaveNewDiscount = () => { diff --git a/Front-End/src/components/Structure/Configuration/FeesManagement.js b/Front-End/src/components/Structure/Configuration/FeesManagement.js index ef4ef98..7c733bc 100644 --- a/Front-End/src/components/Structure/Configuration/FeesManagement.js +++ b/Front-End/src/components/Structure/Configuration/FeesManagement.js @@ -2,41 +2,42 @@ import React, { useState } from 'react'; import FeesSection from './FeesSection'; import DiscountsSection from './DiscountsSection'; import TuitionFeesSection from './TuitionFeesSection'; -import { TuitionFeesProvider } from '@/context/TuitionFeesContext'; import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL, BE_SCHOOL_TUITION_FEE_URL } from '@/utils/Url'; -const FeesManagement = ({ fees, setFees, discounts, setDiscounts, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { - const [errors, setErrors] = useState({}); +const FeesManagement = ({ fees, setFees, discounts, setDiscounts, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { return ( - -
-
- handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees, setErrors)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees, setErrors)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)} - errors - /> -
-
- handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts, setErrors)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts, setErrors)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)} - /> -
-
- handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees, setErrors)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees, setErrors)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)} - /> -
+
+
+ handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)} + />
- +
+ handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)} + /> +
+
+ handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)} + /> +
+
); }; diff --git a/Front-End/src/components/Structure/Configuration/FeesSection.js b/Front-End/src/components/Structure/Configuration/FeesSection.js index 60f494c..518c48e 100644 --- a/Front-End/src/components/Structure/Configuration/FeesSection.js +++ b/Front-End/src/components/Structure/Configuration/FeesSection.js @@ -3,8 +3,9 @@ import { Plus, Trash, Edit3, Check, X } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; +import SelectChoice from '@/components/SelectChoice'; -const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) => { +const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete }) => { const [editingFee, setEditingFee] = useState(null); const [newFee, setNewFee] = useState(null); const [formData, setFormData] = useState({}); @@ -12,8 +13,14 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) = const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(""); + const paymentOptions = [ + { value: 0, label: '1 fois' }, + { value: 1, label: '4 fois' }, + { value: 2, label: '10 fois' } + ]; + const handleAddFee = () => { - setNewFee({ id: Date.now(), name: '', amount: '', description: '' }); + setNewFee({ id: Date.now(), name: '', base_amount: '', description: '' }); }; const handleRemoveFee = (id) => { @@ -21,7 +28,7 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) = }; const handleSaveNewFee = () => { - if (newFee.name && newFee.amount) { + if (newFee.name && newFee.base_amount) { handleCreate(newFee) .then(() => { setNewFee(null); @@ -41,7 +48,7 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) = }; const handleUpdateFee = (id, updatedFee) => { - if (updatedFee.name && updatedFee.amount) { + if (updatedFee.name && updatedFee.base_amount) { handleEdit(id, updatedFee) .then(() => { setEditingFee(null); @@ -60,26 +67,59 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) = } }; + const handleToggleActive = (id, isActive) => { + const fee = fees.find(fee => fee.id === id); + if (!fee) return; + + const updatedData = { + is_active: !isActive, + discounts: fee.discounts + }; + + handleEdit(id, updatedData) + .then(() => { + setFees(prevFees => prevFees.map(fee => fee.id === id ? { ...fee, is_active: !isActive } : fee)); + }) + .catch(error => { + console.error(error); + }); + }; + const handleChange = (e) => { const { name, value } = e.target; + let parsedValue = value; + if (name === 'discounts') { + parsedValue = value.split(',').map(v => parseInt(v, 10)); + } if (editingFee) { setFormData((prevData) => ({ ...prevData, - [name]: value, + [name]: parsedValue, })); } else if (newFee) { setNewFee((prevData) => ({ ...prevData, - [name]: value, + [name]: parsedValue, })); } }; + const calculateFinalAmount = (baseAmount, discountIds) => { + const totalDiscounts = discountIds.reduce((sum, discountId) => { + const discount = discounts.find(d => d.id === discountId); + return discount ? sum + parseFloat(discount.amount) : sum; + }, 0); + + const finalAmount = parseFloat(baseAmount) - totalDiscounts; + + return finalAmount.toFixed(2); + }; + const renderInputField = (field, value, onChange, placeholder) => (
); + const renderSelectField = (field, value, options, callback, label) => ( +
+ +
+ ); + const renderFeeCell = (fee, column) => { const isEditing = editingFee === fee.id; const isCreating = newFee && newFee.id === fee.id; @@ -97,10 +150,14 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) = switch (column) { case 'LIBELLE': return renderInputField('name', currentData.name, handleChange, 'Libellé du frais'); - case 'MONTANT': - return renderInputField('amount', currentData.amount, handleChange, 'Montant'); + case 'MONTANT DE BASE': + return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant'); case 'DESCRIPTION': return renderInputField('description', currentData.description, handleChange, 'Description'); + case 'OPTIONS DE PAIEMENT': + return renderSelectField('payment_option', currentData.payment_option, paymentOptions, handleChange, 'Options de paiement'); + case 'REMISES': + return renderSelectField('discounts', currentData.discounts, discounts.map(discount => ({ value: discount.id, label: discount.name })), handleChange, 'Remises'); case 'ACTIONS': return (
@@ -127,10 +184,20 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) = switch (column) { case 'LIBELLE': return fee.name; - case 'MONTANT': - return fee.amount + ' €'; + case 'MONTANT DE BASE': + return fee.base_amount + ' €'; case 'DESCRIPTION': return fee.description; + case 'OPTIONS DE PAIEMENT': + return paymentOptions.find(option => option.value === fee.payment_option)?.label || ''; + case 'REMISES': + const discountNames = fee.discounts + .map(discountId => discounts.find(discount => discount.id === discountId)?.name) + .filter(name => name) + .join(', '); + return discountNames; + case 'MONTANT FINAL': + return calculateFinalAmount(fee.base_amount, fee.discounts) + ' €'; case 'ACTIONS': return (
@@ -148,6 +215,13 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) = > +
); default: @@ -169,8 +243,11 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) = data={newFee ? [newFee, ...fees] : fees} columns={[ { name: 'LIBELLE', label: 'Libellé' }, - { name: 'MONTANT', label: 'Montant' }, + { name: 'MONTANT DE BASE', label: 'Montant' }, { name: 'DESCRIPTION', label: 'Description' }, + { name: 'OPTIONS DE PAIEMENT', label: 'Options de paiement' }, + { name: 'REMISES', label: 'Remises' }, + { name: 'MONTANT FINAL', label: 'Montant final' }, { name: 'ACTIONS', label: 'Actions' } ]} renderCell={renderFeeCell} diff --git a/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js b/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js index 6bdc25a..83b8c2a 100644 --- a/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js +++ b/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js @@ -1,13 +1,11 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, Calendar } from 'lucide-react'; +import { Plus, Trash, Edit3, Check, X } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; import SelectChoice from '@/components/SelectChoice'; -import { useTuitionFees } from '@/context/TuitionFeesContext'; -const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) => { - const { fees, tuitionFees, setTuitionFees, discounts } = useTuitionFees(); +const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, fees, handleCreate, handleEdit, handleDelete }) => { const [editingTuitionFee, setEditingTuitionFee] = useState(null); const [newTuitionFee, setNewTuitionFee] = useState(null); const [formData, setFormData] = useState({}); @@ -26,18 +24,20 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) }; const handleRemoveTuitionFee = (id) => { - handleDelete(id); - setTuitionFees(tuitionFees.filter(fee => fee.id !== id)); + handleDelete(id) + .then(() => { + setTuitionFees(prevTuitionFees => prevTuitionFees.filter(fee => fee.id !== id)); + }) + .catch(error => { + console.error(error); + }); }; const handleSaveNewTuitionFee = () => { if ( newTuitionFee.name && newTuitionFee.base_amount && - newTuitionFee.payment_option >= 0 && - newTuitionFee.validity_start_date && - newTuitionFee.validity_end_date && - new Date(newTuitionFee.validity_start_date) <= new Date(newTuitionFee.validity_end_date) + newTuitionFee.payment_option >= 0 ) { handleCreate(newTuitionFee) .then((createdTuitionFee) => { @@ -62,10 +62,7 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) if ( updatedTuitionFee.name && updatedTuitionFee.base_amount && - updatedTuitionFee.payment_option >= 0 && - updatedTuitionFee.validity_start_date && - updatedTuitionFee.validity_end_date && - new Date(updatedTuitionFee.validity_start_date) <= new Date(updatedTuitionFee.validity_end_date) + updatedTuitionFee.payment_option >= 0 ) { handleEdit(id, updatedTuitionFee) .then((updatedFee) => { @@ -86,6 +83,24 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) } }; + const handleToggleActive = (id, isActive) => { + const tuitionFee = tuitionFees.find(tuitionFee => tuitionFee.id === id); + if (!tuitionFee) return; + + const updatedData = { + is_active: !isActive, + discounts: tuitionFee.discounts + }; + + handleEdit(id, updatedData) + .then(() => { + setFees(prevTuitionFees => prevTuitionFees.map(tuitionFee => tuitionFee.id === id ? { ...tuitionFee, is_active: !isActive } : tuitionFee)); + }) + .catch(error => { + console.error(error); + }); + }; + const handleChange = (e) => { const { name, value } = e.target; let parsedValue = value; @@ -120,24 +135,11 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
); - const renderDateField = (field, value, onChange) => ( -
- - -
- ); - const renderSelectField = (field, value, options, callback, label) => (
{ - const totalFees = fees.reduce((sum, fee) => sum + parseFloat(fee.amount), 0); - const totalDiscounts = discountIds.reduce((sum, discountId) => { const discount = discounts.find(d => d.id === discountId); return discount ? sum + parseFloat(discount.amount) : sum; }, 0); - const finalAmount = parseFloat(baseAmount) + totalFees - totalDiscounts; + const finalAmount = parseFloat(baseAmount) - totalDiscounts; return finalAmount.toFixed(2); }; @@ -172,10 +172,6 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base'); case 'DESCRIPTION': return renderInputField('description', currentData.description, handleChange, 'Description'); - case 'DATE DE DEBUT': - return renderDateField('validity_start_date', currentData.validity_start_date, handleChange); - case 'DATE DE FIN': - return renderDateField('validity_end_date', currentData.validity_end_date, handleChange); case 'OPTIONS DE PAIEMENT': return renderSelectField('payment_option', currentData.payment_option, paymentOptions, handleChange, 'Options de paiement'); case 'REMISES': @@ -210,10 +206,6 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) return tuitionFee.base_amount + ' €'; case 'DESCRIPTION': return tuitionFee.description; - case 'DATE DE DEBUT': - return tuitionFee.validity_start_date; - case 'DATE DE FIN': - return tuitionFee.validity_end_date; case 'OPTIONS DE PAIEMENT': return paymentOptions.find(option => option.value === tuitionFee.payment_option)?.label || ''; case 'REMISES': @@ -241,6 +233,13 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) > +
); default: @@ -263,8 +262,6 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) { name: 'NOM', label: 'Nom' }, { name: 'MONTANT DE BASE', label: 'Montant de base' }, { name: 'DESCRIPTION', label: 'Description' }, - { name: 'DATE DE DEBUT', label: 'Date de début' }, - { name: 'DATE DE FIN', label: 'Date de fin' }, { name: 'OPTIONS DE PAIEMENT', label: 'Options de paiement' }, { name: 'REMISES', label: 'Remises' }, { name: 'MONTANT FINAL', label: 'Montant final' }, diff --git a/Front-End/src/context/TuitionFeesContext.js b/Front-End/src/context/TuitionFeesContext.js deleted file mode 100644 index dd356e3..0000000 --- a/Front-End/src/context/TuitionFeesContext.js +++ /dev/null @@ -1,24 +0,0 @@ -import React, { createContext, useState, useEffect, useContext } from 'react'; -import { fetchTuitionFees, fetchFees, fetchDiscounts } from '@/app/lib/schoolAction'; - -const TuitionFeesContext = createContext(); - -export const useTuitionFees = () => useContext(TuitionFeesContext); - -export const TuitionFeesProvider = ({ children }) => { - const [tuitionFees, setTuitionFees] = useState([]); - const [fees, setFees] = useState([]); - const [discounts, setDiscounts] = useState([]); - - useEffect(() => { - fetchTuitionFees().then(data => setTuitionFees(data)); - fetchFees().then(data => setFees(data)); - fetchDiscounts().then(data => setDiscounts(data)); - }, []); - - return ( - - {children} - - ); -}; \ No newline at end of file From 5462306a6020493cf747ea3bb8edb3240c36286f Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Tue, 21 Jan 2025 20:39:36 +0100 Subject: [PATCH 008/249] =?UTF-8?q?feat:=20Harmonisation=20des=20fees=20/?= =?UTF-8?q?=20ajout=20de=20type=20de=20r=C3=A9duction=20/=20mise=20=C3=A0?= =?UTF-8?q?=20jour=20du=20calcul=20[#18]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/School/models.py | 35 ++++----- Back-End/School/serializers.py | 39 +--------- Back-End/School/urls.py | 10 +-- Back-End/School/views.py | 69 ++++-------------- .../src/app/[locale]/admin/structure/page.js | 33 +++++---- Front-End/src/app/lib/schoolAction.js | 11 ++- .../Configuration/DiscountsSection.js | 71 +++++++++++++++---- .../Structure/Configuration/FeesManagement.js | 31 ++++---- ...sSection.js => RegistrationFeesSection.js} | 37 ++++++---- .../Configuration/TuitionFeesSection.js | 25 +++++-- Front-End/src/utils/Url.js | 2 - 11 files changed, 169 insertions(+), 194 deletions(-) rename Front-End/src/components/Structure/Configuration/{FeesSection.js => RegistrationFeesSection.js} (88%) diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 25967b2..937b1e5 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -73,40 +73,33 @@ class PaymentOptions(models.IntegerChoices): FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois') TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois') +class DiscountType(models.IntegerChoices): + CURRENCY = 0, 'Currency' + PERCENT = 1, 'Percent' + +class FeeType(models.IntegerChoices): + REGISTRATION_FEE = 0, 'Registration Fee' + TUITION_FEE = 1, 'Tuition Fee' + class Discount(models.Model): name = models.CharField(max_length=255, unique=True) - amount = models.DecimalField(max_digits=10, decimal_places=2) + amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) description = models.TextField(blank=True) + discount_type = models.IntegerField(choices=DiscountType.choices, default=DiscountType.CURRENCY) def __str__(self): return self.name class Fee(models.Model): name = models.CharField(max_length=255, unique=True) - description = models.TextField(blank=True) base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) - currency = models.CharField(max_length=3, default='EUR') + description = models.TextField(blank=True) + payment_option = models.IntegerField(choices=PaymentOptions.choices, default=PaymentOptions.SINGLE_PAYMENT) discounts = models.ManyToManyField('Discount', blank=True) - payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT) is_active = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) + currency = models.CharField(max_length=3, default='EUR') + type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE) def __str__(self): return self.name - -class TuitionFee(models.Model): - name = models.CharField(max_length=255, unique=True) - description = models.TextField(blank=True) - base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) - currency = models.CharField(max_length=3, default='EUR') - discounts = models.ManyToManyField('Discount', blank=True) - payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT) - is_active = models.BooleanField(default=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return self.name - - def clean(self): - if self.validity_end_date <= self.validity_start_date: - raise ValidationError(_('La date de fin de validité doit être après la date de début de validité.')) diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index 08b5588..0aaccc1 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, TuitionFee, Fee +from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee from Subscriptions.models import RegistrationForm from Subscriptions.serializers import StudentSerializer from Auth.serializers import ProfileSerializer @@ -208,42 +208,7 @@ class FeeSerializer(serializers.ModelSerializer): instance.payment_option = validated_data.get('payment_option', instance.payment_option) instance.is_active = validated_data.get('is_active', instance.is_active) instance.updated_at = validated_data.get('updated_at', instance.updated_at) - instance.save() - - # Update discounts if provided - instance.discounts.set(discounts_data) - - return instance - -class TuitionFeeSerializer(serializers.ModelSerializer): - discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) - - class Meta: - model = TuitionFee - fields = '__all__' - - def create(self, validated_data): - discounts_data = validated_data.pop('discounts', []) - - # Create the TuitionFee instance - tuition_fee = TuitionFee.objects.create(**validated_data) - - # Add discounts if provided - tuition_fee.discounts.set(discounts_data) - - return tuition_fee - - def update(self, instance, validated_data): - discounts_data = validated_data.pop('discounts', []) - - # Update the TuitionFee instance - instance.name = validated_data.get('name', instance.name) - instance.description = validated_data.get('description', instance.description) - instance.base_amount = validated_data.get('base_amount', instance.base_amount) - instance.currency = validated_data.get('currency', instance.currency) - instance.payment_option = validated_data.get('payment_option', instance.payment_option) - instance.is_active = validated_data.get('is_active', instance.is_active) - instance.updated_at = validated_data.get('updated_at', instance.updated_at) + instance.type = validated_data.get('type', instance.type) instance.save() # Update discounts if provided diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index 4ccae46..cb04f28 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -11,8 +11,6 @@ from School.views import ( PlanningView, FeesView, FeeView, - TuitionFeesView, - TuitionFeeView, DiscountsView, DiscountView, ) @@ -33,15 +31,11 @@ urlpatterns = [ re_path(r'^plannings$', PlanningsView.as_view(), name="plannings"), re_path(r'^planning$', PlanningView.as_view(), name="planning"), re_path(r'^planning/([0-9]+)$', PlanningView.as_view(), name="planning"), - - re_path(r'^fees$', FeesView.as_view(), name="fees"), + + re_path(r'^fees/(?P<_filter>[a-zA-z]+)$', FeesView.as_view(), name="fees"), re_path(r'^fee$', FeeView.as_view(), name="fee"), re_path(r'^fee/([0-9]+)$', FeeView.as_view(), name="fee"), - re_path(r'^tuitionFees$', TuitionFeesView.as_view(), name="tuitionFees"), - re_path(r'^tuitionFee$', TuitionFeeView.as_view(), name="tuitionFee"), - re_path(r'^tuitionFee/([0-9]+)$', TuitionFeeView.as_view(), name="tuitionFee"), - re_path(r'^discounts$', DiscountsView.as_view(), name="discounts"), re_path(r'^discount$', DiscountView.as_view(), name="discount"), re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"), diff --git a/Back-End/School/views.py b/Back-End/School/views.py index e06e944..35b648a 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -5,8 +5,8 @@ from rest_framework.parsers import JSONParser from rest_framework.views import APIView from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist -from .models import Teacher, Speciality, SchoolClass, Planning, Discount, TuitionFee, Fee -from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, TuitionFeeSerializer, FeeSerializer +from .models import Teacher, Speciality, SchoolClass, Planning, Discount, Fee +from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, FeeSerializer from N3wtSchool import bdd from N3wtSchool.bdd import delete_object, getAllObjects, getObject @@ -267,18 +267,16 @@ class PlanningView(APIView): @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class FeesView(APIView): - def get(self, request): - feesList = Fee.objects.all() - fees_serializer = FeeSerializer(feesList, many=True) - return JsonResponse(fees_serializer.data, safe=False) + def get(self, request, _filter, *args, **kwargs): - def post(self, request): - fee_data = JSONParser().parse(request) - fee_serializer = FeeSerializer(data=fee_data) - if fee_serializer.is_valid(): - fee_serializer.save() - return JsonResponse(fee_serializer.data, safe=False, status=201) - return JsonResponse(fee_serializer.errors, safe=False, status=400) + if _filter not in ['registration', 'tuition']: + return JsonResponse({"error": "Invalid type parameter. Must be 'registration' or 'tuition'."}, safe=False, status=400) + + fee_type_value = 0 if _filter == 'registration' else 1 + fees = Fee.objects.filter(type=fee_type_value) + fee_serializer = FeeSerializer(fees, many=True) + + return JsonResponse(fee_serializer.data, safe=False, status=200) @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') @@ -312,47 +310,4 @@ class FeeView(APIView): return JsonResponse(fee_serializer.errors, safe=False, status=400) def delete(self, request, _id): - return delete_object(Fee, _id) - -# Vues pour les frais de scolarité (TuitionFee) -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class TuitionFeesView(APIView): - def get(self, request): - tuitionFeesList = TuitionFee.objects.all() - tuitionFees_serializer = TuitionFeeSerializer(tuitionFeesList, many=True) - return JsonResponse(tuitionFees_serializer.data, safe=False) - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class TuitionFeeView(APIView): - def get(self, request, _id): - try: - tuitionFee = TuitionFee.objects.get(id=_id) - tuitionFee_serializer = TuitionFeeSerializer(tuitionFee) - return JsonResponse(tuitionFee_serializer.data, safe=False) - except TuitionFee.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=404) - - def post(self, request): - tuitionFee_data = JSONParser().parse(request) - tuitionFee_serializer = TuitionFeeSerializer(data=tuitionFee_data) - if tuitionFee_serializer.is_valid(): - tuitionFee_serializer.save() - return JsonResponse(tuitionFee_serializer.data, safe=False, status=201) - return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400) - - def put(self, request, _id): - tuitionFee_data = JSONParser().parse(request) - try: - tuitionFee = TuitionFee.objects.get(id=_id) - except TuitionFee.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=404) - tuitionFee_serializer = TuitionFeeSerializer(tuitionFee, data=tuitionFee_data, partial=True) # Utilisation de partial=True - if tuitionFee_serializer.is_valid(): - tuitionFee_serializer.save() - return JsonResponse(tuitionFee_serializer.data, safe=False) - return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400) - - def delete(self, request, _id): - return delete_object(TuitionFee, _id) \ No newline at end of file + return delete_object(Fee, _id) \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 4300560..0d893e6 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -1,20 +1,19 @@ 'use client' import React, { useState, useEffect } from 'react'; -import { School, Calendar, DollarSign } from 'lucide-react'; // Import de l'icône DollarSign import StructureManagement from '@/components/Structure/Configuration/StructureManagement'; import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'; import FeesManagement from '@/components/Structure/Configuration/FeesManagement'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; -import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchDiscounts, fetchFees, fetchTuitionFees } from '@/app/lib/schoolAction'; +import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchDiscounts, fetchRegistrationFees, fetchTuitionFees } from '@/app/lib/schoolAction'; import SidebarTabs from '@/components/SidebarTabs'; export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); - const [fees, setFees] = useState([]); + const [registrationFees, setRegistrationFees] = useState([]); const [discounts, setDiscounts] = useState([]); const [tuitionFees, setTuitionFees] = useState([]); @@ -33,13 +32,13 @@ export default function Page() { // Fetch data for schedules handleSchedules(); - // Fetch data for fees - handleFees(); - // Fetch data for discounts handleDiscounts(); + + // Fetch data for registration fees + handleRegistrationFees(); - // Fetch data for TuitionFee + // Fetch data for tuition fees handleTuitionFees(); }, []); @@ -75,14 +74,6 @@ export default function Page() { .catch(error => console.error('Error fetching schedules:', error)); }; - const handleFees = () => { - fetchFees() - .then(data => { - setFees(data); - }) - .catch(error => console.error('Error fetching fees:', error)); - }; - const handleDiscounts = () => { fetchDiscounts() .then(data => { @@ -91,6 +82,14 @@ export default function Page() { .catch(error => console.error('Error fetching discounts:', error)); }; + const handleRegistrationFees = () => { + fetchRegistrationFees() + .then(data => { + setRegistrationFees(data); + }) + .catch(error => console.error('Error fetching registration fees:', error)); + }; + const handleTuitionFees = () => { fetchTuitionFees() .then(data => { @@ -237,10 +236,10 @@ export default function Page() { label: 'Tarifications', content: ( { @@ -46,12 +45,12 @@ export const fetchDiscounts = () => { .then(requestResponseHandler) }; -export const fetchFees = () => { - return fetch(`${BE_SCHOOL_FEES_URL}`) +export const fetchRegistrationFees = () => { + return fetch(`${BE_SCHOOL_FEES_URL}/registration`) .then(requestResponseHandler) }; export const fetchTuitionFees = () => { - return fetch(`${BE_SCHOOL_TUITION_FEES_URL}`) + return fetch(`${BE_SCHOOL_FEES_URL}/tuition`) .then(requestResponseHandler) -}; +}; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/DiscountsSection.js b/Front-End/src/components/Structure/Configuration/DiscountsSection.js index 36d2271..f6c48d6 100644 --- a/Front-End/src/components/Structure/Configuration/DiscountsSection.js +++ b/Front-End/src/components/Structure/Configuration/DiscountsSection.js @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X } from 'lucide-react'; +import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; -const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete }) => { +const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, errors }) => { const [editingDiscount, setEditingDiscount] = useState(null); const [newDiscount, setNewDiscount] = useState(null); const [formData, setFormData] = useState({}); @@ -13,17 +13,24 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete }) const [popupMessage, setPopupMessage] = useState(""); const handleAddDiscount = () => { - setNewDiscount({ id: Date.now(), name: '', amount: '', description: '' }); + setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discountType: 'amount' }); }; const handleRemoveDiscount = (id) => { handleDelete(id) + .then(() => { + setDiscounts(prevDiscounts => prevDiscounts.filter(discount => discount.id !== id)); + }) + .catch(error => { + console.error(error); + }); }; const handleSaveNewDiscount = () => { if (newDiscount.name && newDiscount.amount) { handleCreate(newDiscount) - .then(() => { + .then((createdDiscount) => { + setDiscounts([createdDiscount, ...discounts]); setNewDiscount(null); setLocalErrors({}); }) @@ -60,9 +67,29 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete }) } }; + const handleToggleDiscountType = (id, newType) => { + const discount = discounts.find(discount => discount.id === id); + if (!discount) return; + + const updatedData = { + ...discount, + discount_type: newType + }; + + handleEdit(id, updatedData) + .then(() => { + setDiscounts(prevDiscounts => prevDiscounts.map(discount => discount.id === id ? { ...discount, discount_type: updatedData.discount_type } : discount)); + }) + .catch(error => { + console.error(error); + }); + }; + const handleChange = (e) => { const { name, value } = e.target; - if (editingDiscount) { + if (name === 'discountType') { + setDiscountType(value); + } else if (editingDiscount) { setFormData((prevData) => ({ ...prevData, [name]: value, @@ -97,13 +124,13 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete }) switch (column) { case 'LIBELLE': return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction'); - case 'MONTANT': - return renderInputField('amount', currentData.amount, handleChange, 'Montant'); + case 'VALEUR': + return renderInputField('amount', currentData.amount, handleChange, discount.discount_type === 0 ? 'Montant' : 'Pourcentage'); case 'DESCRIPTION': return renderInputField('description', currentData.description, handleChange, 'Description'); case 'ACTIONS': return ( -
+
+ +
+ ); + case 'DESCRIPTION': return discount.description; case 'ACTIONS': return ( @@ -168,7 +214,8 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete }) data={newDiscount ? [newDiscount, ...discounts] : discounts} columns={[ { name: 'LIBELLE', label: 'Libellé' }, - { name: 'MONTANT', label: 'Montant' }, + { name: 'VALEUR', label: 'Valeur' }, + { name: 'TYPE DE REMISE', label: 'Type de remise' }, { name: 'DESCRIPTION', label: 'Description' }, { name: 'ACTIONS', label: 'Actions' } ]} diff --git a/Front-End/src/components/Structure/Configuration/FeesManagement.js b/Front-End/src/components/Structure/Configuration/FeesManagement.js index 7c733bc..0dbbd87 100644 --- a/Front-End/src/components/Structure/Configuration/FeesManagement.js +++ b/Front-End/src/components/Structure/Configuration/FeesManagement.js @@ -1,29 +1,30 @@ import React, { useState } from 'react'; -import FeesSection from './FeesSection'; -import DiscountsSection from './DiscountsSection'; -import TuitionFeesSection from './TuitionFeesSection'; -import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL, BE_SCHOOL_TUITION_FEE_URL } from '@/utils/Url'; +import RegistrationFeesSection from '@/components/Structure/Configuration/RegistrationFeesSection'; +import DiscountsSection from '@/components/Structure/Configuration/DiscountsSection'; +import TuitionFeesSection from '@/components/Structure/Configuration/TuitionFeesSection'; +import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL } from '@/utils/Url'; -const FeesManagement = ({ fees, setFees, discounts, setDiscounts, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { +const FeesManagement = ({ discounts, setDiscounts, registrationFees, setRegistrationFees, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { return (
handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts)} handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)} />
- handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)} + handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setRegistrationFees)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setRegistrationFees)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setRegistrationFees)} />
@@ -31,10 +32,10 @@ const FeesManagement = ({ fees, setFees, discounts, setDiscounts, tuitionFees, s tuitionFees={tuitionFees} setTuitionFees={setTuitionFees} discounts={discounts} - fees={fees} - handleCreate={(newData) => handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)} + registrationFees={registrationFees} + handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setTuitionFees)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setTuitionFees)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setTuitionFees)} />
diff --git a/Front-End/src/components/Structure/Configuration/FeesSection.js b/Front-End/src/components/Structure/Configuration/RegistrationFeesSection.js similarity index 88% rename from Front-End/src/components/Structure/Configuration/FeesSection.js rename to Front-End/src/components/Structure/Configuration/RegistrationFeesSection.js index 518c48e..f8a9886 100644 --- a/Front-End/src/components/Structure/Configuration/FeesSection.js +++ b/Front-End/src/components/Structure/Configuration/RegistrationFeesSection.js @@ -1,11 +1,11 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X } from 'lucide-react'; +import { Plus, Trash, Edit3, Check, X, EyeOff, Eye } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; import SelectChoice from '@/components/SelectChoice'; -const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete }) => { +const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discounts, handleCreate, handleEdit, handleDelete }) => { const [editingFee, setEditingFee] = useState(null); const [newFee, setNewFee] = useState(null); const [formData, setFormData] = useState({}); @@ -29,8 +29,14 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl const handleSaveNewFee = () => { if (newFee.name && newFee.base_amount) { - handleCreate(newFee) - .then(() => { + const feeData = { + ...newFee, + type: 0 + }; + + handleCreate(feeData) + .then((createdFee) => { + setRegistrationFees([createdFee, ...registrationFees]); setNewFee(null); setLocalErrors({}); }) @@ -68,7 +74,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl }; const handleToggleActive = (id, isActive) => { - const fee = fees.find(fee => fee.id === id); + const fee = registrationFees.find(fee => fee.id === id); if (!fee) return; const updatedData = { @@ -78,7 +84,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl handleEdit(id, updatedData) .then(() => { - setFees(prevFees => prevFees.map(fee => fee.id === id ? { ...fee, is_active: !isActive } : fee)); + setRegistrationFees(prevFees => prevFees.map(fee => fee.id === id ? { ...fee, is_active: !isActive } : fee)); }) .catch(error => { console.error(error); @@ -107,11 +113,18 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl const calculateFinalAmount = (baseAmount, discountIds) => { const totalDiscounts = discountIds.reduce((sum, discountId) => { const discount = discounts.find(d => d.id === discountId); - return discount ? sum + parseFloat(discount.amount) : sum; + if (discount) { + if (discount.discount_type === 0) { // Currency + return sum + parseFloat(discount.amount); + } else if (discount.discount_type === 1) { // Percent + return sum + (parseFloat(baseAmount) * parseFloat(discount.amount) / 100); + } + } + return sum; }, 0); - + const finalAmount = parseFloat(baseAmount) - totalDiscounts; - + return finalAmount.toFixed(2); }; @@ -220,7 +233,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl onClick={() => handleToggleActive(fee.id, fee.is_active)} className={`text-${fee.is_active ? 'gray' : 'green'}-500 hover:text-${fee.is_active ? 'gray' : 'green'}-700`} > - {fee.is_active ? : } + {fee.is_active ? : }
); @@ -240,7 +253,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
{ +const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, registrationFees, handleCreate, handleEdit, handleDelete }) => { const [editingTuitionFee, setEditingTuitionFee] = useState(null); const [newTuitionFee, setNewTuitionFee] = useState(null); const [formData, setFormData] = useState({}); @@ -39,7 +39,11 @@ const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, fees, hand newTuitionFee.base_amount && newTuitionFee.payment_option >= 0 ) { - handleCreate(newTuitionFee) + const tuitionFeeData = { + ...newTuitionFee, + type: 1 + }; + handleCreate(tuitionFeeData) .then((createdTuitionFee) => { setTuitionFees([createdTuitionFee, ...tuitionFees]); setNewTuitionFee(null); @@ -151,11 +155,18 @@ const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, fees, hand const calculateFinalAmount = (baseAmount, discountIds) => { const totalDiscounts = discountIds.reduce((sum, discountId) => { const discount = discounts.find(d => d.id === discountId); - return discount ? sum + parseFloat(discount.amount) : sum; + if (discount) { + if (discount.discount_type === 0) { // Currency + return sum + parseFloat(discount.amount); + } else if (discount.discount_type === 1) { // Percent + return sum + (parseFloat(baseAmount) * parseFloat(discount.amount) / 100); + } + } + return sum; }, 0); - + const finalAmount = parseFloat(baseAmount) - totalDiscounts; - + return finalAmount.toFixed(2); }; @@ -238,7 +249,7 @@ const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, fees, hand onClick={() => handleToggleActive(tuitionFee.id, tuitionFee.is_active)} className={`text-${tuitionFee.is_active ? 'gray' : 'green'}-500 hover:text-${tuitionFee.is_active ? 'gray' : 'green'}-700`} > - {tuitionFee.is_active ? : } + {tuitionFee.is_active ? : } ); diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index 82c3e20..4fbad08 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -39,8 +39,6 @@ export const BE_SCHOOL_FEE_URL = `${BASE_URL}/School/fee`; export const BE_SCHOOL_FEES_URL = `${BASE_URL}/School/fees`; export const BE_SCHOOL_DISCOUNT_URL = `${BASE_URL}/School/discount`; export const BE_SCHOOL_DISCOUNTS_URL = `${BASE_URL}/School/discounts`; -export const BE_SCHOOL_TUITION_FEE_URL = `${BASE_URL}/School/tuitionFee`; -export const BE_SCHOOL_TUITION_FEES_URL = `${BASE_URL}/School/tuitionFees`; // GESTION MESSAGERIE export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie` From 799e1c6717fceec4b29edfbdd0af52268b7e8fce Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Thu, 23 Jan 2025 20:00:17 +0100 Subject: [PATCH 009/249] feat: Sortie des calculs des montants totaux de la partie configuration + revue du rendu [#18] --- Back-End/School/models.py | 4 +- Back-End/School/serializers.py | 18 +- Back-End/School/urls.py | 2 +- Back-End/School/views.py | 101 +++--- .../src/app/[locale]/admin/structure/page.js | 34 +- Front-End/src/app/lib/schoolAction.js | 9 +- .../Configuration/DiscountsSection.js | 58 ++-- .../Structure/Configuration/FeesManagement.js | 99 ++++-- ...istrationFeesSection.js => FeesSection.js} | 175 +++++------ .../Configuration/TuitionFeesSection.js | 294 ------------------ Front-End/src/components/Table.js | 4 +- 11 files changed, 266 insertions(+), 532 deletions(-) rename Front-End/src/components/Structure/Configuration/{RegistrationFeesSection.js => FeesSection.js} (61%) delete mode 100644 Front-End/src/components/Structure/Configuration/TuitionFeesSection.js diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 937b1e5..7c7f609 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -86,6 +86,8 @@ class Discount(models.Model): amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) description = models.TextField(blank=True) discount_type = models.IntegerField(choices=DiscountType.choices, default=DiscountType.CURRENCY) + type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE) + updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name @@ -94,11 +96,9 @@ class Fee(models.Model): name = models.CharField(max_length=255, unique=True) base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) description = models.TextField(blank=True) - payment_option = models.IntegerField(choices=PaymentOptions.choices, default=PaymentOptions.SINGLE_PAYMENT) discounts = models.ManyToManyField('Discount', blank=True) is_active = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) - currency = models.CharField(max_length=3, default='EUR') type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE) def __str__(self): diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index 0aaccc1..a4e2a90 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -175,12 +175,20 @@ class SchoolClassSerializer(serializers.ModelSerializer): return local_time.strftime("%d-%m-%Y %H:%M") class DiscountSerializer(serializers.ModelSerializer): + updated_at_formatted = serializers.SerializerMethodField() class Meta: model = Discount fields = '__all__' + def get_updated_at_formatted(self, obj): + utc_time = timezone.localtime(obj.updated_at) + local_tz = pytz.timezone(settings.TZ_APPLI) + local_time = utc_time.astimezone(local_tz) + return local_time.strftime("%d-%m-%Y %H:%M") + class FeeSerializer(serializers.ModelSerializer): discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) + updated_at_formatted = serializers.SerializerMethodField() class Meta: model = Fee @@ -204,8 +212,6 @@ class FeeSerializer(serializers.ModelSerializer): instance.name = validated_data.get('name', instance.name) instance.description = validated_data.get('description', instance.description) instance.base_amount = validated_data.get('base_amount', instance.base_amount) - instance.currency = validated_data.get('currency', instance.currency) - instance.payment_option = validated_data.get('payment_option', instance.payment_option) instance.is_active = validated_data.get('is_active', instance.is_active) instance.updated_at = validated_data.get('updated_at', instance.updated_at) instance.type = validated_data.get('type', instance.type) @@ -214,4 +220,10 @@ class FeeSerializer(serializers.ModelSerializer): # Update discounts if provided instance.discounts.set(discounts_data) - return instance \ No newline at end of file + return instance + + def get_updated_at_formatted(self, obj): + utc_time = timezone.localtime(obj.updated_at) + local_tz = pytz.timezone(settings.TZ_APPLI) + local_time = utc_time.astimezone(local_tz) + return local_time.strftime("%d-%m-%Y %H:%M") \ No newline at end of file diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index cb04f28..6cd2009 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -36,7 +36,7 @@ urlpatterns = [ re_path(r'^fee$', FeeView.as_view(), name="fee"), re_path(r'^fee/([0-9]+)$', FeeView.as_view(), name="fee"), - re_path(r'^discounts$', DiscountsView.as_view(), name="discounts"), + re_path(r'^discounts/(?P<_filter>[a-zA-z]+)$$', DiscountsView.as_view(), name="discounts"), re_path(r'^discount$', DiscountView.as_view(), name="discount"), re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"), ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index 35b648a..86bbc89 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -65,57 +65,6 @@ class SpecialityView(APIView): def delete(self, request, _id): return delete_object(Speciality, _id) -# Vues pour les réductions (Discount) -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class DiscountsView(APIView): - def get(self, request): - discountsList = Discount.objects.all() - discounts_serializer = DiscountSerializer(discountsList, many=True) - return JsonResponse(discounts_serializer.data, safe=False) - - def post(self, request): - discount_data = JSONParser().parse(request) - discount_serializer = DiscountSerializer(data=discount_data) - if discount_serializer.is_valid(): - discount_serializer.save() - return JsonResponse(discount_serializer.data, safe=False, status=201) - return JsonResponse(discount_serializer.errors, safe=False, status=400) - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class DiscountView(APIView): - def get(self, request, _id): - try: - discount = Discount.objects.get(id=_id) - discount_serializer = DiscountSerializer(discount) - return JsonResponse(discount_serializer.data, safe=False) - except Discount.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=404) - - def post(self, request): - discount_data = JSONParser().parse(request) - discount_serializer = DiscountSerializer(data=discount_data) - if discount_serializer.is_valid(): - discount_serializer.save() - return JsonResponse(discount_serializer.data, safe=False, status=201) - return JsonResponse(discount_serializer.errors, safe=False, status=400) - - def put(self, request, _id): - discount_data = JSONParser().parse(request) - try: - discount = Discount.objects.get(id=_id) - except Discount.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=404) - discount_serializer = DiscountSerializer(discount, data=discount_data, partial=True) # Utilisation de partial=True - if discount_serializer.is_valid(): - discount_serializer.save() - return JsonResponse(discount_serializer.data, safe=False) - return JsonResponse(discount_serializer.errors, safe=False, status=400) - - def delete(self, request, _id): - return delete_object(Discount, _id) - class TeachersView(APIView): def get(self, request): teachersList=getAllObjects(Teacher) @@ -263,7 +212,55 @@ class PlanningView(APIView): return JsonResponse(planning_serializer.errors, safe=False) -# Vues pour les frais (Fee) +# Vues pour les réductions (Discount) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountsView(APIView): + def get(self, request, _filter, *args, **kwargs): + + if _filter not in ['registration', 'tuition']: + return JsonResponse({"error": "Invalid type parameter. Must be 'registration' or 'tuition'."}, safe=False, status=400) + + discount_type_value = 0 if _filter == 'registration' else 1 + discounts = Discount.objects.filter(type=discount_type_value) + discounts_serializer = DiscountSerializer(discounts, many=True) + + return JsonResponse(discounts_serializer.data, safe=False, status=200) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountView(APIView): + def get(self, request, _id): + try: + discount = Discount.objects.get(id=_id) + discount_serializer = DiscountSerializer(discount) + return JsonResponse(discount_serializer.data, safe=False) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + discount_data = JSONParser().parse(request) + discount_serializer = DiscountSerializer(data=discount_data) + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False, status=201) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + discount_data = JSONParser().parse(request) + try: + discount = Discount.objects.get(id=_id) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + discount_serializer = DiscountSerializer(discount, data=discount_data, partial=True) # Utilisation de partial=True + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(Discount, _id) + @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class FeesView(APIView): diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 0d893e6..545a112 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -6,15 +6,16 @@ import FeesManagement from '@/components/Structure/Configuration/FeesManagement' import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; -import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchDiscounts, fetchRegistrationFees, fetchTuitionFees } from '@/app/lib/schoolAction'; +import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchRegistrationDiscounts, fetchTuitionDiscounts, fetchRegistrationFees, fetchTuitionFees } from '@/app/lib/schoolAction'; import SidebarTabs from '@/components/SidebarTabs'; export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); + const [registrationDiscounts, setRegistrationDiscounts] = useState([]); + const [tuitionDiscounts, setTuitionDiscounts] = useState([]); const [registrationFees, setRegistrationFees] = useState([]); - const [discounts, setDiscounts] = useState([]); const [tuitionFees, setTuitionFees] = useState([]); const csrfToken = useCsrfToken(); @@ -32,8 +33,11 @@ export default function Page() { // Fetch data for schedules handleSchedules(); - // Fetch data for discounts - handleDiscounts(); + // Fetch data for registration discounts + handleRegistrationDiscounts(); + + // Fetch data for tuition discounts + handleTuitionDiscounts(); // Fetch data for registration fees handleRegistrationFees(); @@ -74,12 +78,20 @@ export default function Page() { .catch(error => console.error('Error fetching schedules:', error)); }; - const handleDiscounts = () => { - fetchDiscounts() + const handleRegistrationDiscounts = () => { + fetchRegistrationDiscounts() .then(data => { - setDiscounts(data); + setRegistrationDiscounts(data); }) - .catch(error => console.error('Error fetching discounts:', error)); + .catch(error => console.error('Error fetching registration discounts:', error)); + }; + + const handleTuitionDiscounts = () => { + fetchTuitionDiscounts() + .then(data => { + setTuitionDiscounts(data); + }) + .catch(error => console.error('Error fetching tuition discounts:', error)); }; const handleRegistrationFees = () => { @@ -236,8 +248,10 @@ export default function Page() { label: 'Tarifications', content: ( { .then(requestResponseHandler) }; -export const fetchDiscounts = () => { - return fetch(`${BE_SCHOOL_DISCOUNTS_URL}`) +export const fetchRegistrationDiscounts = () => { + return fetch(`${BE_SCHOOL_DISCOUNTS_URL}/registration`) + .then(requestResponseHandler) +}; + +export const fetchTuitionDiscounts = () => { + return fetch(`${BE_SCHOOL_DISCOUNTS_URL}/tuition`) .then(requestResponseHandler) }; diff --git a/Front-End/src/components/Structure/Configuration/DiscountsSection.js b/Front-End/src/components/Structure/Configuration/DiscountsSection.js index f6c48d6..3c19301 100644 --- a/Front-End/src/components/Structure/Configuration/DiscountsSection.js +++ b/Front-End/src/components/Structure/Configuration/DiscountsSection.js @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon } from 'lucide-react'; +import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; -const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, errors }) => { +const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type }) => { const [editingDiscount, setEditingDiscount] = useState(null); const [newDiscount, setNewDiscount] = useState(null); const [formData, setFormData] = useState({}); @@ -13,7 +13,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h const [popupMessage, setPopupMessage] = useState(""); const handleAddDiscount = () => { - setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discountType: 'amount' }); + setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discount_type: 0, type: type }); }; const handleRemoveDiscount = (id) => { @@ -67,13 +67,13 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h } }; - const handleToggleDiscountType = (id, newType) => { + const handleToggleDiscountType = (id) => { const discount = discounts.find(discount => discount.id === id); if (!discount) return; const updatedData = { ...discount, - discount_type: newType + discount_type: discount.discount_type === 0 ? 1 : 0 }; handleEdit(id, updatedData) @@ -87,9 +87,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h const handleChange = (e) => { const { name, value } = e.target; - if (name === 'discountType') { - setDiscountType(value); - } else if (editingDiscount) { + if (editingDiscount) { setFormData((prevData) => ({ ...prevData, [name]: value, @@ -124,8 +122,8 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h switch (column) { case 'LIBELLE': return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction'); - case 'VALEUR': - return renderInputField('amount', currentData.amount, handleChange, discount.discount_type === 0 ? 'Montant' : 'Pourcentage'); + case 'REMISE': + return renderInputField('amount', currentData.amount, handleChange,'Montant'); case 'DESCRIPTION': return renderInputField('description', currentData.description, handleChange, 'Description'); case 'ACTIONS': @@ -154,32 +152,22 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h switch (column) { case 'LIBELLE': return discount.name; - case 'VALEUR': + case 'REMISE': return discount.discount_type === 0 ? `${discount.amount} €` : `${discount.amount} %`; - case 'TYPE DE REMISE': - return ( -
- - -
- ); case 'DESCRIPTION': return discount.description; + case 'MISE A JOUR': + return discount.updated_at_formatted; case 'ACTIONS': return (
+ @@ -214,12 +205,13 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h data={newDiscount ? [newDiscount, ...discounts] : discounts} columns={[ { name: 'LIBELLE', label: 'Libellé' }, - { name: 'VALEUR', label: 'Valeur' }, - { name: 'TYPE DE REMISE', label: 'Type de remise' }, + { name: 'REMISE', label: 'Valeur' }, { name: 'DESCRIPTION', label: 'Description' }, + { name: 'MISE A JOUR', label: 'date mise à jour' }, { name: 'ACTIONS', label: 'Actions' } ]} renderCell={renderDiscountCell} + defaultTheme='bg-yellow-100' /> { +const FeesManagement = ({ registrationDiscounts, setRegistrationDiscounts, tuitionDiscounts, setTuitionDiscounts, registrationFees, setRegistrationFees, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { + + const handleDiscountDelete = (id, type) => { + if (type === 0) { + setRegistrationFees(prevFees => + prevFees.map(fee => ({ + ...fee, + discounts: fee.discounts.filter(discountId => discountId !== id) + })) + ); + } else { + setTuitionFees(prevFees => + prevFees.map(fee => ({ + ...fee, + discounts: fee.discounts.filter(discountId => discountId !== id) + })) + ); + } + }; return (
-
- handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)} - /> +
+
+ handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setRegistrationFees)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setRegistrationFees)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setRegistrationFees)} + type={0} + /> +
+
+ handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setRegistrationDiscounts)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setRegistrationDiscounts)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setRegistrationDiscounts)} + onDiscountDelete={(id) => handleDiscountDelete(id, 0)} + type={0} + /> +
-
- handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setRegistrationFees)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setRegistrationFees)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setRegistrationFees)} - /> -
-
- handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setTuitionFees)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setTuitionFees)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setTuitionFees)} - /> +
+
+ handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setTuitionFees)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setTuitionFees)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setTuitionFees)} + type={1} + /> +
+
+ handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setTuitionDiscounts)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setTuitionDiscounts)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setTuitionDiscounts)} + onDiscountDelete={(id) => handleDiscountDelete(id, 1)} + type={1} + /> +
); diff --git a/Front-End/src/components/Structure/Configuration/RegistrationFeesSection.js b/Front-End/src/components/Structure/Configuration/FeesSection.js similarity index 61% rename from Front-End/src/components/Structure/Configuration/RegistrationFeesSection.js rename to Front-End/src/components/Structure/Configuration/FeesSection.js index f8a9886..cbfe960 100644 --- a/Front-End/src/components/Structure/Configuration/RegistrationFeesSection.js +++ b/Front-End/src/components/Structure/Configuration/FeesSection.js @@ -1,11 +1,10 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, EyeOff, Eye } from 'lucide-react'; +import { Plus, Trash, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; -import SelectChoice from '@/components/SelectChoice'; -const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discounts, handleCreate, handleEdit, handleDelete }) => { +const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type }) => { const [editingFee, setEditingFee] = useState(null); const [newFee, setNewFee] = useState(null); const [formData, setFormData] = useState({}); @@ -13,30 +12,27 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(""); - const paymentOptions = [ - { value: 0, label: '1 fois' }, - { value: 1, label: '4 fois' }, - { value: 2, label: '10 fois' } - ]; - const handleAddFee = () => { - setNewFee({ id: Date.now(), name: '', base_amount: '', description: '' }); + setNewFee({ id: Date.now(), name: '', base_amount: '', description: '', validity_start_date: '', validity_end_date: '', discounts: [], type: type }); }; const handleRemoveFee = (id) => { - handleDelete(id); + handleDelete(id) + .then(() => { + setFees(prevFees => prevFees.filter(fee => fee.id !== id)); + }) + .catch(error => { + console.error(error); + }); }; const handleSaveNewFee = () => { - if (newFee.name && newFee.base_amount) { - const feeData = { - ...newFee, - type: 0 - }; - - handleCreate(feeData) + if ( + newFee.name && + newFee.base_amount) { + handleCreate(newFee) .then((createdFee) => { - setRegistrationFees([createdFee, ...registrationFees]); + setFees([createdFee, ...fees]); setNewFee(null); setLocalErrors({}); }) @@ -48,15 +44,18 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou } }); } else { - setPopupMessage("Tous les champs doivent être remplis"); + setPopupMessage("Tous les champs doivent être remplis et valides"); setPopupVisible(true); } }; const handleUpdateFee = (id, updatedFee) => { - if (updatedFee.name && updatedFee.base_amount) { + if ( + updatedFee.name && + updatedFee.base_amount) { handleEdit(id, updatedFee) - .then(() => { + .then((updatedFee) => { + setFees(fees.map(fee => fee.id === id ? updatedFee : fee)); setEditingFee(null); setLocalErrors({}); }) @@ -68,13 +67,13 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou } }); } else { - setPopupMessage("Tous les champs doivent être remplis"); + setPopupMessage("Tous les champs doivent être remplis et valides"); setPopupVisible(true); } }; const handleToggleActive = (id, isActive) => { - const fee = registrationFees.find(fee => fee.id === id); + const fee = fees.find(fee => fee.id === id); if (!fee) return; const updatedData = { @@ -84,7 +83,7 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou handleEdit(id, updatedData) .then(() => { - setRegistrationFees(prevFees => prevFees.map(fee => fee.id === id ? { ...fee, is_active: !isActive } : fee)); + setFees(prevFees => prevFees.map(fee => fee.id === id ? { ...fee, is_active: !isActive } : fee)); }) .catch(error => { console.error(error); @@ -110,6 +109,19 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou } }; + const renderInputField = (field, value, onChange, placeholder) => ( +
+ +
+ ); + const calculateFinalAmount = (baseAmount, discountIds) => { const totalDiscounts = discountIds.reduce((sum, discountId) => { const discount = discounts.find(d => d.id === discountId); @@ -128,32 +140,6 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou return finalAmount.toFixed(2); }; - const renderInputField = (field, value, onChange, placeholder) => ( -
- -
- ); - - const renderSelectField = (field, value, options, callback, label) => ( -
- -
- ); - const renderFeeCell = (fee, column) => { const isEditing = editingFee === fee.id; const isCreating = newFee && newFee.id === fee.id; @@ -161,19 +147,15 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou if (isEditing || isCreating) { switch (column) { - case 'LIBELLE': - return renderInputField('name', currentData.name, handleChange, 'Libellé du frais'); - case 'MONTANT DE BASE': - return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant'); + case 'NOM': + return renderInputField('name', currentData.name, handleChange, 'Nom des frais'); + case 'MONTANT': + return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base'); case 'DESCRIPTION': return renderInputField('description', currentData.description, handleChange, 'Description'); - case 'OPTIONS DE PAIEMENT': - return renderSelectField('payment_option', currentData.payment_option, paymentOptions, handleChange, 'Options de paiement'); - case 'REMISES': - return renderSelectField('discounts', currentData.discounts, discounts.map(discount => ({ value: discount.id, label: discount.name })), handleChange, 'Remises'); case 'ACTIONS': return ( -
+
-
); default: @@ -244,28 +218,27 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou }; return ( - <> -
-
-

Frais d'inscription

- +
+
+
+ +

{type === 0 ? 'Frais d\'inscription' : 'Frais de scolarité'}

-
+ +
setPopupVisible(false)} uniqueConfirmButton={true} /> - + ); }; -export default RegistrationFeesSection; \ No newline at end of file +export default FeesSection; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js b/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js deleted file mode 100644 index 34dc882..0000000 --- a/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js +++ /dev/null @@ -1,294 +0,0 @@ -import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, EyeOff, Eye } from 'lucide-react'; -import Table from '@/components/Table'; -import InputTextIcon from '@/components/InputTextIcon'; -import Popup from '@/components/Popup'; -import SelectChoice from '@/components/SelectChoice'; - -const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, registrationFees, handleCreate, handleEdit, handleDelete }) => { - const [editingTuitionFee, setEditingTuitionFee] = useState(null); - const [newTuitionFee, setNewTuitionFee] = useState(null); - const [formData, setFormData] = useState({}); - const [localErrors, setLocalErrors] = useState({}); - const [popupVisible, setPopupVisible] = useState(false); - const [popupMessage, setPopupMessage] = useState(""); - - const paymentOptions = [ - { value: 0, label: '1 fois' }, - { value: 1, label: '4 fois' }, - { value: 2, label: '10 fois' } - ]; - - const handleAddTuitionFee = () => { - setNewTuitionFee({ id: Date.now(), name: '', base_amount: '', description: '', validity_start_date: '', validity_end_date: '', payment_option: '', discounts: [] }); - }; - - const handleRemoveTuitionFee = (id) => { - handleDelete(id) - .then(() => { - setTuitionFees(prevTuitionFees => prevTuitionFees.filter(fee => fee.id !== id)); - }) - .catch(error => { - console.error(error); - }); - }; - - const handleSaveNewTuitionFee = () => { - if ( - newTuitionFee.name && - newTuitionFee.base_amount && - newTuitionFee.payment_option >= 0 - ) { - const tuitionFeeData = { - ...newTuitionFee, - type: 1 - }; - handleCreate(tuitionFeeData) - .then((createdTuitionFee) => { - setTuitionFees([createdTuitionFee, ...tuitionFees]); - setNewTuitionFee(null); - setLocalErrors({}); - }) - .catch(error => { - if (error && typeof error === 'object') { - setLocalErrors(error); - } else { - console.error(error); - } - }); - } else { - setPopupMessage("Tous les champs doivent être remplis et valides"); - setPopupVisible(true); - } - }; - - const handleUpdateTuitionFee = (id, updatedTuitionFee) => { - if ( - updatedTuitionFee.name && - updatedTuitionFee.base_amount && - updatedTuitionFee.payment_option >= 0 - ) { - handleEdit(id, updatedTuitionFee) - .then((updatedFee) => { - setTuitionFees(tuitionFees.map(fee => fee.id === id ? updatedFee : fee)); - setEditingTuitionFee(null); - setLocalErrors({}); - }) - .catch(error => { - if (error && typeof error === 'object') { - setLocalErrors(error); - } else { - console.error(error); - } - }); - } else { - setPopupMessage("Tous les champs doivent être remplis et valides"); - setPopupVisible(true); - } - }; - - const handleToggleActive = (id, isActive) => { - const tuitionFee = tuitionFees.find(tuitionFee => tuitionFee.id === id); - if (!tuitionFee) return; - - const updatedData = { - is_active: !isActive, - discounts: tuitionFee.discounts - }; - - handleEdit(id, updatedData) - .then(() => { - setFees(prevTuitionFees => prevTuitionFees.map(tuitionFee => tuitionFee.id === id ? { ...tuitionFee, is_active: !isActive } : tuitionFee)); - }) - .catch(error => { - console.error(error); - }); - }; - - const handleChange = (e) => { - const { name, value } = e.target; - let parsedValue = value; - if (name === 'payment_option') { - parsedValue = parseInt(value, 10); - } else if (name === 'discounts') { - parsedValue = value.split(',').map(v => parseInt(v, 10)); - } - if (editingTuitionFee) { - setFormData((prevData) => ({ - ...prevData, - [name]: parsedValue, - })); - } else if (newTuitionFee) { - setNewTuitionFee((prevData) => ({ - ...prevData, - [name]: parsedValue, - })); - } - }; - - const renderInputField = (field, value, onChange, placeholder) => ( -
- -
- ); - - const renderSelectField = (field, value, options, callback, label) => ( -
- -
- ); - - const calculateFinalAmount = (baseAmount, discountIds) => { - const totalDiscounts = discountIds.reduce((sum, discountId) => { - const discount = discounts.find(d => d.id === discountId); - if (discount) { - if (discount.discount_type === 0) { // Currency - return sum + parseFloat(discount.amount); - } else if (discount.discount_type === 1) { // Percent - return sum + (parseFloat(baseAmount) * parseFloat(discount.amount) / 100); - } - } - return sum; - }, 0); - - const finalAmount = parseFloat(baseAmount) - totalDiscounts; - - return finalAmount.toFixed(2); - }; - - const renderTuitionFeeCell = (tuitionFee, column) => { - const isEditing = editingTuitionFee === tuitionFee.id; - const isCreating = newTuitionFee && newTuitionFee.id === tuitionFee.id; - const currentData = isEditing ? formData : newTuitionFee; - - if (isEditing || isCreating) { - switch (column) { - case 'NOM': - return renderInputField('name', currentData.name, handleChange, 'Nom des frais de scolarité'); - case 'MONTANT DE BASE': - return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base'); - case 'DESCRIPTION': - return renderInputField('description', currentData.description, handleChange, 'Description'); - case 'OPTIONS DE PAIEMENT': - return renderSelectField('payment_option', currentData.payment_option, paymentOptions, handleChange, 'Options de paiement'); - case 'REMISES': - return renderSelectField('discounts', currentData.discounts, discounts.map(discount => ({ value: discount.id, label: discount.name })), handleChange, 'Remises'); - case 'ACTIONS': - return ( -
- - -
- ); - default: - return null; - } - } else { - switch (column) { - case 'NOM': - return tuitionFee.name; - case 'MONTANT DE BASE': - return tuitionFee.base_amount + ' €'; - case 'DESCRIPTION': - return tuitionFee.description; - case 'OPTIONS DE PAIEMENT': - return paymentOptions.find(option => option.value === tuitionFee.payment_option)?.label || ''; - case 'REMISES': - const discountNames = tuitionFee.discounts - .map(discountId => discounts.find(discount => discount.id === discountId)?.name) - .filter(name => name) - .join(', '); - return discountNames; - case 'MONTANT FINAL': - return calculateFinalAmount(tuitionFee.base_amount, tuitionFee.discounts) + ' €'; - case 'ACTIONS': - return ( -
- - - -
- ); - default: - return null; - } - } - }; - - return ( -
-
-

Frais de scolarité

- -
-
- setPopupVisible(false)} - onCancel={() => setPopupVisible(false)} - uniqueConfirmButton={true} - /> - - ); -}; - -export default TuitionFeesSection; \ No newline at end of file diff --git a/Front-End/src/components/Table.js b/Front-End/src/components/Table.js index d22d378..116c98b 100644 --- a/Front-End/src/components/Table.js +++ b/Front-End/src/components/Table.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Pagination from '@/components/Pagination'; // Correction du chemin d'importatio, -const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange, onRowClick, selectedRows, isSelectable = false }) => { +const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange, onRowClick, selectedRows, isSelectable = false, defaultTheme='bg-emerald-50' }) => { const handlePageChange = (newPage) => { onPageChange(newPage); }; @@ -25,7 +25,7 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total key={rowIndex} className={` ${isSelectable ? 'cursor-pointer' : ''} - ${selectedRows?.includes(row.id) ? 'bg-emerald-500 text-white' : rowIndex % 2 === 0 ? 'bg-emerald-50' : ''} + ${selectedRows?.includes(row.id) ? 'bg-emerald-500 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''} ${isSelectable ? 'hover:bg-emerald-600' : ''} `} onClick={() => isSelectable && onRowClick && onRowClick(row)} From b8ef34a04b14c8a8fb980fcd9255296ceb699ec6 Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Sat, 25 Jan 2025 12:19:30 +0100 Subject: [PATCH 010/249] feat: ajout des documents d'inscription [#20] --- Back-End/Subscriptions/models.py | 10 +- Back-End/Subscriptions/util.py | 54 +++- Back-End/Subscriptions/views.py | 45 ++- .../subscriptions/editInscription/page.js | 24 +- .../app/[locale]/admin/subscriptions/page.js | 160 +++++----- .../[locale]/parents/editInscription/page.js | 45 +-- Front-End/src/app/lib/subscriptionAction.js | 80 +++-- .../src/components/AffectationClasseForm.js | 4 +- Front-End/src/components/FileStatusLabel.js | 33 ++ .../Inscription/InscriptionFormShared.js | 299 ++++++++++++------ .../Inscription/ResponsableInputFields.js | 14 +- Front-End/src/hooks/useCsrfToken.js | 2 +- 12 files changed, 449 insertions(+), 321 deletions(-) create mode 100644 Front-End/src/components/FileStatusLabel.js diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index b183107..582cc67 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -182,6 +182,10 @@ class Student(models.Model): return self.birth_date.strftime('%d-%m-%Y') return None +def registration_file_path(instance, filename): + # Génère le chemin : registration_files/dossier_rf_{student_id}/filename + return f'registration_files/dossier_rf_{instance.student_id}/{filename}' + class RegistrationForm(models.Model): """ Gère le dossier d’inscription lié à un élève donné. @@ -201,7 +205,11 @@ class RegistrationForm(models.Model): last_update = models.DateTimeField(auto_now=True) notes = models.CharField(max_length=200, blank=True) registration_link_code = models.CharField(max_length=200, default="", blank=True) - registration_file = models.FileField(upload_to=settings.DOCUMENT_DIR, default="", blank=True) + registration_file = models.FileField( + upload_to=registration_file_path, + null=True, + blank=True + ) associated_rf = models.CharField(max_length=200, default="", blank=True) def __str__(self): diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index e76d06b..622bac5 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -93,31 +93,57 @@ def getArgFromRequest(_argument, _request): def merge_files_pdf(filenames, output_filename): """ - Insère plusieurs fichiers PDF dans un seul document de sortie. + Fusionne plusieurs fichiers PDF en un seul document. + Vérifie l'existence des fichiers sources avant la fusion. """ merger = pymupdf.open() + valid_files = [] + + # Vérifier l'existence des fichiers et ne garder que ceux qui existent for filename in filenames: + if os.path.exists(filename): + valid_files.append(filename) + + # Fusionner les fichiers valides + for filename in valid_files: merger.insert_file(filename) + + # S'assurer que le dossier de destination existe + os.makedirs(os.path.dirname(output_filename), exist_ok=True) + + # Sauvegarder le fichier fusionné merger.save(output_filename) merger.close() -def rfToPDF(registerForm,filename): + return output_filename + +def rfToPDF(registerForm, filename): """ - Génère le PDF d’un dossier d’inscription et l’associe au RegistrationForm. + Génère le PDF d'un dossier d'inscription et l'associe au RegistrationForm. """ - # Ajout du fichier d'inscriptions data = { - 'pdf_title': "Dossier d'inscription de %s"%registerForm.student.first_name, + 'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}", 'signatureDate': convertToStr(_now(), '%d-%m-%Y'), 'signatureTime': convertToStr(_now(), '%H:%M'), - 'student':registerForm.student, + 'student': registerForm.student, } - PDFFileName = filename + + # S'assurer que le dossier parent existe + os.makedirs(os.path.dirname(filename), exist_ok=True) + + # Générer le PDF pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) - pathFichier = Path(filename) - if os.path.exists(str(pathFichier)): - print(f'File exists : {str(pathFichier)}') - os.remove(str(pathFichier)) - receipt_file = BytesIO(pdf.content) - registerForm.fichierInscription = File(receipt_file, PDFFileName) - registerForm.fichierInscription.save() \ No newline at end of file + + # Écrire le fichier directement + with open(filename, 'wb') as f: + f.write(pdf.content) + + # Mettre à jour le champ registration_file du registerForm + with open(filename, 'rb') as f: + registerForm.registration_file.save( + os.path.basename(filename), + File(f), + save=True + ) + + return registerForm.registration_file \ No newline at end of file diff --git a/Back-End/Subscriptions/views.py b/Back-End/Subscriptions/views.py index 5a29bd8..d635bf7 100644 --- a/Back-End/Subscriptions/views.py +++ b/Back-End/Subscriptions/views.py @@ -179,20 +179,37 @@ class RegisterFormView(APIView): registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: - # Le parent a complété le dossier d'inscription, il est soumis à validation par l'école - json.dumps(studentForm_data) - #Génération de la fiche d'inscription au format PDF - PDFFileName = "rf_%s_%s.pdf"%(registerForm.student.last_name, registerForm.student.first_name) - path = Path(f"registration_files/dossier_rf_{registerForm.pk}/{PDFFileName}") - registerForm.fichierInscription = util.rfToPDF(registerForm, path) - # Récupération des fichiers d'inscription - fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) - fileNames.insert(0,path) - # Création du fichier PDF Fusionné avec le dossier complet - output_path = f"registration_files/dossier_rf_{registerForm.pk}/dossier_{registerForm.pk}.pdf" - util.merge_files_pdf(fileNames, output_path) - # Mise à jour de l'automate - updateStateMachine(registerForm, 'saisiDI') + try: + # Génération de la fiche d'inscription au format PDF + base_dir = f"registration_files/dossier_rf_{registerForm.pk}" + os.makedirs(base_dir, exist_ok=True) + + # Fichier PDF initial + initial_pdf = f"{base_dir}/rf_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" + registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf) + registerForm.save() + + # Récupération des fichiers d'inscription + fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) + if registerForm.registration_file: + fileNames.insert(0, registerForm.registration_file.path) + + # Création du fichier PDF Fusionné + merged_pdf = f"{base_dir}/dossier_complet_{registerForm.pk}.pdf" + util.merge_files_pdf(fileNames, merged_pdf) + + # Mise à jour du champ registration_file avec le fichier fusionné + with open(merged_pdf, 'rb') as f: + registerForm.registration_file.save( + os.path.basename(merged_pdf), + File(f), + save=True + ) + + # Mise à jour de l'automate + updateStateMachine(registerForm, 'saisiDI') + except Exception as e: + return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: # L'école a validé le dossier d'inscription # Mise à jour de l'automate diff --git a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js index 35d0520..510e121 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js @@ -16,29 +16,10 @@ export default function Page() { const studentId = searchParams.get('studentId'); // Changé de codeDI à studentId const [initialData, setInitialData] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const [formErrors, setFormErrors] = useState({}); const csrfToken = useCsrfToken(); - useEffect(() => { - if (useFakeData) { - setInitialData(mockStudent); - } else { - fetchRegisterForm(studentId) - .then(data => { - console.log('Fetched data:', data); // Pour le débogage - const formattedData = { - ...data, - guardians: data.guardians || [] - }; - setInitialData(formattedData); - }) - .catch(error => { - console.error('Error fetching student data:', error); - }); - } - setIsLoading(false); - }, [studentId]); // Dépendance changée à studentId const handleSubmit = (data) => { if (useFakeData) { @@ -64,11 +45,10 @@ export default function Page() { return ( ); diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 5ddb2da..516916d 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -320,99 +320,79 @@ useEffect(()=>{ }; const createRF = (updatedData) => { - console.log('createRF updatedData:', updatedData); - if (updatedData.selectedGuardians.length !== 0) { const selectedGuardiansIds = updatedData.selectedGuardians.map(guardianId => guardianId) - const data = { - student: { - last_name: updatedData.studentLastName, - first_name: updatedData.studentFirstName, - }, - idGuardians: selectedGuardiansIds + student: { + last_name: updatedData.studentLastName, + first_name: updatedData.studentFirstName, + }, + idGuardians: selectedGuardiansIds }; - createRegisterForm(data,csrfToken) - .then(data => { - console.log('Success:', data); - setRegistrationFormsDataPending(prevState => { - if (prevState) { - return [...prevState, data]; + createRegisterForm(data, csrfToken) + .then(data => { + // Mise à jour immédiate des données + setRegistrationFormsDataPending(prevState => [...(prevState || []), data]); + setTotalPending(prev => prev + 1); + if (updatedData.autoMail) { + sendConfirmRegisterForm(data.student.id, updatedData.studentLastName, updatedData.studentFirstName); } - return [data]; - }); - setTotalPending(totalPending+1); - if (updatedData.autoMail) { - sendConfirmRegisterForm(data.student.id, updatedData.studentLastName, updatedData.studentFirstName); - } - }) - .catch((error) => { + closeModal(); + // Forcer le rechargement complet des données + setReloadFetch(true); + }) + .catch((error) => { console.error('Error:', error); - }); - } - else { - // Création d'un profil associé à l'adresse mail du responsable saisie - // Le profil est inactif - const data = { - email: updatedData.guardianEmail, - password: 'Provisoire01!', - username: updatedData.guardianEmail, - is_active: 0, // On rend le profil inactif : impossible de s'y connecter dans la fenêtre du login tant qu'il ne s'est pas inscrit - droit:2 // Profil PARENT - } - createProfile(data,csrfToken) + }); + } else { + const data = { + email: updatedData.guardianEmail, + password: 'Provisoire01!', + username: updatedData.guardianEmail, + is_active: 0, + droit: 2 + } + + createProfile(data, csrfToken) .then(response => { - console.log('Success:', response); - if (response.id) { - let idProfile = response.id; + if (response.id) { + const data = { + student: { + last_name: updatedData.studentLastName, + first_name: updatedData.studentFirstName, + guardians: [{ + email: updatedData.guardianEmail, + phone: updatedData.guardianPhone, + associated_profile: response.id + }], + sibling: [] + } + }; - const data = { - student: { - last_name: updatedData.studentLastName, - first_name: updatedData.studentFirstName, - guardians: [ - { - email: updatedData.guardianEmail, - phone: updatedData.guardianPhone, - associated_profile: idProfile // Association entre le responsable de l'élève et le profil créé par défaut précédemment - } - ], - sibling: [] - } - }; - - createRegisterForm(data,csrfToken) - .then(data => { - console.log('Success:', data); - setRegistrationFormsDataPending(prevState => { - if (prevState && prevState.length > 0) { - return [...prevState, data]; - } - return prevState; - }); - setTotalPending(totalPending+1); - if (updatedData.autoMail) { - sendConfirmRegisterForm(data.student.id, updatedData.studentLastName, updatedData.studentFirstName); - } - }) - .catch((error) => { - console.error('Error:', error); - }); - } + createRegisterForm(data, csrfToken) + .then(data => { + // Mise à jour immédiate des données + setRegistrationFormsDataPending(prevState => [...(prevState || []), data]); + setTotalPending(prev => prev + 1); + if (updatedData.autoMail) { + sendConfirmRegisterForm(data.student.id, updatedData.studentLastName, updatedData.studentFirstName); + } + closeModal(); + // Forcer le rechargement complet des données + setReloadFetch(true); + }) + .catch((error) => { + console.error('Error:', error); + }); + } }) .catch(error => { - console.error('Error fetching data:', error); - error = error.errorMessage; - console.log(error); + console.error('Error:', error); }); } - closeModal(); - setReloadFetch(true); } - - const columns = [ { name: t('studentName'), transform: (row) => row.student.last_name }, { name: t('studentFistName'), transform: (row) => row.student.first_name }, @@ -426,11 +406,11 @@ const columns = [ ) }, { name: t('files'), transform: (row) => - (row.registerForms != null) &&( + (row.registration_file != null) &&( ) }, @@ -507,11 +487,11 @@ const columnsSubscribed = [ ) }, { name: t('files'), transform: (row) => - (row.registerForm != null) &&( + (row.registration_file != null) &&( ) }, @@ -677,7 +657,7 @@ const handleFileUpload = ({file, name, is_required, order}) => { text={( <> {t('subscribeFiles')} - ({totalSubscribed}) + ({fichiers.length}) )} active={activeTab === 'subscribeFiles'} @@ -735,12 +715,14 @@ const handleFileUpload = ({file, name, is_required, order}) => { {/*SI STATE == subscribeFiles */} {activeTab === 'subscribeFiles' && (
- +
+ +
{ - if (!studentId || !idProfil) { - console.error('Missing studentId or idProfil'); - return; - } - if (useFakeData) { - setInitialData(mockStudent); - setLastGuardianId(999); - setIsLoading(false); - } else { - Promise.all([ - // Fetch eleve data - fetchRegisterForm(studentId), - // Fetch last guardian ID - fetchLastGuardian() - ]) - .then(async ([studentData, guardianData]) => { - const formattedData = { - ...studentData, - guardians: studentData.guardians || [] - }; - - setInitialData(formattedData); - setLastGuardianId(guardianData.lastid); - - let profils = studentData.profils; - const currentProf = profils.find(profil => profil.id === idProfil); - if (currentProf) { - setCurrentProfil(currentProf); - } - }) - .catch(error => { - console.error('Error fetching data:', error); - }) - .finally(() => { - setIsLoading(false); - }); - } - }, [studentId, idProfil]); const handleSubmit = async (data) => { if (useFakeData) { console.log('Fake submit:', data); return; } - try { - const result = await editRegisterForm(studentId, data, csrfToken); console.log('Success:', result); router.push(FE_PARENTS_HOME_URL); @@ -80,7 +37,7 @@ export default function Page() { return ( { - return fetch(`${BE_SUBSCRIPTION_STUDENT_URL}/${id}`) // Utilisation de studentId au lieu de codeDI + return fetch(`${BE_SUBSCRIPTION_REGISTERFORM_URL}/${id}`) // Utilisation de studentId au lieu de codeDI .then(requestResponseHandler) } export const fetchLastGuardian = () =>{ @@ -98,31 +98,36 @@ export const sendRegisterForm = (id) => { } -export const fetchRegisterFormFileTemplate = () => { - const request = new Request( - `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, - { - method:'GET', - headers: { - 'Content-Type':'application/json' - }, - } - ); - return fetch(request).then(requestResponseHandler) + + +export const fetchRegisterFormFile = (id = null) => { + let url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}` + if (id) { + url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${id}`; + } + const request = new Request( + `${url}`, + { + method:'GET', + headers: { + 'Content-Type':'application/json' + }, + } + ); + return fetch(request).then(requestResponseHandler) }; -export const fetchRegisterFormFile = (id) => { - const request = new Request( - `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${id}`, - { - method:'GET', - headers: { - 'Content-Type':'application/json' - }, - } - ); - return fetch(request).then(requestResponseHandler) -}; +export const editRegistrationFormFile= (fileId, data, csrfToken) => { + return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${fileId}`, { + method: 'PUT', + body: data, + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include', + }) + .then(requestResponseHandler) +} export const createRegistrationFormFile = (data,csrfToken) => { @@ -137,6 +142,33 @@ export const createRegistrationFormFile = (data,csrfToken) => { .then(requestResponseHandler) } +export const deleteRegisterFormFile= (fileId,csrfToken) => { + return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${fileId}`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include', + }) +} + +export const fetchRegisterFormFileTemplate = (id = null) => { + let url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`; + if(id){ + url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}/${id}`; + } + const request = new Request( + `${url}`, + { + method:'GET', + headers: { + 'Content-Type':'application/json' + }, + } + ); + return fetch(request).then(requestResponseHandler) +}; + export const createRegistrationFormFileTemplate = (data,csrfToken) => { return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, { diff --git a/Front-End/src/components/AffectationClasseForm.js b/Front-End/src/components/AffectationClasseForm.js index d118c56..b96db19 100644 --- a/Front-End/src/components/AffectationClasseForm.js +++ b/Front-End/src/components/AffectationClasseForm.js @@ -1,9 +1,9 @@ import React, { useState } from 'react'; -const AffectationClasseForm = ({ eleve, onSubmit, classes }) => { +const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => { const [formData, setFormData] = useState({ - classeAssocie_id: eleve.classeAssocie_id || null, + classeAssocie_id: eleve?.classeAssocie_id || null, }); const handleChange = (e) => { diff --git a/Front-End/src/components/FileStatusLabel.js b/Front-End/src/components/FileStatusLabel.js new file mode 100644 index 0000000..29330c5 --- /dev/null +++ b/Front-End/src/components/FileStatusLabel.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { Check, Clock } from 'lucide-react'; + +const FileStatusLabel = ({ status }) => { + const getStatusConfig = () => { + switch (status) { + case 'sent': + return { + label: 'Envoyé', + className: 'bg-green-50 text-green-600', + icon: + }; + case 'pending': + default: + return { + label: 'En attente', + className: 'bg-orange-50 text-orange-600', + icon: + }; + } + }; + + const { label, className, icon } = getStatusConfig(); + + return ( +
+ {icon} + {label} +
+ ); +}; + +export default FileStatusLabel; diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index 051f749..34aa4c3 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -1,3 +1,4 @@ +// Import des dépendances nécessaires import React, { useState, useEffect } from 'react'; import InputText from '@/components/InputText'; import SelectChoice from '@/components/SelectChoice'; @@ -6,12 +7,14 @@ import Loader from '@/components/Loader'; import Button from '@/components/Button'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import Table from '@/components/Table'; -import { fetchRegisterFormFileTemplate, createRegistrationFormFile } from '@/app/lib/subscriptionAction'; -import { Download, Upload } from 'lucide-react'; +import { fetchRegisterFormFileTemplate, createRegistrationFormFile, fetchRegisterForm, deleteRegisterFormFile } from '@/app/lib/subscriptionAction'; +import { Download, Upload, Trash2, Eye } from 'lucide-react'; import { BASE_URL } from '@/utils/Url'; import DraggableFileUpload from '@/app/[locale]/admin/subscriptions/components/DraggableFileUpload'; import Modal from '@/components/Modal'; +import FileStatusLabel from '@/components/FileStatusLabel'; +// Définition des niveaux scolaires disponibles const levels = [ { value:'1', label: 'TPS - Très Petite Section'}, { value:'2', label: 'PS - Petite Section'}, @@ -19,32 +22,28 @@ const levels = [ { value:'4', label: 'GS - Grande Section'}, ]; +/** + * Composant de formulaire d'inscription partagé + * @param {string} studentId - ID de l'étudiant + * @param {string} csrfToken - Token CSRF pour la sécurité + * @param {function} onSubmit - Fonction de soumission du formulaire + * @param {string} cancelUrl - URL de redirection en cas d'annulation + * @param {object} errors - Erreurs de validation du formulaire + */ export default function InscriptionFormShared({ - initialData, + studentId, csrfToken, onSubmit, cancelUrl, - isLoading = false, errors = {} // Nouvelle prop pour les erreurs }) { + // États pour gérer les données du formulaire + const [isLoading, setIsLoading] = useState(true); + const [formData, setFormData] = useState({}); - const [formData, setFormData] = useState(() => ({ - id: initialData?.id || '', - last_name: initialData?.last_name || '', - first_name: initialData?.first_name || '', - address: initialData?.address || '', - birth_date: initialData?.birth_date || '', - birth_place: initialData?.birth_place || '', - birth_postal_code: initialData?.birth_postal_code || '', - nationality: initialData?.nationality || '', - attending_physician: initialData?.attending_physician || '', - level: initialData?.level || '' - })); - - const [guardians, setGuardians] = useState(() => - initialData?.guardians || [] - ); + const [guardians, setGuardians] = useState([]); + // États pour la gestion des fichiers const [uploadedFiles, setUploadedFiles] = useState([]); const [fileTemplates, setFileTemplates] = useState([]); const [fileName, setFileName] = useState(""); @@ -52,50 +51,104 @@ export default function InscriptionFormShared({ const [showUploadModal, setShowUploadModal] = useState(false); const [currentTemplateId, setCurrentTemplateId] = useState(null); + // Chargement initial des données // Mettre à jour les données quand initialData change useEffect(() => { - if (initialData) { - setFormData({ - id: initialData.id || '', - last_name: initialData.last_name || '', - first_name: initialData.first_name || '', - address: initialData.address || '', - birth_date: initialData.birth_date || '', - birth_place: initialData.birth_place || '', - birth_postal_code: initialData.birth_postal_code || '', - nationality: initialData.nationality || '', - attending_physician: initialData.attending_physician || '', - level: initialData.level || '' + if (studentId) { + fetchRegisterForm(studentId).then((data) => { + console.log(data); + + setFormData({ + id: data?.student?.id || '', + last_name: data?.student?.last_name || '', + first_name: data?.student?.first_name || '', + address: data?.student?.address || '', + birth_date: data?.student?.birth_date || '', + birth_place: data?.student?.birth_place || '', + birth_postal_code: data?.student?.birth_postal_code || '', + nationality: data?.student?.nationality || '', + attending_physician: data?.student?.attending_physician || '', + level: data?.student?.level || '' + }); + setGuardians(data?.student?.guardians || []); + setUploadedFiles(data.registration_files || []); }); - setGuardians(initialData.guardians || []); + fetchRegisterFormFileTemplate().then((data) => { setFileTemplates(data); }); + setIsLoading(false); } - }, [initialData]); + }, [studentId]); + // Fonctions de gestion du formulaire et des fichiers const updateFormField = (field, value) => { setFormData(prev => ({...prev, [field]: value})); }; + // Gestion du téléversement de fichiers const handleFileUpload = async (file, fileName) => { + if (!file || !currentTemplateId || !formData.id) { + console.error('Missing required data for upload'); + return; + } + const data = new FormData(); data.append('file', file); - data.append('name',fileName); + data.append('name', fileName); data.append('template', currentTemplateId); data.append('register_form', formData.id); try { - await createRegistrationFormFile(data, csrfToken); - // Optionnellement, rafraîchir la liste des fichiers - fetchRegisterFormFileTemplate().then((data) => { - setFileTemplates(data); - }); + const response = await createRegistrationFormFile(data, csrfToken); + if (response) { + setUploadedFiles(prev => { + const newFiles = prev.filter(f => parseInt(f.template) !== currentTemplateId); + return [...newFiles, { + name: fileName, + template: currentTemplateId, + file: response.file + }]; + }); + + // Rafraîchir les données du formulaire pour avoir les fichiers à jour + if (studentId) { + fetchRegisterForm(studentId).then((data) => { + setUploadedFiles(data.registration_files || []); + }); + } + } } catch (error) { console.error('Error uploading file:', error); } }; + // Vérification si un fichier est déjà uploadé + const isFileUploaded = (templateId) => { + return uploadedFiles.find(template => + template.template === templateId + ); + }; + + // Récupération d'un fichier uploadé + const getUploadedFile = (templateId) => { + return uploadedFiles.find(file => parseInt(file.template) === templateId); + }; + + // Suppression d'un fichier + const handleDeleteFile = async (templateId) => { + const fileToDelete = getUploadedFile(templateId); + if (!fileToDelete) return; + + try { + await deleteRegisterFormFile(fileToDelete.id, csrfToken); + setUploadedFiles(prev => prev.filter(f => parseInt(f.template) !== templateId)); + } catch (error) { + console.error('Error deleting file:', error); + } + }; + + // Soumission du formulaire const handleSubmit = (e) => { e.preventDefault(); const data ={ @@ -107,36 +160,70 @@ export default function InscriptionFormShared({ onSubmit(data); }; + // Récupération des messages d'erreur const getError = (field) => { return errors?.student?.[field]?.[0]; }; - const getGuardianError = (index, field) => { - return errors?.student?.guardians?.[index]?.[field]?.[0]; - }; - + // Configuration des colonnes pour le tableau des fichiers const columns = [ { name: 'Nom du fichier', transform: (row) => row.name }, { name: 'Fichier à Remplir', transform: (row) => row.is_required ? 'Oui' : 'Non' }, { name: 'Fichier de référence', transform: (row) => row.file && }, - { name: 'Actions', transform: (row) => ( -
- {row.is_required && - +
+ ); + } + + return ( + - } -
- ) }, + ); + }}, ]; + // Affichage du loader pendant le chargement if (isLoading) return ; + // Rendu du composant return (
@@ -245,17 +332,19 @@ export default function InscriptionFormShared({
{/* Section Fichiers d'inscription */} -
-

Fichiers à remplir

-
{}} - /> - + {fileTemplates.length > 0 && ( +
+

Fichiers à remplir

+
{}} + /> + + )} {/* Boutons de contrôle */}
@@ -263,44 +352,52 @@ export default function InscriptionFormShared({
- ( - <> - { - setFile(selectedFile); - setFileName(selectedFile.name); - }} - > - - - -
-
- - )} - /> +
+
+ + )} + /> + )} ); } \ No newline at end of file diff --git a/Front-End/src/components/Inscription/ResponsableInputFields.js b/Front-End/src/components/Inscription/ResponsableInputFields.js index edaf92d..4cdb49b 100644 --- a/Front-End/src/components/Inscription/ResponsableInputFields.js +++ b/Front-End/src/components/Inscription/ResponsableInputFields.js @@ -4,6 +4,7 @@ import Button from '@/components/Button'; import React from 'react'; import { useTranslations } from 'next-intl'; import 'react-phone-number-input/style.css' +import { Trash2, Plus } from 'lucide-react'; export default function ResponsableInputFields({guardians, onGuardiansChange, addGuardian, deleteGuardian, errors = []}) { const t = useTranslations('ResponsableInputFields'); @@ -19,10 +20,9 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad

{t('responsable')} {index+1}

{guardians.length > 1 && ( -
@@ -102,13 +102,9 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad ))}
-
diff --git a/Front-End/src/hooks/useCsrfToken.js b/Front-End/src/hooks/useCsrfToken.js index 284dc8b..1489d70 100644 --- a/Front-End/src/hooks/useCsrfToken.js +++ b/Front-End/src/hooks/useCsrfToken.js @@ -14,7 +14,7 @@ const useCsrfToken = () => { if (data) { if(data.csrfToken != token) { setToken(data.csrfToken); - console.log('------------> CSRF Token reçu:', data.csrfToken); + //console.log('------------> CSRF Token reçu:', data.csrfToken); } } }) From ece23deb19483c50d9999541a482e3378db19d23 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sat, 25 Jan 2025 16:40:08 +0100 Subject: [PATCH 011/249] =?UTF-8?q?feat:=20Ajout=20des=20frais=20d'inscrip?= =?UTF-8?q?tion=20lors=20de=20la=20cr=C3=A9ation=20d'un=20RF=20[#18]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/School/models.py | 1 - Back-End/School/serializers.py | 32 ---- Back-End/Subscriptions/models.py | 8 +- Back-End/Subscriptions/serializers.py | 18 ++- .../src/app/[locale]/admin/structure/page.js | 2 +- .../app/[locale]/admin/subscriptions/page.js | 50 +++++- Front-End/src/components/CheckBox.js | 39 +++++ Front-End/src/components/CheckBoxList.js | 56 ++----- .../components/Inscription/InscriptionForm.js | 143 +++++++++++++++++- .../DiscountsSection.js | 57 ++++--- .../FeesManagement.js | 4 +- .../FeesSection.js | 59 ++++---- 12 files changed, 333 insertions(+), 136 deletions(-) create mode 100644 Front-End/src/components/CheckBox.js rename Front-End/src/components/Structure/{Configuration => Tarification}/DiscountsSection.js (81%) rename Front-End/src/components/Structure/{Configuration => Tarification}/FeesManagement.js (95%) rename Front-End/src/components/Structure/{Configuration => Tarification}/FeesSection.js (86%) diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 7c7f609..f549d77 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -96,7 +96,6 @@ class Fee(models.Model): name = models.CharField(max_length=255, unique=True) base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) description = models.TextField(blank=True) - discounts = models.ManyToManyField('Discount', blank=True) is_active = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE) diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index a4e2a90..7c63f88 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,8 +1,5 @@ from rest_framework import serializers from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee -from Subscriptions.models import RegistrationForm -from Subscriptions.serializers import StudentSerializer -from Auth.serializers import ProfileSerializer from Auth.models import Profile from N3wtSchool import settings, bdd from django.utils import timezone @@ -187,41 +184,12 @@ class DiscountSerializer(serializers.ModelSerializer): return local_time.strftime("%d-%m-%Y %H:%M") class FeeSerializer(serializers.ModelSerializer): - discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) updated_at_formatted = serializers.SerializerMethodField() class Meta: model = Fee fields = '__all__' - def create(self, validated_data): - discounts_data = validated_data.pop('discounts', []) - - # Create the Fee instance - fee = Fee.objects.create(**validated_data) - - # Add discounts if provided - fee.discounts.set(discounts_data) - - return fee - - def update(self, instance, validated_data): - discounts_data = validated_data.pop('discounts', []) - - # Update the Fee instance - instance.name = validated_data.get('name', instance.name) - instance.description = validated_data.get('description', instance.description) - instance.base_amount = validated_data.get('base_amount', instance.base_amount) - instance.is_active = validated_data.get('is_active', instance.is_active) - instance.updated_at = validated_data.get('updated_at', instance.updated_at) - instance.type = validated_data.get('type', instance.type) - instance.save() - - # Update discounts if provided - instance.discounts.set(discounts_data) - - return instance - def get_updated_at_formatted(self, obj): utc_time = timezone.localtime(obj.updated_at) local_tz = pytz.timezone(settings.TZ_APPLI) diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index b183107..fe7d2af 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -4,7 +4,7 @@ from django.conf import settings from django.utils.translation import gettext_lazy as _ from Auth.models import Profile -from School.models import SchoolClass +from School.models import SchoolClass, Fee, Discount from datetime import datetime @@ -204,6 +204,12 @@ class RegistrationForm(models.Model): registration_file = models.FileField(upload_to=settings.DOCUMENT_DIR, default="", blank=True) associated_rf = models.CharField(max_length=200, default="", blank=True) + # Many-to-Many Relationship + fees = models.ManyToManyField(Fee, blank=True, related_name='register_forms') + + # Many-to-Many Relationship + discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms') + def __str__(self): return "RF_" + self.student.last_name + "_" + self.student.first_name diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index b4a996c..2e916a7 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee -from School.models import SchoolClass +from School.models import SchoolClass, Fee, Discount +from School.serializers import FeeSerializer, DiscountSerializer from Auth.models import Profile from Auth.serializers import ProfileSerializer from GestionMessagerie.models import Messagerie @@ -133,6 +134,9 @@ class RegistrationFormSerializer(serializers.ModelSerializer): status_label = serializers.SerializerMethodField() formatted_last_update = serializers.SerializerMethodField() registration_files = RegistrationFileSerializer(many=True, required=False) + fees = serializers.PrimaryKeyRelatedField(queryset=Fee.objects.all(), many=True, required=False) + discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True, required=False) + class Meta: model = RegistrationForm fields = '__all__' @@ -140,11 +144,19 @@ class RegistrationFormSerializer(serializers.ModelSerializer): def create(self, validated_data): student_data = validated_data.pop('student') student = StudentSerializer.create(StudentSerializer(), student_data) + fees_data = validated_data.pop('fees', []) + discounts_data = validated_data.pop('discounts', []) registrationForm = RegistrationForm.objects.create(student=student, **validated_data) + + # Associer les IDs des objets Fee et Discount au RegistrationForm + registrationForm.fees.set([fee.id for fee in fees_data]) + registrationForm.discounts.set([discount.id for discount in discounts_data]) return registrationForm def update(self, instance, validated_data): student_data = validated_data.pop('student', None) + fees_data = validated_data.pop('fees', []) + discounts_data = validated_data.pop('discounts', []) if student_data: student = instance.student StudentSerializer.update(StudentSerializer(), student, student_data) @@ -156,6 +168,10 @@ class RegistrationFormSerializer(serializers.ModelSerializer): pass instance.save() + # Associer les IDs des objets Fee et Discount au RegistrationForm + instance.fees.set([fee.id for fee in fees_data]) + instance.discounts.set([discount.id for discount in discounts_data]) + return instance def get_status_label(self, obj): diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 545a112..e76e5ed 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import StructureManagement from '@/components/Structure/Configuration/StructureManagement'; import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'; -import FeesManagement from '@/components/Structure/Configuration/FeesManagement'; +import FeesManagement from '@/components/Structure/Tarification/FeesManagement'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 5ddb2da..641d223 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -32,7 +32,13 @@ import { fetchStudents, editRegisterForm } from "@/app/lib/subscriptionAction" -import { fetchClasses } from '@/app/lib/schoolAction'; +import { + fetchClasses, + fetchRegistrationDiscounts, + fetchTuitionDiscounts, + fetchRegistrationFees, + fetchTuitionFees } from '@/app/lib/schoolAction'; + import { createProfile } from '@/app/lib/authAction'; import { @@ -75,6 +81,11 @@ export default function Page({ params: { locale } }) { const [isEditing, setIsEditing] = useState(false); const [fileToEdit, setFileToEdit] = useState(null); + const [registrationDiscounts, setRegistrationDiscounts] = useState([]); + const [tuitionDiscounts, setTuitionDiscounts] = useState([]); + const [registrationFees, setRegistrationFees] = useState([]); + const [tuitionFees, setTuitionFees] = useState([]); + const csrfToken = useCsrfToken(); const openModal = () => { @@ -151,6 +162,7 @@ const registerFormArchivedDataHandler = (data) => { } } } + // TODO: revoir le système de pagination et de UseEffect useEffect(() => { @@ -195,7 +207,27 @@ const registerFormArchivedDataHandler = (data) => { setFichiers(data) }) - .catch((err)=>{ err = err.message; console.log(err);}); + .catch((err)=>{ err = err.message; console.log(err);}) + fetchRegistrationDiscounts() + .then(data => { + setRegistrationDiscounts(data); + }) + .catch(requestErrorHandler) + fetchTuitionDiscounts() + .then(data => { + setTuitionDiscounts(data); + }) + .catch(requestErrorHandler) + fetchRegistrationFees() + .then(data => { + setRegistrationFees(data); + }) + .catch(requestErrorHandler) + fetchTuitionFees() + .then(data => { + setTuitionFees(data); + }) + .catch(requestErrorHandler); } else { setTimeout(() => { setRegistrationFormsDataPending(mockFicheInscription); @@ -321,6 +353,8 @@ useEffect(()=>{ const createRF = (updatedData) => { console.log('createRF updatedData:', updatedData); + const selectedRegistrationFeesIds = updatedData.selectedRegistrationFees.map(feeId => feeId) + const selectedRegistrationDiscountsIds = updatedData.selectedRegistrationDiscounts.map(discountId => discountId) if (updatedData.selectedGuardians.length !== 0) { const selectedGuardiansIds = updatedData.selectedGuardians.map(guardianId => guardianId) @@ -330,7 +364,9 @@ useEffect(()=>{ last_name: updatedData.studentLastName, first_name: updatedData.studentFirstName, }, - idGuardians: selectedGuardiansIds + idGuardians: selectedGuardiansIds, + fees: selectedRegistrationFeesIds, + discounts: selectedRegistrationDiscountsIds }; createRegisterForm(data,csrfToken) @@ -379,7 +415,9 @@ useEffect(()=>{ } ], sibling: [] - } + }, + fees: selectedRegistrationFeesIds, + discounts: selectedRegistrationDiscountsIds }; createRegisterForm(data,csrfToken) @@ -784,6 +822,10 @@ const handleFileUpload = ({file, name, is_required, order}) => { size='sm:w-1/4' ContentComponent={() => ( )} diff --git a/Front-End/src/components/CheckBox.js b/Front-End/src/components/CheckBox.js new file mode 100644 index 0000000..3818736 --- /dev/null +++ b/Front-End/src/components/CheckBox.js @@ -0,0 +1,39 @@ +import React from 'react'; + +const CheckBox = ({ item, formData, handleChange, fieldName, itemLabelFunc = () => null, labelAttenuated = () => false, horizontal }) => { + const isChecked = formData[fieldName].includes(parseInt(item.id)); + const isAttenuated = labelAttenuated(item) && !isChecked; + + return ( +
+ {horizontal && ( + + )} + + {!horizontal && ( + + )} +
+ ); +}; + +export default CheckBox; \ No newline at end of file diff --git a/Front-End/src/components/CheckBoxList.js b/Front-End/src/components/CheckBoxList.js index 9514f99..8ff7a8c 100644 --- a/Front-End/src/components/CheckBoxList.js +++ b/Front-End/src/components/CheckBoxList.js @@ -1,4 +1,5 @@ import React from 'react'; +import CheckBox from '@/components/CheckBox'; const CheckBoxList = ({ items, @@ -12,10 +13,6 @@ const CheckBoxList = ({ labelAttenuated = () => false, horizontal = false // Ajouter l'option horizontal }) => { - const handleCheckboxChange = (e) => { - handleChange(e); - }; - return (
- {items.map(item => { - const isChecked = formData[fieldName].includes(parseInt(item.id)); - const isAttenuated = labelAttenuated(item) && !isChecked; - return ( -
- {horizontal && ( - - )} - - {!horizontal && ( - - )} -
- ); - })} + {items.map(item => ( + + ))}
); diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index 9414a19..8895f33 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -1,10 +1,13 @@ -import { useState } from 'react'; -import { User, Mail, Phone, UserCheck } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { User, Mail, Phone, UserCheck, DollarSign, Percent } from 'lucide-react'; import InputTextIcon from '@/components/InputTextIcon'; import ToggleSwitch from '@/components/ToggleSwitch'; import Button from '@/components/Button'; +import Table from '@/components/Table'; +import FeesSection from '@/components/Structure/Tarification/FeesSection'; +import DiscountsSection from '../Structure/Tarification/DiscountsSection'; -const InscriptionForm = ( { students, onSubmit }) => { +const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, onSubmit }) => { const [formData, setFormData] = useState({ studentLastName: '', studentFirstName: '', @@ -12,14 +15,26 @@ const InscriptionForm = ( { students, onSubmit }) => { guardianPhone: '', selectedGuardians: [], responsableType: 'new', - autoMail: false + autoMail: false, + selectedRegistrationDiscounts: [], + selectedRegistrationFees: registrationFees.map(fee => fee.id) }); - const [step, setStep] = useState(1); + const [step, setStep] = useState(0); const [selectedStudent, setSelectedEleve] = useState(''); const [existingGuardians, setExistingGuardians] = useState([]); + const [totalRegistrationAmount, setTotalRegistrationAmount] = useState(0); const maxStep = 4 + useEffect(() => { + // Calcul du montant total lors de l'initialisation + const initialTotalAmount = calculateFinalRegistrationAmount( + registrationFees.map(fee => fee.id), + [] + ); + setTotalRegistrationAmount(initialTotalAmount); + }, [registrationDiscounts, registrationFees]); + const handleToggleChange = () => { setFormData({ ...formData, autoMail: !formData.autoMail }); }; @@ -39,7 +54,7 @@ const InscriptionForm = ( { students, onSubmit }) => { }; const prevStep = () => { - if (step > 1) { + if (step >= 1) { setStep(step - 1); } }; @@ -66,8 +81,122 @@ const InscriptionForm = ( { students, onSubmit }) => { onSubmit(formData); } + const handleFeeSelection = (feeId) => { + setFormData((prevData) => { + const selectedRegistrationFees = prevData.selectedRegistrationFees.includes(feeId) + ? prevData.selectedRegistrationFees.filter(id => id !== feeId) + : [...prevData.selectedRegistrationFees, feeId]; + const finalAmount = calculateFinalRegistrationAmount(selectedRegistrationFees, prevData.selectedRegistrationDiscounts); + setTotalRegistrationAmount(finalAmount); + return { ...prevData, selectedRegistrationFees }; + }); + }; + + const handleDiscountSelection = (discountId) => { + setFormData((prevData) => { + const selectedRegistrationDiscounts = prevData.selectedRegistrationDiscounts.includes(discountId) + ? prevData.selectedRegistrationDiscounts.filter(id => id !== discountId) + : [...prevData.selectedRegistrationDiscounts, discountId]; + const finalAmount = calculateFinalRegistrationAmount(prevData.selectedRegistrationFees, selectedRegistrationDiscounts); + setTotalRegistrationAmount(finalAmount); + return { ...prevData, selectedRegistrationDiscounts }; + }); + }; + + const calculateFinalRegistrationAmount = (selectedRegistrationFees, selectedRegistrationDiscounts) => { + const totalFees = selectedRegistrationFees.reduce((sum, feeId) => { + const fee = registrationFees.find(f => f.id === feeId); + if (fee && !isNaN(parseFloat(fee.base_amount))) { + return sum + parseFloat(fee.base_amount); + } + return sum; + }, 0); + + console.log(totalFees); + + const totalDiscounts = selectedRegistrationDiscounts.reduce((sum, discountId) => { + const discount = registrationDiscounts.find(d => d.id === discountId); + if (discount) { + if (discount.discount_type === 0 && !isNaN(parseFloat(discount.amount))) { // Currency + return sum + parseFloat(discount.amount); + } else if (discount.discount_type === 1 && !isNaN(parseFloat(discount.amount))) { // Percent + return sum + (totalFees * parseFloat(discount.amount) / 100); + } + } + return sum; + }, 0); + + const finalAmount = totalFees - totalDiscounts; + + return finalAmount.toFixed(2); + }; + + const isLabelAttenuated = (item) => { + return !formData.selectedRegistrationDiscounts.includes(parseInt(item.id)); + }; + + const isLabelFunction = (item) => { + return item.name + ' : ' + item.amount + }; + return (
+ {step === 0 && ( +
+

Frais d'inscription

+ {registrationFees.length > 0 ? ( + <> +
+ +
+

Réductions

+
+ {registrationDiscounts.length > 0 ? ( + + ) : ( +

+ Information + Aucune réduction n'a été créée sur les frais d'inscription. +

+ )} +
+
MONTANT TOTAL + }, + { + name: 'TOTAL', + transform: () => {totalRegistrationAmount} € + } + ]} + defaultTheme='bg-cyan-100' + /> + + ) : ( +

+ Attention! + Aucun frais d'inscription n'a été créé. +

+ )} + + + )} + {step === 1 && (

Nouvel élève

@@ -270,7 +399,7 @@ const InscriptionForm = ( { students, onSubmit }) => { )}
- {step > 1 && ( + {step >= 1 && (
); + case '': + return ( +
+ handleDiscountSelection(discount.id)} + fieldName="selectedDiscounts" + /> +
+ ); default: return null; } } }; + const columns = subscriptionMode + ? [ + { name: 'LIBELLE', label: 'Libellé' }, + { name: 'DESCRIPTION', label: 'Description' }, + { name: 'REMISE', label: 'Remise' }, + { name: '', label: 'Sélection' } + ] + : [ + { name: 'LIBELLE', label: 'Libellé' }, + { name: 'REMISE', label: 'Remise' }, + { name: 'DESCRIPTION', label: 'Description' }, + { name: 'MISE A JOUR', label: 'Date mise à jour' }, + { name: 'ACTIONS', label: 'Actions' } + ]; + return (
-
-
- -

Réductions {type === 0 ? 'd\'inscription' : 'de scolarité'}

+ {!subscriptionMode && ( +
+
+ +

Réductions {type === 0 ? 'd\'inscription' : 'de scolarité'}

+
+
- -
+ )}
diff --git a/Front-End/src/components/Structure/Configuration/FeesManagement.js b/Front-End/src/components/Structure/Tarification/FeesManagement.js similarity index 95% rename from Front-End/src/components/Structure/Configuration/FeesManagement.js rename to Front-End/src/components/Structure/Tarification/FeesManagement.js index bbaba49..d83ac9e 100644 --- a/Front-End/src/components/Structure/Configuration/FeesManagement.js +++ b/Front-End/src/components/Structure/Tarification/FeesManagement.js @@ -1,6 +1,6 @@ import React from 'react'; -import FeesSection from '@/components/Structure/Configuration/FeesSection'; -import DiscountsSection from '@/components/Structure/Configuration/DiscountsSection'; +import FeesSection from '@/components/Structure/Tarification/FeesSection'; +import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection'; import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL } from '@/utils/Url'; const FeesManagement = ({ registrationDiscounts, setRegistrationDiscounts, tuitionDiscounts, setTuitionDiscounts, registrationFees, setRegistrationFees, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { diff --git a/Front-End/src/components/Structure/Configuration/FeesSection.js b/Front-End/src/components/Structure/Tarification/FeesSection.js similarity index 86% rename from Front-End/src/components/Structure/Configuration/FeesSection.js rename to Front-End/src/components/Structure/Tarification/FeesSection.js index cbfe960..25669ab 100644 --- a/Front-End/src/components/Structure/Configuration/FeesSection.js +++ b/Front-End/src/components/Structure/Tarification/FeesSection.js @@ -1,10 +1,11 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react'; +import { Plus, Trash, Edit3, Check, X, EyeOff, Eye, CreditCard, BookOpen } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; +import CheckBox from '@/components/CheckBox'; -const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type }) => { +const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedFees, handleFeeSelection }) => { const [editingFee, setEditingFee] = useState(null); const [newFee, setNewFee] = useState(null); const [formData, setFormData] = useState({}); @@ -122,24 +123,6 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl ); - const calculateFinalAmount = (baseAmount, discountIds) => { - const totalDiscounts = discountIds.reduce((sum, discountId) => { - const discount = discounts.find(d => d.id === discountId); - if (discount) { - if (discount.discount_type === 0) { // Currency - return sum + parseFloat(discount.amount); - } else if (discount.discount_type === 1) { // Percent - return sum + (parseFloat(baseAmount) * parseFloat(discount.amount) / 100); - } - } - return sum; - }, 0); - - const finalAmount = parseFloat(baseAmount) - totalDiscounts; - - return finalAmount.toFixed(2); - }; - const renderFeeCell = (fee, column) => { const isEditing = editingFee === fee.id; const isCreating = newFee && newFee.id === fee.id; @@ -211,14 +194,41 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl ); + case '': + return ( +
+ handleFeeSelection(fee.id)} + fieldName="selectedFees" + /> +
+ ); default: return null; } } }; + const columns = subscriptionMode + ? [ + { name: 'NOM', label: 'Nom' }, + { name: 'DESCRIPTION', label: 'Description' }, + { name: 'MONTANT', label: 'Montant de base' }, + { name: '', label: 'Sélection' } + ] + : [ + { name: 'NOM', label: 'Nom' }, + { name: 'MONTANT', label: 'Montant de base' }, + { name: 'DESCRIPTION', label: 'Description' }, + { name: 'MISE A JOUR', label: 'Date mise à jour' }, + { name: 'ACTIONS', label: 'Actions' } + ]; + return (
+ {!subscriptionMode && (
@@ -228,15 +238,10 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
+ )}
Date: Sat, 25 Jan 2025 17:23:15 +0100 Subject: [PATCH 012/249] =?UTF-8?q?feat:=20Ajout=20des=20frais=20de=20scol?= =?UTF-8?q?arit=C3=A9=20dans=20le=20dossier=20d'inscription=20[#18]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/[locale]/admin/subscriptions/page.js | 13 +- .../components/Inscription/InscriptionForm.js | 252 +++++++++++++----- 2 files changed, 188 insertions(+), 77 deletions(-) diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 641d223..c555a73 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -355,6 +355,11 @@ useEffect(()=>{ console.log('createRF updatedData:', updatedData); const selectedRegistrationFeesIds = updatedData.selectedRegistrationFees.map(feeId => feeId) const selectedRegistrationDiscountsIds = updatedData.selectedRegistrationDiscounts.map(discountId => discountId) + const selectedTuitionFeesIds = updatedData.selectedTuitionFees.map(feeId => feeId) + const selectedTuitionDiscountsIds = updatedData.selectedTuitionDiscounts.map(discountId => discountId) + + const allFeesIds = [...selectedRegistrationFeesIds, ...selectedTuitionFeesIds]; + const allDiscountsds = [...selectedRegistrationDiscountsIds, ...selectedTuitionDiscountsIds]; if (updatedData.selectedGuardians.length !== 0) { const selectedGuardiansIds = updatedData.selectedGuardians.map(guardianId => guardianId) @@ -365,8 +370,8 @@ useEffect(()=>{ first_name: updatedData.studentFirstName, }, idGuardians: selectedGuardiansIds, - fees: selectedRegistrationFeesIds, - discounts: selectedRegistrationDiscountsIds + fees: allFeesIds, + discounts: allDiscountsds }; createRegisterForm(data,csrfToken) @@ -416,8 +421,8 @@ useEffect(()=>{ ], sibling: [] }, - fees: selectedRegistrationFeesIds, - discounts: selectedRegistrationDiscountsIds + fees: allFeesIds, + discounts: allDiscountsds }; createRegisterForm(data,csrfToken) diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index 8895f33..adbe6d2 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -17,22 +17,26 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r responsableType: 'new', autoMail: false, selectedRegistrationDiscounts: [], - selectedRegistrationFees: registrationFees.map(fee => fee.id) + selectedRegistrationFees: registrationFees.map(fee => fee.id), + selectedTuitionDiscounts: [], + selectedTuitionFees: [] }); - const [step, setStep] = useState(0); + const [step, setStep] = useState(1); const [selectedStudent, setSelectedEleve] = useState(''); const [existingGuardians, setExistingGuardians] = useState([]); const [totalRegistrationAmount, setTotalRegistrationAmount] = useState(0); - const maxStep = 4 + const [totalTuitionAmount, setTotalTuitionAmount] = useState(0); + const maxStep = 6 useEffect(() => { - // Calcul du montant total lors de l'initialisation - const initialTotalAmount = calculateFinalRegistrationAmount( + // Calcul du montant total des frais d'inscription lors de l'initialisation + const initialTotalRegistrationAmount = calculateFinalRegistrationAmount( registrationFees.map(fee => fee.id), [] ); - setTotalRegistrationAmount(initialTotalAmount); + setTotalRegistrationAmount(initialTotalRegistrationAmount); + }, [registrationDiscounts, registrationFees]); const handleToggleChange = () => { @@ -54,7 +58,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r }; const prevStep = () => { - if (step >= 1) { + if (step > 1) { setStep(step - 1); } }; @@ -81,7 +85,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r onSubmit(formData); } - const handleFeeSelection = (feeId) => { + const handleRegistrationFeeSelection = (feeId) => { setFormData((prevData) => { const selectedRegistrationFees = prevData.selectedRegistrationFees.includes(feeId) ? prevData.selectedRegistrationFees.filter(id => id !== feeId) @@ -90,9 +94,20 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r setTotalRegistrationAmount(finalAmount); return { ...prevData, selectedRegistrationFees }; }); - }; + }; + + const handleTuitionFeeSelection = (feeId) => { + setFormData((prevData) => { + const selectedTuitionFees = prevData.selectedTuitionFees.includes(feeId) + ? prevData.selectedTuitionFees.filter(id => id !== feeId) + : [...prevData.selectedTuitionFees, feeId]; + const finalAmount = calculateFinalTuitionAmount(selectedTuitionFees, prevData.selectedTuitionDiscounts); + setTotalTuitionAmount(finalAmount); + return { ...prevData, selectedTuitionFees }; + }); + }; - const handleDiscountSelection = (discountId) => { + const handleRegistrationDiscountSelection = (discountId) => { setFormData((prevData) => { const selectedRegistrationDiscounts = prevData.selectedRegistrationDiscounts.includes(discountId) ? prevData.selectedRegistrationDiscounts.filter(id => id !== discountId) @@ -101,9 +116,20 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r setTotalRegistrationAmount(finalAmount); return { ...prevData, selectedRegistrationDiscounts }; }); - }; + }; + + const handleTuitionDiscountSelection = (discountId) => { + setFormData((prevData) => { + const selectedTuitionDiscounts = prevData.selectedTuitionDiscounts.includes(discountId) + ? prevData.selectedTuitionDiscounts.filter(id => id !== discountId) + : [...prevData.selectedTuitionDiscounts, discountId]; + const finalAmount = calculateFinalTuitionAmount(prevData.selectedTuitionFees, selectedTuitionDiscounts); + setTotalTuitionAmount(finalAmount); + return { ...prevData, selectedTuitionDiscounts }; + }); + }; - const calculateFinalRegistrationAmount = (selectedRegistrationFees, selectedRegistrationDiscounts) => { + const calculateFinalRegistrationAmount = (selectedRegistrationFees, selectedRegistrationDiscounts) => { const totalFees = selectedRegistrationFees.reduce((sum, feeId) => { const fee = registrationFees.find(f => f.id === feeId); if (fee && !isNaN(parseFloat(fee.base_amount))) { @@ -111,8 +137,6 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r } return sum; }, 0); - - console.log(totalFees); const totalDiscounts = selectedRegistrationDiscounts.reduce((sum, discountId) => { const discount = registrationDiscounts.find(d => d.id === discountId); @@ -129,7 +153,33 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r const finalAmount = totalFees - totalDiscounts; return finalAmount.toFixed(2); - }; + }; + + const calculateFinalTuitionAmount = (selectedTuitionFees, selectedTuitionDiscounts) => { + const totalFees = selectedTuitionFees.reduce((sum, feeId) => { + const fee = tuitionFees.find(f => f.id === feeId); + if (fee && !isNaN(parseFloat(fee.base_amount))) { + return sum + parseFloat(fee.base_amount); + } + return sum; + }, 0); + + const totalDiscounts = selectedTuitionDiscounts.reduce((sum, discountId) => { + const discount = tuitionDiscounts.find(d => d.id === discountId); + if (discount) { + if (discount.discount_type === 0 && !isNaN(parseFloat(discount.amount))) { // Currency + return sum + parseFloat(discount.amount); + } else if (discount.discount_type === 1 && !isNaN(parseFloat(discount.amount))) { // Percent + return sum + (totalFees * parseFloat(discount.amount) / 100); + } + } + return sum; + }, 0); + + const finalAmount = totalFees - totalDiscounts; + + return finalAmount.toFixed(2); + }; const isLabelAttenuated = (item) => { return !formData.selectedRegistrationDiscounts.includes(parseInt(item.id)); @@ -140,63 +190,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r }; return ( -
- {step === 0 && ( -
-

Frais d'inscription

- {registrationFees.length > 0 ? ( - <> -
- -
-

Réductions

-
- {registrationDiscounts.length > 0 ? ( - - ) : ( -

- Information - Aucune réduction n'a été créée sur les frais d'inscription. -

- )} -
-
MONTANT TOTAL - }, - { - name: 'TOTAL', - transform: () => {totalRegistrationAmount} € - } - ]} - defaultTheme='bg-cyan-100' - /> - - ) : ( -

- Attention! - Aucun frais d'inscription n'a été créé. -

- )} - - - )} - +
{step === 1 && (

Nouvel élève

@@ -324,6 +318,118 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
)} + {step === 4 && ( +
+

Frais d'inscription

+ {registrationFees.length > 0 ? ( + <> +
+ +
+

Réductions

+
+ {registrationDiscounts.length > 0 ? ( + + ) : ( +

+ Information + Aucune réduction n'a été créée sur les frais d'inscription. +

+ )} +
+
MONTANT TOTAL + }, + { + name: 'TOTAL', + transform: () => {totalRegistrationAmount} € + } + ]} + defaultTheme='bg-cyan-100' + /> + + ) : ( +

+ Attention! + Aucun frais d'inscription n'a été créé. +

+ )} + + + )} + + {step === 5 && ( +
+

Frais de scolarité

+ {tuitionFees.length > 0 ? ( + <> +
+ +
+

Réductions

+
+ {tuitionDiscounts.length > 0 ? ( + + ) : ( +

+ Information + Aucune réduction n'a été créée sur les frais de scolarité. +

+ )} +
+
MONTANT TOTAL + }, + { + name: 'TOTAL', + transform: () => {totalTuitionAmount} € + } + ]} + defaultTheme='bg-cyan-100' + /> + + ) : ( +

+ Attention! + Aucun frais de scolarité n'a été créé. +

+ )} + + + )} + {step === maxStep && (

Récapitulatif

@@ -399,7 +505,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r )}
- {step >= 1 && ( + {step > 1 && (
- - - - - - - - {students.map((student, index) => ( - handleEleveSelection(student)} - > - - - - ))} - -
NomPrénom
{student.last_name}{student.first_name}
-
- {selectedStudent && ( -
-

Responsables associés à {selectedStudent.last_name} {selectedStudent.first_name} :

- {existingGuardians.map((guardian) => ( -
- -
- ))} -
- )} -
- )} -
- )} - - {step === 3 && ( -
-

Téléphone (optionnel)

- -
- )} - - {step === 4 && ( -
-

Frais d'inscription

- {registrationFees.length > 0 ? ( - <> -
- -
-

Réductions

-
- {registrationDiscounts.length > 0 ? ( - - ) : ( -

- Information - Aucune réduction n'a été créée sur les frais d'inscription. -

- )} -
- MONTANT TOTAL - }, - { - name: 'TOTAL', - transform: () => {totalRegistrationAmount} € - } - ]} - defaultTheme='bg-cyan-100' - /> - - ) : ( -

- Attention! - Aucun frais d'inscription n'a été créé. -

- )} - - - )} - - {step === 5 && ( -
-

Frais de scolarité

- {tuitionFees.length > 0 ? ( - <> -
- -
-

Réductions

-
- {tuitionDiscounts.length > 0 ? ( - - ) : ( -

- Information - Aucune réduction n'a été créée sur les frais de scolarité. -

- )} -
-
MONTANT TOTAL - }, - { - name: 'TOTAL', - transform: () => {totalTuitionAmount} € - } - ]} - defaultTheme='bg-cyan-100' - /> - - ) : ( -

- Attention! - Aucun frais de scolarité n'a été créé. -

- )} - - - )} - - {step === maxStep && ( -
-

Récapitulatif

-
-
-

Élève

-
- - - - - - - - - - - - -
NomPrénom
{formData.studentLastName}{formData.studentFirstName}
- -
-

Responsable(s)

- {formData.responsableType === 'new' && ( - - - - - - - - - - - - - -
EmailTéléphone
{formData.guardianEmail}{formData.guardianPhone}
- )} - {formData.responsableType === 'existing' && selectedStudent && ( -
-

Associé(s) à : {selectedStudent.nom} {selectedStudent.prenom}

- - - - - - - - - - {existingGuardians.filter(guardian => formData.selectedGuardians.includes(guardian.id)).map((guardian) => ( - - - - - - ))} - -
NomPrénomEmail
{guardian.last_name}{guardian.first_name}{guardian.email}
-
- )} -
-
-
- -
-
- )} - -
- {step > 1 && ( -
+ )} + +
+ {step > 1 && ( +
+ ); } diff --git a/Front-End/src/components/Modal.js b/Front-End/src/components/Modal.js index 509aaa5..d84aaaf 100644 --- a/Front-End/src/components/Modal.js +++ b/Front-End/src/components/Modal.js @@ -1,13 +1,12 @@ import * as Dialog from '@radix-ui/react-dialog'; -import Button from '@/components/Button'; -const Modal = ({ isOpen, setIsOpen, title, ContentComponent, size }) => { +const Modal = ({ isOpen, setIsOpen, title, ContentComponent }) => { return ( -
+
{title} diff --git a/Front-End/src/components/Navigation.js b/Front-End/src/components/Navigation.js new file mode 100644 index 0000000..9ad5ba2 --- /dev/null +++ b/Front-End/src/components/Navigation.js @@ -0,0 +1,30 @@ +import React from 'react'; +import StepTitle from '@/components/StepTitle'; + +const Navigation = ({ steps, step, setStep, isStepValid, stepTitles }) => { + return ( +
+
+ {steps.map((stepLabel, index) => { + const isCurrentStep = step === index + 1; + + return ( +
+
+ {stepLabel} +
+
setStep(index + 1)} + style={{ transform: 'translateY(-50%)' }} + >
+
+ ); + })} +
+ +
+ ); +}; + +export default Navigation; \ No newline at end of file diff --git a/Front-End/src/components/StepTitle.js b/Front-End/src/components/StepTitle.js new file mode 100644 index 0000000..e27410d --- /dev/null +++ b/Front-End/src/components/StepTitle.js @@ -0,0 +1,16 @@ +import React from 'react'; + +const StepTitle = ({ title }) => { + return ( +
+
+
+
+
+
{title}
+
+
+ ); +}; + +export default StepTitle; \ No newline at end of file From cb3f909fa4e7a53148cd13cf190c13b0670d35de Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sun, 26 Jan 2025 15:43:11 +0100 Subject: [PATCH 014/249] =?UTF-8?q?refactor:=20Revue=20de=20la=20modale=20?= =?UTF-8?q?permettant=20de=20cr=C3=A9er=20un=20dossier=20d'inscription?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/School/serializers.py | 2 +- Back-End/School/views.py | 6 +- .../app/[locale]/admin/subscriptions/page.js | 3 +- Front-End/src/components/InputColorIcon.js | 28 - .../src/components/InputTextWithColorIcon.js | 34 + .../components/Inscription/InscriptionForm.js | 749 +++++++++--------- Front-End/src/components/Modal.js | 5 +- Front-End/src/components/Navigation.js | 30 + Front-End/src/components/StepTitle.js | 16 + .../Configuration/SpecialitiesSection.js | 302 +++++-- .../Structure/Configuration/SpecialityForm.js | 66 -- .../Configuration/StructureManagement.js | 5 +- .../Structure/Configuration/TeacherForm.js | 122 --- .../Configuration/TeachersSection.js | 487 ++++++++---- .../Tarification/DiscountsSection.js | 4 +- .../Structure/Tarification/FeesSection.js | 4 +- 16 files changed, 1049 insertions(+), 814 deletions(-) delete mode 100644 Front-End/src/components/InputColorIcon.js create mode 100644 Front-End/src/components/InputTextWithColorIcon.js create mode 100644 Front-End/src/components/Navigation.js create mode 100644 Front-End/src/components/StepTitle.js delete mode 100644 Front-End/src/components/Structure/Configuration/SpecialityForm.js delete mode 100644 Front-End/src/components/Structure/Configuration/TeacherForm.js diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index 7c63f88..39a69a6 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -76,7 +76,7 @@ class TeacherSerializer(serializers.ModelSerializer): return None def get_specialities_details(self, obj): - return [{'name': speciality.name, 'color_code': speciality.color_code} for speciality in obj.specialities.all()] + return [{'id': speciality.id, 'name': speciality.name, 'color_code': speciality.color_code} for speciality in obj.specialities.all()] class PlanningSerializer(serializers.ModelSerializer): class Meta: diff --git a/Back-End/School/views.py b/Back-End/School/views.py index 86bbc89..733d2f9 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -48,9 +48,9 @@ class SpecialityView(APIView): if speciality_serializer.is_valid(): speciality_serializer.save() - return JsonResponse(speciality_serializer.data, safe=False) + return JsonResponse(speciality_serializer.data, safe=False, status=201) - return JsonResponse(speciality_serializer.errors, safe=False) + return JsonResponse(speciality_serializer.errors, safe=False, status=400) def put(self, request, _id): speciality_data=JSONParser().parse(request) @@ -300,7 +300,7 @@ class FeeView(APIView): fee = Fee.objects.get(id=_id) except Fee.DoesNotExist: return JsonResponse({'error': 'No object found'}, status=404) - fee_serializer = FeeSerializer(fee, data=fee_data, partial=True) # Utilisation de partial=True + fee_serializer = FeeSerializer(fee, data=fee_data, partial=True) if fee_serializer.is_valid(): fee_serializer.save() return JsonResponse(fee_serializer.data, safe=False) diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index b07f616..6cb654f 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -805,8 +805,7 @@ const handleFileUpload = ({file, name, is_required, order}) => { ( { - return ( - <> -
- -
- - - - -
- {errorMsg &&

{errorMsg}

} -
- - ); -}; - -export default InputColorIcon; diff --git a/Front-End/src/components/InputTextWithColorIcon.js b/Front-End/src/components/InputTextWithColorIcon.js new file mode 100644 index 0000000..4953d7f --- /dev/null +++ b/Front-End/src/components/InputTextWithColorIcon.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { Palette } from 'lucide-react'; + +const InputTextWithColorIcon = ({ name, textValue, colorValue, onTextChange, onColorChange, placeholder, errorMsg }) => { + return ( +
+ +
+ + +
+
+ {errorMsg &&

{errorMsg}

} +
+ ); +}; + +export default InputTextWithColorIcon; \ No newline at end of file diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index adbe6d2..a33d607 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -5,9 +5,11 @@ import ToggleSwitch from '@/components/ToggleSwitch'; import Button from '@/components/Button'; import Table from '@/components/Table'; import FeesSection from '@/components/Structure/Tarification/FeesSection'; -import DiscountsSection from '../Structure/Tarification/DiscountsSection'; +import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection'; +import Navigation from '@/components/Navigation'; +import StepTitle from '@/components/StepTitle'; -const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, onSubmit }) => { +const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, onSubmit, currentStep }) => { const [formData, setFormData] = useState({ studentLastName: '', studentFirstName: '', @@ -22,12 +24,47 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r selectedTuitionFees: [] }); - const [step, setStep] = useState(1); + const [step, setStep] = useState(currentStep || 1); const [selectedStudent, setSelectedEleve] = useState(''); const [existingGuardians, setExistingGuardians] = useState([]); const [totalRegistrationAmount, setTotalRegistrationAmount] = useState(0); const [totalTuitionAmount, setTotalTuitionAmount] = useState(0); - const maxStep = 6 + + const stepTitles = { + 1: 'Nouvel élève', + 2: 'Nouveau Responsable', + 3: "Frais d'inscription", + 4: 'Frais de scolarité', + 5: 'Récapitulatif' + }; + + const steps = ['1', '2', '3', '4', 'Récap']; + + const isStep1Valid = formData.studentLastName && formData.studentFirstName; + const isStep2Valid = ( + (formData.responsableType === "new" && formData.guardianEmail.length > 0) || + (formData.responsableType === "existing" && formData.selectedGuardians.length > 0) + ); + const isStep3Valid = formData.selectedRegistrationFees.length > 0; + const isStep4Valid = formData.selectedTuitionFees.length > 0; + const isStep5Valid = isStep1Valid && isStep2Valid && isStep3Valid && isStep4Valid; + + const isStepValid = (stepNumber) => { + switch (stepNumber) { + case 1: + return isStep1Valid; + case 2: + return isStep2Valid; + case 3: + return isStep3Valid; + case 4: + return isStep4Valid; + case 5: + return isStep5Valid; + default: + return false; + } + }; useEffect(() => { // Calcul du montant total des frais d'inscription lors de l'initialisation @@ -39,6 +76,10 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r }, [registrationDiscounts, registrationFees]); + useEffect(() => { + setStep(currentStep || 1); + }, [currentStep]); + const handleToggleChange = () => { setFormData({ ...formData, autoMail: !formData.autoMail }); }; @@ -52,7 +93,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r }; const nextStep = () => { - if (step < maxStep) { + if (step < steps.length) { setStep(step + 1); } }; @@ -181,362 +222,358 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r return finalAmount.toFixed(2); }; - const isLabelAttenuated = (item) => { - return !formData.selectedRegistrationDiscounts.includes(parseInt(item.id)); - }; - - const isLabelFunction = (item) => { - return item.name + ' : ' + item.amount - }; - return ( -
- {step === 1 && ( -
-

Nouvel élève

- - -
- )} +
+ - {step === 2 && ( -
-

Responsable(s)

-
- - -
- {formData.responsableType === 'new' && ( - - )} + {step === 1 && ( +
+ + +
+ )} - {formData.responsableType === 'existing' && ( -
-
- - - - - - - - - {students.map((student, index) => ( - handleEleveSelection(student)} - > - - - - ))} - -
NomPrénom
{student.last_name}{student.first_name}
-
- {selectedStudent && ( -
-

Responsables associés à {selectedStudent.last_name} {selectedStudent.first_name} :

- {existingGuardians.map((guardian) => ( -
- -
- ))} -
- )} -
- )} -
- )} - - {step === 3 && ( -
-

Téléphone (optionnel)

- -
- )} - - {step === 4 && ( -
-

Frais d'inscription

- {registrationFees.length > 0 ? ( - <> -
- -
-

Réductions

-
- {registrationDiscounts.length > 0 ? ( - - ) : ( -

- Information - Aucune réduction n'a été créée sur les frais d'inscription. -

- )} -
- MONTANT TOTAL - }, - { - name: 'TOTAL', - transform: () => {totalRegistrationAmount} € - } - ]} - defaultTheme='bg-cyan-100' - /> - - ) : ( -

- Attention! - Aucun frais d'inscription n'a été créé. -

- )} - - - )} - - {step === 5 && ( -
-

Frais de scolarité

- {tuitionFees.length > 0 ? ( - <> -
- -
-

Réductions

-
- {tuitionDiscounts.length > 0 ? ( - - ) : ( -

- Information - Aucune réduction n'a été créée sur les frais de scolarité. -

- )} -
-
MONTANT TOTAL - }, - { - name: 'TOTAL', - transform: () => {totalTuitionAmount} € - } - ]} - defaultTheme='bg-cyan-100' - /> - - ) : ( -

- Attention! - Aucun frais de scolarité n'a été créé. -

- )} - - - )} - - {step === maxStep && ( -
-

Récapitulatif

-
-
-

Élève

-
- - - - - - - - - - - - -
NomPrénom
{formData.studentLastName}{formData.studentFirstName}
- -
-

Responsable(s)

- {formData.responsableType === 'new' && ( - - - - - - - - - - - - - -
EmailTéléphone
{formData.guardianEmail}{formData.guardianPhone}
- )} - {formData.responsableType === 'existing' && selectedStudent && ( -
-

Associé(s) à : {selectedStudent.nom} {selectedStudent.prenom}

- - - - - - - - - - {existingGuardians.filter(guardian => formData.selectedGuardians.includes(guardian.id)).map((guardian) => ( - - - - - - ))} - -
NomPrénomEmail
{guardian.last_name}{guardian.first_name}{guardian.email}
-
- )} -
-
-
- -
-
- )} - -
- {step > 1 && ( -
+ )} + +
+ {step > 1 && ( +
+
); } diff --git a/Front-End/src/components/Modal.js b/Front-End/src/components/Modal.js index 509aaa5..d84aaaf 100644 --- a/Front-End/src/components/Modal.js +++ b/Front-End/src/components/Modal.js @@ -1,13 +1,12 @@ import * as Dialog from '@radix-ui/react-dialog'; -import Button from '@/components/Button'; -const Modal = ({ isOpen, setIsOpen, title, ContentComponent, size }) => { +const Modal = ({ isOpen, setIsOpen, title, ContentComponent }) => { return ( -
+
{title} diff --git a/Front-End/src/components/Navigation.js b/Front-End/src/components/Navigation.js new file mode 100644 index 0000000..9ad5ba2 --- /dev/null +++ b/Front-End/src/components/Navigation.js @@ -0,0 +1,30 @@ +import React from 'react'; +import StepTitle from '@/components/StepTitle'; + +const Navigation = ({ steps, step, setStep, isStepValid, stepTitles }) => { + return ( +
+
+ {steps.map((stepLabel, index) => { + const isCurrentStep = step === index + 1; + + return ( +
+
+ {stepLabel} +
+
setStep(index + 1)} + style={{ transform: 'translateY(-50%)' }} + >
+
+ ); + })} +
+ +
+ ); +}; + +export default Navigation; \ No newline at end of file diff --git a/Front-End/src/components/StepTitle.js b/Front-End/src/components/StepTitle.js new file mode 100644 index 0000000..e27410d --- /dev/null +++ b/Front-End/src/components/StepTitle.js @@ -0,0 +1,16 @@ +import React from 'react'; + +const StepTitle = ({ title }) => { + return ( +
+
+
+
+
+
{title}
+
+
+ ); +}; + +export default StepTitle; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js index b4149bc..06cc45c 100644 --- a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js +++ b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js @@ -1,93 +1,231 @@ -import { Trash2, MoreVertical, Edit3, Plus } from 'lucide-react'; +import { Plus, Trash2, Edit3, Check, X, BookOpen } from 'lucide-react'; import { useState } from 'react'; import Table from '@/components/Table'; -import DropdownMenu from '@/components/DropdownMenu'; -import Modal from '@/components/Modal'; -import SpecialityForm from '@/components/Structure/Configuration/SpecialityForm'; -import { SpecialityFormProvider } from '@/context/SpecialityFormContext'; +import Popup from '@/components/Popup'; +import InputTextWithColorIcon from '@/components/InputTextWithColorIcon'; +import { DndProvider, useDrag } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; -const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDelete }) => { +const ItemTypes = { + SPECIALITY: 'speciality', +}; - const [isOpen, setIsOpen] = useState(false); - const [editingSpeciality, setEditingSpeciality] = useState(null); - - const openEditModal = (speciality) => { - setIsOpen(true); - setEditingSpeciality(speciality); - } - - const closeEditModal = () => { - setIsOpen(false); - setEditingSpeciality(null); - }; - - const handleModalSubmit = (updatedData) => { - if (editingSpeciality) { - handleEdit(editingSpeciality.id, updatedData); - } else { - handleCreate(updatedData); - } - closeEditModal(); - }; +const SpecialityItem = ({ speciality }) => { + const [{ isDragging }, drag] = useDrag(() => ({ + type: ItemTypes.SPECIALITY, + item: { id: speciality.id, name: speciality.name }, + collect: (monitor) => ({ + isDragging: !!monitor.isDragging(), + }), + })); return ( -
-
-

Gestion des spécialités

- -
-
- ( -
- {row.name.toUpperCase()} -
- ) - }, - { name: 'DATE DE CREATION', transform: (row) => row.updated_date_formatted }, - { name: 'ACTIONS', transform: (row) => ( - } - items={[ - { label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) }, - { label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) } - ] - } - buttonClassName="text-gray-400 hover:text-gray-600" - menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center" - /> - )} - ]} - data={specialities} - /> - - {isOpen && ( - - ( - - )} - /> - - )} +
+ {speciality.name}
); }; +const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, handleEdit, handleDelete }) => { + + const [newSpeciality, setNewSpeciality] = useState(null); + const [editingSpeciality, setEditingSpeciality] = useState(null); + const [formData, setFormData] = useState({}); + const [localErrors, setLocalErrors] = useState({}); + const [popupVisible, setPopupVisible] = useState(false); + const [popupMessage, setPopupMessage] = useState(""); + + const handleAddSpeciality = () => { + setNewSpeciality({ id: Date.now(), name: '', color_code: '' }); + }; + + const handleRemoveSpeciality = (id) => { + handleDelete(id) + .then(() => { + setSpecialities(prevSpecialities => prevSpecialities.filter(speciality => speciality.id !== id)); + }) + .catch(error => { + console.error(error); + }); + }; + + const handleSaveNewSpeciality = () => { + if ( + newSpeciality.name) { + handleCreate(newSpeciality) + .then((createdSpeciality) => { + setSpecialities([createdSpeciality, ...specialities]); + setNewSpeciality(null); + setLocalErrors({}); + }) + .catch(error => { + if (error && typeof error === 'object') { + setLocalErrors(error); + } else { + console.error(error); + } + }); + } else { + setPopupMessage("Tous les champs doivent être remplis et valides"); + setPopupVisible(true); + } + }; + + const handleUpdateSpeciality = (id, updatedSpeciality) => { + if ( + updatedSpeciality.name) { + handleEdit(id, updatedSpeciality) + .then((updatedSpeciality) => { + setSpecialities(specialities.map(speciality => speciality.id === id ? updatedSpeciality : speciality)); + setEditingSpeciality(null); + setLocalErrors({}); + }) + .catch(error => { + if (error && typeof error === 'object') { + setLocalErrors(error); + } else { + console.error(error); + } + }); + } else { + setPopupMessage("Tous les champs doivent être remplis et valides"); + setPopupVisible(true); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + let parsedValue = value; + if (name.includes('_color')) { + parsedValue = value; + } + + const fieldName = name.includes('_color') ? 'color_code' : name; + if (editingSpeciality) { + setFormData((prevData) => ({ + ...prevData, + [fieldName]: parsedValue, + })); + } else if (newSpeciality) { + setNewSpeciality((prevData) => ({ + ...prevData, + [fieldName]: parsedValue, + })); + } + }; + + const renderSpecialityCell = (speciality, column) => { + const isEditing = editingSpeciality === speciality.id; + const isCreating = newSpeciality && newSpeciality.id === speciality.id; + const currentData = isEditing ? formData : newSpeciality; + + if (isEditing || isCreating) { + switch (column) { + case 'LIBELLE': + return ( + + ); + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } else { + switch (column) { + case 'LIBELLE': + return ( + + ); + case 'MISE A JOUR': + return speciality.updated_date_formatted; + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } + }; + + const columns = [ + { name: 'LIBELLE', label: 'Libellé' }, + { name: 'MISE A JOUR', label: 'Date mise à jour' }, + { name: 'ACTIONS', label: 'Actions' } + ]; + + return ( + +
+
+
+ +

Spécialités

+
+ +
+
+ setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + + ); +}; + export default SpecialitiesSection; diff --git a/Front-End/src/components/Structure/Configuration/SpecialityForm.js b/Front-End/src/components/Structure/Configuration/SpecialityForm.js deleted file mode 100644 index 39293eb..0000000 --- a/Front-End/src/components/Structure/Configuration/SpecialityForm.js +++ /dev/null @@ -1,66 +0,0 @@ -import { useState } from 'react'; -import { BookOpen, Palette } from 'lucide-react'; -import InputTextIcon from '@/components/InputTextIcon'; -import InputColorIcon from '@/components/InputColorIcon'; -import Button from '@/components/Button'; -import { useSpecialityForm } from '@/context/SpecialityFormContext'; - -const SpecialityForm = ({ onSubmit, isNew }) => { - const { formData, setFormData } = useSpecialityForm(); - - const handleChange = (e) => { - const { name, value } = e.target; - - setFormData((prevState) => ({ - ...prevState, - [name]: value, - })); - }; - - const handleSubmit = (e) => { - e.preventDefault(); - onSubmit(formData); - }; - - return ( -
-
- -
-
- -
-
-
-
- ); -}; - -export default SpecialityForm; diff --git a/Front-End/src/components/Structure/Configuration/StructureManagement.js b/Front-End/src/components/Structure/Configuration/StructureManagement.js index 0e187ba..e5fda63 100644 --- a/Front-End/src/components/Structure/Configuration/StructureManagement.js +++ b/Front-End/src/components/Structure/Configuration/StructureManagement.js @@ -9,7 +9,7 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach return (
-
+
handleDelete(`${BE_SCHOOL_SPECIALITY_URL}`, id, setSpecialities)} />
-
+
handleCreate(`${BE_SCHOOL_TEACHER_URL}`, newData, setTeachers)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TEACHER_URL}`, id, updatedData, setTeachers)} diff --git a/Front-End/src/components/Structure/Configuration/TeacherForm.js b/Front-End/src/components/Structure/Configuration/TeacherForm.js deleted file mode 100644 index 72d1510..0000000 --- a/Front-End/src/components/Structure/Configuration/TeacherForm.js +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useState } from 'react'; -import { GraduationCap, Mail, BookOpen, Check } from 'lucide-react'; -import InputTextIcon from '@/components/InputTextIcon'; -import Button from '@/components/Button'; -import CheckBoxList from '@/components/CheckBoxList'; -import ToggleSwitch from '@/components/ToggleSwitch' -import { useTeacherForm } from '@/context/TeacherFormContext'; - -const TeacherForm = ({ onSubmit, isNew, specialities }) => { - const { formData, setFormData } = useTeacherForm(); - - const handleToggleChange = () => { - setFormData({ ...formData, droit: 1-formData.droit.id }); - }; - - const handleChange = (e) => { - const target = e.target || e.currentTarget; - const { name, value, type, checked } = target; - - if (type === 'checkbox') { - setFormData((prevState) => { - const newValues = checked - ? [...(prevState[name] || []), parseInt(value, 10)] - : (prevState[name] || []).filter((v) => v !== parseInt(value, 10)); - return { - ...prevState, - [name]: newValues, - }; - }); - } else { - setFormData((prevState) => ({ - ...prevState, - [name]: type === 'radio' ? parseInt(value, 10) : value, - })); - } - }; - - const handleSubmit = () => { - onSubmit(formData, isNew); - }; - - const getSpecialityLabel = (speciality) => { - return `${speciality.name}`; - }; - - const isLabelAttenuated = (item) => { - return !formData.specialities.includes(parseInt(item.id)); - }; - - return ( -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- - ); -}; - -export default TeacherForm; diff --git a/Front-End/src/components/Structure/Configuration/TeachersSection.js b/Front-End/src/components/Structure/Configuration/TeachersSection.js index 5c4d4cb..2fadfa7 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSection.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSection.js @@ -1,162 +1,359 @@ -import { Trash2, MoreVertical, Edit3, Plus } from 'lucide-react'; -import { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { Plus, Edit3, Trash2, GraduationCap, Check, X } from 'lucide-react'; import Table from '@/components/Table'; -import DropdownMenu from '@/components/DropdownMenu'; -import Modal from '@/components/Modal'; -import TeacherForm from '@/components/Structure/Configuration/TeacherForm'; -import useCsrfToken from '@/hooks/useCsrfToken'; -import { TeacherFormProvider } from '@/context/TeacherFormContext'; +import Popup from '@/components/Popup'; +import InputTextIcon from '@/components/InputTextIcon'; +import ToggleSwitch from '@/components/ToggleSwitch'; import { createProfile, updateProfile } from '@/app/lib/authAction'; +import useCsrfToken from '@/hooks/useCsrfToken'; +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; -const TeachersSection = ({ teachers, specialities , handleCreate, handleEdit, handleDelete}) => { +const ItemTypes = { + SPECIALITY: 'speciality', +}; - const [isOpen, setIsOpen] = useState(false); - const [editingTeacher, setEditingTeacher] = useState(null); +const SpecialitiesDropZone = ({ teacher, handleSpecialitiesChange, specialities, isEditing }) => { + const [localSpecialities, setLocalSpecialities] = useState(teacher.specialities_details || []); - const csrfToken = useCsrfToken(); + useEffect(() => { + setLocalSpecialities(teacher.specialities_details || []); + }, [teacher.specialities_details]); - const openEditModal = (teacher) => { - setIsOpen(true); - setEditingTeacher(teacher); - } + useEffect(() => { + handleSpecialitiesChange(localSpecialities.map(speciality => speciality.id)); + }, [localSpecialities]); - const closeEditModal = () => { - setIsOpen(false); - setEditingTeacher(null); - }; - - const handleModalSubmit = (updatedData) => { - if (editingTeacher) { - // Modification du profil - const data = { - email: updatedData.email, - username: updatedData.email, - droit:updatedData.droit + const [{ isOver }, drop] = useDrop(() => ({ + accept: ItemTypes.SPECIALITY, + drop: (item) => { + const specialityDetails = specialities.find(speciality => speciality.id === item.id); + if (!localSpecialities.some(speciality => speciality.id === item.id)) { + setLocalSpecialities(prevSpecialities => [ + ...prevSpecialities, + { id: item.id, name: specialityDetails.name, color_code: specialityDetails.color_code } + ]); } + }, + collect: (monitor) => ({ + isOver: !!monitor.isOver(), + }), + })); - updateProfile(updatedData.associated_profile,data,csrfToken) - .then(response => { - console.log('Success:', response); - console.log('UpdateData:', updatedData); - handleEdit(editingTeacher.id, updatedData); - }) - .catch(error => { - console.error('Error fetching data:', error); - error = error.errorMessage; - console.log(error); - }); - } else { - // Création d'un profil associé à l'adresse mail du responsable saisie - // Le profil est inactif - const data = { - email: updatedData.email, - password: 'Provisoire01!', - username: updatedData.email, - is_active: 1, // On rend le profil actif : on considère qu'au moment de la configuration de l'école un abonnement a été souscrit - droit:updatedData.droit - } - createProfile(data,csrfToken) - .then(response => { - console.log('Success:', response); - console.log('UpdateData:', updatedData); - if (response.id) { - let idProfil = response.id; - updatedData.associated_profile = idProfil; - handleCreate(updatedData); - } - }) - .catch(error => { - console.error('Error fetching data:', error); - error = error.errorMessage; - console.log(error); - }); - } - closeEditModal(); + const handleRemoveSpeciality = (id) => { + setLocalSpecialities(prevSpecialities => { + const updatedSpecialities = prevSpecialities.filter(speciality => speciality.id !== id); + return updatedSpecialities; + }); }; return ( -
-
-

Gestion des enseignants

- -
-
-
row.last_name }, - { name: 'PRENOM', transform: (row) => row.first_name }, - { name: 'MAIL', transform: (row) => row.email }, - { - name: 'SPÉCIALITÉS', - transform: (row) => ( -
- {row.specialities_details.map((speciality,index) => ( - - {speciality.name} - - ))} -
- ) - }, - { - name: 'TYPE PROFIL', - transform: (row) => { - if (row.associated_profile) { - const badgeClass = row.droit.label === 'ECOLE' ? 'bg-blue-100 text-blue-600' : 'bg-red-100 text-red-600'; - return ( -
- - {row.droit.label} - -
- ); - } else { - return Non définie; - } - } - }, - { name: 'DATE DE CREATION', transform: (row) => row.updated_date_formatted }, - { name: 'ACTIONS', transform: (row) => ( - } - items={[ - { label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) }, - { label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) } - ] - } - buttonClassName="text-gray-400 hover:text-gray-600" - menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center" - /> - )} - ]} - data={teachers} - /> - - {isOpen && ( - - ( - - )} - /> - - )} +
+ {localSpecialities.map((speciality, index) => ( +
+ + {speciality.name} + + {isEditing && ( + + )} +
+ ))}
); }; +const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, handleEdit, handleDelete }) => { + const csrfToken = useCsrfToken(); + const [editingTeacher, setEditingTeacher] = useState(null); + const [newTeacher, setNewTeacher] = useState(null); + const [formData, setFormData] = useState({}); + const [localErrors, setLocalErrors] = useState({}); + const [popupVisible, setPopupVisible] = useState(false); + const [popupMessage, setPopupMessage] = useState(""); + + const handleAddTeacher = () => { + setNewTeacher({ id: Date.now(), last_name: '', first_name: '', email: '', specialities: [], droit: 0 }); + setFormData({ last_name: '', first_name: '', email: '', specialities: [], droit: 0 }); + }; + + const handleRemoveTeacher = (id) => { + handleDelete(id) + .then(() => { + setTeachers(prevTeachers => prevTeachers.filter(teacher => teacher.id !== id)); + }) + .catch(error => { + console.error(error); + }); + }; + + const handleSaveNewTeacher = () => { + if (formData.last_name && formData.first_name && formData.email) { + const data = { + email: formData.email, + password: 'Provisoire01!', + username: formData.email, + is_active: 1, + droit: formData.droit, + }; + createProfile(data, csrfToken) + .then(response => { + console.log('Success:', response); + if (response.id) { + let idProfil = response.id; + newTeacher.associated_profile = idProfil; + handleCreate(newTeacher) + .then((createdTeacher) => { + setTeachers([createdTeacher, ...teachers]); + setNewTeacher(null); + setLocalErrors({}); + }); + } + }) + .catch(error => { + if (error && typeof error === 'object') { + setLocalErrors(error); + } else { + console.error(error); + } + }); + } else { + setPopupMessage("Tous les champs doivent être remplis et valides"); + setPopupVisible(true); + } + }; + + const handleUpdateTeacher = (id, updatedData) => { + console.log('UpdatedData:', updatedData); + const data = { + email: updatedData.email, + username: updatedData.email, + droit: updatedData.droit.id, + }; + updateProfile(updatedData.associated_profile, data, csrfToken) + .then(response => { + console.log('Success:', response); + handleEdit(id, updatedData) + .then((updatedTeacher) => { + setTeachers(prevTeachers => prevTeachers.map(teacher => teacher.id === id ? { ...teacher, ...updatedTeacher } : teacher)); + setEditingTeacher(null); + setFormData({}); + }) + }) + .catch(error => { + console.error(error); + }); + }; + + const handleChange = (e) => { + const { name, value } = e.target; + let parsedValue = value; + + if (editingTeacher) { + setFormData((prevData) => ({ + ...prevData, + [name]: parsedValue, + })); + } else if (newTeacher) { + setNewTeacher((prevData) => ({ + ...prevData, + [name]: parsedValue, + })); + setFormData((prevData) => ({ + ...prevData, + [name]: parsedValue, + })); + } + }; + + const handleSpecialitiesChange = (selectedSpecialities) => { + if (editingTeacher) { + setFormData((prevData) => ({ + ...prevData, + specialities: selectedSpecialities, + })); + } else if (newTeacher) { + setNewTeacher((prevData) => ({ + ...prevData, + specialities: selectedSpecialities, + })); + setFormData((prevData) => ({ + ...prevData, + specialities: selectedSpecialities, + })); + } + }; + + const renderTeacherCell = (teacher, column) => { + const isEditing = editingTeacher === teacher.id; + const isCreating = newTeacher && newTeacher.id === teacher.id; + const currentData = isEditing ? formData : newTeacher; + + if (isEditing || isCreating) { + switch (column) { + case 'NOM': + return ( + + ); + case 'PRENOM': + return ( + + ); + case 'EMAIL': + return ( + + ); + case 'SPECIALITES': + return ( + + ); + // case 'PROFIL': + // return ( + // + // ); + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } else { + switch (column) { + case 'NOM': + return teacher.last_name; + case 'PRENOM': + return teacher.first_name; + case 'EMAIL': + return teacher.email; + case 'SPECIALITES': + return ( + + ); + case 'PROFIL': + if (teacher.associated_profile) { + const badgeClass = teacher.droit.label === 'ECOLE' ? 'bg-blue-100 text-blue-600' : 'bg-red-100 text-red-600'; + return ( +
+ + {teacher.droit.label} + +
+ ); + } else { + return Non définie; + }; + case 'MISE A JOUR': + return teacher.updated_date_formatted; + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } + }; + + const columns = [ + { name: 'NOM', label: 'Nom' }, + { name: 'PRENOM', label: 'Prénom' }, + { name: 'EMAIL', label: 'Email' }, + { name: 'SPECIALITES', label: 'Spécialités' }, + { name: 'PROFIL', label: 'Profil' }, + { name: 'MISE A JOUR', label: 'Mise à jour' }, + { name: 'ACTIONS', label: 'Actions' } + ]; + + return ( + +
+
+
+ +

Enseignants

+
+ +
+
+ setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + + ); +}; + export default TeachersSection; diff --git a/Front-End/src/components/Structure/Tarification/DiscountsSection.js b/Front-End/src/components/Structure/Tarification/DiscountsSection.js index 6ebab1c..47a0a7c 100644 --- a/Front-End/src/components/Structure/Tarification/DiscountsSection.js +++ b/Front-End/src/components/Structure/Tarification/DiscountsSection.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react'; +import { Plus, Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; @@ -181,7 +181,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h onClick={() => handleRemoveDiscount(discount.id)} className="text-red-500 hover:text-red-700" > - + ); diff --git a/Front-End/src/components/Structure/Tarification/FeesSection.js b/Front-End/src/components/Structure/Tarification/FeesSection.js index 25669ab..2bec1d0 100644 --- a/Front-End/src/components/Structure/Tarification/FeesSection.js +++ b/Front-End/src/components/Structure/Tarification/FeesSection.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, EyeOff, Eye, CreditCard, BookOpen } from 'lucide-react'; +import { Plus, Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard, BookOpen } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; @@ -190,7 +190,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl onClick={() => handleRemoveFee(fee.id)} className="text-red-500 hover:text-red-700" > - + ); From a248898203286213c3447333611e1a9981dff64a Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Mon, 27 Jan 2025 11:20:44 +0100 Subject: [PATCH 015/249] refactor: Changement des IconTextInput en TextInput, modification du composant step --- Back-End/Subscriptions/views.py | 4 +- .../[locale]/parents/editInscription/page.js | 19 +-- Front-End/src/app/[locale]/parents/layout.js | 12 +- Front-End/src/components/ClasseDetails.js | 4 +- .../components/Inscription/InscriptionForm.js | 70 ++++---- .../Inscription/InscriptionFormShared.js | 13 +- Front-End/src/components/Modal.js | 8 +- Front-End/src/components/Navigation.js | 30 ---- Front-End/src/components/ProgressStep.js | 157 ++++++++++++++++++ Front-End/src/components/ProtectedRoute.js | 3 + .../{StepTitle.js => SectionTitle.js} | 4 +- .../Configuration/TeachersSection.js | 9 +- .../Structure/Planning/ScheduleManagement.js | 4 +- .../Tarification/DiscountsSection.js | 4 +- .../Structure/Tarification/FeesSection.js | 4 +- Front-End/src/context/ClassesContext.js | 52 +++--- 16 files changed, 270 insertions(+), 127 deletions(-) delete mode 100644 Front-End/src/components/Navigation.js create mode 100644 Front-End/src/components/ProgressStep.js rename Front-End/src/components/{StepTitle.js => SectionTitle.js} (84%) diff --git a/Back-End/Subscriptions/views.py b/Back-End/Subscriptions/views.py index d635bf7..4e0ae69 100644 --- a/Back-End/Subscriptions/views.py +++ b/Back-End/Subscriptions/views.py @@ -319,8 +319,8 @@ class ChildrenListView(APIView): """ # Récupération des élèves d'un parent # idProfile : identifiant du profil connecté rattaché aux fiches d'élèves - def get(self, request, _idProfile): - students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__associated_profile__id', _value=_idProfile) + def get(self, request, _id): + students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__associated_profile__id', _value=_id) students_serializer = RegistrationFormByParentSerializer(students, many=True) return JsonResponse(students_serializer.data, safe=False) diff --git a/Front-End/src/app/[locale]/parents/editInscription/page.js b/Front-End/src/app/[locale]/parents/editInscription/page.js index 12df87d..9d44d3f 100644 --- a/Front-End/src/app/[locale]/parents/editInscription/page.js +++ b/Front-End/src/app/[locale]/parents/editInscription/page.js @@ -1,31 +1,19 @@ 'use client' -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared'; -import { useSearchParams, redirect, useRouter } from 'next/navigation'; +import { useSearchParams, useRouter } from 'next/navigation'; import useCsrfToken from '@/hooks/useCsrfToken'; import { FE_PARENTS_HOME_URL} from '@/utils/Url'; -import { mockStudent } from '@/data/mockStudent'; -import { fetchLastGuardian, fetchRegisterForm } from '@/app/lib/subscriptionAction'; - -const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; +import { editRegisterForm} from '@/app/lib/subscriptionAction'; export default function Page() { const searchParams = useSearchParams(); const idProfil = searchParams.get('id'); const studentId = searchParams.get('studentId'); const router = useRouter(); - - const [initialData, setInitialData] = useState(null); const csrfToken = useCsrfToken(); - const [currentProfil, setCurrentProfil] = useState(""); - - const handleSubmit = async (data) => { - if (useFakeData) { - console.log('Fake submit:', data); - return; - } try { const result = await editRegisterForm(studentId, data, csrfToken); console.log('Success:', result); @@ -41,7 +29,6 @@ export default function Page() { csrfToken={csrfToken} onSubmit={handleSubmit} cancelUrl={FE_PARENTS_HOME_URL} - isLoading={isLoading} /> ); } \ No newline at end of file diff --git a/Front-End/src/app/[locale]/parents/layout.js b/Front-End/src/app/[locale]/parents/layout.js index 4adf56d..b7f2f1a 100644 --- a/Front-End/src/app/[locale]/parents/layout.js +++ b/Front-End/src/app/[locale]/parents/layout.js @@ -17,9 +17,10 @@ export default function Layout({ const router = useRouter(); // Définition de router const [messages, setMessages] = useState([]); const [userId, setUserId] = useLocalStorage("userId", '') ; - + const [isLoading, setIsLoading] = useState(true); useEffect(() => { + setIsLoading(true); setUserId(userId) fetchMessages(userId) .then(data => { @@ -30,8 +31,15 @@ export default function Layout({ }) .catch(error => { console.error('Error fetching data:', error); + }) + .finally(() => { + setIsLoading(false); }); - }, []); + }, [userId]); + + if (isLoading) { + return
Loading...
; + } return ( diff --git a/Front-End/src/components/ClasseDetails.js b/Front-End/src/components/ClasseDetails.js index cc13f29..37d0689 100644 --- a/Front-End/src/components/ClasseDetails.js +++ b/Front-End/src/components/ClasseDetails.js @@ -5,7 +5,7 @@ import { GraduationCap } from 'lucide-react'; const ClasseDetails = ({ classe }) => { if (!classe) return null; - const nombreElevesInscrits = classe.eleves.length; + const nombreElevesInscrits = classe?.eleves?.length||0; const capaciteTotale = classe.number_of_students; const pourcentage = Math.round((nombreElevesInscrits / capaciteTotale) * 100); @@ -34,7 +34,7 @@ const ClasseDetails = ({ classe }) => { - + {/* Section Capacité de la Classe */}
diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index a33d607..0f7bab6 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -6,8 +6,8 @@ import Button from '@/components/Button'; import Table from '@/components/Table'; import FeesSection from '@/components/Structure/Tarification/FeesSection'; import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection'; -import Navigation from '@/components/Navigation'; -import StepTitle from '@/components/StepTitle'; +import SectionTitle from '@/components/SectionTitle'; +import ProgressStep from '@/components/ProgressStep'; const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, onSubmit, currentStep }) => { const [formData, setFormData] = useState({ @@ -37,8 +37,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r 4: 'Frais de scolarité', 5: 'Récapitulatif' }; - - const steps = ['1', '2', '3', '4', 'Récap']; + + const steps = ['Élève', 'Responsable', 'Inscription', 'Scolarité', 'Récap']; const isStep1Valid = formData.studentLastName && formData.studentFirstName; const isStep2Valid = ( @@ -136,7 +136,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r return { ...prevData, selectedRegistrationFees }; }); }; - + const handleTuitionFeeSelection = (feeId) => { setFormData((prevData) => { const selectedTuitionFees = prevData.selectedTuitionFees.includes(feeId) @@ -147,7 +147,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r return { ...prevData, selectedTuitionFees }; }); }; - + const handleRegistrationDiscountSelection = (discountId) => { setFormData((prevData) => { const selectedRegistrationDiscounts = prevData.selectedRegistrationDiscounts.includes(discountId) @@ -169,7 +169,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r return { ...prevData, selectedTuitionDiscounts }; }); }; - + const calculateFinalRegistrationAmount = (selectedRegistrationFees, selectedRegistrationDiscounts) => { const totalFees = selectedRegistrationFees.reduce((sum, feeId) => { const fee = registrationFees.find(f => f.id === feeId); @@ -178,7 +178,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r } return sum; }, 0); - + const totalDiscounts = selectedRegistrationDiscounts.reduce((sum, discountId) => { const discount = registrationDiscounts.find(d => d.id === discountId); if (discount) { @@ -190,9 +190,9 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r } return sum; }, 0); - + const finalAmount = totalFees - totalDiscounts; - + return finalAmount.toFixed(2); }; @@ -204,7 +204,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r } return sum; }, 0); - + const totalDiscounts = selectedTuitionDiscounts.reduce((sum, discountId) => { const discount = tuitionDiscounts.find(d => d.id === discountId); if (discount) { @@ -216,20 +216,20 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r } return sum; }, 0); - + const finalAmount = totalFees - totalDiscounts; - + return finalAmount.toFixed(2); }; return (
- {step === 1 && ( @@ -257,15 +257,6 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r {step === 2 && (
-
{formData.responsableType === 'new' && ( + <> + + )} {formData.responsableType === 'existing' && ( @@ -364,7 +366,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r handleFeeSelection={handleRegistrationFeeSelection} />
- +
{registrationDiscounts.length > 0 ? ( )}
- +
MONTANT TOTAL }, { - name: 'TOTAL', + name: 'TOTAL', transform: () => {totalRegistrationAmount} € } ]} @@ -403,7 +405,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r Aucun frais d'inscription n'a été créé.

)} - + )} {step === 4 && ( @@ -419,7 +421,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r handleFeeSelection={handleTuitionFeeSelection} /> - +
{tuitionDiscounts.length > 0 ? ( )}
- +
MONTANT TOTAL }, { - name: 'TOTAL', + name: 'TOTAL', transform: () => {totalTuitionAmount} € } ]} diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index 34aa4c3..fad9a90 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -39,7 +39,18 @@ export default function InscriptionFormShared({ }) { // États pour gérer les données du formulaire const [isLoading, setIsLoading] = useState(true); - const [formData, setFormData] = useState({}); + const [formData, setFormData] = useState({ + id: '', + last_name: '', + first_name: '', + address: '', + birth_date: '', + birth_place: '', + birth_postal_code: '', + nationality: '', + attending_physician: '', + level: '' + }); const [guardians, setGuardians] = useState([]); diff --git a/Front-End/src/components/Modal.js b/Front-End/src/components/Modal.js index d84aaaf..6ae3cfa 100644 --- a/Front-End/src/components/Modal.js +++ b/Front-End/src/components/Modal.js @@ -5,9 +5,9 @@ const Modal = ({ isOpen, setIsOpen, title, ContentComponent }) => { - -
-
+ +
+
{title} @@ -23,7 +23,7 @@ const Modal = ({ isOpen, setIsOpen, title, ContentComponent }) => {
-
+
diff --git a/Front-End/src/components/Navigation.js b/Front-End/src/components/Navigation.js deleted file mode 100644 index 9ad5ba2..0000000 --- a/Front-End/src/components/Navigation.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import StepTitle from '@/components/StepTitle'; - -const Navigation = ({ steps, step, setStep, isStepValid, stepTitles }) => { - return ( -
-
- {steps.map((stepLabel, index) => { - const isCurrentStep = step === index + 1; - - return ( -
-
- {stepLabel} -
-
setStep(index + 1)} - style={{ transform: 'translateY(-50%)' }} - >
-
- ); - })} -
- -
- ); -}; - -export default Navigation; \ No newline at end of file diff --git a/Front-End/src/components/ProgressStep.js b/Front-End/src/components/ProgressStep.js new file mode 100644 index 0000000..312d133 --- /dev/null +++ b/Front-End/src/components/ProgressStep.js @@ -0,0 +1,157 @@ +import React, { useState, useEffect } from 'react'; + +const Step = ({ number, title, isActive, isValid, isCompleted, onClick }) => { + return ( +
+
+ {isCompleted ? ( + + + + ) : ( + number + )} +
+
+ + {title} + +
+
+ ); +}; + +const SpacerStep = ({ isCompleted }) => { + return ( +
+ ); +}; + +const Dots = () => { + return ( +
+ ... +
+ ... +
+
+ ); +}; + +const ProgressStep = ({ steps, stepTitles, currentStep, setStep, isStepValid }) => { + const [windowWidth, setWindowWidth] = useState(window.innerWidth); + const [visibleSteps, setVisibleSteps] = useState(steps); + + useEffect(() => { + const handleResize = () => setWindowWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + useEffect(() => { + const calculateVisibleSteps = () => { + const minWidth = 150; // Largeur minimale estimée par étape + const maxVisibleSteps = Math.floor(windowWidth / minWidth); + + if (maxVisibleSteps >= steps.length) { + setVisibleSteps(steps); + return; + } + + if (maxVisibleSteps < 4) { + // Garder seulement première, dernière et courante + let filtered = [steps[0]]; + if (currentStep > 1 && currentStep < steps.length) { + filtered.push('...'); + filtered.push(steps[currentStep - 1]); + } + if (currentStep < steps.length) { + filtered.push('...'); + } + filtered.push(steps[steps.length - 1]); + setVisibleSteps(filtered); + } else { + // Garder première, dernière, courante et quelques étapes adjacentes + let filtered = [steps[0]]; + if (currentStep > 2) filtered.push('...'); + if (currentStep > 1 && currentStep < steps.length) { + filtered.push(steps[currentStep - 1]); + } + if (currentStep < steps.length - 1) filtered.push('...'); + filtered.push(steps[steps.length - 1]); + setVisibleSteps(filtered); + } + }; + + calculateVisibleSteps(); + }, [windowWidth, currentStep, steps]); + + const handleStepClick = (stepIndex) => { + // Vérifie si on peut naviguer vers l'étape (toutes les étapes précédentes doivent être valides) + const canNavigate = Array.from({ length: stepIndex }, (_, i) => i + 1) + .every(step => isStepValid(step)); + + if (canNavigate) { + setStep(stepIndex + 1); + } + }; + + return ( +
+
+ {visibleSteps.map((step, index) => { + if (step === '...') { + return ( +
+ + {index !== visibleSteps.length - 1 && } +
+ ); + } + + const originalIndex = steps.indexOf(step); + return ( +
i + 1).every(s => isStepValid(s)) ? 'cursor-pointer' : 'cursor-not-allowed'} + `} + onClick={() => handleStepClick(originalIndex)} + > +
+
+ originalIndex + 1} + isValid={isStepValid(originalIndex + 1)} + /> + {index !== visibleSteps.length - 1 && ( + originalIndex + 1} /> + )} +
+
+
+ ); + })} +
+
+ ); +}; + +export default ProgressStep; diff --git a/Front-End/src/components/ProtectedRoute.js b/Front-End/src/components/ProtectedRoute.js index 61c7671..43f02c5 100644 --- a/Front-End/src/components/ProtectedRoute.js +++ b/Front-End/src/components/ProtectedRoute.js @@ -14,6 +14,9 @@ const ProtectedRoute = ({ children }) => { } }, [userId, router]); + if (!userId) { + return
Loading...
; + } // Afficher les enfants seulement si l'utilisateur est connecté return userId ? children : null; }; diff --git a/Front-End/src/components/StepTitle.js b/Front-End/src/components/SectionTitle.js similarity index 84% rename from Front-End/src/components/StepTitle.js rename to Front-End/src/components/SectionTitle.js index e27410d..6f58e86 100644 --- a/Front-End/src/components/StepTitle.js +++ b/Front-End/src/components/SectionTitle.js @@ -1,6 +1,6 @@ import React from 'react'; -const StepTitle = ({ title }) => { +const SectionTitle = ({ title }) => { return (
@@ -13,4 +13,4 @@ const StepTitle = ({ title }) => { ); }; -export default StepTitle; \ No newline at end of file +export default SectionTitle; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/TeachersSection.js b/Front-End/src/components/Structure/Configuration/TeachersSection.js index 2fadfa7..0eb1e57 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSection.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSection.js @@ -8,6 +8,7 @@ import { createProfile, updateProfile } from '@/app/lib/authAction'; import useCsrfToken from '@/hooks/useCsrfToken'; import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; +import InputText from '@/components/InputText'; const ItemTypes = { SPECIALITY: 'speciality', @@ -158,7 +159,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha const handleChange = (e) => { const { name, value } = e.target; let parsedValue = value; - + if (editingTeacher) { setFormData((prevData) => ({ ...prevData, @@ -203,7 +204,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha switch (column) { case 'NOM': return ( - { setSelectedLevel(niveau); - const currentPlanning = selectedClass.plannings_read.find(planning => planning.niveau === niveau); + const currentPlanning = selectedClass.plannings_read?.find(planning => planning.niveau === niveau); setSchedule(currentPlanning ? currentPlanning.planning : {}); } }, [selectedClass, niveauxLabels]); useEffect(() => { if (selectedClass && selectedLevel) { - const currentPlanning = selectedClass.plannings_read.find(planning => planning.niveau === selectedLevel); + const currentPlanning = selectedClass.plannings_read?.find(planning => planning.niveau === selectedLevel); setSchedule(currentPlanning ? currentPlanning.planning : {}); } }, [selectedClass, selectedLevel]); diff --git a/Front-End/src/components/Structure/Tarification/DiscountsSection.js b/Front-End/src/components/Structure/Tarification/DiscountsSection.js index 47a0a7c..9d8f04d 100644 --- a/Front-End/src/components/Structure/Tarification/DiscountsSection.js +++ b/Front-End/src/components/Structure/Tarification/DiscountsSection.js @@ -1,9 +1,9 @@ import React, { useState } from 'react'; import { Plus, Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react'; import Table from '@/components/Table'; -import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; import CheckBox from '@/components/CheckBox'; +import InputText from '@/components/InputText'; const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedDiscounts, handleDiscountSelection }) => { const [editingDiscount, setEditingDiscount] = useState(null); @@ -103,7 +103,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h const renderInputField = (field, value, onChange, placeholder) => (
- { const [editingFee, setEditingFee] = useState(null); @@ -112,7 +112,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl const renderInputField = (field, value, onChange, placeholder) => (
- { { id: 2, label: 'Semestriel' }, { id: 3, label: 'Trimestriel' }, ]; - + const selectedDays = { 1: 'lundi', 2: 'mardi', @@ -60,14 +60,14 @@ export const ClassesProvider = ({ children }) => { const getNiveauxTabs = (levels) => { // Trier les levels par id const sortedNiveaux = levels.sort((a, b) => a - b); - + // Mapper les labels correspondants return sortedNiveaux.map(niveauId => { const niveau = allNiveaux.find(n => n.id === niveauId); return niveau ? { id: niveau.id, title: niveau.name, icon: School } : { id: 'unknown', title: 'Niveau inconnu', icon: null }; }); }; - + const generateAgeToNiveaux = (minAge, maxAge) => { if (minAge === null || isNaN(minAge)) { @@ -112,7 +112,7 @@ export const ClassesProvider = ({ children }) => { const updatePlannings = (formData, existingPlannings) => { return formData.levels.map(niveau => { let existingPlanning = existingPlannings.find(planning => planning.niveau === niveau); - + const emploiDuTemps = formData.opening_days.reduce((acc, dayId) => { const dayName = selectedDays[dayId]; if (dayName) { @@ -167,7 +167,7 @@ export const ClassesProvider = ({ children }) => { } // Fusionner les plannings existants avec les nouvelles données - return existingPlanning + return existingPlanning ? { ...existingPlanning, ...updatedPlanning } : updatedPlanning; }); @@ -176,17 +176,21 @@ export const ClassesProvider = ({ children }) => { const groupSpecialitiesBySubject = (teachers) => { const groupedSpecialities = {}; + if (!teachers) return []; + teachers.forEach(teacher => { - teacher.specialites.forEach(specialite => { - if (!groupedSpecialities[specialite.id]) { - groupedSpecialities[specialite.id] = { - ...specialite, - teachers: [`${teacher.nom} ${teacher.prenom}`], - }; - } else { - groupedSpecialities[specialite.id].teachers.push(`${teacher.nom} ${teacher.prenom}`); - } - }); + if (teacher && teacher.specialites) { + teacher.specialites.forEach(specialite => { + if (!groupedSpecialities[specialite.id]) { + groupedSpecialities[specialite.id] = { + ...specialite, + teachers: [`${teacher.nom} ${teacher.prenom}`], + }; + } else { + groupedSpecialities[specialite.id].teachers.push(`${teacher.nom} ${teacher.prenom}`); + } + }); + } }); return Object.values(groupedSpecialities); @@ -202,15 +206,15 @@ export const ClassesProvider = ({ children }) => { }; return ( - Date: Fri, 31 Jan 2025 15:41:23 +0100 Subject: [PATCH 016/249] refactor: SpecialitySection + TeacherSection (en cours) --- Front-End/src/components/InputText.js | 2 +- .../Structure/Configuration/ClassesSection.js | 2 +- .../Configuration/SpecialitiesSection.js | 28 +--------- .../Structure/Configuration/SpecialityItem.js | 31 +++++++++++ .../Configuration/StructureManagement.js | 8 +-- .../Configuration/TeachersSection.js | 54 ++++++++++++------- 6 files changed, 75 insertions(+), 50 deletions(-) create mode 100644 Front-End/src/components/Structure/Configuration/SpecialityItem.js diff --git a/Front-End/src/components/InputText.js b/Front-End/src/components/InputText.js index c203ccb..f76f041 100644 --- a/Front-End/src/components/InputText.js +++ b/Front-End/src/components/InputText.js @@ -1,7 +1,7 @@ export default function InputText({name, type, label, value, onChange, errorMsg, placeholder, className, required}) { return ( <> -
+
{ - const ambiance = row.atmosphere_name ? row.atmosphere_name : ''; - const trancheAge = row.age_range ? `${row.age_range} ans` : ''; - - if (ambiance && trancheAge) { - return `${ambiance} (${trancheAge})`; - } else if (ambiance) { - return ambiance; - } else if (trancheAge) { - return trancheAge; - } else { - return 'Non spécifié'; - } - } - }, - { - name: 'NIVEAUX', - transform: (row) => { - const levelLabels = Array.isArray(row.levels) ? getNiveauxLabels(row.levels) : []; - return ( -
- {levelLabels.length > 0 - ? levelLabels.map((label, index) => ( - - )) - : 'Aucun niveau'} -
- ); - } - }, - { name: 'CAPACITÉ MAX', transform: (row) => row.number_of_students }, - { name: 'ANNÉE SCOLAIRE', transform: (row) => row.school_year }, - { - name: 'ENSEIGNANTS', - transform: (row) => ( -
- {row.teachers_details.map((teacher, index) => ( - - ))} -
- ) - }, - { name: 'DATE DE CREATION', transform: (row) => row.updated_date_formatted }, - { - name: 'ACTIONS', transform: (row) => ( - } - items={[ - { label: 'Inspecter', icon: ZoomIn, onClick: () => openEditModalDetails(row) }, - { label: 'Modifier', icon: Edit3, onClick: () => openEditModal(row) }, - { label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) } - ] - } - buttonClassName="text-gray-400 hover:text-gray-600" - menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center" - /> - ) - } - ]} - data={classes} - /> - - {isOpen && ( - - ( - - )} - /> - - )} - - {isOpenDetails && ( - - - {editingClass ? ( - <> - {editingClass.atmosphere_name} - {editingClass.age_range[0]} à {editingClass.age_range[1]} ans - - ) : ''} - - )} - ContentComponent={() => ( - - )} - /> +
+ {isEditing && ( +
+ {/* Ajoutez l'icône Hand */} + Déposez un enseignant ici +
)} + {localTeachers.map((teacher, index) => ( +
+ + {isEditing && ( + + )} +
+ ))}
); }; -export default ClassesSection; +const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdit, handleDelete }) => { + const [formData, setFormData] = useState({}); + const [editingClass, setEditingClass] = useState(null); + const [newClass, setNewClass] = useState(null); + const [localErrors, setLocalErrors] = useState({}); + const [popupVisible, setPopupVisible] = useState(false); + const [popupMessage, setPopupMessage] = useState(""); + + const niveauxPremierCycle = [ + { id: 1, name: 'TPS', age: 2 }, + { id: 2, name: 'PS', age: 3 }, + { id: 3, name: 'MS', age: 4 }, + { id: 4, name: 'GS', age: 5 }, + ]; + + const niveauxSecondCycle = [ + { id: 5, name: 'CP', age: 6 }, + { id: 6, name: 'CE1', age: 7 }, + { id: 7, name: 'CE2', age: 8 }, + ]; + + const niveauxTroisiemeCycle = [ + { id: 8, name: 'CM1', age: 9 }, + { id: 9, name: 'CM2', age: 10 }, + ]; + + const allNiveaux = [...niveauxPremierCycle, ...niveauxSecondCycle, ...niveauxTroisiemeCycle]; + + const getNiveauxLabels = (levels) => { + return levels.map(niveauId => { + const niveau = allNiveaux.find(n => n.id === niveauId); + return niveau ? niveau.name : niveauId; + }); + }; + + // Fonction pour générer les années scolaires + const getSchoolYearChoices = () => { + const currentDate = new Date(); + const currentYear = currentDate.getFullYear(); + const currentMonth = currentDate.getMonth() + 1; // Les mois sont indexés à partir de 0 + + // Si nous sommes avant septembre, l'année scolaire en cours a commencé l'année précédente + const startYear = currentMonth >= 9 ? currentYear : currentYear - 1; + + const choices = []; + for (let i = 0; i < 3; i++) { + const year = startYear + i; + choices.push({ value: `${year}-${year + 1}`, label: `${year}-${year + 1}` }); + } + return choices; + }; + + const handleAddClass = () => { + setNewClass({ id: Date.now(), atmosphere_name: '', age_range: '', levels: [], number_of_students: '', school_year: '', teachers: [] }); + setFormData({ atmosphere_name: '', age_range: '', levels: [], number_of_students: '', school_year: '', teachers: [] }); + }; + + const handleChange = (e) => { + const { name, value } = e.target; + + if (editingClass) { + setFormData((prevData) => ({ + ...prevData, + [name]: value, + })); + } else if (newClass) { + setNewClass((prevData) => ({ + ...prevData, + [name]: value, + })); + } + }; + + const handleSaveNewClass = () => { + if (newClass.atmosphere_name) { + handleCreate(newClass) + .then((createdClass) => { + setClasses((prevClasses) => [createdClass, ...classes]); + setNewClass(null); + setLocalErrors({}); + }) + .catch((error) => { + console.error(error); + }); + } else { + setPopupMessage("Tous les champs doivent être remplis et valides"); + setPopupVisible(true); + } + }; + + const handleUpdateClass = (id, updatedData) => { + if (!updatedData.atmosphere_name) { + setLocalErrors({ atmosphere_name: 'Le nom d\'ambiance est requis.' }); + return; + } + + handleEdit(id, updatedData) + .then((updatedClass) => { + setClasses((prevClasses) => prevClasses.map((classe) => (classe.id === id ? updatedClass : classe))); + setEditingClass(null); + setFormData({}); + setLocalErrors({}); + }) + .catch((error) => { + console.error(error); + }); + }; + + const handleTeachersChange = (selectedTeachers) => { + if (editingClass) { + setFormData((prevData) => ({ + ...prevData, + teachers: selectedTeachers, + })); + } else if (newClass) { + setNewClass((prevData) => ({ + ...prevData, + teachers: selectedTeachers, + })); + setFormData((prevData) => ({ + ...prevData, + teachers: selectedTeachers, + })); + } + }; + + const handleMultiSelectChange = (selectedOptions) => { + const levels = selectedOptions.map(option => option.id); + + if (editingClass) { + setFormData((prevData) => ({ + ...prevData, + levels, + })); + } else if (newClass) { + setNewClass((prevData) => ({ + ...prevData, + levels, + })); + setFormData((prevData) => ({ + ...prevData, + levels, + })); + } + }; + + const renderClassCell = (classe, column) => { + const isEditing = editingClass === classe.id; + const isCreating = newClass && newClass.id === classe.id; + const currentData = isEditing ? formData : newClass || {}; + + if (isEditing || isCreating) { + switch (column) { + case 'AMBIANCE': + return ( + + ); + case 'TRANCHE D\'AGE': + return ( + + ) + case 'NIVEAUX': + return ( + allNiveaux.find(level => level.id === levelId)) : []} + onChange={handleMultiSelectChange} + errorMsg={localErrors && localErrors.levels && Array.isArray(localErrors.levels) ? localErrors.levels[0] : ''} + /> + ); + case 'CAPACITE': + return ( + + ) + case 'ANNÉE SCOLAIRE' : + return ( + + ) + case 'ENSEIGNANTS': + return ( + + ); + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } else { + switch (column) { + case 'AMBIANCE': + return classe.atmosphere_name; + case 'TRANCHE D\'AGE': + return classe.age_range; + case 'NIVEAUX': + const levelLabels = Array.isArray(classe.levels) ? getNiveauxLabels(classe.levels) : []; + return ( +
+ {levelLabels.length > 0 + ? levelLabels.map((label, index) => ( + + )) + : 'Aucun niveau'} +
+ ); + case 'CAPACITE': + return classe.number_of_students; + case 'ANNÉE SCOLAIRE' : + return classe.school_year; + case 'ENSEIGNANTS': + return ( +
+ {classe.teachers_details.map((teacher) => ( + + ))} +
+ ); + case 'MISE A JOUR': + return classe.updated_date_formatted; + case 'ACTIONS': + return ( +
+ + + +
+ ); + default: + return null; + } + } + }; + + const columns = [ + { name: 'AMBIANCE', label: 'Nom d\'ambiance' }, + { name: 'TRANCHE D\'AGE', label: 'Tranche d\'âge' }, + { name: 'NIVEAUX', label: 'Niveaux' }, + { name: 'CAPACITE', label: 'Capacité max' }, + { name: 'ANNÉE SCOLAIRE', label: 'Année scolaire' }, + { name: 'ENSEIGNANTS', label: 'Enseignants' }, + { name: 'MISE A JOUR', label: 'Date mise à jour' }, + { name: 'ACTIONS', label: 'Actions' } + ]; + + return ( + +
+
+
+ +

Classes

+
+ +
+
+ setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + + ); +}; + +export default ClassesSection; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/SpecialityItem.js b/Front-End/src/components/Structure/Configuration/SpecialityItem.js index 8f0b801..6975b5d 100644 --- a/Front-End/src/components/Structure/Configuration/SpecialityItem.js +++ b/Front-End/src/components/Structure/Configuration/SpecialityItem.js @@ -5,6 +5,24 @@ const ItemTypes = { SPECIALITY: 'speciality', }; +const lightenColor = (color, percent) => { + const num = parseInt(color.slice(1), 16), + amt = Math.round(2.55 * percent), + R = (num >> 16) + amt, + G = (num >> 8 & 0x00FF) + amt, + B = (num & 0x0000FF) + amt; + return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`; +}; + +const darkenColor = (color, percent) => { + const num = parseInt(color.slice(1), 16), + amt = Math.round(2.55 * percent), + R = (num >> 16) - amt, + G = (num >> 8 & 0x00FF) - amt, + B = (num & 0x0000FF) - amt; + return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`; +}; + const SpecialityItem = ({ speciality, isDraggable = true }) => { const [{ isDragging }, drag] = useDrag(() => ({ type: ItemTypes.SPECIALITY, @@ -18,10 +36,15 @@ const SpecialityItem = ({ speciality, isDraggable = true }) => { return (
{speciality.name}
diff --git a/Front-End/src/components/Structure/Configuration/StructureManagement.js b/Front-End/src/components/Structure/Configuration/StructureManagement.js index f343ff7..bde54ca 100644 --- a/Front-End/src/components/Structure/Configuration/StructureManagement.js +++ b/Front-End/src/components/Structure/Configuration/StructureManagement.js @@ -7,7 +7,7 @@ import { BE_SCHOOL_SPECIALITY_URL, BE_SCHOOL_TEACHER_URL, BE_SCHOOL_SCHOOLCLASS_ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, handleCreate, handleEdit, handleDelete }) => { return ( -
+
handleDelete(`${BE_SCHOOL_TEACHER_URL}`, id, setTeachers)} />
-
+
handleCreate(`${BE_SCHOOL_SCHOOLCLASS_URL}`, newData, setClasses)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, updatedData, setClasses)} diff --git a/Front-End/src/components/Structure/Configuration/TeacherItem.js b/Front-End/src/components/Structure/Configuration/TeacherItem.js new file mode 100644 index 0000000..a0d6287 --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/TeacherItem.js @@ -0,0 +1,35 @@ +import { useDrag } from 'react-dnd'; +import React from 'react'; + +const ItemTypes = { + TEACHER: 'teacher', +}; + +const TeacherItem = ({ teacher, isDraggable = true }) => { + const [{ isDragging }, drag] = useDrag(() => ({ + type: ItemTypes.TEACHER, + item: { id: teacher.id, name: `${teacher.last_name} ${teacher.first_name}` }, + collect: (monitor) => ({ + isDragging: !!monitor.isDragging(), + }), + canDrag: () => isDraggable, + }), [isDraggable]); + + return ( +
+ {teacher.last_name} {teacher.first_name} +
+ ); +}; + +export default TeacherItem; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/TeachersSection.js b/Front-End/src/components/Structure/Configuration/TeachersSection.js index d6d7d0a..0799e50 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSection.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSection.js @@ -9,6 +9,7 @@ import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import InputText from '@/components/InputText'; import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'; +import TeacherItem from './TeacherItem'; const ItemTypes = { SPECIALITY: 'speciality', @@ -149,11 +150,11 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha }; const handleUpdateTeacher = (id, updatedData) => { - console.log('UpdatedData:', updatedData); + console.log(updatedData) const data = { email: updatedData.email, username: updatedData.email, - droit: updatedData.droit.id, + droit: updatedData.droit, }; updateProfile(updatedData.associated_profile, data, csrfToken) .then(response => { @@ -171,9 +172,15 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha }; const handleChange = (e) => { - const { name, value } = e.target; + const { name, value, type, checked } = e.target; let parsedValue = value; + if (type === 'checkbox') { + parsedValue = checked ? 1 : 0; + } + + console.log(`handleChange - name: ${name}, parsedValue: ${parsedValue}, type: ${type}, checked: ${checked}`); + if (editingTeacher) { setFormData((prevData) => ({ ...prevData, @@ -216,25 +223,26 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha if (isEditing || isCreating) { switch (column) { - case 'NOM': + case 'NOM - PRENOM': return ( - - ); - case 'PRENOM': - return ( - +
+ + +
); case 'EMAIL': return ( @@ -250,14 +258,16 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha return ( ); - // case 'PROFIL': - // return ( - // - // ); + case 'ADMINISTRATEUR': + return ( +
+ +
+ ); case 'ACTIONS': return (
@@ -282,10 +292,10 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha } } else { switch (column) { - case 'NOM': - return teacher.last_name; - case 'PRENOM': - return teacher.first_name; + case 'NOM - PRENOM': + return ( + + ); case 'EMAIL': return teacher.email; case 'SPECIALITES': @@ -296,13 +306,14 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha ))}
); - case 'PROFIL': + case 'ADMINISTRATEUR': if (teacher.associated_profile) { - const badgeClass = teacher.droit.label === 'ECOLE' ? 'bg-blue-100 text-blue-600' : 'bg-red-100 text-red-600'; + const badgeClass = teacher.droit === 1 ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600'; + const label = teacher.droit === 1 ? 'OUI' : 'NON'; return (
- {teacher.droit.label} + {label}
); @@ -337,11 +348,10 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha }; const columns = [ - { name: 'NOM', label: 'Nom' }, - { name: 'PRENOM', label: 'Prénom' }, + { name: 'NOM - PRENOM', label: 'Nom et prénom' }, { name: 'EMAIL', label: 'Email' }, { name: 'SPECIALITES', label: 'Spécialités' }, - { name: 'PROFIL', label: 'Profil' }, + { name: 'ADMINISTRATEUR', label: 'Profil' }, { name: 'MISE A JOUR', label: 'Mise à jour' }, { name: 'ACTIONS', label: 'Actions' } ]; diff --git a/Front-End/src/components/ToggleSwitch.js b/Front-End/src/components/ToggleSwitch.js index 29fbfaa..262bd65 100644 --- a/Front-End/src/components/ToggleSwitch.js +++ b/Front-End/src/components/ToggleSwitch.js @@ -1,6 +1,6 @@ import { useRef } from 'react'; -const ToggleSwitch = ({ label, checked, onChange }) => { +const ToggleSwitch = ({ name, label, checked, onChange }) => { const inputRef = useRef(null); const handleChange = (e) => { @@ -16,15 +16,15 @@ const ToggleSwitch = ({ label, checked, onChange }) => {
From 42b4c99be86f050ccd76302caf725af5df413d17 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Fri, 7 Feb 2025 16:43:29 +0100 Subject: [PATCH 018/249] fix: Champs requis sur les teachers and classes --- .../Structure/Configuration/ClassesSection.js | 30 ++++++++-------- .../Configuration/TeachersSection.js | 35 +++++++++++-------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/Front-End/src/components/Structure/Configuration/ClassesSection.js b/Front-End/src/components/Structure/Configuration/ClassesSection.js index f5ac6b2..c9d7aaf 100644 --- a/Front-End/src/components/Structure/Configuration/ClassesSection.js +++ b/Front-End/src/components/Structure/Configuration/ClassesSection.js @@ -161,7 +161,7 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi }; const handleSaveNewClass = () => { - if (newClass.atmosphere_name) { + if (newClass.atmosphere_name && newClass.age_range && newClass.levels.length > 0 && newClass.number_of_students && newClass.school_year) { handleCreate(newClass) .then((createdClass) => { setClasses((prevClasses) => [createdClass, ...classes]); @@ -178,21 +178,21 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi }; const handleUpdateClass = (id, updatedData) => { - if (!updatedData.atmosphere_name) { - setLocalErrors({ atmosphere_name: 'Le nom d\'ambiance est requis.' }); - return; + if (updatedData.atmosphere_name && updatedData.age_range && updatedData.levels.length > 0 && updatedData.number_of_students && updatedData.school_year) { + handleEdit(id, updatedData) + .then((updatedClass) => { + setClasses((prevClasses) => prevClasses.map((classe) => (classe.id === id ? updatedClass : classe))); + setEditingClass(null); + setFormData({}); + setLocalErrors({}); + }) + .catch((error) => { + console.error(error); + }); + } else { + setPopupMessage("Tous les champs doivent être remplis et valides"); + setPopupVisible(true); } - - handleEdit(id, updatedData) - .then((updatedClass) => { - setClasses((prevClasses) => prevClasses.map((classe) => (classe.id === id ? updatedClass : classe))); - setEditingClass(null); - setFormData({}); - setLocalErrors({}); - }) - .catch((error) => { - console.error(error); - }); }; const handleTeachersChange = (selectedTeachers) => { diff --git a/Front-End/src/components/Structure/Configuration/TeachersSection.js b/Front-End/src/components/Structure/Configuration/TeachersSection.js index 0799e50..27746bc 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSection.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSection.js @@ -150,25 +150,30 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha }; const handleUpdateTeacher = (id, updatedData) => { - console.log(updatedData) - const data = { - email: updatedData.email, - username: updatedData.email, - droit: updatedData.droit, - }; - updateProfile(updatedData.associated_profile, data, csrfToken) - .then(response => { - console.log('Success:', response); - handleEdit(id, updatedData) - .then((updatedTeacher) => { - setTeachers(prevTeachers => prevTeachers.map(teacher => teacher.id === id ? { ...teacher, ...updatedTeacher } : teacher)); - setEditingTeacher(null); - setFormData({}); + if (updatedData.last_name && updatedData.first_name && updatedData.email) { + const data = { + email: updatedData.email, + username: updatedData.email, + droit: updatedData.droit, + }; + updateProfile(updatedData.associated_profile, data, csrfToken) + .then(response => { + console.log('Success:', response); + handleEdit(id, updatedData) + .then((updatedTeacher) => { + setTeachers(prevTeachers => prevTeachers.map(teacher => teacher.id === id ? { ...teacher, ...updatedTeacher } : teacher)); + setEditingTeacher(null); + setFormData({}); + }) }) - }) .catch(error => { console.error(error); }); + } + else { + setPopupMessage("Tous les champs doivent être remplis et valides"); + setPopupVisible(true); + } }; const handleChange = (e) => { From 7f3552764979e098ca2e8c3547354c8ae6feaa23 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Fri, 7 Feb 2025 17:40:30 +0100 Subject: [PATCH 019/249] fix: gestion des codes retours --- Back-End/Auth/views.py | 5 +- .../src/app/[locale]/admin/structure/page.js | 65 ++++--------------- Front-End/src/app/lib/schoolAction.js | 38 +++++++++++ .../Structure/Configuration/ClassesSection.js | 27 ++++++-- .../Configuration/SpecialitiesSection.js | 27 ++++---- .../Configuration/TeachersSection.js | 46 +++++++++---- 6 files changed, 124 insertions(+), 84 deletions(-) diff --git a/Back-End/Auth/views.py b/Back-End/Auth/views.py index cd5c385..eb14f77 100644 --- a/Back-End/Auth/views.py +++ b/Back-End/Auth/views.py @@ -77,8 +77,7 @@ class ProfileView(APIView): return JsonResponse(profil_serializer.data, safe=False) - - return JsonResponse(profil_serializer.errors, safe=False) + return JsonResponse(profil_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) def put(self, request, _id): data=JSONParser().parse(request) @@ -88,7 +87,7 @@ class ProfileView(APIView): profil_serializer.save() return JsonResponse("Updated Successfully", safe=False) - return JsonResponse(profil_serializer.errors, safe=False) + return JsonResponse(profil_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, _id): return bdd.delete_object(Profile, _id) diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index e76e5ed..46ca74e 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -6,7 +6,17 @@ import FeesManagement from '@/components/Structure/Tarification/FeesManagement'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; -import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchRegistrationDiscounts, fetchTuitionDiscounts, fetchRegistrationFees, fetchTuitionFees } from '@/app/lib/schoolAction'; +import { createDatas, + updateDatas, + removeDatas, + fetchSpecialities, + fetchTeachers, + fetchClasses, + fetchSchedules, + fetchRegistrationDiscounts, + fetchTuitionDiscounts, + fetchRegistrationFees, + fetchTuitionFees } from '@/app/lib/schoolAction'; import SidebarTabs from '@/components/SidebarTabs'; export default function Page() { @@ -111,23 +121,7 @@ export default function Page() { }; const handleCreate = (url, newData, setDatas) => { - return fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken - }, - body: JSON.stringify(newData), - credentials: 'include' - }) - .then(response => { - if (!response.ok) { - return response.json().then(errorData => { - throw errorData; - }); - } - return response.json(); - }) + return createDatas(url, newData, csrfToken) .then(data => { setDatas(prevState => [...prevState, data]); return data; @@ -139,23 +133,7 @@ export default function Page() { }; const handleEdit = (url, id, updatedData, setDatas) => { - return fetch(`${url}/${id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken - }, - body: JSON.stringify(updatedData), - credentials: 'include' - }) - .then(response => { - if (!response.ok) { - return response.json().then(errorData => { - throw errorData; - }); - } - return response.json(); - }) + return updateDatas(url, id, updatedData, csrfToken) .then(data => { setDatas(prevState => prevState.map(item => item.id === id ? data : item)); return data; @@ -167,22 +145,7 @@ export default function Page() { }; const handleDelete = (url, id, setDatas) => { - return fetch(`${url}/${id}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken - }, - credentials: 'include' - }) - .then(response => { - if (!response.ok) { - return response.json().then(errorData => { - throw errorData; - }); - } - return response.json(); - }) + return removeDatas(url, id, csrfToken) .then(data => { setDatas(prevState => prevState.filter(item => item.id !== id)); return data; diff --git a/Front-End/src/app/lib/schoolAction.js b/Front-End/src/app/lib/schoolAction.js index 7035cab..2c91893 100644 --- a/Front-End/src/app/lib/schoolAction.js +++ b/Front-End/src/app/lib/schoolAction.js @@ -58,4 +58,42 @@ export const fetchRegistrationFees = () => { export const fetchTuitionFees = () => { return fetch(`${BE_SCHOOL_FEES_URL}/tuition`) .then(requestResponseHandler) +}; + +export const createDatas = (url, newData, csrfToken) => { + return fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(newData), + credentials: 'include' + }) + .then(requestResponseHandler) +}; + +export const updateDatas = (url, id, updatedData, csrfToken) => { + return fetch(`${url}/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(updatedData), + credentials: 'include' + }) + .then(requestResponseHandler) + }; + +export const removeDatas = (url, id, csrfToken) => { + return fetch(`${url}/${id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + credentials: 'include' + }) + .then(requestResponseHandler) }; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/ClassesSection.js b/Front-End/src/components/Structure/Configuration/ClassesSection.js index c9d7aaf..e7af1ad 100644 --- a/Front-End/src/components/Structure/Configuration/ClassesSection.js +++ b/Front-End/src/components/Structure/Configuration/ClassesSection.js @@ -139,6 +139,11 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi return choices; }; + // Récupération des messages d'erreur + const getError = (field) => { + return localErrors?.[field]?.[0]; + }; + const handleAddClass = () => { setNewClass({ id: Date.now(), atmosphere_name: '', age_range: '', levels: [], number_of_students: '', school_year: '', teachers: [] }); setFormData({ atmosphere_name: '', age_range: '', levels: [], number_of_students: '', school_year: '', teachers: [] }); @@ -169,7 +174,11 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi setLocalErrors({}); }) .catch((error) => { - console.error(error); + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); + } }); } else { setPopupMessage("Tous les champs doivent être remplis et valides"); @@ -187,7 +196,11 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi setLocalErrors({}); }) .catch((error) => { - console.error(error); + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); + } }); } else { setPopupMessage("Tous les champs doivent être remplis et valides"); @@ -247,7 +260,7 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi value={currentData.atmosphere_name} onChange={handleChange} placeholder="Nom d'ambiance" - errorMsg={localErrors && localErrors.atmosphere_name && Array.isArray(localErrors.atmosphere_name) ? localErrors.atmosphere_name[0] : ''} + errorMsg={getError('atmosphere_name')} /> ); case 'TRANCHE D\'AGE': @@ -257,7 +270,7 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi value={currentData.age_range} onChange={handleChange} placeholder="Tranche d'âge (ex: 3-6)" - errorMsg={localErrors && localErrors.age_range && Array.isArray(localErrors.age_range) ? localErrors.age_range[0] : ''} + errorMsg={getError('age_range')} /> ) case 'NIVEAUX': @@ -267,7 +280,7 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi options={allNiveaux} selectedOptions={currentData.levels ? currentData.levels.map(levelId => allNiveaux.find(level => level.id === levelId)) : []} onChange={handleMultiSelectChange} - errorMsg={localErrors && localErrors.levels && Array.isArray(localErrors.levels) ? localErrors.levels[0] : ''} + errorMsg={getError('levels')} /> ); case 'CAPACITE': @@ -278,7 +291,7 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi value={currentData.number_of_students} onChange={handleChange} placeholder="Capacité" - errorMsg={localErrors && localErrors.number_of_students && Array.isArray(localErrors.number_of_students) ? localErrors.number_of_students[0] : ''} + errorMsg={getError('number_of_students')} /> ) case 'ANNÉE SCOLAIRE' : @@ -290,7 +303,7 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi choices={getSchoolYearChoices()} callback={handleChange} selected={currentData.school_year || ''} - errorMsg={localErrors && localErrors.school_year && Array.isArray(localErrors.school_year) ? localErrors.school_year[0] : ''} + errorMsg={getError('school_year')} IconItem={null} disabled={false} /> diff --git a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js index 762bbca..b3ae3bd 100644 --- a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js +++ b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js @@ -16,6 +16,11 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(""); + // Récupération des messages d'erreur + const getError = (field) => { + return localErrors?.[field]?.[0]; + }; + const handleAddSpeciality = () => { setNewSpeciality({ id: Date.now(), name: '', color_code: '' }); }; @@ -39,11 +44,11 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand setNewSpeciality(null); setLocalErrors({}); }) - .catch(error => { - if (error && typeof error === 'object') { - setLocalErrors(error); - } else { - console.error(error); + .catch((error) => { + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); } }); } else { @@ -61,11 +66,11 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand setEditingSpeciality(null); setLocalErrors({}); }) - .catch(error => { - if (error && typeof error === 'object') { - setLocalErrors(error); - } else { - console.error(error); + .catch((error) => { + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); } }); } else { @@ -111,7 +116,7 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand onTextChange={handleChange} onColorChange={handleChange} placeholder="Nom de la spécialité" - errorMsg={localErrors && localErrors.name && Array.isArray(localErrors.name) ? localErrors.name[0] : ''} + errorMsg={getError('name')} /> ); case 'ACTIONS': diff --git a/Front-End/src/components/Structure/Configuration/TeachersSection.js b/Front-End/src/components/Structure/Configuration/TeachersSection.js index 27746bc..801ea06 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSection.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSection.js @@ -98,6 +98,11 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(""); + // 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 }); @@ -133,14 +138,22 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha setTeachers([createdTeacher, ...teachers]); setNewTeacher(null); setLocalErrors({}); + }) + .catch((error) => { + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); + } }); } + setLocalErrors({}); }) - .catch(error => { - if (error && typeof error === 'object') { - setLocalErrors(error); - } else { - console.error(error); + .catch((error) => { + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); } }); } else { @@ -165,9 +178,20 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha setEditingTeacher(null); setFormData({}); }) + .catch((error) => { + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); + } + }); }) - .catch(error => { - console.error(error); + .catch((error) => { + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); + } }); } else { @@ -184,8 +208,6 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha parsedValue = checked ? 1 : 0; } - console.log(`handleChange - name: ${name}, parsedValue: ${parsedValue}, type: ${type}, checked: ${checked}`); - if (editingTeacher) { setFormData((prevData) => ({ ...prevData, @@ -237,7 +259,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha value={currentData.last_name} onChange={handleChange} placeholder="Nom de l'enseignant" - errorMsg={localErrors && localErrors.last_name && Array.isArray(localErrors.last_name) ? localErrors.last_name[0] : ''} + errorMsg={getError('last_name')} />
); @@ -256,7 +278,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha value={currentData.email} onChange={handleChange} placeholder="Email de l'enseignant" - errorMsg={localErrors && localErrors.email && Array.isArray(localErrors.email) ? localErrors.email[0] : ''} + errorMsg={getError('email')} /> ); case 'SPECIALITES': From f2628bb45a14da42d014e42b1521820ffeedfb33 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Fri, 7 Feb 2025 17:55:53 +0100 Subject: [PATCH 020/249] =?UTF-8?q?fix:=20Ajout=20du=20%=20ou=20=E2=82=AC?= =?UTF-8?q?=20en=20mode=20=C3=A9dition=20de=20r=C3=A9duction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tarification/DiscountsSection.js | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Front-End/src/components/Structure/Tarification/DiscountsSection.js b/Front-End/src/components/Structure/Tarification/DiscountsSection.js index 9d8f04d..177ab05 100644 --- a/Front-End/src/components/Structure/Tarification/DiscountsSection.js +++ b/Front-End/src/components/Structure/Tarification/DiscountsSection.js @@ -86,6 +86,20 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h }); }; + const handleToggleDiscountTypeEdition = (id) => { + if (editingDiscount) { + setFormData((prevData) => ({ + ...prevData, + discount_type: prevData.discount_type === 0 ? 1 : 0, + })); + } else if (newDiscount) { + setNewDiscount((prevData) => ({ + ...prevData, + discount_type: prevData.discount_type === 0 ? 1 : 0, + })); + } + }; + const handleChange = (e) => { const { name, value } = e.target; if (editingDiscount) { @@ -124,7 +138,19 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h case 'LIBELLE': return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction'); case 'REMISE': - return renderInputField('amount', currentData.amount, handleChange,'Montant'); + return ( +
+ {renderInputField('amount', currentData.amount, handleChange,'Montant')} + + +
+ ); case 'DESCRIPTION': return renderInputField('description', currentData.description, handleChange, 'Description'); case 'ACTIONS': From c269b89d3d58cc65f254b75f6d713c4fd15f6320 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Fri, 7 Feb 2025 20:36:02 +0100 Subject: [PATCH 021/249] =?UTF-8?q?fix:=20Calcul=20du=20montant=20total=20?= =?UTF-8?q?des=20tarif=20par=20RF=20+=20affichage=20des=20tarifs=20actifs?= =?UTF-8?q?=20lors=20de=20la=20cr=C3=A9ation=20d'un=20RF=20[#26]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Subscriptions/models.py | 21 ------- Back-End/Subscriptions/serializers.py | 20 +++--- Back-End/Subscriptions/urls.py | 5 +- Back-End/Subscriptions/views.py | 14 +---- .../app/[locale]/admin/subscriptions/page.js | 9 ++- .../components/Inscription/InscriptionForm.js | 1 + Front-End/src/components/Popup.js | 7 ++- .../Structure/Configuration/ClassesSection.js | 29 ++++++++- .../Configuration/SpecialitiesSection.js | 32 +++++++++- .../Configuration/TeachersSection.js | 32 +++++++++- .../Tarification/DiscountsSection.js | 31 +++++++++- .../Structure/Tarification/FeesSection.js | 61 ++++++++++++++----- 12 files changed, 192 insertions(+), 70 deletions(-) diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 8cfb355..d43dbe5 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -8,27 +8,6 @@ from School.models import SchoolClass, Fee, Discount from datetime import datetime -class RegistrationFee(models.Model): - """ - Représente un tarif ou frais d’inscription avec différentes options de paiement. - """ - class PaymentOptions(models.IntegerChoices): - SINGLE_PAYMENT = 0, _('Paiement en une seule fois') - MONTHLY_PAYMENT = 1, _('Paiement mensuel') - QUARTERLY_PAYMENT = 2, _('Paiement trimestriel') - - name = models.CharField(max_length=255, unique=True) - description = models.TextField(blank=True) - base_amount = models.DecimalField(max_digits=10, decimal_places=2) - discounts = models.JSONField(blank=True, null=True) - supplements = models.JSONField(blank=True, null=True) - validity_start_date = models.DateField() - validity_end_date = models.DateField() - payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT) - - def __str__(self): - return self.name - class Language(models.Model): """ Représente une langue parlée par l’élève. diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index 2e916a7..b11c482 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee -from School.models import SchoolClass, Fee, Discount +from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language +from School.models import SchoolClass, Fee, Discount, FeeType from School.serializers import FeeSerializer, DiscountSerializer from Auth.models import Profile from Auth.serializers import ProfileSerializer @@ -11,12 +11,6 @@ from django.utils import timezone import pytz from datetime import datetime -class RegistrationFeeSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(required=False) - class Meta: - model = RegistrationFee - fields = '__all__' - class RegistrationFileSerializer(serializers.ModelSerializer): class Meta: model = RegistrationFile @@ -136,6 +130,8 @@ class RegistrationFormSerializer(serializers.ModelSerializer): registration_files = RegistrationFileSerializer(many=True, required=False) fees = serializers.PrimaryKeyRelatedField(queryset=Fee.objects.all(), many=True, required=False) discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True, required=False) + totalRegistrationFees = serializers.SerializerMethodField() + totalTuitionFees = serializers.SerializerMethodField() class Meta: model = RegistrationForm @@ -184,6 +180,14 @@ class RegistrationFormSerializer(serializers.ModelSerializer): return local_time.strftime("%d-%m-%Y %H:%M") + def get_totalRegistrationFees(self, obj): + for fee in obj.fees.filter(type=FeeType.REGISTRATION_FEE): + print(fee.base_amount) + return sum(fee.base_amount for fee in obj.fees.filter(type=FeeType.REGISTRATION_FEE)) + + def get_totalTuitionFees(self, obj): + return sum(fee.base_amount for fee in obj.fees.filter(type=FeeType.TUITION_FEE)) + class StudentByParentSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) diff --git a/Back-End/Subscriptions/urls.py b/Back-End/Subscriptions/urls.py index 0e7fbf8..9def08b 100644 --- a/Back-End/Subscriptions/urls.py +++ b/Back-End/Subscriptions/urls.py @@ -1,7 +1,7 @@ from django.urls import path, re_path from . import views -from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView, RegistrationFileView +from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFileView urlpatterns = [ re_path(r'^registerForms/(?P<_filter>[a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"), @@ -30,9 +30,6 @@ urlpatterns = [ # Page INSCRIPTION - Liste des élèves re_path(r'^students$', StudentListView.as_view(), name="students"), - # Frais d'inscription - re_path(r'^registrationFees$', RegistrationFeeView.as_view(), name="registrationFees"), - # modèles de fichiers d'inscription re_path(r'^registrationFileTemplates$', RegistrationFileTemplateView.as_view(), name='registrationFileTemplates'), re_path(r'^registrationFileTemplates/(?P<_id>[0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"), diff --git a/Back-End/Subscriptions/views.py b/Back-End/Subscriptions/views.py index 4e0ae69..4b48e4f 100644 --- a/Back-End/Subscriptions/views.py +++ b/Back-End/Subscriptions/views.py @@ -22,10 +22,10 @@ import Subscriptions.mailManager as mailer import Subscriptions.util as util from Subscriptions.automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine -from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer +from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer from .pagination import CustomPagination from .signals import clear_cache -from .models import Student, Guardian, RegistrationForm, RegistrationFee, RegistrationFileTemplate, RegistrationFile +from .models import Student, Guardian, RegistrationForm, RegistrationFileTemplate, RegistrationFile from .automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine from Auth.models import Profile @@ -335,16 +335,6 @@ class StudentListView(APIView): students_serializer = StudentByRFCreationSerializer(students, many=True) return JsonResponse(students_serializer.data, safe=False) -# API utilisée pour la vue de personnalisation des frais d'inscription pour la structure -class RegistrationFeeView(APIView): - """ - Liste les frais d’inscription. - """ - def get(self, request): - tarifs = bdd.getAllObjects(RegistrationFee) - tarifs_serializer = RegistrationFeeSerializer(tarifs, many=True) - return JsonResponse(tarifs_serializer.data, safe=False) - class RegistrationFileTemplateView(APIView): """ Gère les fichiers templates pour les dossiers d’inscription. diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 6cb654f..9de4858 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -410,7 +410,9 @@ useEffect(()=>{ associated_profile: response.id }], sibling: [] - } + }, + fees: allFeesIds, + discounts: allDiscountsds }; createRegisterForm(data, csrfToken) @@ -422,6 +424,7 @@ useEffect(()=>{ sendConfirmRegisterForm(data.student.id, updatedData.studentLastName, updatedData.studentFirstName); } closeModal(); + console.log('Success:', data); // Forcer le rechargement complet des données setReloadFetch(true); }) @@ -810,8 +813,8 @@ const handleFileUpload = ({file, name, is_required, order}) => { fee.is_active)} + tuitionFees={tuitionFees.filter(fee => fee.is_active)} onSubmit={createRF} /> )} diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index 0f7bab6..22a23fb 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -123,6 +123,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r }; const submit = () => { + console.log('Submitting form data:', formData); onSubmit(formData); } diff --git a/Front-End/src/components/Popup.js b/Front-End/src/components/Popup.js index 809d4f6..cbb4e72 100644 --- a/Front-End/src/components/Popup.js +++ b/Front-End/src/components/Popup.js @@ -4,10 +4,15 @@ import ReactDOM from 'react-dom'; const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = false }) => { if (!visible) return null; + // Diviser le message en lignes + const messageLines = message.split('\n'); + return ReactDOM.createPortal(
-

{message}

+ {messageLines.map((line, index) => ( +

{line}

+ ))}
{!uniqueConfirmButton && ( diff --git a/Front-End/src/components/Structure/Configuration/ClassesSection.js b/Front-End/src/components/Structure/Configuration/ClassesSection.js index e7af1ad..385ccd6 100644 --- a/Front-End/src/components/Structure/Configuration/ClassesSection.js +++ b/Front-End/src/components/Structure/Configuration/ClassesSection.js @@ -94,6 +94,9 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi const [localErrors, setLocalErrors] = useState({}); const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(""); + const [removePopupVisible, setRemovePopupVisible] = useState(false); + const [removePopupMessage, setRemovePopupMessage] = useState(""); + const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); const niveauxPremierCycle = [ { id: 1, name: 'TPS', age: 2 }, @@ -377,7 +380,25 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
); diff --git a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js index b3ae3bd..81a8f6b 100644 --- a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js +++ b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js @@ -16,6 +16,10 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(""); + const [removePopupVisible, setRemovePopupVisible] = useState(false); + const [removePopupMessage, setRemovePopupMessage] = useState(""); + const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); + // Récupération des messages d'erreur const getError = (field) => { return localErrors?.[field]?.[0]; @@ -26,7 +30,7 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand }; const handleRemoveSpeciality = (id) => { - handleDelete(id) + return handleDelete(id) .then(() => { setSpecialities(prevSpecialities => prevSpecialities.filter(speciality => speciality.id !== id)); }) @@ -161,7 +165,25 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
); diff --git a/Front-End/src/components/Structure/Configuration/TeachersSection.js b/Front-End/src/components/Structure/Configuration/TeachersSection.js index 801ea06..01d979c 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSection.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSection.js @@ -98,6 +98,10 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(""); + const [removePopupVisible, setRemovePopupVisible] = useState(false); + const [removePopupMessage, setRemovePopupMessage] = useState(""); + const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); + // Récupération des messages d'erreur const getError = (field) => { return localErrors?.[field]?.[0]; @@ -109,7 +113,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha }; const handleRemoveTeacher = (id) => { - handleDelete(id) + return handleDelete(id) .then(() => { setTeachers(prevTeachers => prevTeachers.filter(teacher => teacher.id !== id)); }) @@ -361,7 +365,25 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
); diff --git a/Front-End/src/components/Structure/Tarification/DiscountsSection.js b/Front-End/src/components/Structure/Tarification/DiscountsSection.js index 177ab05..b38357c 100644 --- a/Front-End/src/components/Structure/Tarification/DiscountsSection.js +++ b/Front-End/src/components/Structure/Tarification/DiscountsSection.js @@ -12,13 +12,16 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h const [localErrors, setLocalErrors] = useState({}); const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(""); + const [removePopupVisible, setRemovePopupVisible] = useState(false); + const [removePopupMessage, setRemovePopupMessage] = useState(""); + const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); const handleAddDiscount = () => { setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discount_type: 0, type: type }); }; const handleRemoveDiscount = (id) => { - handleDelete(id) + return handleDelete(id) .then(() => { setDiscounts(prevDiscounts => prevDiscounts.filter(discount => discount.id !== id)); }) @@ -204,7 +207,25 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
); }; diff --git a/Front-End/src/components/Structure/Tarification/FeesSection.js b/Front-End/src/components/Structure/Tarification/FeesSection.js index 8d6b3ba..f0ab4b7 100644 --- a/Front-End/src/components/Structure/Tarification/FeesSection.js +++ b/Front-End/src/components/Structure/Tarification/FeesSection.js @@ -12,13 +12,22 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl const [localErrors, setLocalErrors] = useState({}); const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(""); + const [removePopupVisible, setRemovePopupVisible] = useState(false); + const [removePopupMessage, setRemovePopupMessage] = useState(""); + const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); + const labelTypeFrais = (type === 0 ? 'Frais d\'inscription' : 'Frais de scolarité'); + + // Récupération des messages d'erreur + const getError = (field) => { + return localErrors?.[field]?.[0]; + }; const handleAddFee = () => { setNewFee({ id: Date.now(), name: '', base_amount: '', description: '', validity_start_date: '', validity_end_date: '', discounts: [], type: type }); }; const handleRemoveFee = (id) => { - handleDelete(id) + return handleDelete(id) .then(() => { setFees(prevFees => prevFees.filter(fee => fee.id !== id)); }) @@ -37,11 +46,11 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl setNewFee(null); setLocalErrors({}); }) - .catch(error => { - if (error && typeof error === 'object') { - setLocalErrors(error); - } else { - console.error(error); + .catch((error) => { + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); } }); } else { @@ -60,11 +69,11 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl setEditingFee(null); setLocalErrors({}); }) - .catch(error => { - if (error && typeof error === 'object') { - setLocalErrors(error); - } else { - console.error(error); + .catch((error) => { + console.error('Error:', error.message); + if (error.details) { + console.error('Form errors:', error.details); + setLocalErrors(error.details); } }); } else { @@ -118,7 +127,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl value={value} onChange={onChange} placeholder={placeholder} - errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''} + errorMsg={getError(field)} />
); @@ -187,7 +196,25 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
); }; From 274db249aa25f2a0281638c318a68cf88a721a45 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sun, 9 Feb 2025 19:20:43 +0100 Subject: [PATCH 022/249] feat: Mise en place des paiements en plusieurs fois - partie BACK [#25] --- Back-End/School/models.py | 15 +++++++--- Back-End/School/serializers.py | 9 ++++-- Back-End/School/urls.py | 6 ++++ Back-End/School/views.py | 54 ++++++++++++++++++++++++++++++++-- 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/Back-End/School/models.py b/Back-End/School/models.py index f549d77..3bc84cf 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -68,10 +68,11 @@ class Planning(models.Model): def __str__(self): return f'Planning for {self.level} of {self.school_class.atmosphere_name}' -class PaymentOptions(models.IntegerChoices): - SINGLE_PAYMENT = 0, _('Paiement en une seule fois') - FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois') - TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois') +class PaymentPlanType(models.IntegerChoices): + ONE_TIME = 1, '1 fois' + THREE_TIMES = 3, '3 fois' + TEN_TIMES = 10, '10 fois' + TWELVE_TIMES = 12, '12 fois' class DiscountType(models.IntegerChoices): CURRENCY = 0, 'Currency' @@ -102,3 +103,9 @@ class Fee(models.Model): def __str__(self): return self.name + +class PaymentPlan(models.Model): + frequency = models.IntegerField(choices=PaymentPlanType.choices, default=PaymentPlanType.ONE_TIME) + due_dates = ArrayField(models.DateField(), blank=True) + type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE) + diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index 44b3571..a3a76c7 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee +from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan from Auth.models import Profile from N3wtSchool import settings, bdd from django.utils import timezone @@ -190,4 +190,9 @@ class FeeSerializer(serializers.ModelSerializer): utc_time = timezone.localtime(obj.updated_at) local_tz = pytz.timezone(settings.TZ_APPLI) local_time = utc_time.astimezone(local_tz) - return local_time.strftime("%d-%m-%Y %H:%M") \ No newline at end of file + return local_time.strftime("%d-%m-%Y %H:%M") + +class PaymentPlanSerializer(serializers.ModelSerializer): + class Meta: + model = PaymentPlan + fields = '__all__' \ No newline at end of file diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index 6cd2009..1780b1b 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -13,6 +13,8 @@ from School.views import ( FeeView, DiscountsView, DiscountView, + PaymentPlansView, + PaymentPlanView ) urlpatterns = [ @@ -39,4 +41,8 @@ urlpatterns = [ re_path(r'^discounts/(?P<_filter>[a-zA-z]+)$$', DiscountsView.as_view(), name="discounts"), re_path(r'^discount$', DiscountView.as_view(), name="discount"), re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"), + + re_path(r'^paymentPlans/(?P<_filter>[a-zA-z]+)$', PaymentPlansView.as_view(), name="paymentPlans"), + re_path(r'^paymentPlan$', PaymentPlanView.as_view(), name="paymentPlan"), + re_path(r'^paymentPlan/([0-9]+)$', PaymentPlanView.as_view(), name="paymentPlan"), ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index 733d2f9..35d27d6 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -5,8 +5,8 @@ from rest_framework.parsers import JSONParser from rest_framework.views import APIView from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist -from .models import Teacher, Speciality, SchoolClass, Planning, Discount, Fee -from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, FeeSerializer +from .models import Teacher, Speciality, SchoolClass, Planning, Discount, Fee, PaymentPlan +from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, FeeSerializer, PaymentPlanSerializer from N3wtSchool import bdd from N3wtSchool.bdd import delete_object, getAllObjects, getObject @@ -307,4 +307,52 @@ class FeeView(APIView): return JsonResponse(fee_serializer.errors, safe=False, status=400) def delete(self, request, _id): - return delete_object(Fee, _id) \ No newline at end of file + return delete_object(Fee, _id) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class PaymentPlansView(APIView): + def get(self, request, _filter, *args, **kwargs): + + if _filter not in ['registration', 'tuition']: + return JsonResponse({"error": "Invalid type parameter. Must be 'registration' or 'tuition'."}, safe=False, status=400) + + type_value = 0 if _filter == 'registration' else 1 + paymentPlans = PaymentPlan.objects.filter(type=type_value) + payment_plans_serializer = PaymentPlanSerializer(paymentPlans, many=True) + + return JsonResponse(payment_plans_serializer.data, safe=False, status=200) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class PaymentPlanView(APIView): + def get(self, request, _id): + try: + payment_plan = PaymentPlan.objects.get(id=_id) + payment_plan_serializer = PaymentPlanSerializer(payment_plan) + return JsonResponse(payment_plan_serializer.data, safe=False) + except PaymentPlan.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + payment_plan_data = JSONParser().parse(request) + payment_plan_serializer = PaymentPlanSerializer(data=payment_plan_data) + if payment_plan_serializer.is_valid(): + payment_plan_serializer.save() + return JsonResponse(payment_plan_serializer.data, safe=False, status=201) + return JsonResponse(payment_plan_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + payment_plan_data = JSONParser().parse(request) + try: + payment_plan = PaymentPlan.objects.get(id=_id) + except PaymentPlan.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + payment_plan_serializer = PaymentPlanSerializer(payment_plan, data=payment_plan_data, partial=True) + if payment_plan_serializer.is_valid(): + payment_plan_serializer.save() + return JsonResponse(payment_plan_serializer.data, safe=False) + return JsonResponse(payment_plan_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(PaymentPlan, _id) \ No newline at end of file From fb7fbaf8394ebf41e6f3f31897e6d009c537a481 Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Mon, 10 Feb 2025 08:58:02 +0100 Subject: [PATCH 023/249] feat: Ajout du suivi de version dans le footer du Front --- Front-End/next.config.mjs | 4 ++++ Front-End/src/app/[locale]/admin/layout.js | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Front-End/next.config.mjs b/Front-End/next.config.mjs index c60203e..2a18797 100644 --- a/Front-End/next.config.mjs +++ b/Front-End/next.config.mjs @@ -1,4 +1,5 @@ import createNextIntlPlugin from 'next-intl/plugin'; +import pkg from "./package.json" assert { type: "json" }; const withNextIntl = createNextIntlPlugin(); @@ -7,6 +8,9 @@ const nextConfig = { experimental: { instrumentationHook: true, }, + env: { + NEXT_PUBLIC_APP_VERSION: pkg.version, + }, }; export default withNextIntl(nextConfig); \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/layout.js b/Front-End/src/app/[locale]/admin/layout.js index c35aac9..fe928b4 100644 --- a/Front-End/src/app/[locale]/admin/layout.js +++ b/Front-End/src/app/[locale]/admin/layout.js @@ -46,7 +46,7 @@ export default function Layout({ const headerTitle = sidebarItems[currentPage]?.name || t('dashboard'); const softwareName = "N3WT School"; - const softwareVersion = "v1.0.0"; + const softwareVersion = `v${process.env.NEXT_PUBLIC_APP_VERSION}`; const dropdownItems = [ @@ -80,9 +80,9 @@ export default function Layout({ {/* Footer - h-16 = 64px */}
-
+
© {new Date().getFullYear()} N3WT-INNOV Tous droits réservés. -
{softwareName} - {softwareVersion}
+
{softwareName} - {softwareVersion}
From ffc6ce8de835e9caf547b6c4a893436aa93513ba Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Mon, 10 Feb 2025 18:35:24 +0100 Subject: [PATCH 024/249] feat: Ajout des Bundles de fichiers [#24] --- Back-End/Subscriptions/models.py | 13 + Back-End/Subscriptions/serializers.py | 7 +- .../templates/pdfs/dossier_inscription.html | 234 ++++++--- Back-End/Subscriptions/urls.py | 52 +- Back-End/Subscriptions/views.py | 450 ------------------ Back-End/Subscriptions/views/__init__.py | 24 + .../Subscriptions/views/guardian_views.py | 34 ++ .../views/register_form_views.py | 385 +++++++++++++++ .../views/registration_file_group_views.py | 125 +++++ .../views/registration_file_views.py | 211 ++++++++ Back-End/Subscriptions/views/student_views.py | 83 ++++ Back-End/src/app.js | 0 Back-End/src/middleware/cors.js | 0 .../src/app/[locale]/admin/structure/page.js | 50 +- .../app/[locale]/admin/subscriptions/page.js | 177 ++----- .../src/app/lib/registerFileGroupAction.js | 74 +++ Front-End/src/app/lib/subscriptionAction.js | 77 +-- .../components/DraggableFileUpload.js | 0 .../components/FileUpload.js | 21 + .../components/Inscription/InscriptionForm.js | 59 ++- .../Inscription/InscriptionFormShared.js | 16 +- .../components/RegistrationFileGroupForm.js | 56 +++ .../components/RegistrationFileGroupList.js | 21 + .../Structure/Files/FilesManagement.js | 302 ++++++++++++ Front-End/src/utils/Url.js | 8 +- 25 files changed, 1736 insertions(+), 743 deletions(-) delete mode 100644 Back-End/Subscriptions/views.py create mode 100644 Back-End/Subscriptions/views/__init__.py create mode 100644 Back-End/Subscriptions/views/guardian_views.py create mode 100644 Back-End/Subscriptions/views/register_form_views.py create mode 100644 Back-End/Subscriptions/views/registration_file_group_views.py create mode 100644 Back-End/Subscriptions/views/registration_file_views.py create mode 100644 Back-End/Subscriptions/views/student_views.py create mode 100644 Back-End/src/app.js create mode 100644 Back-End/src/middleware/cors.js create mode 100644 Front-End/src/app/lib/registerFileGroupAction.js rename Front-End/src/{app/[locale]/admin/subscriptions => }/components/DraggableFileUpload.js (100%) rename Front-End/src/{app/[locale]/admin/subscriptions => }/components/FileUpload.js (74%) create mode 100644 Front-End/src/components/RegistrationFileGroupForm.js create mode 100644 Front-End/src/components/RegistrationFileGroupList.js create mode 100644 Front-End/src/components/Structure/Files/FilesManagement.js diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index d43dbe5..e7b2f9d 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -161,6 +161,13 @@ class Student(models.Model): return self.birth_date.strftime('%d-%m-%Y') return None +class RegistrationFileGroup(models.Model): + name = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + + def __str__(self): + return self.name + def registration_file_path(instance, filename): # Génère le chemin : registration_files/dossier_rf_{student_id}/filename return f'registration_files/dossier_rf_{instance.student_id}/{filename}' @@ -196,6 +203,11 @@ class RegistrationForm(models.Model): # Many-to-Many Relationship discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms') + fileGroup = models.ForeignKey(RegistrationFileGroup, + on_delete=models.CASCADE, + related_name='file_group', + null=True, + blank=True) def __str__(self): return "RF_" + self.student.last_name + "_" + self.student.first_name @@ -209,6 +221,7 @@ class RegistrationFileTemplate(models.Model): order = models.PositiveIntegerField(default=0) # Ajout du champ order date_added = models.DateTimeField(auto_now_add=True) is_required = models.BooleanField(default=False) + group = models.ForeignKey(RegistrationFileGroup, on_delete=models.CASCADE, related_name='file_templates') @property def formatted_date_added(self): diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index b11c482..461caa0 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language +from .models import RegistrationFileTemplate, RegistrationFile, RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language from School.models import SchoolClass, Fee, Discount, FeeType from School.serializers import FeeSerializer, DiscountSerializer from Auth.models import Profile @@ -11,6 +11,11 @@ from django.utils import timezone import pytz from datetime import datetime +class RegistrationFileGroupSerializer(serializers.ModelSerializer): + class Meta: + model = RegistrationFileGroup + fields = '__all__' + class RegistrationFileSerializer(serializers.ModelSerializer): class Meta: model = RegistrationFile diff --git a/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html b/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html index 4170a06..cb01e82 100644 --- a/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html +++ b/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html @@ -3,95 +3,205 @@ {{ pdf_title }} - {% load myTemplateTag %} -
-
-

{{ pdf_title }}

+
+
+

{{ pdf_title }}

-
-
- Signé le : {{ signatureDate }}
- A : {{ signatureTime }} -
-

ELEVE

+ +
+

ÉLÈVE

{% with level=student|getStudentLevel %} {% with gender=student|getStudentGender %} - NOM : {{ student.last_name }}
- PRENOM : {{ student.first_name }}
- ADRESSE : {{ student.address }}
- GENRE : {{ gender }}
- NE(E) LE : {{ student.birth_date }}
- A : {{ student.birth_place }} ({{ student.birth_postal_code }})
- NATIONALITE : {{ student.nationality }}
- NIVEAU : {{ level }}
- MEDECIN TRAITANT : {{ student.attending_physician }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NOM :{{ student.last_name }}PRÉNOM :{{ student.first_name }}
ADRESSE :{{ student.address }}
GENRE :{{ gender }}NÉ(E) LE :{{ student.birth_date }}
À :{{ student.birth_place }} ({{ student.birth_postal_code }})
NATIONALITÉ :{{ student.nationality }}NIVEAU :{{ level }}
MÉDECIN TRAITANT :{{ student.attending_physician }}
{% endwith %} {% endwith %} -
-

RESPONSABLES

+
+ +
+

RESPONSABLES

{% with guardians=student.getGuardians %} - {% with siblings=student.getGuardians %} {% for guardian in guardians%} -

Guardian {{ forloop.counter }}

- NOM : {{ guardian.last_name }}
- PRENOM : {{ guardian.first_name }}
- ADRESSE : {{ guardian.address }}
- NE(E) LE : {{ guardian.birth_date }}
- MAIL : {{ guardian.email }}
- TEL : {{ guardian.phone }}
- PROFESSION : {{ guardian.profession }}
+
+

Responsable {{ forloop.counter }}

+ + + + + + + + + + + + + + + + + + + + + + + +
NOM :{{ guardian.last_name }}PRÉNOM :{{ guardian.first_name }}
ADRESSE :{{ guardian.address }}
NÉ(E) LE :{{ guardian.birth_date }}EMAIL :{{ guardian.email }}
TÉLÉPHONE :{{ guardian.phone }}PROFESSION :{{ guardian.profession }}
+
{% endfor %} -
-

FRATRIE

+ {% endwith %} +
+ +
+

FRATRIE

+ {% with siblings=student.getGuardians %} {% for sibling in siblings%} -

Frère - Soeur {{ forloop.counter }}

- NOM : {{ sibling.last_name }}
- PRENOM : {{ sibling.first_name }}
- NE(E) LE : {{ sibling.birth_date }}
+
+

Frère/Sœur {{ forloop.counter }}

+ + + + + + + + + + + +
NOM :{{ sibling.last_name }}PRÉNOM :{{ sibling.first_name }}
NÉ(E) LE :{{ sibling.birth_date }}
+
{% endfor %} -
-

MODALITES DE PAIEMENT

+ {% endwith %} +
+ +
+

MODALITÉS DE PAIEMENT

{% with paymentMethod=student|getStudentPaymentMethod %} - {{ paymentMethod }}
- {% endwith %} - {% endwith %} +

{{ paymentMethod }}

{% endwith %}
+ +
+ Fait le {{ signatureDate }} à {{ signatureTime }} +
\ No newline at end of file diff --git a/Back-End/Subscriptions/urls.py b/Back-End/Subscriptions/urls.py index 9def08b..5b6252c 100644 --- a/Back-End/Subscriptions/urls.py +++ b/Back-End/Subscriptions/urls.py @@ -1,41 +1,41 @@ from django.urls import path, re_path from . import views -from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFileView + +# RF +from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archive +# SubClasses +from .views import StudentView, GuardianView, ChildrenListView, StudentListView +# Files +from .views import RegistrationFileTemplateView, RegistrationFileTemplateSimpleView, RegistrationFileView, RegistrationFileSimpleView +from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group urlpatterns = [ - re_path(r'^registerForms/(?P<_filter>[a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"), - - re_path(r'^registerForm$', RegisterFormView.as_view(), name="registerForm"), - re_path(r'^registerForm/(?P<_id>[0-9]+)$', RegisterFormView.as_view(), name="registerForm"), - - # Page de formulaire d'inscription - ELEVE - re_path(r'^student/(?P<_id>[0-9]+)$', StudentView.as_view(), name="students"), - - # Page de formulaire d'inscription - RESPONSABLE - re_path(r'^lastGuardian$', GuardianView.as_view(), name="lastGuardian"), - - # Envoi d'un dossier d'inscription - re_path(r'^send/(?P<_id>[0-9]+)$', views.send, name="send"), - - # Archivage d'un dossier d'inscription - re_path(r'^archive/(?P<_id>[0-9]+)$', views.archive, name="archive"), - - # Envoi d'une relance de dossier d'inscription - re_path(r'^sendRelance/(?P<_id>[0-9]+)$', views.relance, name="sendRelance"), - - # Page PARENT - Liste des children - re_path(r'^children/(?P<_id>[0-9]+)$', ChildrenListView.as_view(), name="children"), + re_path(r'^registerForms/(?P[0-9]+)/archive$', archive, name="archive"), + re_path(r'^registerForms/(?P[0-9]+)/resend$', resend, name="resend"), + re_path(r'^registerForms/(?P[0-9]+)/send$', send, name="send"), + re_path(r'^registerForms/(?P[0-9]+)$', RegisterFormWithIdView.as_view(), name="registerForm"), + re_path(r'^registerForms$', RegisterFormView.as_view(), name="registerForms"), # Page INSCRIPTION - Liste des élèves re_path(r'^students$', StudentListView.as_view(), name="students"), + # Page de formulaire d'inscription - ELEVE + re_path(r'^students/(?P[0-9]+)$', StudentView.as_view(), name="students"), + # Page PARENT - Liste des children + re_path(r'^children/(?P[0-9]+)$', ChildrenListView.as_view(), name="children"), + + # Page de formulaire d'inscription - RESPONSABLE + re_path(r'^lastGuardianId$', GuardianView.as_view(), name="lastGuardianId"), # modèles de fichiers d'inscription + re_path(r'^registrationFileTemplates/(?P[0-9]+)$', RegistrationFileTemplateSimpleView.as_view(), name="registrationFileTemplate"), re_path(r'^registrationFileTemplates$', RegistrationFileTemplateView.as_view(), name='registrationFileTemplates'), - re_path(r'^registrationFileTemplates/(?P<_id>[0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"), # fichiers d'inscription - re_path(r'^registrationFiles/(?P<_id>[0-9]+)$', RegistrationFileView.as_view(), name='registrationFiles'), - re_path(r'^registrationFiles', RegistrationFileView.as_view(), name="registrationFiles"), + re_path(r'^registrationFiles/(?P[0-9]+)$', RegistrationFileSimpleView.as_view(), name='registrationFiles'), + re_path(r'^registrationFiles$', RegistrationFileView.as_view(), name="registrationFiles"), + re_path(r'^registrationFileGroups/(?P[0-9]+)$', RegistrationFileGroupSimpleView.as_view(), name='registrationFileGroupDetail'), + re_path(r'^registrationFileGroups/(?P[0-9]+)/registrationFiles$', get_registration_files_by_group, name="get_registration_files_by_group"), + re_path(r'^registrationFileGroups$', RegistrationFileGroupView.as_view(), name='registrationFileGroups'), ] \ No newline at end of file diff --git a/Back-End/Subscriptions/views.py b/Back-End/Subscriptions/views.py deleted file mode 100644 index 4b48e4f..0000000 --- a/Back-End/Subscriptions/views.py +++ /dev/null @@ -1,450 +0,0 @@ -from django.http.response import JsonResponse -from django.contrib.auth import login, authenticate, get_user_model -from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect -from django.utils.decorators import method_decorator -from django.core.cache import cache -from django.core.paginator import Paginator -from django.core.files import File -from django.db.models import Q # Ajout de cet import -from rest_framework.parsers import JSONParser,MultiPartParser, FormParser -from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework import status -from drf_yasg.utils import swagger_auto_schema -from drf_yasg import openapi - -import json -from pathlib import Path -import os -from io import BytesIO - -import Subscriptions.mailManager as mailer -import Subscriptions.util as util - -from Subscriptions.automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine -from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer -from .pagination import CustomPagination -from .signals import clear_cache -from .models import Student, Guardian, RegistrationForm, RegistrationFileTemplate, RegistrationFile -from .automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine - -from Auth.models import Profile - -from N3wtSchool import settings, renderers, bdd - -class RegisterFormListView(APIView): - """ - Gère la liste des dossiers d’inscription, lecture et création. - """ - pagination_class = CustomPagination - - def get_register_form(self, _filter, search=None): - """ - Récupère les fiches d'inscriptions en fonction du filtre passé. - _filter: Filtre pour déterminer l'état des fiches ('pending', 'archived', 'subscribed') - search: Terme de recherche (optionnel) - """ - if _filter == 'pending': - exclude_states = [RegistrationForm.RegistrationFormStatus.RF_VALIDATED, RegistrationForm.RegistrationFormStatus.RF_ARCHIVED] - return bdd.searchObjects(RegistrationForm, search, _excludeStates=exclude_states) - elif _filter == 'archived': - return bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_ARCHIVED) - elif _filter == 'subscribed': - return bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_VALIDATED) - return None - - @swagger_auto_schema( - manual_parameters=[ - openapi.Parameter('_filter', openapi.IN_PATH, description="filtre", type=openapi.TYPE_STRING, enum=['pending', 'archived', 'subscribed'], required=True), - openapi.Parameter('search', openapi.IN_QUERY, description="search", type=openapi.TYPE_STRING, required=False), - openapi.Parameter('page_size', openapi.IN_QUERY, description="limite de page lors de la pagination", type=openapi.TYPE_INTEGER, required=False), - ], - responses={200: RegistrationFormSerializer(many=True)} - ) - def get(self, request, _filter): - """ - Récupère les fiches d'inscriptions en fonction du filtre passé. - """ - # Récupération des paramètres - search = request.GET.get('search', '').strip() - page_size = request.GET.get('page_size', None) - - # Gestion du page_size - if page_size is not None: - try: - page_size = int(page_size) - except ValueError: - page_size = settings.NB_RESULT_PER_PAGE - - # Définir le cache_key en fonction du filtre - page_number = request.GET.get('page', 1) - cache_key = f'N3WT_ficheInscriptions_{_filter}_page_{page_number}_search_{search if _filter == "pending" else ""}' - cached_page = cache.get(cache_key) - if cached_page: - return JsonResponse(cached_page, safe=False) - - # Récupérer les fiches d'inscriptions en fonction du filtre - registerForms_List = self.get_register_form(_filter, search) - - if not registerForms_List: - return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False) - - # Pagination - paginator = self.pagination_class() - page = paginator.paginate_queryset(registerForms_List, request) - if page is not None: - registerForms_serializer = RegistrationFormSerializer(page, many=True) - response_data = paginator.get_paginated_response(registerForms_serializer.data) - cache.set(cache_key, response_data, timeout=60*15) - return JsonResponse(response_data, safe=False) - - return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False) - - @swagger_auto_schema( - manual_parameters=[ - ], - responses={200: RegistrationFormSerializer(many=True)} - ) - def post(self, request): - studentFormList_serializer=JSONParser().parse(request) - for studentForm_data in studentFormList_serializer: - # Ajout de la date de mise à jour - studentForm_data["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - json.dumps(studentForm_data) - # Ajout du code d'inscription - code = util.genereRandomCode(12) - studentForm_data["codeLienInscription"] = code - studentForm_serializer = RegistrationFormSerializer(data=studentForm_data) - - if studentForm_serializer.is_valid(): - studentForm_serializer.save() - - return JsonResponse(studentForm_serializer.errors, safe=False) - - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class RegisterFormView(APIView): - """ - Gère la lecture, création, modification et suppression d’un dossier d’inscription. - """ - pagination_class = CustomPagination - - def get(self, request, _id): - """ - Récupère un dossier d'inscription donné. - """ - registerForm=bdd.getObject(RegistrationForm, "student__id", _id) - registerForm_serializer=RegistrationFormSerializer(registerForm) - return JsonResponse(registerForm_serializer.data, safe=False) - - def post(self, request): - """ - Crée un dossier d'inscription. - """ - studentForm_data=JSONParser().parse(request) - # Ajout de la date de mise à jour - studentForm_data["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - json.dumps(studentForm_data) - # Ajout du code d'inscription - code = util.genereRandomCode(12) - studentForm_data["codeLienInscription"] = code - - guardiansId = studentForm_data.pop('idGuardians', []) - studentForm_serializer = RegistrationFormSerializer(data=studentForm_data) - - if studentForm_serializer.is_valid(): - di = studentForm_serializer.save() - - # Mise à jour de l'automate - updateStateMachine(di, 'creationDI') - - # Récupération du reponsable associé - for guardianId in guardiansId: - guardian = Guardian.objects.get(id=guardianId) - di.student.guardians.add(guardian) - di.save() - - return JsonResponse(studentForm_serializer.data, safe=False) - - return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - - def put(self, request, _id): - """ - Modifie un dossier d'inscription donné. - """ - studentForm_data=JSONParser().parse(request) - _status = studentForm_data.pop('status', 0) - studentForm_data["last_update"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M')) - registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - - if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: - try: - # Génération de la fiche d'inscription au format PDF - base_dir = f"registration_files/dossier_rf_{registerForm.pk}" - os.makedirs(base_dir, exist_ok=True) - - # Fichier PDF initial - initial_pdf = f"{base_dir}/rf_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" - registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf) - registerForm.save() - - # Récupération des fichiers d'inscription - fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) - if registerForm.registration_file: - fileNames.insert(0, registerForm.registration_file.path) - - # Création du fichier PDF Fusionné - merged_pdf = f"{base_dir}/dossier_complet_{registerForm.pk}.pdf" - util.merge_files_pdf(fileNames, merged_pdf) - - # Mise à jour du champ registration_file avec le fichier fusionné - with open(merged_pdf, 'rb') as f: - registerForm.registration_file.save( - os.path.basename(merged_pdf), - File(f), - save=True - ) - - # Mise à jour de l'automate - updateStateMachine(registerForm, 'saisiDI') - except Exception as e: - return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: - # L'école a validé le dossier d'inscription - # Mise à jour de l'automate - updateStateMachine(registerForm, 'valideDI') - - - studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data) - if studentForm_serializer.is_valid(): - studentForm_serializer.save() - return JsonResponse(studentForm_serializer.data, safe=False) - - return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, id): - """ - Supprime un dossier d'inscription donné. - """ - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) - if register_form != None: - student = register_form.student - student.guardians.clear() - student.profiles.clear() - student.registration_files.clear() - student.delete() - clear_cache() - - return JsonResponse("La suppression du dossier a été effectuée avec succès", safe=False) - - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -class StudentView(APIView): - """ - Gère la lecture d’un élève donné. - """ - def get(self, request, _id): - student = bdd.getObject(_objectName=Student, _columnName='id', _value=_id) - if student is None: - return JsonResponse({"errorMessage":'Aucun élève trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - student_serializer = StudentSerializer(student) - return JsonResponse(student_serializer.data, safe=False) - -class GuardianView(APIView): - """ - Récupère le dernier ID de responsable légal créé. - """ - def get(self, request): - lastGuardian = bdd.getLastId(Guardian) - return JsonResponse({"lastid":lastGuardian}, safe=False) - -def send(request, _id): - """ - Envoie le dossier d’inscription par e-mail. - """ - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - if register_form != None: - student = register_form.student - guardian = student.getMainGuardian() - email = guardian.email - errorMessage = mailer.sendRegisterForm(email) - if errorMessage == '': - register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - # Mise à jour de l'automate - updateStateMachine(register_form, 'envoiDI') - return JsonResponse({"message": f"Le dossier d'inscription a bien été envoyé à l'addresse {email}"}, safe=False) - - return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) - - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -def archive(request, _id): - """ - Archive le dossier d’inscription visé. - """ - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - if register_form != None: - register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - # Mise à jour de l'automate - updateStateMachine(register_form, 'archiveDI') - - return JsonResponse({"errorMessage":''}, safe=False, status=status.HTTP_400_BAD_REQUEST) - - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -def relance(request, _id): - """ - Relance un dossier d’inscription par e-mail. - """ - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - if register_form != None: - student = register_form.student - guardian = student.getMainGuardian() - email = guardian.email - errorMessage = mailer.envoieRelanceDossierInscription(email, register_form.codeLienInscription) - if errorMessage == '': - register_form.status=RegistrationForm.RegistrationFormStatus.RF_SENT - register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - register_form.save() - - return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) - - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -# API utilisée pour la vue parent -class ChildrenListView(APIView): - """ - Pour la vue parent : liste les élèves rattachés à un profil donné. - """ - # Récupération des élèves d'un parent - # idProfile : identifiant du profil connecté rattaché aux fiches d'élèves - def get(self, request, _id): - students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__associated_profile__id', _value=_id) - students_serializer = RegistrationFormByParentSerializer(students, many=True) - return JsonResponse(students_serializer.data, safe=False) - -# API utilisée pour la vue de création d'un DI -class StudentListView(APIView): - """ - Pour la vue de création d’un dossier d’inscription : liste les élèves disponibles. - """ - # Récupération de la liste des élèves inscrits ou en cours d'inscriptions - def get(self, request): - students = bdd.getAllObjects(_objectName=Student) - students_serializer = StudentByRFCreationSerializer(students, many=True) - return JsonResponse(students_serializer.data, safe=False) - -class RegistrationFileTemplateView(APIView): - """ - Gère les fichiers templates pour les dossiers d’inscription. - """ - parser_classes = (MultiPartParser, FormParser) - - def get(self, request, _id=None): - """ - Récupère les fichiers templates pour les dossiers d’inscription. - """ - if _id is None: - files = RegistrationFileTemplate.objects.all() - serializer = RegistrationFileTemplateSerializer(files, many=True) - return Response(serializer.data) - else : - registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) - if registationFileTemplate is None: - return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationFileTemplateSerializer(registationFileTemplate) - return JsonResponse(serializer.data, safe=False) - - def put(self, request, _id): - """ - Met à jour un fichier template existant. - """ - registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) - if registationFileTemplate is None: - return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationFileTemplateSerializer(registationFileTemplate,data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def post(self, request): - """ - Crée un fichier template pour les dossiers d’inscription. - """ - serializer = RegistrationFileTemplateSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, _id): - """ - Supprime un fichier template existant. - """ - registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) - if registrationFileTemplate is not None: - registrationFileTemplate.file.delete() # Supprimer le fichier uploadé - registrationFileTemplate.delete() - return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) - else: - return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -class RegistrationFileView(APIView): - """ - Gère la création, mise à jour et suppression de fichiers liés à un dossier d’inscription. - """ - parser_classes = (MultiPartParser, FormParser) - - def get(self, request, _id=None): - """ - Récupère les fichiers liés à un dossier d’inscription donné. - """ - if (_id is None): - files = RegistrationFile.objects.all() - serializer = RegistrationFileSerializer(files, many=True) - return Response(serializer.data) - else: - registationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id) - if registationFile is None: - return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationFileSerializer(registationFile) - return JsonResponse(serializer.data, safe=False) - - def post(self, request): - """ - Crée un RegistrationFile pour le RegistrationForm associé. - """ - serializer = RegistrationFileSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - - def put(self, request, fileId): - """ - Met à jour un RegistrationFile existant. - """ - registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=fileId) - if registrationFile is None: - return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationFileSerializer(registrationFile, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response({'message': 'Fichier mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, _id): - """ - Supprime un RegistrationFile existant. - """ - registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id) - if registrationFile is not None: - registrationFile.file.delete() # Supprimer le fichier uploadé - registrationFile.delete() - return JsonResponse({'message': 'La suppression du fichier a été effectuée avec succès'}, safe=False) - else: - return JsonResponse({'erreur': 'Le fichier n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - diff --git a/Back-End/Subscriptions/views/__init__.py b/Back-End/Subscriptions/views/__init__.py new file mode 100644 index 0000000..755cfd8 --- /dev/null +++ b/Back-End/Subscriptions/views/__init__.py @@ -0,0 +1,24 @@ +from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive +from .registration_file_views import RegistrationFileTemplateView, RegistrationFileTemplateSimpleView, RegistrationFileView, RegistrationFileSimpleView +from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group +from .student_views import StudentView, StudentListView, ChildrenListView +from .guardian_views import GuardianView + +__all__ = [ + 'RegisterFormView', + 'RegisterFormWithIdView', + 'send', + 'resend', + 'archive', + 'RegistrationFileView', + 'RegistrationFileSimpleView', + 'RegistrationFileTemplateView', + 'RegistrationFileTemplateSimpleView', + 'RegistrationFileGroupView', + 'RegistrationFileGroupSimpleView', + 'get_registration_files_by_group', + 'StudentView', + 'StudentListView', + 'ChildrenListView', + 'GuardianView', +] diff --git a/Back-End/Subscriptions/views/guardian_views.py b/Back-End/Subscriptions/views/guardian_views.py new file mode 100644 index 0000000..13e0912 --- /dev/null +++ b/Back-End/Subscriptions/views/guardian_views.py @@ -0,0 +1,34 @@ +from django.http.response import JsonResponse +from rest_framework.views import APIView +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from Subscriptions.models import Guardian +from N3wtSchool import bdd + +class GuardianView(APIView): + """ + Gestion des responsables légaux. + """ + + @swagger_auto_schema( + operation_description="Récupère le dernier ID de responsable légal créé", + operation_summary="Récupèrer le dernier ID de responsable légal créé", + responses={ + 200: openapi.Response( + description="Dernier ID du responsable légal", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'lastid': openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Dernier ID créé" + ) + } + ) + ) + } + ) + def get(self, request): + lastGuardian = bdd.getLastId(Guardian) + return JsonResponse({"lastid":lastGuardian}, safe=False) diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py new file mode 100644 index 0000000..1464ff4 --- /dev/null +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -0,0 +1,385 @@ +from django.http.response import JsonResponse +from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect +from django.utils.decorators import method_decorator +from django.core.cache import cache +from rest_framework.parsers import JSONParser +from rest_framework.views import APIView +from rest_framework.decorators import action, api_view +from rest_framework import status +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +import json +import os +from django.core.files import File + +import Subscriptions.mailManager as mailer +import Subscriptions.util as util + +from Subscriptions.serializers import RegistrationFormSerializer +from Subscriptions.pagination import CustomPagination +from Subscriptions.signals import clear_cache +from Subscriptions.models import Student, Guardian, RegistrationForm, RegistrationFile, RegistrationFileGroup +from Subscriptions.automate import updateStateMachine + +from N3wtSchool import settings, bdd + +import logging +logger = logging.getLogger(__name__) + +# /Subscriptions/registerForms +class RegisterFormView(APIView): + """ + Gère la liste des dossiers d’inscription, lecture et création. + """ + pagination_class = CustomPagination + + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter('filter', openapi.IN_QUERY, description="filtre", type=openapi.TYPE_STRING, enum=['pending', 'archived', 'subscribed'], required=True), + openapi.Parameter('search', openapi.IN_QUERY, description="search", type=openapi.TYPE_STRING, required=False), + openapi.Parameter('page_size', openapi.IN_QUERY, description="limite de page lors de la pagination", type=openapi.TYPE_INTEGER, required=False), + ], + responses={200: RegistrationFormSerializer(many=True)}, + operation_description="Récupère les dossier d'inscriptions en fonction du filtre passé.", + operation_summary="Récupérer les dossier d'inscriptions", + examples={ + "application/json": [ + { + "id": 1, + "student": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "2010-01-01" + }, + "status": "pending", + "last_update": "10-02-2025 10:00" + }, + { + "id": 2, + "student": { + "id": 2, + "first_name": "Jane", + "last_name": "Doe", + "date_of_birth": "2011-02-02" + }, + "status": "archived", + "last_update": "09-02-2025 09:00" + } + ] + } + ) + def get(self, request): + """ + Récupère les fiches d'inscriptions en fonction du filtre passé. + """ + # Récupération des paramètres + filter = request.GET.get('filter', '').strip() + search = request.GET.get('search', '').strip() + page_size = request.GET.get('page_size', None) + + # Gestion du page_size + if page_size is not None: + try: + page_size = int(page_size) + except ValueError: + page_size = settings.NB_RESULT_PER_PAGE + + # Définir le cache_key en fonction du filtre + page_number = request.GET.get('page', 1) + cache_key = f'N3WT_ficheInscriptions_{filter}_page_{page_number}_search_{search if filter == "pending" else ""}' + cached_page = cache.get(cache_key) + if cached_page: + return JsonResponse(cached_page, safe=False) + + # Récupérer les dossier d'inscriptions en fonction du filtre + registerForms_List = None + if filter == 'pending': + exclude_states = [RegistrationForm.RegistrationFormStatus.RF_VALIDATED, RegistrationForm.RegistrationFormStatus.RF_ARCHIVED] + registerForms_List = bdd.searchObjects(RegistrationForm, search, _excludeStates=exclude_states) + elif filter == 'archived': + registerForms_List = bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_ARCHIVED) + elif filter == 'subscribed': + registerForms_List = bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_VALIDATED) + else: + registerForms_List = None + + if not registerForms_List: + return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False) + + # Pagination + paginator = self.pagination_class() + page = paginator.paginate_queryset(registerForms_List, request) + if page is not None: + registerForms_serializer = RegistrationFormSerializer(page, many=True) + response_data = paginator.get_paginated_response(registerForms_serializer.data) + cache.set(cache_key, response_data, timeout=60 * 15) + return JsonResponse(response_data, safe=False) + + return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False) + + @swagger_auto_schema( + request_body=RegistrationFormSerializer, + responses={200: RegistrationFormSerializer()}, + operation_description="Crée un dossier d'inscription.", + operation_summary="Créer un dossier d'inscription", + examples={ + "application/json": { + "student": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "2010-01-01" + }, + "status": "pending", + "last_update": "10-02-2025 10:00", + "codeLienInscription": "ABC123XYZ456" + } + } + ) + @method_decorator(csrf_protect, name='dispatch') + @method_decorator(ensure_csrf_cookie, name='dispatch') + def post(self, request): + """ + Crée un dossier d'inscription. + """ + regiterFormData = request.data.copy() + logger.info(f"Création d'un dossier d'inscription {request}") + # Ajout de la date de mise à jour + regiterFormData["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + # Ajout du code d'inscription + code = util.genereRandomCode(12) + regiterFormData["codeLienInscription"] = code + + guardiansId = regiterFormData.pop('idGuardians', []) + registerForm_serializer = RegistrationFormSerializer(data=regiterFormData) + fileGroupId = regiterFormData.pop('fileGroup', None) + + if registerForm_serializer.is_valid(): + di = registerForm_serializer.save() + + # Mise à jour de l'automate + updateStateMachine(di, 'creationDI') + + # Récupération du reponsable associé + for guardianId in guardiansId: + guardian = Guardian.objects.get(id=guardianId) + di.student.guardians.add(guardian) + di.save() + if fileGroupId: + di.fileGroup = RegistrationFileGroup.objects.get(id=fileGroupId) + di.save() + + return JsonResponse(registerForm_serializer.data, safe=False) + else: + logger.error(f"Erreur lors de la validation des données {regiterFormData}") + + return JsonResponse(registerForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + +# /Subscriptions/registerForms/{id} +class RegisterFormWithIdView(APIView): + """ + Gère la lecture, création, modification et suppression d’un dossier d’inscription. + """ + pagination_class = CustomPagination + + @swagger_auto_schema( + responses={200: RegistrationFormSerializer()}, + operation_description="Récupère un dossier d'inscription donné.", + operation_summary="Récupérer un dossier d'inscription", + examples={ + "application/json": { + "id": 1, + "student": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "2010-01-01" + }, + } + } + ) + def get(self, request, id): + """ + Récupère un dossier d'inscription donné. + """ + registerForm = bdd.getObject(RegistrationForm, "student__id", id) + if registerForm is None: + return JsonResponse({"errorMessage":'Le dossier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + registerForm_serializer = RegistrationFormSerializer(registerForm) + return JsonResponse(registerForm_serializer.data, safe=False) + + @swagger_auto_schema( + request_body=RegistrationFormSerializer, + responses={200: RegistrationFormSerializer()}, + operation_description="Modifie un dossier d'inscription donné.", + operation_summary="Modifier un dossier d'inscription", + examples={ + "application/json": { + "id": 1, + "student": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "2010-01-01" + }, + "status": "under_review", + "last_update": "10-02-2025 10:00" + } + } + ) + @method_decorator(csrf_protect, name='dispatch') + @method_decorator(ensure_csrf_cookie, name='dispatch') + def put(self, request, id): + """ + Modifie un dossier d'inscription donné. + """ + studentForm_data = JSONParser().parse(request) + _status = studentForm_data.pop('status', 0) + studentForm_data["last_update"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M')) + registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + + if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: + try: + # Génération de la fiche d'inscription au format PDF + base_dir = f"data/registration_files/dossier_rf_{registerForm.pk}" + os.makedirs(base_dir, exist_ok=True) + + # Fichier PDF initial + initial_pdf = f"{base_dir}/rf_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" + registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf) + registerForm.save() + + # Récupération des fichiers d'inscription + fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) + if registerForm.registration_file: + fileNames.insert(0, registerForm.registration_file.path) + + # Création du fichier PDF Fusionné + merged_pdf = f"{base_dir}/dossier_complet_{registerForm.pk}.pdf" + util.merge_files_pdf(fileNames, merged_pdf) + + # Mise à jour du champ registration_file avec le fichier fusionné + with open(merged_pdf, 'rb') as f: + registerForm.registration_file.save( + os.path.basename(merged_pdf), + File(f), + save=True + ) + + # Mise à jour de l'automate + updateStateMachine(registerForm, 'saisiDI') + except Exception as e: + return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: + # L'école a validé le dossier d'inscription + # Mise à jour de l'automate + updateStateMachine(registerForm, 'valideDI') + + studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data) + if studentForm_serializer.is_valid(): + studentForm_serializer.save() + return JsonResponse(studentForm_serializer.data, safe=False) + + return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + responses={204: 'No Content'}, + operation_description="Supprime un dossier d'inscription donné.", + operation_summary="Supprimer un dossier d'inscription" + ) + @method_decorator(csrf_protect, name='dispatch') + @method_decorator(ensure_csrf_cookie, name='dispatch') + def delete(self, request, id): + """ + Supprime un dossier d'inscription donné. + """ + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + if register_form != None: + student = register_form.student + student.guardians.clear() + student.profiles.clear() + student.registration_files.clear() + student.delete() + clear_cache() + + return JsonResponse("La suppression du dossier a été effectuée avec succès", safe=False) + + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +@swagger_auto_schema( + method='get', + responses={200: openapi.Response('Success', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'message': openapi.Schema(type=openapi.TYPE_STRING) + } + ))}, + operation_description="Envoie le dossier d'inscription par e-mail", + operation_summary="Envoyer un dossier d'inscription" +) +@api_view(['GET']) +def send(request,id): + """Envoie le dossier d'inscription par e-mail.""" + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + if register_form != None: + student = register_form.student + guardian = student.getMainGuardian() + email = guardian.email + errorMessage = mailer.sendRegisterForm(email) + if errorMessage == '': + register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + updateStateMachine(register_form, 'envoiDI') + return JsonResponse({"message": f"Le dossier d'inscription a bien été envoyé à l'addresse {email}"}, safe=False) + return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) + return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +@swagger_auto_schema( + method='get', + responses={200: openapi.Response('Success', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'message': openapi.Schema(type=openapi.TYPE_STRING) + } + ))}, + operation_description="Archive le dossier d'inscription", + operation_summary="Archiver un dossier d'inscription" +) +@api_view(['GET']) +def archive(request,id): + """Archive le dossier d'inscription.""" + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + if register_form != None: + register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + updateStateMachine(register_form, 'archiveDI') + return JsonResponse({"message": "Le dossier a été archivé avec succès"}, safe=False) + return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +@swagger_auto_schema( + method='get', + responses={200: openapi.Response('Success', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'message': openapi.Schema(type=openapi.TYPE_STRING) + } + ))}, + operation_description="Relance un dossier d'inscription par e-mail", + operation_summary="Relancer un dossier d'inscription" +) +@api_view(['GET']) +def resend(request,id): + """Relance un dossier d'inscription par e-mail.""" + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + if register_form != None: + student = register_form.student + guardian = student.getMainGuardian() + email = guardian.email + errorMessage = mailer.envoieRelanceDossierInscription(email, register_form.codeLienInscription) + if errorMessage == '': + register_form.status=RegistrationForm.RegistrationFormStatus.RF_SENT + register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + register_form.save() + return JsonResponse({"message": f"Le dossier a été renvoyé à l'adresse {email}"}, safe=False) + return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) + return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Back-End/Subscriptions/views/registration_file_group_views.py b/Back-End/Subscriptions/views/registration_file_group_views.py new file mode 100644 index 0000000..b71dfc5 --- /dev/null +++ b/Back-End/Subscriptions/views/registration_file_group_views.py @@ -0,0 +1,125 @@ +from django.http.response import JsonResponse +from drf_yasg.utils import swagger_auto_schema +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status +from rest_framework.decorators import action, api_view +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from Subscriptions.serializers import RegistrationFileGroupSerializer +from Subscriptions.models import RegistrationFileGroup, RegistrationFileTemplate +from N3wtSchool import bdd + +class RegistrationFileGroupView(APIView): + @swagger_auto_schema( + operation_description="Récupère tous les groupes de fichiers d'inscription", + responses={200: RegistrationFileGroupSerializer(many=True)} + ) + def get(self, request): + """ + Récupère tous les groupes de fichiers d'inscription. + """ + groups = RegistrationFileGroup.objects.all() + serializer = RegistrationFileGroupSerializer(groups, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Crée un nouveau groupe de fichiers d'inscription", + request_body=RegistrationFileGroupSerializer, + responses={ + 201: RegistrationFileGroupSerializer, + 400: "Données invalides" + } + ) + def post(self, request): + """ + Crée un nouveau groupe de fichiers d'inscription. + """ + serializer = RegistrationFileGroupSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class RegistrationFileGroupSimpleView(APIView): + @swagger_auto_schema( + operation_description="Récupère un groupe de fichiers d'inscription spécifique", + responses={ + 200: RegistrationFileGroupSerializer, + 404: "Groupe non trouvé" + } + ) + def get(self, request, id): + """ + Récupère un groupe de fichiers d'inscription spécifique. + """ + group = bdd.getObject(_objectName=RegistrationFileGroup, _columnName='id', _value=id) + if group is None: + return JsonResponse({"errorMessage": "Le groupe de fichiers n'a pas été trouvé"}, + status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileGroupSerializer(group) + return JsonResponse(serializer.data) + + @swagger_auto_schema( + operation_description="Met à jour un groupe de fichiers d'inscription", + request_body=RegistrationFileGroupSerializer, + responses={ + 200: RegistrationFileGroupSerializer, + 400: "Données invalides", + 404: "Groupe non trouvé" + } + ) + def put(self, request, id): + """ + Met à jour un groupe de fichiers d'inscription existant. + """ + group = bdd.getObject(_objectName=RegistrationFileGroup, _columnName='id', _value=id) + if group is None: + return JsonResponse({'erreur': "Le groupe de fichiers n'a pas été trouvé"}, + status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileGroupSerializer(group, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_description="Supprime un groupe de fichiers d'inscription", + responses={ + 204: "Suppression réussie", + 404: "Groupe non trouvé" + } + ) + def delete(self, request, id): + """ + Supprime un groupe de fichiers d'inscription. + """ + group = bdd.getObject(_objectName=RegistrationFileGroup, _columnName='id', _value=id) + if group is not None: + group.delete() + return JsonResponse({'message': 'La suppression du groupe a été effectuée avec succès'}, + status=status.HTTP_204_NO_CONTENT) + return JsonResponse({'erreur': "Le groupe de fichiers n'a pas été trouvé"}, + status=status.HTTP_404_NOT_FOUND) + +@swagger_auto_schema( + method='get', + responses={200: openapi.Response('Success', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'message': openapi.Schema(type=openapi.TYPE_STRING) + } + ))}, + operation_description="Récupère les fichiers d'inscription d'un groupe donné", + operation_summary="Récupèrer les fichiers d'inscription d'un groupe donné" +) +@api_view(['GET']) +def get_registration_files_by_group(request, id): + try: + group = RegistrationFileGroup.objects.get(id=id) + templates = RegistrationFileTemplate.objects.filter(group=group) + templates_data = list(templates.values()) + return JsonResponse(templates_data, safe=False) + except RegistrationFileGroup.DoesNotExist: + return JsonResponse({'error': 'Le groupe de fichiers n\'a pas été trouvé'}, status=404) \ No newline at end of file diff --git a/Back-End/Subscriptions/views/registration_file_views.py b/Back-End/Subscriptions/views/registration_file_views.py new file mode 100644 index 0000000..e115b03 --- /dev/null +++ b/Back-End/Subscriptions/views/registration_file_views.py @@ -0,0 +1,211 @@ +from django.http.response import JsonResponse +from django.core.files import File +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from rest_framework.parsers import MultiPartParser, FormParser +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +import os + +from Subscriptions.serializers import RegistrationFileTemplateSerializer, RegistrationFileSerializer +from Subscriptions.models import RegistrationFileTemplate, RegistrationFile +from N3wtSchool import bdd + + +class RegistrationFileTemplateView(APIView): + @swagger_auto_schema( + operation_description="Récupère tous les fichiers templates pour les dossiers d'inscription", + responses={200: RegistrationFileTemplateSerializer(many=True)} + ) + def get(self, request): + """ + Récupère les fichiers templates pour les dossiers d’inscription. + """ + files = RegistrationFileTemplate.objects.all() + serializer = RegistrationFileTemplateSerializer(files, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Crée un nouveau fichier template pour les dossiers d'inscription", + request_body=RegistrationFileTemplateSerializer, + responses={ + 201: RegistrationFileTemplateSerializer, + 400: "Données invalides" + } + ) + def post(self, request): + """ + Crée un fichier template pour les dossiers d’inscription. + """ + serializer = RegistrationFileTemplateSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class RegistrationFileTemplateSimpleView(APIView): + """ + Gère les fichiers templates pour les dossiers d’inscription. + """ + parser_classes = (MultiPartParser, FormParser) + + @swagger_auto_schema( + operation_description="Récupère un fichier template spécifique", + responses={ + 200: RegistrationFileTemplateSerializer, + 404: "Fichier template non trouvé" + } + ) + def get(self, request, id): + """ + Récupère les fichiers templates pour les dossiers d’inscription. + """ + registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=id) + if registationFileTemplate is None: + return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileTemplateSerializer(registationFileTemplate) + return JsonResponse(serializer.data, safe=False) + + @swagger_auto_schema( + operation_description="Met à jour un fichier template existant", + request_body=RegistrationFileTemplateSerializer, + responses={ + 201: RegistrationFileTemplateSerializer, + 400: "Données invalides", + 404: "Fichier template non trouvé" + } + ) + def put(self, request, id): + """ + Met à jour un fichier template existant. + """ + registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=id) + if registationFileTemplate is None: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileTemplateSerializer(registationFileTemplate,data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_description="Supprime un fichier template", + responses={ + 204: "Suppression réussie", + 404: "Fichier template non trouvé" + } + ) + def delete(self, request, id): + """ + Supprime un fichier template existant. + """ + registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=id) + if registrationFileTemplate is not None: + registrationFileTemplate.file.delete() # Supprimer le fichier uploadé + registrationFileTemplate.delete() + return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) + else: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +class RegistrationFileView(APIView): + @swagger_auto_schema( + operation_description="Récupère tous les fichiers d'inscription", + responses={200: RegistrationFileSerializer(many=True)} + ) + def get(self, request): + """ + Récupère les fichiers liés à un dossier d’inscription donné. + """ + files = RegistrationFile.objects.all() + serializer = RegistrationFileSerializer(files, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Crée un nouveau fichier d'inscription", + request_body=RegistrationFileSerializer, + responses={ + 201: RegistrationFileSerializer, + 400: "Données invalides" + } + ) + def post(self, request): + """ + Crée un RegistrationFile pour le RegistrationForm associé. + """ + serializer = RegistrationFileSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + +class RegistrationFileSimpleView(APIView): + """ + Gère la création, mise à jour et suppression de fichiers liés à un dossier d’inscription. + """ + parser_classes = (MultiPartParser, FormParser) + + @swagger_auto_schema( + operation_description="Récupère un fichier d'inscription spécifique", + responses={ + 200: RegistrationFileSerializer, + 404: "Fichier non trouvé" + } + ) + def get(self, request, id): + """ + Récupère les fichiers liés à un dossier d’inscription donné. + """ + registationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=id) + if registationFile is None: + return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileSerializer(registationFile) + return JsonResponse(serializer.data, safe=False) + + @swagger_auto_schema( + operation_description="Met à jour un fichier d'inscription existant", + request_body=RegistrationFileSerializer, + responses={ + 200: openapi.Response( + description="Fichier mis à jour avec succès", + schema=RegistrationFileSerializer + ), + 400: "Données invalides", + 404: "Fichier non trouvé" + } + ) + def put(self, request, id): + """ + Met à jour un RegistrationFile existant. + """ + registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=id) + if registrationFile is None: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileSerializer(registrationFile, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response({'message': 'Fichier mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_description="Supprime un fichier d'inscription", + responses={ + 200: "Suppression réussie", + 404: "Fichier non trouvé" + } + ) + def delete(self, request, id): + """ + Supprime un RegistrationFile existant. + """ + registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=id) + if registrationFile is not None: + registrationFile.file.delete() # Supprimer le fichier uploadé + registrationFile.delete() + return JsonResponse({'message': 'La suppression du fichier a été effectuée avec succès'}, safe=False) + else: + return JsonResponse({'erreur': 'Le fichier n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Back-End/Subscriptions/views/student_views.py b/Back-End/Subscriptions/views/student_views.py new file mode 100644 index 0000000..a84a65a --- /dev/null +++ b/Back-End/Subscriptions/views/student_views.py @@ -0,0 +1,83 @@ +from django.http.response import JsonResponse +from rest_framework.views import APIView +from rest_framework import status +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from Subscriptions.serializers import StudentByRFCreationSerializer, RegistrationFormByParentSerializer, StudentSerializer +from Subscriptions.models import Student, RegistrationForm + +from N3wtSchool import bdd + +class StudentView(APIView): + """ + Gère la lecture d’un élève donné. + """ + @swagger_auto_schema( + operation_summary="Récupérer les informations d'un élève", + operation_description="Retourne les détails d'un élève spécifique à partir de son ID", + responses={ + 200: openapi.Response('Détails de l\'élève', StudentSerializer), + 404: openapi.Response('Élève non trouvé') + }, + manual_parameters=[ + openapi.Parameter( + 'id', openapi.IN_PATH, + description="ID de l'élève", + type=openapi.TYPE_INTEGER, + required=True + ) + ] + ) + def get(self, request, id): + student = bdd.getObject(_objectName=Student, _columnName='id', _value=id) + if student is None: + return JsonResponse({"errorMessage":'Aucun élève trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + student_serializer = StudentSerializer(student) + return JsonResponse(student_serializer.data, safe=False) + +# API utilisée pour la vue de création d'un DI +class StudentListView(APIView): + """ + Pour la vue de création d’un dossier d’inscription : liste les élèves disponibles. + """ + @swagger_auto_schema( + operation_summary="Lister tous les élèves", + operation_description="Retourne la liste de tous les élèves inscrits ou en cours d'inscription", + responses={ + 200: openapi.Response('Liste des élèves', StudentByRFCreationSerializer(many=True)) + } + ) + # Récupération de la liste des élèves inscrits ou en cours d'inscriptions + def get(self, request): + students = bdd.getAllObjects(_objectName=Student) + students_serializer = StudentByRFCreationSerializer(students, many=True) + return JsonResponse(students_serializer.data, safe=False) + + +# API utilisée pour la vue parent +class ChildrenListView(APIView): + """ + Pour la vue parent : liste les élèves rattachés à un profil donné. + """ + @swagger_auto_schema( + operation_summary="Lister les élèves d'un parent", + operation_description="Retourne la liste des élèves associés à un profil parent spécifique", + responses={ + 200: openapi.Response('Liste des élèves du parent', RegistrationFormByParentSerializer(many=True)) + }, + manual_parameters=[ + openapi.Parameter( + 'id', openapi.IN_PATH, + description="ID du profil parent", + type=openapi.TYPE_INTEGER, + required=True + ) + ] + ) + # Récupération des élèves d'un parent + # idProfile : identifiant du profil connecté rattaché aux fiches d'élèves + def get(self, request, id): + students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__associated_profile__id', _value=id) + students_serializer = RegistrationFormByParentSerializer(students, many=True) + return JsonResponse(students_serializer.data, safe=False) diff --git a/Back-End/src/app.js b/Back-End/src/app.js new file mode 100644 index 0000000..e69de29 diff --git a/Back-End/src/middleware/cors.js b/Back-End/src/middleware/cors.js new file mode 100644 index 0000000..e69de29 diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 46ca74e..51a7722 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -6,27 +6,35 @@ import FeesManagement from '@/components/Structure/Tarification/FeesManagement'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; -import { createDatas, +import { createDatas, updateDatas, removeDatas, - fetchSpecialities, - fetchTeachers, - fetchClasses, - fetchSchedules, - fetchRegistrationDiscounts, - fetchTuitionDiscounts, - fetchRegistrationFees, - fetchTuitionFees } from '@/app/lib/schoolAction'; + fetchSpecialities, + fetchTeachers, + fetchClasses, + fetchSchedules, + fetchRegistrationDiscounts, + fetchTuitionDiscounts, + fetchRegistrationFees, + fetchTuitionFees, + } from '@/app/lib/schoolAction'; import SidebarTabs from '@/components/SidebarTabs'; +import FilesManagement from '@/components/Structure/Files/FilesManagement'; + +import { fetchRegisterFormFileTemplate } from '@/app/lib/subscriptionAction'; + + export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); + const [schedules, setSchedules] = useState([]); // Add this line const [registrationDiscounts, setRegistrationDiscounts] = useState([]); const [tuitionDiscounts, setTuitionDiscounts] = useState([]); const [registrationFees, setRegistrationFees] = useState([]); const [tuitionFees, setTuitionFees] = useState([]); + const [fichiers, setFichiers] = useState([]); const csrfToken = useCsrfToken(); @@ -42,18 +50,27 @@ export default function Page() { // Fetch data for schedules handleSchedules(); - + // Fetch data for registration discounts handleRegistrationDiscounts(); - + // Fetch data for tuition discounts handleTuitionDiscounts(); - + // Fetch data for registration fees handleRegistrationFees(); - + // Fetch data for tuition fees handleTuitionFees(); + + // Fetch data for registration file templates + fetchRegisterFormFileTemplate() + .then((data)=> { + setFichiers(data) + }) + .catch(error => console.error('Error fetching files:', error)); + + }, []); const handleSpecialities = () => { @@ -96,7 +113,7 @@ export default function Page() { .catch(error => console.error('Error fetching registration discounts:', error)); }; - const handleTuitionDiscounts = () => { + const handleTuitionDiscounts = () => { fetchTuitionDiscounts() .then(data => { setTuitionDiscounts(data); @@ -224,6 +241,11 @@ export default function Page() { handleDelete={handleDelete} /> ) + }, + { + id: 'Files', + label: 'Documents d\'inscription', + content: } ]; diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 9de4858..9c09fca 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -11,11 +11,10 @@ import Loader from '@/components/Loader'; import AlertWithModal from '@/components/AlertWithModal'; import DropdownMenu from "@/components/DropdownMenu"; import { formatPhoneNumber } from '@/utils/Telephone'; -import { MoreVertical, Send, Edit, Trash2, FileText, CheckCircle, Plus, Download } from 'lucide-react'; +import { MoreVertical, Send, Edit, Trash2, FileText, CheckCircle, Plus } from 'lucide-react'; import Modal from '@/components/Modal'; import InscriptionForm from '@/components/Inscription/InscriptionForm' import AffectationClasseForm from '@/components/AffectationClasseForm' -import FileUpload from './components/FileUpload'; import { PENDING, @@ -26,17 +25,14 @@ import { sendRegisterForm, archiveRegisterForm, fetchRegisterFormFileTemplate, - deleteRegisterFormFileTemplate, - createRegistrationFormFileTemplate, - editRegistrationFormFileTemplate, fetchStudents, editRegisterForm } from "@/app/lib/subscriptionAction" -import { +import { fetchClasses, - fetchRegistrationDiscounts, - fetchTuitionDiscounts, - fetchRegistrationFees, + fetchRegistrationDiscounts, + fetchTuitionDiscounts, + fetchRegistrationFees, fetchTuitionFees } from '@/app/lib/schoolAction'; import { createProfile } from '@/app/lib/authAction'; @@ -47,7 +43,7 @@ import { import DjangoCSRFToken from '@/components/DjangoCSRFToken' import useCsrfToken from '@/hooks/useCsrfToken'; -import { formatDate } from '@/utils/Date'; +import { fetchRegistrationFileGroups } from '@/app/lib/registerFileGroupAction'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; @@ -77,14 +73,13 @@ export default function Page({ params: { locale } }) { const [classes, setClasses] = useState([]); const [students, setEleves] = useState([]); const [reloadFetch, setReloadFetch] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [fileToEdit, setFileToEdit] = useState(null); + const [registrationDiscounts, setRegistrationDiscounts] = useState([]); const [tuitionDiscounts, setTuitionDiscounts] = useState([]); const [registrationFees, setRegistrationFees] = useState([]); const [tuitionFees, setTuitionFees] = useState([]); + const [groups, setGroups] = useState([]); const csrfToken = useCsrfToken(); @@ -228,6 +223,11 @@ const registerFormArchivedDataHandler = (data) => { setTuitionFees(data); }) .catch(requestErrorHandler); + fetchRegistrationFileGroups() + .then(data => { + setGroups(data); + }) + .catch(error => console.error('Error fetching file groups:', error)); } else { setTimeout(() => { setRegistrationFormsDataPending(mockFicheInscription); @@ -357,7 +357,7 @@ useEffect(()=>{ const selectedRegistrationDiscountsIds = updatedData.selectedRegistrationDiscounts.map(discountId => discountId) const selectedTuitionFeesIds = updatedData.selectedTuitionFees.map(feeId => feeId) const selectedTuitionDiscountsIds = updatedData.selectedTuitionDiscounts.map(discountId => discountId) - + const selectedFileGroup = updatedData.selectedFileGroup const allFeesIds = [...selectedRegistrationFeesIds, ...selectedTuitionFeesIds]; const allDiscountsds = [...selectedRegistrationDiscountsIds, ...selectedTuitionDiscountsIds]; @@ -370,7 +370,8 @@ useEffect(()=>{ }, idGuardians: selectedGuardiansIds, fees: allFeesIds, - discounts: allDiscountsds + discounts: allDiscountsds, + fileGroup: selectedFileGroup }; createRegisterForm(data, csrfToken) @@ -567,89 +568,23 @@ const columnsSubscribed = [ ]; -const handleFileDelete = (fileId) => { - deleteRegisterFormFileTemplate(fileId,csrfToken) - .then(response => { - if (response.ok) { - setFichiers(fichiers.filter(fichier => fichier.id !== fileId)); - alert('Fichier supprimé avec succès.'); - } else { - alert('Erreur lors de la suppression du fichier.'); - } - }) - .catch(error => { - console.error('Error deleting file:', error); - alert('Erreur lors de la suppression du fichier.'); - }); -}; - -const handleFileEdit = (file) => { - setIsEditing(true); - setFileToEdit(file); - setIsModalOpen(true); -}; - -const columnsFiles = [ - { name: 'Nom du fichier', transform: (row) => row.name }, - { name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") }, - { name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' }, - { name: 'Ordre de fusion', transform: (row) => row.order }, - { name: 'Actions', transform: (row) => ( -
- { - row.file && ( - - - ) - } - - -
- ) }, -]; - -const handleFileUpload = ({file, name, is_required, order}) => { - if (!name) { - alert('Veuillez entrer un nom de fichier.'); - return; - } - - const formData = new FormData(); - if(file){ - formData.append('file', file); - } - formData.append('name', name); - formData.append('is_required', is_required); - formData.append('order', order); - - if (isEditing && fileToEdit) { - editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken) - .then(data => { - setFichiers(prevFichiers => - prevFichiers.map(f => f.id === fileToEdit.id ? data : f) - ); - setIsModalOpen(false); - setFileToEdit(null); - setIsEditing(false); - }) - .catch(error => { - console.error('Error editing file:', error); - }); - } else { - createRegistrationFormFileTemplate(formData, csrfToken) - .then(data => { - setFichiers([...fichiers, data]); - setIsModalOpen(false); - }) - .catch(error => { - console.error('Error uploading file:', error); - }); - } -}; + const tabs = [ + { + id: 'pending', + label: t('pending'), + count: totalPending + }, + { + id: 'subscribed', + label: t('subscribed'), + count: totalSubscribed + }, + { + id: 'archived', + label: t('archived'), + count: totalArchives + } + ]; if (isLoading) { return ; @@ -699,16 +634,6 @@ const handleFileUpload = ({file, name, is_required, order}) => { active={activeTab === 'archived'} onClick={() => setActiveTab('archived')} /> - - {t('subscribeFiles')} - ({fichiers.length}) - - )} - active={activeTab === 'subscribeFiles'} - onClick={() => setActiveTab('subscribeFiles')} - />
@@ -758,41 +683,6 @@ const handleFileUpload = ({file, name, is_required, order}) => {
) : null} - {/*SI STATE == subscribeFiles */} - {activeTab === 'subscribeFiles' && ( -
-
- -
- ( - - )} - /> -
- - - - )} { tuitionDiscounts={tuitionDiscounts} registrationFees={registrationFees.filter(fee => fee.is_active)} tuitionFees={tuitionFees.filter(fee => fee.is_active)} + groups={groups} onSubmit={createRF} /> )} diff --git a/Front-End/src/app/lib/registerFileGroupAction.js b/Front-End/src/app/lib/registerFileGroupAction.js new file mode 100644 index 0000000..9db0398 --- /dev/null +++ b/Front-End/src/app/lib/registerFileGroupAction.js @@ -0,0 +1,74 @@ +import { BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL } from '@/utils/Url'; + +export async function fetchRegistrationFileGroups() { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`, { + credentials: 'include', + headers: { + 'Accept': 'application/json', + } + }); + if (!response.ok) { + throw new Error('Failed to fetch file groups'); + } + return response.json(); +} + +export async function createRegistrationFileGroup(groupData, csrfToken) { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + }, + body: JSON.stringify(groupData), + credentials: 'include' + }); + + if (!response.ok) { + throw new Error('Failed to create file group'); + } + + return response.json(); +} + +export async function deleteRegistrationFileGroup(groupId, csrfToken) { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include' + }); + + return response; +} + +export const editRegistrationFileGroup = async (groupId, groupData, csrfToken) => { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + }, + body: JSON.stringify(groupData), + }); + + if (!response.ok) { + throw new Error('Erreur lors de la modification du groupe'); + } + + return response.json(); +}; + +export const fetchRegistrationFileFromGroup = async (groupId) => { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/registrationFiles`, { + credentials: 'include', + headers: { + 'Accept': 'application/json', + } + }); + if (!response.ok) { + throw new Error('Erreur lors de la récupération des fichiers associés au groupe'); + } + return response.json(); +} \ No newline at end of file diff --git a/Front-End/src/app/lib/subscriptionAction.js b/Front-End/src/app/lib/subscriptionAction.js index b593394..76cbb0c 100644 --- a/Front-End/src/app/lib/subscriptionAction.js +++ b/Front-End/src/app/lib/subscriptionAction.js @@ -1,13 +1,9 @@ import { BE_SUBSCRIPTION_STUDENTS_URL, - BE_SUBSCRIPTION_STUDENT_URL, - BE_SUBSCRIPTION_ARCHIVE_URL, - BE_SUBSCRIPTION_SEND_URL, BE_SUBSCRIPTION_CHILDRENS_URL, - BE_SUBSCRIPTION_REGISTERFORM_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL, BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL, - BE_SUBSCRIPTION_LAST_GUARDIAN_URL, + BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL, BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL } from '@/utils/Url'; @@ -28,10 +24,10 @@ const requestResponseHandler = async (response) => { throw error; } -export const fetchRegisterForms = (type=PENDING, page='', pageSize='', search = '') => { - let url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${type}`; +export const fetchRegisterForms = (filter=PENDING, page='', pageSize='', search = '') => { + let url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}`; if (page !== '' && pageSize !== '') { - url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${type}?page=${page}&search=${search}`; + url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&page=${page}&search=${search}`; } return fetch(url, { headers: { @@ -40,18 +36,17 @@ export const fetchRegisterForms = (type=PENDING, page='', pageSize='', search = }).then(requestResponseHandler) }; -export const fetchRegisterForm = (id) =>{ - return fetch(`${BE_SUBSCRIPTION_REGISTERFORM_URL}/${id}`) // Utilisation de studentId au lieu de codeDI +export const fetchRegisterForm = (id) =>{ + return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`) // Utilisation de studentId au lieu de codeDI .then(requestResponseHandler) } -export const fetchLastGuardian = () =>{ - return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_URL}`) +export const fetchLastGuardian = () =>{ + return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`) .then(requestResponseHandler) } export const editRegisterForm=(id, data, csrfToken)=>{ - - return fetch(`${BE_SUBSCRIPTION_REGISTERFORM_URL}/${id}`, { + return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -61,11 +56,11 @@ export const editRegisterForm=(id, data, csrfToken)=>{ credentials: 'include' }) .then(requestResponseHandler) - }; + export const createRegisterForm=(data, csrfToken)=>{ - const url = `${BE_SUBSCRIPTION_REGISTERFORM_URL}`; + const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`; return fetch(url, { method: 'POST', headers: { @@ -78,8 +73,26 @@ export const createRegisterForm=(data, csrfToken)=>{ .then(requestResponseHandler) } +export const sendRegisterForm = (id) => { + const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/send`; + return fetch(url, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(requestResponseHandler) +} + +export const resendRegisterForm = (id) => { + const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/resend`; + return fetch(url, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(requestResponseHandler) + +} export const archiveRegisterForm = (id) => { - const url = `${BE_SUBSCRIPTION_ARCHIVE_URL}/${id}`; + const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/archive`; return fetch(url, { method: 'GET', headers: { @@ -88,18 +101,6 @@ export const archiveRegisterForm = (id) => { }).then(requestResponseHandler) } -export const sendRegisterForm = (id) => { - const url = `${BE_SUBSCRIPTION_SEND_URL}/${id}`; - return fetch(url, { - headers: { - 'Content-Type': 'application/json', - }, - }).then(requestResponseHandler) - -} - - - export const fetchRegisterFormFile = (id = null) => { let url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}` if (id) { @@ -204,9 +205,10 @@ export const editRegistrationFormFileTemplate = (fileId, data, csrfToken) => { .then(requestResponseHandler) } -export const fetchStudents = () => { +export const fetchStudents = (id) => { + const url = (id)?`${BE_SUBSCRIPTION_STUDENTS_URL}/${id}`:`${BE_SUBSCRIPTION_STUDENTS_URL}`; const request = new Request( - `${BE_SUBSCRIPTION_STUDENTS_URL}`, + url, { method:'GET', headers: { @@ -229,4 +231,17 @@ export const fetchChildren = (id) =>{ } ); return fetch(request).then(requestResponseHandler) +} + +export async function getRegisterFormFileTemplate(fileId) { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTERFORM_FILE_TEMPLATE_URL}/${fileId}`, { + credentials: 'include', + headers: { + 'Accept': 'application/json', + } + }); + if (!response.ok) { + throw new Error('Failed to fetch file template'); + } + return response.json(); } \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js b/Front-End/src/components/DraggableFileUpload.js similarity index 100% rename from Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js rename to Front-End/src/components/DraggableFileUpload.js diff --git a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js b/Front-End/src/components/FileUpload.js similarity index 74% rename from Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js rename to Front-End/src/components/FileUpload.js index 92cd485..a349e83 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js +++ b/Front-End/src/components/FileUpload.js @@ -1,18 +1,24 @@ import React, { useState, useEffect } from 'react'; import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch import DraggableFileUpload from './DraggableFileUpload'; +import { fetchRegistrationFileGroups } from '@/app/lib/registerFileGroupAction'; export default function FileUpload({ onFileUpload, fileToEdit = null }) { const [fileName, setFileName] = useState(''); const [file, setFile] = useState(null); const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired const [order, setOrder] = useState(0); + const [groups, setGroups] = useState([]); + const [selectedGroup, setSelectedGroup] = useState(''); useEffect(() => { + fetchRegistrationFileGroups().then(data => setGroups(data)); + if (fileToEdit) { setFileName(fileToEdit.name || ''); setIsRequired(fileToEdit.is_required || false); setOrder(fileToEdit.fusion_order || 0); + setSelectedGroup(fileToEdit.group_id || ''); } }, [fileToEdit]); @@ -26,11 +32,13 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) { name: fileName, is_required: isRequired, order: parseInt(order, 10), + groupId: selectedGroup || null }); setFile(null); setFileName(''); setIsRequired(false); setOrder(0); + setSelectedGroup(''); }; return ( @@ -72,6 +80,19 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) { onChange={() => setIsRequired(!isRequired)} /> +
+ + +
); } \ No newline at end of file diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index 22a23fb..9332c80 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -9,7 +9,7 @@ import DiscountsSection from '@/components/Structure/Tarification/DiscountsSecti import SectionTitle from '@/components/SectionTitle'; import ProgressStep from '@/components/ProgressStep'; -const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, onSubmit, currentStep }) => { +const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, onSubmit, currentStep, groups }) => { const [formData, setFormData] = useState({ studentLastName: '', studentFirstName: '', @@ -21,7 +21,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r selectedRegistrationDiscounts: [], selectedRegistrationFees: registrationFees.map(fee => fee.id), selectedTuitionDiscounts: [], - selectedTuitionFees: [] + selectedTuitionFees: [], + selectedFileGroup: null // Ajout du groupe de fichiers sélectionné }); const [step, setStep] = useState(currentStep || 1); @@ -35,10 +36,11 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r 2: 'Nouveau Responsable', 3: "Frais d'inscription", 4: 'Frais de scolarité', - 5: 'Récapitulatif' + 5: 'Documents requis', + 6: 'Récapitulatif' }; - const steps = ['Élève', 'Responsable', 'Inscription', 'Scolarité', 'Récap']; + const steps = ['Élève', 'Responsable', 'Inscription', 'Scolarité', 'Documents', 'Récap']; const isStep1Valid = formData.studentLastName && formData.studentFirstName; const isStep2Valid = ( @@ -47,7 +49,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r ); const isStep3Valid = formData.selectedRegistrationFees.length > 0; const isStep4Valid = formData.selectedTuitionFees.length > 0; - const isStep5Valid = isStep1Valid && isStep2Valid && isStep3Valid && isStep4Valid; + const isStep5Valid = formData.selectedFileGroup !== null; + const isStep6Valid = isStep1Valid && isStep2Valid && isStep3Valid && isStep4Valid && isStep5Valid; const isStepValid = (stepNumber) => { switch (stepNumber) { @@ -61,6 +64,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r return isStep4Valid; case 5: return isStep5Valid; + case 6: + return isStep6Valid; default: return false; } @@ -464,6 +469,44 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r )} + {step === 5 && ( +
+ {groups.length > 0 ? ( +
+

Sélectionnez un groupe de documents

+ {groups.map((group) => ( +
+ setFormData({ + ...formData, + selectedFileGroup: parseInt(e.target.value) + })} + className="form-radio h-4 w-4 text-emerald-600" + /> + +
+ ))} +
+ ) : ( +

+ Attention! + Aucun groupe de documents n'a été créé. +

+ )} +
+ )} + {step === steps.length && (
@@ -553,7 +596,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r (step === 1 && !isStep1Valid) || (step === 2 && !isStep2Valid) || (step === 3 && !isStep3Valid) || - (step === 4 && !isStep4Valid) + (step === 4 && !isStep4Valid) || + (step === 5 && !isStep5Valid) ) ? "bg-gray-300 text-gray-700 cursor-not-allowed" : "bg-emerald-500 text-white hover:bg-emerald-600" @@ -563,7 +607,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r (step === 1 && !isStep1Valid) || (step === 2 && !isStep2Valid) || (step === 3 && !isStep3Valid) || - (step === 4 && !isStep4Valid) + (step === 4 && !isStep4Valid) || + (step === 5 && !isStep5Valid) ) } primary diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index fad9a90..221e9b6 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -8,9 +8,10 @@ import Button from '@/components/Button'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import Table from '@/components/Table'; import { fetchRegisterFormFileTemplate, createRegistrationFormFile, fetchRegisterForm, deleteRegisterFormFile } from '@/app/lib/subscriptionAction'; +import { fetchRegistrationFileFromGroup } from '@/app/lib/registerFileGroupAction'; import { Download, Upload, Trash2, Eye } from 'lucide-react'; import { BASE_URL } from '@/utils/Url'; -import DraggableFileUpload from '@/app/[locale]/admin/subscriptions/components/DraggableFileUpload'; +import DraggableFileUpload from '@/components/DraggableFileUpload'; import Modal from '@/components/Modal'; import FileStatusLabel from '@/components/FileStatusLabel'; @@ -57,6 +58,7 @@ export default function InscriptionFormShared({ // États pour la gestion des fichiers const [uploadedFiles, setUploadedFiles] = useState([]); const [fileTemplates, setFileTemplates] = useState([]); + const [fileGroup, setFileGroup] = useState(null); const [fileName, setFileName] = useState(""); const [file, setFile] = useState(""); const [showUploadModal, setShowUploadModal] = useState(false); @@ -83,15 +85,21 @@ export default function InscriptionFormShared({ }); setGuardians(data?.student?.guardians || []); setUploadedFiles(data.registration_files || []); + setFileGroup(data.fileGroup || null); }); - fetchRegisterFormFileTemplate().then((data) => { - setFileTemplates(data); - }); setIsLoading(false); } }, [studentId]); + useEffect(() => { + if(fileGroup){ + fetchRegistrationFileFromGroup(fileGroup).then((data) => { + setFileTemplates(data); + }); + } + }, [fileGroup]); + // Fonctions de gestion du formulaire et des fichiers const updateFormField = (field, value) => { setFormData(prev => ({...prev, [field]: value})); diff --git a/Front-End/src/components/RegistrationFileGroupForm.js b/Front-End/src/components/RegistrationFileGroupForm.js new file mode 100644 index 0000000..c3ac53e --- /dev/null +++ b/Front-End/src/components/RegistrationFileGroupForm.js @@ -0,0 +1,56 @@ +import React, { useState, useEffect } from 'react'; + +export default function RegistrationFileGroupForm({ onSubmit, initialData }) { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + + useEffect(() => { + if (initialData) { + setName(initialData.name); + setDescription(initialData.description); + } + }, [initialData]); + + const handleSubmit = (e) => { + e.preventDefault(); + onSubmit({ name, description }); + }; + + return ( +
+
+ + setName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + required + /> +
+ +
+ +