mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
412 lines
15 KiB
JavaScript
412 lines
15 KiB
JavaScript
'use client';
|
|
import React, { useState, useEffect } from 'react';
|
|
import SelectChoice from '@/components/SelectChoice';
|
|
import AcademicResults from '@/components/Grades/AcademicResults';
|
|
import Attendance from '@/components/Grades/Attendance';
|
|
import Remarks from '@/components/Grades/Remarks';
|
|
import WorkPlan from '@/components/Grades/WorkPlan';
|
|
import Homeworks from '@/components/Grades/Homeworks';
|
|
import SpecificEvaluations from '@/components/Grades/SpecificEvaluations';
|
|
import Orientation from '@/components/Grades/Orientation';
|
|
import GradesStatsCircle from '@/components/Grades/GradesStatsCircle';
|
|
import Button from '@/components/Button';
|
|
import logger from '@/utils/logger';
|
|
import {
|
|
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
|
|
BASE_URL,
|
|
} from '@/utils/Url';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
fetchStudents,
|
|
fetchStudentCompetencies,
|
|
searchStudents,
|
|
} from '@/app/actions/subscriptionAction';
|
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
|
import { useClasses } from '@/context/ClassesContext';
|
|
import { Award, BookOpen, FileText } from 'lucide-react';
|
|
import SectionHeader from '@/components/SectionHeader';
|
|
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
|
import InputText from '@/components/InputText';
|
|
import dayjs from 'dayjs';
|
|
|
|
export default function Page() {
|
|
const router = useRouter();
|
|
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } =
|
|
useEstablishment();
|
|
const { getNiveauLabel } = useClasses();
|
|
const [formData, setFormData] = useState({
|
|
selectedStudent: null,
|
|
});
|
|
|
|
const [students, setStudents] = useState([]);
|
|
const [studentCompetencies, setStudentCompetencies] = useState(null);
|
|
const [grades, setGrades] = useState({});
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedPeriod, setSelectedPeriod] = useState(null);
|
|
|
|
// Définir les périodes selon la fréquence
|
|
const getPeriods = () => {
|
|
if (selectedEstablishmentEvaluationFrequency === 1) {
|
|
return [
|
|
{ label: 'Trimestre 1', value: 1, start: '09-01', end: '12-31' },
|
|
{ label: 'Trimestre 2', value: 2, start: '01-01', end: '03-31' },
|
|
{ label: 'Trimestre 3', value: 3, start: '04-01', end: '07-15' },
|
|
];
|
|
}
|
|
if (selectedEstablishmentEvaluationFrequency === 2) {
|
|
return [
|
|
{ label: 'Semestre 1', value: 1, start: '09-01', end: '01-31' },
|
|
{ label: 'Semestre 2', value: 2, start: '02-01', end: '07-15' },
|
|
];
|
|
}
|
|
if (selectedEstablishmentEvaluationFrequency === 3) {
|
|
return [{ label: 'Année', value: 1, start: '09-01', end: '07-15' }];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
// Sélection automatique de la période courante
|
|
useEffect(() => {
|
|
if (!formData.selectedStudent) {
|
|
setSelectedPeriod(null);
|
|
return;
|
|
}
|
|
const periods = getPeriods();
|
|
const today = dayjs();
|
|
const current = periods.find((p) => {
|
|
const start = dayjs(`${today.year()}-${p.start}`);
|
|
const end = dayjs(`${today.year()}-${p.end}`);
|
|
return (
|
|
today.isAfter(start.subtract(1, 'day')) &&
|
|
today.isBefore(end.add(1, 'day'))
|
|
);
|
|
});
|
|
setSelectedPeriod(current ? current.value : null);
|
|
}, [formData.selectedStudent, selectedEstablishmentEvaluationFrequency]);
|
|
|
|
const academicResults = [
|
|
{
|
|
subject: 'Mathématiques',
|
|
grade: 16,
|
|
average: 14,
|
|
appreciation: 'Très bon travail',
|
|
},
|
|
{
|
|
subject: 'Français',
|
|
grade: 15,
|
|
average: 13,
|
|
appreciation: 'Bonne participation',
|
|
},
|
|
];
|
|
|
|
const absences = [
|
|
{ date: '2023-09-01', type: 'Absence', reason: 'Maladie', justified: true },
|
|
{ date: '2023-09-15', type: 'Retard', reason: 'Trafic', justified: false },
|
|
];
|
|
|
|
const remarks = [
|
|
{
|
|
date: '2023-09-10',
|
|
teacher: 'Mme Dupont',
|
|
comment: 'Participation active en classe.',
|
|
},
|
|
{
|
|
date: '2023-09-20',
|
|
teacher: 'M. Martin',
|
|
comment: 'Doit améliorer la concentration.',
|
|
},
|
|
];
|
|
|
|
const workPlan = [
|
|
{
|
|
objective: 'Renforcer la lecture',
|
|
support: 'Exercices hebdomadaires',
|
|
followUp: 'En cours',
|
|
},
|
|
{
|
|
objective: 'Maîtriser les tables de multiplication',
|
|
support: 'Jeux éducatifs',
|
|
followUp: 'À démarrer',
|
|
},
|
|
];
|
|
|
|
const homeworks = [
|
|
{ title: 'Rédaction', dueDate: '2023-10-10', status: 'Rendu' },
|
|
{ title: 'Exercices de maths', dueDate: '2023-10-12', status: 'À faire' },
|
|
];
|
|
|
|
const specificEvaluations = [
|
|
{
|
|
test: 'Bilan de compétences',
|
|
date: '2023-09-25',
|
|
result: 'Bon niveau général',
|
|
},
|
|
];
|
|
|
|
const orientation = [
|
|
{
|
|
date: '2023-10-01',
|
|
counselor: 'Mme Leroy',
|
|
advice: 'Poursuivre en filière générale',
|
|
},
|
|
];
|
|
|
|
const handleChange = (field, value) =>
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
|
|
useEffect(() => {
|
|
if (selectedEstablishmentId) {
|
|
fetchStudents(selectedEstablishmentId, null, 5)
|
|
.then((studentsData) => {
|
|
setStudents(studentsData);
|
|
})
|
|
.catch((error) => logger.error('Error fetching students:', error));
|
|
}
|
|
}, [selectedEstablishmentId]);
|
|
|
|
// Charger les compétences et générer les grades à chaque changement d'élève sélectionné
|
|
useEffect(() => {
|
|
if (formData.selectedStudent && selectedPeriod) {
|
|
const periodString = getPeriodString(
|
|
selectedPeriod,
|
|
selectedEstablishmentEvaluationFrequency
|
|
);
|
|
fetchStudentCompetencies(formData.selectedStudent, periodString)
|
|
.then((data) => {
|
|
setStudentCompetencies(data);
|
|
// Générer les grades à partir du retour API
|
|
if (data && data.data) {
|
|
const initialGrades = {};
|
|
data.data.forEach((domaine) => {
|
|
domaine.categories.forEach((cat) => {
|
|
cat.competences.forEach((comp) => {
|
|
initialGrades[comp.competence_id] = comp.score ?? 0;
|
|
});
|
|
});
|
|
});
|
|
setGrades(initialGrades);
|
|
}
|
|
})
|
|
.catch((error) =>
|
|
logger.error('Error fetching studentCompetencies:', error)
|
|
);
|
|
} else {
|
|
setGrades({});
|
|
setStudentCompetencies(null);
|
|
}
|
|
}, [formData.selectedStudent, selectedPeriod]);
|
|
|
|
// Fonction utilitaire pour convertir la période sélectionnée en string backend
|
|
function getPeriodString(selectedPeriod, frequency) {
|
|
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; // année scolaire commence en septembre
|
|
const nextYear = (year + 1).toString();
|
|
const schoolYear = `${year}-${nextYear}`;
|
|
if (frequency === 1) return `T${selectedPeriod}_${schoolYear}`;
|
|
if (frequency === 2) return `S${selectedPeriod}_${schoolYear}`;
|
|
if (frequency === 3) return `A_${schoolYear}`;
|
|
return '';
|
|
}
|
|
|
|
return (
|
|
<div className="p-8 space-y-8">
|
|
<SectionHeader
|
|
icon={BookOpen}
|
|
title="Suivi pédagogique"
|
|
description="Suivez le parcours d'un élève"
|
|
/>
|
|
|
|
{/* Section haute : filtre + bouton + photo élève */}
|
|
<div className="flex flex-row gap-8 items-start">
|
|
{/* Colonne gauche : InputText + bouton */}
|
|
<div className="w-4/5 flex items-end gap-4">
|
|
<div className="flex-1">
|
|
<InputText
|
|
name="studentSearch"
|
|
type="text"
|
|
label="Recherche élève"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
placeholder="Rechercher un élève"
|
|
required={false}
|
|
enable={true}
|
|
/>
|
|
</div>
|
|
{/* Sélecteur de période */}
|
|
{formData.selectedStudent && (
|
|
<SelectChoice
|
|
name="period"
|
|
label="Période"
|
|
placeHolder="Choisir la période"
|
|
choices={getPeriods().map((period) => {
|
|
const today = dayjs();
|
|
const start = dayjs(`${today.year()}-${period.start}`);
|
|
const end = dayjs(`${today.year()}-${period.end}`);
|
|
const isPast = today.isAfter(end);
|
|
return {
|
|
value: period.value,
|
|
label: period.label,
|
|
disabled: isPast,
|
|
};
|
|
})}
|
|
selected={selectedPeriod || ''}
|
|
callback={(e) => setSelectedPeriod(Number(e.target.value))}
|
|
disabled={false}
|
|
/>
|
|
)}
|
|
<Button
|
|
primary
|
|
onClick={() => {
|
|
const periodString = getPeriodString(
|
|
selectedPeriod,
|
|
selectedEstablishmentEvaluationFrequency
|
|
);
|
|
const url = `${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${formData.selectedStudent}&period=${periodString}`;
|
|
router.push(url);
|
|
}}
|
|
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600"
|
|
icon={<Award className="w-6 h-6" />}
|
|
text="Evaluer"
|
|
title="Evaluez l'élève"
|
|
disabled={!formData.selectedStudent || !selectedPeriod}
|
|
/>
|
|
</div>
|
|
{/* Colonne droite : Photo élève */}
|
|
<div className="w-2/5 flex flex-col items-center justify-center">
|
|
{formData.selectedStudent &&
|
|
(() => {
|
|
const student = students.find(
|
|
(s) => s.id === formData.selectedStudent
|
|
);
|
|
if (!student) return null;
|
|
return (
|
|
<>
|
|
{student.photo ? (
|
|
<img
|
|
src={`${BASE_URL}${student.photo}`}
|
|
alt={`${student.first_name} ${student.last_name}`}
|
|
className="w-32 h-32 object-cover rounded-full border-4 border-emerald-200 mb-4 shadow"
|
|
/>
|
|
) : (
|
|
<div className="w-24 h-24 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl mb-4 border-4 border-emerald-100">
|
|
{student.first_name?.[0]}
|
|
{student.last_name?.[0]}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section basse : liste élèves + infos */}
|
|
<div className="flex flex-row gap-8 items-start mt-8">
|
|
{/* Colonne 1 : Liste des élèves */}
|
|
<div className="w-full max-w-xs">
|
|
<h3 className="text-lg font-semibold text-emerald-700 mb-4">
|
|
Liste des élèves
|
|
</h3>
|
|
<ul className="rounded-lg bg-stone-50 shadow border border-gray-100">
|
|
{students
|
|
.filter(
|
|
(student) =>
|
|
!searchTerm ||
|
|
`${student.last_name} ${student.first_name}`
|
|
.toLowerCase()
|
|
.includes(searchTerm.toLowerCase())
|
|
)
|
|
.map((student) => (
|
|
<li
|
|
key={student.id}
|
|
className={`flex items-center gap-4 px-4 py-3 hover:bg-emerald-100 cursor-pointer transition ${
|
|
formData.selectedStudent === student.id
|
|
? 'bg-emerald-100 border-l-4 border-emerald-400'
|
|
: 'border-l-2 border-gray-200'
|
|
}`}
|
|
onClick={() => handleChange('selectedStudent', student.id)}
|
|
>
|
|
{student.photo ? (
|
|
<img
|
|
src={`${BASE_URL}${student.photo}`}
|
|
alt={`${student.first_name} ${student.last_name}`}
|
|
className="w-10 h-10 object-cover rounded-full border-2 border-emerald-200"
|
|
/>
|
|
) : (
|
|
<div className="w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-lg border-2 border-emerald-100">
|
|
{student.first_name?.[0]}
|
|
{student.last_name?.[0]}
|
|
</div>
|
|
)}
|
|
<div className="flex-1">
|
|
<div className="font-semibold text-emerald-800">
|
|
{student.last_name} {student.first_name}
|
|
</div>
|
|
<div className="text-xs text-gray-600">
|
|
Niveau :{' '}
|
|
<span className="font-medium">
|
|
{getNiveauLabel(student.level)}
|
|
</span>
|
|
{' | '}
|
|
Classe :{' '}
|
|
<span className="font-medium">
|
|
{student.associated_class_name}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{/* Icône PDF si bilan dispo pour la période sélectionnée */}
|
|
{selectedPeriod &&
|
|
student.bilans &&
|
|
Array.isArray(student.bilans) &&
|
|
(() => {
|
|
// Génère la string de période attendue
|
|
const periodString = getPeriodString(
|
|
selectedPeriod,
|
|
selectedEstablishmentEvaluationFrequency
|
|
);
|
|
const bilan = student.bilans.find(
|
|
(b) => b.period === periodString && b.file
|
|
);
|
|
if (bilan) {
|
|
return (
|
|
<a
|
|
href={`${BASE_URL}${bilan.file}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="ml-2 text-emerald-600 hover:text-emerald-800"
|
|
title="Télécharger le bilan de compétences"
|
|
onClick={(e) => e.stopPropagation()} // Pour ne pas sélectionner à nouveau l'élève
|
|
>
|
|
<FileText className="w-5 h-5" />
|
|
</a>
|
|
);
|
|
}
|
|
return null;
|
|
})()}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
{/* Colonne 2 : Reste des infos */}
|
|
<div className="flex-1">
|
|
{formData.selectedStudent && (
|
|
<div className="flex flex-col gap-8 w-full justify-center items-stretch">
|
|
<div className="w-full flex flex-row items-stretch gap-4">
|
|
<div className="flex-1 flex items-stretch justify-center h-full">
|
|
<Attendance absences={absences} />
|
|
</div>
|
|
<div className="flex-1 flex items-stretch justify-center h-full">
|
|
<GradesStatsCircle grades={grades} />
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-center">
|
|
<GradesDomainBarChart
|
|
studentCompetencies={studentCompetencies}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|