mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Bilan de compétence d'un élève [#16]
This commit is contained in:
@ -325,7 +325,7 @@ class RegistrationSchoolFileTemplate(models.Model):
|
|||||||
class StudentCompetency(models.Model):
|
class StudentCompetency(models.Model):
|
||||||
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='competency_scores')
|
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')
|
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)
|
comment = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@ -63,12 +63,36 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
"data": result
|
"data": result
|
||||||
}, safe=False, status=200)
|
}, safe=False, status=200)
|
||||||
|
|
||||||
# def post(self, request):
|
def put(self, request):
|
||||||
# serializer = AbsenceManagementSerializer(data=request.data)
|
"""
|
||||||
# if serializer.is_valid():
|
Met à jour en masse les notes des compétences d'un élève.
|
||||||
# serializer.save()
|
Attend une liste d'objets {"competenceId": ..., "grade": ...}
|
||||||
# return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED)
|
"""
|
||||||
# return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
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(csrf_protect, name='dispatch')
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||||
|
|||||||
@ -1,144 +1,158 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
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 Button from '@/components/Button';
|
||||||
import Table from '@/components/Table';
|
import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL } from '@/utils/Url';
|
||||||
import logger from '@/utils/logger';
|
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() {
|
export default function Page() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
|
const { getNiveauLabel } = useClasses();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
selectedStudent: null,
|
selectedStudent: null,
|
||||||
absences: [],
|
|
||||||
competenceReview: [
|
|
||||||
{ competence: 'Lecture', score: null },
|
|
||||||
{ competence: 'Écriture', score: null },
|
|
||||||
{ competence: 'Mathématiques', score: null },
|
|
||||||
{ competence: 'Sciences', score: null },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const students = [
|
const [students, setStudents] = useState([]);
|
||||||
{ id: 1, name: 'John Doe', class: 'CM2' },
|
|
||||||
{ id: 2, name: 'Jane Smith', class: 'CE1' },
|
const academicResults = [
|
||||||
{ id: 3, name: 'Alice Johnson', class: 'CM1' },
|
{
|
||||||
|
subject: 'Mathématiques',
|
||||||
|
grade: 16,
|
||||||
|
average: 14,
|
||||||
|
appreciation: 'Très bon travail',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: 'Français',
|
||||||
|
grade: 15,
|
||||||
|
average: 13,
|
||||||
|
appreciation: 'Bonne participation',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const absences = [
|
const absences = [
|
||||||
{ date: '2023-09-01', reason: 'Maladie' },
|
{ date: '2023-09-01', type: 'Absence', reason: 'Maladie', justified: true },
|
||||||
{ date: '2023-09-15', reason: 'Vacances' },
|
{ date: '2023-09-15', type: 'Retard', reason: 'Trafic', justified: false },
|
||||||
{ date: '2023-10-05', reason: 'Retard justifié' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleChange = (field, value) => {
|
const remarks = [
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
{
|
||||||
};
|
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 workPlan = [
|
||||||
const updatedCompetenceReview = [...formData.competenceReview];
|
{
|
||||||
updatedCompetenceReview[index].score = score;
|
objective: 'Renforcer la lecture',
|
||||||
setFormData((prev) => ({
|
support: 'Exercices hebdomadaires',
|
||||||
...prev,
|
followUp: 'En cours',
|
||||||
competenceReview: updatedCompetenceReview,
|
},
|
||||||
}));
|
{
|
||||||
};
|
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 (
|
return (
|
||||||
<div className="p-8 space-y-8">
|
<div className="p-8 space-y-8">
|
||||||
<h1 className="heading-section">Suivi pédagogique</h1>
|
|
||||||
|
|
||||||
{/* Sélection de l'élève */}
|
{/* Sélection de l'élève */}
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-4">Sélectionner un élève</h2>
|
<h2 className="text-xl font-semibold mb-4">Sélectionner un élève</h2>
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-end gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
<SelectChoice
|
<SelectChoice
|
||||||
name="selectedStudent"
|
name="selectedStudent"
|
||||||
label="Élève"
|
label="Élève"
|
||||||
placeHolder="Sélectionnez un élève"
|
placeHolder="Sélectionnez un élève"
|
||||||
selected={formData.selectedStudent}
|
selected={formData.selectedStudent || ''}
|
||||||
callback={(e) => handleChange('selectedStudent', e.target.value)}
|
callback={(e) => handleChange('selectedStudent', e.target.value)}
|
||||||
choices={students.map((student) => ({
|
choices={students.map((student) => ({
|
||||||
value: student.id,
|
value: student.id,
|
||||||
label: `${student.name} - Classe : ${student.class}`,
|
label: `${student.last_name} ${student.first_name} - ${getNiveauLabel(student.level)} (${student.associated_class_name})`,
|
||||||
}))}
|
}))}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Liste des absences */}
|
|
||||||
{formData.selectedStudent && (
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">Liste des absences</h2>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{absences.map((absence, index) => (
|
|
||||||
<li
|
|
||||||
key={index}
|
|
||||||
className="flex justify-between items-center bg-gray-50 p-4 rounded-md border border-gray-100"
|
|
||||||
>
|
|
||||||
<span className="text-gray-800">{absence.date}</span>
|
|
||||||
<span className="text-gray-500 italic">{absence.reason}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Bilan de compétence */}
|
|
||||||
{formData.selectedStudent && (
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">Bilan de compétence</h2>
|
|
||||||
<Table
|
|
||||||
data={formData.competenceReview}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
name: 'Compétence',
|
|
||||||
transform: (row) => row.competence,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '1',
|
|
||||||
transform: (row, index) => (
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name={`score-${index}`}
|
|
||||||
value="1"
|
|
||||||
checked={row.score === '1'}
|
|
||||||
onChange={() => handleScoreChange(index, '1')}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '2',
|
|
||||||
transform: (row, index) => (
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name={`score-${index}`}
|
|
||||||
value="2"
|
|
||||||
checked={row.score === '2'}
|
|
||||||
onChange={() => handleScoreChange(index, '2')}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '3',
|
|
||||||
transform: (row, index) => (
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name={`score-${index}`}
|
|
||||||
value="3"
|
|
||||||
checked={row.score === '3'}
|
|
||||||
onChange={() => handleScoreChange(index, '3')}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<div className="mt-4">
|
|
||||||
<Button
|
<Button
|
||||||
text="Enregistrer"
|
text="Réaliser le bilan de compétences"
|
||||||
onClick={() => logger.debug('FormData:', formData)}
|
|
||||||
primary
|
primary
|
||||||
className="bg-emerald-500 text-white hover:bg-emerald-600"
|
disabled={!formData.selectedStudent}
|
||||||
|
onClick={() => {
|
||||||
|
const url = `${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${formData.selectedStudent}`;
|
||||||
|
router.push(`${url}`);
|
||||||
|
}}
|
||||||
|
className={`px-6 py-2 rounded-md shadow ${
|
||||||
|
!formData.selectedStudent
|
||||||
|
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||||
|
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{formData.selectedStudent && (
|
||||||
|
<>
|
||||||
|
<AcademicResults results={academicResults} />
|
||||||
|
<Attendance absences={absences} />
|
||||||
|
<Remarks remarks={remarks} />
|
||||||
|
<WorkPlan workPlan={workPlan} />
|
||||||
|
<Homeworks homeworks={homeworks} />
|
||||||
|
<SpecificEvaluations specificEvaluations={specificEvaluations} />
|
||||||
|
<Orientation orientation={orientation} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<div className="h-full flex flex-col p-4">
|
||||||
|
<SectionHeader
|
||||||
|
icon={Award}
|
||||||
|
title="Bilan de compétence"
|
||||||
|
description="Evaluez les compétence de l'élève"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
|
<form
|
||||||
|
className="flex-1 min-h-0 flex flex-col"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Zone scrollable pour les compétences */}
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||||
|
<GradeView
|
||||||
|
data={studentCompetencies.data}
|
||||||
|
grades={grades}
|
||||||
|
onGradeChange={handleGradeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<Button text="Enregistrer le bilan" primary type="submit" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,11 +4,38 @@ import {
|
|||||||
BE_SUBSCRIPTION_REGISTERFORMS_URL,
|
BE_SUBSCRIPTION_REGISTERFORMS_URL,
|
||||||
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
|
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
|
||||||
BE_SUBSCRIPTION_ABSENCES_URL,
|
BE_SUBSCRIPTION_ABSENCES_URL,
|
||||||
|
BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL,
|
||||||
} from '@/utils/Url';
|
} from '@/utils/Url';
|
||||||
|
|
||||||
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
|
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
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 = (
|
export const fetchRegisterForms = (
|
||||||
establishment,
|
establishment,
|
||||||
filter = CURRENT_YEAR_FILTER,
|
filter = CURRENT_YEAR_FILTER,
|
||||||
|
|||||||
28
Front-End/src/components/Grades/AcademicResults.js
Normal file
28
Front-End/src/components/Grades/AcademicResults.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function AcademicResults({ results }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Résultats académiques</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
{results.map((result, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="p-4 rounded-lg bg-emerald-50 flex flex-col gap-2 shadow"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="font-medium">{result.subject}</span>
|
||||||
|
<span className="text-emerald-700 font-bold text-lg">
|
||||||
|
{result.grade}/20
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
Moyenne classe : {result.average}
|
||||||
|
</div>
|
||||||
|
<div className="italic text-gray-500">{result.appreciation}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
Front-End/src/components/Grades/Attendance.js
Normal file
28
Front-End/src/components/Grades/Attendance.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Attendance({ absences }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Présence et assiduité</h2>
|
||||||
|
<ol className="relative border-l border-emerald-200">
|
||||||
|
{absences.map((absence, idx) => (
|
||||||
|
<li key={idx} className="mb-6 ml-4">
|
||||||
|
<div className="absolute w-3 h-3 bg-emerald-400 rounded-full mt-1.5 -left-1.5 border border-white" />
|
||||||
|
<time className="mb-1 text-xs font-normal leading-none text-gray-400">
|
||||||
|
{absence.date}
|
||||||
|
</time>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">{absence.type}</span>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-1 rounded ${absence.justified ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}
|
||||||
|
>
|
||||||
|
{absence.justified ? 'Justifiée' : 'Non justifiée'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">{absence.reason}</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
Front-End/src/components/Grades/GradeView.js
Normal file
155
Front-End/src/components/Grades/GradeView.js
Normal file
@ -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 (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 mr-4 text-right text-emerald-700 font-semibold">
|
||||||
|
{totalCompetencies} compétence{totalCompetencies > 1 ? 's' : ''} au
|
||||||
|
total
|
||||||
|
</div>
|
||||||
|
{data.map((domaine) => (
|
||||||
|
<div key={domaine.domaine_id} className="mb-8">
|
||||||
|
<div
|
||||||
|
className={'flex items-center justify-between cursor-pointer px-6 py-4 rounded-lg transition bg-emerald-50 border border-emerald-200 shadow-sm hover:bg-emerald-100'}
|
||||||
|
onClick={() => toggleDomain(domaine.domaine_id)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<BookOpen className="w-7 h-7 text-emerald-600" />
|
||||||
|
<span className="text-2xl font-bold text-emerald-800">
|
||||||
|
{domaine.domaine_nom}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-emerald-700 text-xl">
|
||||||
|
{openDomains[domaine.domaine_id] ? '▼' : '►'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{openDomains[domaine.domaine_id] && (
|
||||||
|
<div className="mt-4">
|
||||||
|
{domaine.categories.map((categorie) => (
|
||||||
|
<div key={categorie.categorie_id} className="mb-10 mr-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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] ? '▼' : '►'}{' '}
|
||||||
|
{categorie.categorie_nom}
|
||||||
|
</button>
|
||||||
|
{openCategories[categorie.categorie_id] && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-8 w-full">
|
||||||
|
{categorie.competences.map((competence) => {
|
||||||
|
const grade = grades[competence.competence_id];n (
|
||||||
|
<div
|
||||||
|
key={competence.competence_id}
|
||||||
|
className={`border rounded-xl p-6 flex flex-col shadow transition hover:shadow-md ${getGradeStyle(grade)}`}
|
||||||
|
>
|
||||||
|
<div className="mb-4 pb-4 border-b border-emerald-800 flex items-center min-h-[48px]">
|
||||||
|
<span className="text-gray-900 font-semibold text-base">
|
||||||
|
{competence.nom}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex items-center">
|
||||||
|
<RadioList
|
||||||
|
key={`grade-${competence.competence_id}-${grades[competence.competence_id] ?? 0}`}
|
||||||
|
items={LEVELS.map(({ value, label }) => ({
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<hr className="my-6 border-emerald-100" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
Front-End/src/components/Grades/Homeworks.js
Normal file
27
Front-End/src/components/Grades/Homeworks.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Homeworks({ homeworks }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Suivi des devoirs</h2>
|
||||||
|
<ul className="divide-y divide-gray-100">
|
||||||
|
{homeworks.map((hw, idx) => (
|
||||||
|
<li
|
||||||
|
key={idx}
|
||||||
|
className="py-3 flex flex-col sm:flex-row sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{hw.title}</span>
|
||||||
|
<span className="ml-2 text-xs text-gray-400">{hw.dueDate}</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-1 rounded ${hw.status === 'Rendu' ? 'bg-emerald-100 text-emerald-700' : 'bg-yellow-100 text-yellow-700'}`}
|
||||||
|
>
|
||||||
|
{hw.status}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
Front-End/src/components/Grades/Orientation.js
Normal file
23
Front-End/src/components/Grades/Orientation.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Orientation({ orientation }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Orientation & conseils</h2>
|
||||||
|
<ul className="divide-y divide-gray-100">
|
||||||
|
{orientation.map((item, idx) => (
|
||||||
|
<li
|
||||||
|
key={idx}
|
||||||
|
className="py-3 flex flex-col sm:flex-row sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{item.counselor}</span>
|
||||||
|
<span className="ml-2 text-xs text-gray-400">{item.date}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-700 mt-1 sm:mt-0">{item.advice}</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
Front-End/src/components/Grades/Remarks.js
Normal file
23
Front-End/src/components/Grades/Remarks.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Remarks({ remarks }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Remarques & observations</h2>
|
||||||
|
<ul className="divide-y divide-gray-100">
|
||||||
|
{remarks.map((remark, idx) => (
|
||||||
|
<li
|
||||||
|
key={idx}
|
||||||
|
className="py-3 flex flex-col sm:flex-row sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{remark.teacher}</span>
|
||||||
|
<span className="ml-2 text-xs text-gray-400">{remark.date}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-700 mt-1 sm:mt-0">{remark.comment}</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
Front-End/src/components/Grades/SpecificEvaluations.js
Normal file
27
Front-End/src/components/Grades/SpecificEvaluations.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function SpecificEvaluations({ specificEvaluations }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Évaluations spécifiques</h2>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{specificEvaluations.map((evalItem, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="p-3 rounded border border-blue-100 bg-blue-50 flex flex-col sm:flex-row sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{evalItem.test}</span>
|
||||||
|
<span className="ml-2 text-xs text-gray-500">
|
||||||
|
{evalItem.date}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs px-2 py-1 rounded bg-blue-200 text-blue-800 mt-1 sm:mt-0">
|
||||||
|
{evalItem.result}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
Front-End/src/components/Grades/WorkPlan.js
Normal file
29
Front-End/src/components/Grades/WorkPlan.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function WorkPlan({ workPlan }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">
|
||||||
|
Plan de travail personnalisé
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{workPlan.map((plan, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="p-3 rounded border border-emerald-100 bg-emerald-50 flex flex-col sm:flex-row sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{plan.objective}</span>
|
||||||
|
<span className="ml-2 text-xs text-gray-500">
|
||||||
|
({plan.support})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs px-2 py-1 rounded bg-emerald-200 text-emerald-800 mt-1 sm:mt-0">
|
||||||
|
{plan.followUp}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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_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_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`;
|
||||||
export const BE_SUBSCRIPTION_ABSENCES_URL = `${BASE_URL}/Subscriptions/absences`;
|
export const BE_SUBSCRIPTION_ABSENCES_URL = `${BASE_URL}/Subscriptions/absences`;
|
||||||
|
export const BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL = `${BASE_URL}/Subscriptions/studentCompetencies`;
|
||||||
|
|
||||||
//GESTION ECOLE
|
//GESTION ECOLE
|
||||||
export const BE_SCHOOL_SPECIALITIES_URL = `${BASE_URL}/School/specialities`;
|
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
|
//ADMIN/GRADES URL
|
||||||
export const FE_ADMIN_GRADES_URL = '/admin/grades';
|
export const FE_ADMIN_GRADES_URL = '/admin/grades';
|
||||||
|
export const FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL =
|
||||||
|
'/admin/grades/studentCompetencies';
|
||||||
|
|
||||||
//ADMIN/TEACHERS URL
|
//ADMIN/TEACHERS URL
|
||||||
export const FE_ADMIN_TEACHERS_URL = '/admin/teachers';
|
export const FE_ADMIN_TEACHERS_URL = '/admin/teachers';
|
||||||
|
|||||||
Reference in New Issue
Block a user