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,
|
||||
AbsenceManagementDetailView,
|
||||
StudentCompetencyListCreateView,
|
||||
StudentCompetencySimpleView
|
||||
StudentCompetencySimpleView,
|
||||
search_students
|
||||
)
|
||||
|
||||
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
||||
@ -38,6 +39,7 @@ urlpatterns = [
|
||||
re_path(r'^students$', StudentListView.as_view(), name="students"),
|
||||
# Page de formulaire d'inscription - ELEVE
|
||||
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
|
||||
re_path(r'^children/(?P<id>[0-9]+)$', ChildrenListView.as_view(), name="children"),
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ from .registration_file_views import (
|
||||
RegistrationParentFileTemplateView
|
||||
)
|
||||
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 .absences_views import AbsenceManagementDetailView, AbsenceManagementListCreateView
|
||||
from .student_competencies_views import StudentCompetencyListCreateView, StudentCompetencySimpleView
|
||||
@ -42,5 +42,6 @@ __all__ = [
|
||||
'AbsenceManagementDetailView',
|
||||
'AbsenceManagementListCreateView',
|
||||
'StudentCompetencyListCreateView',
|
||||
'StudentCompetencySimpleView'
|
||||
'StudentCompetencySimpleView',
|
||||
'search_students'
|
||||
]
|
||||
|
||||
@ -3,6 +3,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from django.db.models import Q
|
||||
|
||||
from Subscriptions.serializers import StudentByRFCreationSerializer, RegistrationFormByParentSerializer, StudentSerializer
|
||||
from Subscriptions.models import Student, RegistrationForm
|
||||
@ -115,3 +116,37 @@ class ChildrenListView(APIView):
|
||||
).distinct()
|
||||
students_serializer = RegistrationFormByParentSerializer(students, many=True)
|
||||
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 {
|
||||
fetchStudents,
|
||||
fetchStudentCompetencies,
|
||||
searchStudents,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import StudentInput from '@/components/Grades/StudentInput';
|
||||
|
||||
export default function Page() {
|
||||
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>
|
||||
<div className="flex flex-col sm:flex-row sm:items-end gap-4">
|
||||
<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"
|
||||
label="Élève"
|
||||
placeHolder="Sélectionnez un élève"
|
||||
@ -155,10 +169,12 @@ export default function Page() {
|
||||
callback={(e) => handleChange('selectedStudent', e.target.value)}
|
||||
choices={students.map((student) => ({
|
||||
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
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
<Button
|
||||
text="Réaliser le bilan de compétences"
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
|
||||
BE_SUBSCRIPTION_ABSENCES_URL,
|
||||
BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL,
|
||||
BE_SUBSCRIPTION_SEARCH_STUDENTS_URL,
|
||||
} from '@/utils/Url';
|
||||
|
||||
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
|
||||
@ -128,6 +129,18 @@ export const archiveRegisterForm = (id) => {
|
||||
.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) => {
|
||||
let url;
|
||||
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
|
||||
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_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms`;
|
||||
export const BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL = `${BASE_URL}/Subscriptions/registrationFileGroups`;
|
||||
|
||||
Reference in New Issue
Block a user