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.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from School.models import SchoolClass
|
||||||
|
|
||||||
from Establishment.models import Establishment
|
from Establishment.models import Establishment
|
||||||
|
|
||||||
@ -14,25 +15,33 @@ class RecursionType(models.IntegerChoices):
|
|||||||
|
|
||||||
class Planning(models.Model):
|
class Planning(models.Model):
|
||||||
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT)
|
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT)
|
||||||
|
school_class = models.ForeignKey(
|
||||||
|
SchoolClass,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="planning",
|
||||||
|
null=True, # Permet des valeurs nulles
|
||||||
|
blank=True # Rend le champ facultatif dans les formulaires
|
||||||
|
)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(default="", blank=True, null=True)
|
description = models.TextField(default="", blank=True, null=True)
|
||||||
color= models.CharField(max_length=255, default="#000000")
|
color= models.CharField(max_length=255, default="#000000")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Planning for {self.user.username}'
|
return f'Planning {self.name}'
|
||||||
|
|
||||||
|
|
||||||
class Events(models.Model):
|
class Events(models.Model):
|
||||||
planning = models.ForeignKey(Planning, on_delete=models.PROTECT)
|
planning = models.ForeignKey(Planning, on_delete=models.CASCADE)
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
description = models.TextField()
|
description = models.TextField(default="", blank=True, null=True)
|
||||||
start = models.DateTimeField()
|
start = models.DateTimeField()
|
||||||
end = models.DateTimeField()
|
end = models.DateTimeField()
|
||||||
recursionType = models.IntegerField(choices=RecursionType, default=0)
|
recursionType = models.IntegerField(choices=RecursionType, default=0)
|
||||||
|
recursionEnd = models.DateTimeField(default=None, blank=True, null=True)
|
||||||
color= models.CharField(max_length=255)
|
color= models.CharField(max_length=255)
|
||||||
location = models.CharField(max_length=255, default="", blank=True, null=True)
|
location = models.CharField(max_length=255, default="", blank=True, null=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Event for {self.user.username}'
|
return f'Event {self.title}'
|
||||||
@ -7,5 +7,5 @@ urlpatterns = [
|
|||||||
re_path(r'^plannings/(?P<id>[0-9]+)$', PlanningWithIdView.as_view(), name="planning"),
|
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$', EventsView.as_view(), name="events"),
|
||||||
re_path(r'^events/(?P<id>[0-9]+)$', EventsWithIdView.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 django.http.response import JsonResponse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from .models import Planning, Events
|
from .models import Planning, Events, RecursionType
|
||||||
|
|
||||||
from .serializers import PlanningSerializer, EventsSerializer
|
from .serializers import PlanningSerializer, EventsSerializer
|
||||||
|
|
||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
|
|
||||||
|
|
||||||
class PlanningView(APIView):
|
class PlanningView(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
plannings=bdd.getAllObjects(Planning)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
planning_serializer=PlanningSerializer(plannings, many=True)
|
planning_mode = request.GET.get('planning_mode', None)
|
||||||
|
|
||||||
|
plannings = bdd.getAllObjects(Planning)
|
||||||
|
|
||||||
|
if establishment_id is not None:
|
||||||
|
plannings = plannings.filter(establishment=establishment_id)
|
||||||
|
|
||||||
|
# Filtrer en fonction du planning_mode
|
||||||
|
if planning_mode == "classSchedule":
|
||||||
|
plannings = plannings.filter(school_class__isnull=False)
|
||||||
|
elif planning_mode == "planning":
|
||||||
|
plannings = plannings.filter(school_class__isnull=True)
|
||||||
|
|
||||||
|
planning_serializer = PlanningSerializer(plannings.distinct(), many=True)
|
||||||
return JsonResponse(planning_serializer.data, safe=False)
|
return JsonResponse(planning_serializer.data, safe=False)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@ -56,17 +70,63 @@ class PlanningWithIdView(APIView):
|
|||||||
|
|
||||||
class EventsView(APIView):
|
class EventsView(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
events = bdd.getAllObjects(Events)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
|
planning_mode = request.GET.get('planning_mode', None)
|
||||||
|
filterParams = {}
|
||||||
|
plannings=[]
|
||||||
|
events = Events.objects.all()
|
||||||
|
if establishment_id is not None :
|
||||||
|
filterParams['establishment'] = establishment_id
|
||||||
|
if planning_mode is not None:
|
||||||
|
filterParams['school_class__isnull'] = (planning_mode!="classSchedule")
|
||||||
|
if filterParams:
|
||||||
|
plannings = Planning.objects.filter(**filterParams)
|
||||||
|
events = Events.objects.filter(planning__in=plannings)
|
||||||
events_serializer = EventsSerializer(events, many=True)
|
events_serializer = EventsSerializer(events, many=True)
|
||||||
return JsonResponse(events_serializer.data, safe=False)
|
return JsonResponse(events_serializer.data, safe=False)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
events_serializer = EventsSerializer(data=request.data)
|
events_serializer = EventsSerializer(data=request.data)
|
||||||
if events_serializer.is_valid():
|
if events_serializer.is_valid():
|
||||||
events_serializer.save()
|
event = events_serializer.save()
|
||||||
|
|
||||||
|
# Gérer les événements récurrents
|
||||||
|
if event.recursionType != RecursionType.RECURSION_NONE:
|
||||||
|
self.create_recurring_events(event)
|
||||||
|
|
||||||
return JsonResponse(events_serializer.data, status=201)
|
return JsonResponse(events_serializer.data, status=201)
|
||||||
return JsonResponse(events_serializer.errors, status=400)
|
return JsonResponse(events_serializer.errors, status=400)
|
||||||
|
|
||||||
|
def create_recurring_events(self, event):
|
||||||
|
current_start = event.start
|
||||||
|
current_end = event.end
|
||||||
|
|
||||||
|
while current_start < event.recursionEnd:
|
||||||
|
if event.recursionType == RecursionType.RECURSION_DAILY:
|
||||||
|
current_start += relativedelta(days=1)
|
||||||
|
current_end += relativedelta(days=1)
|
||||||
|
elif event.recursionType == RecursionType.RECURSION_WEEKLY:
|
||||||
|
current_start += relativedelta(weeks=1)
|
||||||
|
current_end += relativedelta(weeks=1)
|
||||||
|
elif event.recursionType == RecursionType.RECURSION_MONTHLY:
|
||||||
|
current_start += relativedelta(months=1)
|
||||||
|
current_end += relativedelta(months=1)
|
||||||
|
else:
|
||||||
|
break # Pour d'autres types de récurrence non gérés
|
||||||
|
|
||||||
|
# Créer une nouvelle occurrence
|
||||||
|
Events.objects.create(
|
||||||
|
planning=event.planning,
|
||||||
|
title=event.title,
|
||||||
|
description=event.description,
|
||||||
|
start=current_start,
|
||||||
|
end=current_end,
|
||||||
|
recursionEnd=event.recursionEnd,
|
||||||
|
recursionType=event.recursionType, # Les occurrences ne sont pas récurrentes
|
||||||
|
color=event.color,
|
||||||
|
location=event.location,
|
||||||
|
)
|
||||||
|
|
||||||
class EventsWithIdView(APIView):
|
class EventsWithIdView(APIView):
|
||||||
def put(self, request, id):
|
def put(self, request, id):
|
||||||
try:
|
try:
|
||||||
@ -92,6 +152,18 @@ class EventsWithIdView(APIView):
|
|||||||
class UpcomingEventsView(APIView):
|
class UpcomingEventsView(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
current_date = timezone.now()
|
current_date = timezone.now()
|
||||||
upcoming_events = Events.objects.filter(start__gte=current_date)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
|
|
||||||
|
if establishment_id is not None:
|
||||||
|
# Filtrer les plannings par establishment_id et sans school_class
|
||||||
|
plannings = Planning.objects.filter(establishment=establishment_id, school_class__isnull=True)
|
||||||
|
# Filtrer les événements associés à ces plannings et qui sont à venir
|
||||||
|
upcoming_events = Events.objects.filter(planning__in=plannings, start__gte=current_date)
|
||||||
|
else:
|
||||||
|
# Récupérer tous les événements à venir si aucun establishment_id n'est fourni
|
||||||
|
# et les plannings ne doivent pas être rattachés à une school_class
|
||||||
|
plannings = Planning.objects.filter(school_class__isnull=True)
|
||||||
|
upcoming_events = Events.objects.filter(planning__in=plannings, start__gte=current_date)
|
||||||
|
|
||||||
events_serializer = EventsSerializer(upcoming_events, many=True)
|
events_serializer = EventsSerializer(upcoming_events, many=True)
|
||||||
return JsonResponse(events_serializer.data, safe=False)
|
return JsonResponse(events_serializer.data, safe=False)
|
||||||
@ -5,24 +5,24 @@ from rest_framework.parsers import JSONParser
|
|||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from .models import (
|
from .models import (
|
||||||
Teacher,
|
Teacher,
|
||||||
Speciality,
|
Speciality,
|
||||||
SchoolClass,
|
SchoolClass,
|
||||||
Planning,
|
Planning,
|
||||||
Discount,
|
Discount,
|
||||||
Fee,
|
Fee,
|
||||||
PaymentPlan,
|
PaymentPlan,
|
||||||
PaymentMode,
|
PaymentMode,
|
||||||
AbsenceManagement
|
AbsenceManagement
|
||||||
)
|
)
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
TeacherSerializer,
|
TeacherSerializer,
|
||||||
SpecialitySerializer,
|
SpecialitySerializer,
|
||||||
SchoolClassSerializer,
|
SchoolClassSerializer,
|
||||||
PlanningSerializer,
|
PlanningSerializer,
|
||||||
DiscountSerializer,
|
DiscountSerializer,
|
||||||
FeeSerializer,
|
FeeSerializer,
|
||||||
PaymentPlanSerializer,
|
PaymentPlanSerializer,
|
||||||
PaymentModeSerializer,
|
PaymentModeSerializer,
|
||||||
AbsenceManagementSerializer
|
AbsenceManagementSerializer
|
||||||
)
|
)
|
||||||
@ -93,8 +93,8 @@ class TeacherListCreateView(APIView):
|
|||||||
|
|
||||||
if teacher_serializer.is_valid():
|
if teacher_serializer.is_valid():
|
||||||
teacher_serializer.save()
|
teacher_serializer.save()
|
||||||
|
|
||||||
return JsonResponse(teacher_serializer.data, safe=False)
|
return JsonResponse(teacher_serializer.data, safe=False)
|
||||||
|
|
||||||
return JsonResponse(teacher_serializer.errors, safe=False)
|
return JsonResponse(teacher_serializer.errors, safe=False)
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ class SchoolClassListCreateView(APIView):
|
|||||||
classe_serializer = SchoolClassSerializer(data=classe_data)
|
classe_serializer = SchoolClassSerializer(data=classe_data)
|
||||||
|
|
||||||
if classe_serializer.is_valid():
|
if classe_serializer.is_valid():
|
||||||
classe_serializer.save()
|
classe_serializer.save()
|
||||||
return JsonResponse(classe_serializer.data, safe=False)
|
return JsonResponse(classe_serializer.data, safe=False)
|
||||||
|
|
||||||
return JsonResponse(classe_serializer.errors, safe=False)
|
return JsonResponse(classe_serializer.errors, safe=False)
|
||||||
@ -195,7 +195,7 @@ class PlanningDetailView(APIView):
|
|||||||
|
|
||||||
def put(self, request, id):
|
def put(self, request, id):
|
||||||
planning_data = JSONParser().parse(request)
|
planning_data = JSONParser().parse(request)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
planning = Planning.objects.get(id=id)
|
planning = Planning.objects.get(id=id)
|
||||||
except Planning.DoesNotExist:
|
except Planning.DoesNotExist:
|
||||||
@ -210,7 +210,7 @@ class PlanningDetailView(APIView):
|
|||||||
return JsonResponse(planning_serializer.data, safe=False)
|
return JsonResponse(planning_serializer.data, safe=False)
|
||||||
|
|
||||||
return JsonResponse(planning_serializer.errors, safe=False)
|
return JsonResponse(planning_serializer.errors, safe=False)
|
||||||
|
|
||||||
def delete(self, request, id):
|
def delete(self, request, id):
|
||||||
return delete_object(Planning, 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()
|
fees = Fee.objects.filter(type=fee_type_value, establishment_id=establishment_id).distinct()
|
||||||
fee_serializer = FeeSerializer(fees, many=True)
|
fee_serializer = FeeSerializer(fees, many=True)
|
||||||
|
|
||||||
return JsonResponse(fee_serializer.data, safe=False, status=status.HTTP_200_OK)
|
return JsonResponse(fee_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def post(self, request):
|
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 = Discount.objects.filter(type=discount_type_value, establishment_id=establishment_id).distinct()
|
||||||
discounts_serializer = DiscountSerializer(discounts, many=True)
|
discounts_serializer = DiscountSerializer(discounts, many=True)
|
||||||
|
|
||||||
return JsonResponse(discounts_serializer.data, safe=False, status=status.HTTP_200_OK)
|
return JsonResponse(discounts_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def post(self, request):
|
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 = PaymentPlan.objects.filter(type=type_value, establishment_id=establishment_id).distinct()
|
||||||
payment_plans_serializer = PaymentPlanSerializer(payment_plans, many=True)
|
payment_plans_serializer = PaymentPlanSerializer(payment_plans, many=True)
|
||||||
|
|
||||||
return JsonResponse(payment_plans_serializer.data, safe=False, status=status.HTTP_200_OK)
|
return JsonResponse(payment_plans_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def post(self, request):
|
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 = PaymentMode.objects.filter(type=type_value, establishment_id=establishment_id).distinct()
|
||||||
payment_modes_serializer = PaymentModeSerializer(payment_modes, many=True)
|
payment_modes_serializer = PaymentModeSerializer(payment_modes, many=True)
|
||||||
|
|
||||||
return JsonResponse(payment_modes_serializer.data, safe=False, status=status.HTTP_200_OK)
|
return JsonResponse(payment_modes_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|||||||
@ -43,7 +43,6 @@ export default function Layout({ children }) {
|
|||||||
const { profileRole, establishments, user, clearContext } =
|
const { profileRole, establishments, user, clearContext } =
|
||||||
useEstablishment();
|
useEstablishment();
|
||||||
|
|
||||||
// Déplacer le reste du code ici...
|
|
||||||
const sidebarItems = {
|
const sidebarItems = {
|
||||||
admin: {
|
admin: {
|
||||||
id: 'admin',
|
id: 'admin',
|
||||||
@ -144,12 +143,40 @@ export default function Layout({ children }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute requiredRight={[RIGHTS.ADMIN, RIGHTS.TEACHER]}>
|
<ProtectedRoute requiredRight={[RIGHTS.ADMIN, RIGHTS.TEACHER]}>
|
||||||
<div className="flex min-h-screen bg-gray-50 relative">
|
|
||||||
{/* Retirer la condition !isLoading car on gère déjà le chargement au début */}
|
{/* Topbar */}
|
||||||
{/* Sidebar avec hauteur forcée */}
|
<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
|
<div
|
||||||
className={`md:block ${isSidebarOpen ? 'block' : 'hidden'} fixed md:relative inset-y-0 left-0 z-30 h-full`}
|
className={`absolute top-16 bottom-16 left-0 z-30 w-64 bg-white border-r border-gray-200 box-border ${
|
||||||
style={{ height: '100vh' }} // Force la hauteur à 100% de la hauteur de la vue
|
isSidebarOpen ? 'block' : 'hidden md:block'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
establishments={establishments}
|
establishments={establishments}
|
||||||
@ -159,7 +186,7 @@ export default function Layout({ children }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Overlay pour fermer la sidebar en cliquant à l'extérieur sur mobile */}
|
{/* Overlay for mobile */}
|
||||||
{isSidebarOpen && (
|
{isSidebarOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 z-20 md:hidden"
|
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">
|
{/* Main container */}
|
||||||
{/* Header responsive */}
|
<div className="absolute overflow-auto bg-gray-50 top-16 bottom-16 left-64 right-0 ">
|
||||||
<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">
|
{children}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Footer */}
|
||||||
|
|
||||||
|
<Footer
|
||||||
|
softwareName={softwareName}
|
||||||
|
softwareVersion={softwareVersion}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
<Popup
|
<Popup
|
||||||
visible={isPopupVisible}
|
visible={isPopupVisible}
|
||||||
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?"
|
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?"
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export default function DashboardPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fetch des événements à venir
|
// Fetch des événements à venir
|
||||||
fetchUpcomingEvents()
|
fetchUpcomingEvents(selectedEstablishmentId)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setUpcomingEvents(data);
|
setUpcomingEvents(data);
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { PlanningProvider } from '@/context/PlanningContext';
|
import { PlanningModes, PlanningProvider, RecurrenceType } from '@/context/PlanningContext';
|
||||||
import Calendar from '@/components/Calendar';
|
import Calendar from '@/components/Calendar/Calendar';
|
||||||
import EventModal from '@/components/EventModal';
|
import EventModal from '@/components/Calendar/EventModal';
|
||||||
import ScheduleNavigation from '@/components/ScheduleNavigation';
|
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
@ -14,13 +15,14 @@ export default function Page() {
|
|||||||
end: '',
|
end: '',
|
||||||
location: '',
|
location: '',
|
||||||
planning: '', // Enlever la valeur par défaut ici
|
planning: '', // Enlever la valeur par défaut ici
|
||||||
recurrence: 'none',
|
recursionType: RecurrenceType.NONE,
|
||||||
selectedDays: [],
|
selectedDays: [],
|
||||||
recurrenceEnd: '',
|
recursionEnd: '',
|
||||||
customInterval: 1,
|
customInterval: 1,
|
||||||
customUnit: 'days',
|
customUnit: 'days',
|
||||||
viewType: 'week', // Ajouter la vue semaine par défaut
|
viewType: 'week', // Ajouter la vue semaine par défaut
|
||||||
});
|
});
|
||||||
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
|
|
||||||
const initializeNewEvent = (date = new Date()) => {
|
const initializeNewEvent = (date = new Date()) => {
|
||||||
// S'assurer que date est un objet Date valide
|
// S'assurer que date est un objet Date valide
|
||||||
@ -33,9 +35,11 @@ export default function Page() {
|
|||||||
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
||||||
location: '',
|
location: '',
|
||||||
planning: '', // Ne pas définir de valeur par défaut ici non plus
|
planning: '', // Ne pas définir de valeur par défaut ici non plus
|
||||||
recurrence: 'none',
|
recursionType: RecurrenceType.NONE,
|
||||||
selectedDays: [],
|
selectedDays: [],
|
||||||
recurrenceEnd: '',
|
recursionEnd: new Date(
|
||||||
|
eventDate.getTime() + 2 * 60 * 60 * 1000
|
||||||
|
).toISOString(),
|
||||||
customInterval: 1,
|
customInterval: 1,
|
||||||
customUnit: 'days',
|
customUnit: 'days',
|
||||||
});
|
});
|
||||||
@ -43,7 +47,8 @@ export default function Page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlanningProvider>
|
<PlanningProvider establishmentId={selectedEstablishmentId} modeSet={PlanningModes.PLANNING}>
|
||||||
|
{/* <div className="flex h-full overflow-hidden"> */}
|
||||||
<div className="flex h-full overflow-hidden">
|
<div className="flex h-full overflow-hidden">
|
||||||
<ScheduleNavigation />
|
<ScheduleNavigation />
|
||||||
<Calendar
|
<Calendar
|
||||||
|
|||||||
@ -29,12 +29,12 @@ import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManag
|
|||||||
import { fetchRegistrationSchoolFileMasters } from '@/app/actions/registerFileGroupAction';
|
import { fetchRegistrationSchoolFileMasters } from '@/app/actions/registerFileGroupAction';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
|
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [specialities, setSpecialities] = useState([]);
|
const [specialities, setSpecialities] = useState([]);
|
||||||
const [classes, setClasses] = useState([]);
|
const [classes, setClasses] = useState([]);
|
||||||
const [teachers, setTeachers] = useState([]);
|
const [teachers, setTeachers] = useState([]);
|
||||||
const [schedules, setSchedules] = useState([]);
|
|
||||||
const [registrationDiscounts, setRegistrationDiscounts] = useState([]);
|
const [registrationDiscounts, setRegistrationDiscounts] = useState([]);
|
||||||
const [tuitionDiscounts, setTuitionDiscounts] = useState([]);
|
const [tuitionDiscounts, setTuitionDiscounts] = useState([]);
|
||||||
const [registrationFees, setRegistrationFees] = useState([]);
|
const [registrationFees, setRegistrationFees] = useState([]);
|
||||||
@ -60,8 +60,6 @@ export default function Page() {
|
|||||||
// Fetch data for classes
|
// Fetch data for classes
|
||||||
handleClasses();
|
handleClasses();
|
||||||
|
|
||||||
// Fetch data for schedules
|
|
||||||
handleSchedules();
|
|
||||||
|
|
||||||
// Fetch data for registration discounts
|
// Fetch data for registration discounts
|
||||||
handleRegistrationDiscounts();
|
handleRegistrationDiscounts();
|
||||||
@ -128,13 +126,6 @@ export default function Page() {
|
|||||||
.catch((error) => logger.error('Error fetching classes:', error));
|
.catch((error) => logger.error('Error fetching classes:', error));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSchedules = () => {
|
|
||||||
fetchSchedules()
|
|
||||||
.then((data) => {
|
|
||||||
setSchedules(data);
|
|
||||||
})
|
|
||||||
.catch((error) => logger.error('Error fetching schedules:', error));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRegistrationDiscounts = () => {
|
const handleRegistrationDiscounts = () => {
|
||||||
fetchRegistrationDiscounts(selectedEstablishmentId)
|
fetchRegistrationDiscounts(selectedEstablishmentId)
|
||||||
@ -299,6 +290,8 @@ export default function Page() {
|
|||||||
<ScheduleManagement
|
<ScheduleManagement
|
||||||
handleUpdatePlanning={handleUpdatePlanning}
|
handleUpdatePlanning={handleUpdatePlanning}
|
||||||
classes={classes}
|
classes={classes}
|
||||||
|
specialities={specialities}
|
||||||
|
teachers={teachers}
|
||||||
/>
|
/>
|
||||||
</ClassesProvider>
|
</ClassesProvider>
|
||||||
),
|
),
|
||||||
@ -343,12 +336,12 @@ export default function Page() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<>
|
||||||
|
<PlanningProvider establishmentId={selectedEstablishmentId} modeSet={PlanningModes.CLASS_SCHEDULE}>
|
||||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||||
|
<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 requestResponseHandler = async (response) => {
|
||||||
const body = response.status !== 204 ? await response?.json() : {};
|
const body = response.status !== 204 ? await response?.json() : {};
|
||||||
console.log(response);
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
// Throw an error with the JSON body containing the form errors
|
// Throw an error with the JSON body containing the form errors
|
||||||
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
|
const error = new Error(
|
||||||
|
body?.errorMessage ||
|
||||||
|
`Une erreur est survenue code de retour : ${response.status}`
|
||||||
|
);
|
||||||
error.details = body;
|
error.details = body;
|
||||||
throw error;
|
throw error;
|
||||||
};
|
};
|
||||||
@ -51,8 +53,15 @@ const removeDatas = (url, csrfToken) => {
|
|||||||
}).then(requestResponseHandler);
|
}).then(requestResponseHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchPlannings = () => {
|
export const fetchPlannings = (establishment_id=null,planningMode=null) => {
|
||||||
return getData(`${BE_PLANNING_PLANNINGS_URL}`);
|
let url = `${BE_PLANNING_PLANNINGS_URL}`;
|
||||||
|
if (establishment_id) {
|
||||||
|
url += `?establishment_id=${establishment_id}`;
|
||||||
|
}
|
||||||
|
if (planningMode) {
|
||||||
|
url += `&planning_mode=${planningMode}`;
|
||||||
|
}
|
||||||
|
return getData(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPlanning = (id) => {
|
export const getPlanning = (id) => {
|
||||||
@ -71,8 +80,17 @@ export const deletePlanning = (id, csrfToken) => {
|
|||||||
return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken);
|
return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchEvents = () => {
|
export const fetchEvents = (establishment_id=null, planningMode=null) => {
|
||||||
return getData(`${BE_PLANNING_EVENTS_URL}`);
|
let url = `${BE_PLANNING_EVENTS_URL}`;
|
||||||
|
if (establishment_id) {
|
||||||
|
url += `?establishment_id=${establishment_id}`;
|
||||||
|
}
|
||||||
|
if (planningMode) {
|
||||||
|
url += `&planning_mode=${planningMode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getData(url);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEvent = (id) => {
|
export const getEvent = (id) => {
|
||||||
@ -91,6 +109,10 @@ export const deleteEvent = (id, csrfToken) => {
|
|||||||
return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken);
|
return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchUpcomingEvents = () => {
|
export const fetchUpcomingEvents = (establishment_id=null) => {
|
||||||
return getData(`${BE_PLANNING_EVENTS_URL}/upcoming`);
|
let url = `${BE_PLANNING_EVENTS_URL}/upcoming`;
|
||||||
|
if (establishment_id) {
|
||||||
|
url += `?establishment_id=${establishment_id}`;
|
||||||
|
}
|
||||||
|
return getData(`${url}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export default async function RootLayout({ children, params }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale}>
|
<html lang={locale}>
|
||||||
<body>
|
<body className='p-0 m-0'>
|
||||||
<Providers messages={messages} locale={locale} session={params.session}>
|
<Providers messages={messages} locale={locale} session={params.session}>
|
||||||
{children}
|
{children}
|
||||||
</Providers>
|
</Providers>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { fr } from 'date-fns/locale';
|
|||||||
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
|
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
const Calendar = ({ onDateClick, onEventClick }) => {
|
const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||||
const {
|
const {
|
||||||
currentDate,
|
currentDate,
|
||||||
setCurrentDate,
|
setCurrentDate,
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { usePlanning } from '@/context/PlanningContext';
|
import { usePlanning, RecurrenceType } from '@/context/PlanningContext';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@ -13,23 +13,23 @@ export default function EventModal({
|
|||||||
|
|
||||||
// S'assurer que planning est défini lors du premier rendu
|
// S'assurer que planning est défini lors du premier rendu
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!eventData.planning && schedules.length > 0) {
|
if (!eventData?.planning && schedules.length > 0) {
|
||||||
setEventData((prev) => ({
|
setEventData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
planning: schedules[0].id,
|
planning: schedules[0].id,
|
||||||
color: schedules[0].color,
|
color: schedules[0].color,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [schedules, eventData.planning]);
|
}, [schedules, eventData?.planning]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const recurrenceOptions = [
|
const recurrenceOptions = [
|
||||||
{ value: 'none', label: 'Aucune' },
|
{ value: RecurrenceType.NONE, label: 'Aucune' },
|
||||||
{ value: 'daily', label: 'Quotidienne' },
|
{ value: RecurrenceType.DAILY, label: 'Quotidienne' },
|
||||||
{ value: 'weekly', label: 'Hebdomadaire' },
|
{ value: RecurrenceType.WEEKLY, label: 'Hebdomadaire' },
|
||||||
{ value: 'monthly', label: 'Mensuelle' },
|
{ value: RecurrenceType.MONTHLY, label: 'Mensuelle' },
|
||||||
{ value: 'custom', label: 'Personnalisée' }, // Nouvelle option
|
/* { value: RecurrenceType.CUSTOM, label: 'Personnalisée' }, */
|
||||||
];
|
];
|
||||||
|
|
||||||
const daysOfWeek = [
|
const daysOfWeek = [
|
||||||
@ -171,10 +171,13 @@ export default function EventModal({
|
|||||||
Récurrence
|
Récurrence
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={eventData.recurrence || 'none'}
|
value={eventData.recursionType || RecurrenceType.NONE}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setEventData({ ...eventData, recurrence: e.target.value })
|
return setEventData({
|
||||||
}
|
...eventData,
|
||||||
|
recursionType: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||||
>
|
>
|
||||||
{recurrenceOptions.map((option) => (
|
{recurrenceOptions.map((option) => (
|
||||||
@ -186,46 +189,7 @@ export default function EventModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Paramètres de récurrence personnalisée */}
|
{/* Paramètres de récurrence personnalisée */}
|
||||||
{eventData.recurrence === 'custom' && (
|
{eventData.recursionType == RecurrenceType.CUSTOM && (
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Répéter tous les
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={eventData.customInterval || 1}
|
|
||||||
onChange={(e) =>
|
|
||||||
setEventData({
|
|
||||||
...eventData,
|
|
||||||
customInterval: parseInt(e.target.value) || 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="w-20 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={eventData.customUnit || 'days'}
|
|
||||||
onChange={(e) =>
|
|
||||||
setEventData({
|
|
||||||
...eventData,
|
|
||||||
customUnit: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="flex-1 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
>
|
|
||||||
<option value="days">Jours</option>
|
|
||||||
<option value="weeks">Semaines</option>
|
|
||||||
<option value="months">Mois</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Jours de la semaine (pour récurrence hebdomadaire) */}
|
|
||||||
{eventData.recurrence === 'weekly' && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Jours de répétition
|
Jours de répétition
|
||||||
@ -256,16 +220,25 @@ export default function EventModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Date de fin de récurrence */}
|
{/* Date de fin de récurrence */}
|
||||||
{eventData.recurrence !== 'none' && (
|
{eventData.recursionType != RecurrenceType.NONE && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Fin de récurrence
|
Fin de récurrence
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={eventData.recurrenceEnd || ''}
|
value={
|
||||||
|
eventData.recursionEnd
|
||||||
|
? format(new Date(eventData.recursionEnd), 'yyyy-MM-dd')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEventData({ ...eventData, recurrenceEnd: e.target.value })
|
setEventData({
|
||||||
|
...eventData,
|
||||||
|
recursionEnd: e.target.value
|
||||||
|
? new Date(e.target.value).toISOString()
|
||||||
|
: null,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||||
/>
|
/>
|
||||||
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 React, { useEffect, useState, useRef } from 'react';
|
||||||
import { usePlanning } from '@/context/PlanningContext';
|
import { usePlanning } from '@/context/PlanningContext';
|
||||||
import {
|
import { format, startOfWeek, addDays, isSameDay } from 'date-fns';
|
||||||
format,
|
|
||||||
startOfWeek,
|
|
||||||
addDays,
|
|
||||||
differenceInMinutes,
|
|
||||||
isSameDay,
|
|
||||||
} from 'date-fns';
|
|
||||||
import { fr } from 'date-fns/locale';
|
import { fr } from 'date-fns/locale';
|
||||||
import { getWeekEvents } from '@/utils/events';
|
import { getWeekEvents } from '@/utils/events';
|
||||||
import { isToday } from 'date-fns';
|
import { isToday } from 'date-fns';
|
||||||
@ -49,7 +43,8 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
const getCurrentTimePosition = () => {
|
const getCurrentTimePosition = () => {
|
||||||
const hours = currentTime.getHours();
|
const hours = currentTime.getHours();
|
||||||
const minutes = currentTime.getMinutes();
|
const minutes = currentTime.getMinutes();
|
||||||
return `${(hours + minutes / 60) * 5}rem`;
|
const rowHeight = 5; // Hauteur des lignes en rem (h-20 = 5rem)
|
||||||
|
return `${((hours + minutes / 60) * rowHeight)}rem`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utiliser les événements déjà filtrés passés en props
|
// Utiliser les événements déjà filtrés passés en props
|
||||||
@ -144,17 +139,17 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full overflow-hidden">
|
<div className="flex flex-col h-full overflow-y-auto">
|
||||||
{/* En-tête des jours */}
|
{/* En-tête des jours */}
|
||||||
<div
|
<div
|
||||||
className="grid gap-[1px] bg-gray-100 pr-[17px]"
|
className="grid gap-[1px] w-full bg-gray-100"
|
||||||
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
|
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
|
||||||
>
|
>
|
||||||
<div className="bg-white h-14"></div>
|
<div className="bg-white h-14"></div>
|
||||||
{weekDays.map((day) => (
|
{weekDays.map((day) => (
|
||||||
<div
|
<div
|
||||||
key={day}
|
key={day}
|
||||||
className={`p-2 text-center border-b
|
className={`h-14 p-2 text-center border-b
|
||||||
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
||||||
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
|
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
|
||||||
>
|
>
|
||||||
@ -172,7 +167,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grille horaire */}
|
{/* Grille horaire */}
|
||||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
|
<div ref={scrollContainerRef} className="flex-1 relative">
|
||||||
{/* Ligne de temps actuelle */}
|
{/* Ligne de temps actuelle */}
|
||||||
{isCurrentWeek && (
|
{isCurrentWeek && (
|
||||||
<div
|
<div
|
||||||
@ -181,12 +176,12 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
top: getCurrentTimePosition(),
|
top: getCurrentTimePosition(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500" />
|
<div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="grid gap-[1px] bg-gray-100"
|
className="grid gap-[1px] w-full bg-gray-100"
|
||||||
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
|
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
|
||||||
>
|
>
|
||||||
{timeSlots.map((hour) => (
|
{timeSlots.map((hour) => (
|
||||||
@ -209,9 +204,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
onDateClick(date);
|
onDateClick(date);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1">
|
<div className="grid gap-1">
|
||||||
{' '}
|
|
||||||
{/* Ajout de gap-1 */}
|
|
||||||
{dayEvents
|
{dayEvents
|
||||||
.filter((event) => {
|
.filter((event) => {
|
||||||
const eventStart = new Date(event.start);
|
const eventStart = new Date(event.start);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import Logo from '@/components/Logo';
|
|||||||
|
|
||||||
export default function Footer({ softwareName, softwareVersion }) {
|
export default function Footer({ softwareName, softwareVersion }) {
|
||||||
return (
|
return (
|
||||||
<footer className="h-16 bg-white border-t border-gray-200 px-8 py-4 flex items-center justify-between">
|
<footer className="absolute bottom-0 left-0 right-0 h-16 bg-white border-t border-gray-200 flex items-center justify-center box-border">
|
||||||
<div className="text-sm font-light">
|
<div className="text-sm font-light">
|
||||||
<span>
|
<span>
|
||||||
© {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.
|
© {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);
|
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<>
|
||||||
<div className="flex border-b-2 border-gray-200">
|
<div className="flex h-14 border-b-2 border-gray-200">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@ -20,17 +20,15 @@ const SidebarTabs = ({ tabs }) => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={`${activeTab === tab.id ? 'block' : 'hidden'}`}
|
className={`${activeTab === tab.id ? 'block h-[calc(100%-3.5rem)]' : 'hidden'}`}
|
||||||
>
|
>
|
||||||
{tab.content}
|
{tab.content}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,10 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
|||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL } from '@/utils/Url';
|
import { FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL } from '@/utils/Url';
|
||||||
|
import { usePlanning } from '@/context/PlanningContext';
|
||||||
|
import { useClasses } from '@/context/ClassesContext';
|
||||||
|
|
||||||
|
|
||||||
const ItemTypes = {
|
const ItemTypes = {
|
||||||
TEACHER: 'teacher',
|
TEACHER: 'teacher',
|
||||||
@ -117,6 +119,7 @@ const ClassesSection = ({
|
|||||||
handleCreate,
|
handleCreate,
|
||||||
handleEdit,
|
handleEdit,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
|
|
||||||
}) => {
|
}) => {
|
||||||
const [formData, setFormData] = useState({});
|
const [formData, setFormData] = useState({});
|
||||||
const [editingClass, setEditingClass] = useState(null);
|
const [editingClass, setEditingClass] = useState(null);
|
||||||
@ -129,41 +132,10 @@ const ClassesSection = ({
|
|||||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||||
const [detailsModalVisible, setDetailsModalVisible] = useState(false);
|
const [detailsModalVisible, setDetailsModalVisible] = useState(false);
|
||||||
const [selectedClass, setSelectedClass] = useState(null);
|
const [selectedClass, setSelectedClass] = useState(null);
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
|
const { addSchedule, reloadPlanning, reloadEvents } = usePlanning();
|
||||||
|
const{ getNiveauxLabels, allNiveaux } = useClasses();
|
||||||
|
|
||||||
const niveauxPremierCycle = [
|
|
||||||
{ id: 1, name: 'TPS', age: 2 },
|
|
||||||
{ id: 2, name: 'PS', age: 3 },
|
|
||||||
{ id: 3, name: 'MS', age: 4 },
|
|
||||||
{ id: 4, name: 'GS', age: 5 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const niveauxSecondCycle = [
|
|
||||||
{ id: 5, name: 'CP', age: 6 },
|
|
||||||
{ id: 6, name: 'CE1', age: 7 },
|
|
||||||
{ id: 7, name: 'CE2', age: 8 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const niveauxTroisiemeCycle = [
|
|
||||||
{ id: 8, name: 'CM1', age: 9 },
|
|
||||||
{ id: 9, name: 'CM2', age: 10 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const allNiveaux = [
|
|
||||||
...niveauxPremierCycle,
|
|
||||||
...niveauxSecondCycle,
|
|
||||||
...niveauxTroisiemeCycle,
|
|
||||||
];
|
|
||||||
|
|
||||||
const getNiveauxLabels = (levels) => {
|
|
||||||
return levels.map((niveauId) => {
|
|
||||||
const niveau = allNiveaux.find((n) => n.id === niveauId);
|
|
||||||
return niveau ? niveau.name : niveauId;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fonction pour générer les années scolaires
|
// Fonction pour générer les années scolaires
|
||||||
const getSchoolYearChoices = () => {
|
const getSchoolYearChoices = () => {
|
||||||
@ -241,6 +213,19 @@ const ClassesSection = ({
|
|||||||
setClasses((prevClasses) => [createdClass, ...classes]);
|
setClasses((prevClasses) => [createdClass, ...classes]);
|
||||||
setNewClass(null);
|
setNewClass(null);
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
|
// Creation des plannings associé à la classe
|
||||||
|
|
||||||
|
createdClass.levels.forEach((level) => {
|
||||||
|
const levelName = allNiveaux.find((lvl) => lvl.id === level)?.name;
|
||||||
|
const planningName = `${createdClass.atmosphere_name} - ${levelName}`;
|
||||||
|
const newPlanning = {
|
||||||
|
name: planningName,
|
||||||
|
color: '#FF5733', // Couleur par défaut
|
||||||
|
school_class: createdClass.id,
|
||||||
|
}
|
||||||
|
addSchedule(newPlanning)
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('Error:', error.message);
|
logger.error('Error:', error.message);
|
||||||
@ -505,6 +490,8 @@ const ClassesSection = ({
|
|||||||
);
|
);
|
||||||
setPopupVisible(true);
|
setPopupVisible(true);
|
||||||
setRemovePopupVisible(false);
|
setRemovePopupVisible(false);
|
||||||
|
reloadPlanning();
|
||||||
|
reloadEvents();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('Error archiving data:', error);
|
logger.error('Error archiving data:', error);
|
||||||
|
|||||||
@ -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,
|
handleDelete,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full mx-auto mt-6">
|
<div className="w-full p-4 mx-auto mt-6">
|
||||||
<ClassesProvider>
|
<ClassesProvider>
|
||||||
<div className="mt-8 w-2/5">
|
<div className="mt-8 w-2/5">
|
||||||
<SpecialitiesSection
|
<SpecialitiesSection
|
||||||
|
|||||||
@ -462,7 +462,7 @@ export default function FilesGroupsManagement({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full mx-auto mt-6">
|
<div className="w-full p-4 mx-auto mt-6">
|
||||||
{/* Modal pour les fichiers */}
|
{/* Modal pour les fichiers */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
|
|||||||
@ -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';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
||||||
import { DndProvider } from 'react-dnd';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import PlanningClassView from '@/components/Structure/Planning/PlanningClassView';
|
|
||||||
import SpecialitiesList from '@/components/Structure/Planning/SpecialitiesList';
|
|
||||||
import { BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url';
|
|
||||||
import { useClasses } from '@/context/ClassesContext';
|
|
||||||
import { ClasseFormProvider } from '@/context/ClasseFormContext';
|
|
||||||
import TabsStructure from '@/components/Structure/Configuration/TabsStructure';
|
|
||||||
import { Bookmark, Users, BookOpen, Newspaper } from 'lucide-react';
|
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
import { RecurrenceType } from '@/context/PlanningContext';
|
||||||
|
import Calendar from '@/components/Calendar/Calendar';
|
||||||
|
import ScheduleEventModal from '@/components/Structure/Planning/ScheduleEventModal';
|
||||||
|
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
|
||||||
|
|
||||||
const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
const currentMonth = new Date().getMonth();
|
|
||||||
const currentSchoolYearStart =
|
|
||||||
currentMonth >= 8 ? currentYear : currentYear - 1;
|
|
||||||
|
|
||||||
const [selectedClass, setSelectedClass] = useState(null);
|
|
||||||
const [selectedLevel, setSelectedLevel] = useState('');
|
|
||||||
const [schedule, setSchedule] = useState(null);
|
|
||||||
|
|
||||||
const { getNiveauxTabs } = useClasses();
|
|
||||||
const niveauxLabels = Array.isArray(selectedClass?.levels)
|
|
||||||
? getNiveauxTabs(selectedClass.levels)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
|
export default function ScheduleManagement({classes,specialities,teachers}) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const handleOpenModal = () => setIsModalOpen(true);
|
const [eventData, setEventData] = useState({
|
||||||
const handleCloseModal = () => setIsModalOpen(false);
|
title: '',
|
||||||
|
description: '',
|
||||||
|
start: '',
|
||||||
|
end: '',
|
||||||
|
location: '',
|
||||||
|
planning: '', // Enlever la valeur par défaut ici
|
||||||
|
recursionType: RecurrenceType.NONE,
|
||||||
|
selectedDays: [],
|
||||||
|
recursionEnd: '',
|
||||||
|
customInterval: 1,
|
||||||
|
customUnit: 'days',
|
||||||
|
viewType: 'week', // Ajouter la vue semaine par défaut
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const initializeNewEvent = (date = new Date()) => {
|
||||||
if (selectedClass) {
|
// S'assurer que date est un objet Date valide
|
||||||
const defaultLevel = niveauxLabels.length > 0 ? niveauxLabels[0].id : '';
|
const eventDate = date instanceof Date ? date : new Date();
|
||||||
const niveau = selectedLevel || defaultLevel;
|
|
||||||
|
|
||||||
setSelectedLevel(niveau);
|
setEventData({
|
||||||
|
title: '',
|
||||||
const currentPlanning = selectedClass.plannings_read?.find(
|
description: '',
|
||||||
(planning) => planning.niveau === niveau
|
start: eventDate.toISOString(),
|
||||||
);
|
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
||||||
setSchedule(currentPlanning ? currentPlanning.planning : {});
|
location: '',
|
||||||
}
|
planning: '', // Ne pas définir de valeur par défaut ici non plus
|
||||||
}, [selectedClass, niveauxLabels]);
|
recursionType: RecurrenceType.NONE,
|
||||||
|
selectedDays: [],
|
||||||
useEffect(() => {
|
recursionEnd: new Date(
|
||||||
if (selectedClass && selectedLevel) {
|
eventDate.getTime() + 2 * 60 * 60 * 1000
|
||||||
const currentPlanning = selectedClass.plannings_read?.find(
|
).toISOString(),
|
||||||
(planning) => planning.niveau === selectedLevel
|
customInterval: 1,
|
||||||
);
|
customUnit: 'days',
|
||||||
setSchedule(currentPlanning ? currentPlanning.planning : {});
|
});
|
||||||
}
|
setIsModalOpen(true);
|
||||||
}, [selectedClass, selectedLevel]);
|
|
||||||
|
|
||||||
const handleLevelSelect = (niveau) => {
|
|
||||||
setSelectedLevel(niveau);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClassSelect = (classId) => {
|
|
||||||
const selectedClasse = categorizedClasses['Actives'].find(
|
|
||||||
(classe) => classe.id === classId
|
|
||||||
);
|
|
||||||
setSelectedClass(selectedClasse);
|
|
||||||
setSelectedLevel('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDrop = (item, hour, day) => {
|
|
||||||
const { id, name, color, teachers } = item;
|
|
||||||
const newSchedule = {
|
|
||||||
...schedule,
|
|
||||||
emploiDuTemps: schedule.emploiDuTemps || {},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!newSchedule.emploiDuTemps[day]) {
|
|
||||||
newSchedule.emploiDuTemps[day] = [];
|
|
||||||
}
|
|
||||||
const courseTime = `${hour.toString().padStart(2, '0')}:00`;
|
|
||||||
|
|
||||||
const existingCourseIndex = newSchedule.emploiDuTemps[day].findIndex(
|
|
||||||
(course) => course.heure === courseTime
|
|
||||||
);
|
|
||||||
|
|
||||||
const newCourse = {
|
|
||||||
duree: '1',
|
|
||||||
heure: courseTime,
|
|
||||||
matiere: name,
|
|
||||||
teachers: teachers,
|
|
||||||
color: color,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (existingCourseIndex !== -1) {
|
|
||||||
newSchedule.emploiDuTemps[day][existingCourseIndex] = newCourse;
|
|
||||||
} else {
|
|
||||||
newSchedule.emploiDuTemps[day].push(newCourse);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour scheduleRef
|
|
||||||
setSchedule(newSchedule);
|
|
||||||
|
|
||||||
// Utiliser `handleUpdatePlanning` pour mettre à jour le planning du niveau de la classe
|
|
||||||
const planningId = selectedClass.plannings_read.find(
|
|
||||||
(planning) => planning.niveau === selectedLevel
|
|
||||||
)?.planning.id;
|
|
||||||
if (planningId) {
|
|
||||||
logger.debug('newSchedule : ', newSchedule);
|
|
||||||
handleUpdatePlanning(BE_SCHOOL_PLANNINGS_URL, planningId, newSchedule);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const categorizedClasses = classes.reduce((acc, classe) => {
|
|
||||||
const { school_year } = classe;
|
|
||||||
const [startYear] = school_year.split('-').map(Number);
|
|
||||||
const category =
|
|
||||||
startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes';
|
|
||||||
|
|
||||||
if (!acc[category]) {
|
|
||||||
acc[category] = [];
|
|
||||||
}
|
|
||||||
acc[category].push(classe);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex h-full overflow-hidden">
|
||||||
<DndProvider backend={HTML5Backend}>
|
<ScheduleNavigation classes={classes} />
|
||||||
<div className="p-4 bg-gray-100 border-b">
|
<Calendar
|
||||||
<div className="grid grid-cols-3 gap-4">
|
onDateClick={initializeNewEvent}
|
||||||
{/* Colonne Classes */}
|
onEventClick={(event) => {
|
||||||
<div className="p-4 bg-gray-50 rounded-lg shadow-inner">
|
setEventData(event);
|
||||||
<div className="flex justify-between items-center mb-4">
|
setIsModalOpen(true);
|
||||||
<h2 className="text-3xl text-gray-800 flex items-center">
|
}}
|
||||||
<Users className="w-8 h-8 mr-2" />
|
/>
|
||||||
Classes
|
<ScheduleEventModal
|
||||||
</h2>
|
isOpen={isModalOpen}
|
||||||
</div>
|
onClose={() => setIsModalOpen(false)}
|
||||||
{categorizedClasses['Actives'] && (
|
eventData={eventData}
|
||||||
<TabsStructure
|
setEventData={setEventData}
|
||||||
activeTab={selectedClass?.id}
|
specialities={specialities}
|
||||||
setActiveTab={handleClassSelect}
|
teachers={teachers}
|
||||||
tabs={categorizedClasses['Actives'].map((classe) => ({
|
classes={classes}
|
||||||
id: classe.id,
|
/>
|
||||||
title: classe.atmosphere_name,
|
</div>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="w-full mx-auto mt-6">
|
<div className="w-full p-4 mx-auto mt-6">
|
||||||
<div className="w-4/5 mx-auto flex items-center mt-8">
|
<div className="w-4/5 mx-auto flex items-center mt-8">
|
||||||
<hr className="flex-grow border-t-2 border-gray-300" />
|
<hr className="flex-grow border-t-2 border-gray-300" />
|
||||||
<span className="mx-4 text-gray-600 font-semibold">
|
<span className="mx-4 text-gray-600 font-semibold">
|
||||||
|
|||||||
@ -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,
|
...niveauxTroisiemeCycle,
|
||||||
];
|
];
|
||||||
|
|
||||||
const typeEmploiDuTemps = [
|
|
||||||
{ id: 1, label: 'Annuel' },
|
|
||||||
{ id: 2, label: 'Semestriel' },
|
|
||||||
{ id: 3, label: 'Trimestriel' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectedDays = {
|
|
||||||
1: 'lundi',
|
|
||||||
2: 'mardi',
|
|
||||||
3: 'mercredi',
|
|
||||||
4: 'jeudi',
|
|
||||||
5: 'vendredi',
|
|
||||||
6: 'samedi',
|
|
||||||
7: 'dimanche',
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNiveauxLabels = (levels) => {
|
const getNiveauxLabels = (levels) => {
|
||||||
return levels.map((niveauId) => {
|
return levels.map((niveauId) => {
|
||||||
const niveau = allNiveaux.find((n) => n.id === niveauId);
|
const niveau = allNiveaux.find((n) => n.id === niveauId);
|
||||||
@ -132,71 +116,6 @@ export const ClassesProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePlannings = (formData, existingPlannings) => {
|
|
||||||
return formData.levels.map((niveau) => {
|
|
||||||
let existingPlanning = existingPlannings.find(
|
|
||||||
(planning) => planning.niveau === niveau
|
|
||||||
);
|
|
||||||
|
|
||||||
const emploiDuTemps = formData.opening_days.reduce((acc, dayId) => {
|
|
||||||
const dayName = selectedDays[dayId];
|
|
||||||
if (dayName) {
|
|
||||||
acc[dayName] = existingPlanning?.emploiDuTemps?.[dayName] || [];
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
let updatedPlanning;
|
|
||||||
if (formData.type === 1) {
|
|
||||||
updatedPlanning = {
|
|
||||||
niveau: niveau,
|
|
||||||
emploiDuTemps,
|
|
||||||
};
|
|
||||||
} else if (formData.type === 2) {
|
|
||||||
updatedPlanning = {
|
|
||||||
niveau: niveau,
|
|
||||||
emploiDuTemps: {
|
|
||||||
S1: {
|
|
||||||
DateDebut: formData.date_debut_semestre_1,
|
|
||||||
DateFin: formData.date_fin_semestre_1,
|
|
||||||
...emploiDuTemps,
|
|
||||||
},
|
|
||||||
S2: {
|
|
||||||
DateDebut: formData.date_debut_semestre_2,
|
|
||||||
DateFin: formData.date_fin_semestre_2,
|
|
||||||
...emploiDuTemps,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else if (formData.type === 3) {
|
|
||||||
updatedPlanning = {
|
|
||||||
niveau: niveau,
|
|
||||||
emploiDuTemps: {
|
|
||||||
T1: {
|
|
||||||
DateDebut: formData.date_debut_trimestre_1,
|
|
||||||
DateFin: formData.date_fin_trimestre_1,
|
|
||||||
...emploiDuTemps,
|
|
||||||
},
|
|
||||||
T2: {
|
|
||||||
DateDebut: formData.date_debut_trimestre_2,
|
|
||||||
DateFin: formData.date_fin_trimestre_2,
|
|
||||||
...emploiDuTemps,
|
|
||||||
},
|
|
||||||
T3: {
|
|
||||||
DateDebut: formData.date_debut_trimestre_3,
|
|
||||||
DateFin: formData.date_fin_trimestre_3,
|
|
||||||
...emploiDuTemps,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fusionner les plannings existants avec les nouvelles données
|
|
||||||
return existingPlanning
|
|
||||||
? { ...existingPlanning, ...updatedPlanning }
|
|
||||||
: updatedPlanning;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupSpecialitiesBySubject = (teachers) => {
|
const groupSpecialitiesBySubject = (teachers) => {
|
||||||
const groupedSpecialities = {};
|
const groupedSpecialities = {};
|
||||||
@ -223,14 +142,6 @@ export const ClassesProvider = ({ children }) => {
|
|||||||
return Object.values(groupedSpecialities);
|
return Object.values(groupedSpecialities);
|
||||||
};
|
};
|
||||||
|
|
||||||
const determineInitialPeriod = (emploiDuTemps) => {
|
|
||||||
if (emploiDuTemps.S1 && emploiDuTemps.S2) {
|
|
||||||
return 'S1'; // Planning semestriel
|
|
||||||
} else if (emploiDuTemps.T1 && emploiDuTemps.T2 && emploiDuTemps.T3) {
|
|
||||||
return 'T1'; // Planning trimestriel
|
|
||||||
}
|
|
||||||
return ''; // Planning annuel ou autre
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClassesContext.Provider
|
<ClassesContext.Provider
|
||||||
@ -243,13 +154,11 @@ export const ClassesProvider = ({ children }) => {
|
|||||||
niveauxPremierCycle,
|
niveauxPremierCycle,
|
||||||
niveauxSecondCycle,
|
niveauxSecondCycle,
|
||||||
niveauxTroisiemeCycle,
|
niveauxTroisiemeCycle,
|
||||||
typeEmploiDuTemps,
|
allNiveaux,
|
||||||
updatePlannings,
|
|
||||||
getAmbianceText,
|
getAmbianceText,
|
||||||
getAmbianceName,
|
getAmbianceName,
|
||||||
groupSpecialitiesBySubject,
|
groupSpecialitiesBySubject,
|
||||||
getNiveauNameById,
|
getNiveauNameById,
|
||||||
determineInitialPeriod,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -23,13 +23,25 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
|||||||
*/
|
*/
|
||||||
const PlanningContext = createContext();
|
const PlanningContext = createContext();
|
||||||
|
|
||||||
export function PlanningProvider({ children }) {
|
export const RecurrenceType = Object.freeze({
|
||||||
// const [events, setEvents] = useState([]);
|
NONE: 0,
|
||||||
// const [schedules, setSchedules] = useState([]);
|
DAILY: 1,
|
||||||
// const [selectedSchedule, setSelectedSchedule] = useState(null);
|
WEEKLY: 2,
|
||||||
|
MONTHLY: 3,
|
||||||
|
CUSTOM: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PlanningModes = Object.freeze({
|
||||||
|
CLASS_SCHEDULE: 'classSchedule',
|
||||||
|
PLANNING: 'planning'
|
||||||
|
});
|
||||||
|
|
||||||
|
export function PlanningProvider({ children, modeSet=PlanningModes.PLANNING}) {
|
||||||
|
|
||||||
const [events, setEvents] = useState([]);
|
const [events, setEvents] = useState([]);
|
||||||
const [schedules, setSchedules] = useState([]);
|
const [schedules, setSchedules] = useState([]);
|
||||||
const [selectedSchedule, setSelectedSchedule] = useState(0);
|
const [selectedSchedule, setSelectedSchedule] = useState(0);
|
||||||
|
const [planningMode, setPlanningMode] = useState(modeSet);
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week'
|
const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week'
|
||||||
const [hiddenSchedules, setHiddenSchedules] = useState([]);
|
const [hiddenSchedules, setHiddenSchedules] = useState([]);
|
||||||
@ -37,60 +49,61 @@ export function PlanningProvider({ children }) {
|
|||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPlannings().then((data) => {
|
reloadPlanning();
|
||||||
setSchedules(data);
|
reloadEvents();
|
||||||
if (data.length > 0) {
|
}, [planningMode, selectedEstablishmentId]);
|
||||||
setSelectedSchedule(data[0].id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fetchEvents().then((data) => {
|
|
||||||
setEvents(data);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
|
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) => {
|
const addEvent = (newEvent) => {
|
||||||
createEvent(newEvent).then((data) => {
|
createEvent(newEvent).then(() => {
|
||||||
setEvents((prevEvents) => [...prevEvents, data]);
|
reloadEvents();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateEvent = (id, updatedEvent) => {
|
const handleUpdateEvent = (id, updatedEvent) => {
|
||||||
updateEvent(id, updatedEvent, csrfToken).then((data) => {
|
updateEvent(id, updatedEvent, csrfToken).then((data) => {
|
||||||
setEvents((prevEvents) =>
|
reloadEvents();
|
||||||
prevEvents.map((event) => (event.id === id ? updatedEvent : event))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteEvent = (id) => {
|
const handleDeleteEvent = (id) => {
|
||||||
deleteEvent(id, csrfToken).then((data) => {
|
deleteEvent(id, csrfToken).then((data) => {
|
||||||
setEvents((prevEvents) => prevEvents.filter((event) => event.id !== id));
|
reloadEvents();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSchedule = (newSchedule) => {
|
const addSchedule = (newSchedule) => {
|
||||||
logger.debug('newSchedule', newSchedule);
|
logger.debug('newSchedule', newSchedule);
|
||||||
newSchedule.establishment = selectedEstablishmentId;
|
newSchedule.establishment = selectedEstablishmentId;
|
||||||
createPlanning(newSchedule, csrfToken).then((data) => {
|
createPlanning(newSchedule, csrfToken).then((_) => {
|
||||||
setSchedules((prevSchedules) => [...prevSchedules, data]);
|
reloadPlanning();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSchedule = (id, updatedSchedule) => {
|
const updateSchedule = (id, updatedSchedule) => {
|
||||||
updatePlanning(id, updatedSchedule, csrfToken).then((data) => {
|
updatePlanning(id, updatedSchedule, csrfToken).then((data) => {
|
||||||
setSchedules((prevSchedules) =>
|
reloadPlanning();
|
||||||
prevSchedules.map((schedule) =>
|
|
||||||
schedule.id === id ? updatedSchedule : schedule
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSchedule = (id) => {
|
const deleteSchedule = (id) => {
|
||||||
deletePlanning(id, csrfToken).then((data) => {
|
deletePlanning(id, csrfToken).then((data) => {
|
||||||
setSchedules((prevSchedules) =>
|
reloadPlanning();
|
||||||
prevSchedules.filter((schedule) => schedule.id !== id)
|
reloadEvents();
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -123,6 +136,9 @@ export function PlanningProvider({ children }) {
|
|||||||
setViewType,
|
setViewType,
|
||||||
hiddenSchedules,
|
hiddenSchedules,
|
||||||
toggleScheduleVisibility,
|
toggleScheduleVisibility,
|
||||||
|
planningMode,
|
||||||
|
reloadEvents,
|
||||||
|
reloadPlanning,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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