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 ( -