mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 16:03:21 +00:00
feat: Amorçage de la gestion des absences [#16]
This commit is contained in:
@ -11,6 +11,8 @@ import { useSearchParams } from 'next/navigation';
|
||||
import logger from '@/utils/logger';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import Button from '@/components/Button';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
@ -29,6 +31,33 @@ export default function Page() {
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [selectedLevels, setSelectedLevels] = useState([]); // Par défaut, tous les niveaux sont sélectionnés
|
||||
const [filteredStudents, setFilteredStudents] = useState([]);
|
||||
const [isEditingAttendance, setIsEditingAttendance] = useState(false); // État pour le mode édition
|
||||
const [attendance, setAttendance] = useState({}); // État pour les cases cochées
|
||||
const [absences, setAbsences] = useState({}); // État pour stocker les absences
|
||||
const [absenceDetails, setAbsenceDetails] = useState({
|
||||
day: '',
|
||||
reason: null,
|
||||
moment: null,
|
||||
}); // Détails d'absence
|
||||
|
||||
// AbsenceMoment constants
|
||||
const AbsenceMoment = {
|
||||
MORNING: { value: 1, label: 'Morning' },
|
||||
AFTERNOON: { value: 2, label: 'Afternoon' },
|
||||
TOTAL: { value: 3, label: 'Total' },
|
||||
};
|
||||
|
||||
// AbsenceReason constants
|
||||
const AbsenceReason = {
|
||||
JUSTIFIED_ABSENCE: { value: 1, label: 'Justified Absence' },
|
||||
UNJUSTIFIED_ABSENCE: { value: 2, label: 'Unjustified Absence' },
|
||||
JUSTIFIED_LATE: { value: 3, label: 'Justified Late' },
|
||||
UNJUSTIFIED_LATE: { value: 4, label: 'Unjustified Late' },
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Absences enregistrées :', absences);
|
||||
}, [absences]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialiser les niveaux sélectionnés avec tous les niveaux disponibles
|
||||
@ -50,6 +79,17 @@ export default function Page() {
|
||||
}
|
||||
}, [selectedLevels, classe]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialiser l'état des cases cochées avec tous les élèves présents par défaut
|
||||
if (filteredStudents.length > 0) {
|
||||
const initialAttendance = filteredStudents.reduce((acc, student) => {
|
||||
acc[student.id] = true; // Tous les élèves sont cochés par défaut
|
||||
return acc;
|
||||
}, {});
|
||||
setAttendance(initialAttendance);
|
||||
}
|
||||
}, [filteredStudents]);
|
||||
|
||||
const handleCreateGroup = () => {
|
||||
if (!newGroup.name || !newGroup.level || !newGroup.students.length) {
|
||||
setPopupMessage(
|
||||
@ -73,6 +113,75 @@ export default function Page() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggleAttendanceMode = () => {
|
||||
setIsEditingAttendance((prev) => !prev); // Basculer entre mode édition et visualisation
|
||||
};
|
||||
|
||||
const handleValidateAttendance = () => {
|
||||
console.log('Présence validée :', attendance);
|
||||
console.log('Absences enregistrées :', absences);
|
||||
|
||||
// Exemple : Envoyer les absences à une API
|
||||
Object.entries(absences).forEach(([studentId, absenceData]) => {
|
||||
saveAbsence(studentId, absenceData);
|
||||
});
|
||||
|
||||
setIsEditingAttendance(false); // Revenir au mode visualisation
|
||||
};
|
||||
|
||||
const handleAttendanceChange = (studentId) => {
|
||||
setAttendance((prev) => {
|
||||
const updatedAttendance = {
|
||||
...prev,
|
||||
[studentId]: !prev[studentId], // Inverser l'état de présence
|
||||
};
|
||||
|
||||
// Si l'élève est décoché (absent), initialiser les champs d'absence
|
||||
if (!updatedAttendance[studentId]) {
|
||||
setAbsences((prev) => ({
|
||||
...prev,
|
||||
[studentId]: {
|
||||
day: '',
|
||||
reason: null,
|
||||
moment: null,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
// Si l'élève est recoché, supprimer ses données d'absence
|
||||
setAbsences((prev) => {
|
||||
const updatedAbsences = { ...prev };
|
||||
delete updatedAbsences[studentId];
|
||||
return updatedAbsences;
|
||||
});
|
||||
}
|
||||
|
||||
return updatedAttendance;
|
||||
});
|
||||
};
|
||||
|
||||
const saveAbsence = async (studentId, absenceData) => {
|
||||
try {
|
||||
const response = await fetch('/api/absences', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
studentId,
|
||||
...absenceData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Erreur lors de l'enregistrement de l'absence");
|
||||
}
|
||||
|
||||
console.log(`Absence pour l'élève ${studentId} enregistrée avec succès.`);
|
||||
} catch (error) {
|
||||
console.error('Erreur :', error);
|
||||
}
|
||||
};
|
||||
|
||||
const requestErrorHandler = (err) => {
|
||||
logger.error('Error fetching data:', err);
|
||||
};
|
||||
@ -151,47 +260,148 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
{/* Section Élèves */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-semibold mb-4 flex items-center">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-semibold flex items-center">
|
||||
<Users className="w-6 h-6 mr-2" />
|
||||
Élèves
|
||||
</h2>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
name: 'photo',
|
||||
transform: (row) => (
|
||||
<div className="flex justify-center items-center">
|
||||
{row.photo ? (
|
||||
<a
|
||||
href={`${BASE_URL}${row.photo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
src={`${BASE_URL}${row.photo}`}
|
||||
alt={`${row.first_name} ${row.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.first_name[0]}
|
||||
{row.last_name[0]}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ name: 'Nom', transform: (row) => row.last_name },
|
||||
{ name: 'Prénom', transform: (row) => row.first_name },
|
||||
{ name: 'Niveau', transform: (row) => getNiveauLabel(row.level) },
|
||||
]}
|
||||
data={filteredStudents} // Utiliser les élèves filtrés
|
||||
/>
|
||||
{!isEditingAttendance ? (
|
||||
<Button
|
||||
text="Faire l'appel"
|
||||
onClick={handleToggleAttendanceMode}
|
||||
primary
|
||||
className="px-4 py-2"
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
text="Valider l'appel"
|
||||
onClick={handleValidateAttendance}
|
||||
primary
|
||||
className="px-4 py-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
name: 'photo',
|
||||
transform: (row) => (
|
||||
<div className="flex justify-center items-center">
|
||||
{row.photo ? (
|
||||
<a
|
||||
href={`${BASE_URL}${row.photo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
src={`${BASE_URL}${row.photo}`}
|
||||
alt={`${row.first_name} ${row.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.first_name[0]}
|
||||
{row.last_name[0]}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ name: 'Nom', transform: (row) => row.last_name },
|
||||
{ name: 'Prénom', transform: (row) => row.first_name },
|
||||
{ name: 'Niveau', transform: (row) => getNiveauLabel(row.level) },
|
||||
...(isEditingAttendance
|
||||
? [
|
||||
{
|
||||
name: 'Présent',
|
||||
transform: (row) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={attendance[row.id] || false}
|
||||
onChange={() => handleAttendanceChange(row.id)}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Détails d'absence",
|
||||
transform: (row) =>
|
||||
!attendance[row.id] && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
{/* Champ pour le jour */}
|
||||
<input
|
||||
type="date"
|
||||
value={absences[row.id]?.day || ''}
|
||||
onChange={(e) =>
|
||||
setAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
day: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
|
||||
/>
|
||||
|
||||
{/* Champ pour le motif */}
|
||||
<SelectChoice
|
||||
name={`reason-${row.id}`}
|
||||
label=""
|
||||
placeHolder="Motif"
|
||||
selected={absences[row.id]?.reason || ''}
|
||||
callback={(e) =>
|
||||
setAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
reason: parseInt(e.target.value, 10),
|
||||
},
|
||||
}))
|
||||
}
|
||||
choices={Object.values(AbsenceReason).map(
|
||||
(reason) => ({
|
||||
value: reason.value,
|
||||
label: reason.label,
|
||||
})
|
||||
)}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Champ pour le moment */}
|
||||
<SelectChoice
|
||||
name={`moment-${row.id}`}
|
||||
label=""
|
||||
placeHolder="Moment"
|
||||
selected={absences[row.id]?.moment || ''}
|
||||
callback={(e) =>
|
||||
setAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
moment: parseInt(e.target.value, 10),
|
||||
},
|
||||
}))
|
||||
}
|
||||
choices={Object.values(AbsenceMoment).map(
|
||||
(moment) => ({
|
||||
value: moment.value,
|
||||
label: moment.label,
|
||||
})
|
||||
)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
data={filteredStudents} // Utiliser les élèves filtrés
|
||||
/>
|
||||
|
||||
{/* Popup */}
|
||||
<Popup
|
||||
|
||||
Reference in New Issue
Block a user