mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 20:51:26 +00:00
feat: Ajout d'un système de notation par classe et par matière et par élève [N3WTS-6]
This commit is contained in:
298
Front-End/src/components/Evaluation/EvaluationStudentView.js
Normal file
298
Front-End/src/components/Evaluation/EvaluationStudentView.js
Normal file
@ -0,0 +1,298 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import { BookOpen, TrendingUp, TrendingDown, Minus, Pencil, Trash2, Save, X } from 'lucide-react';
|
||||
|
||||
export default function EvaluationStudentView({
|
||||
evaluations,
|
||||
studentEvaluations,
|
||||
onUpdateGrade,
|
||||
onDeleteGrade,
|
||||
editable = false
|
||||
}) {
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editScore, setEditScore] = useState('');
|
||||
const [editComment, setEditComment] = useState('');
|
||||
const [editAbsent, setEditAbsent] = useState(false);
|
||||
|
||||
if (!evaluations || evaluations.length === 0) {
|
||||
return (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
Aucune évaluation pour cette période
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const startEdit = (ev, studentEval) => {
|
||||
setEditingId(ev.id);
|
||||
setEditScore(studentEval?.score ?? '');
|
||||
setEditComment(studentEval?.comment ?? '');
|
||||
setEditAbsent(studentEval?.is_absent ?? false);
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null);
|
||||
setEditScore('');
|
||||
setEditComment('');
|
||||
setEditAbsent(false);
|
||||
};
|
||||
|
||||
const handleSaveEdit = async (ev, studentEval) => {
|
||||
if (onUpdateGrade && studentEval) {
|
||||
await onUpdateGrade(studentEval.id, {
|
||||
score: editAbsent ? null : (editScore === '' ? null : parseFloat(editScore)),
|
||||
comment: editComment,
|
||||
is_absent: editAbsent,
|
||||
});
|
||||
}
|
||||
cancelEdit();
|
||||
};
|
||||
|
||||
const handleDelete = async (studentEval) => {
|
||||
if (onDeleteGrade && studentEval && confirm('Supprimer cette note ?')) {
|
||||
await onDeleteGrade(studentEval.id);
|
||||
}
|
||||
};
|
||||
|
||||
// Grouper les évaluations par matière
|
||||
const groupedBySpeciality = evaluations.reduce((acc, ev) => {
|
||||
const key = ev.speciality_name || 'Sans matière';
|
||||
if (!acc[key]) {
|
||||
acc[key] = {
|
||||
name: key,
|
||||
color: ev.speciality_color || '#6B7280',
|
||||
evaluations: [],
|
||||
totalScore: 0,
|
||||
totalMaxScore: 0,
|
||||
totalCoef: 0,
|
||||
weightedSum: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const studentEval = studentEvaluations.find(
|
||||
(se) => se.evaluation === ev.id
|
||||
);
|
||||
|
||||
const evalData = {
|
||||
...ev,
|
||||
studentScore: studentEval?.score,
|
||||
studentComment: studentEval?.comment,
|
||||
isAbsent: studentEval?.is_absent,
|
||||
};
|
||||
|
||||
acc[key].evaluations.push(evalData);
|
||||
|
||||
// Calcul de la moyenne pondérée
|
||||
if (studentEval?.score != null && !studentEval?.is_absent) {
|
||||
const normalizedScore = (studentEval.score / ev.max_score) * 20;
|
||||
acc[key].weightedSum += normalizedScore * ev.coefficient;
|
||||
acc[key].totalCoef += parseFloat(ev.coefficient);
|
||||
acc[key].totalScore += studentEval.score;
|
||||
acc[key].totalMaxScore += parseFloat(ev.max_score);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Calcul de la moyenne générale
|
||||
let totalWeightedSum = 0;
|
||||
let totalCoef = 0;
|
||||
Object.values(groupedBySpeciality).forEach((group) => {
|
||||
if (group.totalCoef > 0) {
|
||||
const groupAvg = group.weightedSum / group.totalCoef;
|
||||
totalWeightedSum += groupAvg * group.totalCoef;
|
||||
totalCoef += group.totalCoef;
|
||||
}
|
||||
});
|
||||
const generalAverage = totalCoef > 0 ? totalWeightedSum / totalCoef : null;
|
||||
|
||||
const getScoreColor = (score, maxScore) => {
|
||||
if (score == null) return 'text-gray-400';
|
||||
const percentage = (score / maxScore) * 100;
|
||||
if (percentage >= 70) return 'text-green-600';
|
||||
if (percentage >= 50) return 'text-yellow-600';
|
||||
return 'text-red-600';
|
||||
};
|
||||
|
||||
const getAverageIcon = (avg) => {
|
||||
if (avg >= 14) return <TrendingUp size={16} className="text-green-500" />;
|
||||
if (avg >= 10) return <Minus size={16} className="text-yellow-500" />;
|
||||
return <TrendingDown size={16} className="text-red-500" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Moyenne générale */}
|
||||
{generalAverage !== null && (
|
||||
<div className="bg-emerald-50 border border-emerald-200 rounded-lg p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="text-emerald-600" size={24} />
|
||||
<span className="font-medium text-emerald-800">Moyenne générale</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{getAverageIcon(generalAverage)}
|
||||
<span className="text-2xl font-bold text-emerald-700">
|
||||
{generalAverage.toFixed(2)}/20
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Évaluations par matière */}
|
||||
{Object.values(groupedBySpeciality).map((group) => {
|
||||
const groupAverage =
|
||||
group.totalCoef > 0 ? group.weightedSum / group.totalCoef : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={group.name}
|
||||
className="border border-gray-200 rounded-lg overflow-hidden"
|
||||
>
|
||||
{/* Header de la matière */}
|
||||
<div
|
||||
className="p-3 flex items-center justify-between"
|
||||
style={{ backgroundColor: `${group.color}15` }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: group.color }}
|
||||
/>
|
||||
<span className="font-semibold text-gray-800">{group.name}</span>
|
||||
</div>
|
||||
{groupAverage !== null && (
|
||||
<div className="flex items-center gap-2">
|
||||
{getAverageIcon(groupAverage)}
|
||||
<span className="font-bold" style={{ color: group.color }}>
|
||||
{groupAverage.toFixed(2)}/20
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Liste des évaluations */}
|
||||
<div className="divide-y divide-gray-100">
|
||||
{group.evaluations.map((ev) => {
|
||||
const studentEval = studentEvaluations.find(se => se.evaluation === ev.id);
|
||||
const isEditing = editingId === ev.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ev.id}
|
||||
className="p-3 flex items-center justify-between hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-700">{ev.name}</div>
|
||||
<div className="text-sm text-gray-500 flex gap-2">
|
||||
{ev.date && (
|
||||
<span>
|
||||
{new Date(ev.date).toLocaleDateString('fr-FR')}
|
||||
</span>
|
||||
)}
|
||||
<span>Coef: {ev.coefficient}</span>
|
||||
</div>
|
||||
{!isEditing && ev.studentComment && (
|
||||
<div className="text-sm text-gray-500 italic mt-1">
|
||||
"{ev.studentComment}"
|
||||
</div>
|
||||
)}
|
||||
{isEditing && (
|
||||
<input
|
||||
type="text"
|
||||
value={editComment}
|
||||
onChange={(e) => setEditComment(e.target.value)}
|
||||
placeholder="Commentaire"
|
||||
className="mt-2 w-full text-sm px-2 py-1 border rounded"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<label className="flex items-center gap-1 text-sm text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editAbsent}
|
||||
onChange={(e) => {
|
||||
setEditAbsent(e.target.checked);
|
||||
if (e.target.checked) setEditScore('');
|
||||
}}
|
||||
/>
|
||||
Absent
|
||||
</label>
|
||||
{!editAbsent && (
|
||||
<input
|
||||
type="number"
|
||||
value={editScore}
|
||||
onChange={(e) => setEditScore(e.target.value)}
|
||||
min="0"
|
||||
max={ev.max_score}
|
||||
step="0.5"
|
||||
className="w-16 text-center px-2 py-1 border rounded"
|
||||
/>
|
||||
)}
|
||||
<span className="text-gray-500">/{ev.max_score}</span>
|
||||
<button
|
||||
onClick={() => handleSaveEdit(ev, studentEval)}
|
||||
className="p-1 text-emerald-600 hover:bg-emerald-50 rounded"
|
||||
title="Enregistrer"
|
||||
>
|
||||
<Save size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="p-1 text-gray-500 hover:bg-gray-100 rounded"
|
||||
title="Annuler"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{ev.isAbsent ? (
|
||||
<span className="px-2 py-1 bg-red-100 text-red-700 rounded text-sm font-medium">
|
||||
Absent
|
||||
</span>
|
||||
) : ev.studentScore != null ? (
|
||||
<span
|
||||
className={`text-lg font-bold ${getScoreColor(
|
||||
ev.studentScore,
|
||||
ev.max_score
|
||||
)}`}
|
||||
>
|
||||
{ev.studentScore}/{ev.max_score}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400 text-sm">Non noté</span>
|
||||
)}
|
||||
{editable && studentEval && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => startEdit(ev, studentEval)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
||||
title="Modifier"
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
{onDeleteGrade && (
|
||||
<button
|
||||
onClick={() => handleDelete(studentEval)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user