mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 16:03:21 +00:00
chore: Initial Commit
feat: Gestion des inscriptions [#1] feat(frontend): Création des vues pour le paramétrage de l'école [#2] feat: Gestion du login [#6] fix: Correction lors de la migration des modèle [#8] feat: Révision du menu principal [#9] feat: Ajout d'un footer [#10] feat: Création des dockers compose pour les environnements de développement et de production [#12] doc(ci): Mise en place de Husky et d'un suivi de version automatique [#14]
This commit is contained in:
49
Front-End/src/app/[locale]/admin/classes/page.js
Normal file
49
Front-End/src/app/[locale]/admin/classes/page.js
Normal file
@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
const columns = [
|
||||
{ name: 'Nom', transform: (row) => row.Nom },
|
||||
{ name: 'Niveau', transform: (row) => row.Niveau },
|
||||
{ name: 'Effectif', transform: (row) => row.Effectif },
|
||||
];
|
||||
|
||||
export default function Page() {
|
||||
const [classes, setClasses] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
fetchClasses();
|
||||
}, [currentPage]);
|
||||
|
||||
const fetchClasses = async () => {
|
||||
const fakeData = {
|
||||
classes: [
|
||||
{ Nom: 'Classe A', Niveau: '1ère année', Effectif: 30 },
|
||||
{ Nom: 'Classe B', Niveau: '2ème année', Effectif: 25 },
|
||||
{ Nom: 'Classe C', Niveau: '3ème année', Effectif: 28 },
|
||||
],
|
||||
totalPages: 3
|
||||
};
|
||||
setClasses(fakeData.classes);
|
||||
setTotalPages(fakeData.totalPages);
|
||||
};
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handleCreateClass = () => {
|
||||
console.log('Créer une nouvelle classe');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<h1 className='heading-section'>Gestion des Classes</h1>
|
||||
<Button text="Créer une nouvelle classe" onClick={handleCreateClass} primary />
|
||||
<Table data={classes} columns={columns} itemsPerPage={5} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
Front-End/src/app/[locale]/admin/grades/page.js
Normal file
10
Front-End/src/app/[locale]/admin/grades/page.js
Normal file
@ -0,0 +1,10 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<h1 className='heading-section'>Statistiques</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
96
Front-End/src/app/[locale]/admin/layout.js
Normal file
96
Front-End/src/app/[locale]/admin/layout.js
Normal file
@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
// src/components/Layout.js
|
||||
import React from 'react';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import {
|
||||
Users,
|
||||
Building,
|
||||
Home,
|
||||
Calendar,
|
||||
Settings,
|
||||
FileText,
|
||||
LogOut
|
||||
} from 'lucide-react';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
import Logo from '@/components/Logo';
|
||||
import {
|
||||
FR_ADMIN_HOME_URL,
|
||||
FR_ADMIN_STUDENT_URL,
|
||||
FR_ADMIN_STRUCTURE_URL,
|
||||
FR_ADMIN_GRADES_URL,
|
||||
FR_ADMIN_PLANNING_URL,
|
||||
FR_ADMIN_SETTINGS_URL
|
||||
} from '@/utils/Url';
|
||||
|
||||
import { disconnect } from '@/app/lib/actions';
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}) {
|
||||
const t = useTranslations('sidebar');
|
||||
|
||||
const sidebarItems = {
|
||||
"admin": { "id": "admin", "name": t('dashboard'), "url": FR_ADMIN_HOME_URL, "icon": Home },
|
||||
"students": { "id": "students", "name": t('students'), "url": FR_ADMIN_STUDENT_URL, "icon": Users },
|
||||
"structure": { "id": "structure", "name": t('structure'), "url": FR_ADMIN_STRUCTURE_URL, "icon": Building },
|
||||
"grades": { "id": "grades", "name": t('grades'), "url": FR_ADMIN_GRADES_URL, "icon": FileText },
|
||||
"planning": { "id": "planning", "name": t('planning'), "url": FR_ADMIN_PLANNING_URL, "icon": Calendar },
|
||||
"settings": { "id": "settings", "name": t('settings'), "url": FR_ADMIN_SETTINGS_URL, "icon": Settings }
|
||||
};
|
||||
|
||||
const pathname = usePathname();
|
||||
const currentPage = pathname.split('/').pop();
|
||||
|
||||
const headerTitle = sidebarItems[currentPage]?.name || t('dashboard');
|
||||
|
||||
const softwareName = "N3WT School";
|
||||
const softwareVersion = "v1.0.0";
|
||||
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
label: 'Déconnexion',
|
||||
onClick: disconnect,
|
||||
icon: LogOut,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-screen bg-gray-50">
|
||||
<Sidebar currentPage={currentPage} items={Object.values(sidebarItems)} className="h-full" />
|
||||
<div className="flex flex-col flex-1">
|
||||
{/* Header - h-16 = 64px */}
|
||||
<header className="h-16 bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between z-10">
|
||||
<div className="text-xl font-semibold">{headerTitle}</div>
|
||||
<DropdownMenu
|
||||
buttonContent={<img src="https://i.pravatar.cc/32" alt="Profile" className="w-8 h-8 rounded-full cursor-pointer" />}
|
||||
items={dropdownItems}
|
||||
buttonClassName=""
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded shadow-lg"
|
||||
/>
|
||||
</header>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Content avec scroll si nécessaire */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
{/* Footer - h-16 = 64px */}
|
||||
<footer className="h-16 bg-white border-t border-gray-200 px-8 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<span>© {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.</span>
|
||||
<div>{softwareName} - {softwareVersion}</div>
|
||||
</div>
|
||||
<Logo className="w-8 h-8" />
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
143
Front-End/src/app/[locale]/admin/page.js
Normal file
143
Front-End/src/app/[locale]/admin/page.js
Normal file
@ -0,0 +1,143 @@
|
||||
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react';
|
||||
import Loader from '@/components/Loader';
|
||||
|
||||
// Composant StatCard pour afficher une statistique
|
||||
const StatCard = ({ title, value, icon, change, color = "blue" }) => (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-gray-500 text-sm font-medium">{title}</h3>
|
||||
<p className="text-2xl font-semibold mt-1">{value}</p>
|
||||
{change && (
|
||||
<p className={`text-sm ${change > 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{change > 0 ? '+' : ''}{change}% depuis le mois dernier
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`p-3 rounded-full bg-${color}-100`}>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Composant EventCard pour afficher les événements
|
||||
const EventCard = ({ title, date, description, type }) => (
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-100 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<CalendarCheck className="text-blue-500" size={20} />
|
||||
<div>
|
||||
<h4 className="font-medium">{title}</h4>
|
||||
<p className="text-sm text-gray-500">{date}</p>
|
||||
<p className="text-sm mt-1">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function DashboardPage() {
|
||||
const t = useTranslations('dashboard');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [stats, setStats] = useState({
|
||||
totalStudents: 0,
|
||||
averageInscriptionTime: 0,
|
||||
reInscriptionRate: 0,
|
||||
structureCapacity: 0,
|
||||
upcomingEvents: [],
|
||||
monthlyStats: {
|
||||
inscriptions: [],
|
||||
completionRate: 0
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Simulation de chargement des données
|
||||
setTimeout(() => {
|
||||
setStats({
|
||||
totalStudents: 245,
|
||||
averageInscriptionTime: 3.5,
|
||||
reInscriptionRate: 85,
|
||||
structureCapacity: 300,
|
||||
upcomingEvents: [
|
||||
{
|
||||
title: "Réunion de rentrée",
|
||||
date: "2024-09-01",
|
||||
description: "Présentation de l'année scolaire",
|
||||
type: "meeting"
|
||||
},
|
||||
{
|
||||
title: "Date limite inscriptions",
|
||||
date: "2024-08-15",
|
||||
description: "Clôture des inscriptions",
|
||||
type: "deadline"
|
||||
}
|
||||
],
|
||||
monthlyStats: {
|
||||
inscriptions: [150, 180, 210, 245],
|
||||
completionRate: 78
|
||||
}
|
||||
});
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">{t('dashboard')}</h1>
|
||||
|
||||
{/* Statistiques principales */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<StatCard
|
||||
title={t('totalStudents')}
|
||||
value={stats.totalStudents}
|
||||
icon={<Users className="text-blue-500" size={24} />}
|
||||
change={12}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('averageInscriptionTime')}
|
||||
value={`${stats.averageInscriptionTime} jours`}
|
||||
icon={<Clock className="text-green-500" size={24} />}
|
||||
color="green"
|
||||
/>
|
||||
<StatCard
|
||||
title={t('reInscriptionRate')}
|
||||
value={`${stats.reInscriptionRate}%`}
|
||||
icon={<UserCheck className="text-purple-500" size={24} />}
|
||||
color="purple"
|
||||
/>
|
||||
<StatCard
|
||||
title={t('structureCapacity')}
|
||||
value={`${(stats.totalStudents/stats.structureCapacity * 100).toFixed(1)}%`}
|
||||
icon={<School className="text-orange-500" size={24} />}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Événements et KPIs */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Graphique des inscriptions */}
|
||||
<div className="lg:col-span-2 bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
<h2 className="text-lg font-semibold mb-4">{t('inscriptionTrends')}</h2>
|
||||
{/* Insérer ici un composant de graphique */}
|
||||
<div className="h-64 bg-gray-50 rounded flex items-center justify-center">
|
||||
<TrendingUp size={48} className="text-gray-300" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Événements à venir */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
||||
{stats.upcomingEvents.map((event, index) => (
|
||||
<EventCard key={index} {...event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
Front-End/src/app/[locale]/admin/planning/page.js
Normal file
65
Front-End/src/app/[locale]/admin/planning/page.js
Normal file
@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import { PlanningProvider } from '@/context/PlanningContext';
|
||||
import Calendar from '@/components/Calendar';
|
||||
import EventModal from '@/components/EventModal';
|
||||
import ScheduleNavigation from '@/components/ScheduleNavigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [eventData, setEventData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
start: '',
|
||||
end: '',
|
||||
location: '',
|
||||
scheduleId: '', // Enlever la valeur par défaut ici
|
||||
recurrence: 'none',
|
||||
selectedDays: [],
|
||||
recurrenceEnd: '',
|
||||
customInterval: 1,
|
||||
customUnit: 'days',
|
||||
viewType: 'week' // Ajouter la vue semaine par défaut
|
||||
});
|
||||
|
||||
const initializeNewEvent = (date = new Date()) => {
|
||||
// S'assurer que date est un objet Date valide
|
||||
const eventDate = date instanceof Date ? date : new Date();
|
||||
|
||||
setEventData({
|
||||
title: '',
|
||||
description: '',
|
||||
start: eventDate.toISOString(),
|
||||
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
||||
location: '',
|
||||
scheduleId: '', // Ne pas définir de valeur par défaut ici non plus
|
||||
recurrence: 'none',
|
||||
selectedDays: [],
|
||||
recurrenceEnd: '',
|
||||
customInterval: 1,
|
||||
customUnit: 'days'
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<PlanningProvider>
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<ScheduleNavigation />
|
||||
<Calendar
|
||||
onDateClick={initializeNewEvent}
|
||||
onEventClick={(event) => {
|
||||
setEventData(event);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
<EventModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
eventData={eventData}
|
||||
setEventData={setEventData}
|
||||
/>
|
||||
</div>
|
||||
</PlanningProvider>
|
||||
);
|
||||
}
|
||||
105
Front-End/src/app/[locale]/admin/settings/page.js
Normal file
105
Front-End/src/app/[locale]/admin/settings/page.js
Normal file
@ -0,0 +1,105 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react';
|
||||
import Tab from '@/components/Tab';
|
||||
import TabContent from '@/components/TabContent';
|
||||
import Button from '@/components/Button';
|
||||
import InputText from '@/components/InputText';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [activeTab, setActiveTab] = useState('structure');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [smtpServer, setSmtpServer] = useState('');
|
||||
const [smtpPort, setSmtpPort] = useState('');
|
||||
const [smtpUser, setSmtpUser] = useState('');
|
||||
const [smtpPassword, setSmtpPassword] = useState('');
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
const handleEmailChange = (e) => {
|
||||
setEmail(e.target.value);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e) => {
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleSmtpServerChange = (e) => {
|
||||
setSmtpServer(e.target.value);
|
||||
};
|
||||
|
||||
const handleSmtpPortChange = (e) => {
|
||||
setSmtpPort(e.target.value);
|
||||
};
|
||||
|
||||
const handleSmtpUserChange = (e) => {
|
||||
setSmtpUser(e.target.value);
|
||||
};
|
||||
|
||||
const handleSmtpPasswordChange = (e) => {
|
||||
setSmtpPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (password !== confirmPassword) {
|
||||
alert('Les mots de passe ne correspondent pas');
|
||||
return;
|
||||
}
|
||||
// Logique pour mettre à jour l'email et le mot de passe
|
||||
console.log('Email:', email);
|
||||
console.log('Password:', password);
|
||||
};
|
||||
|
||||
const handleSmtpSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
// Logique pour mettre à jour les paramètres SMTP
|
||||
console.log('SMTP Server:', smtpServer);
|
||||
console.log('SMTP Port:', smtpPort);
|
||||
console.log('SMTP User:', smtpUser);
|
||||
console.log('SMTP Password:', smtpPassword);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex space-x-4 mb-4">
|
||||
<Tab
|
||||
text="Informations de la structure"
|
||||
active={activeTab === 'structure'}
|
||||
onClick={() => handleTabClick('structure')}
|
||||
/>
|
||||
<Tab
|
||||
text="Paramètres SMTP"
|
||||
active={activeTab === 'smtp'}
|
||||
onClick={() => handleTabClick('smtp')}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TabContent isActive={activeTab === 'structure'}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputText label="Email" value={email} onChange={handleEmailChange} />
|
||||
<InputText label="Mot de passe" type="password" value={password} onChange={handlePasswordChange} />
|
||||
<InputText label="Confirmer le mot de passe" type="password" value={confirmPassword} onChange={handleConfirmPasswordChange} />
|
||||
<Button type="submit" primary text="Mettre à jour"></Button>
|
||||
</form>
|
||||
</TabContent>
|
||||
<TabContent isActive={activeTab === 'smtp'}>
|
||||
<form onSubmit={handleSmtpSubmit}>
|
||||
<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} />
|
||||
<Button type="submit" primary text="Mettre à jour"></Button>
|
||||
</form>
|
||||
</TabContent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
159
Front-End/src/app/[locale]/admin/structure/page.js
Normal file
159
Front-End/src/app/[locale]/admin/structure/page.js
Normal file
@ -0,0 +1,159 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import SpecialitiesSection from '@/components/SpecialitiesSection'
|
||||
import ClassesSection from '@/components/ClassesSection'
|
||||
import TeachersSection from '@/components/TeachersSection';
|
||||
import { User, School } from 'lucide-react'
|
||||
import { BK_GESTIONINSCRIPTION_SPECIALITES_URL,
|
||||
BK_GESTIONINSCRIPTION_CLASSES_URL,
|
||||
BK_GESTIONINSCRIPTION_SPECIALITE_URL,
|
||||
BK_GESTIONINSCRIPTION_CLASSE_URL,
|
||||
BK_GESTIONINSCRIPTION_TEACHERS_URL,
|
||||
BK_GESTIONINSCRIPTION_TEACHER_URL } from '@/utils/Url';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
|
||||
export default function Page() {
|
||||
const [specialities, setSpecialities] = useState([]);
|
||||
const [classes, setClasses] = useState([]);
|
||||
const [teachers, setTeachers] = useState([]);
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch data for specialities
|
||||
fetchSpecialities();
|
||||
|
||||
// Fetch data for teachers
|
||||
fetchTeachers();
|
||||
|
||||
// Fetch data for classes
|
||||
fetchClasses();
|
||||
}, []);
|
||||
|
||||
const fetchSpecialities = () => {
|
||||
fetch(`${BK_GESTIONINSCRIPTION_SPECIALITES_URL}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setSpecialities(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching specialities:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchTeachers = () => {
|
||||
fetch(`${BK_GESTIONINSCRIPTION_TEACHERS_URL}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setTeachers(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching teachers:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchClasses = () => {
|
||||
fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setClasses(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching classes:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreate = (url, newData, setDatas) => {
|
||||
console.log('SEND POST :', JSON.stringify(newData));
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify(newData),
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Succes :', data);
|
||||
setDatas(prevState => [...prevState, data]);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur :', error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (url, id, updatedData, setDatas) => {
|
||||
fetch(`${url}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify(updatedData),
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setDatas(prevState => prevState.map(item => item.id === id ? data : item));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur :', error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (url, id, setDatas) => {
|
||||
fetch(`${url}/${id}`, {
|
||||
method:'DELETE',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setDatas(prevState => prevState.filter(item => item.id !== id));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
|
||||
<SpecialitiesSection
|
||||
specialities={specialities}
|
||||
setSpecialities={setSpecialities}
|
||||
handleCreate={(newData) => handleCreate(`${BK_GESTIONINSCRIPTION_SPECIALITE_URL}`, newData, setSpecialities)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_SPECIALITE_URL}`, id, updatedData, setSpecialities)}
|
||||
handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_SPECIALITE_URL}`, id, setSpecialities)}
|
||||
/>
|
||||
|
||||
<TeachersSection
|
||||
teachers={teachers}
|
||||
specialities={specialities}
|
||||
handleCreate={(newData) => handleCreate(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, newData, setTeachers)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, id, updatedData, setTeachers)}
|
||||
handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, id, setTeachers)}
|
||||
/>
|
||||
|
||||
<ClassesSection
|
||||
classes={classes}
|
||||
specialities={specialities}
|
||||
teachers={teachers}
|
||||
handleCreate={(newData) => handleCreate(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, newData, setClasses)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, id, updatedData, setClasses)}
|
||||
handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, id, setClasses)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
|
||||
import { FR_ADMIN_STUDENT_URL,
|
||||
BK_GESTIONINSCRIPTION_ELEVE_URL,
|
||||
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL } from '@/utils/Url';
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
import { mockStudent } from '@/data/mockStudent';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const idProfil = searchParams.get('id');
|
||||
const idEleve = searchParams.get('idEleve'); // Changé de codeDI à idEleve
|
||||
|
||||
const [initialData, setInitialData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (useFakeData) {
|
||||
setInitialData(mockStudent);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
fetch(`${BK_GESTIONINSCRIPTION_ELEVE_URL}/${idEleve}`) // Utilisation de idEleve au lieu de codeDI
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Fetched data:', data); // Pour le débogage
|
||||
const formattedData = {
|
||||
id: data.id,
|
||||
nom: data.nom,
|
||||
prenom: data.prenom,
|
||||
adresse: data.adresse,
|
||||
dateNaissance: data.dateNaissance,
|
||||
lieuNaissance: data.lieuNaissance,
|
||||
codePostalNaissance: data.codePostalNaissance,
|
||||
nationalite: data.nationalite,
|
||||
medecinTraitant: data.medecinTraitant,
|
||||
niveau: data.niveau,
|
||||
responsables: data.responsables || []
|
||||
};
|
||||
setInitialData(formattedData);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching student data:', error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [idEleve]); // Dépendance changée à idEleve
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
if (useFakeData) {
|
||||
console.log('Fake submit:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${idEleve}`, { // Utilisation de idEleve
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Success:', result);
|
||||
// Redirection après succès
|
||||
window.location.href = FR_ADMIN_STUDENT_URL;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Une erreur est survenue lors de la mise à jour des données');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InscriptionFormShared
|
||||
initialData={initialData}
|
||||
csrfToken={csrfToken}
|
||||
onSubmit={handleSubmit}
|
||||
cancelUrl={FR_ADMIN_STUDENT_URL}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
334
Front-End/src/app/[locale]/admin/students/page.js
Normal file
334
Front-End/src/app/[locale]/admin/students/page.js
Normal file
@ -0,0 +1,334 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import {mockFicheInscription} from '@/data/mockFicheInscription';
|
||||
import Tab from '@/components/Tab';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import { Search } from 'lucide-react';
|
||||
import Popup from '@/components/Popup';
|
||||
import Loader from '@/components/Loader';
|
||||
import AlertWithModal from '@/components/AlertWithModal';
|
||||
import Button from '@/components/Button';
|
||||
import DropdownMenu from "@/components/DropdownMenu";
|
||||
import { swapFormatDate } from '@/utils/Date';
|
||||
import { formatPhoneNumber } from '@/utils/Telephone';
|
||||
import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus } from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
import InscriptionForm from '@/components/Inscription/InscriptionForm'
|
||||
|
||||
import { BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL, BK_GESTIONINSCRIPTION_SEND_URL, FR_ADMIN_STUDENT_EDIT_SUBSCRIBE, BK_GESTIONINSCRIPTION_ARCHIVE_URL } from '@/utils/Url';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
|
||||
export default function Page({ params: { locale } }) {
|
||||
const t = useTranslations('students');
|
||||
const [ficheInscriptions, setFicheInscriptions] = useState([]);
|
||||
const [ficheInscriptionsData, setFicheInscriptionsData] = useState([]);
|
||||
const [fichesInscriptionsDataArchivees, setFicheInscriptionsDataArchivees] = useState([]);
|
||||
// const [filter, setFilter] = useState('*');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [alertPage, setAlertPage] = useState(false);
|
||||
const [mailSent, setMailSent] = useState(false);
|
||||
const [ficheArchivee, setFicheArchivee] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [popup, setPopup] = useState({ visible: false, message: '', onConfirm: null });
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalStudents, setTotalStudents] = useState(0);
|
||||
const [totalArchives, setTotalArchives] = useState(0);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(5); // Définir le nombre d'éléments par page
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
// Modifier la fonction fetchData pour inclure le terme de recherche
|
||||
const fetchData = (page, pageSize, search = '') => {
|
||||
const url = `${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/all?page=${page}&page_size=${pageSize}&search=${search}`;
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
setIsLoading(false);
|
||||
if (data) {
|
||||
const { fichesInscriptions, count } = data;
|
||||
setFicheInscriptionsData(fichesInscriptions);
|
||||
const calculatedTotalPages = Math.ceil(count / pageSize);
|
||||
setTotalStudents(count);
|
||||
setTotalPages(calculatedTotalPages);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDataArchived = () => {
|
||||
fetch(`${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/archived`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
setIsLoading(false);
|
||||
if (data) {
|
||||
const { fichesInscriptions, count } = data;
|
||||
setTotalArchives(count);
|
||||
setFicheInscriptionsDataArchivees(fichesInscriptions);
|
||||
}
|
||||
console.log('Success ARCHIVED:', data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDataAndSetState = () => {
|
||||
if (!useFakeData) {
|
||||
fetchData(currentPage, itemsPerPage, searchTerm);
|
||||
fetchDataArchived();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setFicheInscriptionsData(mockFicheInscription);
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
setFicheArchivee(false);
|
||||
setMailSent(false);
|
||||
};
|
||||
|
||||
fetchDataAndSetState();
|
||||
}, [mailSent, ficheArchivee, currentPage, itemsPerPage]);
|
||||
|
||||
// Modifier le useEffect pour la recherche
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
fetchData(currentPage, itemsPerPage, searchTerm);
|
||||
}, 500); // Debounce la recherche
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [searchTerm, currentPage, itemsPerPage]);
|
||||
|
||||
const archiveFicheInscription = (id, nom, prenom) => {
|
||||
setPopup({
|
||||
visible: true,
|
||||
message: `Attentions ! \nVous êtes sur le point d'archiver le dossier d'inscription de ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`,
|
||||
onConfirm: () => {
|
||||
const url = `${BK_GESTIONINSCRIPTION_ARCHIVE_URL}/${id}`;
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setFicheInscriptions(ficheInscriptions.filter(fiche => fiche.id !== id));
|
||||
setFicheArchivee(true);
|
||||
alert("Le dossier d'inscription a été correctement archivé");
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error archiving data:', error);
|
||||
alert("Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur.");
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendConfirmFicheInscription = (id, nom, prenom) => {
|
||||
setPopup({
|
||||
visible: true,
|
||||
message: `Avertissement ! \nVous êtes sur le point d'envoyer un dossier d'inscription à ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`,
|
||||
onConfirm: () => {
|
||||
const url = `${BK_GESTIONINSCRIPTION_SEND_URL}/${id}`;
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setMailSent(true);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateStatusAction = (id, newStatus) => {
|
||||
console.log('Edit fiche inscription with id:', id);
|
||||
};
|
||||
|
||||
const handleLetterClick = (letter) => {
|
||||
setFilter(letter);
|
||||
};
|
||||
|
||||
const handleSearchChange = (event) => {
|
||||
setSearchTerm(event.target.value);
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage) => {
|
||||
setCurrentPage(newPage);
|
||||
fetchData(newPage, itemsPerPage); // Appeler fetchData directement ici
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ name: t('studentName'), transform: (row) => row.eleve.nom },
|
||||
{ name: t('studentFistName'), transform: (row) => row.eleve.prenom },
|
||||
{ name: t('mainContactMail'), transform: (row) => row.eleve.responsables[0].mail },
|
||||
{ name: t('phone'), transform: (row) => formatPhoneNumber(row.eleve.responsables[0].telephone) },
|
||||
{ name: t('lastUpdateDate'), transform: (row) => swapFormatDate(row.dateMAJ, "DD-MM-YYYY hh:mm:ss", "DD/MM/YYYY hh:mm") },
|
||||
{ name: t('registrationFileStatus'), transform: (row) => <StatusLabel etat={row.etat} onChange={(newStatus) => updateStatusAction(row.eleve.id, newStatus)} /> },
|
||||
{ name: t('files'), transform: (row) => (
|
||||
<ul>
|
||||
{row.fichiers?.map((fichier, fileIndex) => (
|
||||
<li key={fileIndex} className="flex items-center gap-2">
|
||||
<FileText size={16} />
|
||||
<a href={fichier.url}>{fichier.nom}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) },
|
||||
{ name: 'Actions', transform: (row) => (
|
||||
<DropdownMenu
|
||||
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
|
||||
items={[
|
||||
...(row.etat === 1 ? [{
|
||||
label: (
|
||||
<>
|
||||
<Send size={16} className="mr-2" /> Envoyer
|
||||
</>
|
||||
),
|
||||
onClick: () => sendConfirmFicheInscription(row.eleve.id, row.eleve.nom, row.eleve.prenom),
|
||||
}] : []),
|
||||
...(row.etat === 1 ? [{
|
||||
label: (
|
||||
<>
|
||||
<Edit size={16} className="mr-2" /> Modifier
|
||||
</>
|
||||
),
|
||||
onClick: () => window.location.href = `${FR_ADMIN_STUDENT_EDIT_SUBSCRIBE}?idEleve=${row.eleve.id}&id=1`,
|
||||
}] : []),
|
||||
...(row.etat === 2 ? [{
|
||||
label: (
|
||||
<>
|
||||
<Edit size={16} className="mr-2" /> Modifier
|
||||
</>
|
||||
),
|
||||
onClick: () => window.location.href = `${FR_ADMIN_STUDENT_EDIT_SUBSCRIBE}?idEleve=${row.eleve.id}&id=1`,
|
||||
}] : []),
|
||||
...(row.etat !== 6 ? [{
|
||||
label: (
|
||||
<>
|
||||
<Trash2 size={16} className="mr-2 text-red-700" /> Archiver
|
||||
</>
|
||||
),
|
||||
onClick: () => archiveFicheInscription(row.eleve.id, row.eleve.nom, row.eleve.prenom),
|
||||
}] : []),
|
||||
]}
|
||||
buttonClassName="text-gray-400 hover:text-gray-600"
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
|
||||
/>
|
||||
) },
|
||||
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
} else {
|
||||
if (ficheInscriptions.length === 0 && fichesInscriptionsDataArchivees.length === 0 && alertPage) {
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<AlertWithModal
|
||||
title={t("information")}
|
||||
message={t("no_records") + " " + t("create_first_record")}
|
||||
buttonText={t("add_button")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<div className="flex gap-8">
|
||||
<Tab
|
||||
text={<>
|
||||
{t('allStudents')}
|
||||
<span className="ml-2 text-sm text-gray-400">({totalStudents})</span>
|
||||
</>}
|
||||
active={activeTab === 'all'}
|
||||
onClick={() => setActiveTab('all')}
|
||||
/>
|
||||
<Tab
|
||||
text={<>
|
||||
{t('pending')}
|
||||
<span className="ml-2 text-sm text-gray-400">({12})</span>
|
||||
</>}
|
||||
active={activeTab === 'pending'}
|
||||
onClick={() => setActiveTab('pending')}
|
||||
/>
|
||||
<Tab
|
||||
text={<>
|
||||
{t('archived')}
|
||||
<span className="ml-2 text-sm text-gray-400">({totalArchives})</span>
|
||||
</>}
|
||||
active={activeTab === 'archived'}
|
||||
onClick={() => setActiveTab('archived')}
|
||||
/>
|
||||
<Button text={t("addStudent")} primary onClick={openModal} icon={<UserPlus size={20} />} />
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
title={"Création d'un nouveau dossier d'inscription"}
|
||||
ContentComponent={InscriptionForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="relative flex-grow mr-4">
|
||||
<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>
|
||||
|
||||
<Table
|
||||
key={`${currentPage}-${searchTerm}`}
|
||||
data={(activeTab === 'all' || activeTab === 'pending') ? ficheInscriptionsData : fichesInscriptionsDataArchivees}
|
||||
columns={columns}
|
||||
itemsPerPage={itemsPerPage}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
<Popup
|
||||
visible={popup.visible}
|
||||
message={popup.message}
|
||||
onConfirm={() => {
|
||||
popup.onConfirm();
|
||||
setPopup({ ...popup, visible: false });
|
||||
}}
|
||||
onCancel={() => setPopup({ ...popup, visible: false })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Front-End/src/app/[locale]/admin/teachers/page.js
Normal file
23
Front-End/src/app/[locale]/admin/teachers/page.js
Normal file
@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Button from '@/components/Button';
|
||||
import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus } from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
|
||||
export default function Page() {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className='p-8'>
|
||||
|
||||
<Button text={"addTeacher"} primary onClick={openModal} icon={<UserPlus size={20} />} />
|
||||
<Modal isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user