Merge pull request 'feat: Gestion du planning [3]' (#57) from feat-3-Gestion_du_planning into develop

Reviewed-on: https://git.v0id.ovh/n3wt-innov/n3wt-school/pulls/57
This commit is contained in:
Luc SORIGNET
2025-05-04 10:05:59 +00:00
39 changed files with 939 additions and 1864 deletions

View File

@ -2,6 +2,7 @@ from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from School.models import SchoolClass
from Establishment.models import Establishment from Establishment.models import Establishment
@ -14,25 +15,33 @@ class RecursionType(models.IntegerChoices):
class Planning(models.Model): class Planning(models.Model):
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT) establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT)
school_class = models.ForeignKey(
SchoolClass,
on_delete=models.CASCADE,
related_name="planning",
null=True, # Permet des valeurs nulles
blank=True # Rend le champ facultatif dans les formulaires
)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
description = models.TextField(default="", blank=True, null=True) description = models.TextField(default="", blank=True, null=True)
color= models.CharField(max_length=255, default="#000000") color= models.CharField(max_length=255, default="#000000")
def __str__(self): def __str__(self):
return f'Planning for {self.user.username}' return f'Planning {self.name}'
class Events(models.Model): class Events(models.Model):
planning = models.ForeignKey(Planning, on_delete=models.PROTECT) planning = models.ForeignKey(Planning, on_delete=models.CASCADE)
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
description = models.TextField() description = models.TextField(default="", blank=True, null=True)
start = models.DateTimeField() start = models.DateTimeField()
end = models.DateTimeField() end = models.DateTimeField()
recursionType = models.IntegerField(choices=RecursionType, default=0) recursionType = models.IntegerField(choices=RecursionType, default=0)
recursionEnd = models.DateTimeField(default=None, blank=True, null=True)
color= models.CharField(max_length=255) color= models.CharField(max_length=255)
location = models.CharField(max_length=255, default="", blank=True, null=True) location = models.CharField(max_length=255, default="", blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return f'Event for {self.user.username}' return f'Event {self.title}'

View File

@ -1,18 +1,32 @@
from django.http.response import JsonResponse from django.http.response import JsonResponse
from rest_framework.views import APIView from rest_framework.views import APIView
from django.utils import timezone from django.utils import timezone
from dateutil.relativedelta import relativedelta
from .models import Planning, Events from .models import Planning, Events, RecursionType
from .serializers import PlanningSerializer, EventsSerializer from .serializers import PlanningSerializer, EventsSerializer
from N3wtSchool import bdd from N3wtSchool import bdd
class PlanningView(APIView): class PlanningView(APIView):
def get(self, request): def get(self, request):
plannings=bdd.getAllObjects(Planning) establishment_id = request.GET.get('establishment_id', None)
planning_serializer=PlanningSerializer(plannings, many=True) planning_mode = request.GET.get('planning_mode', None)
plannings = bdd.getAllObjects(Planning)
if establishment_id is not None:
plannings = plannings.filter(establishment=establishment_id)
# Filtrer en fonction du planning_mode
if planning_mode == "classSchedule":
plannings = plannings.filter(school_class__isnull=False)
elif planning_mode == "planning":
plannings = plannings.filter(school_class__isnull=True)
planning_serializer = PlanningSerializer(plannings.distinct(), many=True)
return JsonResponse(planning_serializer.data, safe=False) return JsonResponse(planning_serializer.data, safe=False)
def post(self, request): def post(self, request):
@ -56,17 +70,63 @@ class PlanningWithIdView(APIView):
class EventsView(APIView): class EventsView(APIView):
def get(self, request): def get(self, request):
events = bdd.getAllObjects(Events) establishment_id = request.GET.get('establishment_id', None)
planning_mode = request.GET.get('planning_mode', None)
filterParams = {}
plannings=[]
events = Events.objects.all()
if establishment_id is not None :
filterParams['establishment'] = establishment_id
if planning_mode is not None:
filterParams['school_class__isnull'] = (planning_mode!="classSchedule")
if filterParams:
plannings = Planning.objects.filter(**filterParams)
events = Events.objects.filter(planning__in=plannings)
events_serializer = EventsSerializer(events, many=True) events_serializer = EventsSerializer(events, many=True)
return JsonResponse(events_serializer.data, safe=False) return JsonResponse(events_serializer.data, safe=False)
def post(self, request): def post(self, request):
events_serializer = EventsSerializer(data=request.data) events_serializer = EventsSerializer(data=request.data)
if events_serializer.is_valid(): if events_serializer.is_valid():
events_serializer.save() event = events_serializer.save()
# Gérer les événements récurrents
if event.recursionType != RecursionType.RECURSION_NONE:
self.create_recurring_events(event)
return JsonResponse(events_serializer.data, status=201) return JsonResponse(events_serializer.data, status=201)
return JsonResponse(events_serializer.errors, status=400) return JsonResponse(events_serializer.errors, status=400)
def create_recurring_events(self, event):
current_start = event.start
current_end = event.end
while current_start < event.recursionEnd:
if event.recursionType == RecursionType.RECURSION_DAILY:
current_start += relativedelta(days=1)
current_end += relativedelta(days=1)
elif event.recursionType == RecursionType.RECURSION_WEEKLY:
current_start += relativedelta(weeks=1)
current_end += relativedelta(weeks=1)
elif event.recursionType == RecursionType.RECURSION_MONTHLY:
current_start += relativedelta(months=1)
current_end += relativedelta(months=1)
else:
break # Pour d'autres types de récurrence non gérés
# Créer une nouvelle occurrence
Events.objects.create(
planning=event.planning,
title=event.title,
description=event.description,
start=current_start,
end=current_end,
recursionEnd=event.recursionEnd,
recursionType=event.recursionType, # Les occurrences ne sont pas récurrentes
color=event.color,
location=event.location,
)
class EventsWithIdView(APIView): class EventsWithIdView(APIView):
def put(self, request, id): def put(self, request, id):
try: try:
@ -92,6 +152,18 @@ class EventsWithIdView(APIView):
class UpcomingEventsView(APIView): class UpcomingEventsView(APIView):
def get(self, request): def get(self, request):
current_date = timezone.now() current_date = timezone.now()
upcoming_events = Events.objects.filter(start__gte=current_date) establishment_id = request.GET.get('establishment_id', None)
if establishment_id is not None:
# Filtrer les plannings par establishment_id et sans school_class
plannings = Planning.objects.filter(establishment=establishment_id, school_class__isnull=True)
# Filtrer les événements associés à ces plannings et qui sont à venir
upcoming_events = Events.objects.filter(planning__in=plannings, start__gte=current_date)
else:
# Récupérer tous les événements à venir si aucun establishment_id n'est fourni
# et les plannings ne doivent pas être rattachés à une school_class
plannings = Planning.objects.filter(school_class__isnull=True)
upcoming_events = Events.objects.filter(planning__in=plannings, start__gte=current_date)
events_serializer = EventsSerializer(upcoming_events, many=True) events_serializer = EventsSerializer(upcoming_events, many=True)
return JsonResponse(events_serializer.data, safe=False) return JsonResponse(events_serializer.data, safe=False)

View File

@ -43,7 +43,6 @@ export default function Layout({ children }) {
const { profileRole, establishments, user, clearContext } = const { profileRole, establishments, user, clearContext } =
useEstablishment(); useEstablishment();
// Déplacer le reste du code ici...
const sidebarItems = { const sidebarItems = {
admin: { admin: {
id: 'admin', id: 'admin',
@ -144,32 +143,9 @@ export default function Layout({ children }) {
return ( return (
<ProtectedRoute requiredRight={[RIGHTS.ADMIN, RIGHTS.TEACHER]}> <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 */}
<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
>
<Sidebar
establishments={establishments}
currentPage={currentPage}
items={Object.values(sidebarItems)}
onCloseMobile={toggleSidebar}
/>
</div>
{/* Overlay pour fermer la sidebar en cliquant à l'extérieur sur mobile */} {/* Topbar */}
{isSidebarOpen && ( <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="fixed inset-0 bg-black bg-opacity-50 z-20 md:hidden"
onClick={toggleSidebar}
/>
)}
<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"> <div className="flex items-center">
<button <button
className="mr-4 md:hidden text-gray-600 hover:text-gray-900" className="mr-4 md:hidden text-gray-600 hover:text-gray-900"
@ -178,9 +154,7 @@ export default function Layout({ children }) {
> >
{isSidebarOpen ? <X size={24} /> : <Menu size={24} />} {isSidebarOpen ? <X size={24} /> : <Menu size={24} />}
</button> </button>
<div className="text-lg md:text-xl font-semibold"> <div className="text-lg md:text-xl font-semibold">{headerTitle}</div>
{headerTitle}
</div>
</div> </div>
<DropdownMenu <DropdownMenu
buttonContent={ buttonContent={
@ -197,18 +171,42 @@ export default function Layout({ children }) {
menuClassName="absolute right-0 mt-2 w-64 bg-white border border-gray-200 rounded shadow-lg" menuClassName="absolute right-0 mt-2 w-64 bg-white border border-gray-200 rounded shadow-lg"
/> />
</header> </header>
{/* Main Content */}
<div className="flex-1 flex flex-col"> {/* Sidebar */}
{/* Content avec scroll si nécessaire */} <div
<div className="flex-1 overflow-auto p-4 md:p-6">{children}</div> className={`absolute top-16 bottom-16 left-0 z-30 w-64 bg-white border-r border-gray-200 box-border ${
{/* Footer responsive */} isSidebarOpen ? 'block' : 'hidden md:block'
}`}
>
<Sidebar
establishments={establishments}
currentPage={currentPage}
items={Object.values(sidebarItems)}
onCloseMobile={toggleSidebar}
/>
</div>
{/* Overlay for mobile */}
{isSidebarOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-20 md:hidden"
onClick={toggleSidebar}
/>
)}
{/* Main container */}
<div className="absolute overflow-auto bg-gray-50 top-16 bottom-16 left-64 right-0 ">
{children}
</div>
{/* Footer */}
<Footer <Footer
softwareName={softwareName} softwareName={softwareName}
softwareVersion={softwareVersion} softwareVersion={softwareVersion}
/> />
</div>
</div>
</div>
<Popup <Popup
visible={isPopupVisible} visible={isPopupVisible}
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?" 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 // Fetch des événements à venir
fetchUpcomingEvents() fetchUpcomingEvents(selectedEstablishmentId)
.then((data) => { .then((data) => {
setUpcomingEvents(data); setUpcomingEvents(data);
}) })

View File

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

View File

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

View File

@ -2,12 +2,14 @@ import { BE_PLANNING_PLANNINGS_URL, BE_PLANNING_EVENTS_URL } from '@/utils/Url';
const requestResponseHandler = async (response) => { const requestResponseHandler = async (response) => {
const body = response.status !== 204 ? await response?.json() : {}; const body = response.status !== 204 ? await response?.json() : {};
console.log(response);
if (response.ok) { if (response.ok) {
return body; return body;
} }
// Throw an error with the JSON body containing the form errors // 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; error.details = body;
throw error; throw error;
}; };
@ -51,8 +53,15 @@ const removeDatas = (url, csrfToken) => {
}).then(requestResponseHandler); }).then(requestResponseHandler);
}; };
export const fetchPlannings = () => { export const fetchPlannings = (establishment_id=null,planningMode=null) => {
return getData(`${BE_PLANNING_PLANNINGS_URL}`); 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) => { export const getPlanning = (id) => {
@ -71,8 +80,17 @@ export const deletePlanning = (id, csrfToken) => {
return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken); return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken);
}; };
export const fetchEvents = () => { export const fetchEvents = (establishment_id=null, planningMode=null) => {
return getData(`${BE_PLANNING_EVENTS_URL}`); 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) => { export const getEvent = (id) => {
@ -91,6 +109,10 @@ export const deleteEvent = (id, csrfToken) => {
return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken); return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken);
}; };
export const fetchUpcomingEvents = () => { export const fetchUpcomingEvents = (establishment_id=null) => {
return getData(`${BE_PLANNING_EVENTS_URL}/upcoming`); 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 ( return (
<html lang={locale}> <html lang={locale}>
<body> <body className='p-0 m-0'>
<Providers messages={messages} locale={locale} session={params.session}> <Providers messages={messages} locale={locale} session={params.session}>
{children} {children}
</Providers> </Providers>

View File

@ -22,7 +22,7 @@ import { fr } from 'date-fns/locale';
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
import logger from '@/utils/logger'; import logger from '@/utils/logger';
const Calendar = ({ onDateClick, onEventClick }) => { const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
const { const {
currentDate, currentDate,
setCurrentDate, setCurrentDate,

View File

@ -1,4 +1,4 @@
import { usePlanning } from '@/context/PlanningContext'; import { usePlanning, RecurrenceType } from '@/context/PlanningContext';
import { format } from 'date-fns'; import { format } from 'date-fns';
import React from 'react'; import React from 'react';
@ -13,23 +13,23 @@ export default function EventModal({
// S'assurer que planning est défini lors du premier rendu // S'assurer que planning est défini lors du premier rendu
React.useEffect(() => { React.useEffect(() => {
if (!eventData.planning && schedules.length > 0) { if (!eventData?.planning && schedules.length > 0) {
setEventData((prev) => ({ setEventData((prev) => ({
...prev, ...prev,
planning: schedules[0].id, planning: schedules[0].id,
color: schedules[0].color, color: schedules[0].color,
})); }));
} }
}, [schedules, eventData.planning]); }, [schedules, eventData?.planning]);
if (!isOpen) return null; if (!isOpen) return null;
const recurrenceOptions = [ const recurrenceOptions = [
{ value: 'none', label: 'Aucune' }, { value: RecurrenceType.NONE, label: 'Aucune' },
{ value: 'daily', label: 'Quotidienne' }, { value: RecurrenceType.DAILY, label: 'Quotidienne' },
{ value: 'weekly', label: 'Hebdomadaire' }, { value: RecurrenceType.WEEKLY, label: 'Hebdomadaire' },
{ value: 'monthly', label: 'Mensuelle' }, { value: RecurrenceType.MONTHLY, label: 'Mensuelle' },
{ value: 'custom', label: 'Personnalisée' }, // Nouvelle option /* { value: RecurrenceType.CUSTOM, label: 'Personnalisée' }, */
]; ];
const daysOfWeek = [ const daysOfWeek = [
@ -171,10 +171,13 @@ export default function EventModal({
Récurrence Récurrence
</label> </label>
<select <select
value={eventData.recurrence || 'none'} value={eventData.recursionType || RecurrenceType.NONE}
onChange={(e) => onChange={(e) => {
setEventData({ ...eventData, recurrence: e.target.value }) return setEventData({
} ...eventData,
recursionType: e.target.value,
});
}}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500" className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
> >
{recurrenceOptions.map((option) => ( {recurrenceOptions.map((option) => (
@ -186,46 +189,7 @@ export default function EventModal({
</div> </div>
{/* Paramètres de récurrence personnalisée */} {/* Paramètres de récurrence personnalisée */}
{eventData.recurrence === 'custom' && ( {eventData.recursionType == RecurrenceType.CUSTOM && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Répéter tous les
</label>
<div className="flex gap-2">
<input
type="number"
min="1"
value={eventData.customInterval || 1}
onChange={(e) =>
setEventData({
...eventData,
customInterval: parseInt(e.target.value) || 1,
})
}
className="w-20 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>
<select
value={eventData.customUnit || 'days'}
onChange={(e) =>
setEventData({
...eventData,
customUnit: e.target.value,
})
}
className="flex-1 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
>
<option value="days">Jours</option>
<option value="weeks">Semaines</option>
<option value="months">Mois</option>
</select>
</div>
</div>
</div>
)}
{/* Jours de la semaine (pour récurrence hebdomadaire) */}
{eventData.recurrence === 'weekly' && (
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Jours de répétition Jours de répétition
@ -256,16 +220,25 @@ export default function EventModal({
)} )}
{/* Date de fin de récurrence */} {/* Date de fin de récurrence */}
{eventData.recurrence !== 'none' && ( {eventData.recursionType != RecurrenceType.NONE && (
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Fin de récurrence Fin de récurrence
</label> </label>
<input <input
type="date" type="date"
value={eventData.recurrenceEnd || ''} value={
eventData.recursionEnd
? format(new Date(eventData.recursionEnd), 'yyyy-MM-dd')
: ''
}
onChange={(e) => onChange={(e) =>
setEventData({ ...eventData, recurrenceEnd: e.target.value }) setEventData({
...eventData,
recursionEnd: e.target.value
? new Date(e.target.value).toISOString()
: null,
})
} }
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500" className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/> />

View File

@ -0,0 +1,249 @@
import { useState } from 'react';
import { usePlanning,PlanningModes } from '@/context/PlanningContext';
import { Plus, Edit2, Eye, EyeOff, Check, X } from 'lucide-react';
export default function ScheduleNavigation({classes, modeSet='event'}) {
const {
schedules,
selectedSchedule,
setSelectedSchedule,
hiddenSchedules,
toggleScheduleVisibility,
addSchedule,
updateSchedule,
planningMode,
} = usePlanning();
const [editingId, setEditingId] = useState(null);
const [editedName, setEditedName] = useState('');
const [editedColor, setEditedColor] = useState('');
const [editedSchoolClass, setEditedSchoolClass] = useState(null);
const [isAddingNew, setIsAddingNew] = useState(false);
const [newSchedule, setNewSchedule] = useState({
name: '',
color: '#10b981',
school_class: '', // Ajout du champ pour la classe
});
const handleEdit = (schedule) => {
setEditingId(schedule.id);
setEditedName(schedule.name);
setEditedColor(schedule.color);
setEditedSchoolClass(schedule.school_class);
};
const handleSave = () => {
if (editingId) {
updateSchedule(editingId, {
...schedules.find((s) => s.id === editingId),
name: editedName,
color: editedColor,
school_class: editedSchoolClass, // Ajout de l'ID de la classe
});
setEditingId(null);
}
};
const handleAddNew = () => {
if (newSchedule.name) {
let payload = {
name: newSchedule.name,
color: newSchedule.color,
};
if (planningMode === PlanningModes.CLASS_SCHEDULE) {
payload.school_class = newSchedule.school_class; // Ajout de l'ID de la classe
}
addSchedule({
id: `schedule-${Date.now()}`,
...payload,
});
setIsAddingNew(false);
setNewSchedule({ name: '', color: '#10b981', school_class: '' });
}
};
return (
<nav className="w-64 border-r p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold">{(planningMode === PlanningModes.CLASS_SCHEDULE)?"Emplois du temps":"Plannings"}</h2>
<button
onClick={() => setIsAddingNew(true)}
className="p-1 hover:bg-gray-100 rounded"
>
<Plus className="w-4 h-4" />
</button>
</div>
{isAddingNew && (
<div className="mb-4 p-2 border rounded">
<input
type="text"
value={newSchedule.name}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, name: e.target.value }))
}
className="w-full p-1 mb-2 border rounded"
placeholder={(planningMode===PlanningModes.CLASS_SCHEDULE)?"Nom de l'emplois du temps":"Nom du planning"}
/>
<div className="flex gap-2 items-center mb-2">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={newSchedule.color}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, color: e.target.value }))
}
className="w-8 h-8"
/>
</div>
{planningMode === PlanningModes.CLASS_SCHEDULE&& (
<div className="mb-2">
<label className="text-sm">Classe (optionnel):</label>
<select
value={newSchedule.school_class}
onChange={(e) =>
setNewSchedule((prev) => ({
...prev,
school_class: e.target.value,
}))
}
className="w-full p-1 border rounded"
>
<option value="">Aucune</option>
{classes.map((classe) => { console.log({classe});
return (
<option key={classe.id} value={classe.id}>
{classe.atmosphere_name}
</option>
)}
)}
</select>
</div>
)}
<div className="flex justify-end gap-2">
<button
onClick={() => setIsAddingNew(false)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleAddNew}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
)}
<ul className="space-y-2">
{schedules
.map((schedule) => (
<li
key={schedule.id}
className={`p-2 rounded ${
selectedSchedule === schedule.id
? 'bg-gray-100'
: 'hover:bg-gray-50'
}`}
>
{editingId === schedule.id ? (
<div className="space-y-2">
<input
type="text"
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className="w-full p-1 border rounded"
/>
<div className="flex gap-2 items-center">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={editedColor}
onChange={(e) => setEditedColor(e.target.value)}
className="w-8 h-8"
/>
</div>
{planningMode === PlanningModes.CLASS_SCHEDULE && (
<div className="mb-2">
<label className="text-sm">Classe:</label>
<select
value={editedSchoolClass}
onChange={(e) => setEditedSchoolClass(e.target.value)}
className="w-full p-1 border rounded"
>
<option value="">Aucune</option>
{classes.map((classe) => (
<option key={classe.id} value={classe.id}>
{classe.atmosphere_name}
</option>
))}
</select>
</div>
)}
<div className="flex justify-end gap-2">
<button
onClick={() => setEditingId(null)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleSave}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
) : (
<div className="flex items-center justify-between">
<div
className="flex items-center gap-2 flex-1 cursor-pointer"
onClick={() => setSelectedSchedule(schedule.id)}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: schedule.color }}
/>
<span
className={
hiddenSchedules.includes(schedule.id)
? 'text-gray-400'
: ''
}
>
{schedule.name}
</span>
</div>
<div className="flex gap-1">
<button
onClick={(e) => {
e.stopPropagation(); // Empêcher la propagation du clic
toggleScheduleVisibility(schedule.id);
}}
className="p-1 hover:bg-gray-100 rounded"
>
{hiddenSchedules.includes(schedule.id) ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleEdit(schedule)}
className="p-1 hover:bg-gray-100 rounded"
>
<Edit2 className="w-4 h-4" />
</button>
</div>
</div>
)}
</li>
))}
</ul>
</nav>
);
}

View File

@ -1,12 +1,6 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { usePlanning } from '@/context/PlanningContext'; import { usePlanning } from '@/context/PlanningContext';
import { import { format, startOfWeek, addDays, isSameDay } from 'date-fns';
format,
startOfWeek,
addDays,
differenceInMinutes,
isSameDay,
} from 'date-fns';
import { fr } from 'date-fns/locale'; import { fr } from 'date-fns/locale';
import { getWeekEvents } from '@/utils/events'; import { getWeekEvents } from '@/utils/events';
import { isToday } from 'date-fns'; import { isToday } from 'date-fns';
@ -49,7 +43,8 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
const getCurrentTimePosition = () => { const getCurrentTimePosition = () => {
const hours = currentTime.getHours(); const hours = currentTime.getHours();
const minutes = currentTime.getMinutes(); const minutes = currentTime.getMinutes();
return `${(hours + minutes / 60) * 5}rem`; const rowHeight = 5; // Hauteur des lignes en rem (h-20 = 5rem)
return `${((hours + minutes / 60) * rowHeight)}rem`;
}; };
// Utiliser les événements déjà filtrés passés en props // Utiliser les événements déjà filtrés passés en props
@ -144,17 +139,17 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
}; };
return ( return (
<div className="flex flex-col h-full overflow-hidden"> <div className="flex flex-col h-full overflow-y-auto">
{/* En-tête des jours */} {/* En-tête des jours */}
<div <div
className="grid gap-[1px] bg-gray-100 pr-[17px]" className="grid gap-[1px] w-full bg-gray-100"
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }} style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
> >
<div className="bg-white h-14"></div> <div className="bg-white h-14"></div>
{weekDays.map((day) => ( {weekDays.map((day) => (
<div <div
key={day} key={day}
className={`p-2 text-center border-b className={`h-14 p-2 text-center border-b
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'} ${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`} ${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
> >
@ -172,7 +167,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
</div> </div>
{/* Grille horaire */} {/* Grille horaire */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative"> <div ref={scrollContainerRef} className="flex-1 relative">
{/* Ligne de temps actuelle */} {/* Ligne de temps actuelle */}
{isCurrentWeek && ( {isCurrentWeek && (
<div <div
@ -181,12 +176,12 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
top: getCurrentTimePosition(), top: getCurrentTimePosition(),
}} }}
> >
<div className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500" /> <div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-emerald-500" />
</div> </div>
)} )}
<div <div
className="grid gap-[1px] bg-gray-100" className="grid gap-[1px] w-full bg-gray-100"
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }} style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
> >
{timeSlots.map((hour) => ( {timeSlots.map((hour) => (
@ -209,9 +204,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
onDateClick(date); onDateClick(date);
}} }}
> >
<div className="flex gap-1"> <div className="grid gap-1">
{' '}
{/* Ajout de gap-1 */}
{dayEvents {dayEvents
.filter((event) => { .filter((event) => {
const eventStart = new Date(event.start); const eventStart = new Date(event.start);

View File

@ -2,7 +2,7 @@ import Logo from '@/components/Logo';
export default function Footer({ softwareName, softwareVersion }) { export default function Footer({ softwareName, softwareVersion }) {
return ( return (
<footer className="h-16 bg-white border-t border-gray-200 px-8 py-4 flex items-center justify-between"> <footer className="absolute bottom-0 left-0 right-0 h-16 bg-white border-t border-gray-200 flex items-center justify-center box-border">
<div className="text-sm font-light"> <div className="text-sm font-light">
<span> <span>
&copy; {new Date().getFullYear()} N3WT-INNOV Tous droits réservés. &copy; {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.

View File

@ -1,193 +0,0 @@
import { useState } from 'react';
import { usePlanning } from '@/context/PlanningContext';
import { Plus, Edit2, Eye, EyeOff, Check, X } from 'lucide-react';
export default function ScheduleNavigation() {
const {
schedules,
selectedSchedule,
setSelectedSchedule,
hiddenSchedules,
toggleScheduleVisibility,
addSchedule,
updateSchedule,
} = usePlanning();
const [editingId, setEditingId] = useState(null);
const [editedName, setEditedName] = useState('');
const [editedColor, setEditedColor] = useState('');
const [isAddingNew, setIsAddingNew] = useState(false);
const [newSchedule, setNewSchedule] = useState({
name: '',
color: '#10b981',
});
const handleEdit = (schedule) => {
setEditingId(schedule.id);
setEditedName(schedule.name);
setEditedColor(schedule.color);
};
const handleSave = () => {
if (editingId) {
updateSchedule(editingId, {
...schedules.find((s) => s.id === editingId),
name: editedName,
color: editedColor,
});
setEditingId(null);
}
};
const handleAddNew = () => {
if (newSchedule.name) {
addSchedule({
id: `schedule-${Date.now()}`,
...newSchedule,
});
setIsAddingNew(false);
setNewSchedule({ name: '', color: '#10b981' });
}
};
return (
<nav className="w-64 border-r p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold">Plannings</h2>
<button
onClick={() => setIsAddingNew(true)}
className="p-1 hover:bg-gray-100 rounded"
>
<Plus className="w-4 h-4" />
</button>
</div>
{isAddingNew && (
<div className="mb-4 p-2 border rounded">
<input
type="text"
value={newSchedule.name}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, name: e.target.value }))
}
className="w-full p-1 mb-2 border rounded"
placeholder="Nom du planning"
/>
<div className="flex gap-2 items-center mb-2">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={newSchedule.color}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, color: e.target.value }))
}
className="w-8 h-8"
/>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setIsAddingNew(false)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleAddNew}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
)}
<ul className="space-y-2">
{schedules.map((schedule) => (
<li
key={schedule.id}
className={`p-2 rounded ${
selectedSchedule === schedule.id
? 'bg-gray-100'
: 'hover:bg-gray-50'
}`}
>
{editingId === schedule.id ? (
<div className="space-y-2">
<input
type="text"
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className="w-full p-1 border rounded"
/>
<div className="flex gap-2 items-center">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={editedColor}
onChange={(e) => setEditedColor(e.target.value)}
className="w-8 h-8"
/>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setEditingId(null)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleSave}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
) : (
<div className="flex items-center justify-between">
<div
className="flex items-center gap-2 flex-1 cursor-pointer"
onClick={() => setSelectedSchedule(schedule.id)}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: schedule.color }}
/>
<span
className={
hiddenSchedules.includes(schedule.id)
? 'text-gray-400'
: ''
}
>
{schedule.name}
</span>
</div>
<div className="flex gap-1">
<button
onClick={(e) => {
e.stopPropagation(); // Empêcher la propagation du clic
toggleScheduleVisibility(schedule.id);
}}
className="p-1 hover:bg-gray-100 rounded"
>
{hiddenSchedules.includes(schedule.id) ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleEdit(schedule)}
className="p-1 hover:bg-gray-100 rounded"
>
<Edit2 className="w-4 h-4" />
</button>
</div>
</div>
)}
</li>
))}
</ul>
</nav>
);
}

View File

@ -4,8 +4,8 @@ const SidebarTabs = ({ tabs }) => {
const [activeTab, setActiveTab] = useState(tabs[0].id); const [activeTab, setActiveTab] = useState(tabs[0].id);
return ( return (
<div className="w-full"> <>
<div className="flex border-b-2 border-gray-200"> <div className="flex h-14 border-b-2 border-gray-200">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.id} key={tab.id}
@ -20,17 +20,15 @@ const SidebarTabs = ({ tabs }) => {
</button> </button>
))} ))}
</div> </div>
<div className="p-4">
{tabs.map((tab) => ( {tabs.map((tab) => (
<div <div
key={tab.id} key={tab.id}
className={`${activeTab === tab.id ? 'block' : 'hidden'}`} className={`${activeTab === tab.id ? 'block h-[calc(100%-3.5rem)]' : 'hidden'}`}
> >
{tab.content} {tab.content}
</div> </div>
))} ))}
</div> </>
</div>
); );
}; };

View File

@ -12,8 +12,10 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { useRouter } from 'next/navigation';
import { FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL } from '@/utils/Url'; import { FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL } from '@/utils/Url';
import { usePlanning } from '@/context/PlanningContext';
import { useClasses } from '@/context/ClassesContext';
const ItemTypes = { const ItemTypes = {
TEACHER: 'teacher', TEACHER: 'teacher',
@ -117,6 +119,7 @@ const ClassesSection = ({
handleCreate, handleCreate,
handleEdit, handleEdit,
handleDelete, handleDelete,
}) => { }) => {
const [formData, setFormData] = useState({}); const [formData, setFormData] = useState({});
const [editingClass, setEditingClass] = useState(null); const [editingClass, setEditingClass] = useState(null);
@ -129,41 +132,10 @@ const ClassesSection = ({
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const [detailsModalVisible, setDetailsModalVisible] = useState(false); const [detailsModalVisible, setDetailsModalVisible] = useState(false);
const [selectedClass, setSelectedClass] = useState(null); const [selectedClass, setSelectedClass] = useState(null);
const router = useRouter();
const { selectedEstablishmentId } = useEstablishment(); const { selectedEstablishmentId } = useEstablishment();
const { addSchedule, reloadPlanning, reloadEvents } = usePlanning();
const{ getNiveauxLabels, allNiveaux } = useClasses();
const niveauxPremierCycle = [
{ id: 1, name: 'TPS', age: 2 },
{ id: 2, name: 'PS', age: 3 },
{ id: 3, name: 'MS', age: 4 },
{ id: 4, name: 'GS', age: 5 },
];
const niveauxSecondCycle = [
{ id: 5, name: 'CP', age: 6 },
{ id: 6, name: 'CE1', age: 7 },
{ id: 7, name: 'CE2', age: 8 },
];
const niveauxTroisiemeCycle = [
{ id: 8, name: 'CM1', age: 9 },
{ id: 9, name: 'CM2', age: 10 },
];
const allNiveaux = [
...niveauxPremierCycle,
...niveauxSecondCycle,
...niveauxTroisiemeCycle,
];
const getNiveauxLabels = (levels) => {
return levels.map((niveauId) => {
const niveau = allNiveaux.find((n) => n.id === niveauId);
return niveau ? niveau.name : niveauId;
});
};
// Fonction pour générer les années scolaires // Fonction pour générer les années scolaires
const getSchoolYearChoices = () => { const getSchoolYearChoices = () => {
@ -241,6 +213,19 @@ const ClassesSection = ({
setClasses((prevClasses) => [createdClass, ...classes]); setClasses((prevClasses) => [createdClass, ...classes]);
setNewClass(null); setNewClass(null);
setLocalErrors({}); setLocalErrors({});
// Creation des plannings associé à la classe
createdClass.levels.forEach((level) => {
const levelName = allNiveaux.find((lvl) => lvl.id === level)?.name;
const planningName = `${createdClass.atmosphere_name} - ${levelName}`;
const newPlanning = {
name: planningName,
color: '#FF5733', // Couleur par défaut
school_class: createdClass.id,
}
addSchedule(newPlanning)
});
}) })
.catch((error) => { .catch((error) => {
logger.error('Error:', error.message); logger.error('Error:', error.message);
@ -505,6 +490,8 @@ const ClassesSection = ({
); );
setPopupVisible(true); setPopupVisible(true);
setRemovePopupVisible(false); setRemovePopupVisible(false);
reloadPlanning();
reloadEvents();
}) })
.catch((error) => { .catch((error) => {
logger.error('Error archiving data:', error); logger.error('Error archiving data:', error);

View File

@ -1,47 +0,0 @@
import React from 'react';
import { Calendar } from 'lucide-react';
const DateRange = ({
nameStart,
nameEnd,
valueStart,
valueEnd,
onChange,
label,
}) => {
return (
<div className="space-y-4 mt-4 p-4 border rounded-md shadow-sm bg-white">
<label className="block text-lg font-medium text-gray-700 mb-2">
{label}
</label>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 items-center">
<div className="relative flex items-center">
<span className="mr-2">Du</span>
<Calendar className="w-5 h-5 text-emerald-500 absolute top-3 left-16" />
<input
type="date"
name={nameStart}
value={valueStart}
onChange={onChange}
className="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500 hover:ring-emerald-400 ml-8"
placeholder="Date de début"
/>
</div>
<div className="relative flex items-center">
<span className="mr-2">Au</span>
<Calendar className="w-5 h-5 text-emerald-500 absolute top-3 left-16" />
<input
type="date"
name={nameEnd}
value={valueEnd}
onChange={onChange}
className="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500 hover:ring-emerald-400 ml-8"
placeholder="Date de fin"
/>
</div>
</div>
</div>
);
};
export default DateRange;

View File

@ -1,121 +0,0 @@
import React from 'react';
import RadioList from '@/components/RadioList';
import DateRange from '@/components/Structure/Configuration/DateRange';
import TimeRange from '@/components/Structure/Configuration/TimeRange';
import CheckBoxList from '@/components/CheckBoxList';
const PlanningConfiguration = ({
formData,
handleChange,
handleTimeChange,
handleJoursChange,
typeEmploiDuTemps,
}) => {
const daysOfWeek = [
{ id: 1, name: 'lun' },
{ id: 2, name: 'mar' },
{ id: 3, name: 'mer' },
{ id: 4, name: 'jeu' },
{ id: 5, name: 'ven' },
{ id: 6, name: 'sam' },
];
const isLabelAttenuated = (item) => {
return !formData.opening_days.includes(parseInt(item.id));
};
return (
<div className="space-y-4">
<label className="mt-6 block text-2xl font-medium text-gray-700">
Emploi du temps
</label>
<div className="flex justify-between space-x-4 items-start">
<div className="w-1/2">
<RadioList
items={typeEmploiDuTemps}
formData={formData}
handleChange={handleChange}
fieldName="type"
className="w-full"
/>
</div>
{/* Plage horaire */}
<div className="w-1/2">
<TimeRange
startTime={formData.time_range[0]}
endTime={formData.time_range[1]}
onStartChange={(e) => handleTimeChange(e, 0)}
onEndChange={(e) => handleTimeChange(e, 1)}
/>
{/* CheckBoxList */}
<CheckBoxList
items={daysOfWeek}
formData={formData}
handleChange={handleJoursChange}
fieldName="opening_days"
horizontal={true}
labelAttenuated={isLabelAttenuated}
/>
</div>
</div>
{/* DateRange */}
<div className="space-y-4 w-full">
{formData.type === 2 && (
<>
<DateRange
nameStart="date_debut_semestre_1"
nameEnd="date_fin_semestre_1"
valueStart={formData.date_debut_semestre_1}
valueEnd={formData.date_fin_semestre_1}
onChange={handleChange}
label="Semestre 1"
/>
<DateRange
nameStart="date_debut_semestre_2"
nameEnd="date_fin_semestre_2"
valueStart={formData.date_debut_semestre_2}
valueEnd={formData.date_fin_semestre_2}
onChange={handleChange}
label="Semestre 2"
/>
</>
)}
{formData.type === 3 && (
<>
<DateRange
nameStart="date_debut_trimestre_1"
nameEnd="date_fin_trimestre_1"
valueStart={formData.date_debut_trimestre_1}
valueEnd={formData.date_fin_trimestre_1}
onChange={handleChange}
label="Trimestre 1"
/>
<DateRange
nameStart="date_debut_trimestre_2"
nameEnd="date_fin_trimestre_2"
valueStart={formData.date_debut_trimestre_2}
valueEnd={formData.date_fin_trimestre_2}
onChange={handleChange}
label="Trimestre 2"
/>
<DateRange
nameStart="date_debut_trimestre_3"
nameEnd="date_fin_trimestre_3"
valueStart={formData.date_debut_trimestre_3}
valueEnd={formData.date_fin_trimestre_3}
onChange={handleChange}
label="Trimestre 3"
/>
</>
)}
</div>
</div>
);
};
export default PlanningConfiguration;

View File

@ -22,7 +22,7 @@ const StructureManagement = ({
handleDelete, handleDelete,
}) => { }) => {
return ( return (
<div className="w-full mx-auto mt-6"> <div className="w-full p-4 mx-auto mt-6">
<ClassesProvider> <ClassesProvider>
<div className="mt-8 w-2/5"> <div className="mt-8 w-2/5">
<SpecialitiesSection <SpecialitiesSection

View File

@ -462,7 +462,7 @@ export default function FilesGroupsManagement({
} }
return ( return (
<div className="w-full mx-auto mt-6"> <div className="w-full p-4 mx-auto mt-6">
{/* Modal pour les fichiers */} {/* Modal pour les fichiers */}
<Modal <Modal
isOpen={isModalOpen} isOpen={isModalOpen}

View File

@ -1,40 +0,0 @@
import React from 'react';
import TeacherLabel from '@/components/CustomLabels/TeacherLabel';
const ClassesInformation = ({ selectedClass, isPastYear }) => {
if (!selectedClass) {
return null;
}
return (
<div
className={`w-full p-6 shadow-lg rounded-full border relative ${isPastYear ? 'bg-gray-200 border-gray-600' : 'bg-emerald-200 border-emerald-500'}`}
>
<div
className={`border-b pb-4 ${isPastYear ? 'border-gray-600' : 'border-emerald-500'}`}
>
<p className="text-gray-700 text-center">
<strong>{selectedClass.age_range} ans</strong>
</p>
</div>
<div
className={`border-b pb-4 ${isPastYear ? 'border-gray-600' : 'border-emerald-500'}`}
>
<div className="flex flex-wrap justify-center space-x-4">
{selectedClass.teachers.map((teacher) => (
<div key={teacher.id} className="relative group mt-4">
<TeacherLabel nom={teacher.nom} prenom={teacher.prenom} />
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-full mb-2 w-max px-4 py-2 text-white bg-gray-800 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<p className="text-sm">
{teacher.nom} {teacher.prenom}
</p>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default ClassesInformation;

View File

@ -1,89 +0,0 @@
import React from 'react';
import { History, Clock, Users } from 'lucide-react';
import logger from '@/utils/logger';
const ClassesList = ({ classes, onClassSelect, selectedClassId }) => {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth();
const currentSchoolYearStart =
currentMonth >= 8 ? currentYear : currentYear - 1;
const handleClassClick = (classe) => {
logger.debug(
`Classe sélectionnée: ${classe.atmosphere_name}, Année scolaire: ${classe.school_year}`
);
onClassSelect(classe);
};
const categorizedClasses = classes.reduce((acc, classe) => {
const { school_year } = classe;
const [startYear] = school_year.split('-').map(Number);
const category =
startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(classe);
return acc;
}, {});
return (
<div className="w-full p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<Users className="w-8 h-8 mr-2" />
Classes
</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<h3 className="text-xl font-semibold mb-2 text-emerald-600 flex items-center space-x-2">
<Clock className="inline-block mr-2 w-5 h-5" /> Actives
</h3>
<div className="flex flex-col">
{categorizedClasses['Actives']?.map((classe) => (
<div
key={classe.id}
className={`flex items-center ${selectedClassId === classe.id ? 'bg-emerald-600 text-white' : 'bg-emerald-100 text-emerald-600'} border border-emerald-300 rounded-lg shadow-lg overflow-hidden hover:bg-emerald-300 hover:text-emerald-700 cursor-pointer p-4 mb-4`}
onClick={() => handleClassClick(classe)}
style={{ maxWidth: '400px' }}
>
<div className="flex-1 text-sm font-medium">
{classe.atmosphere_name}
</div>
<div className="flex-1 text-sm font-medium">
{classe.school_year}
</div>
</div>
))}
</div>
</div>
<div>
<h3 className="text-xl font-semibold mb-2 text-gray-600 flex items-center space-x-2">
<History className="inline-block mr-2 w-5 h-5" /> Anciennes
</h3>
<div className="flex flex-col">
{categorizedClasses['Anciennes']?.map((classe) => (
<div
key={classe.id}
className={`flex items-center ${selectedClassId === classe.id ? 'bg-gray-400 text-white' : 'bg-gray-100 text-gray-600'} border border-gray-300 rounded-lg shadow-lg overflow-hidden hover:bg-gray-300 hover:text-gray-700 cursor-pointer p-4 mb-4`}
onClick={() => handleClassClick(classe)}
style={{ maxWidth: '400px' }}
>
<div className="flex-1 text-sm font-medium">
{classe.atmosphere_name}
</div>
<div className="flex-1 text-sm font-medium">
{classe.school_year}
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default ClassesList;

View File

@ -1,40 +0,0 @@
import React from 'react';
import { useDrag } from 'react-dnd';
import { UserIcon } from 'lucide-react'; // Assure-toi d'importer l'icône que tu souhaites utiliser
const DraggableSpeciality = ({ speciality }) => {
const [{ isDragging }, drag] = useDrag(() => ({
type: 'SPECIALITY',
item: {
id: speciality.id,
name: speciality.nom,
color: speciality.codeCouleur,
teachers: speciality.teachers,
},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));
return (
<span
ref={drag}
key={speciality.id}
className={`relative flex items-center px-4 py-2 rounded-full font-bold text-white text-center shadow-lg cursor-pointer transition-transform duration-200 ease-in-out transform ${isDragging ? 'opacity-50 scale-95' : 'scale-100 hover:scale-105 hover:shadow-xl'}`}
style={{
backgroundColor: speciality.codeCouleur,
minWidth: '200px',
maxWidth: '400px',
}}
title={speciality.nom}
>
{speciality.nom}
<span className="absolute top-0 right-0 mt-1 mr-1 flex items-center justify-center text-xs bg-black bg-opacity-50 rounded-full px-2 py-1">
<UserIcon size={16} className="ml-1" />
{speciality.teachers.length}
</span>
</span>
);
};
export default DraggableSpeciality;

View File

@ -1,87 +0,0 @@
import React from 'react';
import { useDrop } from 'react-dnd';
import PropTypes from 'prop-types';
// Définition du composant DropTargetCell
const DropTargetCell = ({ day, hour, courses, onDrop, onClick }) => {
const [{ isOver, canDrop }, drop] = useDrop(
() => ({
accept: 'SPECIALITY',
drop: (item) => onDrop(item, hour, day),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[hour, day]
);
const isColorDark = (color) => {
if (!color) return false;
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
return r * 0.299 + g * 0.587 + b * 0.114 < 150;
};
const isToday = (someDate) => {
const today = new Date();
return (
someDate.getDate() === today.getDate() &&
someDate.getMonth() === today.getMonth() &&
someDate.getFullYear() === today.getFullYear()
);
};
// Vérifie si c'est une heure pleine
const isFullHour = parseInt(hour.split(':')[1], 10) === 0;
return (
<div
ref={drop}
onClick={() => onClick(hour, day)}
className={`relative cursor-pointer
${isToday(new Date(day)) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}
hover:bg-emerald-100 h-10 border-b
${isFullHour ? 'border-emerald-200' : 'border-gray-300'}
${isOver && canDrop ? 'bg-emerald-200' : ''}`} // Ajouté pour indiquer le drop
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
width: '100%',
}}
>
{courses.map((course) => (
<div
key={course.matiere}
className="flex flex-row items-center justify-center gap-2"
style={{
backgroundColor: course.color,
color: isColorDark(course.color) ? '#E5E5E5' : '#333333',
width: '100%',
height: '100%',
}}
>
<div style={{ textAlign: 'center', fontWeight: 'bold' }}>
{course.matiere}
</div>
<div style={{ fontStyle: 'italic', textAlign: 'center' }}>
{course.teachers.join(', ')}
</div>
</div>
))}
</div>
);
};
DropTargetCell.propTypes = {
day: PropTypes.string.isRequired,
hour: PropTypes.string.isRequired,
courses: PropTypes.array.isRequired,
onDrop: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
formData: PropTypes.object.isRequired,
};
export default DropTargetCell;

View File

@ -1,274 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { format, addDays, startOfWeek } from 'date-fns';
import { fr } from 'date-fns/locale';
import DropTargetCell from '@/components/Structure/Planning/DropTargetCell';
import { useClasseForm } from '@/context/ClasseFormContext';
import { useClasses } from '@/context/ClassesContext';
import { Calendar } from 'lucide-react';
import SpecialityEventModal from '@/components/Structure/Planning/SpecialityEventModal'; // Assurez-vous du bon chemin d'importation
const PlanningClassView = ({
schedule,
onDrop,
selectedLevel,
handleUpdatePlanning,
classe,
}) => {
const { formData } = useClasseForm();
const { determineInitialPeriod } = useClasses();
const [currentPeriod, setCurrentPeriod] = useState(
schedule?.emploiDuTemps
? determineInitialPeriod(schedule.emploiDuTemps)
: null
);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedCell, setSelectedCell] = useState(null);
const [existingEvent, setExistingEvent] = useState(null);
useEffect(() => {
if (schedule?.emploiDuTemps) {
setCurrentPeriod(determineInitialPeriod(schedule.emploiDuTemps));
}
}, [schedule]);
if (!schedule || !schedule.emploiDuTemps) {
return (
<div className="w-full p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<Calendar className="w-8 h-8 mr-2" />
Planning
</h2>
</div>
</div>
);
}
const emploiDuTemps =
schedule.emploiDuTemps[currentPeriod] || schedule.emploiDuTemps;
const joursOuverture = Object.keys(emploiDuTemps);
const currentWeekDays = joursOuverture
.map((day) => {
switch (day.toLowerCase()) {
case 'lundi':
return 1;
case 'mardi':
return 2;
case 'mercredi':
return 3;
case 'jeudi':
return 4;
case 'vendredi':
return 5;
case 'samedi':
return 6;
case 'dimanche':
return 7;
default:
return 0;
}
})
.sort((a, b) => a - b) // Trier les jours dans l'ordre croissant
.map((day) =>
addDays(startOfWeek(new Date(), { weekStartsOn: 1 }), day - 1)
); // Calculer les dates à partir du lundi
const getFilteredEvents = (day, time, level) => {
const [hour, minute] = time.split(':').map(Number);
const startTime = hour + minute / 60; // Convertir l'heure en fraction d'heure
return (
emploiDuTemps[day.toLowerCase()]?.filter((event) => {
const [eventHour, eventMinute] = event.heure.split(':').map(Number);
const eventStartTime = eventHour + eventMinute / 60;
const eventEndTime = eventStartTime + parseFloat(event.duree);
// Filtrer en fonction du selectedLevel
return (
schedule.niveau === level &&
startTime >= eventStartTime &&
startTime < eventEndTime
);
}) || []
);
};
const handleCellClick = (hour, day) => {
const cellEvents = getFilteredEvents(day, hour, selectedLevel);
setSelectedCell({ hour, day, selectedLevel });
setExistingEvent(cellEvents.length ? cellEvents[0] : null);
setIsModalOpen(true);
};
const renderTimeSlots = () => {
const timeSlots = [];
for (
let hour = parseInt(formData.time_range[0], 10);
hour <= parseInt(formData.time_range[1], 10);
hour++
) {
const hourString = hour.toString().padStart(2, '0');
timeSlots.push(
<React.Fragment key={`${hourString}:00-${Math.random()}`}>
<div className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
{`${hourString}:00`}
</div>
{currentWeekDays.map((date, index) => {
const day = format(date, 'iiii', { locale: fr }).toLowerCase();
const uniqueKey = `${hourString}:00-${day}-${index}`;
return (
<div key={uniqueKey} className="flex flex-col">
<DropTargetCell
hour={`${hourString}:00`}
day={day}
courses={getFilteredEvents(
day,
`${hourString}:00`,
selectedLevel
)}
onDrop={onDrop}
onClick={(hour, day) => handleCellClick(hour, day)}
/>
<DropTargetCell
hour={`${hourString}:30`}
day={day}
courses={getFilteredEvents(
day,
`${hourString}:30`,
selectedLevel
)}
onDrop={onDrop}
onClick={(hour, day) => handleCellClick(hour, day)}
/>
</div>
);
})}
</React.Fragment>
);
}
return timeSlots;
};
return (
<div className="w-full p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<Calendar className="w-8 h-8 mr-2" />
Planning
</h2>
{schedule.emploiDuTemps.S1 && schedule.emploiDuTemps.S2 && (
<div>
<button
onClick={() => setCurrentPeriod('S1')}
className={`px-4 py-2 ${currentPeriod === 'S1' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Semestre 1
</button>
<button
onClick={() => setCurrentPeriod('S2')}
className={`px-4 py-2 ${currentPeriod === 'S2' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Semestre 2
</button>
</div>
)}
{schedule.emploiDuTemps.T1 &&
schedule.emploiDuTemps.T2 &&
schedule.emploiDuTemps.T3 && (
<div>
<button
onClick={() => setCurrentPeriod('T1')}
className={`px-4 py-2 ${currentPeriod === 'T1' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Trimestre 1
</button>
<button
onClick={() => setCurrentPeriod('T2')}
className={`px-4 py-2 ${currentPeriod === 'T2' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Trimestre 2
</button>
<button
onClick={() => setCurrentPeriod('T3')}
className={`px-4 py-2 ${currentPeriod === 'T3' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Trimestre 3
</button>
</div>
)}
</div>
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
{/* En-tête des jours */}
<div
className="grid w-full"
style={{
gridTemplateColumns: `2.5rem repeat(${currentWeekDays.length}, 1fr)`,
}}
>
<div className="bg-gray-50 h-14"></div>
{currentWeekDays.map((date, index) => (
<div
key={`${date}-${index}`}
className="p-3 text-center bg-emerald-100 text-emerald-800 border-r border-emerald-200"
>
<div className="text font-semibold">
{format(date, 'EEEE', { locale: fr })}
</div>
</div>
))}
</div>
{/* Contenu du planning */}
<div
className="flex-1 overflow-y-auto relative"
style={{ maxHeight: 'calc(100vh - 300px)' }}
>
<div
className="grid bg-white relative"
style={{
gridTemplateColumns: `2.5rem repeat(${currentWeekDays.length}, 1fr)`,
}}
>
{renderTimeSlots()}
</div>
</div>
</div>
<SpecialityEventModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
selectedCell={selectedCell}
existingEvent={existingEvent}
handleUpdatePlanning={handleUpdatePlanning}
classe={classe}
/>
</div>
);
};
PlanningClassView.propTypes = {
schedule: PropTypes.shape({
emploiDuTemps: PropTypes.objectOf(
PropTypes.arrayOf(
PropTypes.shape({
duree: PropTypes.string.isRequired,
heure: PropTypes.string.isRequired,
matiere: PropTypes.string.isRequired,
teachers: PropTypes.arrayOf(PropTypes.string).isRequired,
color: PropTypes.string.isRequired,
})
)
).isRequired,
plageHoraire: PropTypes.shape({
startHour: PropTypes.number.isRequired,
endHour: PropTypes.number.isRequired,
}).isRequired,
joursOuverture: PropTypes.arrayOf(PropTypes.number).isRequired,
}).isRequired,
};
export default PlanningClassView;

View File

@ -0,0 +1,299 @@
import { usePlanning } from '@/context/PlanningContext';
import { format } from 'date-fns';
import React from 'react';
export default function ScheduleEventModal({
isOpen,
onClose,
eventData,
setEventData,
specialities,
teachers,
classes
}) {
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } = usePlanning();
React.useEffect(() => {
if (!eventData?.planning && schedules.length > 0) {
const defaultSchedule = schedules[0];
if (eventData?.planning !== defaultSchedule.id) {
setEventData((prev) => ({
...prev,
planning: defaultSchedule.id,
}));
}
}
}, [schedules, eventData?.planning]);
const handleSpecialityChange = (specialityId) => {
const selectedSpeciality = specialities.find((s) => s.id === parseInt(specialityId, 10));
if (selectedSpeciality) {
setEventData((prev) => ({
...prev,
speciality: selectedSpeciality.id,
title: selectedSpeciality.name, // Définit la matière
color: selectedSpeciality.color_code, // Définit la couleur associée
}));
}
};
const handleTeacherChange = (teacherId) => {
const selectedTeacher = teachers.find((t) => t.id === parseInt(teacherId, 10));
if (selectedTeacher) {
setEventData((prev) => ({
...prev,
teacher: selectedTeacher.id,
description: `${selectedTeacher.first_name} ${selectedTeacher.last_name}`, // Définit le nom du professeur
}));
}
};
const handlePlanningChange = (planningId) => {
const selectedSchedule = schedules.find((s) => s.id === parseInt(planningId, 10));
if (selectedSchedule) {
setEventData((prev) => ({
...prev,
planning: selectedSchedule.id,
}));
}
};
const handleColorChange = (color) => {
setEventData((prev) => ({
...prev,
color, // Permet de changer manuellement la couleur
}));
};
if (!isOpen) return null;
const handleSubmit = (e) => {
e.preventDefault();
if (!eventData.speciality) {
alert('Veuillez sélectionner une matière');
return;
}
if (!eventData.teacher) {
alert('Veuillez sélectionner un professeur');
return;
}
if (!eventData.planning) {
alert('Veuillez sélectionner un planning');
return;
}
if (!eventData.location) {
alert('Veuillez saisir un lieu');
return;
}
if (eventData.id) {
handleUpdateEvent(eventData.id, eventData);
} else {
addEvent({
...eventData,
id: `event-${Date.now()}`,
});
}
onClose();
};
const handleDelete = () => {
if (
eventData.id &&
confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')
) {
handleDeleteEvent(eventData.id);
onClose();
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg w-full max-w-md">
<h2 className="text-xl font-semibold mb-4">
{eventData.id ? "Modifier l'événement" : 'Nouvel événement'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Planning */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Planning
</label>
<select
value={eventData.planning || ''}
onChange={(e) => handlePlanningChange(e.target.value)}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
>
<option value="" disabled>
Sélectionnez un planning
</option>
{schedules.map((schedule) => (
<option key={schedule.id} value={schedule.id}>
{schedule.name}
</option>
))}
</select>
</div>
{/* Matière */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Matière
</label>
<select
value={eventData.speciality || ''}
onChange={(e) => handleSpecialityChange(e.target.value)}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
>
<option value="" disabled>
Sélectionnez une matière
</option>
{specialities.map((speciality) => (
<option key={speciality.id} value={speciality.id}>
{speciality.name}
</option>
))}
</select>
</div>
{/* Professeur */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Professeur
</label>
<select
value={eventData.teacher || ''}
onChange={(e) => handleTeacherChange(e.target.value)}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
>
<option value="" disabled>
Sélectionnez un professeur
</option>
{teachers.map((teacher) => (
<option key={teacher.id} value={teacher.id}>
{`${teacher.first_name} ${teacher.last_name}`}
</option>
))}
</select>
</div>
{/* Lieu */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Lieu
</label>
<input
type="text"
value={eventData.location || ''}
onChange={(e) =>
setEventData({ ...eventData, location: e.target.value })
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
placeholder="Saisissez un lieu"
required
/>
</div>
{/* Couleur */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Couleur
</label>
<input
type="color"
value={eventData.color || '#10b981'}
onChange={(e) => handleColorChange(e.target.value)}
className="w-full h-10 p-1 rounded border"
/>
</div>
{/* Dates */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Début
</label>
<input
type="datetime-local"
value={
eventData.start
? format(new Date(eventData.start), "yyyy-MM-dd'T'HH:mm")
: ''
}
onChange={(e) =>
setEventData({
...eventData,
start: new Date(e.target.value).toISOString(),
})
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fin
</label>
<input
type="datetime-local"
value={
eventData.end
? format(new Date(eventData.end), "yyyy-MM-dd'T'HH:mm")
: ''
}
onChange={(e) =>
setEventData({
...eventData,
end: new Date(e.target.value).toISOString(),
})
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
/>
</div>
</div>
{/* Boutons */}
<div className="flex justify-between gap-2 mt-6">
<div>
{eventData.id && (
<button
type="button"
onClick={handleDelete}
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded"
>
Supprimer
</button>
)}
</div>
<div className="flex gap-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Annuler
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-700"
>
{eventData.id ? 'Modifier' : 'Créer'}
</button>
</div>
</div>
</form>
</div>
</div>
);
}

View File

@ -1,211 +1,74 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import { AnimatePresence, motion } from 'framer-motion';
import PlanningClassView from '@/components/Structure/Planning/PlanningClassView';
import SpecialitiesList from '@/components/Structure/Planning/SpecialitiesList';
import { BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url';
import { useClasses } from '@/context/ClassesContext';
import { ClasseFormProvider } from '@/context/ClasseFormContext';
import TabsStructure from '@/components/Structure/Configuration/TabsStructure';
import { Bookmark, Users, BookOpen, Newspaper } from 'lucide-react';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { RecurrenceType } from '@/context/PlanningContext';
import Calendar from '@/components/Calendar/Calendar';
import ScheduleEventModal from '@/components/Structure/Planning/ScheduleEventModal';
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth();
const currentSchoolYearStart =
currentMonth >= 8 ? currentYear : currentYear - 1;
const [selectedClass, setSelectedClass] = useState(null);
const [selectedLevel, setSelectedLevel] = useState('');
const [schedule, setSchedule] = useState(null);
const { getNiveauxTabs } = useClasses();
const niveauxLabels = Array.isArray(selectedClass?.levels)
? getNiveauxTabs(selectedClass.levels)
: [];
export default function ScheduleManagement({classes,specialities,teachers}) {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const handleOpenModal = () => setIsModalOpen(true); const [eventData, setEventData] = useState({
const handleCloseModal = () => setIsModalOpen(false); title: '',
description: '',
start: '',
end: '',
location: '',
planning: '', // Enlever la valeur par défaut ici
recursionType: RecurrenceType.NONE,
selectedDays: [],
recursionEnd: '',
customInterval: 1,
customUnit: 'days',
viewType: 'week', // Ajouter la vue semaine par défaut
});
useEffect(() => { const initializeNewEvent = (date = new Date()) => {
if (selectedClass) { // S'assurer que date est un objet Date valide
const defaultLevel = niveauxLabels.length > 0 ? niveauxLabels[0].id : ''; const eventDate = date instanceof Date ? date : new Date();
const niveau = selectedLevel || defaultLevel;
setSelectedLevel(niveau); setEventData({
title: '',
const currentPlanning = selectedClass.plannings_read?.find( description: '',
(planning) => planning.niveau === niveau start: eventDate.toISOString(),
); end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
setSchedule(currentPlanning ? currentPlanning.planning : {}); location: '',
} planning: '', // Ne pas définir de valeur par défaut ici non plus
}, [selectedClass, niveauxLabels]); recursionType: RecurrenceType.NONE,
selectedDays: [],
useEffect(() => { recursionEnd: new Date(
if (selectedClass && selectedLevel) { eventDate.getTime() + 2 * 60 * 60 * 1000
const currentPlanning = selectedClass.plannings_read?.find( ).toISOString(),
(planning) => planning.niveau === selectedLevel customInterval: 1,
); customUnit: 'days',
setSchedule(currentPlanning ? currentPlanning.planning : {}); });
} setIsModalOpen(true);
}, [selectedClass, selectedLevel]);
const handleLevelSelect = (niveau) => {
setSelectedLevel(niveau);
}; };
const handleClassSelect = (classId) => {
const selectedClasse = categorizedClasses['Actives'].find(
(classe) => classe.id === classId
);
setSelectedClass(selectedClasse);
setSelectedLevel('');
};
const onDrop = (item, hour, day) => {
const { id, name, color, teachers } = item;
const newSchedule = {
...schedule,
emploiDuTemps: schedule.emploiDuTemps || {},
};
if (!newSchedule.emploiDuTemps[day]) {
newSchedule.emploiDuTemps[day] = [];
}
const courseTime = `${hour.toString().padStart(2, '0')}:00`;
const existingCourseIndex = newSchedule.emploiDuTemps[day].findIndex(
(course) => course.heure === courseTime
);
const newCourse = {
duree: '1',
heure: courseTime,
matiere: name,
teachers: teachers,
color: color,
};
if (existingCourseIndex !== -1) {
newSchedule.emploiDuTemps[day][existingCourseIndex] = newCourse;
} else {
newSchedule.emploiDuTemps[day].push(newCourse);
}
// Mettre à jour scheduleRef
setSchedule(newSchedule);
// Utiliser `handleUpdatePlanning` pour mettre à jour le planning du niveau de la classe
const planningId = selectedClass.plannings_read.find(
(planning) => planning.niveau === selectedLevel
)?.planning.id;
if (planningId) {
logger.debug('newSchedule : ', newSchedule);
handleUpdatePlanning(BE_SCHOOL_PLANNINGS_URL, planningId, newSchedule);
}
};
const categorizedClasses = classes.reduce((acc, classe) => {
const { school_year } = classe;
const [startYear] = school_year.split('-').map(Number);
const category =
startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(classe);
return acc;
}, {});
return ( return (
<div className="flex flex-col h-full"> <div className="flex h-full overflow-hidden">
<DndProvider backend={HTML5Backend}> <ScheduleNavigation classes={classes} />
<div className="p-4 bg-gray-100 border-b"> <Calendar
<div className="grid grid-cols-3 gap-4"> onDateClick={initializeNewEvent}
{/* Colonne Classes */} onEventClick={(event) => {
<div className="p-4 bg-gray-50 rounded-lg shadow-inner"> setEventData(event);
<div className="flex justify-between items-center mb-4"> setIsModalOpen(true);
<h2 className="text-3xl text-gray-800 flex items-center"> }}
<Users className="w-8 h-8 mr-2" />
Classes
</h2>
</div>
{categorizedClasses['Actives'] && (
<TabsStructure
activeTab={selectedClass?.id}
setActiveTab={handleClassSelect}
tabs={categorizedClasses['Actives'].map((classe) => ({
id: classe.id,
title: classe.atmosphere_name,
icon: Users,
}))}
/> />
)} <ScheduleEventModal
</div> isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
{/* Colonne Niveaux */} eventData={eventData}
<div className="p-4 bg-gray-50 rounded-lg shadow-inner"> setEventData={setEventData}
<div className="flex justify-between items-center mb-4"> specialities={specialities}
<h2 className="text-3xl text-gray-800 flex items-center"> teachers={teachers}
<Bookmark className="w-8 h-8 mr-2" /> classes={classes}
Niveaux
</h2>
</div>
{niveauxLabels && (
<TabsStructure
activeTab={selectedLevel}
setActiveTab={handleLevelSelect}
tabs={niveauxLabels}
/> />
)}
</div>
{/* Colonne Spécialités */}
<div className="p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<BookOpen className="w-8 h-8 mr-2" />
Spécialités
</h2>
</div>
<SpecialitiesList
teachers={selectedClass ? selectedClass.teachers : []}
/>
</div>
</div>
</div>
<div className="flex-1 p-4 overflow-y-auto">
<AnimatePresence mode="wait">
<motion.div
key="year"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
className="flex-1 relative"
>
<ClasseFormProvider initialClasse={selectedClass || {}}>
<PlanningClassView
schedule={schedule}
onDrop={onDrop}
selectedLevel={selectedLevel}
handleUpdatePlanning={handleUpdatePlanning}
classe={selectedClass}
/>
</ClasseFormProvider>
</motion.div>
</AnimatePresence>
</div>
</DndProvider>
</div> </div>
); );
}; }
export default ScheduleManagement;

View File

@ -1,19 +0,0 @@
import React from 'react';
import { useClasses } from '@/context/ClassesContext';
import DraggableSpeciality from '@/components/Structure/Planning/DraggableSpeciality';
const SpecialitiesList = ({ teachers }) => {
const { groupSpecialitiesBySubject } = useClasses();
return (
<div className="flex justify-center items-center w-full">
<div className="flex flex-wrap gap-2 mt-4 justify-center">
{groupSpecialitiesBySubject(teachers).map((speciality) => (
<DraggableSpeciality key={speciality.id} speciality={speciality} />
))}
</div>
</div>
);
};
export default SpecialitiesList;

View File

@ -1,258 +0,0 @@
import React, { useState, useEffect } from 'react';
import SelectChoice from '@/components/SelectChoice';
import { useClasses } from '@/context/ClassesContext';
import { useClasseForm } from '@/context/ClasseFormContext';
import { BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url';
import { BookOpen, Users } from 'lucide-react';
import logger from '@/utils/logger';
const SpecialityEventModal = ({
isOpen,
onClose,
selectedCell,
existingEvent,
handleUpdatePlanning,
classe,
}) => {
const { formData, setFormData } = useClasseForm();
const { groupSpecialitiesBySubject } = useClasses();
const [selectedSpeciality, setSelectedSpeciality] = useState('');
const [selectedTeacher, setSelectedTeacher] = useState('');
const [eventData, setEventData] = useState({
specialiteId: '',
teacherId: '',
start: '',
duration: '1h',
});
useEffect(() => {
if (!isOpen) {
// Réinitialiser eventData lorsque la modale se ferme
setEventData({
specialiteId: '',
teacherId: '',
start: '',
duration: '1h',
});
setSelectedSpeciality('');
setSelectedTeacher('');
}
}, [isOpen]);
useEffect(() => {
if (isOpen) {
logger.debug('debug : ', selectedCell);
if (existingEvent) {
// Mode édition
setEventData(existingEvent);
setSelectedSpeciality(existingEvent.specialiteId);
setSelectedTeacher(existingEvent.teacherId);
} else {
// Mode création
setEventData((prev) => ({
...prev,
start: selectedCell.hour,
duration: '1h',
}));
setSelectedSpeciality('');
setSelectedTeacher('');
}
}
}, [isOpen, existingEvent, selectedCell]);
if (!isOpen) return null;
const handleSubmit = (e) => {
e.preventDefault();
if (!eventData.specialiteId) {
alert('Veuillez sélectionner une spécialité');
return;
}
if (!eventData.teacherId) {
alert('Veuillez sélectionner un enseignant');
return;
}
// Transformer eventData pour correspondre au format du planning
const selectedTeacherData = formData.teachers.find(
(teacher) => teacher.id === parseInt(eventData.teacherId, 10)
);
const newCourse = {
color: '#FF0000', // Vous pouvez définir la couleur de manière dynamique si nécessaire
teachers: selectedTeacherData
? [`${selectedTeacherData.nom} ${selectedTeacherData.prenom}`]
: [],
heure: `${eventData.start}:00`,
duree: eventData.duration.replace('h', ''), // Supposons que '1h' signifie 1
matiere: 'GROUPE',
};
// Mettre à jour le planning
const updatedPlannings = classe.plannings_read.map((planning) => {
if (planning.niveau === selectedCell.selectedLevel) {
const newEmploiDuTemps = { ...planning.emploiDuTemps };
if (!newEmploiDuTemps[selectedCell.day]) {
newEmploiDuTemps[selectedCell.day] = [];
}
const courseTime = newCourse.heure;
const existingCourseIndex = newEmploiDuTemps[
selectedCell.day
].findIndex((course) => course.heure === courseTime);
if (existingCourseIndex !== -1) {
newEmploiDuTemps[selectedCell.day][existingCourseIndex] = newCourse;
} else {
newEmploiDuTemps[selectedCell.day].push(newCourse);
}
return {
...planning,
emploiDuTemps: newEmploiDuTemps,
};
}
return planning;
});
const updatedPlanning = updatedPlannings.find(
(planning) => planning.niveau === selectedCell.selectedLevel
);
setFormData((prevFormData) => ({
...prevFormData,
plannings: updatedPlannings,
}));
// Appeler handleUpdatePlanning avec les arguments appropriés
const planningId = updatedPlanning ? updatedPlanning.planning.id : null;
logger.debug('id : ', planningId);
if (planningId) {
handleUpdatePlanning(
BE_SCHOOL_PLANNINGS_URL,
planningId,
updatedPlanning.emploiDuTemps
);
}
onClose();
};
const filteredTeachers = selectedSpeciality
? formData.teachers.filter((teacher) =>
teacher.specialites.includes(parseInt(selectedSpeciality, 10))
)
: formData.teachers;
const handleSpecialityChange = (e) => {
const specialityId = e.target.value;
setSelectedSpeciality(specialityId);
// Mettre à jour eventData
setEventData((prev) => ({
...prev,
specialiteId: specialityId,
}));
};
const handleTeacherChange = (e) => {
const teacherId = e.target.value;
setSelectedTeacher(teacherId);
// Mettre à jour eventData
setEventData((prev) => ({
...prev,
teacherId: teacherId,
}));
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg w-full max-w-md">
<h2 className="text-xl font-semibold mb-4">
{eventData.id ? "Modifier l'événement" : 'Nouvel événement'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Sélection de la Spécialité */}
<div>
<SelectChoice
name="specialites"
placeHolder="Spécialités"
selected={selectedSpeciality}
choices={[
{ value: '', label: 'Sélectionner une spécialité' },
...groupSpecialitiesBySubject(formData.teachers).map(
(speciality) => ({
value: speciality.id,
label: speciality.nom,
})
),
]}
callback={handleSpecialityChange}
IconItem={BookOpen}
/>
</div>
{/* Sélection de l'enseignant */}
<div>
<SelectChoice
name="teachers"
placeHolder="Enseignants"
selected={selectedTeacher}
choices={[
{ value: '', label: 'Sélectionner un enseignant' },
...filteredTeachers.map((teacher) => ({
value: teacher.id,
label: `${teacher.nom} ${teacher.prenom}`,
})),
]}
callback={handleTeacherChange}
IconItem={Users}
disabled={!selectedSpeciality} // Désactive le sélecteur si aucune spécialité n'est sélectionnée
/>
</div>
{/* Durée */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Durée
</label>
<input
type="text"
value={eventData.duration}
onChange={(e) =>
setEventData((prev) => ({
...prev,
duration: e.target.value,
}))
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>
</div>
{/* Boutons */}
<div className="flex justify-between gap-2 mt-6">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Annuler
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-700"
>
{eventData.id ? 'Modifier' : 'Créer'}
</button>
</div>
</form>
</div>
</div>
);
};
export default SpecialityEventModal;

View File

@ -50,7 +50,7 @@ const FeesManagement = ({
}; };
return ( return (
<div className="w-full mx-auto mt-6"> <div className="w-full p-4 mx-auto mt-6">
<div className="w-4/5 mx-auto flex items-center mt-8"> <div className="w-4/5 mx-auto flex items-center mt-8">
<hr className="flex-grow border-t-2 border-gray-300" /> <hr className="flex-grow border-t-2 border-gray-300" />
<span className="mx-4 text-gray-600 font-semibold"> <span className="mx-4 text-gray-600 font-semibold">

View File

@ -1,71 +0,0 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import { useClasses } from '@/context/ClassesContext';
const ClasseFormContext = createContext();
export const useClasseForm = () => useContext(ClasseFormContext);
export const ClasseFormProvider = ({ children, initialClasse }) => {
const { getNiveauxLabels } = useClasses();
const [formData, setFormData] = useState({});
useEffect(() => {
const plannings = initialClasse.plannings_read || [];
const defaultEmploiDuTemps = {
lundi: [],
mardi: [],
mercredi: [],
jeudi: [],
vendredi: [],
samedi: [],
dimanche: [],
};
const generateEmploiDuTemps = (planningType) => {
if (planningType === 1) {
return defaultEmploiDuTemps;
} else if (planningType === 2) {
return {
S1: { DateDebut: '', DateFin: '', ...defaultEmploiDuTemps },
S2: { DateDebut: '', DateFin: '', ...defaultEmploiDuTemps },
};
} else if (planningType === 3) {
return {
T1: { DateDebut: '', DateFin: '', ...defaultEmploiDuTemps },
T2: { DateDebut: '', DateFin: '', ...defaultEmploiDuTemps },
T3: { DateDebut: '', DateFin: '', ...defaultEmploiDuTemps },
};
}
};
const newFormData = {
atmosphere_name: initialClasse.atmosphere_name || '',
age_range: initialClasse.age_range || '',
number_of_students: initialClasse.number_of_students || '',
teaching_language: initialClasse.teaching_language || 'Français',
school_year: initialClasse.school_year || '',
teachers: initialClasse.teachers || [],
teachers_details: initialClasse.teachers_details || [],
type: initialClasse.type || 1,
time_range: initialClasse.time_range || ['08:30', '17:30'],
opening_days: initialClasse.opening_days || [1, 2, 4, 5],
levels: initialClasse.levels || [],
// plannings: plannings.length ? plannings.map(planning => ({
// niveau: planning.planning.niveau,
// emploiDuTemps: planning.planning.emploiDuTemps
// })) : (initialClasse.levels || []).map(niveau => ({
// niveau: niveau,
// emploiDuTemps: generateEmploiDuTemps(initialClasse.type || 1)
// }))
};
setFormData(newFormData);
}, [initialClasse, getNiveauxLabels]);
return (
<ClasseFormContext.Provider value={{ formData, setFormData }}>
{children}
</ClasseFormContext.Provider>
);
};

View File

@ -47,22 +47,6 @@ export const ClassesProvider = ({ children }) => {
...niveauxTroisiemeCycle, ...niveauxTroisiemeCycle,
]; ];
const typeEmploiDuTemps = [
{ id: 1, label: 'Annuel' },
{ id: 2, label: 'Semestriel' },
{ id: 3, label: 'Trimestriel' },
];
const selectedDays = {
1: 'lundi',
2: 'mardi',
3: 'mercredi',
4: 'jeudi',
5: 'vendredi',
6: 'samedi',
7: 'dimanche',
};
const getNiveauxLabels = (levels) => { const getNiveauxLabels = (levels) => {
return levels.map((niveauId) => { return levels.map((niveauId) => {
const niveau = allNiveaux.find((n) => n.id === niveauId); const niveau = allNiveaux.find((n) => n.id === niveauId);
@ -132,71 +116,6 @@ export const ClassesProvider = ({ children }) => {
} }
}; };
const updatePlannings = (formData, existingPlannings) => {
return formData.levels.map((niveau) => {
let existingPlanning = existingPlannings.find(
(planning) => planning.niveau === niveau
);
const emploiDuTemps = formData.opening_days.reduce((acc, dayId) => {
const dayName = selectedDays[dayId];
if (dayName) {
acc[dayName] = existingPlanning?.emploiDuTemps?.[dayName] || [];
}
return acc;
}, {});
let updatedPlanning;
if (formData.type === 1) {
updatedPlanning = {
niveau: niveau,
emploiDuTemps,
};
} else if (formData.type === 2) {
updatedPlanning = {
niveau: niveau,
emploiDuTemps: {
S1: {
DateDebut: formData.date_debut_semestre_1,
DateFin: formData.date_fin_semestre_1,
...emploiDuTemps,
},
S2: {
DateDebut: formData.date_debut_semestre_2,
DateFin: formData.date_fin_semestre_2,
...emploiDuTemps,
},
},
};
} else if (formData.type === 3) {
updatedPlanning = {
niveau: niveau,
emploiDuTemps: {
T1: {
DateDebut: formData.date_debut_trimestre_1,
DateFin: formData.date_fin_trimestre_1,
...emploiDuTemps,
},
T2: {
DateDebut: formData.date_debut_trimestre_2,
DateFin: formData.date_fin_trimestre_2,
...emploiDuTemps,
},
T3: {
DateDebut: formData.date_debut_trimestre_3,
DateFin: formData.date_fin_trimestre_3,
...emploiDuTemps,
},
},
};
}
// Fusionner les plannings existants avec les nouvelles données
return existingPlanning
? { ...existingPlanning, ...updatedPlanning }
: updatedPlanning;
});
};
const groupSpecialitiesBySubject = (teachers) => { const groupSpecialitiesBySubject = (teachers) => {
const groupedSpecialities = {}; const groupedSpecialities = {};
@ -223,14 +142,6 @@ export const ClassesProvider = ({ children }) => {
return Object.values(groupedSpecialities); return Object.values(groupedSpecialities);
}; };
const determineInitialPeriod = (emploiDuTemps) => {
if (emploiDuTemps.S1 && emploiDuTemps.S2) {
return 'S1'; // Planning semestriel
} else if (emploiDuTemps.T1 && emploiDuTemps.T2 && emploiDuTemps.T3) {
return 'T1'; // Planning trimestriel
}
return ''; // Planning annuel ou autre
};
return ( return (
<ClassesContext.Provider <ClassesContext.Provider
@ -243,13 +154,11 @@ export const ClassesProvider = ({ children }) => {
niveauxPremierCycle, niveauxPremierCycle,
niveauxSecondCycle, niveauxSecondCycle,
niveauxTroisiemeCycle, niveauxTroisiemeCycle,
typeEmploiDuTemps, allNiveaux,
updatePlannings,
getAmbianceText, getAmbianceText,
getAmbianceName, getAmbianceName,
groupSpecialitiesBySubject, groupSpecialitiesBySubject,
getNiveauNameById, getNiveauNameById,
determineInitialPeriod,
}} }}
> >
{children} {children}

View File

@ -23,13 +23,25 @@ import { useEstablishment } from '@/context/EstablishmentContext';
*/ */
const PlanningContext = createContext(); const PlanningContext = createContext();
export function PlanningProvider({ children }) { export const RecurrenceType = Object.freeze({
// const [events, setEvents] = useState([]); NONE: 0,
// const [schedules, setSchedules] = useState([]); DAILY: 1,
// const [selectedSchedule, setSelectedSchedule] = useState(null); WEEKLY: 2,
MONTHLY: 3,
CUSTOM: 4,
});
export const PlanningModes = Object.freeze({
CLASS_SCHEDULE: 'classSchedule',
PLANNING: 'planning'
});
export function PlanningProvider({ children, modeSet=PlanningModes.PLANNING}) {
const [events, setEvents] = useState([]); const [events, setEvents] = useState([]);
const [schedules, setSchedules] = useState([]); const [schedules, setSchedules] = useState([]);
const [selectedSchedule, setSelectedSchedule] = useState(0); const [selectedSchedule, setSelectedSchedule] = useState(0);
const [planningMode, setPlanningMode] = useState(modeSet);
const [currentDate, setCurrentDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(new Date());
const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week' const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week'
const [hiddenSchedules, setHiddenSchedules] = useState([]); const [hiddenSchedules, setHiddenSchedules] = useState([]);
@ -37,60 +49,61 @@ export function PlanningProvider({ children }) {
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
useEffect(() => { useEffect(() => {
fetchPlannings().then((data) => { reloadPlanning();
reloadEvents();
}, [planningMode, selectedEstablishmentId]);
const reloadEvents = () =>{
fetchEvents(selectedEstablishmentId, planningMode).then((data) => {
setEvents(data);
});
}
const reloadPlanning = () =>{
fetchPlannings(selectedEstablishmentId, planningMode).then((data) => {
setSchedules(data); setSchedules(data);
if (data.length > 0) { if (data.length > 0) {
setSelectedSchedule(data[0].id); setSelectedSchedule(data[0].id);
} }
}); });
fetchEvents().then((data) => { }
setEvents(data);
});
}, []);
const addEvent = (newEvent) => { const addEvent = (newEvent) => {
createEvent(newEvent).then((data) => { createEvent(newEvent).then(() => {
setEvents((prevEvents) => [...prevEvents, data]); reloadEvents();
}); });
}; };
const handleUpdateEvent = (id, updatedEvent) => { const handleUpdateEvent = (id, updatedEvent) => {
updateEvent(id, updatedEvent, csrfToken).then((data) => { updateEvent(id, updatedEvent, csrfToken).then((data) => {
setEvents((prevEvents) => reloadEvents();
prevEvents.map((event) => (event.id === id ? updatedEvent : event))
);
}); });
}; };
const handleDeleteEvent = (id) => { const handleDeleteEvent = (id) => {
deleteEvent(id, csrfToken).then((data) => { deleteEvent(id, csrfToken).then((data) => {
setEvents((prevEvents) => prevEvents.filter((event) => event.id !== id)); reloadEvents();
}); });
}; };
const addSchedule = (newSchedule) => { const addSchedule = (newSchedule) => {
logger.debug('newSchedule', newSchedule); logger.debug('newSchedule', newSchedule);
newSchedule.establishment = selectedEstablishmentId; newSchedule.establishment = selectedEstablishmentId;
createPlanning(newSchedule, csrfToken).then((data) => { createPlanning(newSchedule, csrfToken).then((_) => {
setSchedules((prevSchedules) => [...prevSchedules, data]); reloadPlanning();
}); });
}; };
const updateSchedule = (id, updatedSchedule) => { const updateSchedule = (id, updatedSchedule) => {
updatePlanning(id, updatedSchedule, csrfToken).then((data) => { updatePlanning(id, updatedSchedule, csrfToken).then((data) => {
setSchedules((prevSchedules) => reloadPlanning();
prevSchedules.map((schedule) =>
schedule.id === id ? updatedSchedule : schedule
)
);
}); });
}; };
const deleteSchedule = (id) => { const deleteSchedule = (id) => {
deletePlanning(id, csrfToken).then((data) => { deletePlanning(id, csrfToken).then((data) => {
setSchedules((prevSchedules) => reloadPlanning();
prevSchedules.filter((schedule) => schedule.id !== id) reloadEvents();
);
}); });
}; };
@ -123,6 +136,9 @@ export function PlanningProvider({ children }) {
setViewType, setViewType,
hiddenSchedules, hiddenSchedules,
toggleScheduleVisibility, toggleScheduleVisibility,
planningMode,
reloadEvents,
reloadPlanning,
}; };
return ( return (

View File

@ -1,18 +0,0 @@
import React, { createContext, useState, useContext } from 'react';
const SpecialityFormContext = createContext();
export const useSpecialityForm = () => useContext(SpecialityFormContext);
export const SpecialityFormProvider = ({ children, initialSpeciality }) => {
const [formData, setFormData] = useState(() => ({
name: initialSpeciality.name || '',
color_code: initialSpeciality.color_code || '#FFFFFF',
}));
return (
<SpecialityFormContext.Provider value={{ formData, setFormData }}>
{children}
</SpecialityFormContext.Provider>
);
};

View File

@ -1,25 +0,0 @@
import React, { createContext, useState, useContext } from 'react';
const TeacherFormContext = createContext();
export const useTeacherForm = () => useContext(TeacherFormContext);
export const TeacherFormProvider = ({ children, initialTeacher }) => {
const [formData, setFormData] = useState(() => ({
last_name: initialTeacher.last_name || '',
first_name: initialTeacher.first_name || '',
email: initialTeacher.email || '',
specialities: initialTeacher.specialities || [],
associated_profile: initialTeacher.associated_profile || '',
droit: {
label: initialTeacher.droit?.label || '',
id: initialTeacher.droit?.id || 0,
},
}));
return (
<TeacherFormContext.Provider value={{ formData, setFormData }}>
{children}
</TeacherFormContext.Provider>
);
};

View File

@ -1,3 +1,4 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@ -1,30 +0,0 @@
export const WEEKDAYS = [
{ id: 1, name: 'Lundi' },
{ id: 2, name: 'Mardi' },
{ id: 3, name: 'Mercredi' },
{ id: 4, name: 'Jeudi' },
{ id: 5, name: 'Vendredi' },
{ id: 6, name: 'Samedi' },
{ id: 7, name: 'Dimanche' },
];
export const DEFAULT_EVENT = {
title: '',
description: '',
start: '',
end: '',
location: '',
planning: 'default',
color: '#10b981',
recurrence: 'none',
selectedDays: [],
recurrenceEnd: '',
};
export const VIEW_TYPES = {
WEEK: 'week',
MONTH: 'month',
YEAR: 'year',
};
export const TIME_SLOTS = Array.from({ length: 24 }, (_, i) => i);