mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 20:51:26 +00:00
feat: Securisation du téléchargement de fichier
This commit is contained in:
@ -1,21 +1,34 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Award, Eye, Search, BarChart2, X, Pencil, Trash2, Save } from 'lucide-react';
|
||||
import {
|
||||
Award,
|
||||
Eye,
|
||||
Search,
|
||||
BarChart2,
|
||||
X,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Save,
|
||||
} from 'lucide-react';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import Table from '@/components/Table';
|
||||
import logger from '@/utils/logger';
|
||||
import {
|
||||
BASE_URL,
|
||||
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
|
||||
FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL,
|
||||
} from '@/utils/Url';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
import {
|
||||
fetchStudents,
|
||||
fetchStudentCompetencies,
|
||||
fetchAbsences,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { fetchStudentEvaluations, updateStudentEvaluation, deleteStudentEvaluation } from '@/app/actions/schoolAction';
|
||||
import {
|
||||
fetchStudentEvaluations,
|
||||
updateStudentEvaluation,
|
||||
deleteStudentEvaluation,
|
||||
} from '@/app/actions/schoolAction';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
@ -42,9 +55,17 @@ function calcCompetencyStats(data) {
|
||||
const total = scores.length;
|
||||
return {
|
||||
acquired: Math.round((scores.filter((s) => s === 3).length / total) * 100),
|
||||
inProgress: Math.round((scores.filter((s) => s === 2).length / total) * 100),
|
||||
notAcquired: Math.round((scores.filter((s) => s === 1).length / total) * 100),
|
||||
notEvaluated: Math.round((scores.filter((s) => s === null || s === undefined || s === 0).length / total) * 100),
|
||||
inProgress: Math.round(
|
||||
(scores.filter((s) => s === 2).length / total) * 100
|
||||
),
|
||||
notAcquired: Math.round(
|
||||
(scores.filter((s) => s === 1).length / total) * 100
|
||||
),
|
||||
notEvaluated: Math.round(
|
||||
(scores.filter((s) => s === null || s === undefined || s === 0).length /
|
||||
total) *
|
||||
100
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@ -65,10 +86,26 @@ function getPeriodColumns(frequency) {
|
||||
}
|
||||
|
||||
const COMPETENCY_COLUMNS = [
|
||||
{ key: 'acquired', label: 'Acquises', color: 'bg-emerald-100 text-emerald-700' },
|
||||
{ key: 'inProgress', label: 'En cours', color: 'bg-yellow-100 text-yellow-700' },
|
||||
{ key: 'notAcquired', label: 'Non acquises', color: 'bg-red-100 text-red-600' },
|
||||
{ key: 'notEvaluated', label: 'Non évaluées', color: 'bg-gray-100 text-gray-600' },
|
||||
{
|
||||
key: 'acquired',
|
||||
label: 'Acquises',
|
||||
color: 'bg-emerald-100 text-emerald-700',
|
||||
},
|
||||
{
|
||||
key: 'inProgress',
|
||||
label: 'En cours',
|
||||
color: 'bg-yellow-100 text-yellow-700',
|
||||
},
|
||||
{
|
||||
key: 'notAcquired',
|
||||
label: 'Non acquises',
|
||||
color: 'bg-red-100 text-red-600',
|
||||
},
|
||||
{
|
||||
key: 'notEvaluated',
|
||||
label: 'Non évaluées',
|
||||
color: 'bg-gray-100 text-gray-600',
|
||||
},
|
||||
];
|
||||
|
||||
function getCurrentPeriodValue(frequency) {
|
||||
@ -97,13 +134,13 @@ function getCurrentPeriodValue(frequency) {
|
||||
function PercentBadge({ value, loading, color }) {
|
||||
if (loading) return <span className="text-gray-300 text-xs">…</span>;
|
||||
if (value === null) return <span className="text-gray-400 text-xs">—</span>;
|
||||
const badgeColor = color || (
|
||||
value >= 75
|
||||
const badgeColor =
|
||||
color ||
|
||||
(value >= 75
|
||||
? 'bg-emerald-100 text-emerald-700'
|
||||
: value >= 50
|
||||
? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-red-100 text-red-600'
|
||||
);
|
||||
: 'bg-red-100 text-red-600');
|
||||
return (
|
||||
<span
|
||||
className={`inline-block px-2 py-0.5 rounded-full text-xs font-semibold ${badgeColor}`}
|
||||
@ -184,7 +221,9 @@ export default function Page() {
|
||||
if (data?.data) {
|
||||
data.data.forEach((d) =>
|
||||
d.categories.forEach((c) =>
|
||||
c.competences.forEach((comp) => studentScores[studentId].push(comp.score))
|
||||
c.competences.forEach((comp) =>
|
||||
studentScores[studentId].push(comp.score)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -197,10 +236,21 @@ export default function Page() {
|
||||
} else {
|
||||
const total = scores.length;
|
||||
map[studentId] = {
|
||||
acquired: Math.round((scores.filter((s) => s === 3).length / total) * 100),
|
||||
inProgress: Math.round((scores.filter((s) => s === 2).length / total) * 100),
|
||||
notAcquired: Math.round((scores.filter((s) => s === 1).length / total) * 100),
|
||||
notEvaluated: Math.round((scores.filter((s) => s === null || s === undefined || s === 0).length / total) * 100),
|
||||
acquired: Math.round(
|
||||
(scores.filter((s) => s === 3).length / total) * 100
|
||||
),
|
||||
inProgress: Math.round(
|
||||
(scores.filter((s) => s === 2).length / total) * 100
|
||||
),
|
||||
notAcquired: Math.round(
|
||||
(scores.filter((s) => s === 1).length / total) * 100
|
||||
),
|
||||
notEvaluated: Math.round(
|
||||
(scores.filter((s) => s === null || s === undefined || s === 0)
|
||||
.length /
|
||||
total) *
|
||||
100
|
||||
),
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -263,15 +313,31 @@ export default function Page() {
|
||||
|
||||
const handleSaveEval = async (evalItem) => {
|
||||
try {
|
||||
await updateStudentEvaluation(evalItem.id, {
|
||||
score: editAbsent ? null : (editScore === '' ? null : parseFloat(editScore)),
|
||||
is_absent: editAbsent,
|
||||
}, csrfToken);
|
||||
await updateStudentEvaluation(
|
||||
evalItem.id,
|
||||
{
|
||||
score: editAbsent
|
||||
? null
|
||||
: editScore === ''
|
||||
? null
|
||||
: parseFloat(editScore),
|
||||
is_absent: editAbsent,
|
||||
},
|
||||
csrfToken
|
||||
);
|
||||
// Update local state
|
||||
setStudentEvaluations((prev) =>
|
||||
prev.map((e) =>
|
||||
e.id === evalItem.id
|
||||
? { ...e, score: editAbsent ? null : (editScore === '' ? null : parseFloat(editScore)), is_absent: editAbsent }
|
||||
? {
|
||||
...e,
|
||||
score: editAbsent
|
||||
? null
|
||||
: editScore === ''
|
||||
? null
|
||||
: parseFloat(editScore),
|
||||
is_absent: editAbsent,
|
||||
}
|
||||
: e
|
||||
)
|
||||
);
|
||||
@ -318,7 +384,10 @@ export default function Page() {
|
||||
{ name: 'Élève', transform: () => null },
|
||||
{ name: 'Niveau', transform: () => null },
|
||||
{ name: 'Classe', transform: () => null },
|
||||
...COMPETENCY_COLUMNS.map(({ label }) => ({ name: label, transform: () => null })),
|
||||
...COMPETENCY_COLUMNS.map(({ label }) => ({
|
||||
name: label,
|
||||
transform: () => null,
|
||||
})),
|
||||
{ name: 'Absences', transform: () => null },
|
||||
{ name: 'Actions', transform: () => null },
|
||||
];
|
||||
@ -331,13 +400,13 @@ export default function Page() {
|
||||
<div className="flex justify-center items-center">
|
||||
{student.photo ? (
|
||||
<a
|
||||
href={`${BASE_URL}${student.photo}`}
|
||||
href={getSecureFileUrl(student.photo)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<img
|
||||
src={`${BASE_URL}${student.photo}`}
|
||||
src={getSecureFileUrl(student.photo)}
|
||||
alt={`${student.first_name} ${student.last_name}`}
|
||||
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer rounded-full"
|
||||
/>
|
||||
@ -345,7 +414,8 @@ export default function Page() {
|
||||
) : (
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full">
|
||||
<span className="text-gray-500 text-sm font-semibold">
|
||||
{student.first_name?.[0]}{student.last_name?.[0]}
|
||||
{student.first_name?.[0]}
|
||||
{student.last_name?.[0]}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@ -364,7 +434,9 @@ export default function Page() {
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`${FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL}?schoolClassId=${student.associated_class_id}`);
|
||||
router.push(
|
||||
`${FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL}?schoolClassId=${student.associated_class_id}`
|
||||
);
|
||||
}}
|
||||
className="text-emerald-700 hover:underline font-medium"
|
||||
>
|
||||
@ -385,7 +457,10 @@ export default function Page() {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); router.push(`/admin/grades/${student.id}`); }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/admin/grades/${student.id}`);
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 transition whitespace-nowrap"
|
||||
title="Voir la fiche"
|
||||
>
|
||||
@ -469,10 +544,12 @@ export default function Page() {
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b bg-gray-50">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
Notes de {gradesModalStudent.first_name} {gradesModalStudent.last_name}
|
||||
Notes de {gradesModalStudent.first_name}{' '}
|
||||
{gradesModalStudent.last_name}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
{gradesModalStudent.associated_class_name || 'Classe non assignée'}
|
||||
{gradesModalStudent.associated_class_name ||
|
||||
'Classe non assignée'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@ -497,25 +574,38 @@ export default function Page() {
|
||||
<div className="space-y-6">
|
||||
{/* Résumé des moyennes */}
|
||||
{(() => {
|
||||
const subjectAverages = Object.entries(groupedBySubject).map(([subject, { color, evaluations }]) => {
|
||||
const scores = evaluations
|
||||
.filter(e => e.score !== null && e.score !== undefined && !e.is_absent)
|
||||
.map(e => parseFloat(e.score))
|
||||
.filter(s => !isNaN(s));
|
||||
const avg = scores.length
|
||||
? scores.reduce((sum, s) => sum + s, 0) / scores.length
|
||||
: null;
|
||||
return { subject, color, avg };
|
||||
}).filter(s => s.avg !== null && !isNaN(s.avg));
|
||||
|
||||
const subjectAverages = Object.entries(groupedBySubject)
|
||||
.map(([subject, { color, evaluations }]) => {
|
||||
const scores = evaluations
|
||||
.filter(
|
||||
(e) =>
|
||||
e.score !== null &&
|
||||
e.score !== undefined &&
|
||||
!e.is_absent
|
||||
)
|
||||
.map((e) => parseFloat(e.score))
|
||||
.filter((s) => !isNaN(s));
|
||||
const avg = scores.length
|
||||
? scores.reduce((sum, s) => sum + s, 0) /
|
||||
scores.length
|
||||
: null;
|
||||
return { subject, color, avg };
|
||||
})
|
||||
.filter((s) => s.avg !== null && !isNaN(s.avg));
|
||||
|
||||
const overallAvg = subjectAverages.length
|
||||
? (subjectAverages.reduce((sum, s) => sum + s.avg, 0) / subjectAverages.length).toFixed(1)
|
||||
? (
|
||||
subjectAverages.reduce((sum, s) => sum + s.avg, 0) /
|
||||
subjectAverages.length
|
||||
).toFixed(1)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-r from-emerald-50 to-blue-50 rounded-lg p-4 border border-emerald-100">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-gray-600">Résumé</span>
|
||||
<span className="text-sm font-medium text-gray-600">
|
||||
Résumé
|
||||
</span>
|
||||
{overallAvg !== null && (
|
||||
<span className="text-lg font-bold text-emerald-700">
|
||||
Moyenne générale : {overallAvg}/20
|
||||
@ -532,7 +622,9 @@ export default function Page() {
|
||||
className="w-2.5 h-2.5 rounded-full"
|
||||
style={{ backgroundColor: color }}
|
||||
></span>
|
||||
<span className="text-sm text-gray-700">{subject}</span>
|
||||
<span className="text-sm text-gray-700">
|
||||
{subject}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-gray-800">
|
||||
{avg.toFixed(1)}
|
||||
</span>
|
||||
@ -543,134 +635,175 @@ export default function Page() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{Object.entries(groupedBySubject).map(([subject, { color, evaluations }]) => {
|
||||
const scores = evaluations
|
||||
.filter(e => e.score !== null && e.score !== undefined && !e.is_absent)
|
||||
.map(e => parseFloat(e.score))
|
||||
.filter(s => !isNaN(s));
|
||||
const avg = scores.length
|
||||
? (scores.reduce((sum, s) => sum + s, 0) / scores.length).toFixed(1)
|
||||
: null;
|
||||
return (
|
||||
<div key={subject} className="border rounded-lg overflow-hidden">
|
||||
{Object.entries(groupedBySubject).map(
|
||||
([subject, { color, evaluations }]) => {
|
||||
const scores = evaluations
|
||||
.filter(
|
||||
(e) =>
|
||||
e.score !== null &&
|
||||
e.score !== undefined &&
|
||||
!e.is_absent
|
||||
)
|
||||
.map((e) => parseFloat(e.score))
|
||||
.filter((s) => !isNaN(s));
|
||||
const avg = scores.length
|
||||
? (
|
||||
scores.reduce((sum, s) => sum + s, 0) /
|
||||
scores.length
|
||||
).toFixed(1)
|
||||
: null;
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-3"
|
||||
style={{ backgroundColor: `${color}20` }}
|
||||
key={subject}
|
||||
className="border rounded-lg overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: color }}
|
||||
></span>
|
||||
<span className="font-semibold text-gray-800">{subject}</span>
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-3"
|
||||
style={{ backgroundColor: `${color}20` }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: color }}
|
||||
></span>
|
||||
<span className="font-semibold text-gray-800">
|
||||
{subject}
|
||||
</span>
|
||||
</div>
|
||||
{avg !== null && (
|
||||
<span className="text-sm font-bold text-gray-700">
|
||||
Moyenne : {avg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{avg !== null && (
|
||||
<span className="text-sm font-bold text-gray-700">
|
||||
Moyenne : {avg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2 font-medium text-gray-600">Évaluation</th>
|
||||
<th className="text-left px-4 py-2 font-medium text-gray-600">Période</th>
|
||||
<th className="text-right px-4 py-2 font-medium text-gray-600">Note</th>
|
||||
<th className="text-center px-4 py-2 font-medium text-gray-600 w-24">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{evaluations.map((evalItem) => {
|
||||
const isEditing = editingEvalId === evalItem.id;
|
||||
return (
|
||||
<tr key={evalItem.id} className="border-t hover:bg-gray-50">
|
||||
<td className="px-4 py-2 text-gray-700">
|
||||
{evalItem.evaluation_name || 'Évaluation'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500">
|
||||
{evalItem.period || '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<label className="flex items-center gap-1 text-xs text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editAbsent}
|
||||
onChange={(e) => {
|
||||
setEditAbsent(e.target.checked);
|
||||
if (e.target.checked) setEditScore('');
|
||||
}}
|
||||
/>
|
||||
Abs
|
||||
</label>
|
||||
{!editAbsent && (
|
||||
<input
|
||||
type="number"
|
||||
value={editScore}
|
||||
onChange={(e) => setEditScore(e.target.value)}
|
||||
min="0"
|
||||
max={evalItem.max_score || 20}
|
||||
step="0.5"
|
||||
className="w-16 text-center px-1 py-0.5 border rounded text-sm"
|
||||
/>
|
||||
)}
|
||||
<span className="text-gray-500">/{evalItem.max_score || 20}</span>
|
||||
</div>
|
||||
) : evalItem.is_absent ? (
|
||||
<span className="text-orange-500 font-medium">Absent</span>
|
||||
) : evalItem.score !== null ? (
|
||||
<span className="font-semibold text-gray-800">
|
||||
{evalItem.score}/{evalItem.max_score || 20}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() => handleSaveEval(evalItem)}
|
||||
className="p-1 text-emerald-600 hover:bg-emerald-50 rounded"
|
||||
title="Enregistrer"
|
||||
>
|
||||
<Save size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEditingEval}
|
||||
className="p-1 text-gray-500 hover:bg-gray-100 rounded"
|
||||
title="Annuler"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() => startEditingEval(evalItem)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
||||
title="Modifier"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteEval(evalItem)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2 font-medium text-gray-600">
|
||||
Évaluation
|
||||
</th>
|
||||
<th className="text-left px-4 py-2 font-medium text-gray-600">
|
||||
Période
|
||||
</th>
|
||||
<th className="text-right px-4 py-2 font-medium text-gray-600">
|
||||
Note
|
||||
</th>
|
||||
<th className="text-center px-4 py-2 font-medium text-gray-600 w-24">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
);})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody>
|
||||
{evaluations.map((evalItem) => {
|
||||
const isEditing = editingEvalId === evalItem.id;
|
||||
return (
|
||||
<tr
|
||||
key={evalItem.id}
|
||||
className="border-t hover:bg-gray-50"
|
||||
>
|
||||
<td className="px-4 py-2 text-gray-700">
|
||||
{evalItem.evaluation_name || 'Évaluation'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500">
|
||||
{evalItem.period || '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<label className="flex items-center gap-1 text-xs text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editAbsent}
|
||||
onChange={(e) => {
|
||||
setEditAbsent(e.target.checked);
|
||||
if (e.target.checked)
|
||||
setEditScore('');
|
||||
}}
|
||||
/>
|
||||
Abs
|
||||
</label>
|
||||
{!editAbsent && (
|
||||
<input
|
||||
type="number"
|
||||
value={editScore}
|
||||
onChange={(e) =>
|
||||
setEditScore(e.target.value)
|
||||
}
|
||||
min="0"
|
||||
max={evalItem.max_score || 20}
|
||||
step="0.5"
|
||||
className="w-16 text-center px-1 py-0.5 border rounded text-sm"
|
||||
/>
|
||||
)}
|
||||
<span className="text-gray-500">
|
||||
/{evalItem.max_score || 20}
|
||||
</span>
|
||||
</div>
|
||||
) : evalItem.is_absent ? (
|
||||
<span className="text-orange-500 font-medium">
|
||||
Absent
|
||||
</span>
|
||||
) : evalItem.score !== null ? (
|
||||
<span className="font-semibold text-gray-800">
|
||||
{evalItem.score}/
|
||||
{evalItem.max_score || 20}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() =>
|
||||
handleSaveEval(evalItem)
|
||||
}
|
||||
className="p-1 text-emerald-600 hover:bg-emerald-50 rounded"
|
||||
title="Enregistrer"
|
||||
>
|
||||
<Save size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEditingEval}
|
||||
className="p-1 text-gray-500 hover:bg-gray-100 rounded"
|
||||
title="Annuler"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() =>
|
||||
startEditingEval(evalItem)
|
||||
}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
||||
title="Modifier"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleDeleteEval(evalItem)
|
||||
}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user