feat: Champ de recherche de l'élève [#16]

This commit is contained in:
N3WT DE COMPET
2025-05-20 20:22:58 +02:00
parent 56c223f3cc
commit eb7805e54e
7 changed files with 181 additions and 6 deletions

View File

@ -19,7 +19,8 @@ from .views import (
AbsenceManagementListCreateView, AbsenceManagementListCreateView,
AbsenceManagementDetailView, AbsenceManagementDetailView,
StudentCompetencyListCreateView, StudentCompetencyListCreateView,
StudentCompetencySimpleView StudentCompetencySimpleView,
search_students
) )
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
@ -38,6 +39,7 @@ urlpatterns = [
re_path(r'^students$', StudentListView.as_view(), name="students"), re_path(r'^students$', StudentListView.as_view(), name="students"),
# Page de formulaire d'inscription - ELEVE # Page de formulaire d'inscription - ELEVE
re_path(r'^students/(?P<id>[0-9]+)$', StudentView.as_view(), name="students"), re_path(r'^students/(?P<id>[0-9]+)$', StudentView.as_view(), name="students"),
re_path(r'^search-students', search_students, name='search_students'),
# Page PARENT - Liste des children # Page PARENT - Liste des children
re_path(r'^children/(?P<id>[0-9]+)$', ChildrenListView.as_view(), name="children"), re_path(r'^children/(?P<id>[0-9]+)$', ChildrenListView.as_view(), name="children"),

View File

@ -10,7 +10,7 @@ from .registration_file_views import (
RegistrationParentFileTemplateView RegistrationParentFileTemplateView
) )
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .student_views import StudentView, StudentListView, ChildrenListView from .student_views import StudentView, StudentListView, ChildrenListView, search_students
from .guardian_views import GuardianView, DissociateGuardianView from .guardian_views import GuardianView, DissociateGuardianView
from .absences_views import AbsenceManagementDetailView, AbsenceManagementListCreateView from .absences_views import AbsenceManagementDetailView, AbsenceManagementListCreateView
from .student_competencies_views import StudentCompetencyListCreateView, StudentCompetencySimpleView from .student_competencies_views import StudentCompetencyListCreateView, StudentCompetencySimpleView
@ -42,5 +42,6 @@ __all__ = [
'AbsenceManagementDetailView', 'AbsenceManagementDetailView',
'AbsenceManagementListCreateView', 'AbsenceManagementListCreateView',
'StudentCompetencyListCreateView', 'StudentCompetencyListCreateView',
'StudentCompetencySimpleView' 'StudentCompetencySimpleView',
'search_students'
] ]

View File

@ -3,6 +3,7 @@ from rest_framework.views import APIView
from rest_framework import status from rest_framework import status
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from django.db.models import Q
from Subscriptions.serializers import StudentByRFCreationSerializer, RegistrationFormByParentSerializer, StudentSerializer from Subscriptions.serializers import StudentByRFCreationSerializer, RegistrationFormByParentSerializer, StudentSerializer
from Subscriptions.models import Student, RegistrationForm from Subscriptions.models import Student, RegistrationForm
@ -115,3 +116,37 @@ class ChildrenListView(APIView):
).distinct() ).distinct()
students_serializer = RegistrationFormByParentSerializer(students, many=True) students_serializer = RegistrationFormByParentSerializer(students, many=True)
return JsonResponse(students_serializer.data, safe=False) return JsonResponse(students_serializer.data, safe=False)
def search_students(request):
"""
API pour rechercher des étudiants en fonction d'un terme de recherche (nom/prénom) et d'un établissement.
"""
query = request.GET.get('q', '').strip()
establishment_id = request.GET.get('establishment_id', None)
if not query:
return JsonResponse([], safe=False)
if not establishment_id:
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
# Recherche sur Student (nom ou prénom) et filtrage par établissement via RegistrationForm
students = Student.objects.filter(
Q(last_name__icontains=query) | Q(first_name__icontains=query),
registrationform__establishment_id=establishment_id
).distinct()
# Sérialisation simple (adapte selon ton besoin)
results = [
{
'id': student.id,
'first_name': student.first_name,
'last_name': student.last_name,
'level': getattr(student.level, 'name', ''),
'associated_class_name': student.associated_class.atmosphere_name if student.associated_class else '',
'photo': student.photo.url if student.photo else None,
}
for student in students
]
return JsonResponse(results, safe=False)

View File

@ -16,9 +16,11 @@ import { useRouter } from 'next/navigation';
import { import {
fetchStudents, fetchStudents,
fetchStudentCompetencies, fetchStudentCompetencies,
searchStudents,
} from '@/app/actions/subscriptionAction'; } from '@/app/actions/subscriptionAction';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { useClasses } from '@/context/ClassesContext'; import { useClasses } from '@/context/ClassesContext';
import StudentInput from '@/components/Grades/StudentInput';
export default function Page() { export default function Page() {
const router = useRouter(); const router = useRouter();
@ -147,7 +149,19 @@ export default function Page() {
<h2 className="text-xl font-semibold mb-4">Sélectionner un élève</h2> <h2 className="text-xl font-semibold mb-4">Sélectionner un élève</h2>
<div className="flex flex-col sm:flex-row sm:items-end gap-4"> <div className="flex flex-col sm:flex-row sm:items-end gap-4">
<div className="flex-1"> <div className="flex-1">
<SelectChoice <StudentInput
label="Recherche élève"
selectedStudent={
students.find((s) => s.id === formData.selectedStudent) || null
}
setSelectedStudent={(student) =>
handleChange('selectedStudent', student?.id || '')
}
searchStudents={searchStudents}
establishmentId={selectedEstablishmentId}
required
/>
{/* <SelectChoice
name="selectedStudent" name="selectedStudent"
label="Élève" label="Élève"
placeHolder="Sélectionnez un élève" placeHolder="Sélectionnez un élève"
@ -155,10 +169,12 @@ export default function Page() {
callback={(e) => handleChange('selectedStudent', e.target.value)} callback={(e) => handleChange('selectedStudent', e.target.value)}
choices={students.map((student) => ({ choices={students.map((student) => ({
value: student.id, value: student.id,
label: `${student.last_name} ${student.first_name} - ${getNiveauLabel(student.level)} (${student.associated_class_name})`, label: `${student.last_name} ${student.first_name} - ${getNiveauLabel(
student.level
)} (${student.associated_class_name})`,
}))} }))}
required required
/> /> */}
</div> </div>
<Button <Button
text="Réaliser le bilan de compétences" text="Réaliser le bilan de compétences"

View File

@ -5,6 +5,7 @@ import {
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL, BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
BE_SUBSCRIPTION_ABSENCES_URL, BE_SUBSCRIPTION_ABSENCES_URL,
BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL, BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL,
BE_SUBSCRIPTION_SEARCH_STUDENTS_URL,
} from '@/utils/Url'; } from '@/utils/Url';
import { CURRENT_YEAR_FILTER } from '@/utils/constants'; import { CURRENT_YEAR_FILTER } from '@/utils/constants';
@ -128,6 +129,18 @@ export const archiveRegisterForm = (id) => {
.catch(errorHandler); .catch(errorHandler);
}; };
export const searchStudents = (establishmentId, query) => {
const url = `${BE_SUBSCRIPTION_SEARCH_STUDENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchStudents = (establishment, id = null, status = null) => { export const fetchStudents = (establishment, id = null, status = null) => {
let url; let url;
if (id) { if (id) {

View File

@ -0,0 +1,107 @@
import React, { useState } 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);
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} (${student.level}) - ${student.associated_class_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>
);
}

View File

@ -23,6 +23,7 @@ export const BE_AUTH_INFO_SESSION = `${BASE_URL}/Auth/infoSession`;
// GESTION INSCRIPTION // GESTION INSCRIPTION
export const BE_SUBSCRIPTION_STUDENTS_URL = `${BASE_URL}/Subscriptions/students`; // Récupère la liste des élèves inscrits ou en cours d'inscriptions export const BE_SUBSCRIPTION_STUDENTS_URL = `${BASE_URL}/Subscriptions/students`; // Récupère la liste des élèves inscrits ou en cours d'inscriptions
export const BE_SUBSCRIPTION_SEARCH_STUDENTS_URL = `${BASE_URL}/Subscriptions/search-students`;
export const BE_SUBSCRIPTION_CHILDRENS_URL = `${BASE_URL}/Subscriptions/children`; // Récupère la liste des élèves d'un profil export const BE_SUBSCRIPTION_CHILDRENS_URL = `${BASE_URL}/Subscriptions/children`; // Récupère la liste des élèves d'un profil
export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms`; export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms`;
export const BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL = `${BASE_URL}/Subscriptions/registrationFileGroups`; export const BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL = `${BASE_URL}/Subscriptions/registrationFileGroups`;