diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index 3191a7e..a91f6ae 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -452,11 +452,12 @@ class StudentByRFCreationSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) guardians = GuardianByDICreationSerializer(many=True, required=False) associated_class_name = serializers.SerializerMethodField() + associated_class_id = serializers.SerializerMethodField() bilans = BilanCompetenceSerializer(many=True, read_only=True) class Meta: model = Student - fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name', 'photo', 'bilans'] + fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name', 'associated_class_id', 'photo', 'bilans'] def __init__(self, *args, **kwargs): super(StudentByRFCreationSerializer, self).__init__(*args, **kwargs) @@ -466,6 +467,9 @@ class StudentByRFCreationSerializer(serializers.ModelSerializer): def get_associated_class_name(self, obj): return obj.associated_class.atmosphere_name if obj.associated_class else None + def get_associated_class_id(self, obj): + return obj.associated_class.id if obj.associated_class else None + class NotificationSerializer(serializers.ModelSerializer): notification_type_label = serializers.ReadOnlyField() diff --git a/Front-End/public/icons/icon.svg b/Front-End/public/icons/icon.svg new file mode 100644 index 0000000..98c04af --- /dev/null +++ b/Front-End/public/icons/icon.svg @@ -0,0 +1,4 @@ + + + N3 + \ No newline at end of file diff --git a/Front-End/public/sw.js b/Front-End/public/sw.js new file mode 100644 index 0000000..430faa4 --- /dev/null +++ b/Front-End/public/sw.js @@ -0,0 +1,48 @@ +const CACHE_NAME = 'n3wt-school-v1'; + +const STATIC_ASSETS = [ + '/', + '/favicon.svg', + '/favicon.ico', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + // Ne pas intercepter les requêtes API ou d'authentification + const url = new URL(event.request.url); + if ( + url.pathname.startsWith('/api/') || + url.pathname.startsWith('/_next/') || + event.request.method !== 'GET' + ) { + return; + } + + event.respondWith( + fetch(event.request) + .then((response) => { + // Mettre en cache les réponses réussies des ressources statiques + if (response.ok && url.origin === self.location.origin) { + const cloned = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, cloned)); + } + return response; + }) + .catch(() => caches.match(event.request)) + ); +}); diff --git a/Front-End/src/app/[locale]/admin/grades/[studentId]/page.js b/Front-End/src/app/[locale]/admin/grades/[studentId]/page.js new file mode 100644 index 0000000..e00d9f9 --- /dev/null +++ b/Front-End/src/app/[locale]/admin/grades/[studentId]/page.js @@ -0,0 +1,286 @@ +'use client'; +import React, { useState, useEffect } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import SelectChoice from '@/components/Form/SelectChoice'; +import Attendance from '@/components/Grades/Attendance'; +import GradesStatsCircle from '@/components/Grades/GradesStatsCircle'; +import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart'; +import Button from '@/components/Form/Button'; +import logger from '@/utils/logger'; +import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL, BASE_URL } from '@/utils/Url'; +import { + fetchStudents, + fetchStudentCompetencies, + fetchAbsences, + editAbsences, + deleteAbsences, +} from '@/app/actions/subscriptionAction'; +import { useEstablishment } from '@/context/EstablishmentContext'; +import { useClasses } from '@/context/ClassesContext'; +import { Award, ArrowLeft } from 'lucide-react'; +import dayjs from 'dayjs'; +import { useCsrfToken } from '@/context/CsrfContext'; + +function getPeriodString(selectedPeriod, frequency) { + const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; + const nextYear = (year + 1).toString(); + const schoolYear = `${year}-${nextYear}`; + if (frequency === 1) return `T${selectedPeriod}_${schoolYear}`; + if (frequency === 2) return `S${selectedPeriod}_${schoolYear}`; + if (frequency === 3) return `A_${schoolYear}`; + return ''; +} + +export default function StudentGradesPage() { + const router = useRouter(); + const params = useParams(); + const studentId = Number(params.studentId); + const csrfToken = useCsrfToken(); + const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = + useEstablishment(); + const { getNiveauLabel } = useClasses(); + + const [student, setStudent] = useState(null); + const [studentCompetencies, setStudentCompetencies] = useState(null); + const [grades, setGrades] = useState({}); + const [selectedPeriod, setSelectedPeriod] = useState(null); + const [allAbsences, setAllAbsences] = useState([]); + + const getPeriods = () => { + if (selectedEstablishmentEvaluationFrequency === 1) { + return [ + { label: 'Trimestre 1', value: 1, start: '09-01', end: '12-31' }, + { label: 'Trimestre 2', value: 2, start: '01-01', end: '03-31' }, + { label: 'Trimestre 3', value: 3, start: '04-01', end: '07-15' }, + ]; + } + if (selectedEstablishmentEvaluationFrequency === 2) { + return [ + { label: 'Semestre 1', value: 1, start: '09-01', end: '01-31' }, + { label: 'Semestre 2', value: 2, start: '02-01', end: '07-15' }, + ]; + } + if (selectedEstablishmentEvaluationFrequency === 3) { + return [{ label: 'Année', value: 1, start: '09-01', end: '07-15' }]; + } + return []; + }; + + // Load student info + useEffect(() => { + if (selectedEstablishmentId) { + fetchStudents(selectedEstablishmentId, null, 5) + .then((students) => { + const found = students.find((s) => s.id === studentId); + setStudent(found || null); + }) + .catch((error) => logger.error('Error fetching students:', error)); + } + }, [selectedEstablishmentId, studentId]); + + // Auto-select current period + useEffect(() => { + const periods = getPeriods(); + const today = dayjs(); + const current = periods.find((p) => { + const start = dayjs(`${today.year()}-${p.start}`); + const end = dayjs(`${today.year()}-${p.end}`); + return ( + today.isAfter(start.subtract(1, 'day')) && + today.isBefore(end.add(1, 'day')) + ); + }); + setSelectedPeriod(current ? current.value : null); + }, [selectedEstablishmentEvaluationFrequency]); + + // Load competencies + useEffect(() => { + if (studentId && selectedPeriod) { + const periodString = getPeriodString( + selectedPeriod, + selectedEstablishmentEvaluationFrequency + ); + fetchStudentCompetencies(studentId, periodString) + .then((data) => { + setStudentCompetencies(data); + if (data && data.data) { + const initialGrades = {}; + data.data.forEach((domaine) => { + domaine.categories.forEach((cat) => { + cat.competences.forEach((comp) => { + initialGrades[comp.competence_id] = comp.score ?? 0; + }); + }); + }); + setGrades(initialGrades); + } + }) + .catch((error) => + logger.error('Error fetching studentCompetencies:', error) + ); + } else { + setGrades({}); + setStudentCompetencies(null); + } + }, [studentId, selectedPeriod]); + + // Load absences + useEffect(() => { + if (selectedEstablishmentId) { + fetchAbsences(selectedEstablishmentId) + .then((data) => setAllAbsences(data)) + .catch((error) => + logger.error('Erreur lors du fetch des absences:', error) + ); + } + }, [selectedEstablishmentId]); + + const absences = React.useMemo(() => { + return allAbsences + .filter((a) => a.student === studentId) + .map((a) => ({ + id: a.id, + date: a.day, + type: [2, 1].includes(a.reason) ? 'Absence' : 'Retard', + reason: a.reason, + justified: [1, 3].includes(a.reason), + moment: a.moment, + commentaire: a.commentaire, + })); + }, [allAbsences, studentId]); + + const handleToggleJustify = (absence) => { + const newReason = + absence.type === 'Absence' + ? absence.justified ? 2 : 1 + : absence.justified ? 4 : 3; + + editAbsences(absence.id, { ...absence, reason: newReason }, csrfToken) + .then(() => { + setAllAbsences((prev) => + prev.map((a) => + a.id === absence.id ? { ...a, reason: newReason } : a + ) + ); + }) + .catch((e) => logger.error('Erreur lors du changement de justification', e)); + }; + + const handleDeleteAbsence = (absence) => { + return deleteAbsences(absence.id, csrfToken) + .then(() => { + setAllAbsences((prev) => prev.filter((a) => a.id !== absence.id)); + }) + .catch((e) => + logger.error("Erreur lors de la suppression de l'absence", e) + ); + }; + + return ( +
+ {/* Header */} +
+ +

Suivi pédagogique

+
+ + {/* Student profile */} + {student && ( +
+ {student.photo ? ( + {`${student.first_name} + ) : ( +
+ {student.first_name?.[0]} + {student.last_name?.[0]} +
+ )} +
+
+ {student.last_name} {student.first_name} +
+
+ Niveau :{' '} + + {getNiveauLabel(student.level)} + + {' | '} + Classe :{' '} + + {student.associated_class_name} + +
+
+ + {/* Period selector + Evaluate button */} +
+
+ { + const today = dayjs(); + const end = dayjs(`${today.year()}-${period.end}`); + return { + value: period.value, + label: period.label, + disabled: today.isAfter(end), + }; + })} + selected={selectedPeriod || ''} + callback={(e) => setSelectedPeriod(Number(e.target.value))} + /> +
+
+
+ )} + + {/* Stats + Absences */} +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ ); +} diff --git a/Front-End/src/app/[locale]/admin/grades/page.js b/Front-End/src/app/[locale]/admin/grades/page.js index 4b098e8..7a5783e 100644 --- a/Front-End/src/app/[locale]/admin/grades/page.js +++ b/Front-End/src/app/[locale]/admin/grades/page.js @@ -1,479 +1,351 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import SelectChoice from '@/components/Form/SelectChoice'; -import AcademicResults from '@/components/Grades/AcademicResults'; -import Attendance from '@/components/Grades/Attendance'; -import Remarks from '@/components/Grades/Remarks'; -import WorkPlan from '@/components/Grades/WorkPlan'; -import Homeworks from '@/components/Grades/Homeworks'; -import SpecificEvaluations from '@/components/Grades/SpecificEvaluations'; -import Orientation from '@/components/Grades/Orientation'; -import GradesStatsCircle from '@/components/Grades/GradesStatsCircle'; -import Button from '@/components/Form/Button'; +import { useRouter } from 'next/navigation'; +import { Award, Eye, Search } from 'lucide-react'; +import SectionHeader from '@/components/SectionHeader'; +import Table from '@/components/Table'; import logger from '@/utils/logger'; import { - FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL, BASE_URL, + FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL, + FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL, } from '@/utils/Url'; -import { useRouter } from 'next/navigation'; import { fetchStudents, fetchStudentCompetencies, - searchStudents, fetchAbsences, - editAbsences, - deleteAbsences, } from '@/app/actions/subscriptionAction'; import { useEstablishment } from '@/context/EstablishmentContext'; import { useClasses } from '@/context/ClassesContext'; -import { Award, FileText } from 'lucide-react'; -import SectionHeader from '@/components/SectionHeader'; -import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart'; -import InputText from '@/components/Form/InputText'; import dayjs from 'dayjs'; -import { useCsrfToken } from '@/context/CsrfContext'; + +function getPeriodString(periodValue, frequency) { + const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; + const schoolYear = `${year}-${year + 1}`; + if (frequency === 1) return `T${periodValue}_${schoolYear}`; + if (frequency === 2) return `S${periodValue}_${schoolYear}`; + if (frequency === 3) return `A_${schoolYear}`; + return ''; +} + +function calcPercent(data) { + if (!data?.data) return null; + const scores = []; + data.data.forEach((d) => + d.categories.forEach((c) => + c.competences.forEach((comp) => scores.push(comp.score ?? 0)) + ) + ); + if (!scores.length) return null; + return Math.round( + (scores.filter((s) => s === 3).length / scores.length) * 100 + ); +} + +function getPeriodColumns(frequency) { + if (frequency === 1) + return [ + { label: 'Trimestre 1', value: 1 }, + { label: 'Trimestre 2', value: 2 }, + { label: 'Trimestre 3', value: 3 }, + ]; + if (frequency === 2) + return [ + { label: 'Semestre 1', value: 1 }, + { label: 'Semestre 2', value: 2 }, + ]; + if (frequency === 3) return [{ label: 'Année', value: 1 }]; + return []; +} + +function getCurrentPeriodValue(frequency) { + const periods = + { + 1: [ + { value: 1, start: '09-01', end: '12-31' }, + { value: 2, start: '01-01', end: '03-31' }, + { value: 3, start: '04-01', end: '07-15' }, + ], + 2: [ + { value: 1, start: '09-01', end: '01-31' }, + { value: 2, start: '02-01', end: '07-15' }, + ], + 3: [{ value: 1, start: '09-01', end: '07-15' }], + }[frequency] || []; + const today = dayjs(); + const current = periods.find( + (p) => + today.isAfter(dayjs(`${today.year()}-${p.start}`).subtract(1, 'day')) && + today.isBefore(dayjs(`${today.year()}-${p.end}`).add(1, 'day')) + ); + return current?.value ?? null; +} + +function PercentBadge({ value, loading }) { + if (loading) return ; + if (value === null) return ; + const color = + value >= 75 + ? 'bg-emerald-100 text-emerald-700' + : value >= 50 + ? 'bg-yellow-100 text-yellow-700' + : 'bg-red-100 text-red-600'; + return ( + + {value}% + + ); +} export default function Page() { const router = useRouter(); - const csrfToken = useCsrfToken(); const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = useEstablishment(); const { getNiveauLabel } = useClasses(); - const [formData, setFormData] = useState({ - selectedStudent: null, - }); - const [students, setStudents] = useState([]); - const [studentCompetencies, setStudentCompetencies] = useState(null); - const [grades, setGrades] = useState({}); const [searchTerm, setSearchTerm] = useState(''); - const [selectedPeriod, setSelectedPeriod] = useState(null); - const [allAbsences, setAllAbsences] = useState([]); + const ITEMS_PER_PAGE = 15; + const [currentPage, setCurrentPage] = useState(1); + const [statsMap, setStatsMap] = useState({}); + const [statsLoading, setStatsLoading] = useState(false); + const [absencesMap, setAbsencesMap] = useState({}); - // Définir les périodes selon la fréquence - const getPeriods = () => { - if (selectedEstablishmentEvaluationFrequency === 1) { - return [ - { label: 'Trimestre 1', value: 1, start: '09-01', end: '12-31' }, - { label: 'Trimestre 2', value: 2, start: '01-01', end: '03-31' }, - { label: 'Trimestre 3', value: 3, start: '04-01', end: '07-15' }, - ]; - } - if (selectedEstablishmentEvaluationFrequency === 2) { - return [ - { label: 'Semestre 1', value: 1, start: '09-01', end: '01-31' }, - { label: 'Semestre 2', value: 2, start: '02-01', end: '07-15' }, - ]; - } - if (selectedEstablishmentEvaluationFrequency === 3) { - return [{ label: 'Année', value: 1, start: '09-01', end: '07-15' }]; - } - return []; - }; - - // Sélection automatique de la période courante - useEffect(() => { - if (!formData.selectedStudent) { - setSelectedPeriod(null); - return; - } - const periods = getPeriods(); - const today = dayjs(); - const current = periods.find((p) => { - const start = dayjs(`${today.year()}-${p.start}`); - const end = dayjs(`${today.year()}-${p.end}`); - return ( - today.isAfter(start.subtract(1, 'day')) && - today.isBefore(end.add(1, 'day')) - ); - }); - setSelectedPeriod(current ? current.value : null); - }, [formData.selectedStudent, selectedEstablishmentEvaluationFrequency]); - - const academicResults = [ - { - subject: 'Mathématiques', - grade: 16, - average: 14, - appreciation: 'Très bon travail', - }, - { - subject: 'Français', - grade: 15, - average: 13, - appreciation: 'Bonne participation', - }, - ]; - - const remarks = [ - { - date: '2023-09-10', - teacher: 'Mme Dupont', - comment: 'Participation active en classe.', - }, - { - date: '2023-09-20', - teacher: 'M. Martin', - comment: 'Doit améliorer la concentration.', - }, - ]; - - const workPlan = [ - { - objective: 'Renforcer la lecture', - support: 'Exercices hebdomadaires', - followUp: 'En cours', - }, - { - objective: 'Maîtriser les tables de multiplication', - support: 'Jeux éducatifs', - followUp: 'À démarrer', - }, - ]; - - const homeworks = [ - { title: 'Rédaction', dueDate: '2023-10-10', status: 'Rendu' }, - { title: 'Exercices de maths', dueDate: '2023-10-12', status: 'À faire' }, - ]; - - const specificEvaluations = [ - { - test: 'Bilan de compétences', - date: '2023-09-25', - result: 'Bon niveau général', - }, - ]; - - const orientation = [ - { - date: '2023-10-01', - counselor: 'Mme Leroy', - advice: 'Poursuivre en filière générale', - }, - ]; - - const handleChange = (field, value) => - setFormData((prev) => ({ ...prev, [field]: value })); + const periodColumns = getPeriodColumns( + selectedEstablishmentEvaluationFrequency + ); + const currentPeriodValue = getCurrentPeriodValue( + selectedEstablishmentEvaluationFrequency + ); useEffect(() => { - if (selectedEstablishmentId) { - fetchStudents(selectedEstablishmentId, null, 5) - .then((studentsData) => { - setStudents(studentsData); - }) - .catch((error) => logger.error('Error fetching students:', error)); - } - }, [selectedEstablishmentId]); + if (!selectedEstablishmentId) return; + fetchStudents(selectedEstablishmentId, null, 5) + .then((data) => setStudents(data)) + .catch((error) => logger.error('Error fetching students:', error)); - // Charger les compétences et générer les grades à chaque changement d'élève sélectionné - useEffect(() => { - if (formData.selectedStudent && selectedPeriod) { - const periodString = getPeriodString( - selectedPeriod, - selectedEstablishmentEvaluationFrequency - ); - fetchStudentCompetencies(formData.selectedStudent, periodString) - .then((data) => { - setStudentCompetencies(data); - // Générer les grades à partir du retour API - if (data && data.data) { - const initialGrades = {}; - data.data.forEach((domaine) => { - domaine.categories.forEach((cat) => { - cat.competences.forEach((comp) => { - initialGrades[comp.competence_id] = comp.score ?? 0; - }); - }); - }); - setGrades(initialGrades); + fetchAbsences(selectedEstablishmentId) + .then((data) => { + const map = {}; + (data || []).forEach((a) => { + if ([1, 2].includes(a.reason)) { + map[a.student] = (map[a.student] || 0) + 1; } - }) - .catch((error) => - logger.error('Error fetching studentCompetencies:', error) - ); - } else { - setGrades({}); - setStudentCompetencies(null); - } - }, [formData.selectedStudent, selectedPeriod]); - - useEffect(() => { - if (selectedEstablishmentId) { - fetchAbsences(selectedEstablishmentId) - .then((data) => setAllAbsences(data)) - .catch((error) => - logger.error('Erreur lors du fetch des absences:', error) - ); - } + }); + setAbsencesMap(map); + }) + .catch((error) => logger.error('Error fetching absences:', error)); }, [selectedEstablishmentId]); - // Transforme les absences backend pour l'élève sélectionné - const absences = React.useMemo(() => { - if (!formData.selectedStudent) return []; - return allAbsences - .filter((a) => a.student === formData.selectedStudent) - .map((a) => ({ - id: a.id, - date: a.day, - type: [2, 1].includes(a.reason) ? 'Absence' : 'Retard', - reason: a.reason, // tu peux mapper le code vers un label si besoin - justified: [1, 3].includes(a.reason), // 1 et 3 = justifié - moment: a.moment, - commentaire: a.commentaire, - })); - }, [allAbsences, formData.selectedStudent]); + // Fetch stats for all students × all periods + useEffect(() => { + if (!students.length || !selectedEstablishmentEvaluationFrequency) return; - // Fonction utilitaire pour convertir la période sélectionnée en string backend - function getPeriodString(selectedPeriod, frequency) { - const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; // année scolaire commence en septembre - const nextYear = (year + 1).toString(); - const schoolYear = `${year}-${nextYear}`; - if (frequency === 1) return `T${selectedPeriod}_${schoolYear}`; - if (frequency === 2) return `S${selectedPeriod}_${schoolYear}`; - if (frequency === 3) return `A_${schoolYear}`; - return ''; - } + setStatsLoading(true); + const frequency = selectedEstablishmentEvaluationFrequency; - // Callback pour justifier/non justifier une absence - const handleToggleJustify = (absence) => { - // Inverser l'état justifié (1/3 = justifié, 2/4 = non justifié) - const newReason = - absence.type === 'Absence' - ? absence.justified - ? 2 // Absence non justifiée - : 1 // Absence justifiée - : absence.justified - ? 4 // Retard non justifié - : 3; // Retard justifié - - editAbsences(absence.id, { ...absence, reason: newReason }, csrfToken) - .then(() => { - setAllAbsences((prev) => - prev.map((a) => - a.id === absence.id ? { ...a, reason: newReason } : a - ) - ); + const tasks = students.flatMap((student) => + periodColumns.map(({ value: periodValue }) => { + const periodStr = getPeriodString(periodValue, frequency); + return fetchStudentCompetencies(student.id, periodStr) + .then((data) => ({ studentId: student.id, periodValue, data })) + .catch(() => ({ studentId: student.id, periodValue, data: null })); }) - .catch((e) => { - logger.error('Erreur lors du changement de justification', e); + ); + + Promise.all(tasks).then((results) => { + const map = {}; + results.forEach(({ studentId, periodValue, data }) => { + if (!map[studentId]) map[studentId] = {}; + map[studentId][periodValue] = calcPercent(data); }); + Object.keys(map).forEach((id) => { + const vals = Object.values(map[id]).filter((v) => v !== null); + map[id].global = vals.length + ? Math.round(vals.reduce((a, b) => a + b, 0) / vals.length) + : null; + }); + setStatsMap(map); + setStatsLoading(false); + }); + }, [students, selectedEstablishmentEvaluationFrequency]); + + const filteredStudents = students.filter( + (student) => + !searchTerm || + `${student.last_name} ${student.first_name}` + .toLowerCase() + .includes(searchTerm.toLowerCase()) + ); + + useEffect(() => { + setCurrentPage(1); + }, [searchTerm, students]); + + const totalPages = Math.ceil(filteredStudents.length / ITEMS_PER_PAGE); + const pagedStudents = filteredStudents.slice( + (currentPage - 1) * ITEMS_PER_PAGE, + currentPage * ITEMS_PER_PAGE + ); + + const handleEvaluer = (e, studentId) => { + e.stopPropagation(); + const periodStr = getPeriodString( + currentPeriodValue, + selectedEstablishmentEvaluationFrequency + ); + router.push( + `${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${studentId}&period=${periodStr}` + ); }; - // Callback pour supprimer une absence - const handleDeleteAbsence = (absence) => { - return deleteAbsences(absence.id, csrfToken) - .then(() => { - setAllAbsences((prev) => prev.filter((a) => a.id !== absence.id)); - }) - .catch((e) => { - logger.error("Erreur lors de la suppression de l'absence", e); - }); + const columns = [ + { name: 'Photo', transform: () => null }, + { name: 'Élève', transform: () => null }, + { name: 'Niveau', transform: () => null }, + { name: 'Classe', transform: () => null }, + ...periodColumns.map(({ label }) => ({ name: label, transform: () => null })), + { name: 'Stat globale', transform: () => null }, + { name: 'Absences', transform: () => null }, + { name: 'Actions', transform: () => null }, + ]; + + const renderCell = (student, column) => { + const stats = statsMap[student.id] || {}; + switch (column) { + case 'Photo': + return ( +
+ {student.photo ? ( + e.stopPropagation()} + > + {`${student.first_name} + + ) : ( +
+ + {student.first_name?.[0]}{student.last_name?.[0]} + +
+ )} +
+ ); + case 'Élève': + return ( + + {student.last_name} {student.first_name} + + ); + case 'Niveau': + return getNiveauLabel(student.level); + case 'Classe': + return student.associated_class_id ? ( + + ) : ( + student.associated_class_name + ); + case 'Stat globale': + return ( + + ); + case 'Absences': + return absencesMap[student.id] ? ( + + {absencesMap[student.id]} + + ) : ( + 0 + ); + case 'Actions': + return ( +
+ + +
+ ); + default: { + const col = periodColumns.find((c) => c.label === column); + if (col) { + return ( + + ); + } + return null; + } + } }; return ( -
+
- - {/* Section haute : filtre + bouton + photo élève */} -
- {/* Colonne gauche : InputText + bouton */} -
-
- setSearchTerm(e.target.value)} - placeholder="Rechercher un élève" - required={false} - enable={true} - /> -
-
- { - const today = dayjs(); - const start = dayjs(`${today.year()}-${period.start}`); - const end = dayjs(`${today.year()}-${period.end}`); - const isPast = today.isAfter(end); - return { - value: period.value, - label: period.label, - disabled: isPast, - }; - })} - selected={selectedPeriod || ''} - callback={(e) => setSelectedPeriod(Number(e.target.value))} - disabled={!formData.selectedStudent} - /> -
-
-
-
- {/* Colonne droite : Photo élève */} -
- {formData.selectedStudent && - (() => { - const student = students.find( - (s) => s.id === formData.selectedStudent - ); - if (!student) return null; - return ( - <> - {student.photo ? ( - {`${student.first_name} - ) : ( -
- {student.first_name?.[0]} - {student.last_name?.[0]} -
- )} - - ); - })()} -
+
+ + setSearchTerm(e.target.value)} + />
- {/* Section basse : liste élèves + infos */} -
- {/* Colonne 1 : Liste des élèves */} -
-

- Liste des élèves -

-
    - {students - .filter( - (student) => - !searchTerm || - `${student.last_name} ${student.first_name}` - .toLowerCase() - .includes(searchTerm.toLowerCase()) - ) - .map((student) => ( -
  • handleChange('selectedStudent', student.id)} - > - {student.photo ? ( - {`${student.first_name} - ) : ( -
    - {student.first_name?.[0]} - {student.last_name?.[0]} -
    - )} -
    -
    - {student.last_name} {student.first_name} -
    -
    - Niveau :{' '} - - {getNiveauLabel(student.level)} - - {' | '} - Classe :{' '} - - {student.associated_class_name} - -
    -
    - {/* Icône PDF si bilan dispo pour la période sélectionnée */} - {selectedPeriod && - student.bilans && - Array.isArray(student.bilans) && - (() => { - // Génère la string de période attendue - const periodString = getPeriodString( - selectedPeriod, - selectedEstablishmentEvaluationFrequency - ); - const bilan = student.bilans.find( - (b) => b.period === periodString && b.file - ); - if (bilan) { - return ( - e.stopPropagation()} // Pour ne pas sélectionner à nouveau l'élève - > - - - ); - } - return null; - })()} -
  • - ))} -
-
- {/* Colonne 2 : Reste des infos */} -
- {formData.selectedStudent && ( -
-
-
- -
-
- -
-
-
- -
-
- )} -
-
+ Aucun élève trouvé + } + /> ); } diff --git a/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js b/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js index cca7e8e..5500667 100644 --- a/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js +++ b/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js @@ -8,8 +8,8 @@ import { fetchStudentCompetencies, editStudentCompetencies, } from '@/app/actions/subscriptionAction'; -import SectionHeader from '@/components/SectionHeader'; -import { Award } from 'lucide-react'; +import { Award, ArrowLeft } from 'lucide-react'; +import logger from '@/utils/logger'; import { useCsrfToken } from '@/context/CsrfContext'; import { useNotification } from '@/context/NotificationContext'; @@ -70,7 +70,7 @@ export default function StudentCompetenciesPage() { 'success', 'Succès' ); - router.back(); + router.push(`/admin/grades/${studentId}`); }) .catch((error) => { showNotification( @@ -83,11 +83,16 @@ export default function StudentCompetenciesPage() { return (
- +
+ +

Bilan de compétence

+
-
diff --git a/Front-End/src/app/[locale]/admin/layout.js b/Front-End/src/app/[locale]/admin/layout.js index 72d8819..75587bb 100644 --- a/Front-End/src/app/[locale]/admin/layout.js +++ b/Front-End/src/app/[locale]/admin/layout.js @@ -29,6 +29,7 @@ import { import { disconnect } from '@/app/actions/authAction'; import ProtectedRoute from '@/components/ProtectedRoute'; import Footer from '@/components/Footer'; +import MobileTopbar from '@/components/MobileTopbar'; import { RIGHTS } from '@/utils/rights'; import { useEstablishment } from '@/context/EstablishmentContext'; @@ -123,9 +124,12 @@ export default function Layout({ children }) { return ( + {/* Topbar mobile (hamburger + logo) */} + + {/* Sidebar */}
@@ -146,7 +150,7 @@ export default function Layout({ children }) { )} {/* Main container */} -
+
{children}
diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index 787f3d2..12a12b9 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -163,7 +163,7 @@ export default function DashboardPage() { if (isLoading) return ; return ( -
+
{/* Statistiques principales */}
{/* Graphique des inscriptions */} -
-

+
+

{t('inscriptionTrends')}

-
-
+
+
@@ -214,13 +214,13 @@ export default function DashboardPage() {
{/* Présence et assiduité */} -
+
{/* Colonne de droite : Événements à venir */} -
+

{t('upcomingEvents')}

{upcomingEvents.map((event, index) => ( diff --git a/Front-End/src/app/[locale]/admin/planning/page.js b/Front-End/src/app/[locale]/admin/planning/page.js index 1563a07..cff8307 100644 --- a/Front-End/src/app/[locale]/admin/planning/page.js +++ b/Front-End/src/app/[locale]/admin/planning/page.js @@ -12,6 +12,7 @@ import { useEstablishment } from '@/context/EstablishmentContext'; export default function Page() { const [isModalOpen, setIsModalOpen] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [eventData, setEventData] = useState({ title: '', description: '', @@ -56,13 +57,17 @@ export default function Page() { modeSet={PlanningModes.PLANNING} >
- + setIsDrawerOpen(false)} + /> { setEventData(event); setIsModalOpen(true); }} + onOpenDrawer={() => setIsDrawerOpen(true)} /> { setStudentsPage(1); }, [students]); + useEffect(() => { if (!formData.guardianEmail) { // Si l'email est vide, réinitialiser existingProfileId et existingProfileInSchool @@ -709,6 +713,9 @@ export default function CreateSubscriptionPage() { return finalAmount.toFixed(2); }; + const studentsTotalPages = Math.ceil(students.length / ITEMS_PER_PAGE); + const pagedStudents = students.slice((studentsPage - 1) * ITEMS_PER_PAGE, studentsPage * ITEMS_PER_PAGE); + if (isLoading === true) { return ; // Affichez le composant Loader } @@ -869,7 +876,7 @@ export default function CreateSubscriptionPage() { {!isNewResponsable && (

{selectedStudent && ( diff --git a/Front-End/src/app/[locale]/parents/layout.js b/Front-End/src/app/[locale]/parents/layout.js index 9c8003d..22319ae 100644 --- a/Front-End/src/app/[locale]/parents/layout.js +++ b/Front-End/src/app/[locale]/parents/layout.js @@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react'; import Sidebar from '@/components/Sidebar'; import { useRouter, usePathname } from 'next/navigation'; -import { MessageSquare, Settings, Home, Menu } from 'lucide-react'; +import { MessageSquare, Settings, Home } from 'lucide-react'; import { FE_PARENTS_HOME_URL, FE_PARENTS_MESSAGERIE_URL @@ -11,6 +11,7 @@ import { import ProtectedRoute from '@/components/ProtectedRoute'; import { disconnect } from '@/app/actions/authAction'; import Popup from '@/components/Popup'; +import MobileTopbar from '@/components/MobileTopbar'; import { RIGHTS } from '@/utils/rights'; import { useEstablishment } from '@/context/EstablishmentContext'; import Footer from '@/components/Footer'; @@ -73,17 +74,12 @@ export default function Layout({ children }) { return ( - {/* Bouton hamburger pour mobile */} - + {/* Topbar mobile (hamburger + logo) */} + {/* Sidebar */}
@@ -104,7 +100,7 @@ export default function Layout({ children }) { {/* Main container */}
{children}
diff --git a/Front-End/src/app/layout.js b/Front-End/src/app/layout.js index 19f8d28..1c9643d 100644 --- a/Front-End/src/app/layout.js +++ b/Front-End/src/app/layout.js @@ -1,12 +1,19 @@ import React from 'react'; import { getMessages } from 'next-intl/server'; import Providers from '@/components/Providers'; +import ServiceWorkerRegister from '@/components/ServiceWorkerRegister'; import '@/css/tailwind.css'; import { headers } from 'next/headers'; export const metadata = { title: 'N3WT-SCHOOL', description: "Gestion de l'école", + manifest: '/manifest.webmanifest', + appleWebApp: { + capable: true, + statusBarStyle: 'default', + title: 'N3WT School', + }, icons: { icon: [ { @@ -14,10 +21,11 @@ export const metadata = { type: 'image/svg+xml', }, { - url: '/favicon.ico', // Fallback pour les anciens navigateurs + url: '/favicon.ico', sizes: 'any', }, ], + apple: '/icons/icon.svg', }, }; @@ -32,6 +40,7 @@ export default async function RootLayout({ children, params }) { {children} + ); diff --git a/Front-End/src/app/manifest.js b/Front-End/src/app/manifest.js new file mode 100644 index 0000000..10a6b89 --- /dev/null +++ b/Front-End/src/app/manifest.js @@ -0,0 +1,26 @@ +export default function manifest() { + return { + name: 'N3WT School', + short_name: 'N3WT School', + description: "Gestion de l'école", + start_url: '/', + display: 'standalone', + background_color: '#f0fdf4', + theme_color: '#10b981', + orientation: 'portrait', + icons: [ + { + src: '/icons/icon.svg', + sizes: 'any', + type: 'image/svg+xml', + purpose: 'any', + }, + { + src: '/icons/icon.svg', + sizes: 'any', + type: 'image/svg+xml', + purpose: 'maskable', + }, + ], + }; +} diff --git a/Front-End/src/components/Calendar/Calendar.js b/Front-End/src/components/Calendar/Calendar.js index 548aa33..2223e38 100644 --- a/Front-End/src/components/Calendar/Calendar.js +++ b/Front-End/src/components/Calendar/Calendar.js @@ -4,6 +4,7 @@ import WeekView from '@/components/Calendar/WeekView'; import MonthView from '@/components/Calendar/MonthView'; import YearView from '@/components/Calendar/YearView'; import PlanningView from '@/components/Calendar/PlanningView'; +import DayView from '@/components/Calendar/DayView'; import ToggleView from '@/components/ToggleView'; import { ChevronLeft, ChevronRight, Plus, ChevronDown } from 'lucide-react'; import { @@ -11,9 +12,11 @@ import { addWeeks, addMonths, addYears, + addDays, subWeeks, subMonths, subYears, + subDays, getWeek, setMonth, setYear, @@ -22,7 +25,7 @@ import { fr } from 'date-fns/locale'; import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import import logger from '@/utils/logger'; -const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' }) => { +const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName = '', onOpenDrawer = () => {} }) => { const { currentDate, setCurrentDate, @@ -35,6 +38,14 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' }) } = usePlanning(); const [visibleEvents, setVisibleEvents] = useState([]); const [showDatePicker, setShowDatePicker] = useState(false); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const check = () => setIsMobile(window.innerWidth < 768); + check(); + window.addEventListener('resize', check); + return () => window.removeEventListener('resize', check); + }, []); // Ajouter ces fonctions pour la gestion des mois et années const months = Array.from({ length: 12 }, (_, i) => ({ @@ -68,7 +79,12 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' }) const navigateDate = (direction) => { const getNewDate = () => { - switch (viewType) { + const effectiveView = isMobile ? 'day' : viewType; + switch (effectiveView) { + case 'day': + return direction === 'next' + ? addDays(currentDate, 1) + : subDays(currentDate, 1); case 'week': return direction === 'next' ? addWeeks(currentDate, 1) @@ -91,116 +107,114 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' }) return (
-
- {/* Navigation à gauche */} - {planningMode === PlanningModes.PLANNING && ( -
- - -
- - {showDatePicker && ( -
- {viewType !== 'year' && ( -
-
- {months.map((month) => ( - - ))} + {/* Header uniquement sur desktop */} +
+ <> + {planningMode === PlanningModes.PLANNING && ( +
+ + +
+ + {showDatePicker && ( +
+ {viewType !== 'year' && ( +
+
+ {months.map((month) => ( + + ))} +
+
+ )} +
+
+ {years.map((year) => ( + + ))} +
)} -
-
- {years.map((year) => ( - - ))} -
-
+
+ +
+ )} + +
+ {((planningMode === PlanningModes.PLANNING || planningMode === PlanningModes.CLASS_SCHEDULE) && viewType === 'week' && !parentView) && ( +
+ Semaine + + {getWeek(currentDate, { weekStartsOn: 1 })} +
)} + {parentView && ( + + {planningClassName} + + )}
- -
- )} - {/* Centre : numéro de semaine ou classe/niveau */} -
- {((planningMode === PlanningModes.PLANNING || planningMode === PlanningModes.CLASS_SCHEDULE) && viewType === 'week' && !parentView) && ( -
- Semaine - - {getWeek(currentDate, { weekStartsOn: 1 })} - +
+ {planningMode === PlanningModes.PLANNING && ( + + )} + {(planningMode === PlanningModes.PLANNING || (planningMode === PlanningModes.CLASS_SCHEDULE && !parentView)) && ( + + )}
- )} - {parentView && ( - - {/* À adapter selon les props disponibles */} - {planningClassName} - - )} -
- - {/* Contrôles à droite */} -
- {planningMode === PlanningModes.PLANNING && ( - - )} - {(planningMode === PlanningModes.PLANNING || (planningMode === PlanningModes.CLASS_SCHEDULE && !parentView)) && ( - - )} -
+
{/* Contenu scrollable */}
- {viewType === 'week' && ( + {isMobile && ( + + + + )} + {!isMobile && viewType === 'week' && ( )} - {viewType === 'month' && ( + {!isMobile && viewType === 'month' && ( )} - {viewType === 'year' && ( + {!isMobile && viewType === 'year' && ( )} - {viewType === 'planning' && ( + {!isMobile && viewType === 'planning' && ( { + const { currentDate, setCurrentDate, parentView } = usePlanning(); + const [currentTime, setCurrentTime] = useState(new Date()); + const scrollRef = useRef(null); + + const timeSlots = Array.from({ length: 24 }, (_, i) => i); + const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 }); + const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)); + const isCurrentDay = isSameDay(currentDate, new Date()); + const dayEvents = getWeekEvents(currentDate, events) || []; + + useEffect(() => { + const interval = setInterval(() => setCurrentTime(new Date()), 60000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + if (scrollRef.current && isCurrentDay) { + const currentHour = new Date().getHours(); + setTimeout(() => { + scrollRef.current.scrollTop = currentHour * 80 - 200; + }, 0); + } + }, [currentDate, isCurrentDay]); + + const getCurrentTimePosition = () => { + const hours = currentTime.getHours(); + const minutes = currentTime.getMinutes(); + return `${(hours + minutes / 60) * 5}rem`; + }; + + const calculateEventStyle = (event, allDayEvents) => { + const start = new Date(event.start); + const end = new Date(event.end); + const startMinutes = (start.getMinutes() / 60) * 5; + const duration = ((end - start) / (1000 * 60 * 60)) * 5; + + const overlapping = allDayEvents.filter((other) => { + if (other.id === event.id) return false; + const oStart = new Date(other.start); + const oEnd = new Date(other.end); + return !(oEnd <= start || oStart >= end); + }); + + const eventIndex = overlapping.findIndex((e) => e.id > event.id) + 1; + const total = overlapping.length + 1; + + return { + height: `${Math.max(duration, 1.5)}rem`, + position: 'absolute', + width: `calc((100% / ${total}) - 4px)`, + left: `calc((100% / ${total}) * ${eventIndex})`, + backgroundColor: `${event.color}15`, + borderLeft: `3px solid ${event.color}`, + borderRadius: '0.25rem', + zIndex: 1, + transform: `translateY(${startMinutes}rem)`, + }; + }; + + return ( +
+ {/* Barre de navigation (remplace le header Calendar sur mobile) */} +
+ + +
+ + + +
+ + +
+ + {/* Bandeau jours de la semaine */} +
+ {weekDays.map((day) => ( + + ))} +
+ + {/* Grille horaire */} +
+ {isCurrentDay && ( +
+
+
+ )} + +
+ {timeSlots.map((hour) => ( + +
+ {`${hour.toString().padStart(2, '0')}:00`} +
+
{ + const date = new Date(currentDate); + date.setHours(hour); + date.setMinutes(0); + onDateClick(date); + } + } + > + {dayEvents + .filter((e) => new Date(e.start).getHours() === hour) + .map((event) => ( +
{ + e.stopPropagation(); + onEventClick(event); + } + } + > +
+
+ {event.title} +
+
+ {format(new Date(event.start), 'HH:mm')} –{' '} + {format(new Date(event.end), 'HH:mm')} +
+ {event.location && ( +
+ {event.location} +
+ )} +
+
+ ))} +
+
+ ))} +
+
+
+ ); +}; + +export default DayView; diff --git a/Front-End/src/components/Calendar/EventModal.js b/Front-End/src/components/Calendar/EventModal.js index d89c098..04d8e22 100644 --- a/Front-End/src/components/Calendar/EventModal.js +++ b/Front-End/src/components/Calendar/EventModal.js @@ -253,7 +253,7 @@ export default function EventModal({ )} {/* Dates */} -
+
+
diff --git a/Front-End/src/components/Calendar/ScheduleNavigation.js b/Front-End/src/components/Calendar/ScheduleNavigation.js index e9c60ac..e7670fc 100644 --- a/Front-End/src/components/Calendar/ScheduleNavigation.js +++ b/Front-End/src/components/Calendar/ScheduleNavigation.js @@ -3,7 +3,7 @@ import { usePlanning, PlanningModes } from '@/context/PlanningContext'; import { Plus, Edit2, Eye, EyeOff, Check, X } from 'lucide-react'; import logger from '@/utils/logger'; -export default function ScheduleNavigation({ classes, modeSet = 'event' }) { +export default function ScheduleNavigation({ classes, modeSet = 'event', isOpen = false, onClose = () => {} }) { const { schedules, selectedSchedule, @@ -62,22 +62,10 @@ export default function ScheduleNavigation({ classes, modeSet = 'event' }) { } }; - return ( - + + ); + + return ( + <> + {/* Desktop : sidebar fixe */} + + + {/* Mobile : drawer en overlay */} +
+
+
+
+

{title}

+
+ + +
+
+
+ {listContent} +
+
+
+ ); } diff --git a/Front-End/src/components/Calendar/YearView.js b/Front-End/src/components/Calendar/YearView.js index e6261ed..9c90c18 100644 --- a/Front-End/src/components/Calendar/YearView.js +++ b/Front-End/src/components/Calendar/YearView.js @@ -36,7 +36,7 @@ const YearView = ({ onDateClick }) => { }; return ( -
+
{months.map((month) => ( idx !== -1); return ( -
+
{data.map((point, idx) => { - const barHeight = Math.max((point.value / maxValue) * chartHeight, 8); // min 8px + const barHeight = Math.max((point.value / maxValue) * chartHeight, 8); const isMax = maxIndices.includes(idx); return (
- {/* Valeur au-dessus de la barre */} + {/* Valeur au-dessus de la barre — hors de la zone hauteur fixe */} {point.value} + {/* Zone barres à hauteur fixe, alignées en bas */}
+ className="w-full flex items-end justify-center" + style={{ height: chartHeight }} + > +
+
+ {/* Label mois en dessous */} {point.month}
); diff --git a/Front-End/src/components/Chat/InstantChat.js b/Front-End/src/components/Chat/InstantChat.js index f2b9fb7..315ef0e 100644 --- a/Front-End/src/components/Chat/InstantChat.js +++ b/Front-End/src/components/Chat/InstantChat.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { MessageSquare, Plus, Search, Trash2 } from 'lucide-react'; +import { MessageSquare, Plus, Search, Trash2, ArrowLeft } from 'lucide-react'; import { useSession } from 'next-auth/react'; import { useCsrfToken } from '@/context/CsrfContext'; import { useNotification } from '@/context/NotificationContext'; @@ -99,6 +99,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => { // États pour la confirmation de suppression const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false); const [conversationToDelete, setConversationToDelete] = useState(null); + const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(true); // Refs const messagesEndRef = useRef(null); @@ -541,6 +542,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => { logger.debug('🔄 Sélection de la conversation:', conversation); setSelectedConversation(conversation); setTypingUsers([]); + setIsMobileSidebarOpen(false); // Utiliser id ou conversation_id selon ce qui est disponible const conversationId = conversation.id || conversation.conversation_id; @@ -828,7 +830,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => { return (
{/* Sidebar des conversations */} -
+
{/* En-tête */}
@@ -986,12 +988,20 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
{/* Zone de chat principale */} -
+
{selectedConversation ? ( <> {/* En-tête de la conversation */}
+ {/* Bouton retour liste sur mobile */} +
{/* Rectangle gauche avec l'icône */}
{icon}
{/* Zone de texte */} -
+

{title}

{message}

{type === 'error' && errorCode && ( diff --git a/Front-End/src/components/Footer.js b/Front-End/src/components/Footer.js index 3aaed9b..588f4ea 100644 --- a/Front-End/src/components/Footer.js +++ b/Front-End/src/components/Footer.js @@ -2,7 +2,7 @@ import Logo from '@/components/Logo'; export default function Footer({ softwareName, softwareVersion }) { return ( -
+
© {new Date().getFullYear()} N3WT-INNOV Tous droits réservés. diff --git a/Front-End/src/components/Grades/GradeView.js b/Front-End/src/components/Grades/GradeView.js index d245fba..d296ad3 100644 --- a/Front-End/src/components/Grades/GradeView.js +++ b/Front-End/src/components/Grades/GradeView.js @@ -1,5 +1,5 @@ import React, { useState, useMemo, useEffect } from 'react'; -import { BookOpen, CheckCircle, AlertCircle, Clock } from 'lucide-react'; +import { BookOpen, CheckCircle, AlertCircle, Clock, ChevronDown, ChevronRight } from 'lucide-react'; import RadioList from '@/components/Form/RadioList'; const LEVELS = [ @@ -86,9 +86,10 @@ export default function GradeView({ data, grades, onGradeChange }) { {domaine.domaine_nom}
- - {openDomains[domaine.domaine_id] ? '▼' : '►'} - + {openDomains[domaine.domaine_id] + ? + : + }
{openDomains[domaine.domaine_id] && (
@@ -99,7 +100,10 @@ export default function GradeView({ data, grades, onGradeChange }) { className="flex items-center gap-2 text-lg font-semibold text-emerald-700 mb-4 hover:underline" onClick={() => toggleCategory(categorie.categorie_id)} > - {openCategories[categorie.categorie_id] ? '▼' : '►'}{' '} + {openCategories[categorie.categorie_id] + ? + : + } {categorie.categorie_nom} {openCategories[categorie.categorie_id] && ( diff --git a/Front-End/src/components/MobileTopbar.js b/Front-End/src/components/MobileTopbar.js new file mode 100644 index 0000000..4869918 --- /dev/null +++ b/Front-End/src/components/MobileTopbar.js @@ -0,0 +1,18 @@ +'use client'; +import { Menu } from 'lucide-react'; +import ProfileSelector from '@/components/ProfileSelector'; + +export default function MobileTopbar({ onMenuClick }) { + return ( +
+ + +
+ ); +} diff --git a/Front-End/src/components/Pagination.js b/Front-End/src/components/Pagination.js index 9957381..4655495 100644 --- a/Front-End/src/components/Pagination.js +++ b/Front-End/src/components/Pagination.js @@ -6,11 +6,11 @@ const Pagination = ({ currentPage, totalPages, onPageChange }) => { const pages = Array.from({ length: totalPages }, (_, i) => i + 1); return ( -
+
{t('page')} {currentPage} {t('of')} {pages.length}
-
+
{currentPage > 1 && ( { +const ProfileSelector = ({ onRoleChange, className = '', compact = false }) => { const { establishments, selectedRoleId, @@ -103,50 +103,72 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => { // Suppression du tronquage JS, on utilise uniquement CSS const isSingleRole = establishments && establishments.length === 1; + const buttonContent = compact ? ( + /* Mode compact : avatar seul pour la topbar mobile */ +
+
+ Profile +
+
+ +
+ ) : ( + /* Mode normal : avatar + infos texte */ +
+
+ Profile +
+
+
+
+ {user?.email} +
+
+ {selectedEstablishment?.name || ''} +
+
+ {getRightStr(selectedEstablishment?.role_type) || ''} +
+
+ +
+ ); + return (
-
- Profile - {/* Bulle de statut de connexion au chat */} -
-
-
-
- {user?.email} -
-
- {selectedEstablishment?.name || ''} -
-
- {getRightStr(selectedEstablishment?.role_type) || ''} -
-
- -
- } + buttonContent={buttonContent} items={ isSingleRole ? [ @@ -190,7 +212,10 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => { ] } buttonClassName="w-full" - menuClassName="absolute mt-2 w-full bg-white border border-gray-200 rounded shadow-lg z-10" + menuClassName={compact + ? 'absolute right-0 mt-2 min-w-[200px] bg-white border border-gray-200 rounded shadow-lg z-50' + : 'absolute mt-2 w-full bg-white border border-gray-200 rounded shadow-lg z-10' + } dropdownOpen={dropdownOpen} setDropdownOpen={setDropdownOpen} /> diff --git a/Front-End/src/components/ServiceWorkerRegister.js b/Front-End/src/components/ServiceWorkerRegister.js new file mode 100644 index 0000000..6ee20a6 --- /dev/null +++ b/Front-End/src/components/ServiceWorkerRegister.js @@ -0,0 +1,15 @@ +'use client'; +import { useEffect } from 'react'; +import logger from '@/utils/logger'; + +export default function ServiceWorkerRegister() { + useEffect(() => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker + .register('/sw.js') + .catch((err) => logger.error('Service worker registration failed:', err)); + } + }, []); + + return null; +} diff --git a/Front-End/src/components/Sidebar.js b/Front-End/src/components/Sidebar.js index 0a064f6..41da641 100644 --- a/Front-End/src/components/Sidebar.js +++ b/Front-End/src/components/Sidebar.js @@ -34,7 +34,7 @@ function Sidebar({ currentPage, items, onCloseMobile }) { return (
-
+