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 @@
+
\ 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?.[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 (
+
+ );
+ 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?.[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
-
-
-
- {/* 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}
+