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

- -
- )} - - {/* 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 ( +
+ +
+
{ + e.preventDefault(); + handleSubmit(); + }} + > + {/* Zone scrollable pour les compétences */} +
+ +
+
+
+ +
+
+ ); +} 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) => ( +
  1. +
    + +
    + {absence.type} + + {absence.justified ? 'Justifiée' : 'Non justifiée'} + +
    +
    {absence.reason}
    +
  2. + ))} +
+
+ ); +} 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) => ( +
+ + {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 ( +
+

Suivi des devoirs

+
    + {homeworks.map((hw, idx) => ( +
  • +
    + {hw.title} + {hw.dueDate} +
    + + {hw.status} + +
  • + ))} +
+
+ ); +} 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

+
    + {orientation.map((item, idx) => ( +
  • +
    + {item.counselor} + {item.date} +
    +
    {item.advice}
    +
  • + ))} +
+
+ ); +} 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

+
    + {remarks.map((remark, idx) => ( +
  • +
    + {remark.teacher} + {remark.date} +
    +
    {remark.comment}
    +
  • + ))} +
+
+ ); +} 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';