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):
|
||||
establishment_id = request.GET.get('establishment_id', None)
|
||||
school_year = request.GET.get('school_year', None)
|
||||
if establishment_id is None:
|
||||
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
specialities_list = getAllObjects(Speciality)
|
||||
if establishment_id:
|
||||
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)
|
||||
return JsonResponse(specialities_serializer.data, safe=False)
|
||||
|
||||
|
||||
@ -399,10 +399,11 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
|
||||
class StudentByParentSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(required=False)
|
||||
associated_class_name = serializers.SerializerMethodField()
|
||||
associated_class_id = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
super(StudentByParentSerializer, self).__init__(*args, **kwargs)
|
||||
@ -412,6 +413,9 @@ class StudentByParentSerializer(serializers.ModelSerializer):
|
||||
def get_associated_class_name(self, obj):
|
||||
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):
|
||||
student = StudentByParentSerializer(many=False, required=True)
|
||||
|
||||
|
||||
@ -205,10 +205,12 @@ export default function Page() {
|
||||
}
|
||||
}, [filteredStudents, fetchedAbsences]);
|
||||
|
||||
// Load specialities for evaluations
|
||||
// Load specialities for evaluations (filtered by current school year)
|
||||
useEffect(() => {
|
||||
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))
|
||||
.catch((error) => logger.error('Erreur lors du chargement des matières:', error));
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Table from '@/components/Table';
|
||||
import {
|
||||
Edit3,
|
||||
Users,
|
||||
@ -9,6 +8,12 @@ import {
|
||||
Eye,
|
||||
Upload,
|
||||
CalendarDays,
|
||||
Award,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
BookOpen,
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import FileUpload from '@/components/Form/FileUpload';
|
||||
@ -16,7 +21,13 @@ import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
|
||||
import {
|
||||
fetchChildren,
|
||||
editRegisterForm,
|
||||
fetchStudentCompetencies,
|
||||
fetchAbsences,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import {
|
||||
fetchEvaluations,
|
||||
fetchStudentEvaluations,
|
||||
} from '@/app/actions/schoolAction';
|
||||
import { fetchUpcomingEvents } from '@/app/actions/planningAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
@ -27,13 +38,47 @@ import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import ParentPlanningSection from '@/components/ParentPlanningSection';
|
||||
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() {
|
||||
const [children, setChildren] = useState([]);
|
||||
const { user, selectedEstablishmentId } = useEstablishment();
|
||||
const [uploadingStudentId, setUploadingStudentId] = useState(null); // ID de l'étudiant pour l'upload
|
||||
const [uploadedFile, setUploadedFile] = useState(null); // Fichier uploadé
|
||||
const [uploadState, setUploadState] = useState('off'); // État "on" ou "off" pour l'affichage du composant
|
||||
const { user, selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = useEstablishment();
|
||||
const [uploadingStudentId, setUploadingStudentId] = useState(null);
|
||||
const [uploadedFile, setUploadedFile] = useState(null);
|
||||
const [uploadState, setUploadState] = useState('off');
|
||||
const [showPlanning, setShowPlanning] = useState(false);
|
||||
const [planningClassName, setPlanningClassName] = useState(null);
|
||||
const [upcomingEvents, setUpcomingEvents] = useState([]);
|
||||
@ -42,16 +87,114 @@ export default function ParentHomePage() {
|
||||
const [reloadFetch, setReloadFetch] = useState(false);
|
||||
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(() => {
|
||||
if (user !== null) {
|
||||
const userIdFromSession = user.user_id;
|
||||
fetchChildren(userIdFromSession, selectedEstablishmentId).then((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);
|
||||
}
|
||||
}, [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(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
// Fetch des événements à venir
|
||||
@ -132,153 +275,6 @@ export default function ParentHomePage() {
|
||||
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 (
|
||||
<div className="w-full h-full">
|
||||
{showPlanning && planningClassName ? (
|
||||
@ -326,12 +322,163 @@ export default function ParentHomePage() {
|
||||
title="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>
|
||||
{/* Composant FileUpload et bouton Valider en dessous du tableau */}
|
||||
{uploadState === 'on' && uploadingStudentId && (
|
||||
<div className="mt-4">
|
||||
)}
|
||||
</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}
|
||||
@ -339,7 +486,7 @@ export default function ParentHomePage() {
|
||||
<button
|
||||
className={`mt-4 px-6 py-2 rounded-md ${
|
||||
uploadedFile
|
||||
? 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
? 'bg-primary text-white hover:bg-secondary'
|
||||
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||
}`}
|
||||
onClick={handleSubmit}
|
||||
@ -349,6 +496,201 @@ export default function ParentHomePage() {
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -37,10 +37,10 @@ export const fetchEstablishmentCompetencies = (establishment, cycle = 1) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchSpecialities = (establishment) => {
|
||||
return fetchWithAuth(
|
||||
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
|
||||
);
|
||||
export const fetchSpecialities = (establishment, schoolYear = null) => {
|
||||
let url = `${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`;
|
||||
if (schoolYear) url += `&school_year=${schoolYear}`;
|
||||
return fetchWithAuth(url);
|
||||
};
|
||||
|
||||
export const fetchTeachers = (establishment) => {
|
||||
|
||||
Reference in New Issue
Block a user