mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
chore: câblage des absences/retard dans le suivi pédagogique
This commit is contained in:
@ -20,6 +20,9 @@ import {
|
||||
fetchStudents,
|
||||
fetchStudentCompetencies,
|
||||
searchStudents,
|
||||
fetchAbsences,
|
||||
editAbsences,
|
||||
deleteAbsences,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
@ -28,9 +31,11 @@ import SectionHeader from '@/components/SectionHeader';
|
||||
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
||||
import InputText from '@/components/InputText';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } =
|
||||
useEstablishment();
|
||||
const { getNiveauLabel } = useClasses();
|
||||
@ -43,6 +48,7 @@ export default function Page() {
|
||||
const [grades, setGrades] = useState({});
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedPeriod, setSelectedPeriod] = useState(null);
|
||||
const [allAbsences, setAllAbsences] = useState([]);
|
||||
|
||||
// Définir les périodes selon la fréquence
|
||||
const getPeriods = () => {
|
||||
@ -99,11 +105,6 @@ export default function Page() {
|
||||
},
|
||||
];
|
||||
|
||||
const absences = [
|
||||
{ date: '2023-09-01', type: 'Absence', reason: 'Maladie', justified: true },
|
||||
{ date: '2023-09-15', type: 'Retard', reason: 'Trafic', justified: false },
|
||||
];
|
||||
|
||||
const remarks = [
|
||||
{
|
||||
date: '2023-09-10',
|
||||
@ -196,6 +197,32 @@ export default function Page() {
|
||||
}
|
||||
}, [formData.selectedStudent, selectedPeriod]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
fetchAbsences(selectedEstablishmentId)
|
||||
.then((data) => setAllAbsences(data))
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors du fetch des absences:', error)
|
||||
);
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
// Transforme les absences backend pour l'élève sélectionné
|
||||
const absences = React.useMemo(() => {
|
||||
if (!formData.selectedStudent) return [];
|
||||
return allAbsences
|
||||
.filter((a) => a.student === formData.selectedStudent)
|
||||
.map((a) => ({
|
||||
id: a.id,
|
||||
date: a.day,
|
||||
type: [2, 1].includes(a.reason) ? 'Absence' : 'Retard',
|
||||
reason: a.reason, // tu peux mapper le code vers un label si besoin
|
||||
justified: [1, 3].includes(a.reason), // 1 et 3 = justifié
|
||||
moment: a.moment,
|
||||
commentaire: a.commentaire,
|
||||
}));
|
||||
}, [allAbsences, formData.selectedStudent]);
|
||||
|
||||
// Fonction utilitaire pour convertir la période sélectionnée en string backend
|
||||
function getPeriodString(selectedPeriod, frequency) {
|
||||
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; // année scolaire commence en septembre
|
||||
@ -207,6 +234,42 @@ export default function Page() {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Callback pour justifier/non justifier une absence
|
||||
const handleToggleJustify = (absence) => {
|
||||
// Inverser l'état justifié (1/3 = justifié, 2/4 = non justifié)
|
||||
const newReason =
|
||||
absence.type === 'Absence'
|
||||
? absence.justified
|
||||
? 2 // Absence non justifiée
|
||||
: 1 // Absence justifiée
|
||||
: absence.justified
|
||||
? 4 // Retard non justifié
|
||||
: 3; // Retard justifié
|
||||
|
||||
editAbsences(absence.id, { ...absence, reason: newReason }, csrfToken)
|
||||
.then(() => {
|
||||
setAllAbsences((prev) =>
|
||||
prev.map((a) =>
|
||||
a.id === absence.id ? { ...a, reason: newReason } : a
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error('Erreur lors du changement de justification', e);
|
||||
});
|
||||
};
|
||||
|
||||
// Callback pour supprimer une absence
|
||||
const handleDeleteAbsence = (absence) => {
|
||||
return deleteAbsences(absence.id, csrfToken)
|
||||
.then(() => {
|
||||
setAllAbsences((prev) => prev.filter((a) => a.id !== absence.id));
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Erreur lors de la suppression de l'absence", e);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<SectionHeader
|
||||
@ -392,7 +455,11 @@ export default function Page() {
|
||||
<div className="flex flex-col gap-8 w-full justify-center items-stretch">
|
||||
<div className="w-full flex flex-row items-stretch gap-4">
|
||||
<div className="flex-1 flex items-stretch justify-center h-full">
|
||||
<Attendance absences={absences} />
|
||||
<Attendance
|
||||
absences={absences}
|
||||
onToggleJustify={handleToggleJustify}
|
||||
onDelete={handleDeleteAbsence}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex items-stretch justify-center h-full">
|
||||
<GradesStatsCircle grades={grades} />
|
||||
|
||||
@ -345,6 +345,7 @@ export default function Page() {
|
||||
reason: reason,
|
||||
moment: absenceData.moment,
|
||||
establishment: selectedEstablishmentId,
|
||||
commentaire: absenceData.commentaire,
|
||||
};
|
||||
|
||||
if (absenceData.id) {
|
||||
@ -574,7 +575,7 @@ export default function Page() {
|
||||
Motif d'absence
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 items-center">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 items-center">
|
||||
{/* Select Absence/Retard */}
|
||||
<SelectChoice
|
||||
name={`type-${row.id}`}
|
||||
@ -619,6 +620,23 @@ export default function Page() {
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Nouveau champ commentaire */}
|
||||
<input
|
||||
type="text"
|
||||
className="border rounded px-2 py-1 text-sm w-full"
|
||||
placeholder="Commentaire"
|
||||
value={formAbsences[row.id]?.commentaire || ''}
|
||||
onChange={(e) =>
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
commentaire: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Checkbox Justifié */}
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<CheckBox
|
||||
|
||||
@ -1,28 +1,103 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
||||
import Button from '@/components/Button';
|
||||
import Popup from '@/components/Popup';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Attendance({ absences, onToggleJustify, onDelete }) {
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [removePopupVisible, setRemovePopupVisible] = useState(false);
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
export default function Attendance({ absences }) {
|
||||
return (
|
||||
<div className="w-full bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<h2 className="text-xl font-semibold mb-4">Présence et assiduité</h2>
|
||||
<ol className="relative border-l border-emerald-200">
|
||||
{absences.map((absence, idx) => (
|
||||
<li key={idx} className="mb-6 ml-4">
|
||||
<div className="absolute w-3 h-3 bg-emerald-400 rounded-full mt-1.5 -left-1.5 border border-white" />
|
||||
<time className="mb-1 text-xs font-normal leading-none text-gray-400">
|
||||
{absence.date}
|
||||
</time>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{absence.type}</span>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded ${absence.justified ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}
|
||||
>
|
||||
{absence.justified ? 'Justifiée' : 'Non justifiée'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{absence.reason}</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
{absences.length === 0 ? (
|
||||
<div className="text-center text-emerald-600 font-medium py-8">
|
||||
Aucune absence enregistrée 🎉
|
||||
</div>
|
||||
) : (
|
||||
<ol className="relative border-l border-emerald-200">
|
||||
{absences.map((absence, idx) => (
|
||||
<li key={idx} className="mb-6 ml-4">
|
||||
<div className="absolute w-3 h-3 bg-emerald-400 rounded-full mt-1.5 -left-1.5 border border-white" />
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{/* Infos principales à gauche */}
|
||||
<div className="flex flex-col">
|
||||
<time className="mb-1 text-xs font-normal leading-none text-gray-400">
|
||||
{absence.date}
|
||||
</time>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{absence.type}</span>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded ${absence.justified ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}
|
||||
>
|
||||
{absence.justified ? 'Justifiée' : 'Non justifiée'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{absence.commentaire}
|
||||
</div>
|
||||
</div>
|
||||
{/* Actions à droite */}
|
||||
<div className="flex flex-col items-end gap-2 min-w-[110px]">
|
||||
<ToggleSwitch
|
||||
name={`justified-${idx}`}
|
||||
checked={absence.justified}
|
||||
onChange={() => onToggleJustify(absence)}
|
||||
label="Justifiée"
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
onClick={() => {
|
||||
setRemovePopupVisible(true);
|
||||
setRemovePopupMessage(
|
||||
"Attention ! \nVous êtes sur le point de supprimer l'absence enregistrée.\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
|
||||
);
|
||||
setRemovePopupOnConfirm(() => () => {
|
||||
onDelete(absence)
|
||||
.then((data) => {
|
||||
showNotification(
|
||||
'Opération effectuée avec succès.',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotification(
|
||||
'Erreur lors de la suppression de l\absence',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
});
|
||||
});
|
||||
}}
|
||||
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
|
||||
icon={<Trash2 className="w-6 h-6" />}
|
||||
text="Supprimer"
|
||||
title="Evaluez l'élève"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
<Popup
|
||||
isOpen={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user