mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
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:
@ -2,6 +2,7 @@ from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from School.models import SchoolClass
|
||||
|
||||
from Establishment.models import Establishment
|
||||
|
||||
@ -14,25 +15,33 @@ class RecursionType(models.IntegerChoices):
|
||||
|
||||
class Planning(models.Model):
|
||||
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)
|
||||
description = models.TextField(default="", blank=True, null=True)
|
||||
color= models.CharField(max_length=255, default="#000000")
|
||||
|
||||
def __str__(self):
|
||||
return f'Planning for {self.user.username}'
|
||||
return f'Planning {self.name}'
|
||||
|
||||
|
||||
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)
|
||||
description = models.TextField()
|
||||
description = models.TextField(default="", blank=True, null=True)
|
||||
start = models.DateTimeField()
|
||||
end = models.DateTimeField()
|
||||
recursionType = models.IntegerField(choices=RecursionType, default=0)
|
||||
recursionEnd = models.DateTimeField(default=None, blank=True, null=True)
|
||||
color= models.CharField(max_length=255)
|
||||
location = models.CharField(max_length=255, default="", blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f'Event for {self.user.username}'
|
||||
return f'Event {self.title}'
|
||||
@ -7,5 +7,5 @@ urlpatterns = [
|
||||
re_path(r'^plannings/(?P<id>[0-9]+)$', PlanningWithIdView.as_view(), name="planning"),
|
||||
re_path(r'^events$', EventsView.as_view(), name="events"),
|
||||
re_path(r'^events/(?P<id>[0-9]+)$', EventsWithIdView.as_view(), name="events"),
|
||||
re_path(r'^events/upcoming', UpcomingEventsView.as_view(), name="events"),
|
||||
re_path(r'^events/upcoming', UpcomingEventsView.as_view(), name="events"),
|
||||
]
|
||||
@ -1,18 +1,32 @@
|
||||
from django.http.response import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
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 N3wtSchool import bdd
|
||||
|
||||
|
||||
class PlanningView(APIView):
|
||||
def get(self, request):
|
||||
plannings=bdd.getAllObjects(Planning)
|
||||
planning_serializer=PlanningSerializer(plannings, many=True)
|
||||
establishment_id = request.GET.get('establishment_id', None)
|
||||
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)
|
||||
|
||||
def post(self, request):
|
||||
@ -56,17 +70,63 @@ class PlanningWithIdView(APIView):
|
||||
|
||||
class EventsView(APIView):
|
||||
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)
|
||||
return JsonResponse(events_serializer.data, safe=False)
|
||||
|
||||
def post(self, request):
|
||||
events_serializer = EventsSerializer(data=request.data)
|
||||
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.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):
|
||||
def put(self, request, id):
|
||||
try:
|
||||
@ -92,6 +152,18 @@ class EventsWithIdView(APIView):
|
||||
class UpcomingEventsView(APIView):
|
||||
def get(self, request):
|
||||
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)
|
||||
return JsonResponse(events_serializer.data, safe=False)
|
||||
@ -5,24 +5,24 @@ from rest_framework.parsers import JSONParser
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
from .models import (
|
||||
Teacher,
|
||||
Speciality,
|
||||
SchoolClass,
|
||||
Planning,
|
||||
Discount,
|
||||
Fee,
|
||||
PaymentPlan,
|
||||
Teacher,
|
||||
Speciality,
|
||||
SchoolClass,
|
||||
Planning,
|
||||
Discount,
|
||||
Fee,
|
||||
PaymentPlan,
|
||||
PaymentMode,
|
||||
AbsenceManagement
|
||||
)
|
||||
from .serializers import (
|
||||
TeacherSerializer,
|
||||
SpecialitySerializer,
|
||||
SchoolClassSerializer,
|
||||
PlanningSerializer,
|
||||
DiscountSerializer,
|
||||
FeeSerializer,
|
||||
PaymentPlanSerializer,
|
||||
TeacherSerializer,
|
||||
SpecialitySerializer,
|
||||
SchoolClassSerializer,
|
||||
PlanningSerializer,
|
||||
DiscountSerializer,
|
||||
FeeSerializer,
|
||||
PaymentPlanSerializer,
|
||||
PaymentModeSerializer,
|
||||
AbsenceManagementSerializer
|
||||
)
|
||||
@ -93,8 +93,8 @@ class TeacherListCreateView(APIView):
|
||||
|
||||
if teacher_serializer.is_valid():
|
||||
teacher_serializer.save()
|
||||
|
||||
return JsonResponse(teacher_serializer.data, safe=False)
|
||||
|
||||
return JsonResponse(teacher_serializer.data, safe=False)
|
||||
|
||||
return JsonResponse(teacher_serializer.errors, safe=False)
|
||||
|
||||
@ -139,7 +139,7 @@ class SchoolClassListCreateView(APIView):
|
||||
classe_serializer = SchoolClassSerializer(data=classe_data)
|
||||
|
||||
if classe_serializer.is_valid():
|
||||
classe_serializer.save()
|
||||
classe_serializer.save()
|
||||
return JsonResponse(classe_serializer.data, safe=False)
|
||||
|
||||
return JsonResponse(classe_serializer.errors, safe=False)
|
||||
@ -195,7 +195,7 @@ class PlanningDetailView(APIView):
|
||||
|
||||
def put(self, request, id):
|
||||
planning_data = JSONParser().parse(request)
|
||||
|
||||
|
||||
try:
|
||||
planning = Planning.objects.get(id=id)
|
||||
except Planning.DoesNotExist:
|
||||
@ -210,7 +210,7 @@ class PlanningDetailView(APIView):
|
||||
return JsonResponse(planning_serializer.data, safe=False)
|
||||
|
||||
return JsonResponse(planning_serializer.errors, safe=False)
|
||||
|
||||
|
||||
def delete(self, request, id):
|
||||
return delete_object(Planning, id)
|
||||
|
||||
@ -227,7 +227,7 @@ class FeeListCreateView(APIView):
|
||||
|
||||
fees = Fee.objects.filter(type=fee_type_value, establishment_id=establishment_id).distinct()
|
||||
fee_serializer = FeeSerializer(fees, many=True)
|
||||
|
||||
|
||||
return JsonResponse(fee_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request):
|
||||
@ -277,7 +277,7 @@ class DiscountListCreateView(APIView):
|
||||
|
||||
discounts = Discount.objects.filter(type=discount_type_value, establishment_id=establishment_id).distinct()
|
||||
discounts_serializer = DiscountSerializer(discounts, many=True)
|
||||
|
||||
|
||||
return JsonResponse(discounts_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request):
|
||||
@ -327,7 +327,7 @@ class PaymentPlanListCreateView(APIView):
|
||||
|
||||
payment_plans = PaymentPlan.objects.filter(type=type_value, establishment_id=establishment_id).distinct()
|
||||
payment_plans_serializer = PaymentPlanSerializer(payment_plans, many=True)
|
||||
|
||||
|
||||
return JsonResponse(payment_plans_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request):
|
||||
@ -377,7 +377,7 @@ class PaymentModeListCreateView(APIView):
|
||||
|
||||
payment_modes = PaymentMode.objects.filter(type=type_value, establishment_id=establishment_id).distinct()
|
||||
payment_modes_serializer = PaymentModeSerializer(payment_modes, many=True)
|
||||
|
||||
|
||||
return JsonResponse(payment_modes_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request):
|
||||
|
||||
@ -43,7 +43,6 @@ export default function Layout({ children }) {
|
||||
const { profileRole, establishments, user, clearContext } =
|
||||
useEstablishment();
|
||||
|
||||
// Déplacer le reste du code ici...
|
||||
const sidebarItems = {
|
||||
admin: {
|
||||
id: 'admin',
|
||||
@ -144,12 +143,40 @@ export default function Layout({ children }) {
|
||||
|
||||
return (
|
||||
<ProtectedRoute requiredRight={[RIGHTS.ADMIN, RIGHTS.TEACHER]}>
|
||||
<div className="flex min-h-screen bg-gray-50 relative">
|
||||
{/* Retirer la condition !isLoading car on gère déjà le chargement au début */}
|
||||
{/* Sidebar avec hauteur forcée */}
|
||||
|
||||
{/* Topbar */}
|
||||
<header className="absolute top-0 left-0 right-0 h-16 bg-white border-b border-gray-200 px-4 md:px-8 flex items-center justify-between z-10 box-border">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="mr-4 md:hidden text-gray-600 hover:text-gray-900"
|
||||
onClick={toggleSidebar}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{isSidebarOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
<div className="text-lg md:text-xl font-semibold">{headerTitle}</div>
|
||||
</div>
|
||||
<DropdownMenu
|
||||
buttonContent={
|
||||
<Image
|
||||
src={getGravatarUrl(user?.email)}
|
||||
alt="Profile"
|
||||
className="w-8 h-8 rounded-full cursor-pointer"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
}
|
||||
items={dropdownItems}
|
||||
buttonClassName=""
|
||||
menuClassName="absolute right-0 mt-2 w-64 bg-white border border-gray-200 rounded shadow-lg"
|
||||
/>
|
||||
</header>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className={`md:block ${isSidebarOpen ? 'block' : 'hidden'} fixed md:relative inset-y-0 left-0 z-30 h-full`}
|
||||
style={{ height: '100vh' }} // Force la hauteur à 100% de la hauteur de la vue
|
||||
className={`absolute top-16 bottom-16 left-0 z-30 w-64 bg-white border-r border-gray-200 box-border ${
|
||||
isSidebarOpen ? 'block' : 'hidden md:block'
|
||||
}`}
|
||||
>
|
||||
<Sidebar
|
||||
establishments={establishments}
|
||||
@ -159,7 +186,7 @@ export default function Layout({ children }) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Overlay pour fermer la sidebar en cliquant à l'extérieur sur mobile */}
|
||||
{/* Overlay for mobile */}
|
||||
{isSidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-20 md:hidden"
|
||||
@ -167,48 +194,19 @@ export default function Layout({ children }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Header responsive */}
|
||||
<header className="h-16 bg-white border-b border-gray-200 px-4 md:px-8 py-4 flex items-center justify-between z-10">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="mr-4 md:hidden text-gray-600 hover:text-gray-900"
|
||||
onClick={toggleSidebar}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{isSidebarOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
<div className="text-lg md:text-xl font-semibold">
|
||||
{headerTitle}
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu
|
||||
buttonContent={
|
||||
<Image
|
||||
src={getGravatarUrl(user?.email)}
|
||||
alt="Profile"
|
||||
className="w-8 h-8 rounded-full cursor-pointer"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
}
|
||||
items={dropdownItems}
|
||||
buttonClassName=""
|
||||
menuClassName="absolute right-0 mt-2 w-64 bg-white border border-gray-200 rounded shadow-lg"
|
||||
/>
|
||||
</header>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Content avec scroll si nécessaire */}
|
||||
<div className="flex-1 overflow-auto p-4 md:p-6">{children}</div>
|
||||
{/* Footer responsive */}
|
||||
<Footer
|
||||
softwareName={softwareName}
|
||||
softwareVersion={softwareVersion}
|
||||
/>
|
||||
</div>
|
||||
{/* Main container */}
|
||||
<div className="absolute overflow-auto bg-gray-50 top-16 bottom-16 left-64 right-0 ">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
<Footer
|
||||
softwareName={softwareName}
|
||||
softwareVersion={softwareVersion}
|
||||
/>
|
||||
|
||||
|
||||
<Popup
|
||||
visible={isPopupVisible}
|
||||
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?"
|
||||
|
||||
@ -77,7 +77,7 @@ export default function DashboardPage() {
|
||||
});
|
||||
|
||||
// Fetch des événements à venir
|
||||
fetchUpcomingEvents()
|
||||
fetchUpcomingEvents(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setUpcomingEvents(data);
|
||||
})
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
import { PlanningProvider } from '@/context/PlanningContext';
|
||||
import Calendar from '@/components/Calendar';
|
||||
import EventModal from '@/components/EventModal';
|
||||
import ScheduleNavigation from '@/components/ScheduleNavigation';
|
||||
import { PlanningModes, PlanningProvider, RecurrenceType } from '@/context/PlanningContext';
|
||||
import Calendar from '@/components/Calendar/Calendar';
|
||||
import EventModal from '@/components/Calendar/EventModal';
|
||||
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
|
||||
import { useState } from 'react';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
|
||||
export default function Page() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@ -14,13 +15,14 @@ export default function Page() {
|
||||
end: '',
|
||||
location: '',
|
||||
planning: '', // Enlever la valeur par défaut ici
|
||||
recurrence: 'none',
|
||||
recursionType: RecurrenceType.NONE,
|
||||
selectedDays: [],
|
||||
recurrenceEnd: '',
|
||||
recursionEnd: '',
|
||||
customInterval: 1,
|
||||
customUnit: 'days',
|
||||
viewType: 'week', // Ajouter la vue semaine par défaut
|
||||
});
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
const initializeNewEvent = (date = new Date()) => {
|
||||
// S'assurer que date est un objet Date valide
|
||||
@ -33,9 +35,11 @@ export default function Page() {
|
||||
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
||||
location: '',
|
||||
planning: '', // Ne pas définir de valeur par défaut ici non plus
|
||||
recurrence: 'none',
|
||||
recursionType: RecurrenceType.NONE,
|
||||
selectedDays: [],
|
||||
recurrenceEnd: '',
|
||||
recursionEnd: new Date(
|
||||
eventDate.getTime() + 2 * 60 * 60 * 1000
|
||||
).toISOString(),
|
||||
customInterval: 1,
|
||||
customUnit: 'days',
|
||||
});
|
||||
@ -43,7 +47,8 @@ export default function Page() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PlanningProvider>
|
||||
<PlanningProvider establishmentId={selectedEstablishmentId} modeSet={PlanningModes.PLANNING}>
|
||||
{/* <div className="flex h-full overflow-hidden"> */}
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<ScheduleNavigation />
|
||||
<Calendar
|
||||
|
||||
@ -29,12 +29,12 @@ import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManag
|
||||
import { fetchRegistrationSchoolFileMasters } from '@/app/actions/registerFileGroupAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
||||
|
||||
export default function Page() {
|
||||
const [specialities, setSpecialities] = useState([]);
|
||||
const [classes, setClasses] = useState([]);
|
||||
const [teachers, setTeachers] = useState([]);
|
||||
const [schedules, setSchedules] = useState([]);
|
||||
const [registrationDiscounts, setRegistrationDiscounts] = useState([]);
|
||||
const [tuitionDiscounts, setTuitionDiscounts] = useState([]);
|
||||
const [registrationFees, setRegistrationFees] = useState([]);
|
||||
@ -60,8 +60,6 @@ export default function Page() {
|
||||
// Fetch data for classes
|
||||
handleClasses();
|
||||
|
||||
// Fetch data for schedules
|
||||
handleSchedules();
|
||||
|
||||
// Fetch data for registration discounts
|
||||
handleRegistrationDiscounts();
|
||||
@ -128,13 +126,6 @@ export default function Page() {
|
||||
.catch((error) => logger.error('Error fetching classes:', error));
|
||||
};
|
||||
|
||||
const handleSchedules = () => {
|
||||
fetchSchedules()
|
||||
.then((data) => {
|
||||
setSchedules(data);
|
||||
})
|
||||
.catch((error) => logger.error('Error fetching schedules:', error));
|
||||
};
|
||||
|
||||
const handleRegistrationDiscounts = () => {
|
||||
fetchRegistrationDiscounts(selectedEstablishmentId)
|
||||
@ -299,6 +290,8 @@ export default function Page() {
|
||||
<ScheduleManagement
|
||||
handleUpdatePlanning={handleUpdatePlanning}
|
||||
classes={classes}
|
||||
specialities={specialities}
|
||||
teachers={teachers}
|
||||
/>
|
||||
</ClassesProvider>
|
||||
),
|
||||
@ -343,12 +336,12 @@ export default function Page() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<>
|
||||
<PlanningProvider establishmentId={selectedEstablishmentId} modeSet={PlanningModes.CLASS_SCHEDULE}>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<SidebarTabs tabs={tabs} />
|
||||
</PlanningProvider>
|
||||
</>
|
||||
|
||||
<div className="w-full p-4">
|
||||
<SidebarTabs tabs={tabs} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,12 +2,14 @@ import { BE_PLANNING_PLANNINGS_URL, BE_PLANNING_EVENTS_URL } from '@/utils/Url';
|
||||
|
||||
const requestResponseHandler = async (response) => {
|
||||
const body = response.status !== 204 ? await response?.json() : {};
|
||||
console.log(response);
|
||||
if (response.ok) {
|
||||
return body;
|
||||
}
|
||||
// Throw an error with the JSON body containing the form errors
|
||||
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
|
||||
const error = new Error(
|
||||
body?.errorMessage ||
|
||||
`Une erreur est survenue code de retour : ${response.status}`
|
||||
);
|
||||
error.details = body;
|
||||
throw error;
|
||||
};
|
||||
@ -51,8 +53,15 @@ const removeDatas = (url, csrfToken) => {
|
||||
}).then(requestResponseHandler);
|
||||
};
|
||||
|
||||
export const fetchPlannings = () => {
|
||||
return getData(`${BE_PLANNING_PLANNINGS_URL}`);
|
||||
export const fetchPlannings = (establishment_id=null,planningMode=null) => {
|
||||
let url = `${BE_PLANNING_PLANNINGS_URL}`;
|
||||
if (establishment_id) {
|
||||
url += `?establishment_id=${establishment_id}`;
|
||||
}
|
||||
if (planningMode) {
|
||||
url += `&planning_mode=${planningMode}`;
|
||||
}
|
||||
return getData(url);
|
||||
};
|
||||
|
||||
export const getPlanning = (id) => {
|
||||
@ -71,8 +80,17 @@ export const deletePlanning = (id, csrfToken) => {
|
||||
return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken);
|
||||
};
|
||||
|
||||
export const fetchEvents = () => {
|
||||
return getData(`${BE_PLANNING_EVENTS_URL}`);
|
||||
export const fetchEvents = (establishment_id=null, planningMode=null) => {
|
||||
let url = `${BE_PLANNING_EVENTS_URL}`;
|
||||
if (establishment_id) {
|
||||
url += `?establishment_id=${establishment_id}`;
|
||||
}
|
||||
if (planningMode) {
|
||||
url += `&planning_mode=${planningMode}`;
|
||||
}
|
||||
|
||||
return getData(url);
|
||||
|
||||
};
|
||||
|
||||
export const getEvent = (id) => {
|
||||
@ -91,6 +109,10 @@ export const deleteEvent = (id, csrfToken) => {
|
||||
return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken);
|
||||
};
|
||||
|
||||
export const fetchUpcomingEvents = () => {
|
||||
return getData(`${BE_PLANNING_EVENTS_URL}/upcoming`);
|
||||
export const fetchUpcomingEvents = (establishment_id=null) => {
|
||||
let url = `${BE_PLANNING_EVENTS_URL}/upcoming`;
|
||||
if (establishment_id) {
|
||||
url += `?establishment_id=${establishment_id}`;
|
||||
}
|
||||
return getData(`${url}`);
|
||||
};
|
||||
|
||||
@ -28,7 +28,7 @@ export default async function RootLayout({ children, params }) {
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body>
|
||||
<body className='p-0 m-0'>
|
||||
<Providers messages={messages} locale={locale} session={params.session}>
|
||||
{children}
|
||||
</Providers>
|
||||
|
||||
@ -22,7 +22,7 @@ import { fr } from 'date-fns/locale';
|
||||
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
const {
|
||||
currentDate,
|
||||
setCurrentDate,
|
||||
@ -1,4 +1,4 @@
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { usePlanning, RecurrenceType } from '@/context/PlanningContext';
|
||||
import { format } from 'date-fns';
|
||||
import React from 'react';
|
||||
|
||||
@ -13,23 +13,23 @@ export default function EventModal({
|
||||
|
||||
// S'assurer que planning est défini lors du premier rendu
|
||||
React.useEffect(() => {
|
||||
if (!eventData.planning && schedules.length > 0) {
|
||||
if (!eventData?.planning && schedules.length > 0) {
|
||||
setEventData((prev) => ({
|
||||
...prev,
|
||||
planning: schedules[0].id,
|
||||
color: schedules[0].color,
|
||||
}));
|
||||
}
|
||||
}, [schedules, eventData.planning]);
|
||||
}, [schedules, eventData?.planning]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const recurrenceOptions = [
|
||||
{ value: 'none', label: 'Aucune' },
|
||||
{ value: 'daily', label: 'Quotidienne' },
|
||||
{ value: 'weekly', label: 'Hebdomadaire' },
|
||||
{ value: 'monthly', label: 'Mensuelle' },
|
||||
{ value: 'custom', label: 'Personnalisée' }, // Nouvelle option
|
||||
{ value: RecurrenceType.NONE, label: 'Aucune' },
|
||||
{ value: RecurrenceType.DAILY, label: 'Quotidienne' },
|
||||
{ value: RecurrenceType.WEEKLY, label: 'Hebdomadaire' },
|
||||
{ value: RecurrenceType.MONTHLY, label: 'Mensuelle' },
|
||||
/* { value: RecurrenceType.CUSTOM, label: 'Personnalisée' }, */
|
||||
];
|
||||
|
||||
const daysOfWeek = [
|
||||
@ -171,10 +171,13 @@ export default function EventModal({
|
||||
Récurrence
|
||||
</label>
|
||||
<select
|
||||
value={eventData.recurrence || 'none'}
|
||||
onChange={(e) =>
|
||||
setEventData({ ...eventData, recurrence: e.target.value })
|
||||
}
|
||||
value={eventData.recursionType || RecurrenceType.NONE}
|
||||
onChange={(e) => {
|
||||
return setEventData({
|
||||
...eventData,
|
||||
recursionType: e.target.value,
|
||||
});
|
||||
}}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
>
|
||||
{recurrenceOptions.map((option) => (
|
||||
@ -186,46 +189,7 @@ export default function EventModal({
|
||||
</div>
|
||||
|
||||
{/* Paramètres de récurrence personnalisée */}
|
||||
{eventData.recurrence === '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' && (
|
||||
{eventData.recursionType == RecurrenceType.CUSTOM && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Jours de répétition
|
||||
@ -256,16 +220,25 @@ export default function EventModal({
|
||||
)}
|
||||
|
||||
{/* Date de fin de récurrence */}
|
||||
{eventData.recurrence !== 'none' && (
|
||||
{eventData.recursionType != RecurrenceType.NONE && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Fin de récurrence
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={eventData.recurrenceEnd || ''}
|
||||
value={
|
||||
eventData.recursionEnd
|
||||
? format(new Date(eventData.recursionEnd), 'yyyy-MM-dd')
|
||||
: ''
|
||||
}
|
||||
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"
|
||||
/>
|
||||
249
Front-End/src/components/Calendar/ScheduleNavigation.js
Normal file
249
Front-End/src/components/Calendar/ScheduleNavigation.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import {
|
||||
format,
|
||||
startOfWeek,
|
||||
addDays,
|
||||
differenceInMinutes,
|
||||
isSameDay,
|
||||
} from 'date-fns';
|
||||
import { format, startOfWeek, addDays, isSameDay } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getWeekEvents } from '@/utils/events';
|
||||
import { isToday } from 'date-fns';
|
||||
@ -49,7 +43,8 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const getCurrentTimePosition = () => {
|
||||
const hours = currentTime.getHours();
|
||||
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
|
||||
@ -144,17 +139,17 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
};
|
||||
|
||||
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 */}
|
||||
<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)' }}
|
||||
>
|
||||
<div className="bg-white h-14"></div>
|
||||
{weekDays.map((day) => (
|
||||
<div
|
||||
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'}
|
||||
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
|
||||
>
|
||||
@ -172,7 +167,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
</div>
|
||||
|
||||
{/* Grille horaire */}
|
||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
|
||||
<div ref={scrollContainerRef} className="flex-1 relative">
|
||||
{/* Ligne de temps actuelle */}
|
||||
{isCurrentWeek && (
|
||||
<div
|
||||
@ -181,12 +176,12 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
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
|
||||
className="grid gap-[1px] bg-gray-100"
|
||||
className="grid gap-[1px] w-full bg-gray-100"
|
||||
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
|
||||
>
|
||||
{timeSlots.map((hour) => (
|
||||
@ -209,9 +204,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
onDateClick(date);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
{' '}
|
||||
{/* Ajout de gap-1 */}
|
||||
<div className="grid gap-1">
|
||||
{dayEvents
|
||||
.filter((event) => {
|
||||
const eventStart = new Date(event.start);
|
||||
|
||||
@ -2,7 +2,7 @@ import Logo from '@/components/Logo';
|
||||
|
||||
export default function Footer({ softwareName, softwareVersion }) {
|
||||
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">
|
||||
<span>
|
||||
© {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -4,8 +4,8 @@ const SidebarTabs = ({ tabs }) => {
|
||||
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
||||
|
||||
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) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
@ -20,17 +20,15 @@ const SidebarTabs = ({ tabs }) => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`${activeTab === tab.id ? 'block' : 'hidden'}`}
|
||||
className={`${activeTab === tab.id ? 'block h-[calc(100%-3.5rem)]' : 'hidden'}`}
|
||||
>
|
||||
{tab.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -12,8 +12,10 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import logger from '@/utils/logger';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL } from '@/utils/Url';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
|
||||
|
||||
const ItemTypes = {
|
||||
TEACHER: 'teacher',
|
||||
@ -117,6 +119,7 @@ const ClassesSection = ({
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({});
|
||||
const [editingClass, setEditingClass] = useState(null);
|
||||
@ -129,41 +132,10 @@ const ClassesSection = ({
|
||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||
const [detailsModalVisible, setDetailsModalVisible] = useState(false);
|
||||
const [selectedClass, setSelectedClass] = useState(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
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
|
||||
const getSchoolYearChoices = () => {
|
||||
@ -241,6 +213,19 @@ const ClassesSection = ({
|
||||
setClasses((prevClasses) => [createdClass, ...classes]);
|
||||
setNewClass(null);
|
||||
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) => {
|
||||
logger.error('Error:', error.message);
|
||||
@ -505,6 +490,8 @@ const ClassesSection = ({
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
reloadPlanning();
|
||||
reloadEvents();
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -22,7 +22,7 @@ const StructureManagement = ({
|
||||
handleDelete,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full mx-auto mt-6">
|
||||
<div className="w-full p-4 mx-auto mt-6">
|
||||
<ClassesProvider>
|
||||
<div className="mt-8 w-2/5">
|
||||
<SpecialitiesSection
|
||||
|
||||
@ -462,7 +462,7 @@ export default function FilesGroupsManagement({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto mt-6">
|
||||
<div className="w-full p-4 mx-auto mt-6">
|
||||
{/* Modal pour les fichiers */}
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -1,211 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } 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 React, { useState } from 'react';
|
||||
|
||||
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 handleOpenModal = () => setIsModalOpen(true);
|
||||
const handleCloseModal = () => setIsModalOpen(false);
|
||||
const [eventData, setEventData] = useState({
|
||||
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(() => {
|
||||
if (selectedClass) {
|
||||
const defaultLevel = niveauxLabels.length > 0 ? niveauxLabels[0].id : '';
|
||||
const niveau = selectedLevel || defaultLevel;
|
||||
const initializeNewEvent = (date = new Date()) => {
|
||||
// S'assurer que date est un objet Date valide
|
||||
const eventDate = date instanceof Date ? date : new Date();
|
||||
|
||||
setSelectedLevel(niveau);
|
||||
|
||||
const currentPlanning = selectedClass.plannings_read?.find(
|
||||
(planning) => planning.niveau === niveau
|
||||
);
|
||||
setSchedule(currentPlanning ? currentPlanning.planning : {});
|
||||
}
|
||||
}, [selectedClass, niveauxLabels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedClass && selectedLevel) {
|
||||
const currentPlanning = selectedClass.plannings_read?.find(
|
||||
(planning) => planning.niveau === selectedLevel
|
||||
);
|
||||
setSchedule(currentPlanning ? currentPlanning.planning : {});
|
||||
}
|
||||
}, [selectedClass, selectedLevel]);
|
||||
|
||||
const handleLevelSelect = (niveau) => {
|
||||
setSelectedLevel(niveau);
|
||||
setEventData({
|
||||
title: '',
|
||||
description: '',
|
||||
start: eventDate.toISOString(),
|
||||
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
||||
location: '',
|
||||
planning: '', // Ne pas définir de valeur par défaut ici non plus
|
||||
recursionType: RecurrenceType.NONE,
|
||||
selectedDays: [],
|
||||
recursionEnd: new Date(
|
||||
eventDate.getTime() + 2 * 60 * 60 * 1000
|
||||
).toISOString(),
|
||||
customInterval: 1,
|
||||
customUnit: 'days',
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col h-full">
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="p-4 bg-gray-100 border-b">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{/* Colonne Classes */}
|
||||
<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">
|
||||
<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,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Colonne Niveaux */}
|
||||
<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">
|
||||
<Bookmark className="w-8 h-8 mr-2" />
|
||||
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 className="flex h-full overflow-hidden">
|
||||
<ScheduleNavigation classes={classes} />
|
||||
<Calendar
|
||||
onDateClick={initializeNewEvent}
|
||||
onEventClick={(event) => {
|
||||
setEventData(event);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
<ScheduleEventModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
eventData={eventData}
|
||||
setEventData={setEventData}
|
||||
specialities={specialities}
|
||||
teachers={teachers}
|
||||
classes={classes}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ScheduleManagement;
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -50,7 +50,7 @@ const FeesManagement = ({
|
||||
};
|
||||
|
||||
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">
|
||||
<hr className="flex-grow border-t-2 border-gray-300" />
|
||||
<span className="mx-4 text-gray-600 font-semibold">
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -47,22 +47,6 @@ export const ClassesProvider = ({ children }) => {
|
||||
...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) => {
|
||||
return levels.map((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 groupedSpecialities = {};
|
||||
@ -223,14 +142,6 @@ export const ClassesProvider = ({ children }) => {
|
||||
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 (
|
||||
<ClassesContext.Provider
|
||||
@ -243,13 +154,11 @@ export const ClassesProvider = ({ children }) => {
|
||||
niveauxPremierCycle,
|
||||
niveauxSecondCycle,
|
||||
niveauxTroisiemeCycle,
|
||||
typeEmploiDuTemps,
|
||||
updatePlannings,
|
||||
allNiveaux,
|
||||
getAmbianceText,
|
||||
getAmbianceName,
|
||||
groupSpecialitiesBySubject,
|
||||
getNiveauNameById,
|
||||
determineInitialPeriod,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -23,13 +23,25 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
*/
|
||||
const PlanningContext = createContext();
|
||||
|
||||
export function PlanningProvider({ children }) {
|
||||
// const [events, setEvents] = useState([]);
|
||||
// const [schedules, setSchedules] = useState([]);
|
||||
// const [selectedSchedule, setSelectedSchedule] = useState(null);
|
||||
export const RecurrenceType = Object.freeze({
|
||||
NONE: 0,
|
||||
DAILY: 1,
|
||||
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 [schedules, setSchedules] = useState([]);
|
||||
const [selectedSchedule, setSelectedSchedule] = useState(0);
|
||||
const [planningMode, setPlanningMode] = useState(modeSet);
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week'
|
||||
const [hiddenSchedules, setHiddenSchedules] = useState([]);
|
||||
@ -37,60 +49,61 @@ export function PlanningProvider({ children }) {
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
useEffect(() => {
|
||||
fetchPlannings().then((data) => {
|
||||
setSchedules(data);
|
||||
if (data.length > 0) {
|
||||
setSelectedSchedule(data[0].id);
|
||||
}
|
||||
});
|
||||
fetchEvents().then((data) => {
|
||||
setEvents(data);
|
||||
});
|
||||
}, []);
|
||||
reloadPlanning();
|
||||
reloadEvents();
|
||||
}, [planningMode, selectedEstablishmentId]);
|
||||
|
||||
|
||||
const reloadEvents = () =>{
|
||||
fetchEvents(selectedEstablishmentId, planningMode).then((data) => {
|
||||
setEvents(data);
|
||||
});
|
||||
}
|
||||
|
||||
const reloadPlanning = () =>{
|
||||
fetchPlannings(selectedEstablishmentId, planningMode).then((data) => {
|
||||
setSchedules(data);
|
||||
if (data.length > 0) {
|
||||
setSelectedSchedule(data[0].id);
|
||||
}
|
||||
});
|
||||
}
|
||||
const addEvent = (newEvent) => {
|
||||
createEvent(newEvent).then((data) => {
|
||||
setEvents((prevEvents) => [...prevEvents, data]);
|
||||
createEvent(newEvent).then(() => {
|
||||
reloadEvents();
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateEvent = (id, updatedEvent) => {
|
||||
updateEvent(id, updatedEvent, csrfToken).then((data) => {
|
||||
setEvents((prevEvents) =>
|
||||
prevEvents.map((event) => (event.id === id ? updatedEvent : event))
|
||||
);
|
||||
reloadEvents();
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteEvent = (id) => {
|
||||
deleteEvent(id, csrfToken).then((data) => {
|
||||
setEvents((prevEvents) => prevEvents.filter((event) => event.id !== id));
|
||||
reloadEvents();
|
||||
});
|
||||
};
|
||||
|
||||
const addSchedule = (newSchedule) => {
|
||||
logger.debug('newSchedule', newSchedule);
|
||||
newSchedule.establishment = selectedEstablishmentId;
|
||||
createPlanning(newSchedule, csrfToken).then((data) => {
|
||||
setSchedules((prevSchedules) => [...prevSchedules, data]);
|
||||
createPlanning(newSchedule, csrfToken).then((_) => {
|
||||
reloadPlanning();
|
||||
});
|
||||
};
|
||||
|
||||
const updateSchedule = (id, updatedSchedule) => {
|
||||
updatePlanning(id, updatedSchedule, csrfToken).then((data) => {
|
||||
setSchedules((prevSchedules) =>
|
||||
prevSchedules.map((schedule) =>
|
||||
schedule.id === id ? updatedSchedule : schedule
|
||||
)
|
||||
);
|
||||
reloadPlanning();
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSchedule = (id) => {
|
||||
deletePlanning(id, csrfToken).then((data) => {
|
||||
setSchedules((prevSchedules) =>
|
||||
prevSchedules.filter((schedule) => schedule.id !== id)
|
||||
);
|
||||
reloadPlanning();
|
||||
reloadEvents();
|
||||
});
|
||||
};
|
||||
|
||||
@ -123,6 +136,9 @@ export function PlanningProvider({ children }) {
|
||||
setViewType,
|
||||
hiddenSchedules,
|
||||
toggleScheduleVisibility,
|
||||
planningMode,
|
||||
reloadEvents,
|
||||
reloadPlanning,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@ -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);
|
||||
Reference in New Issue
Block a user