mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
567 lines
18 KiB
JavaScript
567 lines
18 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react';
|
|
import Table from '@/components/Table';
|
|
import Popup from '@/components/Popup';
|
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
|
import { useCsrfToken } from '@/context/CsrfContext';
|
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
import InputText from '@/components/Form/InputText';
|
|
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
|
import TeacherItem from './TeacherItem';
|
|
import logger from '@/utils/logger';
|
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
|
import SectionHeader from '@/components/SectionHeader';
|
|
import AlertMessage from '@/components/AlertMessage';
|
|
|
|
const ItemTypes = {
|
|
SPECIALITY: 'speciality',
|
|
};
|
|
|
|
const SpecialitiesDropZone = ({
|
|
teacher,
|
|
handleSpecialitiesChange,
|
|
specialities,
|
|
isEditing,
|
|
}) => {
|
|
const [localSpecialities, setLocalSpecialities] = useState(
|
|
teacher.specialities_details || []
|
|
);
|
|
|
|
useEffect(() => {}, [specialities]);
|
|
|
|
useEffect(() => {
|
|
setLocalSpecialities(teacher.specialities_details || []);
|
|
}, [teacher.specialities_details]);
|
|
|
|
useEffect(() => {
|
|
handleSpecialitiesChange(
|
|
localSpecialities.map((speciality) => speciality.id)
|
|
);
|
|
}, [localSpecialities]);
|
|
|
|
const [{ isOver, canDrop }, drop] = useDrop({
|
|
accept: ItemTypes.SPECIALITY,
|
|
drop: (item) => {
|
|
const specialityDetails = specialities.find(
|
|
(speciality) => speciality.id === item.id
|
|
);
|
|
const exists = localSpecialities.some(
|
|
(speciality) => speciality.id === item.id
|
|
);
|
|
if (!exists) {
|
|
setLocalSpecialities((prevSpecialities) => {
|
|
const updatedSpecialities = [
|
|
...prevSpecialities,
|
|
{
|
|
id: item.id,
|
|
name: specialityDetails.name,
|
|
color_code: specialityDetails.color_code,
|
|
},
|
|
];
|
|
return updatedSpecialities;
|
|
});
|
|
}
|
|
},
|
|
collect: (monitor) => ({
|
|
isOver: !!monitor.isOver(),
|
|
canDrop: !!monitor.canDrop(),
|
|
}),
|
|
canDrop: () => {
|
|
return isEditing;
|
|
},
|
|
});
|
|
|
|
const handleRemoveSpeciality = (id) => {
|
|
setLocalSpecialities((prevSpecialities) => {
|
|
const updatedSpecialities = prevSpecialities.filter(
|
|
(speciality) => speciality.id !== id
|
|
);
|
|
return updatedSpecialities;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={drop}
|
|
className={`p-2 rounded-md flex flex-col items-center ${isEditing ? 'border-2 border-dashed border-blue-500 bg-blue-50' : ''} ${
|
|
isOver && canDrop ? 'border-2 border-solid border-blue-300' : ''
|
|
}`}
|
|
>
|
|
{isEditing && (
|
|
<div className="mb-2 text-blue-500 font-semibold flex items-center space-x-2">
|
|
<Hand className="w-5 h-5" /> {/* Ajoutez l'icône Hand */}
|
|
<span>Déposez une spécialité ici</span>
|
|
</div>
|
|
)}
|
|
{localSpecialities.map((speciality, index) => (
|
|
<div
|
|
key={`${speciality.id}-${index}`}
|
|
className="flex items-center space-x-2 mb-2"
|
|
>
|
|
<SpecialityItem
|
|
key={speciality.id}
|
|
speciality={speciality}
|
|
isDraggable={false}
|
|
/>
|
|
{isEditing && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveSpeciality(speciality.id)}
|
|
className="text-red-500 hover:text-red-700"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TeachersSection = ({
|
|
teachers,
|
|
setTeachers,
|
|
specialities,
|
|
profiles,
|
|
handleCreate,
|
|
handleEdit,
|
|
handleDelete,
|
|
}) => {
|
|
const csrfToken = useCsrfToken();
|
|
const [editingTeacher, setEditingTeacher] = useState(null);
|
|
const [newTeacher, setNewTeacher] = useState(null);
|
|
const [formData, setFormData] = useState({});
|
|
const [localErrors, setLocalErrors] = useState({});
|
|
const [popupVisible, setPopupVisible] = useState(false);
|
|
const [popupMessage, setPopupMessage] = useState('');
|
|
|
|
const [removePopupVisible, setRemovePopupVisible] = useState(false);
|
|
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
|
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
|
|
|
const { selectedEstablishmentId } = useEstablishment();
|
|
|
|
// --- UTILS ---
|
|
|
|
// Retourne le profil existant pour un email
|
|
const getUsedProfileForEmail = (email) => {
|
|
// On cherche tous les profils dont l'email correspond
|
|
const matchingProfiles = profiles.filter(p => p.email === email);
|
|
|
|
// On retourne le premier profil correspondant (ou undefined)
|
|
const result = matchingProfiles.length > 0 ? matchingProfiles[0] : undefined;
|
|
|
|
return result;
|
|
};
|
|
|
|
// Met à jour le formData et newTeacher si besoin
|
|
const updateFormData = (data) => {
|
|
setFormData(prev => ({ ...prev, ...data }));
|
|
if (newTeacher) setNewTeacher(prev => ({ ...prev, ...data }));
|
|
};
|
|
|
|
// Récupération des messages d'erreur pour un champ donné
|
|
const getError = (field) => {
|
|
return localErrors?.[field]?.[0];
|
|
};
|
|
|
|
// --- HANDLERS ---
|
|
|
|
const handleEmailChange = (e) => {
|
|
const email = e.target.value;
|
|
const existingProfile = getUsedProfileForEmail(email);
|
|
|
|
if (existingProfile) {
|
|
logger.info(`Adresse email déjà utilisée pour le profil ${existingProfile.id}`);
|
|
}
|
|
|
|
updateFormData({
|
|
associated_profile_email: email,
|
|
existingProfileId: existingProfile ? existingProfile.id : null,
|
|
});
|
|
};
|
|
|
|
const handleAddTeacher = () => {
|
|
setNewTeacher({
|
|
id: Date.now(),
|
|
last_name: '',
|
|
first_name: '',
|
|
associated_profile_email: '',
|
|
specialities: [],
|
|
role_type: 0,
|
|
});
|
|
setFormData({
|
|
last_name: '',
|
|
first_name: '',
|
|
associated_profile_email: '',
|
|
specialities: [],
|
|
role_type: 0,
|
|
});
|
|
};
|
|
|
|
const handleRemoveTeacher = (id) => {
|
|
logger.debug('[DELETE] Suppression teacher id:', id);
|
|
return handleDelete(id)
|
|
.then(() => {
|
|
setTeachers(prevTeachers =>
|
|
prevTeachers.filter(teacher => teacher.id !== id)
|
|
);
|
|
logger.debug('[DELETE] Teacher supprimé:', id);
|
|
})
|
|
.catch(logger.error);
|
|
};
|
|
|
|
const handleSaveNewTeacher = () => {
|
|
if (
|
|
formData.last_name &&
|
|
formData.first_name &&
|
|
formData.associated_profile_email
|
|
) {
|
|
const data = {
|
|
last_name: formData.last_name,
|
|
first_name: formData.first_name,
|
|
profile_role_data: {
|
|
establishment: selectedEstablishmentId,
|
|
role_type: formData.role_type || 0,
|
|
is_active: true,
|
|
...(formData.existingProfileId
|
|
? { profile: formData.existingProfileId }
|
|
: {
|
|
profile_data: {
|
|
email: formData.associated_profile_email,
|
|
username: formData.associated_profile_email,
|
|
password: 'Provisoire01!',
|
|
},
|
|
}),
|
|
},
|
|
specialities: formData.specialities || [],
|
|
};
|
|
|
|
handleCreate(data)
|
|
.then((createdTeacher) => {
|
|
// Recherche du profile associé dans profiles
|
|
let newProfileId = undefined;
|
|
let foundProfile = undefined;
|
|
if (
|
|
createdTeacher &&
|
|
createdTeacher.profile_role &&
|
|
createdTeacher.profile
|
|
) {
|
|
newProfileId = createdTeacher.profile;
|
|
foundProfile = profiles.find(p => p.id === newProfileId);
|
|
}
|
|
|
|
setTeachers([createdTeacher, ...teachers]);
|
|
setNewTeacher(null);
|
|
setLocalErrors({});
|
|
setFormData(prev => ({
|
|
...prev,
|
|
existingProfileId: newProfileId,
|
|
}));
|
|
})
|
|
.catch((error) => {
|
|
logger.error('Error:', error.message);
|
|
if (error.details) setLocalErrors(error.details);
|
|
});
|
|
} else {
|
|
setPopupMessage('Tous les champs doivent être remplis et valides');
|
|
setPopupVisible(true);
|
|
}
|
|
};
|
|
|
|
const handleUpdateTeacher = (id, updatedData) => {
|
|
if (
|
|
updatedData.last_name &&
|
|
updatedData.first_name &&
|
|
updatedData.associated_profile_email
|
|
) {
|
|
const profileRoleData = {
|
|
id: updatedData.profile_role,
|
|
establishment: selectedEstablishmentId,
|
|
role_type: updatedData.role_type || 0,
|
|
profile: updatedData.existingProfileId,
|
|
};
|
|
|
|
handleEdit(id, {
|
|
last_name: updatedData.last_name,
|
|
first_name: updatedData.first_name,
|
|
profile_role_data: profileRoleData,
|
|
specialities: updatedData.specialities || [],
|
|
})
|
|
.then((updatedTeacher) => {
|
|
setTeachers((prevTeachers) =>
|
|
prevTeachers.map((teacher) =>
|
|
teacher.id === id ? { ...teacher, ...updatedTeacher } : teacher
|
|
)
|
|
);
|
|
setEditingTeacher(null);
|
|
setFormData({});
|
|
})
|
|
.catch((error) => {
|
|
logger.error('Error:', error.message);
|
|
if (error.details) setLocalErrors(error.details);
|
|
});
|
|
} else {
|
|
setPopupMessage('Tous les champs doivent être remplis et valides');
|
|
setPopupVisible(true);
|
|
}
|
|
};
|
|
|
|
const handleChange = (e) => {
|
|
const { name, value, type, checked } = e.target;
|
|
let parsedValue = type === 'checkbox' ? (checked ? 1 : 0) : value;
|
|
updateFormData({ [name]: parsedValue });
|
|
};
|
|
|
|
const handleSpecialitiesChange = (selectedSpecialities) => {
|
|
updateFormData({ specialities: selectedSpecialities });
|
|
};
|
|
|
|
const handleEditTeacher = (teacher) => {
|
|
setEditingTeacher(teacher.id);
|
|
setFormData({
|
|
...teacher,
|
|
associated_profile_email: teacher.associated_profile_email,
|
|
role_type: teacher.role_type,
|
|
});
|
|
};
|
|
|
|
const renderTeacherCell = (teacher, column) => {
|
|
const isEditing = editingTeacher === teacher.id;
|
|
const isCreating = newTeacher && newTeacher.id === teacher.id;
|
|
const currentData = isEditing ? formData : newTeacher;
|
|
|
|
if (isEditing || isCreating) {
|
|
switch (column) {
|
|
case 'NOM - PRENOM':
|
|
return (
|
|
<div className="flex justify-center space-x-2">
|
|
<InputText
|
|
name="last_name"
|
|
value={currentData.last_name}
|
|
onChange={handleChange}
|
|
placeholder="Nom de l'enseignant"
|
|
errorMsg={getError('last_name')}
|
|
/>
|
|
<InputText
|
|
name="first_name"
|
|
value={currentData.first_name}
|
|
onChange={handleChange}
|
|
placeholder="Prénom de l'enseignant"
|
|
errorMsg={getError('first_name')}
|
|
/>
|
|
</div>
|
|
);
|
|
case 'EMAIL':
|
|
return (
|
|
<InputText
|
|
name="associated_profile_email"
|
|
type="email"
|
|
value={currentData.associated_profile_email || ''}
|
|
onChange={handleEmailChange}
|
|
placeholder="Adresse email de l'enseignant"
|
|
errorMsg={getError('email')}
|
|
enable={!isEditing}
|
|
/>
|
|
);
|
|
case 'SPECIALITES':
|
|
return (
|
|
<SpecialitiesDropZone
|
|
teacher={currentData}
|
|
handleSpecialitiesChange={handleSpecialitiesChange}
|
|
specialities={specialities}
|
|
isEditing={isEditing || isCreating}
|
|
/>
|
|
);
|
|
case 'ADMINISTRATEUR':
|
|
return (
|
|
<div className="flex justify-center">
|
|
<ToggleSwitch
|
|
name="role_type"
|
|
checked={currentData.role_type === 1}
|
|
onChange={handleChange}
|
|
/>
|
|
</div>
|
|
);
|
|
case 'ACTIONS':
|
|
return (
|
|
<div className="flex justify-center space-x-2">
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
isEditing
|
|
? handleUpdateTeacher(editingTeacher, formData)
|
|
: handleSaveNewTeacher()
|
|
}
|
|
className="text-green-500 hover:text-green-700"
|
|
>
|
|
<Check className="w-5 h-5" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
isEditing ? setEditingTeacher(null) : setNewTeacher(null)
|
|
}
|
|
className="text-red-500 hover:text-red-700"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
} else {
|
|
switch (column) {
|
|
case 'NOM - PRENOM':
|
|
return <TeacherItem key={teacher.id} teacher={teacher} />;
|
|
case 'EMAIL':
|
|
return teacher.associated_profile_email;
|
|
case 'SPECIALITES':
|
|
return (
|
|
<div className="flex justify-center space-x-2 flex-wrap">
|
|
{teacher.specialities_details.map((speciality) => (
|
|
<SpecialityItem
|
|
key={speciality.id}
|
|
speciality={speciality}
|
|
isDraggable={false}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
case 'ADMINISTRATEUR':
|
|
if (teacher.associated_profile_email) {
|
|
const badgeClass =
|
|
teacher.role_type === 1
|
|
? 'bg-red-100 text-red-600'
|
|
: 'bg-blue-100 text-blue-600';
|
|
const label = teacher.role_type === 1 ? 'OUI' : 'NON';
|
|
return (
|
|
<div
|
|
key={teacher.id}
|
|
className="flex justify-center items-center space-x-2"
|
|
>
|
|
<span
|
|
className={`px-3 py-1 rounded-full font-bold ${badgeClass}`}
|
|
>
|
|
{label}
|
|
</span>
|
|
</div>
|
|
);
|
|
} else {
|
|
return <i>Non définie</i>;
|
|
}
|
|
case 'MISE A JOUR':
|
|
return teacher.updated_date_formatted;
|
|
case 'ACTIONS':
|
|
return (
|
|
<div className="flex justify-center space-x-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleEditTeacher(teacher)}
|
|
className="text-blue-500 hover:text-blue-700"
|
|
>
|
|
<Edit3 className="w-5 h-5" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setRemovePopupVisible(true);
|
|
setRemovePopupMessage(
|
|
"Attention ! \nVous êtes sur le point de supprimer l'enseignant " +
|
|
teacher.last_name +
|
|
' ' +
|
|
teacher.first_name +
|
|
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
|
|
);
|
|
setRemovePopupOnConfirm(() => () => {
|
|
handleRemoveTeacher(teacher.id)
|
|
.then((data) => {
|
|
logger.debug('Success:', data);
|
|
setPopupMessage(
|
|
"L'enseignant " +
|
|
teacher.last_name +
|
|
' ' +
|
|
teacher.first_name +
|
|
' a été correctement supprimé'
|
|
);
|
|
setPopupVisible(true);
|
|
setRemovePopupVisible(false);
|
|
})
|
|
.catch((error) => {
|
|
logger.error('Error archiving data:', error);
|
|
setPopupMessage(
|
|
"Erreur lors de la suppression de l'enseignant " +
|
|
teacher.last_name +
|
|
' ' +
|
|
teacher.first_name
|
|
);
|
|
setPopupVisible(true);
|
|
setRemovePopupVisible(false);
|
|
});
|
|
});
|
|
}}
|
|
className="text-red-500 hover:text-red-700"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{ name: 'NOM - PRENOM', label: 'Nom et prénom' },
|
|
{ name: 'EMAIL', label: 'Email' },
|
|
{ name: 'SPECIALITES', label: 'Spécialités' },
|
|
{ name: 'ADMINISTRATEUR', label: 'Profil' },
|
|
{ name: 'MISE A JOUR', label: 'Mise à jour' },
|
|
{ name: 'ACTIONS', label: 'Actions' },
|
|
];
|
|
|
|
return (
|
|
<DndProvider backend={HTML5Backend}>
|
|
<div className="space-y-4">
|
|
<SectionHeader
|
|
icon={GraduationCap}
|
|
title="Liste des enseignants.es"
|
|
description="Gérez les enseignants.es de votre école"
|
|
button={true}
|
|
onClick={handleAddTeacher}
|
|
/>
|
|
<Table
|
|
data={newTeacher ? [newTeacher, ...teachers] : teachers}
|
|
columns={columns}
|
|
renderCell={renderTeacherCell}
|
|
emptyMessage={
|
|
<AlertMessage
|
|
type="warning"
|
|
title="Aucun enseignant enregistré"
|
|
message="Veuillez procéder à la création d'un nouvel enseignant."
|
|
/>
|
|
}
|
|
/>
|
|
<Popup
|
|
isOpen={popupVisible}
|
|
message={popupMessage}
|
|
onConfirm={() => setPopupVisible(false)}
|
|
onCancel={() => setPopupVisible(false)}
|
|
uniqueConfirmButton={true}
|
|
/>
|
|
<Popup
|
|
isOpen={removePopupVisible}
|
|
message={removePopupMessage}
|
|
onConfirm={removePopupOnConfirm}
|
|
onCancel={() => setRemovePopupVisible(false)}
|
|
/>
|
|
</div>
|
|
</DndProvider>
|
|
);
|
|
};
|
|
|
|
export default TeachersSection;
|