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

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

View File

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

View File

@ -7,5 +7,5 @@ urlpatterns = [
re_path(r'^plannings/(?P<id>[0-9]+)$', PlanningWithIdView.as_view(), name="planning"),
re_path(r'^events$', EventsView.as_view(), name="events"),
re_path(r'^events/(?P<id>[0-9]+)$', EventsWithIdView.as_view(), name="events"),
re_path(r'^events/upcoming', UpcomingEventsView.as_view(), name="events"),
re_path(r'^events/upcoming', UpcomingEventsView.as_view(), name="events"),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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