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:
Luc SORIGNET
2024-11-18 10:02:58 +01:00
committed by N3WT DE COMPET
commit af0cd1c840
228 changed files with 22694 additions and 0 deletions

View 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>
);
}

View 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>
);
}

View 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>&copy; {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>
</>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
};

View File

@ -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}
/>
);
}

View 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>
);
}
}
}

View 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>
);
}

View File

@ -0,0 +1,18 @@
'use client'
import {useTranslations} from 'next-intl';
import React from 'react';
import Button from '@/components/Button';
import Logo from '@/components/Logo'; // Import du composant Logo
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>
<Button text={t('loginButton')} primary href="/users/login" />
</div>
);
}

View File

@ -0,0 +1,113 @@
'use client'
import React, { useState, useEffect } from 'react';
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
import { useSearchParams, redirect, useRouter } from 'next/navigation';
import useCsrfToken from '@/hooks/useCsrfToken';
import { FR_PARENTS_HOME_URL,
BK_GESTIONINSCRIPTION_ELEVE_URL,
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL,
BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL } from '@/utils/Url';
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');
const router = useRouter();
const [initialData, setInitialData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const csrfToken = useCsrfToken();
const [currentProfil, setCurrentProfil] = useState("");
const [lastIdResponsable, setLastIdResponsable] = useState(1);
useEffect(() => {
if (!idEleve || !idProfil) {
console.error('Missing idEleve or idProfil');
return;
}
if (useFakeData) {
setInitialData(mockStudent);
setLastIdResponsable(999);
setIsLoading(false);
} else {
Promise.all([
// Fetch eleve data
fetch(`${BK_GESTIONINSCRIPTION_ELEVE_URL}/${idEleve}`),
// Fetch last responsable ID
fetch(BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL)
])
.then(async ([eleveResponse, responsableResponse]) => {
const eleveData = await eleveResponse.json();
const responsableData = await responsableResponse.json();
const formattedData = {
id: eleveData.id,
nom: eleveData.nom,
prenom: eleveData.prenom,
adresse: eleveData.adresse,
dateNaissance: eleveData.dateNaissance,
lieuNaissance: eleveData.lieuNaissance,
codePostalNaissance: eleveData.codePostalNaissance,
nationalite: eleveData.nationalite,
medecinTraitant: eleveData.medecinTraitant,
niveau: eleveData.niveau,
responsables: eleveData.responsables || []
};
setInitialData(formattedData);
setLastIdResponsable(responsableData.lastid);
let profils = eleveData.profils;
const currentProf = profils.find(profil => profil.id === idProfil);
if (currentProf) {
setCurrentProfil(currentProf);
}
})
.catch(error => {
console.error('Error fetching data:', error);
})
.finally(() => {
setIsLoading(false);
});
}
}, [idEleve, idProfil]);
const handleSubmit = async (data) => {
if (useFakeData) {
console.log('Fake submit:', data);
return;
}
try {
const response = await fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${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);
router.push(FR_PARENTS_HOME_URL);
} catch (error) {
console.error('Error:', error);
}
};
return (
<InscriptionFormShared
initialData={initialData}
csrfToken={csrfToken}
onSubmit={handleSubmit}
cancelUrl={FR_PARENTS_HOME_URL}
isLoading={isLoading}
/>
);
}

View File

@ -0,0 +1,92 @@
'use client'
// src/components/Layout.js
import React, { useState, useEffect } from 'react';
import DropdownMenu from '@/components/DropdownMenu';
import { useRouter } from 'next/navigation'; // Ajout de l'importation
import { Bell, User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home
import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo
import { FR_PARENTS_HOME_URL,FR_PARENTS_MESSAGERIE_URL,FR_PARENTS_SETTINGS_URL, BK_GESTIONINSCRIPTION_MESSAGES_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent
import useLocalStorage from '@/hooks/useLocalStorage';
export default function Layout({
children,
}) {
const router = useRouter(); // Définition de router
const [messages, setMessages] = useState([]);
const [userId, setUserId] = useLocalStorage("userId", '') ;
useEffect(() => {
setUserId(userId);
fetch(`${BK_GESTIONINSCRIPTION_MESSAGES_URL}/${userId}`, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
if (data) {
setMessages(data);
}
console.log('Success :', data);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}, []);
return (
<>
<div className="flex flex-col min-h-screen bg-gray-50">
{/* Entête */}
<header className="bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between fixed top-0 left-0 right-0 z-10">
<div className="flex items-center space-x-2">
<Logo className="h-8 w-8" /> {/* Utilisation du composant Logo */}
<div className="text-xl font-semibold">Accueil</div>
</div>
<div className="flex items-center space-x-4">
<button
className="p-2 rounded-full hover:bg-gray-200"
onClick={() => { router.push(FR_PARENTS_HOME_URL); }} // Utilisation de router pour revenir à l'accueil parent
>
<Home />
</button>
<div className="relative">
<button
className="p-2 rounded-full hover:bg-gray-200"
onClick={() => { router.push(FR_PARENTS_MESSAGERIE_URL); }} // Utilisation de router
>
<MessageSquare />
</button>
{messages.length > 0 && (
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-emerald-600"></span>
)}
</div>
<DropdownMenu
buttonContent={<User />}
items={[
{ label: 'Se déconnecter', icon: LogOut, onClick: () => {} },
{ label: 'Settings', icon: Settings , onClick: () => { router.push(FR_PARENTS_SETTINGS_URL); } }
]}
buttonClassName="p-2 rounded-full hover:bg-gray-200"
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg"
/>
</div>
</header>
{/* Content */}
<div className="pt-20 p-8 flex-1"> {/* Ajout de flex-1 pour utiliser toute la hauteur disponible */}
{children}
</div>
</div>
</>
);
}

View File

@ -0,0 +1,106 @@
'use client'
import React, { useState, useRef, useEffect } from 'react';
import { SendHorizontal } from 'lucide-react';
const contacts = [
{ id: 1, name: 'Facturation', profilePic: 'https://i.pravatar.cc/32' },
{ id: 2, name: 'Enseignant 1', profilePic: 'https://i.pravatar.cc/32' },
{ id: 3, name: 'Contact', profilePic: 'https://i.pravatar.cc/32' },
];
export default function MessageriePage() {
const [selectedContact, setSelectedContact] = useState(null);
const [messages, setMessages] = useState({});
const [newMessage, setNewMessage] = useState('');
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSendMessage = () => {
if (newMessage.trim() && selectedContact) {
const contactMessages = messages[selectedContact.id] || [];
setMessages({
...messages,
[selectedContact.id]: [...contactMessages, { id: contactMessages.length + 1, text: newMessage, date: new Date() }],
});
setNewMessage('');
simulateContactResponse(selectedContact.id);
}
};
const handleKeyPress = (event) => {
if (event.key === 'Enter') {
handleSendMessage();
}
};
const simulateContactResponse = (contactId) => {
setTimeout(() => {
setMessages((prevMessages) => {
const contactMessages = prevMessages[contactId] || [];
return {
...prevMessages,
[contactId]: [...contactMessages, { id: contactMessages.length + 2, text: 'Réponse automatique', isResponse: true, date: new Date() }],
};
});
}, 2000);
};
return (
<div className="flex" style={{ height: 'calc(100vh - 128px )' }}> {/* Utilisation de calc pour soustraire la hauteur de l'entête */}
<div className="w-1/4 border-r border-gray-200 p-4 overflow-y-auto h-full ">
{contacts.map((contact) => (
<div
key={contact.id}
className={`p-2 cursor-pointer ${selectedContact?.id === contact.id ? 'bg-gray-200' : ''}`}
onClick={() => setSelectedContact(contact)}
>
<img src={contact.profilePic} alt={`${contact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" />
{contact.name}
</div>
))}
</div>
<div className="flex-1 flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 h-full">
{selectedContact && (messages[selectedContact.id] || []).map((message) => (
<div
key={message.id}
className={`mb-2 p-2 rounded max-w-xs ${message.isResponse ? 'bg-gray-200 justify-self-end' : 'bg-emerald-200 justify-self-start'}`}
style={{ borderRadius: message.isResponse ? '20px 20px 0 20px' : '20px 20px 20px 0', minWidth: '25%' }}
>
<div className="flex items-center mb-1">
<img src={selectedContact.profilePic} alt={`${selectedContact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" />
<span className="text-xs text-gray-600">{selectedContact.name}</span>
<span className="text-xs text-gray-400 ml-2">{new Date(message.date).toLocaleTimeString()}</span>
</div>
{message.text}
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="p-4 border-t border-gray-200 flex">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
className="w-full p-2 border border-gray-300 rounded"
placeholder="Écrire un message..."
onKeyDown={handleKeyPress}
/>
<button
onClick={handleSendMessage}
className="p-2 bg-emerald-500 text-white rounded mr-2"
>
<SendHorizontal />
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,90 @@
'use client'
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Table from '@/components/Table';
import { Edit } from 'lucide-react';
import StatusLabel from '@/components/StatusLabel';
import useLocalStorage from '@/hooks/useLocalStorage';
import { BK_GESTIONINSCRIPTION_ENFANTS_URL , FR_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url';
export default function ParentHomePage() {
const [actions, setActions] = useState([]);
const [children, setChildren] = useState([]);
const [userId, setUserId] = useLocalStorage("userId", '') ;
const router = useRouter();
useEffect(() => {
if (!userId) return;
const fetchActions = async () => {
const response = await fetch('/api/actions');
const data = await response.json();
setActions(data);
};
const fetchEleves = async () => {
const response = await fetch(`${BK_GESTIONINSCRIPTION_ENFANTS_URL}/${userId}`);
const data = await response.json();
console.log(data);
setChildren(data);
};
fetchEleves();
}, [userId]);
function handleEdit(eleveId) {
// Logique pour éditer le dossier de l'élève
console.log(`Edit dossier for eleve id: ${eleveId}`);
router.push(`${FR_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&idEleve=${eleveId}`);
}
const actionColumns = [
{ name: 'Action', transform: (row) => row.action },
];
const getShadowColor = (etat) => {
switch (etat) {
case 1:
return 'shadow-blue-500'; // Couleur d'ombre plus visible
case 2:
return 'shadow-orange-500'; // Couleur d'ombre plus visible
case 3:
return 'shadow-purple-500'; // Couleur d'ombre plus visible
case 4:
return 'shadow-red-500'; // Couleur d'ombre plus visible
case 5:
return 'shadow-green-500'; // Couleur d'ombre plus visible
case 6:
return 'shadow-red-500'; // Couleur d'ombre plus visible
default:
return 'shadow-green-500'; // Couleur d'ombre plus visible
}
};
return (
<div>
<div>
<h2 className="text-xl font-semibold mb-4">Dernières actions à effectuer</h2>
<Table data={actions} columns={actionColumns} itemsPerPage={5} />
</div>
<div>
<h2 className="text-xl font-semibold mb-4">Enfants</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{children.map((child) => (
<div key={child.eleve.id} className={`border p-4 rounded shadow ${getShadowColor(child.etat)}`}>
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">{child.eleve.nom} {child.eleve.prenom}</h3>
<Edit className="cursor-pointer" onClick={() => handleEdit(child.eleve.id)} />
</div>
<StatusLabel etat={child.etat } showDropdown={false}/>
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,74 @@
'use client'
import React, { useState } from 'react';
import Button from '@/components/Button';
import InputText from '@/components/InputText';
export default function SettingsPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const handleEmailChange = (e) => {
setEmail(e.target.value);
};
const handlePasswordChange = (e) => {
setPassword(e.target.value);
};
const handleConfirmPasswordChange = (e) => {
setConfirmPassword(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);
};
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>
);
}

View File

@ -0,0 +1,124 @@
'use client'
import React, { useState, useEffect } from 'react'
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import Logo from '@/components/Logo';
import { useSearchParams, useRouter } from 'next/navigation'
import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Button'; // Importez le composant Button
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import { BK_LOGIN_URL, FR_ADMIN_STUDENT_EDIT_SUBSCRIBE, FR_PARENTS_HOME_URL, FR_USERS_NEW_PASSWORD_URL, FR_USERS_SUBSCRIBE_URL } from '@/utils/Url';
import useLocalStorage from '@/hooks/useLocalStorage';
import useCsrfToken from '@/hooks/useCsrfToken';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page() {
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState("");
const [userFieldError,setUserFieldError] = useState("")
const [passwordFieldError,setPasswordFieldError] = useState("")
const [isLoading, setIsLoading] = useState(false);
const [userId, setUserId] = useLocalStorage("userId", '') ;
const router = useRouter();
const csrfToken = useCsrfToken();
function isOK(data) {
return data.errorMessage === ""
}
function handleFormLogin(formData) {
if (useFakeData) {
// Simuler une réponse réussie
const data = {
errorFields: {},
errorMessage: "",
profil: "fakeProfileId"
};
setUserFieldError("")
setPasswordFieldError("")
setErrorMessage("")
if(isOK(data)){
localStorage.setItem('userId', data.profil); // Stocker l'identifiant de l'utilisateur
router.push(`${FR_ADMIN_STUDENT_EDIT_SUBSCRIBE}?id=${data.profil}`);
} else {
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPasswordFieldError(data.errorFields.password);
}
if(data.errorMessage){
setErrorMessage(data.errorMessage)
}
}
} else {
const request = new Request(
`${BK_LOGIN_URL}`,
{
method:'POST',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify( {
email: formData.get('login'),
password: formData.get('password'),
}),
credentials: 'include',
}
);
fetch(request).then(response => response.json())
.then(data => {
console.log('Success:', data);
setUserFieldError("")
setPasswordFieldError("")
setErrorMessage("")
if(isOK(data)){
localStorage.setItem('userId', data.profil); // Stocker l'identifiant de l'utilisateur
router.push(`${FR_PARENTS_HOME_URL}`);
} else {
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPasswordFieldError(data.errorFields.password);
}
if(data.errorMessage){
setErrorMessage(data.errorMessage)
}
}
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.message;
console.log(error);
});
}
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader
} else {
return <>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl text-emerald-900 font-bold text-center mb-4">Authentification</h1>
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); handleFormLogin(new FormData(e.target)); }}>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="login" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full" />
<InputTextIcon name="password" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={passwordFieldError} className="w-full" />
<div className="input-group mb-4">
</div>
<label className="text-red-500">{errorMessage}</label>
<label><a className="float-right text-emerald-900" href={`${FR_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>
</form>
</div>
</>
}
};

View File

@ -0,0 +1,107 @@
'use client'
import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import Logo from '@/components/Logo';
import { useSearchParams, useRouter } from 'next/navigation';
import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Button'; // Importez le composant Button
import Popup from '@/components/Popup'; // Importez le composant Popup
import { User } from 'lucide-react'; // Importez directement les icônes nécessaires
import { BK_NEW_PASSWORD_URL,FR_USERS_LOGIN_URL } from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page() {
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState("");
const [userFieldError, setUserFieldError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [popupConfirmAction, setPopupConfirmAction] = useState(null);
const csrfToken = useCsrfToken();
function validate(formData) {
if (useFakeData) {
setTimeout(() => {
setUserFieldError("");
setErrorMessage("");
setPopupMessage("Mot de passe réinitialisé avec succès !");
setPopupConfirmAction(() => () => setPopupVisible(false));
setPopupVisible(true);
}, 1000); // Simule un délai de traitement
} else {
const request = new Request(
`${BK_NEW_PASSWORD_URL}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify({
email: formData.get('email')
}),
}
);
fetch(request).then(response => response.json())
.then(data => {
console.log('Success:', data);
setUserFieldError("");
setErrorMessage("");
if (data.errorMessage === "") {
setPopupMessage(data.message);
setPopupConfirmAction(() => () => setPopupVisible(false));
setPopupVisible(true);
} else {
if (data.errorFields) {
setUserFieldError(data.errorFields.email);
}
if (data.errorMessage) {
setErrorMessage(data.errorMessage);
}
}
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(error);
});
}
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader
} else {
return <>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">Nouveau Mot de passe</h1>
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); validate(new FormData(e.target)); }}>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="email" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full" />
<p className="text-red-500">{errorMessage}</p>
<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={ `${FR_USERS_LOGIN_URL}`} />
</div>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={popupConfirmAction}
onCancel={() => setPopupVisible(false)}
/>
</>
}
}

View File

@ -0,0 +1,144 @@
'use client'
// src/app/pages/subscribe.js
import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import Logo from '@/components/Logo';
import { useSearchParams, useRouter } from 'next/navigation'
import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Button'; // Importez le composant Button
import Popup from '@/components/Popup';
import { BK_RESET_PASSWORD_URL, FR_USERS_LOGIN_URL } from '@/utils/Url';
import { KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import useCsrfToken from '@/hooks/useCsrfToken';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page() {
const searchParams = useSearchParams();
const uuid = searchParams.get('uuid');
const [errorMessage, setErrorMessage] = useState("");
const [password1FieldError,setPassword1FieldError] = useState("")
const [password2FieldError,setPassword2FieldError] = useState("")
const [isLoading, setIsLoading] = useState(true);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => {
if (useFakeData) {
setTimeout(() => {
setIsLoading(false);
}, 1000);
} else {
const url= `${BK_RESET_PASSWORD_URL}/${uuid}`;
fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
console.log('Success:', data);
setIsLoading(true);
if(data.errorFields){
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
if(data.errorMessage){
setErrorMessage(data.errorMessage)
}
setIsLoading(false);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
}, []);
function validate(formData) {
if (useFakeData) {
setTimeout(() => {
setPopupMessage("Mot de passe réinitialisé avec succès");
setPopupVisible(true);
}, 1000);
} else {
const request = new Request(
`${BK_RESET_PASSWORD_URL}/${uuid}`,
{
method:'POST',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify( {
password1: formData.get('password1'),
password2: formData.get('password2'),
}),
}
);
fetch(request).then(response => response.json())
.then(data => {
console.log('Success:', data);
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
if(data.errorMessage === ""){
setPopupMessage(data.message);
setPopupVisible(true);
} else {
if(data.errorMessage){
setErrorMessage(data.errorMessage);
}
if(data.errorFields){
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
}
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(error);
});
}
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader
} else {
return <>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => {
setPopupVisible(false);
router.push(`${FR_USERS_LOGIN_URL}`);
}}
onCancel={() => setPopupVisible(false)}
/>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">Réinitialisation du mot de passe</h1>
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); validate(new FormData(e.target)); }}>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="password1" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={password1FieldError} className="w-full" />
<InputTextIcon name="password2" type="password" IconItem={KeySquare} label="Confirmation mot de passe" placeholder="Confirmation mot de passe" errorMsg={password2FieldError} className="w-full" />
<label className="text-red-500">{errorMessage}</label>
<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={`${FR_USERS_LOGIN_URL}`} />
</div>
</div>
</>
}
}

View File

@ -0,0 +1,180 @@
'use client'
// src/app/pages/subscribe.js
import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import Logo from '@/components/Logo';
import { useSearchParams, useRouter } from 'next/navigation'
import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Button'; // Importez le composant Button
import Popup from '@/components/Popup'; // Importez le composant Popup
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import { BK_REGISTER_URL, FR_USERS_LOGIN_URL } from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page() {
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState("");
const [userFieldError,setUserFieldError] = useState("")
const [password1FieldError,setPassword1FieldError] = useState("")
const [password2FieldError,setPassword2FieldError] = useState("")
const [isLoading, setIsLoading] = useState(true);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => {
if (useFakeData) {
// Simuler une réponse réussie
const data = {
errorFields: {},
errorMessage: ""
};
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
setIsLoading(false);
} else {
const url= `${BK_REGISTER_URL}`;
fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
console.log('Success:', data);
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
setIsLoading(true);
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
if(data.errorMessage){
setErrorMessage(data.errorMessage)
}
setIsLoading(false);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
}, []);
function isOK(data) {
return data.errorMessage === ""
}
function suscribe(formData) {
if (useFakeData) {
// Simuler une réponse réussie
const data = {
errorFields: {},
errorMessage: ""
};
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
if(isOK(data)){
setPopupMessage("Votre compte a été créé avec succès");
setPopupVisible(true);
} else {
if(data.errorMessage){
setErrorMessage(data.errorMessage);
}
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
}
} else {
const request = new Request(
`${BK_REGISTER_URL}`,
{
method:'POST',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify( {
email: formData.get('login'),
password1: formData.get('password1'),
password2: formData.get('password2'),
}),
}
);
fetch(request).then(response => response.json())
.then(data => {
console.log('Success:', data);
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
if(isOK(data)){
setPopupMessage(data.message);
setPopupVisible(true);
} else {
if(data.errorMessage){
setErrorMessage(data.errorMessage);
}
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
}
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(error);
});
}
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader
} else {
return <>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">Nouveau profil</h1>
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); suscribe(new FormData(e.target)); }}>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="login" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full" />
<InputTextIcon name="password1" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={password1FieldError} className="w-full" />
<InputTextIcon name="password2" type="password" IconItem={KeySquare} label="Confirmation mot de passe" placeholder="Confirmation mot de passe" errorMsg={password2FieldError} className="w-full" />
<p className="text-red-500">{errorMessage}</p>
<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(`${FR_USERS_LOGIN_URL}`)}} /></div>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => {
setPopupVisible(false);
router.push(`${FR_USERS_LOGIN_URL}`);
}}
onCancel={() => setPopupVisible(false)}
/>
</>
}
}