fix: On n'historise plus les matières ni les enseignants

This commit is contained in:
N3WT DE COMPET
2026-04-05 11:41:52 +02:00
parent 12939fca85
commit f9c0585b30
5 changed files with 93 additions and 169 deletions

View File

@ -21,7 +21,6 @@ class Speciality(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
color_code = models.CharField(max_length=7, default='#FFFFFF') color_code = models.CharField(max_length=7, default='#FFFFFF')
school_year = models.CharField(max_length=9, blank=True)
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='specialities') establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='specialities')
def __str__(self): def __str__(self):
@ -32,7 +31,6 @@ class Teacher(models.Model):
first_name = models.CharField(max_length=100) first_name = models.CharField(max_length=100)
specialities = models.ManyToManyField(Speciality, blank=True) specialities = models.ManyToManyField(Speciality, blank=True)
profile_role = models.OneToOneField('Auth.ProfileRole', on_delete=models.CASCADE, related_name='teacher_profile', null=True, blank=True) profile_role = models.OneToOneField('Auth.ProfileRole', on_delete=models.CASCADE, related_name='teacher_profile', null=True, blank=True)
school_year = models.CharField(max_length=9, blank=True)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):

View File

@ -73,15 +73,12 @@ 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)

View File

@ -1,14 +1,12 @@
import { Trash2, Edit3, Check, X, BookOpen, Download } from 'lucide-react'; import { Trash2, Edit3, Check, X, BookOpen, Download } from 'lucide-react';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import InputTextWithColorIcon from '@/components/Form/InputTextWithColorIcon'; import InputTextWithColorIcon from '@/components/Form/InputTextWithColorIcon';
import SelectChoice from '@/components/Form/SelectChoice';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'; import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { useSchoolYearFilter } from '@/context/SchoolYearFilterContext';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import AlertMessage from '@/components/AlertMessage'; import AlertMessage from '@/components/AlertMessage';
@ -34,47 +32,39 @@ const SpecialitiesSection = ({
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const ITEMS_PER_PAGE = 10; const ITEMS_PER_PAGE = 10;
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
useEffect(() => { setCurrentPage(1); }, [specialities]);
useEffect(() => { if (newSpeciality) setCurrentPage(1); }, [newSpeciality]); useEffect(() => {
setCurrentPage(1);
}, [specialities]);
useEffect(() => {
if (newSpeciality) setCurrentPage(1);
}, [newSpeciality]);
const totalPages = Math.ceil(specialities.length / ITEMS_PER_PAGE); const totalPages = Math.ceil(specialities.length / ITEMS_PER_PAGE);
const pagedSpecialities = specialities.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE); const pagedSpecialities = specialities.slice(
(currentPage - 1) * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE
);
const { selectedEstablishmentId, profileRole } = useEstablishment(); const { selectedEstablishmentId, profileRole } = useEstablishment();
const { selectedSchoolYear } = useSchoolYearFilter();
// Fonction pour générer les années scolaires
const getSchoolYearChoices = () => {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth() + 1;
const startYear = currentMonth >= 9 ? currentYear : currentYear - 1;
const choices = [];
for (let i = 0; i < 3; i++) {
const year = startYear + i;
choices.push({ value: `${year}-${year + 1}`, label: `${year}-${year + 1}` });
}
return choices;
};
// Récupération des messages d'erreur
const getError = (field) => { const getError = (field) => {
return localErrors?.[field]?.[0]; return localErrors?.[field]?.[0];
}; };
const handleAddSpeciality = () => { const handleAddSpeciality = () => {
setNewSpeciality({ id: Date.now(), name: '', color_code: '', school_year: selectedSchoolYear }); setNewSpeciality({ id: Date.now(), name: '', color_code: '' });
}; };
// Export CSV
const handleExportCSV = () => { const handleExportCSV = () => {
const exportColumns = [ const exportColumns = [
{ key: 'id', label: 'ID' }, { key: 'id', label: 'ID' },
{ key: 'name', label: 'Nom' }, { key: 'name', label: 'Nom' },
{ key: 'color_code', label: 'Code couleur' }, { key: 'color_code', label: 'Code couleur' },
{ key: 'school_year', label: 'Année scolaire' },
{ key: 'updated_date_formatted', label: 'Date de mise à jour' }, { key: 'updated_date_formatted', label: 'Date de mise à jour' },
]; ];
const filename = `specialites_${selectedSchoolYear || 'toutes'}_${new Date().toISOString().split('T')[0]}`; const filename = `specialites_${new Date().toISOString().split('T')[0]}`;
exportToCSV(specialities, exportColumns, filename); exportToCSV(specialities, exportColumns, filename);
}; };
@ -92,10 +82,9 @@ const SpecialitiesSection = ({
const handleSaveNewSpeciality = () => { const handleSaveNewSpeciality = () => {
if (newSpeciality.name) { if (newSpeciality.name) {
// Ajouter l'ID de l'établissement à la nouvelle spécialité
const specialityData = { const specialityData = {
...newSpeciality, ...newSpeciality,
establishment: selectedEstablishmentId, // Inclure l'ID de l'établissement establishment: selectedEstablishmentId,
}; };
handleCreate(specialityData) handleCreate(specialityData)
@ -120,10 +109,10 @@ const SpecialitiesSection = ({
const handleUpdateSpeciality = (id, updatedSpeciality) => { const handleUpdateSpeciality = (id, updatedSpeciality) => {
if (updatedSpeciality.name) { if (updatedSpeciality.name) {
handleEdit(id, updatedSpeciality) handleEdit(id, updatedSpeciality)
.then((updatedSpeciality) => { .then((updatedItem) => {
setSpecialities( setSpecialities(
specialities.map((speciality) => specialities.map((speciality) =>
speciality.id === id ? updatedSpeciality : speciality speciality.id === id ? updatedItem : speciality
) )
); );
setEditingSpeciality(null); setEditingSpeciality(null);
@ -144,21 +133,17 @@ const SpecialitiesSection = ({
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
let parsedValue = value;
if (name.includes('_color')) {
parsedValue = value;
}
const fieldName = name.includes('_color') ? 'color_code' : name; const fieldName = name.includes('_color') ? 'color_code' : name;
if (editingSpeciality) { if (editingSpeciality) {
setFormData((prevData) => ({ setFormData((prevData) => ({
...prevData, ...prevData,
[fieldName]: parsedValue, [fieldName]: value,
})); }));
} else if (newSpeciality) { } else if (newSpeciality) {
setNewSpeciality((prevData) => ({ setNewSpeciality((prevData) => ({
...prevData, ...prevData,
[fieldName]: parsedValue, [fieldName]: value,
})); }));
} }
}; };
@ -182,20 +167,6 @@ const SpecialitiesSection = ({
errorMsg={getError('name')} errorMsg={getError('name')}
/> />
); );
case 'ANNÉE SCOLAIRE':
return (
<SelectChoice
type="select"
name="school_year"
placeHolder="Sélectionnez une année scolaire"
choices={getSchoolYearChoices()}
callback={handleChange}
selected={currentData.school_year || ''}
errorMsg={getError('school_year')}
IconItem={null}
disabled={false}
/>
);
case 'ACTIONS': case 'ACTIONS':
return ( return (
<div className="flex justify-center space-x-2"> <div className="flex justify-center space-x-2">
@ -226,73 +197,70 @@ const SpecialitiesSection = ({
default: default:
return null; return null;
} }
} else { }
switch (column) {
case 'LIBELLE': switch (column) {
return <SpecialityItem key={speciality.id} speciality={speciality} />; case 'LIBELLE':
case 'ANNÉE SCOLAIRE': return <SpecialityItem key={speciality.id} speciality={speciality} />;
return speciality.school_year; case 'MISE A JOUR':
case 'MISE A JOUR': return speciality.updated_date_formatted;
return speciality.updated_date_formatted; case 'ACTIONS':
case 'ACTIONS': return (
return ( <div className="flex justify-center space-x-2">
<div className="flex justify-center space-x-2"> <button
<button type="button"
type="button" onClick={() =>
onClick={() => setEditingSpeciality(speciality.id) || setFormData(speciality)
setEditingSpeciality(speciality.id) || setFormData(speciality) }
} className="text-blue-500 hover:text-blue-700"
className="text-blue-500 hover:text-blue-700" >
> <Edit3 className="w-5 h-5" />
<Edit3 className="w-5 h-5" /> </button>
</button> <button
<button type="button"
type="button" onClick={() => {
onClick={() => { setRemovePopupVisible(true);
setRemovePopupVisible(true); setRemovePopupMessage(
setRemovePopupMessage( 'Attention ! \nVous êtes sur le point de supprimer la spécialité ' +
'Attention ! \nVous êtes sur le point de supprimer la spécialité ' + speciality.name +
speciality.name + ".\nÊtes-vous sûr(e) de vouloir poursuivre l\'opération ?"
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?" );
); setRemovePopupOnConfirm(() => () => {
setRemovePopupOnConfirm(() => () => { handleRemoveSpeciality(speciality.id)
handleRemoveSpeciality(speciality.id) .then((data) => {
.then((data) => { logger.debug('Success:', data);
logger.debug('Success:', data); setPopupMessage(
setPopupMessage( 'La spécialité ' +
'La spécialité ' + speciality.name +
speciality.name + ' a été correctement supprimée'
' a été correctement supprimée' );
); setPopupVisible(true);
setPopupVisible(true); setRemovePopupVisible(false);
setRemovePopupVisible(false); })
}) .catch((error) => {
.catch((error) => { logger.error('Error archiving data:', error);
logger.error('Error archiving data:', error); setPopupMessage(
setPopupMessage( 'Erreur lors de la suppression de la spécialité ' +
'Erreur lors de la suppression de la spécialité ' + speciality.name
speciality.name );
); setPopupVisible(true);
setPopupVisible(true); setRemovePopupVisible(false);
setRemovePopupVisible(false); });
}); });
}); }}
}} className="text-red-500 hover:text-red-700"
className="text-red-500 hover:text-red-700" >
> <Trash2 className="w-5 h-5" />
<Trash2 className="w-5 h-5" /> </button>
</button> </div>
</div> );
); default:
default: return null;
return null;
}
} }
}; };
const columns = [ const columns = [
{ name: 'LIBELLE', label: 'Libellé' }, { name: 'LIBELLE', label: 'Libellé' },
{ name: 'ANNÉE SCOLAIRE', label: 'Année scolaire' },
{ name: 'MISE A JOUR', label: 'Date mise à jour' }, { name: 'MISE A JOUR', label: 'Date mise à jour' },
...(profileRole !== 0 ? [{ name: 'ACTIONS', label: 'Actions' }] : []), ...(profileRole !== 0 ? [{ name: 'ACTIONS', label: 'Actions' }] : []),
]; ];

View File

@ -25,21 +25,13 @@ const StructureManagementContent = ({
}) => { }) => {
const { activeYearFilter, setActiveYearFilter, filterByYear } = useSchoolYearFilter(); const { activeYearFilter, setActiveYearFilter, filterByYear } = useSchoolYearFilter();
// Filtrer les données par année scolaire // Les spécialités et enseignants ne sont plus historisés par année scolaire.
const filteredSpecialities = useMemo(() => filterByYear(specialities), [specialities, filterByYear]); const filteredSpecialities = specialities;
const filteredTeachers = useMemo(() => filterByYear(teachers), [teachers, filterByYear]); const filteredTeachers = teachers;
const filteredClasses = useMemo(() => filterByYear(classes), [classes, filterByYear]); const filteredClasses = useMemo(() => filterByYear(classes), [classes, filterByYear]);
return ( return (
<> <>
<div className="mb-6">
<SchoolYearFilter
activeFilter={activeYearFilter}
onFilterChange={setActiveYearFilter}
showNextYear={true}
showHistorical={true}
/>
</div>
{/* Spécialités + Enseignants : côte à côte sur desktop, empilés sur mobile */} {/* Spécialités + Enseignants : côte à côte sur desktop, empilés sur mobile */}
<div className="mt-8 flex flex-col xl:flex-row gap-8"> <div className="mt-8 flex flex-col xl:flex-row gap-8">
<div className="w-full xl:w-2/5"> <div className="w-full xl:w-2/5">
@ -92,6 +84,14 @@ const StructureManagementContent = ({
</div> </div>
</div> </div>
<div className="w-full mt-8 xl:mt-12"> <div className="w-full mt-8 xl:mt-12">
<div className="mb-6">
<SchoolYearFilter
activeFilter={activeYearFilter}
onFilterChange={setActiveYearFilter}
showNextYear={true}
showHistorical={true}
/>
</div>
<ClassesSection <ClassesSection
classes={filteredClasses} classes={filteredClasses}
allClasses={classes} allClasses={classes}

View File

@ -11,7 +11,6 @@ import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'
import TeacherItem from './TeacherItem'; import TeacherItem from './TeacherItem';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { useSchoolYearFilter } from '@/context/SchoolYearFilterContext';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import AlertMessage from '@/components/AlertMessage'; import AlertMessage from '@/components/AlertMessage';
import { exportToCSV } from '@/utils/exportCSV'; import { exportToCSV } from '@/utils/exportCSV';
@ -149,22 +148,6 @@ const TeachersSection = ({
const pagedTeachers = teachers.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE); const pagedTeachers = teachers.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
const { selectedEstablishmentId, profileRole } = useEstablishment(); const { selectedEstablishmentId, profileRole } = useEstablishment();
const { selectedSchoolYear } = useSchoolYearFilter();
// Génère les choix d'année scolaire (année en cours + 2 suivantes)
const getSchoolYearChoices = () => {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth() + 1;
// Si on est avant septembre, l'année scolaire a commencé l'année précédente
const startYear = currentMonth >= 9 ? currentYear : currentYear - 1;
const choices = [];
for (let i = 0; i < 3; i++) {
const year = startYear + i;
choices.push({ value: `${year}-${year + 1}`, label: `${year}-${year + 1}` });
}
return choices;
};
// --- UTILS --- // --- UTILS ---
@ -217,7 +200,6 @@ const TeachersSection = ({
associated_profile_email: '', associated_profile_email: '',
specialities: [], specialities: [],
role_type: 0, role_type: 0,
school_year: selectedSchoolYear,
}); });
setFormData({ setFormData({
last_name: '', last_name: '',
@ -225,7 +207,6 @@ const TeachersSection = ({
associated_profile_email: '', associated_profile_email: '',
specialities: [], specialities: [],
role_type: 0, role_type: 0,
school_year: selectedSchoolYear,
}); });
}; };
@ -246,10 +227,9 @@ const TeachersSection = ({
label: 'Profil', label: 'Profil',
transform: (value) => value === 1 ? 'Administrateur' : 'Enseignant' transform: (value) => value === 1 ? 'Administrateur' : 'Enseignant'
}, },
{ key: 'school_year', label: 'Année scolaire' },
{ key: 'updated_date_formatted', label: 'Date de mise à jour' }, { key: 'updated_date_formatted', label: 'Date de mise à jour' },
]; ];
const filename = `enseignants_${selectedSchoolYear || 'toutes'}_${new Date().toISOString().split('T')[0]}`; const filename = `enseignants_${new Date().toISOString().split('T')[0]}`;
exportToCSV(teachers, exportColumns, filename); exportToCSV(teachers, exportColumns, filename);
}; };
@ -288,7 +268,6 @@ const TeachersSection = ({
}, },
}), }),
}, },
school_year: formData.school_year || selectedSchoolYear,
specialities: formData.specialities || [], specialities: formData.specialities || [],
}; };
@ -340,7 +319,6 @@ const TeachersSection = ({
handleEdit(id, { handleEdit(id, {
last_name: updatedData.last_name, last_name: updatedData.last_name,
first_name: updatedData.first_name, first_name: updatedData.first_name,
school_year: updatedData.school_year || selectedSchoolYear,
profile_role_data: profileRoleData, profile_role_data: profileRoleData,
specialities: updatedData.specialities || [], specialities: updatedData.specialities || [],
}) })
@ -439,20 +417,6 @@ const TeachersSection = ({
/> />
</div> </div>
); );
case 'ANNÉE SCOLAIRE':
return (
<SelectChoice
type="select"
name="school_year"
placeHolder="Sélectionnez une année scolaire"
choices={getSchoolYearChoices()}
callback={handleChange}
selected={currentData.school_year || ''}
errorMsg={getError('school_year')}
IconItem={null}
disabled={false}
/>
);
case 'ACTIONS': case 'ACTIONS':
return ( return (
<div className="flex justify-center space-x-2"> <div className="flex justify-center space-x-2">
@ -521,8 +485,6 @@ const TeachersSection = ({
} else { } else {
return <i>Non définie</i>; return <i>Non définie</i>;
} }
case 'ANNÉE SCOLAIRE':
return teacher.school_year;
case 'MISE A JOUR': case 'MISE A JOUR':
return teacher.updated_date_formatted; return teacher.updated_date_formatted;
case 'ACTIONS': case 'ACTIONS':
@ -590,7 +552,6 @@ const TeachersSection = ({
{ name: 'EMAIL', label: 'Email' }, { name: 'EMAIL', label: 'Email' },
{ name: 'SPECIALITES', label: 'Spécialités' }, { name: 'SPECIALITES', label: 'Spécialités' },
{ name: 'ADMINISTRATEUR', label: 'Profil' }, { name: 'ADMINISTRATEUR', label: 'Profil' },
{ name: 'ANNÉE SCOLAIRE', label: 'Année scolaire' },
{ name: 'MISE A JOUR', label: 'Mise à jour' }, { name: 'MISE A JOUR', label: 'Mise à jour' },
...(profileRole !== 0 ? [{ name: 'ACTIONS', label: 'Actions' }] : []), ...(profileRole !== 0 ? [{ name: 'ACTIONS', label: 'Actions' }] : []),
]; ];