mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Champ de recherche de l'élève [#16]
This commit is contained in:
@ -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"),
|
||||||
|
|
||||||
|
|||||||
@ -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'
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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)
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
107
Front-End/src/components/Grades/StudentInput.js
Normal file
107
Front-End/src/components/Grades/StudentInput.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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`;
|
||||||
|
|||||||
Reference in New Issue
Block a user