'use client'; import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Award, Eye, Search, BarChart2, X, Pencil, Trash2, Save, } from 'lucide-react'; import SectionHeader from '@/components/SectionHeader'; import Table from '@/components/Table'; import logger from '@/utils/logger'; import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL, FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL, } from '@/utils/Url'; import { getSecureFileUrl } from '@/utils/fileUrl'; import { fetchStudents, fetchStudentCompetencies, fetchAbsences, } from '@/app/actions/subscriptionAction'; import { fetchStudentEvaluations, updateStudentEvaluation, deleteStudentEvaluation, } from '@/app/actions/schoolAction'; import { useEstablishment } from '@/context/EstablishmentContext'; import { useClasses } from '@/context/ClassesContext'; import { useCsrfToken } from '@/context/CsrfContext'; 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}`; return ''; } function calcCompetencyStats(data) { if (!data?.data) return null; const scores = []; data.data.forEach((d) => d.categories.forEach((c) => c.competences.forEach((comp) => scores.push(comp.score)) ) ); if (!scores.length) return null; const total = scores.length; return { acquired: Math.round((scores.filter((s) => s === 3).length / total) * 100), inProgress: Math.round( (scores.filter((s) => s === 2).length / total) * 100 ), notAcquired: Math.round( (scores.filter((s) => s === 1).length / total) * 100 ), notEvaluated: Math.round( (scores.filter((s) => s === null || s === undefined || s === 0).length / total) * 100 ), }; } function getPeriodColumns(frequency) { if (frequency === 1) return [ { label: 'Trimestre 1', value: 1 }, { label: 'Trimestre 2', value: 2 }, { label: 'Trimestre 3', value: 3 }, ]; if (frequency === 2) return [ { label: 'Semestre 1', value: 1 }, { label: 'Semestre 2', value: 2 }, ]; if (frequency === 3) return [{ label: 'Année', value: 1 }]; return []; } const COMPETENCY_COLUMNS = [ { key: 'acquired', label: 'Acquises', color: 'bg-emerald-100 text-emerald-700', }, { key: 'inProgress', label: 'En cours', color: 'bg-yellow-100 text-yellow-700', }, { key: 'notAcquired', label: 'Non acquises', color: 'bg-red-100 text-red-600', }, { key: 'notEvaluated', label: 'Non évaluées', color: 'bg-gray-100 text-gray-600', }, ]; function getCurrentPeriodValue(frequency) { const periods = { 1: [ { value: 1, start: '09-01', end: '12-31' }, { value: 2, start: '01-01', end: '03-31' }, { value: 3, start: '04-01', end: '07-15' }, ], 2: [ { value: 1, start: '09-01', end: '01-31' }, { value: 2, start: '02-01', end: '07-15' }, ], 3: [{ value: 1, start: '09-01', end: '07-15' }], }[frequency] || []; const today = dayjs(); const current = periods.find( (p) => today.isAfter(dayjs(`${today.year()}-${p.start}`).subtract(1, 'day')) && today.isBefore(dayjs(`${today.year()}-${p.end}`).add(1, 'day')) ); return current?.value ?? null; } function PercentBadge({ value, loading, color }) { if (loading) return …; if (value === null) return —; const badgeColor = color || (value >= 75 ? 'bg-emerald-100 text-emerald-700' : value >= 50 ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-600'); return ( {value}% ); } export default function Page() { const router = useRouter(); const csrfToken = useCsrfToken(); const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = useEstablishment(); const { getNiveauLabel } = useClasses(); const [students, setStudents] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const ITEMS_PER_PAGE = 15; const [currentPage, setCurrentPage] = useState(1); const [statsMap, setStatsMap] = useState({}); const [statsLoading, setStatsLoading] = useState(false); const [absencesMap, setAbsencesMap] = useState({}); const [gradesModalStudent, setGradesModalStudent] = useState(null); const [studentEvaluations, setStudentEvaluations] = useState([]); const [gradesLoading, setGradesLoading] = useState(false); const [editingEvalId, setEditingEvalId] = useState(null); const [editScore, setEditScore] = useState(''); const [editAbsent, setEditAbsent] = useState(false); const periodColumns = getPeriodColumns( selectedEstablishmentEvaluationFrequency ); const currentPeriodValue = getCurrentPeriodValue( selectedEstablishmentEvaluationFrequency ); useEffect(() => { if (!selectedEstablishmentId) return; fetchStudents(selectedEstablishmentId, null, 5) .then((data) => setStudents(data)) .catch((error) => logger.error('Error fetching students:', error)); fetchAbsences(selectedEstablishmentId) .then((data) => { const map = {}; (data || []).forEach((a) => { if ([1, 2].includes(a.reason)) { map[a.student] = (map[a.student] || 0) + 1; } }); setAbsencesMap(map); }) .catch((error) => logger.error('Error fetching absences:', error)); }, [selectedEstablishmentId]); // Fetch stats for all students - aggregate all periods useEffect(() => { if (!students.length || !selectedEstablishmentEvaluationFrequency) return; setStatsLoading(true); const frequency = selectedEstablishmentEvaluationFrequency; const tasks = students.flatMap((student) => periodColumns.map(({ value: periodValue }) => { const periodStr = getPeriodString(periodValue, frequency); return fetchStudentCompetencies(student.id, periodStr) .then((data) => ({ studentId: student.id, periodValue, data })) .catch(() => ({ studentId: student.id, periodValue, data: null })); }) ); Promise.all(tasks).then((results) => { const map = {}; // Group by student and aggregate all competency scores across periods const studentScores = {}; results.forEach(({ studentId, data }) => { if (!studentScores[studentId]) studentScores[studentId] = []; if (data?.data) { data.data.forEach((d) => d.categories.forEach((c) => c.competences.forEach((comp) => studentScores[studentId].push(comp.score) ) ) ); } }); // Calculate stats for each student Object.keys(studentScores).forEach((studentId) => { const scores = studentScores[studentId]; if (!scores.length) { map[studentId] = null; } else { const total = scores.length; map[studentId] = { acquired: Math.round( (scores.filter((s) => s === 3).length / total) * 100 ), inProgress: Math.round( (scores.filter((s) => s === 2).length / total) * 100 ), notAcquired: Math.round( (scores.filter((s) => s === 1).length / total) * 100 ), notEvaluated: Math.round( (scores.filter((s) => s === null || s === undefined || s === 0) .length / total) * 100 ), }; } }); setStatsMap(map); setStatsLoading(false); }); }, [students, selectedEstablishmentEvaluationFrequency]); const filteredStudents = students.filter( (student) => !searchTerm || `${student.last_name} ${student.first_name}` .toLowerCase() .includes(searchTerm.toLowerCase()) ); useEffect(() => { setCurrentPage(1); }, [searchTerm, students]); const totalPages = Math.ceil(filteredStudents.length / ITEMS_PER_PAGE); const pagedStudents = filteredStudents.slice( (currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE ); const openGradesModal = (e, student) => { e.stopPropagation(); setGradesModalStudent(student); setGradesLoading(true); fetchStudentEvaluations(student.id) .then((data) => { setStudentEvaluations(data || []); setGradesLoading(false); }) .catch((error) => { logger.error('Error fetching student evaluations:', error); setStudentEvaluations([]); setGradesLoading(false); }); }; const closeGradesModal = () => { setGradesModalStudent(null); setStudentEvaluations([]); setEditingEvalId(null); }; const startEditingEval = (evalItem) => { setEditingEvalId(evalItem.id); setEditScore(evalItem.score ?? ''); setEditAbsent(evalItem.is_absent ?? false); }; const cancelEditingEval = () => { setEditingEvalId(null); setEditScore(''); setEditAbsent(false); }; const handleSaveEval = async (evalItem) => { try { await updateStudentEvaluation( evalItem.id, { score: editAbsent ? null : editScore === '' ? null : parseFloat(editScore), is_absent: editAbsent, }, csrfToken ); // Update local state setStudentEvaluations((prev) => prev.map((e) => e.id === evalItem.id ? { ...e, score: editAbsent ? null : editScore === '' ? null : parseFloat(editScore), is_absent: editAbsent, } : e ) ); cancelEditingEval(); } catch (error) { logger.error('Error updating evaluation:', error); } }; const handleDeleteEval = async (evalItem) => { if (!confirm('Supprimer cette note ?')) return; try { await deleteStudentEvaluation(evalItem.id, csrfToken); setStudentEvaluations((prev) => prev.filter((e) => e.id !== evalItem.id)); } catch (error) { logger.error('Error deleting evaluation:', error); } }; // Group evaluations by subject const groupedBySubject = studentEvaluations.reduce((acc, evalItem) => { const subjectName = evalItem.speciality_name || 'Sans matière'; const subjectColor = evalItem.speciality_color || '#6B7280'; if (!acc[subjectName]) { acc[subjectName] = { color: subjectColor, evaluations: [] }; } acc[subjectName].evaluations.push(evalItem); return acc; }, {}); const handleEvaluer = (e, studentId) => { e.stopPropagation(); const periodStr = getPeriodString( currentPeriodValue, selectedEstablishmentEvaluationFrequency ); router.push( `${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${studentId}&period=${periodStr}` ); }; const columns = [ { name: 'Photo', transform: () => null }, { name: 'Élève', transform: () => null }, { name: 'Niveau', transform: () => null }, { name: 'Classe', transform: () => null }, ...COMPETENCY_COLUMNS.map(({ label }) => ({ name: label, transform: () => null, })), { name: 'Absences', transform: () => null }, { name: 'Actions', transform: () => null }, ]; const renderCell = (student, column) => { const stats = statsMap[student.id] || {}; switch (column) { case 'Photo': return (
| Évaluation | Période | Note | Actions |
|---|---|---|---|
| {evalItem.evaluation_name || 'Évaluation'} | {evalItem.period || '—'} |
{isEditing ? (
{!editAbsent && (
setEditScore(e.target.value)
}
min="0"
max={evalItem.max_score || 20}
step="0.5"
className="w-16 text-center px-1 py-0.5 border rounded text-sm"
/>
)}
/{evalItem.max_score || 20}
) : evalItem.is_absent ? (
Absent
) : evalItem.score !== null ? (
{evalItem.score}/
{evalItem.max_score || 20}
) : (
—
)}
|
{isEditing ? (
|