Files
n3wt-school/Front-End/src/components/Evaluation/EvaluationStudentView.js
2026-04-05 12:00:34 +02:00

299 lines
11 KiB
JavaScript

'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-primary/5 border border-primary/20 rounded-lg p-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<BookOpen className="text-primary" size={24} />
<span className="font-medium text-secondary">Moyenne générale</span>
</div>
<div className="flex items-center gap-2">
{getAverageIcon(generalAverage)}
<span className="text-2xl font-bold text-secondary">
{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">
&quot;{ev.studentComment}&quot;
</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-primary hover:bg-primary/5 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>
);
}