mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 20:51:26 +00:00
Merge remote-tracking branch 'origin/develop' into N3WTS-5-Historique-ACA
This commit is contained in:
@ -8,7 +8,8 @@ import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
||||
import { EvaluationStudentView } from '@/components/Evaluation';
|
||||
import Button from '@/components/Form/Button';
|
||||
import logger from '@/utils/logger';
|
||||
import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL, BASE_URL } from '@/utils/Url';
|
||||
import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL } from '@/utils/Url';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
import {
|
||||
fetchStudents,
|
||||
fetchStudentCompetencies,
|
||||
@ -147,21 +148,33 @@ export default function StudentGradesPage() {
|
||||
|
||||
// Load evaluations for the student
|
||||
useEffect(() => {
|
||||
if (student?.associated_class_id && selectedPeriod && selectedEstablishmentId) {
|
||||
if (
|
||||
student?.associated_class_id &&
|
||||
selectedPeriod &&
|
||||
selectedEstablishmentId
|
||||
) {
|
||||
const periodString = getPeriodString(
|
||||
selectedPeriod,
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
|
||||
|
||||
// Load evaluations for the class
|
||||
fetchEvaluations(selectedEstablishmentId, student.associated_class_id, periodString)
|
||||
fetchEvaluations(
|
||||
selectedEstablishmentId,
|
||||
student.associated_class_id,
|
||||
periodString
|
||||
)
|
||||
.then((data) => setEvaluations(data))
|
||||
.catch((error) => logger.error('Erreur lors du fetch des évaluations:', error));
|
||||
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors du fetch des évaluations:', error)
|
||||
);
|
||||
|
||||
// Load student's evaluation scores
|
||||
fetchStudentEvaluations(studentId, null, periodString, null)
|
||||
.then((data) => setStudentEvaluationsData(data))
|
||||
.catch((error) => logger.error('Erreur lors du fetch des notes:', error));
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors du fetch des notes:', error)
|
||||
);
|
||||
}
|
||||
}, [student, selectedPeriod, selectedEstablishmentId]);
|
||||
|
||||
@ -182,8 +195,12 @@ export default function StudentGradesPage() {
|
||||
const handleToggleJustify = (absence) => {
|
||||
const newReason =
|
||||
absence.type === 'Absence'
|
||||
? absence.justified ? 2 : 1
|
||||
: absence.justified ? 4 : 3;
|
||||
? absence.justified
|
||||
? 2
|
||||
: 1
|
||||
: absence.justified
|
||||
? 4
|
||||
: 3;
|
||||
|
||||
editAbsences(absence.id, { ...absence, reason: newReason }, csrfToken)
|
||||
.then(() => {
|
||||
@ -193,7 +210,9 @@ export default function StudentGradesPage() {
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch((e) => logger.error('Erreur lors du changement de justification', e));
|
||||
.catch((e) =>
|
||||
logger.error('Erreur lors du changement de justification', e)
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteAbsence = (absence) => {
|
||||
@ -210,8 +229,16 @@ export default function StudentGradesPage() {
|
||||
try {
|
||||
await updateStudentEvaluation(studentEvalId, data, csrfToken);
|
||||
// Reload student evaluations
|
||||
const periodString = getPeriodString(selectedPeriod, selectedEstablishmentEvaluationFrequency);
|
||||
const updatedData = await fetchStudentEvaluations(studentId, null, periodString, null);
|
||||
const periodString = getPeriodString(
|
||||
selectedPeriod,
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
const updatedData = await fetchStudentEvaluations(
|
||||
studentId,
|
||||
null,
|
||||
periodString,
|
||||
null
|
||||
);
|
||||
setStudentEvaluationsData(updatedData);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la modification de la note:', error);
|
||||
@ -237,7 +264,7 @@ export default function StudentGradesPage() {
|
||||
<div className="bg-stone-50 rounded-lg shadow-sm border border-gray-100 p-4 md:p-6 flex flex-col sm:flex-row items-center sm:items-start gap-4">
|
||||
{student.photo ? (
|
||||
<img
|
||||
src={`${BASE_URL}${student.photo}`}
|
||||
src={getSecureFileUrl(student.photo)}
|
||||
alt={`${student.first_name} ${student.last_name}`}
|
||||
className="w-24 h-24 object-cover rounded-full border-4 border-emerald-200 shadow"
|
||||
/>
|
||||
|
||||
@ -1,21 +1,35 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Award, Eye, Search, BarChart2, X, Pencil, Trash2, Save, Download } from 'lucide-react';
|
||||
import {
|
||||
Award,
|
||||
Eye,
|
||||
Search,
|
||||
BarChart2,
|
||||
X,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Save,
|
||||
Download
|
||||
} 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';
|
||||
@ -48,9 +62,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
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@ -71,10 +93,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) {
|
||||
@ -103,13 +141,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}`}
|
||||
@ -206,7 +244,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)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -219,10 +259,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
|
||||
),
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -317,15 +368,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
|
||||
)
|
||||
);
|
||||
@ -373,7 +440,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 },
|
||||
];
|
||||
@ -386,13 +456,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"
|
||||
/>
|
||||
@ -400,7 +470,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>
|
||||
)}
|
||||
@ -419,7 +490,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"
|
||||
>
|
||||
@ -440,7 +513,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"
|
||||
>
|
||||
@ -540,10 +616,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
|
||||
@ -568,25 +646,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
|
||||
@ -603,7 +694,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>
|
||||
@ -614,134 +707,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>
|
||||
|
||||
@ -34,12 +34,13 @@ import {
|
||||
import {
|
||||
fetchRegistrationFileGroups,
|
||||
fetchRegistrationSchoolFileMasters,
|
||||
fetchRegistrationParentFileMasters
|
||||
fetchRegistrationParentFileMasters,
|
||||
} from '@/app/actions/registerFileGroupAction';
|
||||
import { fetchProfiles } from '@/app/actions/authAction';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { FE_ADMIN_SUBSCRIPTIONS_URL, BASE_URL } from '@/utils/Url';
|
||||
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function CreateSubscriptionPage() {
|
||||
@ -181,7 +182,9 @@ export default function CreateSubscriptionPage() {
|
||||
formDataRef.current = formData;
|
||||
}, [formData]);
|
||||
|
||||
useEffect(() => { setStudentsPage(1); }, [students]);
|
||||
useEffect(() => {
|
||||
setStudentsPage(1);
|
||||
}, [students]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!formData.guardianEmail) {
|
||||
@ -530,7 +533,7 @@ export default function CreateSubscriptionPage() {
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
logger.error('Erreur lors de la mise à jour du dossier:', error);
|
||||
@ -714,7 +717,10 @@ export default function CreateSubscriptionPage() {
|
||||
};
|
||||
|
||||
const studentsTotalPages = Math.ceil(students.length / ITEMS_PER_PAGE);
|
||||
const pagedStudents = students.slice((studentsPage - 1) * ITEMS_PER_PAGE, studentsPage * ITEMS_PER_PAGE);
|
||||
const pagedStudents = students.slice(
|
||||
(studentsPage - 1) * ITEMS_PER_PAGE,
|
||||
studentsPage * ITEMS_PER_PAGE
|
||||
);
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
@ -884,12 +890,12 @@ export default function CreateSubscriptionPage() {
|
||||
<div className="flex justify-center items-center">
|
||||
{row.photo ? (
|
||||
<a
|
||||
href={`${BASE_URL}${row.photo}`} // Lien vers la photo
|
||||
href={getSecureFileUrl(row.photo)} // Lien vers la photo
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
src={`${BASE_URL}${row.photo}`}
|
||||
src={getSecureFileUrl(row.photo)}
|
||||
alt={`${row.first_name} ${row.last_name}`}
|
||||
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer rounded-full"
|
||||
/>
|
||||
|
||||
@ -37,8 +37,8 @@ import {
|
||||
FE_ADMIN_SUBSCRIPTIONS_EDIT_URL,
|
||||
FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL,
|
||||
FE_ADMIN_SUBSCRIPTIONS_CREATE_URL,
|
||||
BASE_URL,
|
||||
} from '@/utils/Url';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
@ -114,15 +114,29 @@ export default function Page({ params: { locale } }) {
|
||||
// Valide le refus
|
||||
const handleRefuse = () => {
|
||||
if (!refuseReason.trim()) {
|
||||
showNotification('Merci de préciser la raison du refus.', 'error', 'Erreur');
|
||||
showNotification(
|
||||
'Merci de préciser la raison du refus.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('data', JSON.stringify({ status: RegistrationFormStatus.STATUS_ARCHIVED, notes: refuseReason }));
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
JSON.stringify({
|
||||
status: RegistrationFormStatus.STATUS_ARCHIVED,
|
||||
notes: refuseReason,
|
||||
})
|
||||
);
|
||||
|
||||
editRegisterForm(rowToRefuse.student.id, formData, csrfToken)
|
||||
.then(() => {
|
||||
showNotification('Le dossier a été refusé et archivé.', 'success', 'Succès');
|
||||
showNotification(
|
||||
'Le dossier a été refusé et archivé.',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
setReloadFetch(true);
|
||||
setIsRefusePopupOpen(false);
|
||||
})
|
||||
@ -713,12 +727,12 @@ export default function Page({ params: { locale } }) {
|
||||
<div className="flex justify-center items-center">
|
||||
{row.student.photo ? (
|
||||
<a
|
||||
href={`${BASE_URL}${row.student.photo}`} // Lien vers la photo
|
||||
href={getSecureFileUrl(row.student.photo)} // Lien vers la photo
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
src={`${BASE_URL}${row.student.photo}`}
|
||||
src={getSecureFileUrl(row.student.photo)}
|
||||
alt={`${row.student.first_name} ${row.student.last_name}`}
|
||||
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer rounded-full"
|
||||
/>
|
||||
@ -953,7 +967,9 @@ export default function Page({ params: { locale } }) {
|
||||
isOpen={isRefusePopupOpen}
|
||||
message={
|
||||
<div>
|
||||
<div className="mb-2 font-semibold">Veuillez indiquer la raison du refus :</div>
|
||||
<div className="mb-2 font-semibold">
|
||||
Veuillez indiquer la raison du refus :
|
||||
</div>
|
||||
<Textarea
|
||||
value={refuseReason}
|
||||
onChange={(e) => setRefuseReason(e.target.value)}
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { fetchUpcomingEvents } from '@/app/actions/planningAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
@ -139,12 +139,12 @@ export default function ParentHomePage() {
|
||||
<div className="flex justify-center items-center">
|
||||
{row.student.photo ? (
|
||||
<a
|
||||
href={`${BASE_URL}${row.student.photo}`} // Lien vers la photo
|
||||
href={getSecureFileUrl(row.student.photo)} // Lien vers la photo
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
src={`${BASE_URL}${row.student.photo}`}
|
||||
src={getSecureFileUrl(row.student.photo)}
|
||||
alt={`${row.student.first_name} ${row.student.last_name}`}
|
||||
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer rounded-full"
|
||||
/>
|
||||
@ -225,7 +225,7 @@ export default function ParentHomePage() {
|
||||
<Eye className="h-5 w-5" />
|
||||
</button>
|
||||
<a
|
||||
href={`${BASE_URL}${row.sepa_file}`}
|
||||
href={getSecureFileUrl(row.sepa_file)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full text-green-500 hover:text-green-700"
|
||||
|
||||
@ -1,10 +1,23 @@
|
||||
import React from 'react';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
import { Inter, Manrope } from 'next/font/google';
|
||||
import Providers from '@/components/Providers';
|
||||
import ServiceWorkerRegister from '@/components/ServiceWorkerRegister';
|
||||
import '@/css/tailwind.css';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const manrope = Manrope({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-manrope',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
export const metadata = {
|
||||
title: 'N3WT-SCHOOL',
|
||||
description: "Gestion de l'école",
|
||||
@ -36,7 +49,7 @@ export default async function RootLayout({ children, params }) {
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body className="p-0 m-0">
|
||||
<body className={`p-0 m-0 font-body ${inter.variable} ${manrope.variable}`}>
|
||||
<Providers messages={messages} locale={locale} session={params.session}>
|
||||
{children}
|
||||
</Providers>
|
||||
|
||||
Reference in New Issue
Block a user