mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-04 04:01:27 +00:00
feat(frontend): refonte mobile planning et ameliorations suivi pedagogique [#NEWTS-4]
Fonction PWA et ajout du responsive design Planning mobile : - Nouvelle vue DayView avec bandeau semaine scrollable, date picker natif et navigation integree - ScheduleNavigation converti en drawer overlay sur mobile, sidebar fixe sur desktop - Suppression double barre navigation mobile, controles deplaces dans DayView - Date picker natif via label+input sur mobile Suivi pedagogique : - Refactorisation page grades avec composant Table partage - Colonnes stats par periode, absences, actions (Fiche + Evaluer) - Lien cliquable sur la classe vers SchoolClassManagement feat(backend): ajout associated_class_id dans StudentByRFCreationSerializer [#NEWTS-4] UI global : - Remplacement fleches texte par icones Lucide ChevronDown/ChevronRight - Pagination conditionnelle sur tous les tableaux plats - Layout responsive mobile : cartes separees fond transparent - Table.js : pagination optionnelle, wrapper md uniquement
This commit is contained in:
286
Front-End/src/app/[locale]/admin/grades/[studentId]/page.js
Normal file
286
Front-End/src/app/[locale]/admin/grades/[studentId]/page.js
Normal file
@ -0,0 +1,286 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import SelectChoice from '@/components/Form/SelectChoice';
|
||||
import Attendance from '@/components/Grades/Attendance';
|
||||
import GradesStatsCircle from '@/components/Grades/GradesStatsCircle';
|
||||
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
||||
import Button from '@/components/Form/Button';
|
||||
import logger from '@/utils/logger';
|
||||
import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL, BASE_URL } from '@/utils/Url';
|
||||
import {
|
||||
fetchStudents,
|
||||
fetchStudentCompetencies,
|
||||
fetchAbsences,
|
||||
editAbsences,
|
||||
deleteAbsences,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { Award, ArrowLeft } from 'lucide-react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
|
||||
function getPeriodString(selectedPeriod, frequency) {
|
||||
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
||||
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 '';
|
||||
}
|
||||
|
||||
export default function StudentGradesPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const studentId = Number(params.studentId);
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } =
|
||||
useEstablishment();
|
||||
const { getNiveauLabel } = useClasses();
|
||||
|
||||
const [student, setStudent] = useState(null);
|
||||
const [studentCompetencies, setStudentCompetencies] = useState(null);
|
||||
const [grades, setGrades] = useState({});
|
||||
const [selectedPeriod, setSelectedPeriod] = useState(null);
|
||||
const [allAbsences, setAllAbsences] = useState([]);
|
||||
|
||||
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 [];
|
||||
};
|
||||
|
||||
// Load student info
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
fetchStudents(selectedEstablishmentId, null, 5)
|
||||
.then((students) => {
|
||||
const found = students.find((s) => s.id === studentId);
|
||||
setStudent(found || null);
|
||||
})
|
||||
.catch((error) => logger.error('Error fetching students:', error));
|
||||
}
|
||||
}, [selectedEstablishmentId, studentId]);
|
||||
|
||||
// Auto-select current period
|
||||
useEffect(() => {
|
||||
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);
|
||||
}, [selectedEstablishmentEvaluationFrequency]);
|
||||
|
||||
// Load competencies
|
||||
useEffect(() => {
|
||||
if (studentId && selectedPeriod) {
|
||||
const periodString = getPeriodString(
|
||||
selectedPeriod,
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
fetchStudentCompetencies(studentId, periodString)
|
||||
.then((data) => {
|
||||
setStudentCompetencies(data);
|
||||
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);
|
||||
}
|
||||
}, [studentId, selectedPeriod]);
|
||||
|
||||
// Load absences
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
fetchAbsences(selectedEstablishmentId)
|
||||
.then((data) => setAllAbsences(data))
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors du fetch des absences:', error)
|
||||
);
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
const absences = React.useMemo(() => {
|
||||
return allAbsences
|
||||
.filter((a) => a.student === studentId)
|
||||
.map((a) => ({
|
||||
id: a.id,
|
||||
date: a.day,
|
||||
type: [2, 1].includes(a.reason) ? 'Absence' : 'Retard',
|
||||
reason: a.reason,
|
||||
justified: [1, 3].includes(a.reason),
|
||||
moment: a.moment,
|
||||
commentaire: a.commentaire,
|
||||
}));
|
||||
}, [allAbsences, studentId]);
|
||||
|
||||
const handleToggleJustify = (absence) => {
|
||||
const newReason =
|
||||
absence.type === 'Absence'
|
||||
? absence.justified ? 2 : 1
|
||||
: absence.justified ? 4 : 3;
|
||||
|
||||
editAbsences(absence.id, { ...absence, reason: newReason }, csrfToken)
|
||||
.then(() => {
|
||||
setAllAbsences((prev) =>
|
||||
prev.map((a) =>
|
||||
a.id === absence.id ? { ...a, reason: newReason } : a
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch((e) => logger.error('Erreur lors du changement de justification', e));
|
||||
};
|
||||
|
||||
const handleDeleteAbsence = (absence) => {
|
||||
return deleteAbsences(absence.id, csrfToken)
|
||||
.then(() => {
|
||||
setAllAbsences((prev) => prev.filter((a) => a.id !== absence.id));
|
||||
})
|
||||
.catch((e) =>
|
||||
logger.error("Erreur lors de la suppression de l'absence", e)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => router.push('/admin/grades')}
|
||||
className="p-2 rounded-md hover:bg-gray-100 border border-gray-200"
|
||||
aria-label="Retour à la liste"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="text-xl font-bold text-gray-800">Suivi pédagogique</h1>
|
||||
</div>
|
||||
|
||||
{/* Student profile */}
|
||||
{student && (
|
||||
<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}`}
|
||||
alt={`${student.first_name} ${student.last_name}`}
|
||||
className="w-24 h-24 object-cover rounded-full border-4 border-emerald-200 shadow"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-24 h-24 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl border-4 border-emerald-100">
|
||||
{student.first_name?.[0]}
|
||||
{student.last_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 text-center sm:text-left">
|
||||
<div className="text-xl font-bold text-emerald-800">
|
||||
{student.last_name} {student.first_name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-1">
|
||||
Niveau :{' '}
|
||||
<span className="font-medium">
|
||||
{getNiveauLabel(student.level)}
|
||||
</span>
|
||||
{' | '}
|
||||
Classe :{' '}
|
||||
<span className="font-medium">
|
||||
{student.associated_class_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Period selector + Evaluate button */}
|
||||
<div className="flex flex-col sm:flex-row items-end gap-3 w-full sm:w-auto">
|
||||
<div className="w-full sm:w-44">
|
||||
<SelectChoice
|
||||
name="period"
|
||||
label="Période"
|
||||
placeHolder="Choisir la période"
|
||||
choices={getPeriods().map((period) => {
|
||||
const today = dayjs();
|
||||
const end = dayjs(`${today.year()}-${period.end}`);
|
||||
return {
|
||||
value: period.value,
|
||||
label: period.label,
|
||||
disabled: today.isAfter(end),
|
||||
};
|
||||
})}
|
||||
selected={selectedPeriod || ''}
|
||||
callback={(e) => setSelectedPeriod(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
primary
|
||||
onClick={() => {
|
||||
const periodString = getPeriodString(
|
||||
selectedPeriod,
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
router.push(
|
||||
`${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${studentId}&period=${periodString}`
|
||||
);
|
||||
}}
|
||||
className="px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full sm:w-auto"
|
||||
icon={<Award className="w-5 h-5" />}
|
||||
text="Évaluer"
|
||||
title="Évaluer l'élève"
|
||||
disabled={!selectedPeriod}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats + Absences */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Attendance
|
||||
absences={absences}
|
||||
onToggleJustify={handleToggleJustify}
|
||||
onDelete={handleDeleteAbsence}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<GradesStatsCircle grades={grades} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<GradesDomainBarChart studentCompetencies={studentCompetencies} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user