mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Rattachement d'un dossier de compétences à une période scolaire
(configuration dans l'établissement) [#16]
This commit is contained in:
12
Front-End/package-lock.json
generated
12
Front-End/package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"framer-motion": "^11.11.11",
|
||||
"ics": "^3.8.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
@ -1960,6 +1961,12 @@
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
@ -7897,6 +7904,11 @@
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"framer-motion": "^11.11.11",
|
||||
"ics": "^3.8.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
||||
@ -23,14 +23,16 @@ import {
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import StudentInput from '@/components/Grades/StudentInput';
|
||||
import { Award, BookOpen } from 'lucide-react';
|
||||
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 } = useEstablishment();
|
||||
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } =
|
||||
useEstablishment();
|
||||
const { getNiveauLabel } = useClasses();
|
||||
const [formData, setFormData] = useState({
|
||||
selectedStudent: null,
|
||||
@ -39,6 +41,48 @@ export default function Page() {
|
||||
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 = [
|
||||
{
|
||||
@ -122,8 +166,12 @@ export default function Page() {
|
||||
|
||||
// Charger les compétences et générer les grades à chaque changement d'élève sélectionné
|
||||
useEffect(() => {
|
||||
if (formData.selectedStudent) {
|
||||
fetchStudentCompetencies(formData.selectedStudent)
|
||||
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
|
||||
@ -146,105 +194,218 @@ export default function Page() {
|
||||
setGrades({});
|
||||
setStudentCompetencies(null);
|
||||
}
|
||||
}, [formData.selectedStudent]);
|
||||
}, [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">
|
||||
{/* Sélection de l'élève */}
|
||||
<SectionHeader
|
||||
icon={BookOpen}
|
||||
title="Suivi pédagogique"
|
||||
description="Suivez le parcours d'un élève"
|
||||
/>
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-4">
|
||||
{/* Recherche élève + bouton + fiche élève */}
|
||||
<div className="flex-1 flex flex-row gap-4 items-start">
|
||||
|
||||
{/* 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">
|
||||
<StudentInput
|
||||
<InputText
|
||||
name="studentSearch"
|
||||
type="text"
|
||||
label="Recherche élève"
|
||||
selectedStudent={
|
||||
students.find((s) => s.id === formData.selectedStudent) || null
|
||||
}
|
||||
setSelectedStudent={(student) =>
|
||||
handleChange('selectedStudent', student?.id || '')
|
||||
}
|
||||
searchStudents={searchStudents}
|
||||
establishmentId={selectedEstablishmentId}
|
||||
required
|
||||
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 && (
|
||||
<div className="ml-4 flex flex-col items-center min-w-[220px] max-w-xs p-4 rounded-lg border border-emerald-100 shadow">
|
||||
{(() => {
|
||||
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-20 h-20 object-cover rounded-full border-4 border-emerald-200 mb-2 shadow"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-20 h-20 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl mb-2 border-4 border-emerald-100">
|
||||
{student.first_name?.[0]}
|
||||
{student.last_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center">
|
||||
<div className="text-base font-semibold text-emerald-800">
|
||||
{student.last_name} {student.first_name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 mt-1">
|
||||
Niveau :{' '}
|
||||
<span className="font-medium">
|
||||
{getNiveauLabel(student.level)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">
|
||||
Classe :{' '}
|
||||
<span className="font-medium">
|
||||
{student.associated_class_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Bouton bilan de compétences en dessous */}
|
||||
<Button
|
||||
primary
|
||||
onClick={() => {
|
||||
const url = `${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${formData.selectedStudent}`;
|
||||
router.push(`${url}`);
|
||||
}}
|
||||
className="mt-4 px-6 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
icon={<Award className="w-6 h-6" />}
|
||||
title="Réaliser le bilan de compétences"
|
||||
<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>
|
||||
{/* Partie basse : stats à gauche, présence à droite, sans bg */}
|
||||
{formData.selectedStudent && (
|
||||
<div className="flex flex-col gap-8 w-full justify-center items-stretch mt-8">
|
||||
<div className="w-3/4 flex flex-row items-stretch gap-4 mx-auto">
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Attendance absences={absences} />
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<GradesStatsCircle grades={grades} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<GradesDomainBarChart studentCompetencies={studentCompetencies} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -21,9 +21,10 @@ export default function StudentCompetenciesPage() {
|
||||
const [studentCompetencies, setStudentCompetencies] = useState([]);
|
||||
const [grades, setGrades] = useState({});
|
||||
const studentId = searchParams.get('studentId');
|
||||
const period = searchParams.get('period');
|
||||
|
||||
useEffect(() => {
|
||||
fetchStudentCompetencies(studentId)
|
||||
fetchStudentCompetencies(studentId, period)
|
||||
.then((data) => {
|
||||
setStudentCompetencies(data);
|
||||
})
|
||||
@ -64,6 +65,7 @@ export default function StudentCompetenciesPage() {
|
||||
studentId,
|
||||
competenceId,
|
||||
grade: score,
|
||||
period: period,
|
||||
}));
|
||||
editStudentCompetencies(data, csrfToken)
|
||||
.then(() => {
|
||||
|
||||
@ -42,7 +42,7 @@ import {
|
||||
import { fetchProfiles } from '@/app/actions/authAction';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
||||
import { FE_ADMIN_SUBSCRIPTIONS_URL, BASE_URL } from '@/utils/Url';
|
||||
|
||||
export default function CreateSubscriptionPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
|
||||
@ -24,16 +24,18 @@ export const editStudentCompetencies = (data, csrfToken) => {
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchStudentCompetencies = (id) => {
|
||||
const request = new Request(
|
||||
`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
export const fetchStudentCompetencies = (id, period) => {
|
||||
// Si period est vide, ne pas l'ajouter à l'URL
|
||||
const url = period
|
||||
? `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}&period=${period}`
|
||||
: `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`;
|
||||
|
||||
const request = new Request(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ const typeStyles = {
|
||||
};
|
||||
|
||||
export default function FlashNotification({
|
||||
displayPeriod = 3000,
|
||||
displayPeriod = 5000,
|
||||
title,
|
||||
message,
|
||||
type = 'info',
|
||||
|
||||
@ -32,7 +32,7 @@ export default function GradesDomainBarChart({ studentCompetencies }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-3/4 flex flex-col items-center gap-4 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<div className="w-full flex flex-col items-center gap-4 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<h2 className="text-xl font-semibold mb-2">Moyenne par domaine</h2>
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
{domainStats.map((d) => (
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
|
||||
export default function StudentInput({
|
||||
label,
|
||||
selectedStudent,
|
||||
setSelectedStudent,
|
||||
searchStudents,
|
||||
establishmentId,
|
||||
required = false,
|
||||
}) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
|
||||
// Désélectionner si l'input ne correspond plus à l'élève sélectionné
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedStudent &&
|
||||
inputValue !==
|
||||
`${selectedStudent.last_name} ${selectedStudent.first_name}`
|
||||
) {
|
||||
setSelectedStudent(null);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [inputValue]);
|
||||
|
||||
const handleInputChange = async (e) => {
|
||||
const value = e.target.value;
|
||||
setInputValue(value);
|
||||
|
||||
if (value.trim() !== '') {
|
||||
try {
|
||||
const results = await searchStudents(establishmentId, value);
|
||||
setSuggestions(results);
|
||||
} catch {
|
||||
setSuggestions([]);
|
||||
}
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (student) => {
|
||||
setSelectedStudent(student);
|
||||
setInputValue(`${student.last_name} ${student.first_name}`);
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
|
||||
handleSuggestionClick(suggestions[selectedIndex]);
|
||||
}
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
setSelectedIndex((prev) =>
|
||||
prev < suggestions.length - 1 ? prev + 1 : 0
|
||||
);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
setSelectedIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : suggestions.length - 1
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Rechercher un élève"
|
||||
className="mt-1 px-3 py-2 block w-full border rounded-md"
|
||||
required={required}
|
||||
/>
|
||||
{suggestions.length > 0 && (
|
||||
<ul className="border rounded mt-2 bg-white shadow">
|
||||
{suggestions.map((student, idx) => (
|
||||
<li
|
||||
key={student.id}
|
||||
className={`flex items-center gap-2 p-2 cursor-pointer transition-colors ${
|
||||
idx === selectedIndex
|
||||
? 'bg-emerald-100 text-emerald-800'
|
||||
: 'hover:bg-emerald-50'
|
||||
}`}
|
||||
onClick={() => handleSuggestionClick(student)}
|
||||
onMouseEnter={() => setSelectedIndex(idx)}
|
||||
>
|
||||
{student.photo ? (
|
||||
<img
|
||||
src={`${BASE_URL}${student.photo}`}
|
||||
alt={`${student.first_name} ${student.last_name}`}
|
||||
className="w-8 h-8 object-cover rounded-full border"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-semibold">
|
||||
{student.first_name?.[0]}
|
||||
{student.last_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
<span>
|
||||
{student.last_name} {student.first_name} ({student.level}) -{' '}
|
||||
{student.associated_class_name}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -12,6 +12,15 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
return storedEstablishmentId;
|
||||
}
|
||||
);
|
||||
const [
|
||||
selectedEstablishmentEvaluationFrequency,
|
||||
setSelectedEstablishmentEvaluationFrequencyState,
|
||||
] = useState(() => {
|
||||
const storedEstablishmentEvaluationFrequency = +sessionStorage.getItem(
|
||||
'selectedEstablishmentEvaluationFrequency'
|
||||
);
|
||||
return storedEstablishmentEvaluationFrequency;
|
||||
});
|
||||
const [selectedRoleId, setSelectedRoleIdState] = useState(() => {
|
||||
const storedRoleId = +sessionStorage.getItem('selectedRoleId');
|
||||
return storedRoleId;
|
||||
@ -36,6 +45,12 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
sessionStorage.setItem('selectedEstablishmentId', id);
|
||||
};
|
||||
|
||||
const setSelectedEstablishmentEvaluationFrequency = (id) => {
|
||||
setSelectedEstablishmentEvaluationFrequencyState(id);
|
||||
logger.debug('setSelectedEstablishmentEvaluationFrequency', id);
|
||||
sessionStorage.setItem('selectedEstablishmentEvaluationFrequency', id);
|
||||
};
|
||||
|
||||
const setSelectedRoleId = (id) => {
|
||||
setSelectedRoleIdState(id);
|
||||
sessionStorage.setItem('selectedRoleId', id);
|
||||
@ -72,6 +87,7 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
const userEstablishments = user.roles.map((role, i) => ({
|
||||
id: role.establishment__id,
|
||||
name: role.establishment__name,
|
||||
evaluation_frequency: role.establishment__evaluation_frequency,
|
||||
role_id: i,
|
||||
role_type: role.role_type,
|
||||
}));
|
||||
@ -85,6 +101,9 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
setSelectedRoleId(roleIndexDefault);
|
||||
if (userEstablishments.length > 0) {
|
||||
setSelectedEstablishmentId(userEstablishments[roleIndexDefault].id);
|
||||
setSelectedEstablishmentEvaluationFrequency(
|
||||
userEstablishments[roleIndexDefault].evaluation_frequency
|
||||
);
|
||||
setProfileRole(userEstablishments[roleIndexDefault].role_type);
|
||||
}
|
||||
if (endInitFunctionHandler) {
|
||||
@ -112,6 +131,8 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
clearContext,
|
||||
selectedEstablishmentId,
|
||||
setSelectedEstablishmentId,
|
||||
selectedEstablishmentEvaluationFrequency,
|
||||
setSelectedEstablishmentEvaluationFrequency,
|
||||
selectedRoleId,
|
||||
setSelectedRoleId,
|
||||
profileRole,
|
||||
|
||||
Reference in New Issue
Block a user