mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 20:51:26 +00:00
chore: Application du design system
This commit is contained in:
@ -3,16 +3,19 @@ import Logo from '../components/Logo';
|
||||
|
||||
export default function Custom500() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-emerald-500">
|
||||
<div className="text-center p-6 ">
|
||||
<div className="flex items-center justify-center min-h-screen bg-primary">
|
||||
<div className="text-center p-6 bg-white rounded-md shadow-sm border border-gray-200">
|
||||
<Logo className="w-32 h-32 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-emerald-900 mb-4">
|
||||
<h2 className="font-headline text-2xl font-bold text-secondary mb-4">
|
||||
500 | Erreur interne
|
||||
</h2>
|
||||
<p className="text-emerald-900 mb-4">
|
||||
<p className="font-body text-gray-600 mb-4">
|
||||
Une erreur interne est survenue.
|
||||
</p>
|
||||
<Link className="text-gray-900 hover:underline" href="/">
|
||||
<Link
|
||||
className="inline-flex items-center justify-center min-h-[44px] px-4 py-2 rounded font-label font-medium bg-primary hover:bg-secondary text-white transition-colors"
|
||||
href="/"
|
||||
>
|
||||
Retour Accueil
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@ -2,8 +2,17 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { PARENT_FILTER, SCHOOL_FILTER } from '@/utils/constants';
|
||||
import { Trash2, ToggleLeft, ToggleRight, Info, XCircle } from 'lucide-react';
|
||||
import {
|
||||
Trash2,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
Info,
|
||||
XCircle,
|
||||
Users,
|
||||
UserPlus,
|
||||
} from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import Popup from '@/components/Popup';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||
@ -17,7 +26,6 @@ import { dissociateGuardian } from '@/app/actions/subscriptionAction';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import logger from '@/utils/logger';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
|
||||
const roleTypeToLabel = (roleType) => {
|
||||
switch (roleType) {
|
||||
@ -39,7 +47,7 @@ const roleTypeToBadgeClass = (roleType) => {
|
||||
case 1:
|
||||
return 'bg-red-100 text-red-600';
|
||||
case 2:
|
||||
return 'bg-green-100 text-green-600';
|
||||
return 'bg-tertiary/10 text-tertiary';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-600';
|
||||
}
|
||||
@ -378,7 +386,7 @@ export default function Page() {
|
||||
type="button"
|
||||
className={
|
||||
row.is_active
|
||||
? 'text-emerald-500 hover:text-emerald-700'
|
||||
? 'text-primary hover:text-secondary'
|
||||
: 'text-orange-500 hover:text-orange-700'
|
||||
}
|
||||
onClick={() => handleConfirmActivateProfile(row)}
|
||||
@ -474,7 +482,7 @@ export default function Page() {
|
||||
type="button"
|
||||
className={
|
||||
row.is_active
|
||||
? 'text-emerald-500 hover:text-emerald-700'
|
||||
? 'text-primary hover:text-secondary'
|
||||
: 'text-orange-500 hover:text-orange-700'
|
||||
}
|
||||
onClick={() => handleConfirmActivateProfile(row)}
|
||||
@ -516,10 +524,10 @@ export default function Page() {
|
||||
totalPages={totalProfilesParentPages}
|
||||
onPageChange={handlePageChange}
|
||||
emptyMessage={
|
||||
<AlertMessage
|
||||
type="info"
|
||||
title="Aucun profil PARENT enregistré"
|
||||
message="Un profil Parent est ajouté lors de la création d'un nouveau dossier d'inscription."
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title="Aucun profil parent enregistré"
|
||||
description="Les profils parents sont créés automatiquement lors de la création d'un dossier d'inscription."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@ -540,10 +548,10 @@ export default function Page() {
|
||||
totalPages={totalProfilesSchoolPages}
|
||||
onPageChange={handlePageChange}
|
||||
emptyMessage={
|
||||
<AlertMessage
|
||||
type="info"
|
||||
title="Aucun profil ECOLE enregistré"
|
||||
message="Un profil ECOLE est ajouté lors de la création d'un nouvel enseignant."
|
||||
<EmptyState
|
||||
icon={UserPlus}
|
||||
title="Aucun profil école enregistré"
|
||||
description="Les profils école sont créés automatiquement lors de l'ajout d'un enseignant."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -82,12 +82,12 @@ export default function FeedbackPage() {
|
||||
return (
|
||||
<div className="h-full flex flex-col p-4">
|
||||
<div className="max-w-3xl mx-auto w-full">
|
||||
<h1 className="text-2xl font-headline font-bold text-gray-800 mb-2">
|
||||
<h1 className="font-headline text-2xl font-bold text-gray-800 mb-2">
|
||||
{t('title')}
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">{t('description')}</p>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="bg-white rounded-md shadow-sm border border-gray-200 p-6">
|
||||
{/* Catégorie */}
|
||||
<SelectChoice
|
||||
name="category"
|
||||
|
||||
@ -251,31 +251,31 @@ export default function StudentGradesPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => router.push('/admin/grades')}
|
||||
className="p-2 rounded-md hover:bg-gray-100 border border-gray-200"
|
||||
className="p-2 rounded hover:bg-gray-100 border border-gray-200 min-h-[44px] min-w-[44px] flex items-center justify-center transition-colors"
|
||||
aria-label="Retour à la liste"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="text-xl font-bold text-gray-800">Suivi pédagogique</h1>
|
||||
<h1 className="font-headline text-xl font-bold text-gray-800">Suivi pédagogique</h1>
|
||||
</div>
|
||||
|
||||
{/* Student profile */}
|
||||
{student && (
|
||||
<div className="bg-stone-50 rounded-lg shadow-sm border border-gray-100 p-4 md:p-6 flex flex-col sm:flex-row items-center sm:items-start gap-4">
|
||||
<div className="bg-neutral rounded-md shadow-sm border border-gray-100 p-4 md:p-6 flex flex-col sm:flex-row items-center sm:items-start gap-4">
|
||||
{student.photo ? (
|
||||
<img
|
||||
src={getSecureFileUrl(student.photo)}
|
||||
alt={`${student.first_name} ${student.last_name}`}
|
||||
className="w-24 h-24 object-cover rounded-full border-4 border-emerald-200 shadow"
|
||||
className="w-24 h-24 object-cover rounded-full border-4 border-primary/20 shadow"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-24 h-24 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl border-4 border-emerald-100">
|
||||
<div className="w-24 h-24 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl border-4 border-primary/10">
|
||||
{student.first_name?.[0]}
|
||||
{student.last_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 text-center sm:text-left">
|
||||
<div className="text-xl font-bold text-emerald-800">
|
||||
<div className="text-xl font-bold text-secondary">
|
||||
{student.last_name} {student.first_name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-1">
|
||||
@ -322,7 +322,7 @@ export default function StudentGradesPage() {
|
||||
`${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${studentId}&period=${periodString}`
|
||||
);
|
||||
}}
|
||||
className="px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full sm:w-auto"
|
||||
className="px-4 py-2 rounded shadow bg-primary text-white font-label font-medium hover:bg-secondary w-full sm:w-auto min-h-[44px] transition-colors"
|
||||
icon={<Award className="w-5 h-5" />}
|
||||
text="Évaluer"
|
||||
title="Évaluer l'élève"
|
||||
@ -351,10 +351,10 @@ export default function StudentGradesPage() {
|
||||
</div>
|
||||
|
||||
{/* Évaluations par matière */}
|
||||
<div className="bg-stone-50 rounded-lg shadow-sm border border-gray-200 p-4 md:p-6">
|
||||
<div className="bg-neutral rounded-md shadow-sm border border-gray-200 p-4 md:p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<BookOpen className="w-6 h-6 text-emerald-600" />
|
||||
<h2 className="text-xl font-semibold text-gray-800">
|
||||
<BookOpen className="w-6 h-6 text-primary" />
|
||||
<h2 className="font-headline text-xl font-semibold text-gray-800">
|
||||
Évaluations par matière
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@ -12,13 +12,17 @@ import {
|
||||
Save,
|
||||
Download,
|
||||
FileText,
|
||||
UserPlus,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import Table from '@/components/Table';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import logger from '@/utils/logger';
|
||||
import {
|
||||
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
|
||||
FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL,
|
||||
FE_ADMIN_SUBSCRIPTIONS_CREATE_URL,
|
||||
} from '@/utils/Url';
|
||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||
import {
|
||||
@ -36,15 +40,25 @@ import { useClasses } from '@/context/ClassesContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { exportToCSV } from '@/utils/exportCSV';
|
||||
import SchoolYearFilter from '@/components/SchoolYearFilter';
|
||||
import { getCurrentSchoolYear, getNextSchoolYear, getHistoricalYears } from '@/utils/Date';
|
||||
import { CURRENT_YEAR_FILTER, NEXT_YEAR_FILTER, HISTORICAL_FILTER } from '@/utils/constants';
|
||||
import {
|
||||
getCurrentSchoolYear,
|
||||
getNextSchoolYear,
|
||||
getHistoricalYears,
|
||||
} from '@/utils/Date';
|
||||
import {
|
||||
CURRENT_YEAR_FILTER,
|
||||
NEXT_YEAR_FILTER,
|
||||
HISTORICAL_FILTER,
|
||||
} from '@/utils/constants';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
function getPeriodString(periodValue, frequency, schoolYear = null) {
|
||||
const year = schoolYear || (() => {
|
||||
const y = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
||||
return `${y}-${y + 1}`;
|
||||
})();
|
||||
const year =
|
||||
schoolYear ||
|
||||
(() => {
|
||||
const y = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
||||
return `${y}-${y + 1}`;
|
||||
})();
|
||||
if (frequency === 1) return `T${periodValue}_${year}`;
|
||||
if (frequency === 2) return `S${periodValue}_${year}`;
|
||||
if (frequency === 3) return `A_${year}`;
|
||||
@ -97,7 +111,7 @@ const COMPETENCY_COLUMNS = [
|
||||
{
|
||||
key: 'acquired',
|
||||
label: 'Acquises',
|
||||
color: 'bg-emerald-100 text-emerald-700',
|
||||
color: 'bg-primary/10 text-secondary',
|
||||
},
|
||||
{
|
||||
key: 'inProgress',
|
||||
@ -145,7 +159,7 @@ function PercentBadge({ value, loading, color }) {
|
||||
const badgeColor =
|
||||
color ||
|
||||
(value >= 75
|
||||
? 'bg-emerald-100 text-emerald-700'
|
||||
? 'bg-primary/10 text-secondary'
|
||||
: value >= 50
|
||||
? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-red-100 text-red-600');
|
||||
@ -177,13 +191,13 @@ export default function Page() {
|
||||
const [editingEvalId, setEditingEvalId] = useState(null);
|
||||
const [editScore, setEditScore] = useState('');
|
||||
const [editAbsent, setEditAbsent] = useState(false);
|
||||
|
||||
|
||||
// Filtrage par année scolaire
|
||||
const [activeYearFilter, setActiveYearFilter] = useState(CURRENT_YEAR_FILTER);
|
||||
const currentSchoolYear = useMemo(() => getCurrentSchoolYear(), []);
|
||||
const nextSchoolYear = useMemo(() => getNextSchoolYear(), []);
|
||||
const historicalYears = useMemo(() => getHistoricalYears(5), []);
|
||||
|
||||
|
||||
// Déterminer l'année scolaire sélectionnée
|
||||
const selectedSchoolYear = useMemo(() => {
|
||||
if (activeYearFilter === CURRENT_YEAR_FILTER) return currentSchoolYear;
|
||||
@ -193,10 +207,11 @@ export default function Page() {
|
||||
return historicalYears[0];
|
||||
}, [activeYearFilter, currentSchoolYear, nextSchoolYear, historicalYears]);
|
||||
|
||||
const periodColumns = useMemo(() => getPeriodColumns(
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
), [selectedEstablishmentEvaluationFrequency]);
|
||||
|
||||
const periodColumns = useMemo(
|
||||
() => getPeriodColumns(selectedEstablishmentEvaluationFrequency),
|
||||
[selectedEstablishmentEvaluationFrequency]
|
||||
);
|
||||
|
||||
const currentPeriodValue = getCurrentPeriodValue(
|
||||
selectedEstablishmentEvaluationFrequency
|
||||
);
|
||||
@ -229,7 +244,11 @@ export default function Page() {
|
||||
|
||||
const tasks = students.flatMap((student) =>
|
||||
periodColumns.map(({ value: periodValue }) => {
|
||||
const periodStr = getPeriodString(periodValue, frequency, selectedSchoolYear);
|
||||
const periodStr = getPeriodString(
|
||||
periodValue,
|
||||
frequency,
|
||||
selectedSchoolYear
|
||||
);
|
||||
return fetchStudentCompetencies(student.id, periodStr)
|
||||
.then((data) => ({ studentId: student.id, periodValue, data }))
|
||||
.catch(() => ({ studentId: student.id, periodValue, data: null }));
|
||||
@ -281,7 +300,12 @@ export default function Page() {
|
||||
setStatsMap(map);
|
||||
setStatsLoading(false);
|
||||
});
|
||||
}, [students, selectedEstablishmentEvaluationFrequency, selectedSchoolYear, periodColumns]);
|
||||
}, [
|
||||
students,
|
||||
selectedEstablishmentEvaluationFrequency,
|
||||
selectedSchoolYear,
|
||||
periodColumns,
|
||||
]);
|
||||
|
||||
const filteredStudents = students.filter(
|
||||
(student) =>
|
||||
@ -330,12 +354,16 @@ export default function Page() {
|
||||
{ key: 'last_name', label: 'Nom' },
|
||||
{ key: 'first_name', label: 'Prénom' },
|
||||
{ key: 'birth_date', label: 'Date de naissance' },
|
||||
{ key: 'level', label: 'Niveau', transform: (value) => getNiveauLabel(value) },
|
||||
{
|
||||
key: 'level',
|
||||
label: 'Niveau',
|
||||
transform: (value) => getNiveauLabel(value),
|
||||
},
|
||||
{ key: 'associated_class_name', label: 'Classe' },
|
||||
{
|
||||
key: 'id',
|
||||
{
|
||||
key: 'id',
|
||||
label: 'Absences',
|
||||
transform: (value) => absencesMap[value] || 0
|
||||
transform: (value) => absencesMap[value] || 0,
|
||||
},
|
||||
];
|
||||
|
||||
@ -347,7 +375,7 @@ export default function Page() {
|
||||
transform: (value) => {
|
||||
const stats = statsMap[value];
|
||||
return stats?.[key] !== undefined ? `${stats[key]}%` : '';
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -526,7 +554,7 @@ export default function Page() {
|
||||
`${FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL}?schoolClassId=${student.associated_class_id}`
|
||||
);
|
||||
}}
|
||||
className="text-emerald-700 hover:underline font-medium"
|
||||
className="text-secondary hover:underline font-medium"
|
||||
>
|
||||
{student.associated_class_name}
|
||||
</button>
|
||||
@ -581,7 +609,7 @@ export default function Page() {
|
||||
<button
|
||||
onClick={(e) => handleEvaluer(e, student.id)}
|
||||
disabled={!currentPeriodValue}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-emerald-100 text-emerald-700 hover:bg-emerald-200 transition whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-primary/10 text-secondary hover:bg-primary/20 transition whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title="Évaluer"
|
||||
>
|
||||
<Award size={14} />
|
||||
@ -634,7 +662,7 @@ export default function Page() {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-100 rounded-lg hover:bg-emerald-200 transition-colors ml-4"
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-secondary bg-primary/10 rounded hover:bg-primary/20 transition-colors ml-4"
|
||||
title="Exporter en CSV"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
@ -651,7 +679,22 @@ export default function Page() {
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
emptyMessage={
|
||||
<span className="text-gray-400 text-sm">Aucun élève trouvé</span>
|
||||
students.length === 0 && !searchTerm ? (
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title="Aucun élève inscrit"
|
||||
description="Commencez par inscrire des élèves pour suivre leur parcours pédagogique."
|
||||
actionLabel="Inscrire un élève"
|
||||
actionIcon={UserPlus}
|
||||
onAction={() => router.push(FE_ADMIN_SUBSCRIPTIONS_CREATE_URL)}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={Search}
|
||||
title="Aucun élève trouvé"
|
||||
description="Modifiez votre recherche pour trouver un élève."
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@ -662,7 +705,7 @@ export default function Page() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b bg-gray-50">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
<h2 className="font-headline text-lg font-semibold text-gray-800">
|
||||
Notes de {gradesModalStudent.first_name}{' '}
|
||||
{gradesModalStudent.last_name}
|
||||
</h2>
|
||||
@ -683,7 +726,7 @@ export default function Page() {
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{gradesLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-600"></div>
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : Object.keys(groupedBySubject).length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
@ -720,13 +763,13 @@ export default function Page() {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-r from-emerald-50 to-blue-50 rounded-lg p-4 border border-emerald-100">
|
||||
<div className="bg-gradient-to-r from-primary/5 to-blue-50 rounded-lg p-4 border border-primary/10">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-gray-600">
|
||||
Résumé
|
||||
</span>
|
||||
{overallAvg !== null && (
|
||||
<span className="text-lg font-bold text-emerald-700">
|
||||
<span className="text-lg font-bold text-secondary">
|
||||
Moyenne générale : {overallAvg}/20
|
||||
</span>
|
||||
)}
|
||||
@ -878,7 +921,7 @@ export default function Page() {
|
||||
onClick={() =>
|
||||
handleSaveEval(evalItem)
|
||||
}
|
||||
className="p-1 text-emerald-600 hover:bg-emerald-50 rounded"
|
||||
className="p-1 text-primary hover:bg-primary/5 rounded"
|
||||
title="Enregistrer"
|
||||
>
|
||||
<Save size={14} />
|
||||
|
||||
@ -86,12 +86,12 @@ export default function StudentCompetenciesPage() {
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<button
|
||||
onClick={() => router.push('/admin/grades')}
|
||||
className="p-2 rounded-md hover:bg-gray-100 border border-gray-200"
|
||||
className="p-2 rounded hover:bg-gray-100 border border-gray-200 min-h-[44px] min-w-[44px] flex items-center justify-center transition-colors"
|
||||
aria-label="Retour à la fiche élève"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="text-xl font-bold text-gray-800">Bilan de compétence</h1>
|
||||
<h1 className="font-headline text-xl font-bold text-gray-800">Bilan de compétence</h1>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<form
|
||||
|
||||
@ -158,7 +158,7 @@ export default function Layout({ children }) {
|
||||
)}
|
||||
|
||||
{/* Main container */}
|
||||
<div className="absolute overflow-auto bg-gradient-to-br from-emerald-50 via-sky-50 to-emerald-100 top-14 md:top-0 bottom-16 left-0 md:left-64 right-0">
|
||||
<div className="absolute overflow-auto bg-gradient-to-br from-primary/5 via-sky-50 to-primary/10 top-14 md:top-0 bottom-16 left-0 md:left-64 right-0">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
|
||||
@ -174,14 +174,14 @@ export default function DashboardPage() {
|
||||
<StatCard
|
||||
title={t('pendingRegistrations')}
|
||||
value={pendingRegistrationCount}
|
||||
icon={<Clock className="text-green-500" size={24} />}
|
||||
color="green"
|
||||
icon={<Clock className="text-tertiary" size={24} />}
|
||||
color="tertiary"
|
||||
/>
|
||||
<StatCard
|
||||
title={t('structureCapacity')}
|
||||
value={selectedEstablishmentTotalCapacity}
|
||||
icon={<School className="text-green-500" size={24} />}
|
||||
color="emerald"
|
||||
icon={<School className="text-primary" size={24} />}
|
||||
color="primary"
|
||||
/>
|
||||
<StatCard
|
||||
title={t('capacityRate')}
|
||||
@ -200,8 +200,8 @@ export default function DashboardPage() {
|
||||
{/* Colonne de gauche : Graphique des inscriptions + Présence */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Graphique des inscriptions */}
|
||||
<div className="bg-stone-50 p-4 md:p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
||||
<h2 className="text-lg font-semibold mb-4 md:mb-6">
|
||||
<div className="bg-neutral p-4 md:p-6 rounded-md shadow-sm border border-gray-100 flex-1">
|
||||
<h2 className="font-headline text-lg font-semibold mb-4 md:mb-6">
|
||||
{t('inscriptionTrends')}
|
||||
</h2>
|
||||
<div className="flex flex-col sm:flex-row gap-6 mt-4">
|
||||
@ -214,14 +214,14 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
{/* Présence et assiduité */}
|
||||
<div className="bg-stone-50 p-4 md:p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
||||
<div className="bg-neutral p-4 md:p-6 rounded-md shadow-sm border border-gray-100 flex-1">
|
||||
<Attendance absences={absencesToday} readOnly={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Colonne de droite : Événements à venir */}
|
||||
<div className="bg-stone-50 p-4 md:p-6 rounded-lg shadow-sm border border-gray-100 flex-1 h-full">
|
||||
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
||||
<div className="bg-neutral p-4 md:p-6 rounded-md shadow-sm border border-gray-100 flex-1 h-full">
|
||||
<h2 className="font-headline text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
||||
{upcomingEvents.map((event, index) => (
|
||||
<EventCard key={index} {...event} />
|
||||
))}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Tab from '@/components/Tab';
|
||||
import TabContent from '@/components/TabContent';
|
||||
import Button from '@/components/Form/Button';
|
||||
import InputText from '@/components/Form/InputText';
|
||||
import CheckBox from '@/components/Form/CheckBox'; // Import du composant CheckBox
|
||||
@ -13,13 +11,8 @@ import {
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext'; // Import du hook pour récupérer le csrfToken
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import { useSearchParams } from 'next/navigation'; // Ajoute cet import
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [activeTab, setActiveTab] = useState('smtp');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [smtpServer, setSmtpServer] = useState('');
|
||||
const [smtpPort, setSmtpPort] = useState('');
|
||||
const [smtpUser, setSmtpUser] = useState('');
|
||||
@ -29,23 +22,10 @@ export default function SettingsPage() {
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const csrfToken = useCsrfToken(); // Récupération du csrfToken
|
||||
const { showNotification } = useNotification();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
// Ajout : sélection automatique de l'onglet via l'ancre ou le paramètre de recherche
|
||||
useEffect(() => {
|
||||
const tabParam = searchParams.get('tab');
|
||||
if (tabParam === 'smtp') {
|
||||
setActiveTab('smtp');
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// Charger les paramètres SMTP existants
|
||||
useEffect(() => {
|
||||
if (activeTab === 'smtp') {
|
||||
if (csrfToken && selectedEstablishmentId) {
|
||||
fetchSmtpSettings(csrfToken, selectedEstablishmentId) // Passer le csrfToken ici
|
||||
.then((data) => {
|
||||
setSmtpServer(data.smtp_server || '');
|
||||
@ -75,7 +55,7 @@ export default function SettingsPage() {
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [activeTab, csrfToken]); // Ajouter csrfToken comme dépendance
|
||||
}, [csrfToken, selectedEstablishmentId]);
|
||||
|
||||
const handleSmtpServerChange = (e) => {
|
||||
setSmtpServer(e.target.value);
|
||||
@ -128,66 +108,63 @@ export default function SettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex space-x-4 mb-4">
|
||||
<Tab
|
||||
text="Paramètres SMTP"
|
||||
active={activeTab === 'smtp'}
|
||||
onClick={() => handleTabClick('smtp')}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TabContent isActive={activeTab === 'smtp'}>
|
||||
<form onSubmit={handleSmtpSubmit}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputText
|
||||
label="Serveur SMTP"
|
||||
value={smtpServer}
|
||||
onChange={handleSmtpServerChange}
|
||||
<div className="p-6">
|
||||
<h1 className="font-headline text-2xl font-bold text-gray-900 mb-6">
|
||||
Paramètres
|
||||
</h1>
|
||||
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-6">
|
||||
<h2 className="font-headline text-lg font-semibold text-gray-800 mb-4">
|
||||
Paramètres SMTP
|
||||
</h2>
|
||||
<form onSubmit={handleSmtpSubmit}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputText
|
||||
label="Serveur SMTP"
|
||||
value={smtpServer}
|
||||
onChange={handleSmtpServerChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Port SMTP"
|
||||
value={smtpPort}
|
||||
onChange={handleSmtpPortChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Utilisateur SMTP"
|
||||
value={smtpUser}
|
||||
onChange={handleSmtpUserChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Mot de passe SMTP"
|
||||
type="password"
|
||||
value={smtpPassword}
|
||||
onChange={handleSmtpPasswordChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 border-t pt-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<CheckBox
|
||||
item={{ id: 'useTls' }}
|
||||
formData={{ useTls }}
|
||||
handleChange={() => setUseTls((prev) => !prev)} // Inverser la valeur booléenne
|
||||
fieldName="useTls"
|
||||
itemLabelFunc={() => 'Utiliser TLS'}
|
||||
/>
|
||||
<InputText
|
||||
label="Port SMTP"
|
||||
value={smtpPort}
|
||||
onChange={handleSmtpPortChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Utilisateur SMTP"
|
||||
value={smtpUser}
|
||||
onChange={handleSmtpUserChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Mot de passe SMTP"
|
||||
type="password"
|
||||
value={smtpPassword}
|
||||
onChange={handleSmtpPasswordChange}
|
||||
<CheckBox
|
||||
item={{ id: 'useSsl' }}
|
||||
formData={{ useSsl }}
|
||||
handleChange={() => setUseSsl((prev) => !prev)} // Inverser la valeur booléenne
|
||||
fieldName="useSsl"
|
||||
itemLabelFunc={() => 'Utiliser SSL'}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 border-t pt-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<CheckBox
|
||||
item={{ id: 'useTls' }}
|
||||
formData={{ useTls }}
|
||||
handleChange={() => setUseTls((prev) => !prev)} // Inverser la valeur booléenne
|
||||
fieldName="useTls"
|
||||
itemLabelFunc={() => 'Utiliser TLS'}
|
||||
/>
|
||||
<CheckBox
|
||||
item={{ id: 'useSsl' }}
|
||||
formData={{ useSsl }}
|
||||
handleChange={() => setUseSsl((prev) => !prev)} // Inverser la valeur booléenne
|
||||
fieldName="useSsl"
|
||||
itemLabelFunc={() => 'Utiliser SSL'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
primary
|
||||
text="Mettre à jour"
|
||||
className="mt-6"
|
||||
></Button>
|
||||
</form>
|
||||
</TabContent>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
primary
|
||||
text="Mettre à jour"
|
||||
className="mt-6"
|
||||
></Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -204,7 +204,7 @@ export default function FormBuilderPage() {
|
||||
<ArrowLeft size={20} />
|
||||
Retour
|
||||
</button>
|
||||
<h1 className="text-lg font-headline font-semibold text-gray-800">
|
||||
<h1 className="font-headline text-lg font-headline font-semibold text-gray-800">
|
||||
{isEditing ? 'Modifier le formulaire' : 'Créer un formulaire personnalisé'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Users, Layers, CheckCircle, Clock, XCircle, ClipboardList, Plus } from 'lucide-react';
|
||||
import {
|
||||
Users,
|
||||
Layers,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
XCircle,
|
||||
ClipboardList,
|
||||
Plus,
|
||||
} from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import Popup from '@/components/Popup';
|
||||
import { fetchClasse, fetchSpecialities, fetchEvaluations, createEvaluation, updateEvaluation, deleteEvaluation, fetchStudentEvaluations, saveStudentEvaluations, deleteStudentEvaluation } from '@/app/actions/schoolAction';
|
||||
import {
|
||||
fetchClasse,
|
||||
fetchSpecialities,
|
||||
fetchEvaluations,
|
||||
createEvaluation,
|
||||
updateEvaluation,
|
||||
deleteEvaluation,
|
||||
fetchStudentEvaluations,
|
||||
saveStudentEvaluations,
|
||||
deleteStudentEvaluation,
|
||||
} from '@/app/actions/schoolAction';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import logger from '@/utils/logger';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
@ -17,7 +35,11 @@ import {
|
||||
editAbsences,
|
||||
deleteAbsences,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { EvaluationForm, EvaluationList, EvaluationGradeTable } from '@/components/Evaluation';
|
||||
import {
|
||||
EvaluationForm,
|
||||
EvaluationList,
|
||||
EvaluationGradeTable,
|
||||
} from '@/components/Evaluation';
|
||||
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
@ -53,14 +75,15 @@ export default function Page() {
|
||||
const [editingEvaluation, setEditingEvaluation] = useState(null);
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = useEstablishment();
|
||||
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } =
|
||||
useEstablishment();
|
||||
|
||||
// Périodes selon la fréquence d'évaluation
|
||||
const getPeriods = () => {
|
||||
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
||||
const nextYear = (year + 1).toString();
|
||||
const schoolYear = `${year}-${nextYear}`;
|
||||
|
||||
|
||||
if (selectedEstablishmentEvaluationFrequency === 1) {
|
||||
return [
|
||||
{ label: 'Trimestre 1', value: `T1_${schoolYear}` },
|
||||
@ -212,16 +235,25 @@ export default function Page() {
|
||||
const currentSchoolYear = `${year}-${year + 1}`;
|
||||
fetchSpecialities(selectedEstablishmentId, currentSchoolYear)
|
||||
.then((data) => setSpecialities(data))
|
||||
.catch((error) => logger.error('Erreur lors du chargement des matières:', error));
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors du chargement des matières:', error)
|
||||
);
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
// Load evaluations when tab is active and period is selected
|
||||
useEffect(() => {
|
||||
if (activeTab === 'evaluations' && selectedEstablishmentId && schoolClassId && selectedPeriod) {
|
||||
if (
|
||||
activeTab === 'evaluations' &&
|
||||
selectedEstablishmentId &&
|
||||
schoolClassId &&
|
||||
selectedPeriod
|
||||
) {
|
||||
fetchEvaluations(selectedEstablishmentId, schoolClassId, selectedPeriod)
|
||||
.then((data) => setEvaluations(data))
|
||||
.catch((error) => logger.error('Erreur lors du chargement des évaluations:', error));
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors du chargement des évaluations:', error)
|
||||
);
|
||||
}
|
||||
}, [activeTab, selectedEstablishmentId, schoolClassId, selectedPeriod]);
|
||||
|
||||
@ -230,7 +262,9 @@ export default function Page() {
|
||||
if (selectedEvaluation && schoolClassId) {
|
||||
fetchStudentEvaluations(null, selectedEvaluation.id, null, schoolClassId)
|
||||
.then((data) => setStudentEvaluations(data))
|
||||
.catch((error) => logger.error('Erreur lors du chargement des notes:', error));
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors du chargement des notes:', error)
|
||||
);
|
||||
}
|
||||
}, [selectedEvaluation, schoolClassId]);
|
||||
|
||||
@ -241,7 +275,11 @@ export default function Page() {
|
||||
showNotification('Évaluation créée avec succès', 'success', 'Succès');
|
||||
setShowEvaluationForm(false);
|
||||
// Reload evaluations
|
||||
const updatedEvaluations = await fetchEvaluations(selectedEstablishmentId, schoolClassId, selectedPeriod);
|
||||
const updatedEvaluations = await fetchEvaluations(
|
||||
selectedEstablishmentId,
|
||||
schoolClassId,
|
||||
selectedPeriod
|
||||
);
|
||||
setEvaluations(updatedEvaluations);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la création:', error);
|
||||
@ -261,7 +299,11 @@ export default function Page() {
|
||||
setShowEvaluationForm(false);
|
||||
setEditingEvaluation(null);
|
||||
// Reload evaluations
|
||||
const updatedEvaluations = await fetchEvaluations(selectedEstablishmentId, schoolClassId, selectedPeriod);
|
||||
const updatedEvaluations = await fetchEvaluations(
|
||||
selectedEstablishmentId,
|
||||
schoolClassId,
|
||||
selectedPeriod
|
||||
);
|
||||
setEvaluations(updatedEvaluations);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la modification:', error);
|
||||
@ -272,20 +314,31 @@ export default function Page() {
|
||||
const handleDeleteEvaluation = async (evaluationId) => {
|
||||
await deleteEvaluation(evaluationId, csrfToken);
|
||||
// Reload evaluations
|
||||
const updatedEvaluations = await fetchEvaluations(selectedEstablishmentId, schoolClassId, selectedPeriod);
|
||||
const updatedEvaluations = await fetchEvaluations(
|
||||
selectedEstablishmentId,
|
||||
schoolClassId,
|
||||
selectedPeriod
|
||||
);
|
||||
setEvaluations(updatedEvaluations);
|
||||
};
|
||||
|
||||
const handleSaveGrades = async (gradesData) => {
|
||||
await saveStudentEvaluations(gradesData, csrfToken);
|
||||
// Reload student evaluations
|
||||
const updatedStudentEvaluations = await fetchStudentEvaluations(null, selectedEvaluation.id, null, schoolClassId);
|
||||
const updatedStudentEvaluations = await fetchStudentEvaluations(
|
||||
null,
|
||||
selectedEvaluation.id,
|
||||
null,
|
||||
schoolClassId
|
||||
);
|
||||
setStudentEvaluations(updatedStudentEvaluations);
|
||||
};
|
||||
|
||||
const handleDeleteGrade = async (studentEvalId) => {
|
||||
await deleteStudentEvaluation(studentEvalId, csrfToken);
|
||||
setStudentEvaluations((prev) => prev.filter((se) => se.id !== studentEvalId));
|
||||
setStudentEvaluations((prev) =>
|
||||
prev.filter((se) => se.id !== studentEvalId)
|
||||
);
|
||||
};
|
||||
|
||||
const handleLevelClick = (label) => {
|
||||
@ -543,14 +596,16 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<h1 className="text-2xl font-bold">{classe?.atmosphere_name}</h1>
|
||||
<h1 className="font-headline text-2xl font-bold">
|
||||
{classe?.atmosphere_name}
|
||||
</h1>
|
||||
|
||||
{/* Section Niveaux et Enseignants */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Section Niveaux */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-semibold mb-4 flex items-center">
|
||||
<Layers className="w-6 h-6 mr-2" />
|
||||
<div className="bg-white p-4 rounded-md shadow-sm">
|
||||
<h2 className="font-headline text-xl font-semibold mb-4 flex items-center">
|
||||
<Layers className="w-6 h-6 mr-2 text-primary" />
|
||||
Niveaux
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
@ -559,24 +614,24 @@ export default function Page() {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{classe?.levels?.length > 0 ? (
|
||||
getNiveauxLabels(classe.levels).map((label, index) => (
|
||||
<span
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleLevelClick(label)} // Gérer le clic sur un niveau
|
||||
className={`px-4 py-2 rounded-full cursor-pointer border transition-all duration-200 ${
|
||||
onClick={() => handleLevelClick(label)}
|
||||
className={`px-4 py-2 rounded font-label font-medium cursor-pointer border transition-colors min-h-[44px] ${
|
||||
selectedLevels.includes(label)
|
||||
? 'bg-emerald-200 text-emerald-800 border-emerald-300 shadow-md'
|
||||
? 'bg-primary/20 text-secondary border-primary/30 shadow-sm'
|
||||
: 'bg-gray-200 text-gray-800 border-gray-300 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{selectedLevels.includes(label) ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
||||
<CheckCircle className="w-4 h-4 text-primary" />
|
||||
{label}
|
||||
</span>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-gray-500">Aucun niveau associé</span>
|
||||
@ -585,9 +640,9 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
{/* Section Enseignants */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-semibold mb-4 flex items-center">
|
||||
<Users className="w-6 h-6 mr-2" />
|
||||
<div className="bg-white p-4 rounded-md shadow-sm">
|
||||
<h2 className="font-headline text-xl font-semibold mb-4 flex items-center">
|
||||
<Users className="w-6 h-6 mr-2 text-primary" />
|
||||
Enseignants
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">Liste des enseignants</p>
|
||||
@ -595,7 +650,7 @@ export default function Page() {
|
||||
{classe?.teachers_details?.map((teacher) => (
|
||||
<span
|
||||
key={teacher.id}
|
||||
className="px-3 py-1 bg-emerald-200 rounded-full text-emerald-800"
|
||||
className="px-3 py-1 bg-primary/20 rounded text-secondary font-label text-sm"
|
||||
>
|
||||
{teacher.last_name} {teacher.first_name}
|
||||
</span>
|
||||
@ -605,14 +660,14 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div className="bg-white rounded-md shadow-sm overflow-hidden">
|
||||
<div className="flex border-b border-gray-200">
|
||||
<button
|
||||
onClick={() => setActiveTab('attendance')}
|
||||
className={`flex-1 py-3 px-4 text-center font-medium transition-colors ${
|
||||
className={`flex-1 py-3 px-4 text-center font-label font-medium transition-colors min-h-[44px] ${
|
||||
activeTab === 'attendance'
|
||||
? 'text-emerald-600 border-b-2 border-emerald-600 bg-emerald-50'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
|
||||
? 'text-primary border-b-2 border-primary bg-primary/5'
|
||||
: 'text-gray-500 hover:text-secondary hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
@ -622,10 +677,10 @@ export default function Page() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('evaluations')}
|
||||
className={`flex-1 py-3 px-4 text-center font-medium transition-colors ${
|
||||
className={`flex-1 py-3 px-4 text-center font-label font-medium transition-colors min-h-[44px] ${
|
||||
activeTab === 'evaluations'
|
||||
? 'text-emerald-600 border-b-2 border-emerald-600 bg-emerald-50'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
|
||||
? 'text-primary border-b-2 border-primary bg-primary/5'
|
||||
: 'text-gray-500 hover:text-secondary hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
@ -640,14 +695,14 @@ export default function Page() {
|
||||
{activeTab === 'attendance' && (
|
||||
<>
|
||||
{/* Affichage de la date du jour */}
|
||||
<div className="flex justify-between items-center mb-4 bg-white p-4 rounded-lg shadow-md">
|
||||
<div className="flex justify-between items-center mb-4 bg-white p-4 rounded-md shadow-sm">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-emerald-100 text-emerald-600 rounded-full">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-primary/10 text-primary rounded">
|
||||
<Clock className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
<h2 className="font-headline text-lg font-semibold text-gray-800">
|
||||
Appel du jour :{' '}
|
||||
<span className="ml-2 text-emerald-600">{today}</span>
|
||||
<span className="ml-2 text-primary">{today}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
@ -656,14 +711,14 @@ export default function Page() {
|
||||
text="Faire l'appel"
|
||||
onClick={handleToggleAttendanceMode}
|
||||
primary
|
||||
className="px-4 py-2 bg-emerald-500 text-white rounded-lg shadow hover:bg-emerald-600 transition-all"
|
||||
className="px-4 py-2 bg-primary text-white font-label font-medium rounded shadow-sm hover:bg-secondary transition-colors min-h-[44px]"
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
text="Valider l'appel"
|
||||
onClick={handleValidateAttendance}
|
||||
primary
|
||||
className="px-4 py-2 bg-emerald-500 text-white rounded-lg shadow hover:bg-emerald-600 transition-all"
|
||||
className="px-4 py-2 bg-primary text-white font-label font-medium rounded shadow-sm hover:bg-secondary transition-colors min-h-[44px]"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -681,218 +736,224 @@ export default function Page() {
|
||||
name: 'Prénom',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{row.first_name}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Niveau',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{getNiveauLabel(row.level)}</div>
|
||||
),
|
||||
},
|
||||
...(isEditingAttendance
|
||||
? [
|
||||
{
|
||||
name: "Gestion de l'appel",
|
||||
transform: (row) => (
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
{/* Présence */}
|
||||
<div className="flex items-center gap-2">
|
||||
{attendance[row.id] ? (
|
||||
<>
|
||||
<CheckBox
|
||||
item={{ id: row.id }}
|
||||
formData={{
|
||||
attendance: attendance[row.id] ? [row.id] : [],
|
||||
}}
|
||||
handleChange={() =>
|
||||
handleAttendanceChange(row.id)
|
||||
}
|
||||
fieldName="attendance"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Présent
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Icône croix pour remettre l'élève en présent */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAttendanceChange(row.id)}
|
||||
className="text-red-500 hover:text-red-700 transition"
|
||||
title="Annuler l'absence"
|
||||
>
|
||||
<XCircle className="w-6 h-6" />
|
||||
</button>
|
||||
<span className="text-sm font-medium text-red-600">
|
||||
Effacer l'absence
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Détails absence/retard */}
|
||||
{!attendance[row.id] && (
|
||||
<div className="w-full bg-emerald-50 border border-emerald-100 rounded-lg p-3 mt-2 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Clock className="w-4 h-4 text-emerald-500" />
|
||||
<span className="font-semibold text-emerald-700 text-sm">
|
||||
Motif d'absence
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Niveau',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{getNiveauLabel(row.level)}</div>
|
||||
),
|
||||
},
|
||||
...(isEditingAttendance
|
||||
? [
|
||||
{
|
||||
name: "Gestion de l'appel",
|
||||
transform: (row) => (
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
{/* Présence */}
|
||||
<div className="flex items-center gap-2">
|
||||
{attendance[row.id] ? (
|
||||
<>
|
||||
<CheckBox
|
||||
item={{ id: row.id }}
|
||||
formData={{
|
||||
attendance: attendance[row.id]
|
||||
? [row.id]
|
||||
: [],
|
||||
}}
|
||||
handleChange={() =>
|
||||
handleAttendanceChange(row.id)
|
||||
}
|
||||
fieldName="attendance"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Présent
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Icône croix pour remettre l'élève en présent */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAttendanceChange(row.id)}
|
||||
className="text-red-500 hover:text-red-700 transition"
|
||||
title="Annuler l'absence"
|
||||
>
|
||||
<XCircle className="w-6 h-6" />
|
||||
</button>
|
||||
<span className="text-sm font-medium text-red-600">
|
||||
Effacer l'absence
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 items-center">
|
||||
{/* Select Absence/Retard */}
|
||||
<SelectChoice
|
||||
name={`type-${row.id}`}
|
||||
label=""
|
||||
placeHolder="Type"
|
||||
selected={formAbsences[row.id]?.type || ''}
|
||||
callback={(e) =>
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
type: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
choices={[
|
||||
{ value: 'absence', label: 'Absence' },
|
||||
{ value: 'retard', label: 'Retard' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Select Moment */}
|
||||
<SelectChoice
|
||||
name={`moment-${row.id}`}
|
||||
label=""
|
||||
placeHolder="Durée"
|
||||
selected={formAbsences[row.id]?.moment || ''}
|
||||
callback={(e) =>
|
||||
setFormAbsences((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,
|
||||
})
|
||||
)}
|
||||
/>
|
||||
{/* Détails absence/retard */}
|
||||
{!attendance[row.id] && (
|
||||
<div className="w-full bg-primary/5 border border-primary/10 rounded-lg p-3 mt-2 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Clock className="w-4 h-4 text-primary" />
|
||||
<span className="font-semibold text-secondary text-sm">
|
||||
Motif d'absence
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 items-center">
|
||||
{/* Select Absence/Retard */}
|
||||
<SelectChoice
|
||||
name={`type-${row.id}`}
|
||||
label=""
|
||||
placeHolder="Type"
|
||||
selected={formAbsences[row.id]?.type || ''}
|
||||
callback={(e) =>
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
type: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
choices={[
|
||||
{ value: 'absence', label: 'Absence' },
|
||||
{ value: 'retard', label: 'Retard' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 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,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{/* Select Moment */}
|
||||
<SelectChoice
|
||||
name={`moment-${row.id}`}
|
||||
label=""
|
||||
placeHolder="Durée"
|
||||
selected={formAbsences[row.id]?.moment || ''}
|
||||
callback={(e) =>
|
||||
setFormAbsences((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,
|
||||
})
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Checkbox Justifié */}
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<CheckBox
|
||||
item={{ id: `justified-${row.id}` }}
|
||||
formData={{
|
||||
justified: !!formAbsences[row.id]?.justified,
|
||||
}}
|
||||
handleChange={() =>
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
justified: !prev[row.id]?.justified,
|
||||
},
|
||||
}))
|
||||
}
|
||||
fieldName="justified"
|
||||
itemLabelFunc={() => 'Justifié'}
|
||||
/>
|
||||
{/* 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
|
||||
item={{ id: `justified-${row.id}` }}
|
||||
formData={{
|
||||
justified:
|
||||
!!formAbsences[row.id]?.justified,
|
||||
}}
|
||||
handleChange={() =>
|
||||
setFormAbsences((prev) => ({
|
||||
...prev,
|
||||
[row.id]: {
|
||||
...prev[row.id],
|
||||
justified: !prev[row.id]?.justified,
|
||||
},
|
||||
}))
|
||||
}
|
||||
fieldName="justified"
|
||||
itemLabelFunc={() => 'Justifié'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Statut',
|
||||
transform: (row) => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const absence =
|
||||
formAbsences[row.id] ||
|
||||
Object.values(fetchedAbsences).find(
|
||||
(absence) =>
|
||||
absence.student === row.id && absence.day === today
|
||||
);
|
||||
),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Statut',
|
||||
transform: (row) => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const absence =
|
||||
formAbsences[row.id] ||
|
||||
Object.values(fetchedAbsences).find(
|
||||
(absence) =>
|
||||
absence.student === row.id &&
|
||||
absence.day === today
|
||||
);
|
||||
|
||||
if (!absence) {
|
||||
return (
|
||||
<div className="text-center text-green-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Présent
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!absence) {
|
||||
return (
|
||||
<div className="text-center text-green-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Présent
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (absence.reason) {
|
||||
case AbsenceReason.JUSTIFIED_LATE.value:
|
||||
return (
|
||||
<div className="text-center text-yellow-500 flex justify-center items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
Retard justifié
|
||||
</div>
|
||||
);
|
||||
case AbsenceReason.UNJUSTIFIED_LATE.value:
|
||||
return (
|
||||
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
Retard non justifié
|
||||
</div>
|
||||
);
|
||||
case AbsenceReason.JUSTIFIED_ABSENCE.value:
|
||||
return (
|
||||
<div className="text-center text-blue-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Absence justifiée
|
||||
</div>
|
||||
);
|
||||
case AbsenceReason.UNJUSTIFIED_ABSENCE.value:
|
||||
return (
|
||||
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Absence non justifiée
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="text-center text-gray-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Statut inconnu
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]),
|
||||
]}
|
||||
data={filteredStudents} // Utiliser les élèves filtrés
|
||||
/>
|
||||
switch (absence.reason) {
|
||||
case AbsenceReason.JUSTIFIED_LATE.value:
|
||||
return (
|
||||
<div className="text-center text-yellow-500 flex justify-center items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
Retard justifié
|
||||
</div>
|
||||
);
|
||||
case AbsenceReason.UNJUSTIFIED_LATE.value:
|
||||
return (
|
||||
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
Retard non justifié
|
||||
</div>
|
||||
);
|
||||
case AbsenceReason.JUSTIFIED_ABSENCE.value:
|
||||
return (
|
||||
<div className="text-center text-blue-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Absence justifiée
|
||||
</div>
|
||||
);
|
||||
case AbsenceReason.UNJUSTIFIED_ABSENCE.value:
|
||||
return (
|
||||
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Absence non justifiée
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="text-center text-gray-500 flex justify-center items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
Statut inconnu
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]),
|
||||
]}
|
||||
data={filteredStudents} // Utiliser les élèves filtrés
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -900,11 +961,11 @@ export default function Page() {
|
||||
{activeTab === 'evaluations' && (
|
||||
<div className="space-y-4">
|
||||
{/* Header avec sélecteur de période et bouton d'ajout */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<div className="bg-white p-4 rounded-md shadow-sm border border-gray-200">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<ClipboardList className="w-6 h-6 text-emerald-600" />
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
<ClipboardList className="w-6 h-6 text-primary" />
|
||||
<h2 className="font-headline text-lg font-semibold text-gray-800">
|
||||
Évaluations de la classe
|
||||
</h2>
|
||||
</div>
|
||||
@ -936,7 +997,11 @@ export default function Page() {
|
||||
schoolClassId={parseInt(schoolClassId)}
|
||||
establishmentId={selectedEstablishmentId}
|
||||
initialValues={editingEvaluation}
|
||||
onSubmit={editingEvaluation ? handleUpdateEvaluation : handleCreateEvaluation}
|
||||
onSubmit={
|
||||
editingEvaluation
|
||||
? handleUpdateEvaluation
|
||||
: handleCreateEvaluation
|
||||
}
|
||||
onCancel={() => {
|
||||
setShowEvaluationForm(false);
|
||||
setEditingEvaluation(null);
|
||||
@ -945,12 +1010,14 @@ export default function Page() {
|
||||
)}
|
||||
|
||||
{/* Liste des évaluations */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<div className="bg-white p-4 rounded-md shadow-sm border border-gray-200">
|
||||
<EvaluationList
|
||||
evaluations={evaluations}
|
||||
onDelete={handleDeleteEvaluation}
|
||||
onEdit={handleEditEvaluation}
|
||||
onGradeStudents={(evaluation) => setSelectedEvaluation(evaluation)}
|
||||
onGradeStudents={(evaluation) =>
|
||||
setSelectedEvaluation(evaluation)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -746,11 +746,11 @@ export default function CreateSubscriptionPage() {
|
||||
return (
|
||||
<div className="mx-auto p-12 space-y-12">
|
||||
{registerFormID ? (
|
||||
<h1 className="text-2xl font-bold">
|
||||
<h1 className="font-headline text-2xl font-bold">
|
||||
Modifier un dossier d'inscription
|
||||
</h1>
|
||||
) : (
|
||||
<h1 className="text-2xl font-bold">
|
||||
<h1 className="font-headline text-2xl font-bold">
|
||||
Créer un dossier d'inscription
|
||||
</h1>
|
||||
)}
|
||||
@ -953,7 +953,7 @@ export default function CreateSubscriptionPage() {
|
||||
}}
|
||||
rowClassName={(row) =>
|
||||
selectedStudent && selectedStudent.id === row.id
|
||||
? 'bg-emerald-600 text-white'
|
||||
? 'bg-primary text-white'
|
||||
: ''
|
||||
}
|
||||
selectedRows={selectedStudent ? [selectedStudent.id] : []} // Assurez-vous que selectedRows est un tableau
|
||||
@ -965,7 +965,7 @@ export default function CreateSubscriptionPage() {
|
||||
|
||||
{selectedStudent && (
|
||||
<div className="mt-4">
|
||||
<h3 className="font-bold">
|
||||
<h3 className="font-headline font-bold">
|
||||
Responsables associés à {selectedStudent.last_name}{' '}
|
||||
{selectedStudent.first_name} :
|
||||
</h3>
|
||||
@ -1026,7 +1026,7 @@ export default function CreateSubscriptionPage() {
|
||||
</div>
|
||||
|
||||
{/* Montant total */}
|
||||
<div className="flex items-center justify-between bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-300 mt-4">
|
||||
<div className="flex items-center justify-between bg-gray-50 p-4 rounded-md shadow-sm border border-gray-300 mt-4">
|
||||
<span className="text-sm font-medium text-gray-600">
|
||||
Montant total des frais d'inscription :
|
||||
</span>
|
||||
@ -1073,7 +1073,7 @@ export default function CreateSubscriptionPage() {
|
||||
</div>
|
||||
|
||||
{/* Montant total */}
|
||||
<div className="flex items-center justify-between bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-300 mt-4">
|
||||
<div className="flex items-center justify-between bg-gray-50 p-4 rounded-md shadow-sm border border-gray-300 mt-4">
|
||||
<span className="text-sm font-medium text-gray-600">
|
||||
Montant total des frais de scolarité :
|
||||
</span>
|
||||
@ -1136,7 +1136,7 @@ export default function CreateSubscriptionPage() {
|
||||
className={`px-6 py-2 rounded-md shadow ${
|
||||
isSubmitDisabled()
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
: 'bg-primary text-white hover:bg-primary'
|
||||
}`}
|
||||
primary
|
||||
disabled={isSubmitDisabled()}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import Tab from '@/components/Tab';
|
||||
import SidebarTabs from '@/components/SidebarTabs';
|
||||
import Textarea from '@/components/Textarea';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
@ -55,6 +55,7 @@ import {
|
||||
HISTORICAL_FILTER,
|
||||
} from '@/utils/constants';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import { exportToCSV } from '@/utils/exportCSV';
|
||||
|
||||
@ -167,43 +168,86 @@ export default function Page({ params: { locale } }) {
|
||||
|
||||
// Export CSV
|
||||
const handleExportCSV = () => {
|
||||
const dataToExport = activeTab === CURRENT_YEAR_FILTER
|
||||
? registrationFormsDataCurrentYear
|
||||
: activeTab === NEXT_YEAR_FILTER
|
||||
? registrationFormsDataNextYear
|
||||
: registrationFormsDataHistorical;
|
||||
const dataToExport =
|
||||
activeTab === CURRENT_YEAR_FILTER
|
||||
? registrationFormsDataCurrentYear
|
||||
: activeTab === NEXT_YEAR_FILTER
|
||||
? registrationFormsDataNextYear
|
||||
: registrationFormsDataHistorical;
|
||||
|
||||
const exportColumns = [
|
||||
{ key: 'student', label: 'Nom', transform: (value) => value?.last_name || '' },
|
||||
{ key: 'student', label: 'Prénom', transform: (value) => value?.first_name || '' },
|
||||
{ key: 'student', label: 'Date de naissance', transform: (value) => value?.birth_date || '' },
|
||||
{ key: 'student', label: 'Email contact', transform: (value) => value?.guardians?.[0]?.associated_profile_email || '' },
|
||||
{ key: 'student', label: 'Téléphone contact', transform: (value) => value?.guardians?.[0]?.phone || '' },
|
||||
{ key: 'student', label: 'Nom responsable 1', transform: (value) => value?.guardians?.[0]?.last_name || '' },
|
||||
{ key: 'student', label: 'Prénom responsable 1', transform: (value) => value?.guardians?.[0]?.first_name || '' },
|
||||
{ key: 'student', label: 'Nom responsable 2', transform: (value) => value?.guardians?.[1]?.last_name || '' },
|
||||
{ key: 'student', label: 'Prénom responsable 2', transform: (value) => value?.guardians?.[1]?.first_name || '' },
|
||||
{
|
||||
key: 'student',
|
||||
label: 'Nom',
|
||||
transform: (value) => value?.last_name || '',
|
||||
},
|
||||
{
|
||||
key: 'student',
|
||||
label: 'Prénom',
|
||||
transform: (value) => value?.first_name || '',
|
||||
},
|
||||
{
|
||||
key: 'student',
|
||||
label: 'Date de naissance',
|
||||
transform: (value) => value?.birth_date || '',
|
||||
},
|
||||
{
|
||||
key: 'student',
|
||||
label: 'Email contact',
|
||||
transform: (value) =>
|
||||
value?.guardians?.[0]?.associated_profile_email || '',
|
||||
},
|
||||
{
|
||||
key: 'student',
|
||||
label: 'Téléphone contact',
|
||||
transform: (value) => value?.guardians?.[0]?.phone || '',
|
||||
},
|
||||
{
|
||||
key: 'student',
|
||||
label: 'Nom responsable 1',
|
||||
transform: (value) => value?.guardians?.[0]?.last_name || '',
|
||||
},
|
||||
{
|
||||
key: 'student',
|
||||
label: 'Prénom responsable 1',
|
||||
transform: (value) => value?.guardians?.[0]?.first_name || '',
|
||||
},
|
||||
{
|
||||
key: 'student',
|
||||
label: 'Nom responsable 2',
|
||||
transform: (value) => value?.guardians?.[1]?.last_name || '',
|
||||
},
|
||||
{
|
||||
key: 'student',
|
||||
label: 'Prénom responsable 2',
|
||||
transform: (value) => value?.guardians?.[1]?.first_name || '',
|
||||
},
|
||||
{ key: 'school_year', label: 'Année scolaire' },
|
||||
{ key: 'status', label: 'Statut', transform: (value) => {
|
||||
const statusMap = {
|
||||
0: 'En attente',
|
||||
1: 'En cours',
|
||||
2: 'Envoyé',
|
||||
3: 'À relancer',
|
||||
4: 'À valider',
|
||||
5: 'Validé',
|
||||
6: 'Archivé',
|
||||
};
|
||||
return statusMap[value] || value;
|
||||
}},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Statut',
|
||||
transform: (value) => {
|
||||
const statusMap = {
|
||||
0: 'En attente',
|
||||
1: 'En cours',
|
||||
2: 'Envoyé',
|
||||
3: 'À relancer',
|
||||
4: 'À valider',
|
||||
5: 'Validé',
|
||||
6: 'Archivé',
|
||||
};
|
||||
return statusMap[value] || value;
|
||||
},
|
||||
},
|
||||
{ key: 'formatted_last_update', label: 'Dernière mise à jour' },
|
||||
];
|
||||
|
||||
const yearLabel = activeTab === CURRENT_YEAR_FILTER
|
||||
? currentSchoolYear
|
||||
: activeTab === NEXT_YEAR_FILTER
|
||||
? nextSchoolYear
|
||||
: 'historique';
|
||||
const yearLabel =
|
||||
activeTab === CURRENT_YEAR_FILTER
|
||||
? currentSchoolYear
|
||||
: activeTab === NEXT_YEAR_FILTER
|
||||
? nextSchoolYear
|
||||
: 'historique';
|
||||
const filename = `inscriptions_${yearLabel}_${new Date().toISOString().split('T')[0]}`;
|
||||
exportToCSV(dataToExport, exportColumns, filename);
|
||||
};
|
||||
@ -506,7 +550,7 @@ export default function Page({ params: { locale } }) {
|
||||
{
|
||||
icon: (
|
||||
<span title="Envoyer le dossier">
|
||||
<Send className="w-5 h-5 text-green-500 hover:text-green-700" />
|
||||
<Send className="w-5 h-5 text-primary hover:text-secondary" />
|
||||
</span>
|
||||
),
|
||||
onClick: () =>
|
||||
@ -536,7 +580,7 @@ export default function Page({ params: { locale } }) {
|
||||
{
|
||||
icon: (
|
||||
<span title="Renvoyer le dossier">
|
||||
<Send className="w-5 h-5 text-green-500 hover:text-green-700" />
|
||||
<Send className="w-5 h-5 text-primary hover:text-secondary" />
|
||||
</span>
|
||||
),
|
||||
onClick: () =>
|
||||
@ -575,7 +619,7 @@ export default function Page({ params: { locale } }) {
|
||||
{
|
||||
icon: (
|
||||
<span title="Valider le dossier">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 hover:text-green-700" />
|
||||
<CheckCircle className="w-5 h-5 text-primary hover:text-secondary" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => {
|
||||
@ -690,7 +734,7 @@ export default function Page({ params: { locale } }) {
|
||||
{
|
||||
icon: (
|
||||
<span title="Uploader un mandat SEPA">
|
||||
<Upload className="w-5 h-5 text-emerald-500 hover:text-emerald-700" />
|
||||
<Upload className="w-5 h-5 text-primary hover:text-secondary" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => openSepaUploadModal(row),
|
||||
@ -800,161 +844,139 @@ export default function Page({ params: { locale } }) {
|
||||
},
|
||||
];
|
||||
|
||||
let emptyMessage;
|
||||
if (activeTab === CURRENT_YEAR_FILTER && searchTerm === '') {
|
||||
emptyMessage = (
|
||||
<AlertMessage
|
||||
type="warning"
|
||||
title="Aucun dossier d'inscription pour l'année en cours"
|
||||
message="Veuillez procéder à la création d'un nouveau dossier d'inscription pour l'année scolaire en cours."
|
||||
const getEmptyMessageForTab = (tabFilter) => {
|
||||
if (searchTerm !== '') {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Search}
|
||||
title="Aucun dossier trouvé"
|
||||
description="Modifiez votre recherche pour trouver un dossier d'inscription."
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (tabFilter === CURRENT_YEAR_FILTER) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={FileText}
|
||||
title="Aucun dossier d'inscription pour l'année en cours"
|
||||
description="Commencez par créer un dossier d'inscription pour l'année scolaire en cours."
|
||||
actionLabel="Créer un dossier"
|
||||
actionIcon={Plus}
|
||||
onAction={() => router.push(FE_ADMIN_SUBSCRIPTIONS_CREATE_URL)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (tabFilter === NEXT_YEAR_FILTER) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={FileText}
|
||||
title="Aucun dossier pour l'année prochaine"
|
||||
description="Aucun dossier n'a encore été créé pour la prochaine année scolaire."
|
||||
actionLabel="Créer un dossier"
|
||||
actionIcon={Plus}
|
||||
onAction={() => router.push(FE_ADMIN_SUBSCRIPTIONS_CREATE_URL)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Archive}
|
||||
title="Aucun dossier archivé"
|
||||
description="Aucun dossier archivé n'est disponible pour les années précédentes."
|
||||
/>
|
||||
);
|
||||
} else if (activeTab === NEXT_YEAR_FILTER && searchTerm === '') {
|
||||
emptyMessage = (
|
||||
<AlertMessage
|
||||
type="info"
|
||||
title="Aucun dossier d'inscription pour l'année prochaine"
|
||||
message="Aucun dossier n'a encore été créé pour la prochaine année scolaire."
|
||||
};
|
||||
|
||||
const renderTabContent = (data, currentPage, totalPages, tabFilter) => (
|
||||
<div className="p-4">
|
||||
<div className="flex justify-between items-center mb-4 w-full">
|
||||
<div className="relative flex-grow">
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
||||
size={20}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('searchStudent')}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-secondary bg-primary/10 rounded hover:bg-primary/20 transition-colors"
|
||||
title="Exporter en CSV"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Exporter
|
||||
</button>
|
||||
{profileRole !== 0 && (
|
||||
<button
|
||||
onClick={() => router.push(FE_ADMIN_SUBSCRIPTIONS_CREATE_URL)}
|
||||
className="flex items-center bg-primary text-white p-2 rounded-full shadow hover:bg-secondary transition duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<Table
|
||||
key={`${tabFilter}-${currentPage}-${searchTerm}`}
|
||||
data={data}
|
||||
columns={columns}
|
||||
itemsPerPage={itemsPerPage}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
emptyMessage={getEmptyMessageForTab(tabFilter)}
|
||||
/>
|
||||
);
|
||||
} else if (activeTab === HISTORICAL_FILTER && searchTerm === '') {
|
||||
emptyMessage = (
|
||||
<AlertMessage
|
||||
type="info"
|
||||
title="Aucun dossier d'inscription historique"
|
||||
message="Aucun dossier archivé n'est disponible pour les années précédentes."
|
||||
/>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<div className="flex items-center gap-8">
|
||||
{/* Tab pour l'année scolaire en cours */}
|
||||
<Tab
|
||||
text={
|
||||
<>
|
||||
{currentSchoolYear}
|
||||
<span className="ml-2 text-sm text-gray-400">
|
||||
({totalCurrentYear})
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
active={activeTab === CURRENT_YEAR_FILTER}
|
||||
onClick={() => setActiveTab(CURRENT_YEAR_FILTER)}
|
||||
/>
|
||||
|
||||
{/* Tab pour l'année scolaire prochaine */}
|
||||
<Tab
|
||||
text={
|
||||
<>
|
||||
{nextSchoolYear}
|
||||
<span className="ml-2 text-sm text-gray-400">
|
||||
({totalNextYear})
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
active={activeTab === NEXT_YEAR_FILTER}
|
||||
onClick={() => setActiveTab(NEXT_YEAR_FILTER)}
|
||||
/>
|
||||
|
||||
{/* Tab pour l'historique */}
|
||||
<Tab
|
||||
text={
|
||||
<>
|
||||
{t('historical')}
|
||||
<span className="ml-2 text-sm text-gray-400">
|
||||
({totalHistorical})
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
active={activeTab === HISTORICAL_FILTER}
|
||||
onClick={() => setActiveTab(HISTORICAL_FILTER)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-gray-200 mb-6 w-full">
|
||||
{activeTab === CURRENT_YEAR_FILTER ||
|
||||
activeTab === NEXT_YEAR_FILTER ||
|
||||
activeTab === HISTORICAL_FILTER ? (
|
||||
<React.Fragment>
|
||||
<div className="flex justify-between items-center mb-4 w-full">
|
||||
<div className="relative flex-grow">
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
||||
size={20}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('searchStudent')}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-100 rounded-lg hover:bg-emerald-200 transition-colors"
|
||||
title="Exporter en CSV"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Exporter
|
||||
</button>
|
||||
{profileRole !== 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = `${FE_ADMIN_SUBSCRIPTIONS_CREATE_URL}`;
|
||||
router.push(url);
|
||||
}}
|
||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<Table
|
||||
key={`${currentSchoolYearPage}-${searchTerm}`}
|
||||
data={
|
||||
activeTab === CURRENT_YEAR_FILTER
|
||||
? registrationFormsDataCurrentYear
|
||||
: activeTab === NEXT_YEAR_FILTER
|
||||
? registrationFormsDataNextYear
|
||||
: registrationFormsDataHistorical
|
||||
}
|
||||
columns={columns}
|
||||
itemsPerPage={itemsPerPage}
|
||||
currentPage={
|
||||
activeTab === CURRENT_YEAR_FILTER
|
||||
? currentSchoolYearPage
|
||||
: activeTab === NEXT_YEAR_FILTER
|
||||
? currentSchoolNextYearPage
|
||||
: currentSchoolHistoricalYearPage
|
||||
}
|
||||
totalPages={
|
||||
activeTab === CURRENT_YEAR_FILTER
|
||||
? totalCurrentSchoolYearPages
|
||||
: activeTab === NEXT_YEAR_FILTER
|
||||
? totalNextSchoolYearPages
|
||||
: totalHistoricalPages
|
||||
}
|
||||
onPageChange={handlePageChange}
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="h-full flex flex-col">
|
||||
<SidebarTabs
|
||||
tabs={[
|
||||
{
|
||||
id: CURRENT_YEAR_FILTER,
|
||||
label: `${currentSchoolYear}${totalCurrentYear > 0 ? ` (${totalCurrentYear})` : ''}`,
|
||||
content: renderTabContent(
|
||||
registrationFormsDataCurrentYear,
|
||||
currentSchoolYearPage,
|
||||
totalCurrentSchoolYearPages,
|
||||
CURRENT_YEAR_FILTER
|
||||
),
|
||||
},
|
||||
{
|
||||
id: NEXT_YEAR_FILTER,
|
||||
label: `${nextSchoolYear}${totalNextYear > 0 ? ` (${totalNextYear})` : ''}`,
|
||||
content: renderTabContent(
|
||||
registrationFormsDataNextYear,
|
||||
currentSchoolNextYearPage,
|
||||
totalNextSchoolYearPages,
|
||||
NEXT_YEAR_FILTER
|
||||
),
|
||||
},
|
||||
{
|
||||
id: HISTORICAL_FILTER,
|
||||
label: `${t('historical')}${totalHistorical > 0 ? ` (${totalHistorical})` : ''}`,
|
||||
content: renderTabContent(
|
||||
registrationFormsDataHistorical,
|
||||
currentSchoolHistoricalYearPage,
|
||||
totalHistoricalPages,
|
||||
HISTORICAL_FILTER
|
||||
),
|
||||
},
|
||||
]}
|
||||
onTabChange={(newTab) => setActiveTab(newTab)}
|
||||
/>
|
||||
<Popup
|
||||
isOpen={confirmPopupVisible}
|
||||
message={confirmPopupMessage}
|
||||
|
||||
@ -10,10 +10,10 @@ export default function Home() {
|
||||
const t = useTranslations('homePage');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen py-2">
|
||||
<Logo className="mb-4" /> {/* Ajout du logo */}
|
||||
<h1 className="text-4xl font-bold mb-4">{t('welcomeParents')}</h1>
|
||||
<p className="text-lg mb-8">{t('pleaseLogin')}</p>
|
||||
<div className="flex flex-col items-center justify-center min-h-screen py-2 bg-neutral">
|
||||
<Logo className="mb-4" />
|
||||
<h1 className="font-headline text-4xl font-bold mb-4">{t('welcomeParents')}</h1>
|
||||
<p className="font-body text-lg mb-8">{t('pleaseLogin')}</p>
|
||||
<Button text={t('loginButton')} primary href="/users/login" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -100,7 +100,7 @@ export default function Layout({ children }) {
|
||||
|
||||
{/* Main container */}
|
||||
<div
|
||||
className={`absolute overflow-auto bg-gradient-to-br from-emerald-50 via-sky-50 to-emerald-100 top-14 md:top-0 bottom-16 left-0 md:left-64 right-0 ${!isMessagingPage ? 'p-4 md:p-8' : ''}`}
|
||||
className={`absolute overflow-auto bg-gradient-to-br from-primary/5 via-sky-50 to-primary/10 top-14 md:top-0 bottom-16 left-0 md:left-64 right-0 ${!isMessagingPage ? 'p-4 md:p-8' : ''}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -282,10 +282,10 @@ export default function ParentHomePage() {
|
||||
<>
|
||||
<div className="p-4 flex items-center border-b">
|
||||
<button
|
||||
className="text-emerald-600 hover:text-emerald-800 font-semibold flex items-center"
|
||||
className="text-primary hover:text-secondary font-label font-medium min-h-[44px] flex items-center transition-colors"
|
||||
onClick={() => setShowPlanning(false)}
|
||||
>
|
||||
← Retour
|
||||
<ArrowLeft className="w-4 h-4 mr-1" /> Retour
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
@ -309,7 +309,7 @@ export default function ParentHomePage() {
|
||||
title="Événements à venir"
|
||||
description="Prochains événements de l'établissement"
|
||||
/>
|
||||
<div className="bg-stone-50 p-4 rounded-lg shadow-sm border border-gray-100">
|
||||
<div className="bg-neutral p-4 rounded-md shadow-sm border border-gray-100">
|
||||
{upcomingEvents.slice(0, 3).map((event, index) => (
|
||||
<EventCard key={index} {...event} />
|
||||
))}
|
||||
@ -343,7 +343,7 @@ export default function ParentHomePage() {
|
||||
return (
|
||||
<div
|
||||
key={student.id}
|
||||
className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden"
|
||||
className="bg-white rounded-md shadow-sm border border-gray-200 overflow-hidden"
|
||||
>
|
||||
{/* En-tête de la carte (toujours visible) */}
|
||||
<div
|
||||
@ -373,7 +373,7 @@ export default function ParentHomePage() {
|
||||
|
||||
{/* Infos principales */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-gray-800">
|
||||
<h3 className="font-headline text-lg font-semibold text-gray-800">
|
||||
{student.last_name} {student.first_name}
|
||||
</h3>
|
||||
<div className="mt-1">
|
||||
@ -399,7 +399,7 @@ export default function ParentHomePage() {
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{child.status === 2 && (
|
||||
<button
|
||||
className="p-2 text-blue-500 hover:text-blue-700 hover:bg-blue-50 rounded-full"
|
||||
className="p-2 min-h-[44px] min-w-[44px] flex items-center justify-center text-blue-500 hover:text-blue-700 hover:bg-blue-50 rounded-full transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(student.id);
|
||||
@ -412,7 +412,7 @@ export default function ParentHomePage() {
|
||||
|
||||
{(child.status === 3 || child.status === 8 || child.status === 5 || child.status === 7) && (
|
||||
<button
|
||||
className="p-2 text-purple-500 hover:text-purple-700 hover:bg-purple-50 rounded-full"
|
||||
className="p-2 min-h-[44px] min-w-[44px] flex items-center justify-center text-purple-500 hover:text-purple-700 hover:bg-purple-50 rounded-full transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleView(student.id);
|
||||
@ -429,14 +429,14 @@ export default function ParentHomePage() {
|
||||
href={getSecureFileUrl(child.sepa_file)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 text-green-500 hover:text-green-700 hover:bg-green-50 rounded-full"
|
||||
className="p-2 min-h-[44px] min-w-[44px] flex items-center justify-center text-primary hover:text-secondary hover:bg-primary/5 rounded-full transition-colors"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title="Télécharger le mandat SEPA"
|
||||
>
|
||||
<Download className="h-5 w-5" />
|
||||
</a>
|
||||
<button
|
||||
className={`p-2 rounded-full ${
|
||||
className={`p-2 min-h-[44px] min-w-[44px] flex items-center justify-center rounded-full transition-colors ${
|
||||
uploadingStudentId === student.id && uploadState === 'on'
|
||||
? 'bg-blue-100 text-blue-600'
|
||||
: 'text-blue-500 hover:text-blue-700 hover:bg-blue-50'
|
||||
@ -455,7 +455,7 @@ export default function ParentHomePage() {
|
||||
{isEnrolled && (
|
||||
<>
|
||||
<button
|
||||
className="p-2 text-primary hover:text-secondary hover:bg-tertiary/10 rounded-full"
|
||||
className="p-2 min-h-[44px] min-w-[44px] flex items-center justify-center text-primary hover:text-secondary hover:bg-tertiary/10 rounded-full transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showClassPlanning(student);
|
||||
@ -484,7 +484,7 @@ export default function ParentHomePage() {
|
||||
onFileSelect={handleFileUpload}
|
||||
/>
|
||||
<button
|
||||
className={`mt-4 px-6 py-2 rounded-md ${
|
||||
className={`mt-4 px-4 py-2 rounded font-label font-medium min-h-[44px] transition-colors ${
|
||||
uploadedFile
|
||||
? 'bg-primary text-white hover:bg-secondary'
|
||||
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||
@ -499,10 +499,10 @@ export default function ParentHomePage() {
|
||||
|
||||
{/* Section détaillée pour les élèves inscrits (expanded) */}
|
||||
{isEnrolled && isExpanded && (
|
||||
<div className="border-t bg-stone-50 p-4 space-y-6">
|
||||
<div className="border-t bg-neutral p-4 space-y-6">
|
||||
|
||||
{/* Bloc période : compétences + notes */}
|
||||
<div className="bg-white rounded-lg border border-primary/20 p-4 space-y-4">
|
||||
<div className="bg-white rounded-md border border-primary/20 p-4 space-y-4">
|
||||
{/* Sélecteur de période */}
|
||||
<div className="flex items-center gap-3 pb-3 border-b border-gray-100">
|
||||
<div className="w-full sm:w-48">
|
||||
@ -540,10 +540,10 @@ export default function ParentHomePage() {
|
||||
const pctNotEvaluated = total ? Math.round((notEvaluated / total) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="border border-gray-100 rounded-lg p-4">
|
||||
<div className="border border-gray-100 rounded-md p-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Award className="w-5 h-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold text-gray-800">
|
||||
<h3 className="font-headline text-lg font-semibold text-gray-800">
|
||||
Compétences
|
||||
</h3>
|
||||
{total > 0 && (
|
||||
@ -552,19 +552,19 @@ export default function ParentHomePage() {
|
||||
</div>
|
||||
{total > 0 ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<div className="flex flex-col items-center p-3 bg-emerald-50 rounded-lg">
|
||||
<span className="text-2xl font-bold text-emerald-600">{pctAcquired}%</span>
|
||||
<span className="text-sm text-emerald-700">Acquises</span>
|
||||
<div className="flex flex-col items-center p-3 bg-primary/5 rounded-md">
|
||||
<span className="text-2xl font-bold text-primary">{pctAcquired}%</span>
|
||||
<span className="text-sm text-secondary">Acquises</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-yellow-50 rounded-lg">
|
||||
<div className="flex flex-col items-center p-3 bg-yellow-50 rounded-md">
|
||||
<span className="text-2xl font-bold text-yellow-600">{pctInProgress}%</span>
|
||||
<span className="text-sm text-yellow-700">En cours</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-red-50 rounded-lg">
|
||||
<div className="flex flex-col items-center p-3 bg-red-50 rounded-md">
|
||||
<span className="text-2xl font-bold text-red-500">{pctNotAcquired}%</span>
|
||||
<span className="text-sm text-red-600">Non acquises</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-gray-100 rounded-lg">
|
||||
<div className="flex flex-col items-center p-3 bg-gray-100 rounded-md">
|
||||
<span className="text-2xl font-bold text-gray-500">{pctNotEvaluated}%</span>
|
||||
<span className="text-sm text-gray-600">Non évaluées</span>
|
||||
</div>
|
||||
@ -577,10 +577,10 @@ export default function ParentHomePage() {
|
||||
})()}
|
||||
|
||||
{/* Notes par matière - Vue simplifiée */}
|
||||
<div className="border border-gray-100 rounded-lg p-4">
|
||||
<div className="border border-gray-100 rounded-md p-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Award className="w-5 h-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold text-gray-800">
|
||||
<h3 className="font-headline text-lg font-semibold text-gray-800">
|
||||
Notes par matière
|
||||
</h3>
|
||||
</div>
|
||||
@ -617,7 +617,7 @@ export default function ParentHomePage() {
|
||||
return (
|
||||
<div
|
||||
key={group.name}
|
||||
className="rounded-lg p-4 border"
|
||||
className="rounded-md p-4 border"
|
||||
style={{
|
||||
backgroundColor: `${group.color}10`,
|
||||
borderColor: `${group.color}40`,
|
||||
@ -657,28 +657,28 @@ export default function ParentHomePage() {
|
||||
{/* Fin bloc période */}
|
||||
|
||||
{/* Section Absences — toute l'année scolaire */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div className="bg-white rounded-md border border-gray-200 p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Clock className="w-5 h-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold text-gray-800">
|
||||
<h3 className="font-headline text-lg font-semibold text-gray-800">
|
||||
Absences & Retards
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mb-4">Toute l'année scolaire</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<div className="flex flex-col items-center p-3 bg-green-50 rounded-lg">
|
||||
<span className="text-2xl font-bold text-green-600">{absenceStats.justifiedAbsence}</span>
|
||||
<span className="text-sm text-green-700 text-center">Absences justifiées</span>
|
||||
<div className="flex flex-col items-center p-3 bg-primary/5 rounded-md">
|
||||
<span className="text-2xl font-bold text-primary">{absenceStats.justifiedAbsence}</span>
|
||||
<span className="text-sm text-secondary text-center">Absences justifiées</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-red-50 rounded-lg">
|
||||
<div className="flex flex-col items-center p-3 bg-red-50 rounded-md">
|
||||
<span className="text-2xl font-bold text-red-500">{absenceStats.unjustifiedAbsence}</span>
|
||||
<span className="text-sm text-red-600 text-center">Absences non justifiées</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-blue-50 rounded-lg">
|
||||
<div className="flex flex-col items-center p-3 bg-blue-50 rounded-md">
|
||||
<span className="text-2xl font-bold text-blue-600">{absenceStats.justifiedLate}</span>
|
||||
<span className="text-sm text-blue-700 text-center">Retards justifiés</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-3 bg-orange-50 rounded-lg">
|
||||
<div className="flex flex-col items-center p-3 bg-orange-50 rounded-md">
|
||||
<span className="text-2xl font-bold text-orange-500">{absenceStats.unjustifiedLate}</span>
|
||||
<span className="text-sm text-orange-600 text-center">Retards non justifiés</span>
|
||||
</div>
|
||||
|
||||
@ -39,37 +39,41 @@ export default function SettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl mb-4">Paramètres du compte</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputText
|
||||
type="email"
|
||||
id="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
type="password"
|
||||
id="password"
|
||||
label="Nouveau mot de passe"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
label="Confirmer le mot de passe"
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button type="submit" primary text={' Mettre à jour'} />
|
||||
</div>
|
||||
</form>
|
||||
<div className="p-6">
|
||||
<h1 className="font-headline text-2xl font-bold text-gray-900 mb-6">
|
||||
Paramètres du compte
|
||||
</h1>
|
||||
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-6 max-w-md">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<InputText
|
||||
type="email"
|
||||
id="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
type="password"
|
||||
id="password"
|
||||
label="Nouveau mot de passe"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
label="Confirmer le mot de passe"
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<Button type="submit" primary text={'Mettre à jour'} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -80,16 +80,20 @@ export default function Page() {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div
|
||||
className="min-h-screen flex items-center justify-center p-4"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #80fdd6 100%, #2bb180 100%)',
|
||||
}}
|
||||
>
|
||||
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
||||
<div className="flex justify-center mb-6">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">
|
||||
<h1 className="font-headline text-2xl font-bold text-center text-gray-900 mb-6">
|
||||
Authentification
|
||||
</h1>
|
||||
<form
|
||||
className="max-w-md mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleFormLogin(new FormData(e.target));
|
||||
@ -112,27 +116,24 @@ export default function Page() {
|
||||
placeholder="Mot de passe"
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<div className="input-group mb-4"></div>
|
||||
<label>
|
||||
<div className="flex justify-end mb-4">
|
||||
<a
|
||||
className="float-right mb-4"
|
||||
className="text-sm text-primary hover:text-secondary font-label transition-colors"
|
||||
href={`${FE_USERS_NEW_PASSWORD_URL}`}
|
||||
>
|
||||
Mot de passe oublié ?
|
||||
</a>
|
||||
</label>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Se Connecter"
|
||||
className="w-full"
|
||||
primary
|
||||
type="submit"
|
||||
name="connect"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
text="Se Connecter"
|
||||
className="w-full"
|
||||
primary
|
||||
type="submit"
|
||||
name="connect"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,16 +48,15 @@ export default function Page() {
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="min-h-screen bg-neutral flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
||||
<div className="flex justify-center mb-6">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">
|
||||
<h1 className="font-headline text-2xl font-bold text-center text-gray-900 mb-6">
|
||||
Nouveau Mot de passe
|
||||
</h1>
|
||||
<form
|
||||
className="max-w-md mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
validate(new FormData(e.target));
|
||||
@ -70,28 +69,23 @@ export default function Page() {
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
className="w-full"
|
||||
className="w-full mb-6"
|
||||
/>
|
||||
<Button
|
||||
text="Réinitialiser"
|
||||
className="w-full mb-3"
|
||||
primary
|
||||
type="submit"
|
||||
name="validate"
|
||||
/>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Réinitialiser"
|
||||
className="w-full"
|
||||
primary
|
||||
type="submit"
|
||||
name="validate"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
||||
<Button
|
||||
text="Annuler"
|
||||
className="w-full"
|
||||
href={`${FE_USERS_LOGIN_URL}`}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,16 +61,15 @@ export default function Page() {
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="min-h-screen bg-neutral flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
||||
<div className="flex justify-center mb-6">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">
|
||||
<h1 className="font-headline text-2xl font-bold text-center text-gray-900 mb-6">
|
||||
Réinitialisation du mot de passe
|
||||
</h1>
|
||||
<form
|
||||
className="max-w-md mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
validate(new FormData(e.target));
|
||||
@ -91,28 +90,23 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Confirmation mot de passe"
|
||||
placeholder="Confirmation mot de passe"
|
||||
className="w-full"
|
||||
className="w-full mb-6"
|
||||
/>
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
className="w-full mb-3"
|
||||
primary
|
||||
type="submit"
|
||||
name="validate"
|
||||
/>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
className="w-full"
|
||||
primary
|
||||
type="submit"
|
||||
name="validate"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
||||
<Button
|
||||
text="Annuler"
|
||||
className="w-full"
|
||||
href={`${FE_USERS_LOGIN_URL}`}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,16 +65,15 @@ export default function Page() {
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="min-h-screen bg-neutral flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
||||
<div className="flex justify-center mb-6">
|
||||
<Logo className="h-150 w-150" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center mb-4">
|
||||
<h1 className="font-headline text-2xl font-bold text-center text-gray-900 mb-6">
|
||||
Nouveau profil
|
||||
</h1>
|
||||
<form
|
||||
className="max-w-md mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
subscribeFormSubmit(new FormData(e.target));
|
||||
@ -103,30 +102,23 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Confirmation mot de passe"
|
||||
placeholder="Confirmation mot de passe"
|
||||
className="w-full"
|
||||
className="w-full mb-6"
|
||||
/>
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
className="w-full mb-3"
|
||||
primary
|
||||
type="submit"
|
||||
name="validate"
|
||||
/>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
className="w-full"
|
||||
primary
|
||||
type="submit"
|
||||
name="validate"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
||||
<Button
|
||||
text="Annuler"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
}}
|
||||
onClick={() => router.push(`${FE_USERS_LOGIN_URL}`)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import React from 'react';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
import { Inter, Manrope } from 'next/font/google';
|
||||
import localFont from 'next/font/local';
|
||||
import Providers from '@/components/Providers';
|
||||
import ServiceWorkerRegister from '@/components/ServiceWorkerRegister';
|
||||
import '@/css/tailwind.css';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
const inter = localFont({
|
||||
src: '../fonts/Inter-Variable.woff2',
|
||||
variable: '--font-inter',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const manrope = Manrope({
|
||||
subsets: ['latin'],
|
||||
const manrope = localFont({
|
||||
src: '../fonts/Manrope-Variable.woff2',
|
||||
variable: '--font-manrope',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
@ -3,16 +3,19 @@ import Logo from '../components/Logo';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-emerald-500">
|
||||
<div className="text-center p-6 ">
|
||||
<div className="flex items-center justify-center min-h-screen bg-primary">
|
||||
<div className="text-center p-6 bg-white rounded-md shadow-sm border border-gray-200">
|
||||
<Logo className="w-32 h-32 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-emerald-900 mb-4">
|
||||
<h2 className="font-headline text-2xl font-bold text-secondary mb-4">
|
||||
404 | Page non trouvée
|
||||
</h2>
|
||||
<p className="text-emerald-900 mb-4">
|
||||
<p className="font-body text-gray-600 mb-4">
|
||||
La ressource que vous souhaitez consulter n'existe pas ou plus.
|
||||
</p>
|
||||
<Link className="text-gray-900 hover:underline" href="/">
|
||||
<Link
|
||||
className="inline-flex items-center justify-center min-h-[44px] px-4 py-2 rounded font-label font-medium bg-primary hover:bg-secondary text-white transition-colors"
|
||||
href="/"
|
||||
>
|
||||
Retour Accueil
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user