feat: Sauvegarde des compétences d'un élève [#16]

This commit is contained in:
N3WT DE COMPET
2025-05-20 17:31:50 +02:00
parent c9c7e7715e
commit 05136035ab
19 changed files with 269 additions and 137 deletions

View File

@ -23,6 +23,7 @@
"next-logger": "^5.0.1",
"pino": "^9.6.0",
"react": "^18",
"react-circular-progressbar": "^2.2.0",
"react-cookie": "^7.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
@ -5015,6 +5016,15 @@
"node": ">=0.10.0"
}
},
"node_modules/react-circular-progressbar": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz",
"integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==",
"license": "MIT",
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/react-cookie": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.2.tgz",
@ -10037,6 +10047,12 @@
"loose-envify": "^1.1.0"
}
},
"react-circular-progressbar": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz",
"integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==",
"requires": {}
},
"react-cookie": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.2.tgz",

View File

@ -26,6 +26,7 @@
"next-logger": "^5.0.1",
"pino": "^9.6.0",
"react": "^18",
"react-circular-progressbar": "^2.2.0",
"react-cookie": "^7.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",

View File

@ -8,11 +8,15 @@ 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/Button';
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 {
fetchStudents,
fetchStudentCompetencies,
} from '@/app/actions/subscriptionAction';
import { useEstablishment } from '@/context/EstablishmentContext';
import { useClasses } from '@/context/ClassesContext';
@ -25,6 +29,8 @@ export default function Page() {
});
const [students, setStudents] = useState([]);
const [studentCompetencies, setStudentCompetencies] = useState(null);
const [grades, setGrades] = useState({});
const academicResults = [
{
@ -106,6 +112,34 @@ export default function Page() {
}
}, [selectedEstablishmentId]);
// Charger les compétences et générer les grades à chaque changement d'élève sélectionné
useEffect(() => {
if (formData.selectedStudent) {
fetchStudentCompetencies(formData.selectedStudent)
.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);
}
})
.catch((error) =>
logger.error('Error fetching studentCompetencies:', error)
);
} else {
setGrades({});
setStudentCompetencies(null);
}
}, [formData.selectedStudent]);
return (
<div className="p-8 space-y-8">
{/* Sélection de l'élève */}
@ -145,13 +179,14 @@ export default function Page() {
{formData.selectedStudent && (
<>
<AcademicResults results={academicResults} />
{/* <AcademicResults results={academicResults} /> */}
<Attendance absences={absences} />
<Remarks remarks={remarks} />
<GradesStatsCircle grades={grades} />
{/* <Remarks remarks={remarks} />
<WorkPlan workPlan={workPlan} />
<Homeworks homeworks={homeworks} />
<SpecificEvaluations specificEvaluations={specificEvaluations} />
<Orientation orientation={orientation} />
<Orientation orientation={orientation} /> */}
</>
)}
</div>

View File

@ -11,18 +11,13 @@ import {
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 à loral', score: null },
];
import { useNotification } from '@/context/NotificationContext';
export default function StudentCompetenciesPage() {
const searchParams = useSearchParams();
const router = useRouter();
const csrfToken = useCsrfToken();
const { showNotification } = useNotification();
const [studentCompetencies, setStudentCompetencies] = useState([]);
const [grades, setGrades] = useState({});
const studentId = searchParams.get('studentId');
@ -70,14 +65,21 @@ export default function StudentCompetenciesPage() {
competenceId,
grade: score,
}));
editStudentCompetencies(data, csrfToken)
.then(() => {
alert('Bilan de compétence enregistré !');
showNotification(
'Bilan de compétence sauvegardé avec succès',
'success',
'Succès'
);
router.back();
})
.catch((error) => {
alert("Erreur lors de l'enregistrement du bilan");
showNotification(
"Erreur lors de l'enregistrement du bilan de compétence",
'error',
'Erreur'
);
});
};

View File

@ -75,7 +75,9 @@ export default function GradeView({ data, grades, onGradeChange }) {
{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'}
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">

View File

@ -0,0 +1,40 @@
import React from 'react';
import { CircularProgressbar, buildStyles } from 'react-circular-progressbar';
import 'react-circular-progressbar/dist/styles.css';
export default function GradesStatsCircle({ grades }) {
// grades : { [competence_id]: grade }
const total = Object.keys(grades).length;
const acquired = Object.values(grades).filter((g) => g === 3).length;
const inProgress = Object.values(grades).filter((g) => g === 2).length;
const notAcquired = Object.values(grades).filter((g) => g === 1).length;
const notEvaluated = Object.values(grades).filter((g) => g === 0).length;
// Pourcentage d'acquis
const percent = total ? Math.round((acquired / total) * 100) : 0;
return (
<div className="flex flex-col items-center gap-4 bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-2">Statistiques globales</h2>
<div style={{ width: 120, height: 120 }}>
<CircularProgressbar
value={percent}
text={`${percent}%`}
styles={buildStyles({
textColor: '#059669',
pathColor: '#059669',
trailColor: '#d1fae5',
})}
/>
</div>
<div className="flex flex-col items-center text-sm mt-2">
<span className="text-emerald-700 font-semibold">
{acquired} acquis
</span>
<span className="text-yellow-600">{inProgress} en cours</span>
<span className="text-red-500">{notAcquired} non acquis</span>
<span className="text-gray-400">{notEvaluated} non évalués</span>
</div>
</div>
);
}