mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 20:51:26 +00:00
feat: Ajout d'un système d'historisation et d'export de données en CSV [N3WTS-5]
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Award, Eye, Search, BarChart2, X, Pencil, Trash2, Save } from 'lucide-react';
|
||||
import { Award, Eye, Search, BarChart2, X, Pencil, Trash2, Save, Download } from 'lucide-react';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import Table from '@/components/Table';
|
||||
import logger from '@/utils/logger';
|
||||
@ -19,14 +19,20 @@ import { fetchStudentEvaluations, updateStudentEvaluation, deleteStudentEvaluati
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { exportToCSV } from '@/utils/exportCSV';
|
||||
import SchoolYearFilter from '@/components/SchoolYearFilter';
|
||||
import { getCurrentSchoolYear, getNextSchoolYear, getHistoricalYears } from '@/utils/Date';
|
||||
import { CURRENT_YEAR_FILTER, NEXT_YEAR_FILTER, HISTORICAL_FILTER } from '@/utils/constants';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
function getPeriodString(periodValue, frequency) {
|
||||
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
||||
const schoolYear = `${year}-${year + 1}`;
|
||||
if (frequency === 1) return `T${periodValue}_${schoolYear}`;
|
||||
if (frequency === 2) return `S${periodValue}_${schoolYear}`;
|
||||
if (frequency === 3) return `A_${schoolYear}`;
|
||||
function getPeriodString(periodValue, frequency, schoolYear = null) {
|
||||
const year = schoolYear || (() => {
|
||||
const y = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
||||
return `${y}-${y + 1}`;
|
||||
})();
|
||||
if (frequency === 1) return `T${periodValue}_${year}`;
|
||||
if (frequency === 2) return `S${periodValue}_${year}`;
|
||||
if (frequency === 3) return `A_${year}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -132,17 +138,33 @@ export default function Page() {
|
||||
const [editingEvalId, setEditingEvalId] = useState(null);
|
||||
const [editScore, setEditScore] = useState('');
|
||||
const [editAbsent, setEditAbsent] = useState(false);
|
||||
|
||||
// Filtrage par année scolaire
|
||||
const [activeYearFilter, setActiveYearFilter] = useState(CURRENT_YEAR_FILTER);
|
||||
const currentSchoolYear = useMemo(() => getCurrentSchoolYear(), []);
|
||||
const nextSchoolYear = useMemo(() => getNextSchoolYear(), []);
|
||||
const historicalYears = useMemo(() => getHistoricalYears(5), []);
|
||||
|
||||
// Déterminer l'année scolaire sélectionnée
|
||||
const selectedSchoolYear = useMemo(() => {
|
||||
if (activeYearFilter === CURRENT_YEAR_FILTER) return currentSchoolYear;
|
||||
if (activeYearFilter === NEXT_YEAR_FILTER) return nextSchoolYear;
|
||||
// Pour l'historique, on utilise la première année historique par défaut
|
||||
// L'utilisateur pourra choisir une année spécifique si nécessaire
|
||||
return historicalYears[0];
|
||||
}, [activeYearFilter, currentSchoolYear, nextSchoolYear, historicalYears]);
|
||||
|
||||
const periodColumns = getPeriodColumns(
|
||||
const periodColumns = useMemo(() => getPeriodColumns(
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
), [selectedEstablishmentEvaluationFrequency]);
|
||||
|
||||
const currentPeriodValue = getCurrentPeriodValue(
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedEstablishmentId) return;
|
||||
fetchStudents(selectedEstablishmentId, null, 5)
|
||||
fetchStudents(selectedEstablishmentId, null, 5, selectedSchoolYear)
|
||||
.then((data) => setStudents(data))
|
||||
.catch((error) => logger.error('Error fetching students:', error));
|
||||
|
||||
@ -157,7 +179,7 @@ export default function Page() {
|
||||
setAbsencesMap(map);
|
||||
})
|
||||
.catch((error) => logger.error('Error fetching absences:', error));
|
||||
}, [selectedEstablishmentId]);
|
||||
}, [selectedEstablishmentId, selectedSchoolYear]);
|
||||
|
||||
// Fetch stats for all students - aggregate all periods
|
||||
useEffect(() => {
|
||||
@ -168,7 +190,7 @@ export default function Page() {
|
||||
|
||||
const tasks = students.flatMap((student) =>
|
||||
periodColumns.map(({ value: periodValue }) => {
|
||||
const periodStr = getPeriodString(periodValue, frequency);
|
||||
const periodStr = getPeriodString(periodValue, frequency, selectedSchoolYear);
|
||||
return fetchStudentCompetencies(student.id, periodStr)
|
||||
.then((data) => ({ studentId: student.id, periodValue, data }))
|
||||
.catch(() => ({ studentId: student.id, periodValue, data: null }));
|
||||
@ -207,7 +229,7 @@ export default function Page() {
|
||||
setStatsMap(map);
|
||||
setStatsLoading(false);
|
||||
});
|
||||
}, [students, selectedEstablishmentEvaluationFrequency]);
|
||||
}, [students, selectedEstablishmentEvaluationFrequency, selectedSchoolYear, periodColumns]);
|
||||
|
||||
const filteredStudents = students.filter(
|
||||
(student) =>
|
||||
@ -249,6 +271,38 @@ export default function Page() {
|
||||
setEditingEvalId(null);
|
||||
};
|
||||
|
||||
// Export CSV
|
||||
const handleExportCSV = () => {
|
||||
const exportColumns = [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'last_name', label: 'Nom' },
|
||||
{ key: 'first_name', label: 'Prénom' },
|
||||
{ key: 'birth_date', label: 'Date de naissance' },
|
||||
{ key: 'level', label: 'Niveau', transform: (value) => getNiveauLabel(value) },
|
||||
{ key: 'associated_class_name', label: 'Classe' },
|
||||
{
|
||||
key: 'id',
|
||||
label: 'Absences',
|
||||
transform: (value) => absencesMap[value] || 0
|
||||
},
|
||||
];
|
||||
|
||||
// Ajouter les colonnes de compétences si les stats sont chargées
|
||||
COMPETENCY_COLUMNS.forEach(({ key, label }) => {
|
||||
exportColumns.push({
|
||||
key: 'id',
|
||||
label: label,
|
||||
transform: (value) => {
|
||||
const stats = statsMap[value];
|
||||
return stats?.[key] !== undefined ? `${stats[key]}%` : '';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const filename = `suivi_eleves_${selectedSchoolYear}_${new Date().toISOString().split('T')[0]}`;
|
||||
exportToCSV(filteredStudents, exportColumns, filename);
|
||||
};
|
||||
|
||||
const startEditingEval = (evalItem) => {
|
||||
setEditingEvalId(evalItem.id);
|
||||
setEditScore(evalItem.score ?? '');
|
||||
@ -306,7 +360,8 @@ export default function Page() {
|
||||
e.stopPropagation();
|
||||
const periodStr = getPeriodString(
|
||||
currentPeriodValue,
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
selectedEstablishmentEvaluationFrequency,
|
||||
selectedSchoolYear
|
||||
);
|
||||
router.push(
|
||||
`${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${studentId}&period=${periodStr}`
|
||||
@ -434,18 +489,34 @@ export default function Page() {
|
||||
title="Suivi pédagogique"
|
||||
description="Suivez le parcours d'un élève"
|
||||
/>
|
||||
<div className="relative flex-grow max-w-md">
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
||||
size={20}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher un élève"
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<SchoolYearFilter
|
||||
activeFilter={activeYearFilter}
|
||||
onFilterChange={setActiveYearFilter}
|
||||
showNextYear={true}
|
||||
showHistorical={true}
|
||||
/>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div className="relative flex-grow">
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
||||
size={20}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher un élève"
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-100 rounded-lg hover:bg-emerald-200 transition-colors ml-4"
|
||||
title="Exporter en CSV"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Exporter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
Upload,
|
||||
Eye,
|
||||
XCircle,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
@ -55,6 +56,7 @@ import {
|
||||
} from '@/utils/constants';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import { exportToCSV } from '@/utils/exportCSV';
|
||||
|
||||
export default function Page({ params: { locale } }) {
|
||||
const t = useTranslations('subscriptions');
|
||||
@ -149,6 +151,49 @@ export default function Page({ params: { locale } }) {
|
||||
setIsFilesModalOpen(true);
|
||||
};
|
||||
|
||||
// Export CSV
|
||||
const handleExportCSV = () => {
|
||||
const dataToExport = activeTab === CURRENT_YEAR_FILTER
|
||||
? registrationFormsDataCurrentYear
|
||||
: activeTab === NEXT_YEAR_FILTER
|
||||
? registrationFormsDataNextYear
|
||||
: registrationFormsDataHistorical;
|
||||
|
||||
const exportColumns = [
|
||||
{ key: 'student', label: 'Nom', transform: (value) => value?.last_name || '' },
|
||||
{ key: 'student', label: 'Prénom', transform: (value) => value?.first_name || '' },
|
||||
{ key: 'student', label: 'Date de naissance', transform: (value) => value?.birth_date || '' },
|
||||
{ key: 'student', label: 'Email contact', transform: (value) => value?.guardians?.[0]?.associated_profile_email || '' },
|
||||
{ key: 'student', label: 'Téléphone contact', transform: (value) => value?.guardians?.[0]?.phone || '' },
|
||||
{ key: 'student', label: 'Nom responsable 1', transform: (value) => value?.guardians?.[0]?.last_name || '' },
|
||||
{ key: 'student', label: 'Prénom responsable 1', transform: (value) => value?.guardians?.[0]?.first_name || '' },
|
||||
{ key: 'student', label: 'Nom responsable 2', transform: (value) => value?.guardians?.[1]?.last_name || '' },
|
||||
{ key: 'student', label: 'Prénom responsable 2', transform: (value) => value?.guardians?.[1]?.first_name || '' },
|
||||
{ key: 'school_year', label: 'Année scolaire' },
|
||||
{ key: 'status', label: 'Statut', transform: (value) => {
|
||||
const statusMap = {
|
||||
0: 'En attente',
|
||||
1: 'En cours',
|
||||
2: 'Envoyé',
|
||||
3: 'À relancer',
|
||||
4: 'À valider',
|
||||
5: 'Validé',
|
||||
6: 'Archivé',
|
||||
};
|
||||
return statusMap[value] || value;
|
||||
}},
|
||||
{ key: 'formatted_last_update', label: 'Dernière mise à jour' },
|
||||
];
|
||||
|
||||
const yearLabel = activeTab === CURRENT_YEAR_FILTER
|
||||
? currentSchoolYear
|
||||
: activeTab === NEXT_YEAR_FILTER
|
||||
? nextSchoolYear
|
||||
: 'historique';
|
||||
const filename = `inscriptions_${yearLabel}_${new Date().toISOString().split('T')[0]}`;
|
||||
exportToCSV(dataToExport, exportColumns, filename);
|
||||
};
|
||||
|
||||
const requestErrorHandler = (err) => {
|
||||
logger.error('Error fetching data:', err);
|
||||
};
|
||||
@ -839,17 +884,27 @@ export default function Page({ params: { locale } }) {
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</div>
|
||||
{profileRole !== 0 && (
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = `${FE_ADMIN_SUBSCRIPTIONS_CREATE_URL}`;
|
||||
router.push(url);
|
||||
}}
|
||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200 ml-4"
|
||||
onClick={handleExportCSV}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-100 rounded-lg hover:bg-emerald-200 transition-colors"
|
||||
title="Exporter en CSV"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
<Download className="w-4 h-4" />
|
||||
Exporter
|
||||
</button>
|
||||
)}
|
||||
{profileRole !== 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = `${FE_ADMIN_SUBSCRIPTIONS_CREATE_URL}`;
|
||||
router.push(url);
|
||||
}}
|
||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL,
|
||||
BE_SCHOOL_EVALUATIONS_URL,
|
||||
BE_SCHOOL_STUDENT_EVALUATIONS_URL,
|
||||
BE_SCHOOL_SCHOOL_YEARS_URL,
|
||||
} from '@/utils/Url';
|
||||
import { fetchWithAuth } from '@/utils/fetchWithAuth';
|
||||
|
||||
@ -46,10 +47,15 @@ export const fetchTeachers = (establishment) => {
|
||||
return fetchWithAuth(`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`);
|
||||
};
|
||||
|
||||
export const fetchClasses = (establishment) => {
|
||||
return fetchWithAuth(
|
||||
`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`
|
||||
);
|
||||
export const fetchClasses = (establishment, options = {}) => {
|
||||
let url = `${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`;
|
||||
if (options.schoolYear) url += `&school_year=${options.schoolYear}`;
|
||||
if (options.yearFilter) url += `&year_filter=${options.yearFilter}`;
|
||||
return fetchWithAuth(url);
|
||||
};
|
||||
|
||||
export const fetchSchoolYears = () => {
|
||||
return fetchWithAuth(BE_SCHOOL_SCHOOL_YEARS_URL);
|
||||
};
|
||||
|
||||
export const fetchClasse = (id) => {
|
||||
|
||||
@ -124,7 +124,7 @@ export const searchStudents = (establishmentId, query) => {
|
||||
return fetchWithAuth(url);
|
||||
};
|
||||
|
||||
export const fetchStudents = (establishment, id = null, status = null) => {
|
||||
export const fetchStudents = (establishment, id = null, status = null, schoolYear = null) => {
|
||||
let url;
|
||||
if (id) {
|
||||
url = `${BE_SUBSCRIPTION_STUDENTS_URL}/${id}`;
|
||||
@ -133,6 +133,9 @@ export const fetchStudents = (establishment, id = null, status = null) => {
|
||||
if (status) {
|
||||
url += `&status=${status}`;
|
||||
}
|
||||
if (schoolYear) {
|
||||
url += `&school_year=${encodeURIComponent(schoolYear)}`;
|
||||
}
|
||||
}
|
||||
return fetchWithAuth(url);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user