mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 12:41:27 +00:00
feat: Mise à jour de la page parent
This commit is contained in:
@ -73,12 +73,15 @@ class SpecialityListCreateView(APIView):
|
|||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
|
school_year = request.GET.get('school_year', None)
|
||||||
if establishment_id is None:
|
if establishment_id is None:
|
||||||
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
specialities_list = getAllObjects(Speciality)
|
specialities_list = getAllObjects(Speciality)
|
||||||
if establishment_id:
|
if establishment_id:
|
||||||
specialities_list = specialities_list.filter(establishment__id=establishment_id).distinct()
|
specialities_list = specialities_list.filter(establishment__id=establishment_id).distinct()
|
||||||
|
if school_year:
|
||||||
|
specialities_list = specialities_list.filter(school_year=school_year)
|
||||||
specialities_serializer = SpecialitySerializer(specialities_list, many=True)
|
specialities_serializer = SpecialitySerializer(specialities_list, many=True)
|
||||||
return JsonResponse(specialities_serializer.data, safe=False)
|
return JsonResponse(specialities_serializer.data, safe=False)
|
||||||
|
|
||||||
|
|||||||
@ -399,10 +399,11 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
|
|||||||
class StudentByParentSerializer(serializers.ModelSerializer):
|
class StudentByParentSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.IntegerField(required=False)
|
id = serializers.IntegerField(required=False)
|
||||||
associated_class_name = serializers.SerializerMethodField()
|
associated_class_name = serializers.SerializerMethodField()
|
||||||
|
associated_class_id = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Student
|
model = Student
|
||||||
fields = ['id', 'last_name', 'first_name', 'level', 'photo', 'associated_class_name']
|
fields = ['id', 'last_name', 'first_name', 'level', 'photo', 'associated_class_name', 'associated_class_id']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(StudentByParentSerializer, self).__init__(*args, **kwargs)
|
super(StudentByParentSerializer, self).__init__(*args, **kwargs)
|
||||||
@ -412,6 +413,9 @@ class StudentByParentSerializer(serializers.ModelSerializer):
|
|||||||
def get_associated_class_name(self, obj):
|
def get_associated_class_name(self, obj):
|
||||||
return obj.associated_class.atmosphere_name if obj.associated_class else None
|
return obj.associated_class.atmosphere_name if obj.associated_class else None
|
||||||
|
|
||||||
|
def get_associated_class_id(self, obj):
|
||||||
|
return obj.associated_class.id if obj.associated_class else None
|
||||||
|
|
||||||
class RegistrationFormByParentSerializer(serializers.ModelSerializer):
|
class RegistrationFormByParentSerializer(serializers.ModelSerializer):
|
||||||
student = StudentByParentSerializer(many=False, required=True)
|
student = StudentByParentSerializer(many=False, required=True)
|
||||||
|
|
||||||
|
|||||||
@ -205,10 +205,12 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}, [filteredStudents, fetchedAbsences]);
|
}, [filteredStudents, fetchedAbsences]);
|
||||||
|
|
||||||
// Load specialities for evaluations
|
// Load specialities for evaluations (filtered by current school year)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEstablishmentId) {
|
if (selectedEstablishmentId) {
|
||||||
fetchSpecialities(selectedEstablishmentId)
|
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
||||||
|
const currentSchoolYear = `${year}-${year + 1}`;
|
||||||
|
fetchSpecialities(selectedEstablishmentId, currentSchoolYear)
|
||||||
.then((data) => setSpecialities(data))
|
.then((data) => setSpecialities(data))
|
||||||
.catch((error) => logger.error('Erreur lors du chargement des matières:', error));
|
.catch((error) => logger.error('Erreur lors du chargement des matières:', error));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Table from '@/components/Table';
|
|
||||||
import {
|
import {
|
||||||
Edit3,
|
Edit3,
|
||||||
Users,
|
Users,
|
||||||
@ -9,6 +8,12 @@ import {
|
|||||||
Eye,
|
Eye,
|
||||||
Upload,
|
Upload,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
|
Award,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
BookOpen,
|
||||||
|
ArrowLeft,
|
||||||
|
Clock,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import StatusLabel from '@/components/StatusLabel';
|
import StatusLabel from '@/components/StatusLabel';
|
||||||
import FileUpload from '@/components/Form/FileUpload';
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
@ -16,7 +21,13 @@ import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
|
|||||||
import {
|
import {
|
||||||
fetchChildren,
|
fetchChildren,
|
||||||
editRegisterForm,
|
editRegisterForm,
|
||||||
|
fetchStudentCompetencies,
|
||||||
|
fetchAbsences,
|
||||||
} from '@/app/actions/subscriptionAction';
|
} from '@/app/actions/subscriptionAction';
|
||||||
|
import {
|
||||||
|
fetchEvaluations,
|
||||||
|
fetchStudentEvaluations,
|
||||||
|
} from '@/app/actions/schoolAction';
|
||||||
import { fetchUpcomingEvents } from '@/app/actions/planningAction';
|
import { fetchUpcomingEvents } from '@/app/actions/planningAction';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||||
@ -27,13 +38,47 @@ import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
|||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import ParentPlanningSection from '@/components/ParentPlanningSection';
|
import ParentPlanningSection from '@/components/ParentPlanningSection';
|
||||||
import EventCard from '@/components/EventCard';
|
import EventCard from '@/components/EventCard';
|
||||||
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
// Fonction utilitaire pour générer la chaîne de période
|
||||||
|
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 '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour obtenir les périodes selon la fréquence d'évaluation
|
||||||
|
function getPeriods(frequency) {
|
||||||
|
if (frequency === 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 (frequency === 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 (frequency === 3) {
|
||||||
|
return [{ label: 'Année', value: 1, start: '09-01', end: '07-15' }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
export default function ParentHomePage() {
|
export default function ParentHomePage() {
|
||||||
const [children, setChildren] = useState([]);
|
const [children, setChildren] = useState([]);
|
||||||
const { user, selectedEstablishmentId } = useEstablishment();
|
const { user, selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = useEstablishment();
|
||||||
const [uploadingStudentId, setUploadingStudentId] = useState(null); // ID de l'étudiant pour l'upload
|
const [uploadingStudentId, setUploadingStudentId] = useState(null);
|
||||||
const [uploadedFile, setUploadedFile] = useState(null); // Fichier uploadé
|
const [uploadedFile, setUploadedFile] = useState(null);
|
||||||
const [uploadState, setUploadState] = useState('off'); // État "on" ou "off" pour l'affichage du composant
|
const [uploadState, setUploadState] = useState('off');
|
||||||
const [showPlanning, setShowPlanning] = useState(false);
|
const [showPlanning, setShowPlanning] = useState(false);
|
||||||
const [planningClassName, setPlanningClassName] = useState(null);
|
const [planningClassName, setPlanningClassName] = useState(null);
|
||||||
const [upcomingEvents, setUpcomingEvents] = useState([]);
|
const [upcomingEvents, setUpcomingEvents] = useState([]);
|
||||||
@ -42,16 +87,114 @@ export default function ParentHomePage() {
|
|||||||
const [reloadFetch, setReloadFetch] = useState(false);
|
const [reloadFetch, setReloadFetch] = useState(false);
|
||||||
const { getNiveauLabel } = useClasses();
|
const { getNiveauLabel } = useClasses();
|
||||||
|
|
||||||
|
// États pour la vue détaillée de l'élève inscrit
|
||||||
|
const [expandedStudentId, setExpandedStudentId] = useState(null);
|
||||||
|
const [studentCompetencies, setStudentCompetencies] = useState(null);
|
||||||
|
const [grades, setGrades] = useState({});
|
||||||
|
const [selectedPeriod, setSelectedPeriod] = useState(null);
|
||||||
|
const [evaluations, setEvaluations] = useState([]);
|
||||||
|
const [studentEvaluationsData, setStudentEvaluationsData] = useState([]);
|
||||||
|
const [allAbsences, setAllAbsences] = useState([]);
|
||||||
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
|
|
||||||
|
// Périodes disponibles selon la fréquence d'évaluation
|
||||||
|
const periods = useMemo(
|
||||||
|
() => getPeriods(selectedEstablishmentEvaluationFrequency),
|
||||||
|
[selectedEstablishmentEvaluationFrequency]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-sélection de la période courante
|
||||||
|
useEffect(() => {
|
||||||
|
if (periods.length > 0 && !selectedPeriod) {
|
||||||
|
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 : periods[0]?.value);
|
||||||
|
}
|
||||||
|
}, [periods, selectedPeriod]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user !== null) {
|
if (user !== null) {
|
||||||
const userIdFromSession = user.user_id;
|
const userIdFromSession = user.user_id;
|
||||||
fetchChildren(userIdFromSession, selectedEstablishmentId).then((data) => {
|
fetchChildren(userIdFromSession, selectedEstablishmentId).then((data) => {
|
||||||
setChildren(data);
|
setChildren(data);
|
||||||
|
// Auto-expand si un seul enfant inscrit
|
||||||
|
const enrolledChildren = (data || []).filter((c) => c.status === 5);
|
||||||
|
if (enrolledChildren.length === 1) {
|
||||||
|
setExpandedStudentId(enrolledChildren[0].student.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setReloadFetch(false);
|
setReloadFetch(false);
|
||||||
}
|
}
|
||||||
}, [selectedEstablishmentId, reloadFetch, user]);
|
}, [selectedEstablishmentId, reloadFetch, user]);
|
||||||
|
|
||||||
|
// Charger les absences
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedEstablishmentId) {
|
||||||
|
fetchAbsences(selectedEstablishmentId)
|
||||||
|
.then((data) => setAllAbsences(data || []))
|
||||||
|
.catch((error) => logger.error('Erreur fetch absences:', error));
|
||||||
|
}
|
||||||
|
}, [selectedEstablishmentId]);
|
||||||
|
|
||||||
|
// Charger les données détaillées quand un élève est étendu
|
||||||
|
useEffect(() => {
|
||||||
|
if (!expandedStudentId || !selectedPeriod || !selectedEstablishmentEvaluationFrequency) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandedChild = children.find((c) => c.student.id === expandedStudentId);
|
||||||
|
if (!expandedChild || expandedChild.status !== 5) return;
|
||||||
|
|
||||||
|
const loadDetails = async () => {
|
||||||
|
setDetailLoading(true);
|
||||||
|
const periodString = getPeriodString(selectedPeriod, selectedEstablishmentEvaluationFrequency);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Charger les compétences
|
||||||
|
const competenciesData = await fetchStudentCompetencies(expandedStudentId, periodString);
|
||||||
|
setStudentCompetencies(competenciesData);
|
||||||
|
if (competenciesData?.data) {
|
||||||
|
const initialGrades = {};
|
||||||
|
competenciesData.data.forEach((domaine) => {
|
||||||
|
domaine.categories.forEach((cat) => {
|
||||||
|
cat.competences.forEach((comp) => {
|
||||||
|
initialGrades[comp.competence_id] = comp.score ?? 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setGrades(initialGrades);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charger les évaluations si l'élève a une classe
|
||||||
|
if (expandedChild.student.associated_class_id) {
|
||||||
|
const [evalData, studentEvalData] = await Promise.all([
|
||||||
|
fetchEvaluations(
|
||||||
|
selectedEstablishmentId,
|
||||||
|
expandedChild.student.associated_class_id,
|
||||||
|
periodString
|
||||||
|
),
|
||||||
|
fetchStudentEvaluations(expandedStudentId, null, periodString, null)
|
||||||
|
]);
|
||||||
|
setEvaluations(evalData || []);
|
||||||
|
setStudentEvaluationsData(studentEvalData || []);
|
||||||
|
} else {
|
||||||
|
setEvaluations([]);
|
||||||
|
setStudentEvaluationsData([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors du chargement des détails:', error);
|
||||||
|
} finally {
|
||||||
|
setDetailLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadDetails();
|
||||||
|
}, [expandedStudentId, selectedPeriod, selectedEstablishmentEvaluationFrequency, children, selectedEstablishmentId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEstablishmentId) {
|
if (selectedEstablishmentId) {
|
||||||
// Fetch des événements à venir
|
// Fetch des événements à venir
|
||||||
@ -132,153 +275,6 @@ export default function ParentHomePage() {
|
|||||||
setShowPlanning(true);
|
setShowPlanning(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const childrenColumns = [
|
|
||||||
{
|
|
||||||
name: 'photo',
|
|
||||||
transform: (row) => (
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
{row.student.photo ? (
|
|
||||||
<a
|
|
||||||
href={getSecureFileUrl(row.student.photo)} // Lien vers la photo
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={getSecureFileUrl(row.student.photo)}
|
|
||||||
alt={`${row.student.first_name} ${row.student.last_name}`}
|
|
||||||
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer rounded-full"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<div className="w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full">
|
|
||||||
<span className="text-gray-500 text-sm font-semibold">
|
|
||||||
{row.student.first_name[0]}
|
|
||||||
{row.student.last_name[0]}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ name: 'Nom', transform: (row) => `${row.student.last_name}` },
|
|
||||||
{ name: 'Prénom', transform: (row) => `${row.student.first_name}` },
|
|
||||||
{
|
|
||||||
name: 'Classe',
|
|
||||||
transform: (row) => (
|
|
||||||
<div className="text-center">{row.student.associated_class_name}</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Niveau',
|
|
||||||
transform: (row) => (
|
|
||||||
<div className="text-center">{getNiveauLabel(row.student.level)}</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Statut',
|
|
||||||
transform: (row) => (
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
<StatusLabel status={row.status} showDropdown={false} parent />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Actions',
|
|
||||||
transform: (row) => (
|
|
||||||
<div className="flex justify-center items-center gap-2">
|
|
||||||
{row.status === 2 && (
|
|
||||||
<button
|
|
||||||
className="text-blue-500 hover:text-blue-700"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleEdit(row.student.id);
|
|
||||||
}}
|
|
||||||
aria-label="Remplir le dossier"
|
|
||||||
>
|
|
||||||
<Edit3 className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(row.status === 3 || row.status === 8) && (
|
|
||||||
<button
|
|
||||||
className="text-purple-500 hover:text-purple-700"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleView(row.student.id);
|
|
||||||
}}
|
|
||||||
aria-label="Visualiser le dossier"
|
|
||||||
>
|
|
||||||
<Eye className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{row.status === 7 && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="flex items-center justify-center w-8 h-8 rounded-full text-purple-500 hover:text-purple-700"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleView(row.student.id);
|
|
||||||
}}
|
|
||||||
aria-label="Visualiser le dossier"
|
|
||||||
>
|
|
||||||
<Eye className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
href={getSecureFileUrl(row.sepa_file)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center justify-center w-8 h-8 rounded-full text-green-500 hover:text-green-700"
|
|
||||||
aria-label="Télécharger le mandat SEPA"
|
|
||||||
>
|
|
||||||
<Download className="h-5 w-5" />
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
|
||||||
uploadingStudentId === row.student.id && uploadState === 'on'
|
|
||||||
? 'bg-blue-100 text-blue-600 ring-3 ring-blue-500'
|
|
||||||
: 'text-blue-500 hover:text-blue-700'
|
|
||||||
}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
toggleUpload(row.student.id);
|
|
||||||
}}
|
|
||||||
aria-label="Uploader un fichier"
|
|
||||||
>
|
|
||||||
<Upload className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{row.status === 5 && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="text-purple-500 hover:text-purple-700"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleView(row.student.id);
|
|
||||||
}}
|
|
||||||
aria-label="Visualiser le dossier"
|
|
||||||
>
|
|
||||||
<Eye className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="text-emerald-500 hover:text-emerald-700 ml-1"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
showClassPlanning(row.student);
|
|
||||||
}}
|
|
||||||
aria-label="Voir le planning de la classe"
|
|
||||||
>
|
|
||||||
<CalendarDays className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
{showPlanning && planningClassName ? (
|
{showPlanning && planningClassName ? (
|
||||||
@ -326,29 +322,375 @@ export default function ParentHomePage() {
|
|||||||
title="Vos enfants"
|
title="Vos enfants"
|
||||||
description="Suivez le parcours de vos enfants"
|
description="Suivez le parcours de vos enfants"
|
||||||
/>
|
/>
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<Table data={children} columns={childrenColumns} />
|
{/* Cartes des enfants */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{children.map((child) => {
|
||||||
|
const student = child.student;
|
||||||
|
const isEnrolled = child.status === 5;
|
||||||
|
const isExpanded = expandedStudentId === student.id;
|
||||||
|
|
||||||
|
// Absences pour cet élève (détaillées par type)
|
||||||
|
const studentAbsencesList = allAbsences.filter((a) => a.student === student.id);
|
||||||
|
const absenceStats = {
|
||||||
|
justifiedAbsence: studentAbsencesList.filter((a) => a.reason === 1).length,
|
||||||
|
unjustifiedAbsence: studentAbsencesList.filter((a) => a.reason === 2).length,
|
||||||
|
justifiedLate: studentAbsencesList.filter((a) => a.reason === 3).length,
|
||||||
|
unjustifiedLate: studentAbsencesList.filter((a) => a.reason === 4).length,
|
||||||
|
};
|
||||||
|
const totalAbsences = absenceStats.justifiedAbsence + absenceStats.unjustifiedAbsence;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={student.id}
|
||||||
|
className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* En-tête de la carte (toujours visible) */}
|
||||||
|
<div
|
||||||
|
className={`p-4 flex flex-col sm:flex-row items-start sm:items-center gap-4 ${
|
||||||
|
isEnrolled ? 'cursor-pointer hover:bg-gray-50' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (isEnrolled) {
|
||||||
|
setExpandedStudentId(isExpanded ? null : student.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Photo */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{student.photo ? (
|
||||||
|
<img
|
||||||
|
src={getSecureFileUrl(student.photo)}
|
||||||
|
alt={`${student.first_name} ${student.last_name}`}
|
||||||
|
className="w-16 h-16 object-cover rounded-full border-2 border-primary"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-16 h-16 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-xl">
|
||||||
|
{student.first_name?.[0]}{student.last_name?.[0]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Infos principales */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">
|
||||||
|
{student.last_name} {student.first_name}
|
||||||
|
</h3>
|
||||||
|
<div className="mt-1">
|
||||||
|
<StatusLabel status={child.status} showDropdown={false} parent />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 mt-2">
|
||||||
|
{student.associated_class_name && (
|
||||||
|
<span>Classe : <span className="font-medium">{student.associated_class_name}</span></span>
|
||||||
|
)}
|
||||||
|
{student.level !== undefined && (
|
||||||
|
<span className="ml-3">Niveau : <span className="font-medium">{getNiveauLabel(student.level)}</span></span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isEnrolled && (
|
||||||
|
<div className="text-xs text-gray-500 mt-1 flex items-center gap-1">
|
||||||
|
<Award className="w-3 h-3" />
|
||||||
|
<span>Cliquez pour voir le suivi pédagogique</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
{child.status === 2 && (
|
||||||
|
<button
|
||||||
|
className="p-2 text-blue-500 hover:text-blue-700 hover:bg-blue-50 rounded-full"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEdit(student.id);
|
||||||
|
}}
|
||||||
|
title="Remplir le dossier"
|
||||||
|
>
|
||||||
|
<Edit3 className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(child.status === 3 || child.status === 8 || child.status === 5 || child.status === 7) && (
|
||||||
|
<button
|
||||||
|
className="p-2 text-purple-500 hover:text-purple-700 hover:bg-purple-50 rounded-full"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleView(student.id);
|
||||||
|
}}
|
||||||
|
title="Visualiser le dossier"
|
||||||
|
>
|
||||||
|
<Eye className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{child.status === 7 && (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href={getSecureFileUrl(child.sepa_file)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="p-2 text-green-500 hover:text-green-700 hover:bg-green-50 rounded-full"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
title="Télécharger le mandat SEPA"
|
||||||
|
>
|
||||||
|
<Download className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
className={`p-2 rounded-full ${
|
||||||
|
uploadingStudentId === student.id && uploadState === 'on'
|
||||||
|
? 'bg-blue-100 text-blue-600'
|
||||||
|
: 'text-blue-500 hover:text-blue-700 hover:bg-blue-50'
|
||||||
|
}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleUpload(student.id);
|
||||||
|
}}
|
||||||
|
title="Uploader un fichier"
|
||||||
|
>
|
||||||
|
<Upload className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEnrolled && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="p-2 text-primary hover:text-secondary hover:bg-tertiary/10 rounded-full"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
showClassPlanning(student);
|
||||||
|
}}
|
||||||
|
title="Voir le planning de la classe"
|
||||||
|
>
|
||||||
|
<CalendarDays className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<div className="ml-2">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-5 h-5 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-5 h-5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload SEPA si activé */}
|
||||||
|
{uploadState === 'on' && uploadingStudentId === student.id && (
|
||||||
|
<div className="p-4 border-t bg-gray-50">
|
||||||
|
<FileUpload
|
||||||
|
selectionMessage="Sélectionnez un fichier à uploader"
|
||||||
|
onFileSelect={handleFileUpload}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={`mt-4 px-6 py-2 rounded-md ${
|
||||||
|
uploadedFile
|
||||||
|
? 'bg-primary text-white hover:bg-secondary'
|
||||||
|
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!uploadedFile}
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Section détaillée pour les élèves inscrits (expanded) */}
|
||||||
|
{isEnrolled && isExpanded && (
|
||||||
|
<div className="border-t bg-stone-50 p-4 space-y-6">
|
||||||
|
|
||||||
|
{/* Bloc période : compétences + notes */}
|
||||||
|
<div className="bg-white rounded-lg border border-primary/20 p-4 space-y-4">
|
||||||
|
{/* Sélecteur de période */}
|
||||||
|
<div className="flex items-center gap-3 pb-3 border-b border-gray-100">
|
||||||
|
<div className="w-full sm:w-48">
|
||||||
|
<SelectChoice
|
||||||
|
name="period"
|
||||||
|
label="Période"
|
||||||
|
placeHolder="Choisir la période"
|
||||||
|
choices={periods.map((period) => ({
|
||||||
|
value: period.value,
|
||||||
|
label: period.label,
|
||||||
|
}))}
|
||||||
|
selected={selectedPeriod || ''}
|
||||||
|
callback={(e) => setSelectedPeriod(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{detailLoading ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
Chargement des données...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Résumé des compétences (pourcentages) */}
|
||||||
|
{(() => {
|
||||||
|
const total = Object.keys(grades).length;
|
||||||
|
const acquired = Object.values(grades).filter((g) => g === 3).length;
|
||||||
|
const inProgress = Object.values(grades).filter((g) => g === 2).length;
|
||||||
|
const notAcquired = Object.values(grades).filter((g) => g === 1).length;
|
||||||
|
const notEvaluated = Object.values(grades).filter((g) => g === 0).length;
|
||||||
|
|
||||||
|
const pctAcquired = total ? Math.round((acquired / total) * 100) : 0;
|
||||||
|
const pctInProgress = total ? Math.round((inProgress / total) * 100) : 0;
|
||||||
|
const pctNotAcquired = total ? Math.round((notAcquired / total) * 100) : 0;
|
||||||
|
const pctNotEvaluated = total ? Math.round((notEvaluated / total) * 100) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-gray-100 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Award className="w-5 h-5 text-primary" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">
|
||||||
|
Compétences
|
||||||
|
</h3>
|
||||||
|
{total > 0 && (
|
||||||
|
<span className="text-sm text-gray-500">({total} compétences)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{total > 0 ? (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
|
<div className="flex flex-col items-center p-3 bg-emerald-50 rounded-lg">
|
||||||
|
<span className="text-2xl font-bold text-emerald-600">{pctAcquired}%</span>
|
||||||
|
<span className="text-sm text-emerald-700">Acquises</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-3 bg-yellow-50 rounded-lg">
|
||||||
|
<span className="text-2xl font-bold text-yellow-600">{pctInProgress}%</span>
|
||||||
|
<span className="text-sm text-yellow-700">En cours</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-3 bg-red-50 rounded-lg">
|
||||||
|
<span className="text-2xl font-bold text-red-500">{pctNotAcquired}%</span>
|
||||||
|
<span className="text-sm text-red-600">Non acquises</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-3 bg-gray-100 rounded-lg">
|
||||||
|
<span className="text-2xl font-bold text-gray-500">{pctNotEvaluated}%</span>
|
||||||
|
<span className="text-sm text-gray-600">Non évaluées</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 text-sm">Aucune compétence évaluée pour cette période.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Notes par matière - Vue simplifiée */}
|
||||||
|
<div className="border border-gray-100 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Award className="w-5 h-5 text-primary" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">
|
||||||
|
Notes par matière
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{evaluations.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{(() => {
|
||||||
|
// Grouper par matière
|
||||||
|
const bySpeciality = evaluations.reduce((acc, ev) => {
|
||||||
|
const key = ev.speciality_name || 'Sans matière';
|
||||||
|
if (!acc[key]) {
|
||||||
|
acc[key] = {
|
||||||
|
name: key,
|
||||||
|
color: ev.speciality_color || '#6B7280',
|
||||||
|
evaluations: [],
|
||||||
|
totalWeighted: 0,
|
||||||
|
totalCoef: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const studentEval = studentEvaluationsData.find((se) => se.evaluation === ev.id);
|
||||||
|
acc[key].evaluations.push({ ...ev, studentScore: studentEval?.score, isAbsent: studentEval?.is_absent });
|
||||||
|
if (studentEval?.score != null && !studentEval?.is_absent) {
|
||||||
|
const normalized = (studentEval.score / ev.max_score) * 20;
|
||||||
|
acc[key].totalWeighted += normalized * ev.coefficient;
|
||||||
|
acc[key].totalCoef += parseFloat(ev.coefficient);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return Object.values(bySpeciality).map((group) => {
|
||||||
|
const avg = group.totalCoef > 0 ? (group.totalWeighted / group.totalCoef) : null;
|
||||||
|
const evalCount = group.evaluations.length;
|
||||||
|
const gradedCount = group.evaluations.filter((e) => e.studentScore != null).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={group.name}
|
||||||
|
className="rounded-lg p-4 border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${group.color}10`,
|
||||||
|
borderColor: `${group.color}40`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span
|
||||||
|
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||||
|
style={{ backgroundColor: group.color }}
|
||||||
|
/>
|
||||||
|
<span className="font-medium text-gray-800 truncate">{group.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-baseline justify-between">
|
||||||
|
<span
|
||||||
|
className="text-2xl font-bold"
|
||||||
|
style={{ color: group.color }}
|
||||||
|
>
|
||||||
|
{avg !== null ? avg.toFixed(1) : '-'}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500">/20</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
{gradedCount}/{evalCount} évaluation{evalCount > 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 text-sm text-center py-4">Aucune évaluation pour cette période.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Fin bloc période */}
|
||||||
|
|
||||||
|
{/* Section Absences — toute l'année scolaire */}
|
||||||
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Clock className="w-5 h-5 text-primary" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">
|
||||||
|
Absences & Retards
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400 mb-4">Toute l'année scolaire</p>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
|
<div className="flex flex-col items-center p-3 bg-green-50 rounded-lg">
|
||||||
|
<span className="text-2xl font-bold text-green-600">{absenceStats.justifiedAbsence}</span>
|
||||||
|
<span className="text-sm text-green-700 text-center">Absences justifiées</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-3 bg-red-50 rounded-lg">
|
||||||
|
<span className="text-2xl font-bold text-red-500">{absenceStats.unjustifiedAbsence}</span>
|
||||||
|
<span className="text-sm text-red-600 text-center">Absences non justifiées</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-3 bg-blue-50 rounded-lg">
|
||||||
|
<span className="text-2xl font-bold text-blue-600">{absenceStats.justifiedLate}</span>
|
||||||
|
<span className="text-sm text-blue-700 text-center">Retards justifiés</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-3 bg-orange-50 rounded-lg">
|
||||||
|
<span className="text-2xl font-bold text-orange-500">{absenceStats.unjustifiedLate}</span>
|
||||||
|
<span className="text-sm text-orange-600 text-center">Retards non justifiés</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* Composant FileUpload et bouton Valider en dessous du tableau */}
|
|
||||||
{uploadState === 'on' && uploadingStudentId && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<FileUpload
|
|
||||||
selectionMessage="Sélectionnez un fichier à uploader"
|
|
||||||
onFileSelect={handleFileUpload}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className={`mt-4 px-6 py-2 rounded-md ${
|
|
||||||
uploadedFile
|
|
||||||
? 'bg-emerald-500 text-white hover:bg-emerald-600'
|
|
||||||
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
|
||||||
}`}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={!uploadedFile}
|
|
||||||
>
|
|
||||||
Valider
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -37,10 +37,10 @@ export const fetchEstablishmentCompetencies = (establishment, cycle = 1) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchSpecialities = (establishment) => {
|
export const fetchSpecialities = (establishment, schoolYear = null) => {
|
||||||
return fetchWithAuth(
|
let url = `${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`;
|
||||||
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
|
if (schoolYear) url += `&school_year=${schoolYear}`;
|
||||||
);
|
return fetchWithAuth(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTeachers = (establishment) => {
|
export const fetchTeachers = (establishment) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user