diff --git a/Front-End/messages/fr/dashboard.json b/Front-End/messages/fr/dashboard.json index b85a89a..c3b5a17 100644 --- a/Front-End/messages/fr/dashboard.json +++ b/Front-End/messages/fr/dashboard.json @@ -1,6 +1,6 @@ { "dashboard": "Tableau de bord", - "totalStudents": "Total des étudiants", + "totalStudents": "Total d'étudiants inscrits", "pendingRegistrations": "Inscriptions en attente", "reInscriptionRate": "Taux de réinscription", "structureCapacity": "Capacité de la structure", diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index c2bf5ba..c4c31bf 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -1,10 +1,8 @@ 'use client'; import React, { useState, useEffect } from 'react'; import { useTranslations } from 'next-intl'; -import { Users, Clock, CalendarCheck, School, TrendingUp } from 'lucide-react'; +import { Users, Clock, CalendarCheck, School } from 'lucide-react'; import Loader from '@/components/Loader'; -import ClasseDetails from '@/components/ClasseDetails'; -import { fetchClasses } from '@/app/actions/schoolAction'; import StatCard from '@/components/StatCard'; import logger from '@/utils/logger'; import { @@ -13,8 +11,9 @@ import { } from '@/app/actions/subscriptionAction'; import { fetchUpcomingEvents } from '@/app/actions/planningAction'; import { useEstablishment } from '@/context/EstablishmentContext'; -import { useNotification } from '@/context/NotificationContext'; import Attendance from '@/components/Grades/Attendance'; +import LineChart from '@/components/Charts/LineChart'; +import PieChart from '@/components/Charts/PieChart'; // Composant EventCard pour afficher les événements const EventCard = ({ title, date, description, type }) => ( @@ -30,60 +29,95 @@ const EventCard = ({ title, date, description, type }) => ( ); +const mockCompletionRate = 72; // en pourcentage + export default function DashboardPage() { const t = useTranslations('dashboard'); const [isLoading, setIsLoading] = useState(false); - const [totalStudents, setTotalStudents] = useState(0); - const [pendingRegistration, setPendingRegistration] = useState(0); - const [structureCapacity, setStructureCapacity] = useState(0); + const [currentYearRegistrationCount, setCurrentYearRegistrationCount] = + useState(0); const [upcomingEvents, setUpcomingEvents] = useState([]); - const [monthlyStats, setMonthlyStats] = useState({ - inscriptions: [], - completionRate: 0, - }); - const [classes, setClasses] = useState([]); const [absencesToday, setAbsencesToday] = useState([]); const { selectedEstablishmentId, selectedEstablishmentTotalCapacity } = useEstablishment(); - const { showNotification } = useNotification(); + + const [statusDistribution, setStatusDistribution] = useState([ + { label: 'Non envoyé', value: 0 }, + { label: 'En attente', value: 0 }, + { label: 'En validation', value: 0 }, + { label: 'Validé', value: 0 }, + ]); + const [monthlyRegistrations, setMonthlyRegistrations] = useState([]); useEffect(() => { if (!selectedEstablishmentId) return; setIsLoading(true); // Début du chargement - // Fetch des classes - fetchClasses(selectedEstablishmentId) - .then((data) => { - setClasses(data); - logger.info('Classes fetched:', data); - - const nbMaxStudents = data.reduce( - (acc, classe) => acc + classe.number_of_students, - 0 - ); - const nbStudents = data.reduce( - (acc, classe) => acc + classe.students.length, - 0 - ); - setStructureCapacity(nbMaxStudents); - setTotalStudents(nbStudents); - }) - .catch((error) => { - logger.error('Error fetching classes:', error); - showNotification( - 'Error fetching classes: ' + error.message, - 'error', - 'Erreur' - ); - }); - // Fetch des formulaires d'inscription fetchRegisterForms(selectedEstablishmentId) .then((data) => { logger.info('Pending registrations fetched:', data); - setPendingRegistration(data.count); + setCurrentYearRegistrationCount(data.count); + const forms = data.registerForms; + + // Filtrage des statuts + const distribution = [ + { + label: 'Non envoyé', + value: forms.filter((f) => f.status === 1).length, + }, + { + label: 'En attente', + value: forms.filter((f) => f.status === 2 || f.status === 7).length, + }, + { + label: 'En validation', + value: forms.filter((f) => f.status === 3 || f.status === 8).length, + }, + { + label: 'Validé', + value: forms.filter((f) => f.status === 5).length, + }, + ]; + setStatusDistribution(distribution); + + // Calcul des inscriptions validées par mois + const validForms = forms.filter( + (f) => f.status === 5 && f.formatted_last_update + ); + // Format attendu : "29-05-2025 09:23" + const monthLabels = [ + 'Janv', + 'Fév', + 'Mars', + 'Avr', + 'Mai', + 'Juin', + 'Juil', + 'Août', + 'Sept', + 'Oct', + 'Nov', + 'Déc', + ]; + const monthlyCount = Array(12).fill(0); + + validForms.forEach((f) => { + const [day, month, yearAndTime] = f.formatted_last_update.split('-'); + const monthIdx = parseInt(month, 10) - 1; + if (monthIdx >= 0 && monthIdx < 12) { + monthlyCount[monthIdx]++; + } + }); + + const monthlyData = monthLabels.map((label, idx) => ({ + month: label, + value: monthlyCount[idx], + })); + + setMonthlyRegistrations(monthlyData); }) .catch((error) => { logger.error('Error fetching pending registrations:', error); @@ -123,6 +157,13 @@ export default function DashboardPage() { }); }, [selectedEstablishmentId]); + // Calculs à partir de statusDistribution + const totalStudents = + statusDistribution.find((s) => s.label === 'Validé')?.value || 0; + const pendingRegistrationCount = statusDistribution + .filter((s) => s.label !== 'Validé') + .reduce((acc, s) => acc + s.value, 0); + if (isLoading) return ; return ( @@ -138,13 +179,13 @@ export default function DashboardPage() { /> } color="green" /> } color="emerald" /> @@ -152,7 +193,7 @@ export default function DashboardPage() { title={t('capacityRate')} value={ selectedEstablishmentTotalCapacity > 0 - ? `${((totalStudents / selectedEstablishmentTotalCapacity) * 100).toFixed(1)}%` + ? `${((totalStudents / selectedEstablishmentTotalCapacity) * 100).toFixed(2)}%` : 0 } icon={} @@ -161,15 +202,19 @@ export default function DashboardPage() { {/* Événements et KPIs */} -
+
{/* Graphique des inscriptions */} -
-

+
+

{t('inscriptionTrends')}

- {/* Insérer ici un composant de graphique */} -
- +
+
+ +
+
+ +
@@ -183,7 +228,9 @@ export default function DashboardPage() {
{/* Ajout du composant Attendance en dessous, en lecture seule */} - +
+ +

); } diff --git a/Front-End/src/components/Charts/LineChart.js b/Front-End/src/components/Charts/LineChart.js new file mode 100644 index 0000000..9bbbf4b --- /dev/null +++ b/Front-End/src/components/Charts/LineChart.js @@ -0,0 +1,45 @@ +import React from 'react'; + +export default function LineChart({ data }) { + if (!data || data.length === 0) { + return
Aucune donnée
; + } + + // Hauteur max du graphique en pixels + const chartHeight = 120; + const maxValue = Math.max(...data.map((d) => d.value), 1); + + // Trouver les indices des barres ayant la valeur max (pour gérer les égalités) + const maxIndices = data + .map((d, idx) => (d.value === maxValue ? idx : -1)) + .filter((idx) => idx !== -1); + + return ( +
+ {data.map((point, idx) => { + const barHeight = Math.max((point.value / maxValue) * chartHeight, 8); // min 8px + const isMax = maxIndices.includes(idx); + return ( +
+ {/* Valeur au-dessus de la barre */} + + {point.value} + +
+ {point.month} +
+ ); + })} +
+ ); +} diff --git a/Front-End/src/components/Charts/PieChart.js b/Front-End/src/components/Charts/PieChart.js new file mode 100644 index 0000000..d144e1e --- /dev/null +++ b/Front-End/src/components/Charts/PieChart.js @@ -0,0 +1,67 @@ +import React from 'react'; + +const COLORS = [ + 'fill-blue-400 text-blue-400', + 'fill-orange-400 text-orange-400', + 'fill-purple-400 text-purple-400', + 'fill-emerald-400 text-emerald-400', +]; + +export default function PieChart({ data }) { + if (!data || data.length === 0) { + return
Aucune donnée
; + } + + const total = data.reduce((acc, d) => acc + d.value, 0); + if (total === 0) { + return
Aucune donnée
; + } + + let cumulative = 0; + + return ( +
+ + {data.map((slice, idx) => { + const value = (slice.value / total) * 100; + const startAngle = (cumulative / 100) * 360; + const endAngle = ((cumulative + value) / 100) * 360; + const largeArc = value > 50 ? 1 : 0; + const x1 = 16 + 16 * Math.cos((Math.PI * (startAngle - 90)) / 180); + const y1 = 16 + 16 * Math.sin((Math.PI * (startAngle - 90)) / 180); + const x2 = 16 + 16 * Math.cos((Math.PI * (endAngle - 90)) / 180); + const y2 = 16 + 16 * Math.sin((Math.PI * (endAngle - 90)) / 180); + const pathData = ` + M16,16 + L${x1},${y1} + A16,16 0 ${largeArc} 1 ${x2},${y2} + Z + `; + cumulative += value; + return ( + + ); + })} + +
+ {data.map((slice, idx) => ( +
+ + {slice.label} : {slice.value} +
+ ))} +
+
+ ); +}