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:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user