feat: Gestion du planning [3]

This commit is contained in:
Luc SORIGNET
2025-05-03 15:12:17 +02:00
parent cb4fe74a9e
commit 58144ba0d0
39 changed files with 939 additions and 1864 deletions

View File

@ -43,7 +43,6 @@ export default function Layout({ children }) {
const { profileRole, establishments, user, clearContext } =
useEstablishment();
// Déplacer le reste du code ici...
const sidebarItems = {
admin: {
id: 'admin',
@ -144,12 +143,40 @@ export default function Layout({ children }) {
return (
<ProtectedRoute requiredRight={[RIGHTS.ADMIN, RIGHTS.TEACHER]}>
<div className="flex min-h-screen bg-gray-50 relative">
{/* Retirer la condition !isLoading car on gère déjà le chargement au début */}
{/* Sidebar avec hauteur forcée */}
{/* Topbar */}
<header className="absolute top-0 left-0 right-0 h-16 bg-white border-b border-gray-200 px-4 md:px-8 flex items-center justify-between z-10 box-border">
<div className="flex items-center">
<button
className="mr-4 md:hidden text-gray-600 hover:text-gray-900"
onClick={toggleSidebar}
aria-label="Toggle menu"
>
{isSidebarOpen ? <X size={24} /> : <Menu size={24} />}
</button>
<div className="text-lg md:text-xl font-semibold">{headerTitle}</div>
</div>
<DropdownMenu
buttonContent={
<Image
src={getGravatarUrl(user?.email)}
alt="Profile"
className="w-8 h-8 rounded-full cursor-pointer"
width={32}
height={32}
/>
}
items={dropdownItems}
buttonClassName=""
menuClassName="absolute right-0 mt-2 w-64 bg-white border border-gray-200 rounded shadow-lg"
/>
</header>
{/* Sidebar */}
<div
className={`md:block ${isSidebarOpen ? 'block' : 'hidden'} fixed md:relative inset-y-0 left-0 z-30 h-full`}
style={{ height: '100vh' }} // Force la hauteur à 100% de la hauteur de la vue
className={`absolute top-16 bottom-16 left-0 z-30 w-64 bg-white border-r border-gray-200 box-border ${
isSidebarOpen ? 'block' : 'hidden md:block'
}`}
>
<Sidebar
establishments={establishments}
@ -159,7 +186,7 @@ export default function Layout({ children }) {
/>
</div>
{/* Overlay pour fermer la sidebar en cliquant à l'extérieur sur mobile */}
{/* Overlay for mobile */}
{isSidebarOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-20 md:hidden"
@ -167,48 +194,19 @@ export default function Layout({ children }) {
/>
)}
<div className="flex-1 flex flex-col">
{/* Header responsive */}
<header className="h-16 bg-white border-b border-gray-200 px-4 md:px-8 py-4 flex items-center justify-between z-10">
<div className="flex items-center">
<button
className="mr-4 md:hidden text-gray-600 hover:text-gray-900"
onClick={toggleSidebar}
aria-label="Toggle menu"
>
{isSidebarOpen ? <X size={24} /> : <Menu size={24} />}
</button>
<div className="text-lg md:text-xl font-semibold">
{headerTitle}
</div>
</div>
<DropdownMenu
buttonContent={
<Image
src={getGravatarUrl(user?.email)}
alt="Profile"
className="w-8 h-8 rounded-full cursor-pointer"
width={32}
height={32}
/>
}
items={dropdownItems}
buttonClassName=""
menuClassName="absolute right-0 mt-2 w-64 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 p-4 md:p-6">{children}</div>
{/* Footer responsive */}
<Footer
softwareName={softwareName}
softwareVersion={softwareVersion}
/>
</div>
{/* Main container */}
<div className="absolute overflow-auto bg-gray-50 top-16 bottom-16 left-64 right-0 ">
{children}
</div>
</div>
{/* Footer */}
<Footer
softwareName={softwareName}
softwareVersion={softwareVersion}
/>
<Popup
visible={isPopupVisible}
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?"

View File

@ -77,7 +77,7 @@ export default function DashboardPage() {
});
// Fetch des événements à venir
fetchUpcomingEvents()
fetchUpcomingEvents(selectedEstablishmentId)
.then((data) => {
setUpcomingEvents(data);
})

View File

@ -1,9 +1,10 @@
'use client';
import { PlanningProvider } from '@/context/PlanningContext';
import Calendar from '@/components/Calendar';
import EventModal from '@/components/EventModal';
import ScheduleNavigation from '@/components/ScheduleNavigation';
import { PlanningModes, PlanningProvider, RecurrenceType } from '@/context/PlanningContext';
import Calendar from '@/components/Calendar/Calendar';
import EventModal from '@/components/Calendar/EventModal';
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
import { useState } from 'react';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function Page() {
const [isModalOpen, setIsModalOpen] = useState(false);
@ -14,13 +15,14 @@ export default function Page() {
end: '',
location: '',
planning: '', // Enlever la valeur par défaut ici
recurrence: 'none',
recursionType: RecurrenceType.NONE,
selectedDays: [],
recurrenceEnd: '',
recursionEnd: '',
customInterval: 1,
customUnit: 'days',
viewType: 'week', // Ajouter la vue semaine par défaut
});
const { selectedEstablishmentId } = useEstablishment();
const initializeNewEvent = (date = new Date()) => {
// S'assurer que date est un objet Date valide
@ -33,9 +35,11 @@ export default function Page() {
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
location: '',
planning: '', // Ne pas définir de valeur par défaut ici non plus
recurrence: 'none',
recursionType: RecurrenceType.NONE,
selectedDays: [],
recurrenceEnd: '',
recursionEnd: new Date(
eventDate.getTime() + 2 * 60 * 60 * 1000
).toISOString(),
customInterval: 1,
customUnit: 'days',
});
@ -43,7 +47,8 @@ export default function Page() {
};
return (
<PlanningProvider>
<PlanningProvider establishmentId={selectedEstablishmentId} modeSet={PlanningModes.PLANNING}>
{/* <div className="flex h-full overflow-hidden"> */}
<div className="flex h-full overflow-hidden">
<ScheduleNavigation />
<Calendar

View File

@ -29,12 +29,12 @@ import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManag
import { fetchRegistrationSchoolFileMasters } from '@/app/actions/registerFileGroupAction';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
export default function Page() {
const [specialities, setSpecialities] = useState([]);
const [classes, setClasses] = useState([]);
const [teachers, setTeachers] = useState([]);
const [schedules, setSchedules] = useState([]);
const [registrationDiscounts, setRegistrationDiscounts] = useState([]);
const [tuitionDiscounts, setTuitionDiscounts] = useState([]);
const [registrationFees, setRegistrationFees] = useState([]);
@ -60,8 +60,6 @@ export default function Page() {
// Fetch data for classes
handleClasses();
// Fetch data for schedules
handleSchedules();
// Fetch data for registration discounts
handleRegistrationDiscounts();
@ -128,13 +126,6 @@ export default function Page() {
.catch((error) => logger.error('Error fetching classes:', error));
};
const handleSchedules = () => {
fetchSchedules()
.then((data) => {
setSchedules(data);
})
.catch((error) => logger.error('Error fetching schedules:', error));
};
const handleRegistrationDiscounts = () => {
fetchRegistrationDiscounts(selectedEstablishmentId)
@ -299,6 +290,8 @@ export default function Page() {
<ScheduleManagement
handleUpdatePlanning={handleUpdatePlanning}
classes={classes}
specialities={specialities}
teachers={teachers}
/>
</ClassesProvider>
),
@ -343,12 +336,12 @@ export default function Page() {
];
return (
<div className="p-4">
<>
<PlanningProvider establishmentId={selectedEstablishmentId} modeSet={PlanningModes.CLASS_SCHEDULE}>
<DjangoCSRFToken csrfToken={csrfToken} />
<SidebarTabs tabs={tabs} />
</PlanningProvider>
</>
<div className="w-full p-4">
<SidebarTabs tabs={tabs} />
</div>
</div>
);
}

View File

@ -2,12 +2,14 @@ import { BE_PLANNING_PLANNINGS_URL, BE_PLANNING_EVENTS_URL } from '@/utils/Url';
const requestResponseHandler = async (response) => {
const body = response.status !== 204 ? await response?.json() : {};
console.log(response);
if (response.ok) {
return body;
}
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
const error = new Error(
body?.errorMessage ||
`Une erreur est survenue code de retour : ${response.status}`
);
error.details = body;
throw error;
};
@ -51,8 +53,15 @@ const removeDatas = (url, csrfToken) => {
}).then(requestResponseHandler);
};
export const fetchPlannings = () => {
return getData(`${BE_PLANNING_PLANNINGS_URL}`);
export const fetchPlannings = (establishment_id=null,planningMode=null) => {
let url = `${BE_PLANNING_PLANNINGS_URL}`;
if (establishment_id) {
url += `?establishment_id=${establishment_id}`;
}
if (planningMode) {
url += `&planning_mode=${planningMode}`;
}
return getData(url);
};
export const getPlanning = (id) => {
@ -71,8 +80,17 @@ export const deletePlanning = (id, csrfToken) => {
return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken);
};
export const fetchEvents = () => {
return getData(`${BE_PLANNING_EVENTS_URL}`);
export const fetchEvents = (establishment_id=null, planningMode=null) => {
let url = `${BE_PLANNING_EVENTS_URL}`;
if (establishment_id) {
url += `?establishment_id=${establishment_id}`;
}
if (planningMode) {
url += `&planning_mode=${planningMode}`;
}
return getData(url);
};
export const getEvent = (id) => {
@ -91,6 +109,10 @@ export const deleteEvent = (id, csrfToken) => {
return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken);
};
export const fetchUpcomingEvents = () => {
return getData(`${BE_PLANNING_EVENTS_URL}/upcoming`);
export const fetchUpcomingEvents = (establishment_id=null) => {
let url = `${BE_PLANNING_EVENTS_URL}/upcoming`;
if (establishment_id) {
url += `?establishment_id=${establishment_id}`;
}
return getData(`${url}`);
};

View File

@ -28,7 +28,7 @@ export default async function RootLayout({ children, params }) {
return (
<html lang={locale}>
<body>
<body className='p-0 m-0'>
<Providers messages={messages} locale={locale} session={params.session}>
{children}
</Providers>