diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py
index a00f6cd..5068ae1 100644
--- a/Back-End/Subscriptions/models.py
+++ b/Back-End/Subscriptions/models.py
@@ -325,7 +325,7 @@ class RegistrationSchoolFileTemplate(models.Model):
class StudentCompetency(models.Model):
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='competency_scores')
competency = models.ForeignKey('Common.Competency', on_delete=models.CASCADE, related_name='student_scores')
- score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
+ score = models.IntegerField(null=True, blank=True)
comment = models.TextField(blank=True, null=True)
class Meta:
diff --git a/Back-End/Subscriptions/views/student_competencies_views.py b/Back-End/Subscriptions/views/student_competencies_views.py
index 9e9e150..40e8732 100644
--- a/Back-End/Subscriptions/views/student_competencies_views.py
+++ b/Back-End/Subscriptions/views/student_competencies_views.py
@@ -63,12 +63,36 @@ class StudentCompetencyListCreateView(APIView):
"data": result
}, safe=False, status=200)
- # def post(self, request):
- # serializer = AbsenceManagementSerializer(data=request.data)
- # if serializer.is_valid():
- # serializer.save()
- # return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED)
- # return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
+ def put(self, request):
+ """
+ Met à jour en masse les notes des compétences d'un élève.
+ Attend une liste d'objets {"competenceId": ..., "grade": ...}
+ """
+ data = request.data
+ if not isinstance(data, list):
+ return JsonResponse({"error": "Une liste est attendue."}, status=400)
+ updated = []
+ errors = []
+ for item in data:
+ comp_id = item.get("competenceId")
+ grade = item.get("grade")
+ student_id = item.get('studentId')
+ print(f'lecture des données : {comp_id} - {grade} - {student_id}')
+ if comp_id is None or grade is None:
+ errors.append({"competenceId": comp_id, "error": "champ manquant"})
+ continue
+ try:
+ # Ajoute le filtre student_id
+ sc = StudentCompetency.objects.get(
+ competency_id=comp_id,
+ student_id=student_id
+ )
+ sc.score = grade
+ sc.save()
+ updated.append(comp_id)
+ except StudentCompetency.DoesNotExist:
+ errors.append({"competenceId": comp_id, "error": "not found"})
+ return JsonResponse({"updated": updated, "errors": errors}, status=200)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
diff --git a/Front-End/src/app/[locale]/admin/grades/page.js b/Front-End/src/app/[locale]/admin/grades/page.js
index 0df2923..8a4e599 100644
--- a/Front-End/src/app/[locale]/admin/grades/page.js
+++ b/Front-End/src/app/[locale]/admin/grades/page.js
@@ -1,144 +1,158 @@
'use client';
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import SelectChoice from '@/components/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 Button from '@/components/Button';
-import Table from '@/components/Table';
-import logger from '@/utils/logger';
+import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL } from '@/utils/Url';
+import { useRouter } from 'next/navigation';
+import { fetchStudents } from '@/app/actions/subscriptionAction';
+
+import { useEstablishment } from '@/context/EstablishmentContext';
+import { useClasses } from '@/context/ClassesContext';
export default function Page() {
+ const router = useRouter();
+ const { selectedEstablishmentId } = useEstablishment();
+ const { getNiveauLabel } = useClasses();
const [formData, setFormData] = useState({
selectedStudent: null,
- absences: [],
- competenceReview: [
- { competence: 'Lecture', score: null },
- { competence: 'Écriture', score: null },
- { competence: 'Mathématiques', score: null },
- { competence: 'Sciences', score: null },
- ],
});
- const students = [
- { id: 1, name: 'John Doe', class: 'CM2' },
- { id: 2, name: 'Jane Smith', class: 'CE1' },
- { id: 3, name: 'Alice Johnson', class: 'CM1' },
+ const [students, setStudents] = useState([]);
+
+ 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 absences = [
- { date: '2023-09-01', reason: 'Maladie' },
- { date: '2023-09-15', reason: 'Vacances' },
- { date: '2023-10-05', reason: 'Retard justifié' },
+ { date: '2023-09-01', type: 'Absence', reason: 'Maladie', justified: true },
+ { date: '2023-09-15', type: 'Retard', reason: 'Trafic', justified: false },
];
- const handleChange = (field, value) => {
- setFormData((prev) => ({ ...prev, [field]: value }));
- };
+ 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 handleScoreChange = (index, score) => {
- const updatedCompetenceReview = [...formData.competenceReview];
- updatedCompetenceReview[index].score = score;
- setFormData((prev) => ({
- ...prev,
- competenceReview: updatedCompetenceReview,
- }));
- };
+ 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 }));
+
+ useEffect(() => {
+ if (selectedEstablishmentId) {
+ fetchStudents(selectedEstablishmentId)
+ .then((studentsData) => {
+ setStudents(studentsData);
+ })
+ .catch((error) => logger.error('Error fetching students:', error));
+ }
+ }, [selectedEstablishmentId]);
return (
-
Suivi pédagogique
-
{/* Sélection de l'élève */}
Sélectionner un élève
- handleChange('selectedStudent', e.target.value)}
- choices={students.map((student) => ({
- value: student.id,
- label: `${student.name} - Classe : ${student.class}`,
- }))}
- required
- />
-
-
- {/* Liste des absences */}
- {formData.selectedStudent && (
-
-
Liste des absences
-
- {absences.map((absence, index) => (
- -
- {absence.date}
- {absence.reason}
-
- ))}
-
-
- )}
-
- {/* Bilan de compétence */}
- {formData.selectedStudent && (
-
-
Bilan de compétence
-
row.competence,
- },
- {
- name: '1',
- transform: (row, index) => (
- handleScoreChange(index, '1')}
- />
- ),
- },
- {
- name: '2',
- transform: (row, index) => (
- handleScoreChange(index, '2')}
- />
- ),
- },
- {
- name: '3',
- transform: (row, index) => (
- handleScoreChange(index, '3')}
- />
- ),
- },
- ]}
- />
-
-
+
+ {formData.selectedStudent && (
+ <>
+
+
+
+
+
+
+
+ >
)}
);
diff --git a/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js b/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js
new file mode 100644
index 0000000..b12b370
--- /dev/null
+++ b/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js
@@ -0,0 +1,114 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import { useSearchParams, useRouter } from 'next/navigation';
+import Button from '@/components/Button';
+import GradeView from '@/components/Grades/GradeView';
+import {
+ fetchStudentCompetencies,
+ editStudentCompetencies,
+} from '@/app/actions/subscriptionAction';
+import SectionHeader from '@/components/SectionHeader';
+import { Award } from 'lucide-react';
+import { useCsrfToken } from '@/context/CsrfContext';
+
+// À remplacer par un fetch réel des compétences selon l'élève
+const mockCompetencies = [
+ { id: 1, name: 'Lire un texte court', score: null },
+ { id: 2, name: 'Résoudre un problème simple', score: null },
+ { id: 3, name: 'Exprimer une idée à l’oral', score: null },
+];
+
+export default function StudentCompetenciesPage() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const csrfToken = useCsrfToken();
+ const [studentCompetencies, setStudentCompetencies] = useState([]);
+ const [grades, setGrades] = useState({});
+ const studentId = searchParams.get('studentId');
+
+ useEffect(() => {
+ fetchStudentCompetencies(studentId)
+ .then((data) => {
+ setStudentCompetencies(data);
+ })
+ .catch((error) =>
+ logger.error('Error fetching studentCompetencies:', error)
+ );
+ }, []);
+
+ useEffect(() => {
+ if (studentCompetencies.data) {
+ const initialGrades = {};
+ studentCompetencies.data.forEach((domaine) => {
+ domaine.categories.forEach((cat) => {
+ cat.competences.forEach((comp) => {
+ initialGrades[comp.competence_id] = comp.score ?? 0;
+ });
+ });
+ });
+ setGrades(initialGrades);
+ }
+ }, [studentCompetencies.data]);
+
+ const handleScoreChange = (competencyId, score) => {
+ setCompetencies((prev) =>
+ prev.map((comp) => (comp.id === competencyId ? { ...comp, score } : comp))
+ );
+ };
+
+ const handleGradeChange = (competenceId, level) => {
+ setGrades((prev) => ({
+ ...prev,
+ [competenceId]: level,
+ }));
+ };
+
+ const handleSubmit = () => {
+ const data = Object.entries(grades).map(([competenceId, score]) => ({
+ studentId,
+ competenceId,
+ grade: score,
+ }));
+
+ editStudentCompetencies(data, csrfToken)
+ .then(() => {
+ alert('Bilan de compétence enregistré !');
+ router.back();
+ })
+ .catch((error) => {
+ alert("Erreur lors de l'enregistrement du bilan");
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/Front-End/src/app/actions/subscriptionAction.js b/Front-End/src/app/actions/subscriptionAction.js
index 9a20cc5..7da95ed 100644
--- a/Front-End/src/app/actions/subscriptionAction.js
+++ b/Front-End/src/app/actions/subscriptionAction.js
@@ -4,11 +4,38 @@ import {
BE_SUBSCRIPTION_REGISTERFORMS_URL,
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
BE_SUBSCRIPTION_ABSENCES_URL,
+ BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL,
} from '@/utils/Url';
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
+export const editStudentCompetencies = (data, csrfToken) => {
+ const request = new Request(`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}`, {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ headers: {
+ 'X-CSRFToken': csrfToken,
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ });
+ return fetch(request).then(requestResponseHandler).catch(errorHandler);
+};
+
+export const fetchStudentCompetencies = (id) => {
+ const request = new Request(
+ `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }
+ );
+ return fetch(request).then(requestResponseHandler).catch(errorHandler);
+};
+
export const fetchRegisterForms = (
establishment,
filter = CURRENT_YEAR_FILTER,
diff --git a/Front-End/src/components/Grades/AcademicResults.js b/Front-End/src/components/Grades/AcademicResults.js
new file mode 100644
index 0000000..2a81dc8
--- /dev/null
+++ b/Front-End/src/components/Grades/AcademicResults.js
@@ -0,0 +1,28 @@
+import React from 'react';
+
+export default function AcademicResults({ results }) {
+ return (
+
+
Résultats académiques
+
+ {results.map((result, idx) => (
+
+
+ {result.subject}
+
+ {result.grade}/20
+
+
+
+ Moyenne classe : {result.average}
+
+
{result.appreciation}
+
+ ))}
+
+
+ );
+}
diff --git a/Front-End/src/components/Grades/Attendance.js b/Front-End/src/components/Grades/Attendance.js
new file mode 100644
index 0000000..d95f772
--- /dev/null
+++ b/Front-End/src/components/Grades/Attendance.js
@@ -0,0 +1,28 @@
+import React from 'react';
+
+export default function Attendance({ absences }) {
+ return (
+
+
Présence et assiduité
+
+ {absences.map((absence, idx) => (
+ -
+
+
+
+ {absence.type}
+
+ {absence.justified ? 'Justifiée' : 'Non justifiée'}
+
+
+ {absence.reason}
+
+ ))}
+
+
+ );
+}
diff --git a/Front-End/src/components/Grades/GradeView.js b/Front-End/src/components/Grades/GradeView.js
new file mode 100644
index 0000000..6f240a7
--- /dev/null
+++ b/Front-End/src/components/Grades/GradeView.js
@@ -0,0 +1,155 @@
+import React, { useState, useMemo, useEffect } from 'react';
+import { BookOpen, CheckCircle, AlertCircle, Clock } from 'lucide-react';
+import RadioList from '@/components/RadioList';
+
+const LEVELS = [
+ { value: 0, label: 'Non évalué' },
+ { value: 1, label: '1 - Non acquis' },
+ { value: 2, label: "2 - En cours d'acquisition" },
+ { value: 3, label: '3 - Acquis' },
+];
+
+const getGradeStyle = (grade) => {
+ switch (grade) {
+ case 1:
+ return 'bg-red-50 border-red-200';
+ case 2:
+ return 'bg-yellow-50 border-yellow-200';
+ case 3:
+ return 'bg-emerald-50 border-emerald-200';
+ default:
+ return 'bg-gray-50 border-gray-200';
+ }
+};
+
+export default function GradeView({ data, grades, onGradeChange }) {
+ const [openDomains, setOpenDomains] = useState({});
+ const [openCategories, setOpenCategories] = useState({});
+
+ // Initialiser tout ouvert au premier rendu ou quand data change
+ useEffect(() => {
+ if (data && data.length > 0) {
+ // Initialisation des domaines et catégories (collapsed)
+ const domains = {};
+ const categories = {};
+ data.forEach((domaine) => {
+ domains[domaine.domaine_id] = false;
+ domaine.categories.forEach((cat) => {
+ categories[cat.categorie_id] = false;
+ });
+ });
+ setOpenDomains(domains);
+ setOpenCategories(categories);
+ }
+ }, [data]);
+
+ // Calcul du nombre total de compétences
+ const totalCompetencies = useMemo(
+ () =>
+ (data || []).reduce(
+ (sum, domaine) =>
+ sum +
+ domaine.categories.reduce(
+ (catSum, cat) => catSum + cat.competences.length,
+ 0
+ ),
+ 0
+ ),
+ [data]
+ );
+
+ if (!data) return null;
+
+ const toggleDomain = (id) =>
+ setOpenDomains((prev) => ({ ...prev, [id]: !prev[id] }));
+
+ const toggleCategory = (id) =>
+ setOpenCategories((prev) => ({ ...prev, [id]: !prev[id] }));
+
+ return (
+
+
+ {totalCompetencies} compétence{totalCompetencies > 1 ? 's' : ''} au
+ total
+
+ {data.map((domaine) => (
+
+
toggleDomain(domaine.domaine_id)}
+ >
+
+
+
+ {domaine.domaine_nom}
+
+
+
+ {openDomains[domaine.domaine_id] ? '▼' : '►'}
+
+
+ {openDomains[domaine.domaine_id] && (
+
+ {domaine.categories.map((categorie) => (
+
+
toggleCategory(categorie.categorie_id)}
+ >
+ {openCategories[categorie.categorie_id] ? '▼' : '►'}{' '}
+ {categorie.categorie_nom}
+
+ {openCategories[categorie.categorie_id] && (
+
+ {categorie.competences.map((competence) => {
+ const grade = grades[competence.competence_id];n (
+
+
+
+ {competence.nom}
+
+
+
+ ({
+ id: value,
+ label,
+ }))}
+ formData={{
+ [`grade-${competence.competence_id}`]:
+ grades[competence.competence_id] !==
+ undefined
+ ? grades[competence.competence_id]
+ : 0,
+ }}
+ handleChange={(e) =>
+ onGradeChange(
+ competence.competence_id,
+ parseInt(e.target.value, 10)
+ )
+ }
+ fieldName={`grade-${competence.competence_id}`}
+ disabled={competence.state === 'required'}
+ className="mt-2"
+ />
+
+
+ );
+ })}
+
+ )}
+
+ ))}
+
+ )}
+
+
+ ))}
+
+ );
+}
diff --git a/Front-End/src/components/Grades/Homeworks.js b/Front-End/src/components/Grades/Homeworks.js
new file mode 100644
index 0000000..1622f9d
--- /dev/null
+++ b/Front-End/src/components/Grades/Homeworks.js
@@ -0,0 +1,27 @@
+import React from 'react';
+
+export default function Homeworks({ homeworks }) {
+ return (
+
+ );
+}
diff --git a/Front-End/src/components/Grades/Orientation.js b/Front-End/src/components/Grades/Orientation.js
new file mode 100644
index 0000000..f8941cb
--- /dev/null
+++ b/Front-End/src/components/Grades/Orientation.js
@@ -0,0 +1,23 @@
+import React from 'react';
+
+export default function Orientation({ orientation }) {
+ return (
+
+
Orientation & conseils
+
+
+ );
+}
diff --git a/Front-End/src/components/Grades/Remarks.js b/Front-End/src/components/Grades/Remarks.js
new file mode 100644
index 0000000..33c9fa7
--- /dev/null
+++ b/Front-End/src/components/Grades/Remarks.js
@@ -0,0 +1,23 @@
+import React from 'react';
+
+export default function Remarks({ remarks }) {
+ return (
+
+
Remarques & observations
+
+
+ );
+}
diff --git a/Front-End/src/components/Grades/SpecificEvaluations.js b/Front-End/src/components/Grades/SpecificEvaluations.js
new file mode 100644
index 0000000..083badd
--- /dev/null
+++ b/Front-End/src/components/Grades/SpecificEvaluations.js
@@ -0,0 +1,27 @@
+import React from 'react';
+
+export default function SpecificEvaluations({ specificEvaluations }) {
+ return (
+
+
Évaluations spécifiques
+
+ {specificEvaluations.map((evalItem, idx) => (
+
+
+ {evalItem.test}
+
+ {evalItem.date}
+
+
+
+ {evalItem.result}
+
+
+ ))}
+
+
+ );
+}
diff --git a/Front-End/src/components/Grades/WorkPlan.js b/Front-End/src/components/Grades/WorkPlan.js
new file mode 100644
index 0000000..ebbead4
--- /dev/null
+++ b/Front-End/src/components/Grades/WorkPlan.js
@@ -0,0 +1,29 @@
+import React from 'react';
+
+export default function WorkPlan({ workPlan }) {
+ return (
+
+
+ Plan de travail personnalisé
+
+
+ {workPlan.map((plan, idx) => (
+
+
+ {plan.objective}
+
+ ({plan.support})
+
+
+
+ {plan.followUp}
+
+
+ ))}
+
+
+ );
+}
diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js
index b92ed79..f0caf95 100644
--- a/Front-End/src/utils/Url.js
+++ b/Front-End/src/utils/Url.js
@@ -32,6 +32,7 @@ export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL = `${BASE_URL}
export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationParentFileTemplates`;
export const BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`;
export const BE_SUBSCRIPTION_ABSENCES_URL = `${BASE_URL}/Subscriptions/absences`;
+export const BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL = `${BASE_URL}/Subscriptions/studentCompetencies`;
//GESTION ECOLE
export const BE_SCHOOL_SPECIALITIES_URL = `${BASE_URL}/School/specialities`;
@@ -96,6 +97,8 @@ export const FE_ADMIN_DIRECTORY_URL = '/admin/directory';
//ADMIN/GRADES URL
export const FE_ADMIN_GRADES_URL = '/admin/grades';
+export const FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL =
+ '/admin/grades/studentCompetencies';
//ADMIN/TEACHERS URL
export const FE_ADMIN_TEACHERS_URL = '/admin/teachers';