feat: planning events

This commit is contained in:
Luc SORIGNET
2025-03-02 15:35:56 +01:00
parent e3879f516b
commit c9b0f0d77a
21 changed files with 371 additions and 172 deletions

View File

@ -43,6 +43,7 @@ INSTALLED_APPS = [
'GestionMessagerie.apps.GestionMessagerieConfig',
'GestionNotification.apps.GestionNotificationConfig',
'School.apps.SchoolConfig',
'Planning.apps.PlanningConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',

View File

@ -45,6 +45,7 @@ urlpatterns = [
path("GestionNotification/", include(("GestionNotification.urls", 'GestionNotification'), namespace='GestionNotification')),
path("School/", include(("School.urls", 'School'), namespace='School')),
path("DocuSeal/", include(("DocuSeal.urls", 'DocuSeal'), namespace='DocuSeal')),
path("Planning/", include(("Planning.urls", 'Planning'), namespace='Planning')),
# Documentation Api
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),

View File

@ -0,0 +1 @@
default_app_config = 'Planning.apps.PlanningConfig'

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class PlanningConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'Planning'

View File

@ -0,0 +1,38 @@
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 Establishment
class RecursionType(models.IntegerChoices):
RECURSION_NONE = 0, _('Aucune')
RECURSION_DAILY = 1, _('Quotidienne')
RECURSION_WEEKLY = 2, _('Hebdomadaire')
RECURSION_MONTHLY = 3, _('Mensuel')
RECURSION_CUSTOM = 4, _('Personnalisé')
class Planning(models.Model):
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT)
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}'
class Events(models.Model):
planning = models.ForeignKey(Planning, on_delete=models.PROTECT)
title = models.CharField(max_length=255)
description = models.TextField()
start = models.DateTimeField()
end = models.DateTimeField()
recursionType = models.IntegerField(choices=RecursionType, default=0)
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}'

View File

@ -0,0 +1,13 @@
from rest_framework import serializers
from .models import Planning, Events
class PlanningSerializer(serializers.ModelSerializer):
class Meta:
model = Planning
fields = '__all__'
class EventsSerializer(serializers.ModelSerializer):
class Meta:
model = Events
fields = '__all__'

10
Back-End/Planning/urls.py Normal file
View File

@ -0,0 +1,10 @@
from django.urls import path, re_path
from Planning.views import PlanningView,PlanningWithIdView,EventsView,EventsWithIdView
urlpatterns = [
re_path(r'^plannings$', PlanningView.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/(?P<id>[0-9]+)$', EventsWithIdView.as_view(), name="events"),
]

View File

@ -0,0 +1,89 @@
from django.http.response import JsonResponse
from rest_framework.views import APIView
from .models import Planning, Events
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)
return JsonResponse(planning_serializer.data, safe=False)
def post(self, request):
planning_serializer = PlanningSerializer(data=request.data)
if planning_serializer.is_valid():
planning_serializer.save()
return JsonResponse(planning_serializer.data, status=201)
return JsonResponse(planning_serializer.errors, status=400)
class PlanningWithIdView(APIView):
def get(self, request,id):
planning = Planning.objects.get(pk=id)
if planning is None:
return JsonResponse({"errorMessage":'Le dossier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
planning_serializer=PlanningSerializer(planning)
return JsonResponse(planning_serializer.data, safe=False)
def put(self, request, id):
try:
planning = Planning.objects.get(pk=id)
except Planning.DoesNotExist:
return JsonResponse({'error': 'Planning not found'}, status=404)
planning_serializer = PlanningSerializer(planning, data=request.data)
if planning_serializer.is_valid():
planning_serializer.save()
return JsonResponse(planning_serializer.data)
return JsonResponse(planning_serializer.errors, status=400)
def delete(self, request, id):
try:
planning = Planning.objects.get(pk=id)
except Planning.DoesNotExist:
return JsonResponse({'error': 'Planning not found'}, status=404)
planning.delete()
return JsonResponse({'message': 'Planning deleted'}, status=204)
class EventsView(APIView):
def get(self, request):
events = bdd.getAllObjects(Events)
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()
return JsonResponse(events_serializer.data, status=201)
return JsonResponse(events_serializer.errors, status=400)
class EventsWithIdView(APIView):
def put(self, request, id):
try:
event = Events.objects.get(pk=id)
except Events.DoesNotExist:
return JsonResponse({'error': 'Event not found'}, status=404)
events_serializer = EventsSerializer(event, data=request.data)
if events_serializer.is_valid():
events_serializer.save()
return JsonResponse(events_serializer.data)
return JsonResponse(events_serializer.errors, status=400)
def delete(self, request, id):
try:
event = Events.objects.get(pk=id)
except Events.DoesNotExist:
return JsonResponse({'error': 'Event not found'}, status=404)
event.delete()
return JsonResponse({'message': 'Event deleted'}, status=204)

View File

@ -16,6 +16,7 @@ commands = [
["python", "manage.py", "collectstatic", "--noinput"],
["python", "manage.py", "flush", "--noinput"],
["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"],
["python", "manage.py", "makemigrations", "Planning", "--noinput"],
["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"],
["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"],
["python", "manage.py", "makemigrations", "Auth", "--noinput"],

View File

@ -13,7 +13,7 @@ export default function Page() {
start: '',
end: '',
location: '',
scheduleId: '', // Enlever la valeur par défaut ici
planning: '', // Enlever la valeur par défaut ici
recurrence: 'none',
selectedDays: [],
recurrenceEnd: '',
@ -32,7 +32,7 @@ export default function Page() {
start: eventDate.toISOString(),
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
location: '',
scheduleId: '', // 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',
selectedDays: [],
recurrenceEnd: '',

View File

@ -0,0 +1,108 @@
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");
error.details = body;
throw error;
}
const getData = (url) => {
return fetch(`${url}`).then(requestResponseHandler);
}
const createDatas = (url, newData, csrfToken) => {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(newData),
credentials: 'include'
})
.then(requestResponseHandler)
};
const updateDatas = (url, updatedData, csrfToken) => {
return fetch(`${url}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(updatedData),
credentials: 'include'
})
.then(requestResponseHandler)
};
const removeDatas = (url, csrfToken) => {
return fetch(`${url}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include'
})
.then(requestResponseHandler)
};
export const fetchPlannings = () => {
return getData(`${BE_PLANNING_PLANNINGS_URL}`)
};
export const getPlanning = (id) => {
return getData(`${BE_PLANNING_PLANNINGS_URL}/${id}`)
};
export const createPlanning = (newData, csrfToken) => {
return createDatas(`${BE_PLANNING_PLANNINGS_URL}`, newData, csrfToken)
}
export const updatePlanning = (id,newData, csrfToken) => {
return updateDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, newData, csrfToken)
}
export const deletePlanning = (id, csrfToken) => {
return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken)
}
export const fetchEvents = () => {
return getData(`${BE_PLANNING_EVENTS_URL}`)
};
export const getEvent = (id) => {
return getData(`${BE_PLANNING_EVENTS_URL}/${id}`)
};
export const createEvent = (newData, csrfToken) => {
return createDatas(`${BE_PLANNING_EVENTS_URL}`, newData, csrfToken)
}
export const updateEvent = (id,newData, csrfToken) => {
return updateDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, newData, csrfToken)
}
export const deleteEvent = (id, csrfToken) => {
return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken)
}

View File

@ -39,7 +39,7 @@ const Calendar = ({ onDateClick, onEventClick }) => {
useEffect(() => {
// S'assurer que le filtrage est fait au niveau parent
const filtered = events.filter(event => !hiddenSchedules.includes(event.scheduleId));
const filtered = events?.filter(event => !hiddenSchedules.includes(event.planning));
setVisibleEvents(filtered);
logger.debug('Events filtrés:', filtered); // Debug
}, [events, hiddenSchedules]);

View File

@ -3,18 +3,18 @@ import { format } from 'date-fns';
import React from 'react';
export default function EventModal({ isOpen, onClose, eventData, setEventData }) {
const { addEvent, updateEvent, deleteEvent, schedules } = usePlanning();
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } = usePlanning();
// S'assurer que scheduleId est défini lors du premier rendu
// S'assurer que planning est défini lors du premier rendu
React.useEffect(() => {
if (!eventData.scheduleId && schedules.length > 0) {
if (!eventData.planning && schedules.length > 0) {
setEventData(prev => ({
...prev,
scheduleId: schedules[0].id,
planning: schedules[0].id,
color: schedules[0].color
}));
}
}, [schedules, eventData.scheduleId]);
}, [schedules, eventData.planning]);
if (!isOpen) return null;
@ -39,24 +39,24 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
const handleSubmit = (e) => {
e.preventDefault();
if (!eventData.scheduleId) {
if (!eventData.planning) {
alert('Veuillez sélectionner un planning');
return;
}
const selectedSchedule = schedules.find(s => s.id === eventData.scheduleId);
const selectedSchedule = schedules.find(s => s.id === eventData.planning);
if (eventData.id) {
updateEvent(eventData.id, {
handleUpdateEvent(eventData.id, {
...eventData,
scheduleId: eventData.scheduleId, // S'assurer que scheduleId est bien défini
planning: eventData.planning, // S'assurer que planning est bien défini
color: eventData.color || selectedSchedule?.color
});
} else {
addEvent({
...eventData,
id: `event-${Date.now()}`,
scheduleId: eventData.scheduleId, // S'assurer que scheduleId est bien défini
planning: eventData.planning, // S'assurer que planning est bien défini
color: eventData.color || selectedSchedule?.color
});
}
@ -65,7 +65,7 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
const handleDelete = () => {
if (eventData.id && confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) {
deleteEvent(eventData.id);
handleDeleteEvent(eventData.id);
onClose();
}
};
@ -111,12 +111,12 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
Planning
</label>
<select
value={eventData.scheduleId || schedules[0]?.id}
value={eventData.planning || schedules[0]?.id}
onChange={(e) => {
const selectedSchedule = schedules.find(s => s.id === e.target.value);
setEventData({
...eventData,
scheduleId: e.target.value,
planning: e.target.value,
color: selectedSchedule?.color || '#10b981'
});
}}
@ -138,7 +138,7 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
</label>
<input
type="color"
value={eventData.color || schedules.find(s => s.id === eventData.scheduleId)?.color || '#10b981'}
value={eventData.color || schedules.find(s => s.id === eventData.planning)?.color || '#10b981'}
onChange={(e) => setEventData({ ...eventData, color: e.target.value })}
className="w-full h-10 p-1 rounded border"
/>

View File

@ -1,5 +1,10 @@
import { createContext, useContext, useState } from 'react';
import { mockEvents, mockSchedules } from '@/data/mockData';
import { createContext, useContext, useEffect, useState } from 'react';
import { createPlanning, fetchEvents, fetchPlannings, updatePlanning, createEvent, deleteEvent, updateEvent } from '@/app/actions/planningAction';
import { useCsrfToken } from './CsrfContext';
import { ESTABLISHMENT_ID } from '@/utils/Url';
import logger from '@/utils/logger';
/**
* Contexte de planification pour gérer l'état global du planning
@ -16,51 +21,90 @@ export function PlanningProvider({ children }) {
// const [events, setEvents] = useState([]);
// const [schedules, setSchedules] = useState([]);
// const [selectedSchedule, setSelectedSchedule] = useState(null);
const [events, setEvents] = useState(mockEvents);
const [schedules, setSchedules] = useState(mockSchedules);
const [selectedSchedule, setSelectedSchedule] = useState(mockSchedules[0].id);
const [events, setEvents] = useState([]);
const [schedules, setSchedules] = useState([]);
const [selectedSchedule, setSelectedSchedule] = useState(0);
const [currentDate, setCurrentDate] = useState(new Date());
const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week'
const [hiddenSchedules, setHiddenSchedules] = useState([]);
const csrfToken = useCsrfToken();
useEffect(()=>{
fetchPlannings().then((data) => {
setSchedules(data)
setSelectedSchedule(data[0].id);
});
fetchEvents().then((data)=>{
setEvents(data);
});
},[]);
const addEvent = (newEvent) => {
setEvents((prevEvents) => [...prevEvents, newEvent]);
createEvent(newEvent).then((data) => {
setEvents((prevEvents) => [...prevEvents, data]);
});
console.log('newEvent',newEvent);
//dssetEvents((prevEvents) => [...prevEvents, newEvent]);
};
const updateEvent = (id, updatedEvent) => {
setEvents((prevEvents) =>
prevEvents.map((event) => (event.id === id ? updatedEvent : event))
);
const handleUpdateEvent = (id, updatedEvent) => {
updateEvent(id,updatedEvent,csrfToken).then((data) => {
setEvents((prevEvents) =>
prevEvents.map((event) => (event.id === id ? updatedEvent : event))
);
});
};
const deleteEvent = (id) => {
setEvents((prevEvents) => prevEvents.filter((event) => event.id !== id));
const handleDeleteEvent = (id) => {
deleteEvent(id,csrfToken).then((data) => {
setEvents((prevEvents) => prevEvents.filter((event) => event.id !== id));
});
};
const addSchedule = (newSchedule) => {
setSchedules((prevSchedules) => [...prevSchedules, newSchedule]);
//FIXME:Gerenr lestablshment
logger.debug('newSchedule',newSchedule);
newSchedule.establishment = ESTABLISHMENT_ID;
createPlanning(newSchedule,csrfToken).then((data) => {
setSchedules((prevSchedules) => [...prevSchedules, data]);
});
};
const updateSchedule = (id, updatedSchedule) => {
setSchedules((prevSchedules) =>
prevSchedules.map((schedule) =>
schedule.id === id ? updatedSchedule : schedule
)
);
updatePlanning(id,updatedSchedule,csrfToken).then((data) => {
setSchedules((prevSchedules) =>
prevSchedules.map((schedule) =>
schedule.id === id ? updatedSchedule : schedule
)
);
});
};
const deleteSchedule = (id) => {
setSchedules((prevSchedules) =>
prevSchedules.filter((schedule) => schedule.id !== id)
deletePlanning(id,csrfToken).then((data) => {
setSchedules((prevSchedules) =>
prevSchedules.filter((schedule) => schedule.id !== id)
);
}
);
};
const toggleScheduleVisibility = (scheduleId) => {
const toggleScheduleVisibility = (planning) => {
setHiddenSchedules((prev) => {
const isHidden = prev.includes(scheduleId);
const isHidden = prev.includes(planning);
const newHiddenSchedules = isHidden
? prev.filter((id) => id !== scheduleId)
: [...prev, scheduleId];
? prev.filter((id) => id !== planning)
: [...prev, planning];
return newHiddenSchedules;
});
};
@ -73,8 +117,8 @@ export function PlanningProvider({ children }) {
selectedSchedule,
setSelectedSchedule,
addEvent,
updateEvent,
deleteEvent,
handleUpdateEvent,
handleDeleteEvent,
addSchedule,
updateSchedule,
deleteSchedule,

View File

@ -12,7 +12,7 @@ export const mockEvents = [
description: 'Cours de mathématiques avancées',
start: '2024-02-20T08:00:00',
end: '2024-02-20T10:00:00',
scheduleId: 'default',
planning: 'default',
location: 'Salle A101',
color: '#10b981'
},
@ -22,7 +22,7 @@ export const mockEvents = [
description: 'Examen final de physique',
start: '2024-02-21T14:00:00',
end: '2024-02-21T16:00:00',
scheduleId: 'exam',
planning: 'exam',
location: 'Amphithéâtre B',
color: '#f59e0b'
}

View File

@ -1,63 +0,0 @@
import { useState } from 'react';
export function useSchedules() {
const [schedules, setSchedules] = useState([
{ id: 'default', name: 'Planning principal', color: '#10b981' },
{ id: 'secondary', name: 'Planning secondaire', color: '#3b82f6' },
{ id: 'special', name: 'Événements spéciaux', color: '#ef4444' },
{ id: 'exam', name: 'Planning examens', color: '#f59e0b' }
]);
const addSchedule = (newSchedule) => {
setSchedules(prev => [...prev, {
...newSchedule,
id: `schedule-${Date.now()}`
}]);
};
const updateSchedule = (id, updates) => {
setSchedules(prev => prev.map(schedule =>
schedule.id === id ? { ...schedule, ...updates } : schedule
));
};
const deleteSchedule = (id) => {
setSchedules(prev => prev.filter(schedule => schedule.id !== id));
};
return {
schedules,
addSchedule,
updateSchedule,
deleteSchedule
};
}
export function useEvents(initialEvents = []) {
const [events, setEvents] = useState(initialEvents);
const addEvent = (newEvent) => {
setEvents(prev => [...prev, {
...newEvent,
id: `event-${Date.now()}`
}]);
};
const updateEvent = (id, updates) => {
setEvents(prev => prev.map(event =>
event.id === id ? { ...event, ...updates } : event
));
};
const deleteEvent = (id) => {
setEvents(prev => prev.filter(event => event.id !== id));
};
return {
events,
setEvents,
addEvent,
updateEvent,
deleteEvent
};
}

View File

@ -1,60 +0,0 @@
import { useState } from 'react';
import { mockEvents, mockSchedules } from '@/data/mockData';
export default function useSchedules() {
const [schedules, setSchedules] = useState(mockSchedules);
const [events, setEvents] = useState(mockEvents);
const [selectedSchedule, setSelectedSchedule] = useState(mockSchedules[0].id);
const addEvent = async (eventData) => {
const newEvent = {
...eventData,
id: `event-${Date.now()}`,
color: schedules.find(s => s.id === eventData.scheduleId)?.color || '#10b981'
};
setEvents(prev => [...prev, newEvent]);
return newEvent;
};
const updateEvent = async (eventId, updates) => {
setEvents(prev => prev.map(event =>
event.id === eventId ? { ...event, ...updates } : event
));
return updates;
};
const deleteEvent = async (eventId) => {
setEvents(prev => prev.filter(event => event.id !== eventId));
return eventId;
};
const addSchedule = (newSchedule) => {
setSchedules(prev => [...prev, {
...newSchedule,
id: `schedule-${Date.now()}`
}]);
};
const updateSchedule = (id, updates) => {
setSchedules(prev => prev.map(schedule =>
schedule.id === id ? { ...schedule, ...updates } : schedule
));
};
const deleteSchedule = (id) => {
setSchedules(prev => prev.filter(schedule => schedule.id !== id));
};
return {
events,
schedules,
selectedSchedule,
setSelectedSchedule,
addEvent,
updateEvent,
deleteEvent,
addSchedule,
updateSchedule,
deleteSchedule
};
}

View File

@ -42,7 +42,13 @@ export const BE_SCHOOL_PAYMENT_PLANS_URL = `${BASE_URL}/School/paymentPlans`;
export const BE_SCHOOL_PAYMENT_MODES_URL = `${BASE_URL}/School/paymentModes`;
export const BE_SCHOOL_ESTABLISHMENT_URL = `${BASE_URL}/School/establishments`;
// En attendant la gestion des sessions
// GESTION PLANNING
export const BE_PLANNING_PLANNINGS_URL = `${BASE_URL}/Planning/plannings`
export const BE_PLANNING_EVENTS_URL = `${BASE_URL}/Planning/events`
// FIXME : En attendant la gestion des sessions
export const ESTABLISHMENT_ID = 1;
// GESTION MESSAGERIE

View File

@ -14,7 +14,7 @@ export const DEFAULT_EVENT = {
start: '',
end: '',
location: '',
scheduleId: 'default',
planning: 'default',
color: '#10b981',
recurrence: 'none',
selectedDays: [],

View File

@ -85,7 +85,7 @@ export const getWeekEvents = (day, events) => {
const start = startOfDay(day);
const end = endOfDay(day);
return events.filter(event => {
return events?.filter(event => {
const eventStart = new Date(event.start);
const eventEnd = new Date(event.end);
@ -106,5 +106,5 @@ export const getWeekEvents = (day, events) => {
* @returns {Array<Object>} Liste des événements filtrés
*/
export const filterEventsByVisibleSchedules = (events, hiddenSchedules) => {
return events.filter(event => !hiddenSchedules.includes(event.scheduleId));
return events.filter(event => !hiddenSchedules.includes(event.planning));
};