mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
chore: Initial Commit
feat: Gestion des inscriptions [#1] feat(frontend): Création des vues pour le paramétrage de l'école [#2] feat: Gestion du login [#6] fix: Correction lors de la migration des modèle [#8] feat: Révision du menu principal [#9] feat: Ajout d'un footer [#10] feat: Création des dockers compose pour les environnements de développement et de production [#12] doc(ci): Mise en place de Husky et d'un suivi de version automatique [#14]
This commit is contained in:
17
Front-End/src/components/AlertMessage.js
Normal file
17
Front-End/src/components/AlertMessage.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
const AlertMessage = ({ title, message, buttonText, buttonLink }) => {
|
||||
return (
|
||||
<div className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
|
||||
<h3 className="font-bold">{title}</h3>
|
||||
<p className="mt-2">{message}</p>
|
||||
<div className="alert-actions mt-4">
|
||||
<a className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600" href={buttonLink}>
|
||||
{buttonText} <i className="icon profile-add"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertMessage;
|
||||
29
Front-End/src/components/AlertWithModal.js
Normal file
29
Front-End/src/components/AlertWithModal.js
Normal file
@ -0,0 +1,29 @@
|
||||
import React, { useState } from 'react';
|
||||
import Modal from '@/components/Modal';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
|
||||
const AlertWithModal = ({ title, message, buttonText}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
|
||||
<h3 className="font-bold">{title}</h3>
|
||||
<p className="mt-2">{message}</p>
|
||||
<div className="alert-actions mt-4">
|
||||
<button
|
||||
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600 flex items-center"
|
||||
onClick={openModal}
|
||||
>
|
||||
{buttonText} <UserPlus size={20} className="ml-2" />
|
||||
</button>
|
||||
</div>
|
||||
<Modal isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertWithModal;
|
||||
37
Front-End/src/components/AlphabetLinks.js
Normal file
37
Front-End/src/components/AlphabetLinks.js
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
|
||||
const AlphabetPaginationNumber = ({ letter, active , onClick}) => (
|
||||
<button className={`w-8 h-8 flex items-center justify-center rounded ${
|
||||
active ? 'bg-emerald-500 text-white' : 'text-gray-600 bg-gray-200 hover:bg-gray-50'
|
||||
}`} onClick={onClick}>
|
||||
{letter}
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
const AlphabetLinks = ({filter, onLetterClick }) => {
|
||||
const [currentLetter, setCurrentLetter] = useState(filter);
|
||||
const alphabet = "*ABCDEFGHIJKLMNOPQRSTUVWXYZ".split('');
|
||||
|
||||
|
||||
return (
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{alphabet.map((letter) => (
|
||||
<AlphabetPaginationNumber
|
||||
key={letter}
|
||||
letter={letter}
|
||||
active={currentLetter === letter }
|
||||
onClick={() => {setCurrentLetter(letter);onLetterClick(letter)}}
|
||||
/>
|
||||
|
||||
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlphabetLinks;
|
||||
27
Front-End/src/components/Button.js
Normal file
27
Front-End/src/components/Button.js
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const Button = ({ text, onClick, href, className, primary, icon }) => {
|
||||
const router = useRouter();
|
||||
const baseClass = 'px-4 py-2 rounded-md text-white h-8 flex items-center justify-center';
|
||||
const primaryClass = 'bg-emerald-500 hover:bg-emerald-600';
|
||||
const secondaryClass = 'bg-gray-300 hover:bg-gray-400 text-black';
|
||||
const buttonClass = `${baseClass} ${primary ? primaryClass : secondaryClass} ${className}`;
|
||||
|
||||
const handleClick = (e) => {
|
||||
if (href) {
|
||||
router.push(href);
|
||||
} else if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={buttonClass} onClick={handleClick}>
|
||||
{icon && <span className="mr-2">{icon}</span>}
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
212
Front-End/src/components/Calendar.js
Normal file
212
Front-End/src/components/Calendar.js
Normal file
@ -0,0 +1,212 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import WeekView from '@/components/Calendar/WeekView';
|
||||
import MonthView from '@/components/Calendar/MonthView';
|
||||
import YearView from '@/components/Calendar/YearView';
|
||||
import PlanningView from '@/components/Calendar/PlanningView';
|
||||
import ToggleView from '@/components/ToggleView';
|
||||
import { ChevronLeft, ChevronRight, Plus, ChevronDown } from 'lucide-react';
|
||||
import { format, addWeeks, addMonths, addYears, subWeeks, subMonths, subYears, getWeek, setMonth, setYear } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
|
||||
|
||||
const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
const { currentDate, setCurrentDate, viewType, setViewType, events, hiddenSchedules } = usePlanning();
|
||||
const [visibleEvents, setVisibleEvents] = useState([]);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
|
||||
// Ajouter ces fonctions pour la gestion des mois et années
|
||||
const months = Array.from({ length: 12 }, (_, i) => ({
|
||||
value: i,
|
||||
label: format(new Date(2024, i, 1), 'MMMM', { locale: fr })
|
||||
}));
|
||||
|
||||
const years = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: new Date().getFullYear() - 5 + i,
|
||||
label: new Date().getFullYear() - 5 + i
|
||||
}));
|
||||
|
||||
const handleMonthSelect = (monthIndex) => {
|
||||
setCurrentDate(setMonth(currentDate, monthIndex));
|
||||
setShowDatePicker(false);
|
||||
};
|
||||
|
||||
const handleYearSelect = (year) => {
|
||||
setCurrentDate(setYear(currentDate, year));
|
||||
setShowDatePicker(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// S'assurer que le filtrage est fait au niveau parent
|
||||
const filtered = events.filter(event => !hiddenSchedules.includes(event.scheduleId));
|
||||
setVisibleEvents(filtered);
|
||||
console.log('Events filtrés:', filtered); // Debug
|
||||
}, [events, hiddenSchedules]);
|
||||
|
||||
const navigateDate = (direction) => {
|
||||
const getNewDate = () => {
|
||||
switch (viewType) {
|
||||
case 'week':
|
||||
return direction === 'next'
|
||||
? addWeeks(currentDate, 1)
|
||||
: subWeeks(currentDate, 1);
|
||||
case 'month':
|
||||
return direction === 'next'
|
||||
? addMonths(currentDate, 1)
|
||||
: subMonths(currentDate, 1);
|
||||
case 'year':
|
||||
return direction === 'next'
|
||||
? addYears(currentDate, 1)
|
||||
: subYears(currentDate, 1);
|
||||
default:
|
||||
return currentDate;
|
||||
}
|
||||
};
|
||||
|
||||
setCurrentDate(getNewDate());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex h-full flex-col">
|
||||
<div className="flex items-center justify-between p-4 bg-white sticky top-0 z-30 border-b shadow-sm h-[64px]">
|
||||
{/* Navigation à gauche */}
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setCurrentDate(new Date())}
|
||||
className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
Aujourd'hui
|
||||
</button>
|
||||
<button onClick={() => navigateDate('prev')} className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Menu déroulant pour le mois/année */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{format(currentDate, viewType === 'year' ? 'yyyy' : 'MMMM yyyy', { locale: fr })}
|
||||
</h2>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Menu de sélection du mois/année */}
|
||||
{showDatePicker && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-64">
|
||||
{viewType !== 'year' && (
|
||||
<div className="p-2 border-b">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{months.map((month) => (
|
||||
<button
|
||||
key={month.value}
|
||||
onClick={() => handleMonthSelect(month.value)}
|
||||
className="p-2 text-sm hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
{month.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{years.map((year) => (
|
||||
<button
|
||||
key={year.value}
|
||||
onClick={() => handleYearSelect(year.value)}
|
||||
className="p-2 text-sm hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
{year.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button onClick={() => navigateDate('next')} className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Numéro de semaine au centre */}
|
||||
{viewType === 'week' && (
|
||||
<div className="flex items-center gap-1 text-sm font-medium text-gray-600">
|
||||
<span>Semaine</span>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md">
|
||||
{getWeek(currentDate, { weekStartsOn: 1 })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contrôles à droite */}
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleView viewType={viewType} setViewType={setViewType} />
|
||||
<button
|
||||
onClick={onDateClick}
|
||||
className="w-10 h-10 flex items-center justify-center bg-emerald-600 text-white rounded-full hover:bg-emerald-700 shadow-md transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu scrollable */}
|
||||
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{viewType === 'week' && (
|
||||
<motion.div
|
||||
key="week"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
<WeekView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'month' && (
|
||||
<motion.div
|
||||
key="month"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<MonthView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'year' && (
|
||||
<motion.div
|
||||
key="year"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<YearView onDateClick={onDateClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'planning' && (
|
||||
<motion.div
|
||||
key="planning"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<PlanningView onEventClick={onEventClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
86
Front-End/src/components/Calendar/MonthView.js
Normal file
86
Front-End/src/components/Calendar/MonthView.js
Normal file
@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { format, startOfWeek, endOfWeek, eachDayOfInterval, startOfMonth, endOfMonth, isSameMonth, isToday } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getEventsForDate } from '@/utils/events';
|
||||
|
||||
const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
const { currentDate, setViewType, setCurrentDate, events } = usePlanning();
|
||||
|
||||
// Obtenir tous les jours du mois actuel
|
||||
const monthStart = startOfMonth(currentDate);
|
||||
const monthEnd = endOfMonth(currentDate);
|
||||
const startDate = startOfWeek(monthStart, { locale: fr, weekStartsOn: 1 });
|
||||
const endDate = endOfWeek(monthEnd, { locale: fr, weekStartsOn: 1 });
|
||||
|
||||
const days = eachDayOfInterval({ start: startDate, end: endDate });
|
||||
|
||||
const handleDayClick = (day) => {
|
||||
setCurrentDate(day); // Met à jour la date courante
|
||||
setViewType('week'); // Change la vue en mode semaine
|
||||
};
|
||||
|
||||
const renderDay = (day) => {
|
||||
const isCurrentMonth = isSameMonth(day, currentDate);
|
||||
const dayEvents = getEventsForDate(day, events);
|
||||
const isCurrentDay = isToday(day);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={day.toString()}
|
||||
className={`p-2 overflow-y-auto relative flex flex-col
|
||||
${!isCurrentMonth ? 'bg-gray-100 text-gray-400' : ''}
|
||||
${isCurrentDay ? 'bg-emerald-50' : ''}
|
||||
hover:bg-gray-100 cursor-pointer border-b border-r`}
|
||||
onClick={() => handleDayClick(day)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className={`text-sm font-medium rounded-full w-7 h-7 flex items-center justify-center
|
||||
${isCurrentDay ? 'bg-emerald-500 text-white' : ''}
|
||||
${!isCurrentMonth ? 'text-gray-400' : ''}`}
|
||||
>
|
||||
{format(day, 'd')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1 flex-1">
|
||||
{dayEvents.map((event, index) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="text-xs p-1 rounded truncate cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: `${event.color}15`,
|
||||
color: event.color,
|
||||
borderLeft: `2px solid ${event.color}`
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEventClick(event);
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col border border-gray-200 rounded-lg bg-white">
|
||||
{/* En-tête des jours de la semaine */}
|
||||
<div className="grid grid-cols-7 border-b">
|
||||
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map(day => (
|
||||
<div key={day} className="p-2 text-center text-sm font-medium text-gray-500">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Grille des jours */}
|
||||
<div className="flex-1 grid grid-cols-7 grid-rows-[repeat(6,1fr)]">
|
||||
{days.map((day) => renderDay(day))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthView;
|
||||
111
Front-End/src/components/Calendar/PlanningView.js
Normal file
111
Front-End/src/components/Calendar/PlanningView.js
Normal file
@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { format, isSameDay, eachDayOfInterval } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
|
||||
const PlanningView = ({ events, onEventClick }) => {
|
||||
// Fonction pour diviser un événement en jours
|
||||
const splitEventByDays = (event) => {
|
||||
const start = new Date(event.start);
|
||||
const end = new Date(event.end);
|
||||
|
||||
// Si même jour, retourner l'événement tel quel
|
||||
if (isSameDay(start, end)) {
|
||||
return [event];
|
||||
}
|
||||
|
||||
// Sinon, créer une entrée pour chaque jour
|
||||
const days = eachDayOfInterval({ start, end });
|
||||
return days.map(day => ({
|
||||
...event,
|
||||
displayDate: day,
|
||||
isMultiDay: true
|
||||
}));
|
||||
};
|
||||
|
||||
// Aplatir tous les événements en incluant les événements sur plusieurs jours
|
||||
const flattenedEvents = events
|
||||
.flatMap(splitEventByDays)
|
||||
.sort((a, b) => new Date(a.displayDate || a.start) - new Date(b.displayDate || b.start));
|
||||
|
||||
return (
|
||||
<div className="bg-white h-full overflow-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="bg-gray-50 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
|
||||
Date
|
||||
</th>
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
|
||||
Horaires
|
||||
</th>
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
|
||||
Événement
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{flattenedEvents.map((event, index) => {
|
||||
const start = new Date(event.displayDate || event.start);
|
||||
const end = new Date(event.end);
|
||||
const isMultiDay = event.isMultiDay;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={`${event.id}-${index}`}
|
||||
className="hover:bg-gray-50 cursor-pointer"
|
||||
onClick={() => onEventClick(event)}
|
||||
>
|
||||
<td className="py-3 px-4 text-sm text-gray-900 whitespace-nowrap">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-extrabold">{format(start, 'd')}</span>
|
||||
<span className="font-semibold">{format(start, 'MMM', { locale: fr }).toLowerCase()}</span>
|
||||
<span className="font-semibold">{format(start, 'EEE', { locale: fr })}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-900 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full mr-2"
|
||||
style={{ backgroundColor: event.color }}
|
||||
/>
|
||||
{isMultiDay
|
||||
? (isSameDay(start, new Date(event.start))
|
||||
? "À partir de "
|
||||
: isSameDay(start, end)
|
||||
? "Jusqu'à "
|
||||
: "Toute la journée")
|
||||
: ""
|
||||
}
|
||||
{format(new Date(event.start), 'HH:mm')}
|
||||
{!isMultiDay && ` - ${format(end, 'HH:mm')}`}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{event.title}
|
||||
</div>
|
||||
{event.description && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{event.description}
|
||||
</div>
|
||||
)}
|
||||
{event.location && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{event.location}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanningView;
|
||||
208
Front-End/src/components/Calendar/WeekView.js
Normal file
208
Front-End/src/components/Calendar/WeekView.js
Normal file
@ -0,0 +1,208 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { format, startOfWeek, addDays, differenceInMinutes, isSameDay } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getWeekEvents } from '@/utils/events';
|
||||
import { isToday } from 'date-fns';
|
||||
|
||||
const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const { currentDate } = usePlanning();
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const scrollContainerRef = useRef(null); // Ajouter cette référence
|
||||
|
||||
// Déplacer ces déclarations avant leur utilisation
|
||||
const timeSlots = Array.from({ length: 24 }, (_, i) => i);
|
||||
const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 });
|
||||
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
|
||||
|
||||
// Maintenant on peut utiliser weekDays
|
||||
const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date()));
|
||||
|
||||
// Mettre à jour la position de la ligne toutes les minutes
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(new Date());
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Modifier l'useEffect pour l'auto-scroll
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current && isCurrentWeek) {
|
||||
const currentHour = new Date().getHours();
|
||||
const scrollPosition = currentHour * 80;
|
||||
// Ajout d'un délai pour laisser le temps au DOM de se mettre à jour
|
||||
setTimeout(() => {
|
||||
scrollContainerRef.current.scrollTop = scrollPosition - 200;
|
||||
}, 0);
|
||||
}
|
||||
}, [currentDate, isCurrentWeek]); // Ajout de currentDate dans les dépendances
|
||||
|
||||
// Calculer la position de la ligne de temps
|
||||
const getCurrentTimePosition = () => {
|
||||
const hours = currentTime.getHours();
|
||||
const minutes = currentTime.getMinutes();
|
||||
return `${(hours + minutes / 60) * 5}rem`;
|
||||
};
|
||||
|
||||
// Utiliser les événements déjà filtrés passés en props
|
||||
const weekEventsMap = weekDays.reduce((acc, day) => {
|
||||
acc[format(day, 'yyyy-MM-dd')] = getWeekEvents(day, events);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const isWeekend = (date) => {
|
||||
const day = date.getDay();
|
||||
return day === 0 || day === 6;
|
||||
};
|
||||
|
||||
const findOverlappingEvents = (event, dayEvents) => {
|
||||
const eventStart = new Date(event.start);
|
||||
const eventEnd = new Date(event.end);
|
||||
|
||||
return dayEvents.filter(otherEvent => {
|
||||
if (otherEvent.id === event.id) return false;
|
||||
const otherStart = new Date(otherEvent.start);
|
||||
const otherEnd = new Date(otherEvent.end);
|
||||
return !(otherEnd <= eventStart || otherStart >= eventEnd);
|
||||
});
|
||||
};
|
||||
|
||||
const calculateEventStyle = (event, dayEvents) => {
|
||||
const start = new Date(event.start);
|
||||
const end = new Date(event.end);
|
||||
const startMinutes = (start.getMinutes() / 60) * 5;
|
||||
const duration = ((end - start) / (1000 * 60 * 60)) * 5;
|
||||
|
||||
// Trouver les événements qui se chevauchent
|
||||
const overlappingEvents = findOverlappingEvents(event, dayEvents);
|
||||
const eventIndex = overlappingEvents.findIndex(e => e.id > event.id) + 1;
|
||||
const totalOverlapping = overlappingEvents.length + 1;
|
||||
|
||||
// Calculer la largeur et la position horizontale
|
||||
const width = `calc((100% / ${totalOverlapping}) - 4px)`;
|
||||
const left = `calc((100% / ${totalOverlapping}) * ${eventIndex})`;
|
||||
|
||||
return {
|
||||
height: `${duration}rem`,
|
||||
position: 'absolute',
|
||||
width,
|
||||
left,
|
||||
backgroundColor: `${event.color}15`,
|
||||
borderLeft: `3px solid ${event.color}`,
|
||||
borderRadius: '0.25rem',
|
||||
zIndex: 1,
|
||||
transform: `translateY(${startMinutes}rem)`
|
||||
};
|
||||
};
|
||||
|
||||
const renderEventInCell = (event, dayEvents) => {
|
||||
const eventStyle = calculateEventStyle(event, dayEvents);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
|
||||
style={eventStyle}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEventClick(event);
|
||||
}}
|
||||
>
|
||||
<div className="p-1">
|
||||
<div className="font-semibold text-xs truncate" style={{ color: event.color }}>
|
||||
{event.title}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: event.color, opacity: 0.75 }}>
|
||||
{format(new Date(event.start), 'HH:mm')} - {format(new Date(event.end), 'HH:mm')}
|
||||
</div>
|
||||
{event.location && (
|
||||
<div className="text-xs truncate" style={{ color: event.color, opacity: 0.75 }}>
|
||||
{event.location}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* En-tête des jours */}
|
||||
<div className="grid gap-[1px] bg-gray-100 pr-[17px]" 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
|
||||
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
||||
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
|
||||
>
|
||||
<div className="text-xs font-medium text-gray-500">
|
||||
{format(day, 'EEEE', { locale: fr })}
|
||||
</div>
|
||||
<div className={`text-sm font-semibold inline-block rounded-full w-7 h-7 leading-7
|
||||
${isToday(day) ? 'bg-emerald-500 text-white' : ''}`}>
|
||||
{format(day, 'd', { locale: fr })}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Grille horaire */}
|
||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
|
||||
{/* Ligne de temps actuelle */}
|
||||
{isCurrentWeek && (
|
||||
<div
|
||||
className="absolute left-0 right-0 z-10 border-emerald-500 border pointer-events-none"
|
||||
style={{
|
||||
top: getCurrentTimePosition(),
|
||||
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-[1px] bg-gray-100" style={{ gridTemplateColumns: "2.5rem repeat(7, 1fr)" }}>
|
||||
{timeSlots.map(hour => (
|
||||
<React.Fragment key={hour}>
|
||||
<div className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
|
||||
{`${hour.toString().padStart(2, '0')}:00`}
|
||||
</div>
|
||||
{weekDays.map((day) => {
|
||||
const dayKey = format(day, 'yyyy-MM-dd');
|
||||
const dayEvents = weekEventsMap[dayKey] || [];
|
||||
return (
|
||||
<div
|
||||
key={`${hour}-${day}`}
|
||||
className={`h-20 relative border-b border-gray-100
|
||||
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
||||
${isToday(day) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}`}
|
||||
onClick={() => {
|
||||
const date = new Date(day);
|
||||
date.setHours(hour);
|
||||
onDateClick(date);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-1"> {/* Ajout de gap-1 */}
|
||||
{dayEvents.filter(event => {
|
||||
const eventStart = new Date(event.start);
|
||||
return eventStart.getHours() === hour;
|
||||
}).map(event => renderEventInCell(event, dayEvents))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeekView;
|
||||
52
Front-End/src/components/Calendar/YearView.js
Normal file
52
Front-End/src/components/Calendar/YearView.js
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { format } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getMonthEventCount } from '@/utils/events';
|
||||
import { isSameMonth } from 'date-fns';
|
||||
|
||||
const MonthCard = ({ month, eventCount, onClick }) => (
|
||||
<div
|
||||
className={`bg-white p-4 rounded shadow hover:shadow-lg cursor-pointer
|
||||
${isSameMonth(month, new Date()) ? 'ring-2 ring-emerald-500' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<h3 className="font-medium text-center mb-2">
|
||||
{format(month, 'MMMM', { locale: fr })}
|
||||
</h3>
|
||||
<div className="text-center text-sm">
|
||||
<span className="inline-flex items-center justify-center bg-emerald-100 text-emerald-800 px-2 py-1 rounded-full">
|
||||
{eventCount} événements
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const YearView = ({ onDateClick }) => {
|
||||
const { currentDate, events, setViewType, setCurrentDate } = usePlanning();
|
||||
|
||||
const months = Array.from(
|
||||
{ length: 12 },
|
||||
(_, i) => new Date(currentDate.getFullYear(), i, 1)
|
||||
);
|
||||
|
||||
const handleMonthClick = (month) => {
|
||||
setCurrentDate(month);
|
||||
setViewType('month');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4 p-4">
|
||||
{months.map(month => (
|
||||
<MonthCard
|
||||
key={month.getTime()}
|
||||
month={month}
|
||||
eventCount={getMonthEventCount(month, events)}
|
||||
onClick={() => handleMonthClick(month)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default YearView;
|
||||
188
Front-End/src/components/ClassForm.js
Normal file
188
Front-End/src/components/ClassForm.js
Normal file
@ -0,0 +1,188 @@
|
||||
import React, { useState } from 'react';
|
||||
import Slider from '@/components/Slider'
|
||||
|
||||
const ClassForm = ({ classe, onSubmit, isNew, specialities, teachers }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
nom_ambiance: classe.nom_ambiance || '',
|
||||
tranche_age: classe.tranche_age || [3, 6],
|
||||
nombre_eleves: classe.nombre_eleves || '',
|
||||
langue_enseignement: classe.langue_enseignement || 'Français',
|
||||
annee_scolaire: classe.annee_scolaire || '2024-2025',
|
||||
specialites_ids: classe.specialites_ids || [],
|
||||
enseignant_principal_id: classe.enseignant_principal_id || null,
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type } = e.target;
|
||||
const newValue = type === 'radio' ? parseInt(value) : value;
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[name]: newValue,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSliderChange = (value) => {
|
||||
console.log('update value : ', value)
|
||||
setFormData(prevFormData => ({
|
||||
...prevFormData,
|
||||
tranche_age: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleNumberChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prevFormData => ({
|
||||
...prevFormData,
|
||||
[name]: Number(value)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(formData, isNew);
|
||||
};
|
||||
|
||||
const handleSpecialityChange = (id) => {
|
||||
setFormData(prevFormData => {
|
||||
const specialites_ids = prevFormData.specialites_ids.includes(id)
|
||||
? prevFormData.specialites_ids.filter(specialityId => specialityId !== id)
|
||||
: [...prevFormData.specialites_ids, id];
|
||||
return { ...prevFormData, specialites_ids };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Nom d'ambiance
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nom de l'ambiance"
|
||||
name="nom_ambiance"
|
||||
value={formData.nom_ambiance}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Tranche d'âge
|
||||
</label>
|
||||
<Slider
|
||||
min={3}
|
||||
max={12}
|
||||
value={formData.tranche_age}
|
||||
onChange={handleSliderChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Nombre d'élèves
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="nombre_eleves"
|
||||
value={formData.nombre_eleves}
|
||||
onChange={handleNumberChange}
|
||||
min="1"
|
||||
max="40"
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Langue d'enseignement
|
||||
</label>
|
||||
<select
|
||||
name="langue_enseignement"
|
||||
value={formData.langue_enseignement}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
|
||||
>
|
||||
<option value="Français">Français</option>
|
||||
<option value="Anglais">Anglais</option>
|
||||
<option value="Espagnol">Espagnol</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Année scolaire
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="annee_scolaire"
|
||||
value={formData.annee_scolaire}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Spécialités
|
||||
</label>
|
||||
<div className="mt-2 grid grid-cols-1 gap-4">
|
||||
{specialities.map(speciality => (
|
||||
<div key={speciality.id} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`speciality-${speciality.id}`}
|
||||
name="specialites"
|
||||
value={speciality.id}
|
||||
checked={formData.specialites_ids.includes(speciality.id)}
|
||||
onChange={() => handleSpecialityChange(speciality.id)}
|
||||
className="h-4 w-4 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500"
|
||||
/>
|
||||
<label htmlFor={`speciality-${speciality.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
|
||||
{speciality.nom}
|
||||
<div
|
||||
className="w-4 h-4 rounded-full ml-2"
|
||||
style={{ backgroundColor: speciality.codeCouleur }}
|
||||
title={speciality.codeCouleur}
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Enseignant principal
|
||||
</label>
|
||||
<div className="mt-2 grid grid-cols-1 gap-4">
|
||||
{teachers.map(teacher => (
|
||||
<div key={teacher.id} className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
id={`teacher-${teacher.id}`}
|
||||
name="enseignant_principal_id"
|
||||
value={teacher.id}
|
||||
checked={formData.enseignant_principal_id === teacher.id}
|
||||
onChange={handleChange}
|
||||
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
|
||||
/>
|
||||
<label htmlFor={`speciality-${teacher.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
|
||||
{teacher.nom} {teacher.prenom}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 space-x-4">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
(!formData.nom_ambiance || !formData.nombre_eleves)
|
||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
}`}
|
||||
disabled={(!formData.nom_ambiance || !formData.nombre_eleves)}
|
||||
>
|
||||
Soumettre
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClassForm;
|
||||
111
Front-End/src/components/ClassesSection.js
Normal file
111
Front-End/src/components/ClassesSection.js
Normal file
@ -0,0 +1,111 @@
|
||||
import { School, Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
import Modal from '@/components/Modal';
|
||||
import ClassForm from '@/components/ClassForm';
|
||||
|
||||
const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleEdit, handleDelete }) => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [editingClass, setEditingClass] = useState(null);
|
||||
|
||||
const openEditModal = (classe) => {
|
||||
setIsOpen(true);
|
||||
setEditingClass(classe);
|
||||
}
|
||||
|
||||
const closeEditModal = () => {
|
||||
setIsOpen(false);
|
||||
setEditingClass(null);
|
||||
};
|
||||
|
||||
const handleModalSubmit = (updatedData) => {
|
||||
if (editingClass) {
|
||||
handleEdit(editingClass.id, updatedData);
|
||||
} else {
|
||||
handleCreate(updatedData);
|
||||
}
|
||||
closeEditModal();
|
||||
};
|
||||
|
||||
const handleInspect = (data) => {
|
||||
console.log('inspect classe : ', data)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
|
||||
<h2 className="text-3xl text-gray-800 flex items-center">
|
||||
<School className="w-8 h-8 mr-2" />
|
||||
Classes
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
|
||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-gray-200 max-w-8xl ml-0">
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'AMBIANCE', transform: (row) => row.nom_ambiance },
|
||||
{ name: 'AGE', transform: (row) => `${row.tranche_age[0]} - ${row.tranche_age[1]} ans` },
|
||||
{ name: 'NOMBRE D\'ELEVES', transform: (row) => row.nombre_eleves },
|
||||
{ name: 'LANGUE D\'ENSEIGNEMENT', transform: (row) => row.langue_enseignement },
|
||||
{ name: 'ANNEE SCOLAIRE', transform: (row) => row.annee_scolaire },
|
||||
{
|
||||
name: 'SPECIALITES',
|
||||
transform: (row) => (
|
||||
<div key={row.id} className="flex justify-center items-center space-x-2">
|
||||
{row.specialites.map(specialite => (
|
||||
<span
|
||||
key={specialite.id}
|
||||
className="w-4 h-4 rounded-full"
|
||||
style={{ backgroundColor: specialite.codeCouleur }}
|
||||
title={specialite.nom}
|
||||
></span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'ENSEIGNANT PRINCIPAL',
|
||||
transform: (row) => {
|
||||
return row.enseignant_principal
|
||||
? `${row.enseignant_principal.nom || ''} ${row.enseignant_principal.prenom || ''}`
|
||||
: <i>Non assigné</i>;
|
||||
}
|
||||
},
|
||||
{ name: 'ACTIONS', transform: (row) => (
|
||||
<DropdownMenu
|
||||
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
|
||||
items={[
|
||||
{ label: 'Inspecter', icon: ZoomIn, onClick: () => handleInspect(row) },
|
||||
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) },
|
||||
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
|
||||
]
|
||||
}
|
||||
buttonClassName="text-gray-400 hover:text-gray-600"
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
|
||||
/>
|
||||
)}
|
||||
]}
|
||||
data={classes}
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
title={editingClass ? "Modification de la classe" : "Création d'une nouvelle classe"} ContentComponent={() => (
|
||||
<ClassForm classe={editingClass || {}} onSubmit={handleModalSubmit} isNew={!editingClass} specialities={specialities} teachers={teachers} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClassesSection;
|
||||
16
Front-End/src/components/DjangoCSRFToken.js
Normal file
16
Front-End/src/components/DjangoCSRFToken.js
Normal file
@ -0,0 +1,16 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useCookies } from 'react-cookie';
|
||||
|
||||
export default function DjangoCSRFToken({ csrfToken }) {
|
||||
const [cookies, setCookie] = useCookies(['csrftoken']);
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken && csrfToken !== cookies.csrftoken) {
|
||||
setCookie('csrftoken', csrfToken, { path: '/' });
|
||||
}
|
||||
}, [csrfToken, cookies.csrftoken, setCookie]);
|
||||
|
||||
return (
|
||||
<input type="hidden" value={cookies.csrftoken} name="csrfmiddlewaretoken" />
|
||||
);
|
||||
}
|
||||
52
Front-End/src/components/DropdownMenu.js
Normal file
52
Front-End/src/components/DropdownMenu.js
Normal file
@ -0,0 +1,52 @@
|
||||
// Composant générique pour les menus dropdown
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const DropdownMenu = ({ buttonContent, items, buttonClassName, menuClassName, dropdownOpen: propDropdownOpen, setDropdownOpen: propSetDropdownOpen }) => {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const menuRef = useRef(null);
|
||||
const router = useRouter();
|
||||
const isControlled = propDropdownOpen !== undefined && propSetDropdownOpen !== undefined;
|
||||
const actualDropdownOpen = isControlled ? propDropdownOpen : dropdownOpen;
|
||||
const actualSetDropdownOpen = isControlled ? propSetDropdownOpen : setDropdownOpen;
|
||||
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
actualSetDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="relative" ref={menuRef}>
|
||||
<button className={buttonClassName} onClick={() => actualSetDropdownOpen(!actualDropdownOpen)}>
|
||||
{buttonContent}
|
||||
</button>
|
||||
{actualDropdownOpen && (
|
||||
<div className={menuClassName}>
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
|
||||
onClick={() => {
|
||||
item.onClick();
|
||||
actualSetDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
{item.icon && <item.icon className="w-4 h-4" />}
|
||||
<span className="flex items-center justify-center">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenu;
|
||||
320
Front-End/src/components/EventModal.js
Normal file
320
Front-End/src/components/EventModal.js
Normal file
@ -0,0 +1,320 @@
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { format } from 'date-fns';
|
||||
import React from 'react';
|
||||
|
||||
export default function EventModal({ isOpen, onClose, eventData, setEventData }) {
|
||||
const { addEvent, updateEvent, deleteEvent, schedules } = usePlanning();
|
||||
|
||||
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
|
||||
];
|
||||
|
||||
const daysOfWeek = [
|
||||
{ value: 1, label: 'Lun' },
|
||||
{ value: 2, label: 'Mar' },
|
||||
{ value: 3, label: 'Mer' },
|
||||
{ value: 4, label: 'Jeu' },
|
||||
{ value: 5, label: 'Ven' },
|
||||
{ value: 6, label: 'Sam' },
|
||||
{ value: 0, label: 'Dim' }
|
||||
];
|
||||
|
||||
// S'assurer que scheduleId est défini lors du premier rendu
|
||||
React.useEffect(() => {
|
||||
if (!eventData.scheduleId && schedules.length > 0) {
|
||||
setEventData(prev => ({
|
||||
...prev,
|
||||
scheduleId: schedules[0].id,
|
||||
color: schedules[0].color
|
||||
}));
|
||||
}
|
||||
}, [schedules, eventData.scheduleId]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!eventData.scheduleId) {
|
||||
alert('Veuillez sélectionner un planning');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSchedule = schedules.find(s => s.id === eventData.scheduleId);
|
||||
|
||||
if (eventData.id) {
|
||||
updateEvent(eventData.id, {
|
||||
...eventData,
|
||||
scheduleId: eventData.scheduleId, // S'assurer que scheduleId 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
|
||||
color: eventData.color || selectedSchedule?.color
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (eventData.id && confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) {
|
||||
deleteEvent(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">
|
||||
{/* Titre */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Titre
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={eventData.title || ''}
|
||||
onChange={(e) => setEventData({ ...eventData, title: e.target.value })}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={eventData.description || ''}
|
||||
onChange={(e) => setEventData({ ...eventData, description: e.target.value })}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Planning */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Planning
|
||||
</label>
|
||||
<select
|
||||
value={eventData.scheduleId || schedules[0]?.id}
|
||||
onChange={(e) => {
|
||||
const selectedSchedule = schedules.find(s => s.id === e.target.value);
|
||||
setEventData({
|
||||
...eventData,
|
||||
scheduleId: e.target.value,
|
||||
color: selectedSchedule?.color || '#10b981'
|
||||
});
|
||||
}}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
required
|
||||
>
|
||||
{schedules.map(schedule => (
|
||||
<option key={schedule.id} value={schedule.id}>
|
||||
{schedule.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Couleur */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Couleur
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
value={eventData.color || schedules.find(s => s.id === eventData.scheduleId)?.color || '#10b981'}
|
||||
onChange={(e) => setEventData({ ...eventData, color: e.target.value })}
|
||||
className="w-full h-10 p-1 rounded border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Récurrence */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Récurrence
|
||||
</label>
|
||||
<select
|
||||
value={eventData.recurrence || 'none'}
|
||||
onChange={(e) => setEventData({ ...eventData, recurrence: e.target.value })}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
>
|
||||
{recurrenceOptions.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</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' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Jours de répétition
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{daysOfWeek.map(day => (
|
||||
<button
|
||||
key={day.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const days = eventData.selectedDays || [];
|
||||
const newDays = days.includes(day.value)
|
||||
? days.filter(d => d !== day.value)
|
||||
: [...days, day.value];
|
||||
setEventData({ ...eventData, selectedDays: newDays });
|
||||
}}
|
||||
className={`px-3 py-1 rounded-full text-sm ${
|
||||
(eventData.selectedDays || []).includes(day.value)
|
||||
? 'bg-emerald-100 text-emerald-800'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date de fin de récurrence */}
|
||||
{eventData.recurrence !== '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 || ''}
|
||||
onChange={(e) => setEventData({ ...eventData, recurrenceEnd: e.target.value })}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
/>
|
||||
</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={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={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>
|
||||
|
||||
{/* 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"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
37
Front-End/src/components/InputPhone.js
Normal file
37
Front-End/src/components/InputPhone.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { isValidPhoneNumber } from 'react-phone-number-input';
|
||||
|
||||
export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className }) {
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const newValue = e.target.value;
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`mb-4 ${className}`}>
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
|
||||
<div className={`flex items-center border-2 border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500 h-8`}>
|
||||
<input
|
||||
type="tel"
|
||||
name={name}
|
||||
ref={inputRef}
|
||||
className="flex-1 pl-2 block w-full sm:text-sm focus:ring-0 h-full rounded-md border-none outline-none"
|
||||
value={typeof value === 'string' ? value : ''}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
21
Front-End/src/components/InputText.js
Normal file
21
Front-End/src/components/InputText.js
Normal file
@ -0,0 +1,21 @@
|
||||
export default function InputText({name, type, label, value, onChange, errorMsg, placeholder,className}) {
|
||||
return (
|
||||
<>
|
||||
<div className={`mb-4 ${className}`}>
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
|
||||
<div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md"
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
25
Front-End/src/components/InputTextIcon.js
Normal file
25
Front-End/src/components/InputTextIcon.js
Normal file
@ -0,0 +1,25 @@
|
||||
export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`mb-4 ${className}`}>
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
|
||||
<div className={`flex items-center border-2 border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500 h-8`}>
|
||||
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full">
|
||||
{IconItem && <IconItem />}
|
||||
</span>
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="flex-1 pl-2 block w-full rounded-r-md sm:text-sm border-none focus:ring-0 outline-none h-full"
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
375
Front-End/src/components/Inscription/InscriptionForm.js
Normal file
375
Front-End/src/components/Inscription/InscriptionForm.js
Normal file
@ -0,0 +1,375 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {BK_GESTIONINSCRIPTION_ELEVES_URL,
|
||||
BK_PROFILE_URL,
|
||||
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL } from '@/utils/Url';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
import { useSearchParams, redirect, useRouter } from 'next/navigation'
|
||||
|
||||
const InscriptionForm = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [eleveNom, setEleveNom] = useState('');
|
||||
const [elevePrenom, setElevePrenom] = useState('');
|
||||
const [responsableEmail, setResponsableEmail] = useState('');
|
||||
const [responsableType, setResponsableType] = useState('new');
|
||||
const [selectedEleve, setSelectedEleve] = useState(null);
|
||||
const [existingResponsables, setExistingResponsables] = useState([]);
|
||||
const [allEleves, setAllEleves] = useState([]);
|
||||
const [selectedResponsables, setSelectedResponsables] = useState([]);
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const request = new Request(
|
||||
`${BK_GESTIONINSCRIPTION_ELEVES_URL}`,
|
||||
{
|
||||
method:'GET',
|
||||
headers: {
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
}
|
||||
);
|
||||
fetch(request).then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
setAllEleves(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.message;
|
||||
console.log(error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const nextStep = () => {
|
||||
if (step < 3) {
|
||||
setStep(step + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (step > 1) {
|
||||
setStep(step - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEleveSelection = (eleve) => {
|
||||
setSelectedEleve(eleve);
|
||||
setExistingResponsables(eleve.responsables);
|
||||
};
|
||||
|
||||
const handleResponsableSelection = (id) => {
|
||||
setSelectedResponsables((prevSelectedResponsables) => {
|
||||
const newSelectedResponsables = new Set(prevSelectedResponsables);
|
||||
if (newSelectedResponsables.has(id)) {
|
||||
newSelectedResponsables.delete(id);
|
||||
} else {
|
||||
newSelectedResponsables.add(id);
|
||||
}
|
||||
return Array.from(newSelectedResponsables);
|
||||
});
|
||||
};
|
||||
|
||||
const resetResponsableEmail = () => {
|
||||
setResponsableEmail('');
|
||||
};
|
||||
|
||||
const submit = function(){
|
||||
if (selectedResponsables.length !== 0) {
|
||||
const selectedResponsablesIds = selectedResponsables.map(responsableId => responsableId)
|
||||
|
||||
const data = {
|
||||
eleve: {
|
||||
nom: eleveNom,
|
||||
prenom: elevePrenom,
|
||||
},
|
||||
idResponsables: selectedResponsablesIds
|
||||
};
|
||||
|
||||
console.log(data);
|
||||
|
||||
const url = `${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}`;
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
|
||||
// Ajouter vérifications sur le retour de la commande (saisies incorrecte, ...)
|
||||
window.location.reload()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Création d'un profil associé à l'adresse mail du responsable saisie
|
||||
// Le profil est inactif
|
||||
const request = new Request(
|
||||
`${BK_PROFILE_URL}`,
|
||||
{
|
||||
method:'POST',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify( {
|
||||
email: responsableEmail,
|
||||
password: 'Provisoire01!',
|
||||
username: responsableEmail,
|
||||
is_active: 0, // On rend le profil inactif : impossible de s'y connecter dans la fenêtre du login tant qu'il ne s'est pas inscrit
|
||||
droit:1
|
||||
}),
|
||||
}
|
||||
);
|
||||
fetch(request).then(response => response.json())
|
||||
.then(response => {
|
||||
console.log('Success:', response);
|
||||
if (response.id) {
|
||||
let idProfil = response.id;
|
||||
|
||||
const data = {
|
||||
eleve: {
|
||||
nom: eleveNom,
|
||||
prenom: elevePrenom,
|
||||
responsables: [
|
||||
{
|
||||
mail: responsableEmail,
|
||||
//telephone: telephoneResponsable,
|
||||
profilAssocie: idProfil // Association entre le reponsable de l'élève et le profil créé par défaut précédemment
|
||||
}
|
||||
],
|
||||
freres: []
|
||||
}
|
||||
};
|
||||
const url = `${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}`;
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
|
||||
// Ajouter vérifications sur le retour de la commande (saisies incorrecte, ...)
|
||||
window.location.reload()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 w-full max-w-4xl bg-white rounded-xl shadow-md space-y-4">
|
||||
{step === 1 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Nouvel élève</h2>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nom de l'élève"
|
||||
value={eleveNom}
|
||||
onChange={(e) => setEleveNom(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Prénom de l'élève"
|
||||
value={elevePrenom}
|
||||
onChange={(e) => setElevePrenom(e.target.value)}
|
||||
className="mt-4 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
|
||||
/>
|
||||
<div className="flex justify-between mt-4">
|
||||
{step > 1 && (
|
||||
<button
|
||||
onClick={prevStep}
|
||||
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md shadow-sm hover:bg-gray-400 focus:outline-none"
|
||||
>
|
||||
Précédent
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{step === 2 && (
|
||||
<div className="mt-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Responsable(s)</h2>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="responsableType"
|
||||
value="new"
|
||||
checked={responsableType === 'new'}
|
||||
onChange={() => setResponsableType('new')}
|
||||
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
|
||||
/>
|
||||
<span className="text-gray-900">Nouveau Responsable</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="responsableType"
|
||||
value="existing"
|
||||
checked={responsableType === 'existing'}
|
||||
onChange={() => {
|
||||
setResponsableType('existing');
|
||||
resetResponsableEmail();
|
||||
}}
|
||||
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
|
||||
/>
|
||||
<span className="text-gray-900">Responsable Existant</span>
|
||||
</label>
|
||||
</div>
|
||||
{responsableType === 'new' && (
|
||||
<div className="mt-4">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email du responsable"
|
||||
value={responsableEmail}
|
||||
onChange={(e) => setResponsableEmail(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{responsableType === 'existing' && (
|
||||
<div className="mt-4">
|
||||
<div className="mt-4" style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||
<table className="min-w-full bg-white border">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 border">Nom</th>
|
||||
<th className="px-4 py-2 border">Prénom</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allEleves.map((eleve, index) => (
|
||||
<tr
|
||||
key={eleve.id}
|
||||
className={`cursor-pointer ${selectedEleve && selectedEleve.id === eleve.id ? 'bg-emerald-600 text-white' : index % 2 === 0 ? 'bg-emerald-100' : ''}`}
|
||||
onClick={() => handleEleveSelection(eleve)}
|
||||
>
|
||||
<td className="px-4 py-2 border">{eleve.nom}</td>
|
||||
<td className="px-4 py-2 border">{eleve.prenom}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{selectedEleve && (
|
||||
<div className="mt-4">
|
||||
<h3 className="font-bold">Responsables associés à {selectedEleve.nom} {selectedEleve.prenom} :</h3>
|
||||
{existingResponsables.map((responsable) => (
|
||||
<div key={responsable.id}>
|
||||
<label className="flex items-center space-x-3 mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedResponsables.includes(responsable.id)}
|
||||
className="form-checkbox h-5 w-5 text-emerald-600"
|
||||
onChange={() => handleResponsableSelection(responsable.id)}
|
||||
/>
|
||||
<span className="text-gray-900">
|
||||
{responsable.nom && responsable.prenom ? `${responsable.nom} ${responsable.prenom}` : `adresse mail : ${responsable.mail}`}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{step === 3 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Récapitulatif</h2>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-semibold">Élève</h3>
|
||||
<p>Nom : {eleveNom}</p>
|
||||
<p>Prénom : {elevePrenom}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-semibold">Responsable(s)</h3>
|
||||
{responsableType === 'new' && (
|
||||
<p>Email du nouveau responsable : {responsableEmail}</p>
|
||||
)}
|
||||
{responsableType === 'existing' && selectedEleve && (
|
||||
<div>
|
||||
<h4 className="font-bold">Responsables associés à {selectedEleve.nom} {selectedEleve.prenom} :</h4>
|
||||
<ul className="list-disc ml-6">
|
||||
{existingResponsables.filter(responsable => selectedResponsables.includes(responsable.id)).map((responsable) => (
|
||||
<li key={responsable.id}>
|
||||
{responsable.nom && responsable.prenom ? `${responsable.nom} ${responsable.prenom}` : responsable.mail}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end mt-4 space-x-4">
|
||||
{step > 1 && (
|
||||
<button
|
||||
onClick={prevStep}
|
||||
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md shadow-sm hover:bg-gray-400 focus:outline-none"
|
||||
>
|
||||
Précédent
|
||||
</button>
|
||||
)}
|
||||
{step < 3 ? (
|
||||
<button
|
||||
onClick={nextStep}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
(step === 1 && (!eleveNom || !elevePrenom)) ||
|
||||
(step === 2 && (responsableType === 'new' && !responsableEmail || responsableType === 'existing' && (!selectedEleve || selectedResponsables.length === 0)))
|
||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
}`}
|
||||
disabled={(step === 1 && (!eleveNom || !elevePrenom)) ||
|
||||
(step === 2 && (responsableType === 'new' && !responsableEmail || responsableType === 'existing' && (!selectedEleve || selectedResponsables.length === 0)))} // Désactive le bouton "Suivant" selon les conditions spécifiées
|
||||
>
|
||||
Suivant
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
<button
|
||||
onClick={submit}
|
||||
className="px-4 py-2 bg-emerald-500 text-white rounded-md shadow-sm hover:bg-emerald-600 focus:outline-none"
|
||||
>
|
||||
Soumettre
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InscriptionForm;
|
||||
181
Front-End/src/components/Inscription/InscriptionFormShared.js
Normal file
181
Front-End/src/components/Inscription/InscriptionFormShared.js
Normal file
@ -0,0 +1,181 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import InputText from '@/components/InputText';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
import ResponsableInputFields from '@/components/Inscription/ResponsableInputFields';
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Button';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
|
||||
|
||||
const niveaux = [
|
||||
{ value:'1', label: 'TPS - Très Petite Section'},
|
||||
{ value:'2', label: 'PS - Petite Section'},
|
||||
{ value:'3', label: 'MS - Moyenne Section'},
|
||||
{ value:'4', label: 'GS - Grande Section'},
|
||||
];
|
||||
|
||||
export default function InscriptionFormShared({
|
||||
initialData,
|
||||
csrfToken,
|
||||
onSubmit,
|
||||
cancelUrl,
|
||||
isLoading = false
|
||||
}) {
|
||||
|
||||
const [formData, setFormData] = useState(() => ({
|
||||
id: initialData?.id || '',
|
||||
nom: initialData?.nom || '',
|
||||
prenom: initialData?.prenom || '',
|
||||
adresse: initialData?.adresse || '',
|
||||
dateNaissance: initialData?.dateNaissance || '',
|
||||
lieuNaissance: initialData?.lieuNaissance || '',
|
||||
codePostalNaissance: initialData?.codePostalNaissance || '',
|
||||
nationalite: initialData?.nationalite || '',
|
||||
medecinTraitant: initialData?.medecinTraitant || '',
|
||||
niveau: initialData?.niveau || ''
|
||||
}));
|
||||
|
||||
const [responsables, setReponsables] = useState(() =>
|
||||
initialData?.responsables || []
|
||||
);
|
||||
|
||||
// Mettre à jour les données quand initialData change
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setFormData({
|
||||
id: initialData.id || '',
|
||||
nom: initialData.nom || '',
|
||||
prenom: initialData.prenom || '',
|
||||
adresse: initialData.adresse || '',
|
||||
dateNaissance: initialData.dateNaissance || '',
|
||||
lieuNaissance: initialData.lieuNaissance || '',
|
||||
codePostalNaissance: initialData.codePostalNaissance || '',
|
||||
nationalite: initialData.nationalite || '',
|
||||
medecinTraitant: initialData.medecinTraitant || '',
|
||||
niveau: initialData.niveau || ''
|
||||
});
|
||||
setReponsables(initialData.responsables || []);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const updateFormField = (field, value) => {
|
||||
setFormData(prev => ({...prev, [field]: value}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit({
|
||||
eleve: {
|
||||
...formData,
|
||||
responsables
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<DjangoCSRFToken csrfToken={csrfToken}/>
|
||||
{/* Section Élève */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-800">Informations de l'élève</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<InputText
|
||||
name="nom"
|
||||
label="Nom"
|
||||
value={formData.nom}
|
||||
onChange={(e) => updateFormField('nom', e.target.value)}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
name="prenom"
|
||||
label="Prénom"
|
||||
value={formData.prenom}
|
||||
onChange={(e) => updateFormField('prenom', e.target.value)}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
name="nationalite"
|
||||
label="Nationalité"
|
||||
value={formData.nationalite}
|
||||
onChange={(e) => updateFormField('nationalite', e.target.value)}
|
||||
/>
|
||||
<InputText
|
||||
name="dateNaissance"
|
||||
type="date"
|
||||
label="Date de Naissance"
|
||||
value={formData.dateNaissance}
|
||||
onChange={(e) => updateFormField('dateNaissance', e.target.value)}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
name="lieuNaissance"
|
||||
label="Lieu de Naissance"
|
||||
value={formData.lieuNaissance}
|
||||
onChange={(e) => updateFormField('lieuNaissance', e.target.value)}
|
||||
/>
|
||||
<InputText
|
||||
name="codePostalNaissance"
|
||||
label="Code Postal de Naissance"
|
||||
value={formData.codePostalNaissance}
|
||||
onChange={(e) => updateFormField('codePostalNaissance', e.target.value)}
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<InputText
|
||||
name="adresse"
|
||||
label="Adresse"
|
||||
value={formData.adresse}
|
||||
onChange={(e) => updateFormField('adresse', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<InputText
|
||||
name="medecinTraitant"
|
||||
label="Médecin Traitant"
|
||||
value={formData.medecinTraitant}
|
||||
onChange={(e) => updateFormField('medecinTraitant', e.target.value)}
|
||||
/>
|
||||
<SelectChoice
|
||||
name="niveau"
|
||||
label="Niveau"
|
||||
selected={formData.niveau}
|
||||
callback={(e) => updateFormField('niveau', e.target.value)}
|
||||
choices={niveaux}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Responsables */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-800">Responsables</h2>
|
||||
<ResponsableInputFields
|
||||
responsables={responsables}
|
||||
onResponsablesChange={(id, field, value) => {
|
||||
const updatedResponsables = responsables.map(resp =>
|
||||
resp.id === id ? { ...resp, [field]: value } : resp
|
||||
);
|
||||
setReponsables(updatedResponsables);
|
||||
}}
|
||||
addResponsible={(e) => {
|
||||
e.preventDefault();
|
||||
setReponsables([...responsables, { id: Date.now() }]);
|
||||
}}
|
||||
deleteResponsable={(index) => {
|
||||
const newArray = [...responsables];
|
||||
newArray.splice(index, 1);
|
||||
setReponsables(newArray);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Boutons de contrôle */}
|
||||
<div className="flex justify-end space-x-4">
|
||||
<Button href={cancelUrl} text="Annuler" />
|
||||
<Button type="submit" text="Valider" primary />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
Front-End/src/components/Inscription/ResponsableInputFields.js
Normal file
103
Front-End/src/components/Inscription/ResponsableInputFields.js
Normal file
@ -0,0 +1,103 @@
|
||||
import InputText from '@/components/InputText';
|
||||
import InputPhone from '@/components/InputPhone';
|
||||
import Button from '@/components/Button';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import 'react-phone-number-input/style.css'
|
||||
|
||||
export default function ResponsableInputFields({responsables, onResponsablesChange, addResponsible, deleteResponsable}) {
|
||||
const t = useTranslations('ResponsableInputFields');
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{responsables.map((item, index) => (
|
||||
<div className="p-6 bg-gray-50 rounded-lg shadow-sm" key={index}>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<h3 className='text-xl font-bold'>{t('responsable')} {index+1}</h3>
|
||||
{responsables.length > 1 && (
|
||||
<Button
|
||||
text={t('delete')}
|
||||
onClick={() => deleteResponsable(index)}
|
||||
className="w-32"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="idResponsable" value={item.id} />
|
||||
|
||||
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'>
|
||||
<InputText
|
||||
name="nomResponsable"
|
||||
type="text"
|
||||
label={t('lastname')}
|
||||
value={item.nom}
|
||||
onChange={(event) => {onResponsablesChange(item.id, "nom", event.target.value)}}
|
||||
/>
|
||||
<InputText
|
||||
name="prenomResponsable"
|
||||
type="text"
|
||||
label={t('firstname')}
|
||||
value={item.prenom}
|
||||
onChange={(event) => {onResponsablesChange(item.id, "prenom", event.target.value)}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'>
|
||||
<InputText
|
||||
name="mailResponsable"
|
||||
type="email"
|
||||
label={t('email')}
|
||||
value={item.mail}
|
||||
onChange={(event) => {onResponsablesChange(item.id, "mail", event.target.value)}}
|
||||
/>
|
||||
<InputPhone
|
||||
name="telephoneResponsable"
|
||||
label={t('phone')}
|
||||
value={item.telephone}
|
||||
onChange={(event) => {onResponsablesChange(item.id, "telephone", event)}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'>
|
||||
<InputText
|
||||
name="dateNaissanceResponsable"
|
||||
type="date"
|
||||
label={t('birthdate')}
|
||||
value={item.dateNaissance}
|
||||
onChange={(event) => {onResponsablesChange(item.id, "dateNaissance", event.target.value)}}
|
||||
/>
|
||||
<InputText
|
||||
name="professionResponsable"
|
||||
type="text"
|
||||
label={t('profession')}
|
||||
value={item.profession}
|
||||
onChange={(event) => {onResponsablesChange(item.id, "profession", event.target.value)}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-4'>
|
||||
<InputText
|
||||
name="adresseResponsable"
|
||||
type="text"
|
||||
label={t('address')}
|
||||
value={item.adresse}
|
||||
onChange={(event) => {onResponsablesChange(item.id, "adresse", event.target.value)}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
text={t('add_responsible')}
|
||||
onClick={(e) => addResponsible(e)}
|
||||
primary
|
||||
icon={<i className="icon profile-add" />}
|
||||
type="button"
|
||||
className="w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
Front-End/src/components/Loader.js
Normal file
9
Front-End/src/components/Loader.js
Normal file
@ -0,0 +1,9 @@
|
||||
const Loader = () => {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
<div className="w-9 h-9 border-4 border-t-4 border-t-emerald-500 border-gray-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
13
Front-End/src/components/Logo.js
Normal file
13
Front-End/src/components/Logo.js
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import logoImage from '@/img/logo_min.svg'; // Assurez-vous que le chemin vers l'image du logo est correct
|
||||
|
||||
const Logo = ({ className }) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Image src={logoImage} alt="Logo" width={150} height={150} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
33
Front-End/src/components/Modal.js
Normal file
33
Front-End/src/components/Modal.js
Normal file
@ -0,0 +1,33 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
|
||||
const Modal = ({ isOpen, setIsOpen, title, ContentComponent }) => {
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
<Dialog.Content className="fixed inset-0 flex items-center justify-center">
|
||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full sm:p-6">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<ContentComponent />
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
className="inline-flex justify-center px-4 py-2 bg-emerald-500 text-white rounded-md shadow-sm hover:bg-emerald-600 focus:outline-none"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Fermer
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
47
Front-End/src/components/Pagination.js
Normal file
47
Front-End/src/components/Pagination.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
|
||||
const t = useTranslations('pagination');
|
||||
const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
|
||||
return (
|
||||
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">{t('page')} {currentPage} {t('of')} {pages.length}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentPage > 1 && (
|
||||
<PaginationButton text={t('previous')} onClick={() => onPageChange(currentPage - 1)}/>
|
||||
)}
|
||||
{pages.map((page) => (
|
||||
<PaginationNumber
|
||||
key={page}
|
||||
number={page}
|
||||
active={page === currentPage}
|
||||
onClick={() => onPageChange(page)}
|
||||
/>
|
||||
|
||||
))}
|
||||
{currentPage < totalPages && (
|
||||
<PaginationButton text={t('next')} onClick={() => onPageChange(currentPage + 1)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PaginationButton = ({ text , onClick}) => (
|
||||
<button className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-50 rounded" onClick={onClick}>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
|
||||
const PaginationNumber = ({ number, active , onClick}) => (
|
||||
<button className={`w-8 h-8 flex items-center justify-center rounded ${
|
||||
active ? 'bg-emerald-500 text-white' : 'text-gray-600 hover:bg-gray-50'
|
||||
}`} onClick={onClick}>
|
||||
{number}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default Pagination;
|
||||
21
Front-End/src/components/Popup.js
Normal file
21
Front-End/src/components/Popup.js
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
const Popup = ({ visible, message, onConfirm, onCancel }) => {
|
||||
if (!visible) return null;
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div className="bg-white p-6 rounded-md shadow-md">
|
||||
<p className="mb-4">{message}</p>
|
||||
<div className="flex justify-end gap-4">
|
||||
<button className="px-4 py-2 bg-gray-200 rounded-md" onClick={onCancel}>Annuler</button>
|
||||
<button className="px-4 py-2 bg-emerald-500 text-white rounded-md" onClick={onConfirm}>Confirmer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default Popup;
|
||||
170
Front-End/src/components/ScheduleNavigation.js
Normal file
170
Front-End/src/components/ScheduleNavigation.js
Normal file
@ -0,0 +1,170 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
20
Front-End/src/components/SelectChoice.js
Normal file
20
Front-End/src/components/SelectChoice.js
Normal file
@ -0,0 +1,20 @@
|
||||
export default function SelectChoice({type, name, label, choices, callback, selected, error }) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
|
||||
<select
|
||||
className={`mt-1 block w-full px-3 py-2 text-base border ${error ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md`}
|
||||
type={type}
|
||||
id={name}
|
||||
name={name}
|
||||
value={selected}
|
||||
onChange={callback}
|
||||
>
|
||||
{choices.map(({ value, label }, index) => <option key={value} value={value}>{label}</option>)}
|
||||
</select>
|
||||
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
55
Front-End/src/components/Sidebar.js
Normal file
55
Front-End/src/components/Sidebar.js
Normal file
@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`flex items-center gap-3 px-2 py-2 rounded-md cursor-pointer ${
|
||||
active ? 'bg-emerald-50 text-emerald-600' : 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Icon size={20} />
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
function Sidebar({ currentPage, items }) {
|
||||
const router = useRouter();
|
||||
const [selectedItem, setSelectedItem] = useState(currentPage);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedItem(currentPage);
|
||||
}, [currentPage]);
|
||||
|
||||
const handleItemClick = (url) => {
|
||||
setSelectedItem(url);
|
||||
router.push(url);
|
||||
};
|
||||
|
||||
return <>
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-white border-r border-gray-200 py-6 px-4">
|
||||
<div className="flex items-center mb-8 px-2">
|
||||
<div className="text-xl font-semibold">Collège Saint-Joseph</div>
|
||||
</div>
|
||||
|
||||
<nav className="space-y-1">
|
||||
{
|
||||
items.map((item) => (
|
||||
<SidebarItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
text={item.name}
|
||||
active={item.id === selectedItem}
|
||||
url={item.url}
|
||||
onClick={() => handleItemClick(item.url)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
59
Front-End/src/components/Slider.js
Normal file
59
Front-End/src/components/Slider.js
Normal file
@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
|
||||
const Slider = ({ min, max, value, onChange }) => {
|
||||
const handleMinChange = (event) => {
|
||||
const newMin = Number(event.target.value);
|
||||
if (newMin < value[1]) {
|
||||
onChange([newMin, value[1]]);
|
||||
} else {
|
||||
onChange([newMin, newMin + 1]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMaxChange = (event) => {
|
||||
const newMax = Number(event.target.value);
|
||||
if (newMax > value[0]) {
|
||||
onChange([value[0], newMax]);
|
||||
} else {
|
||||
onChange([newMax - 1, newMax]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2 w-1/2">
|
||||
<span className="text-emerald-600">{value[0]}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value[0]}
|
||||
onChange={handleMinChange}
|
||||
className="w-full h-2 bg-emerald-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 w-1/2">
|
||||
<span className="text-emerald-600">{value[1]}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={value[0] + 1}
|
||||
max={max}
|
||||
value={value[1]}
|
||||
onChange={handleMaxChange}
|
||||
className="w-full h-2 bg-emerald-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Slider.propTypes = {
|
||||
min: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Slider;
|
||||
87
Front-End/src/components/SpecialitiesSection.js
Normal file
87
Front-End/src/components/SpecialitiesSection.js
Normal file
@ -0,0 +1,87 @@
|
||||
import { BookOpen, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
import Modal from '@/components/Modal';
|
||||
import SpecialityForm from '@/components/SpecialityForm';
|
||||
|
||||
const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDelete }) => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [editingSpeciality, setEditingSpeciality] = useState(null);
|
||||
|
||||
const openEditModal = (speciality) => {
|
||||
setIsOpen(true);
|
||||
setEditingSpeciality(speciality);
|
||||
}
|
||||
|
||||
const closeEditModal = () => {
|
||||
setIsOpen(false);
|
||||
setEditingSpeciality(null);
|
||||
};
|
||||
|
||||
const handleModalSubmit = (updatedData) => {
|
||||
if (editingSpeciality) {
|
||||
handleEdit(editingSpeciality.id, updatedData);
|
||||
} else {
|
||||
handleCreate(updatedData);
|
||||
}
|
||||
closeEditModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-4 max-w-4xl ml-0">
|
||||
<h2 className="text-3xl text-gray-800 flex items-center">
|
||||
<BookOpen className="w-8 h-8 mr-2" />
|
||||
Spécialités
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
|
||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-gray-200 max-w-4xl ml-0">
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'NOM', transform: (row) => row.nom.toUpperCase() },
|
||||
{ name: 'CODE', transform: (row) => (
|
||||
<div
|
||||
className="w-4 h-4 rounded-full mx-auto"
|
||||
style={{ backgroundColor: row.codeCouleur }}
|
||||
title={row.codeCouleur}
|
||||
></div>
|
||||
)},
|
||||
{ name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee },
|
||||
{ name: 'ACTIONS', transform: (row) => (
|
||||
<DropdownMenu
|
||||
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
|
||||
items={[
|
||||
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) },
|
||||
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
|
||||
]
|
||||
}
|
||||
buttonClassName="text-gray-400 hover:text-gray-600"
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
|
||||
/>
|
||||
)}
|
||||
]}
|
||||
data={specialities}
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
title={editingSpeciality ? "Modification de la spécialité" : "Création d'une nouvelle spécialité"} ContentComponent={() => (
|
||||
<SpecialityForm speciality={editingSpeciality || {}} onSubmit={handleModalSubmit} isNew={!editingSpeciality} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecialitiesSection;
|
||||
50
Front-End/src/components/SpecialityForm.js
Normal file
50
Front-End/src/components/SpecialityForm.js
Normal file
@ -0,0 +1,50 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
const SpecialityForm = ({ speciality = {}, onSubmit, isNew }) => {
|
||||
const [nom, setNom] = useState(speciality.nom || '');
|
||||
const [codeCouleur, setCodeCouleur] = useState(speciality.codeCouleur || '#FFFFFF');
|
||||
|
||||
const handleSubmit = () => {
|
||||
const updatedData = {
|
||||
nom,
|
||||
codeCouleur,
|
||||
};
|
||||
onSubmit(updatedData, isNew);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nom de la spécialité"
|
||||
value={nom}
|
||||
onChange={(e) => setNom(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Code couleur de la spécialité
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
value={codeCouleur}
|
||||
onChange={(e) => setCodeCouleur(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 h-10 w-10 p-0 cursor-pointer"
|
||||
style={{ appearance: 'none', borderRadius: '0' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 space-x-4">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="px-4 py-2 bg-emerald-500 text-white rounded-md shadow-sm hover:bg-emerald-600 focus:outline-none"
|
||||
>
|
||||
Soumettre
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecialityForm;
|
||||
59
Front-End/src/components/StatusLabel.js
Normal file
59
Front-End/src/components/StatusLabel.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronUp } from 'lucide-react';
|
||||
import DropdownMenu from './DropdownMenu';
|
||||
|
||||
const StatusLabel = ({ etat, onChange, showDropdown = true }) => {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const statusOptions = [
|
||||
{ value: 1, label: 'Créé' },
|
||||
{ value: 2, label: 'Envoyé' },
|
||||
{ value: 3, label: 'En Validation' },
|
||||
{ value: 4, label: 'A Relancer' },
|
||||
{ value: 5, label: 'Validé' },
|
||||
{ value: 6, label: 'Archivé' },
|
||||
];
|
||||
|
||||
const currentStatus = statusOptions.find(option => option.value === etat);
|
||||
return (
|
||||
<>
|
||||
{showDropdown ? (
|
||||
<DropdownMenu
|
||||
buttonContent={
|
||||
<>
|
||||
{currentStatus ? currentStatus.label : 'Statut inconnu'}
|
||||
<ChevronUp size={16} className={`transform transition-transform duration-200 ${dropdownOpen ? 'rotate-180' : 'rotate-90'}`} />
|
||||
</>
|
||||
}
|
||||
items={statusOptions.map(option => ({
|
||||
label: option.label,
|
||||
onClick: () => onChange(option.value),
|
||||
}))}
|
||||
buttonClassName={`w-[150px] flex items-center justify-center gap-2 px-2 py-2 rounded-md text-sm text-center font-medium ${
|
||||
etat === 1 && 'bg-blue-50 text-blue-600' ||
|
||||
etat === 2 && 'bg-orange-50 text-orange-600' ||
|
||||
etat === 3 && 'bg-purple-50 text-purple-600' ||
|
||||
etat === 4 && 'bg-red-50 text-red-600' ||
|
||||
etat === 5 && 'bg-green-50 text-green-600' ||
|
||||
etat === 6 && 'bg-red-50 text-red-600'
|
||||
}`}
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10"
|
||||
dropdownOpen={dropdownOpen}
|
||||
setDropdownOpen={setDropdownOpen}
|
||||
/>
|
||||
) : (
|
||||
<div className={`w-[150px] flex items-center justify-center gap-2 px-2 py-2 rounded-md text-sm text-center font-medium ${
|
||||
etat === 1 && 'bg-blue-50 text-blue-600' ||
|
||||
etat === 2 && 'bg-orange-50 text-orange-600' ||
|
||||
etat === 3 && 'bg-purple-50 text-purple-600' ||
|
||||
etat === 4 && 'bg-red-50 text-red-600' ||
|
||||
etat === 5 && 'bg-green-50 text-green-600' ||
|
||||
etat === 6 && 'bg-red-50 text-red-600'
|
||||
}`}>
|
||||
{currentStatus ? currentStatus.label : 'Statut inconnu'}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusLabel;
|
||||
10
Front-End/src/components/Tab.js
Normal file
10
Front-End/src/components/Tab.js
Normal file
@ -0,0 +1,10 @@
|
||||
const Tab = ({ text, active, count, onClick}) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`pb-4 px-2 relative ${
|
||||
active ? 'text-emerald-600 border-b-2 border-emerald-600' : 'text-gray-600'
|
||||
}`}>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
export default Tab;
|
||||
9
Front-End/src/components/TabContent.js
Normal file
9
Front-End/src/components/TabContent.js
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
const TabContent = ({ children, isActive }) => (
|
||||
<div className={`${isActive ? 'block' : 'hidden'}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TabContent;
|
||||
58
Front-End/src/components/Table.js
Normal file
58
Front-End/src/components/Table.js
Normal file
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Pagination from '@/components/Pagination'; // Correction du chemin d'importation
|
||||
|
||||
const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange }) => {
|
||||
const handlePageChange = (newPage) => {
|
||||
onPageChange(newPage);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200">
|
||||
<table className="min-w-full bg-white">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<th key={index} className="py-2 px-4 border-b border-gray-200 bg-gray-100 text-center text-sm font-semibold text-gray-600">
|
||||
{column.name}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className={` ${rowIndex % 2 === 0 ? 'bg-emerald-50' : ''}`}>
|
||||
{columns.map((column, colIndex) => (
|
||||
<td key={colIndex} className="py-2 px-4 border-b border-gray-200 text-center text-sm text-gray-700">
|
||||
{renderCell ? renderCell(row, column.name) : column.transform(row)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{itemsPerPage > 0 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Table.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
transform: PropTypes.func.isRequired,
|
||||
})).isRequired,
|
||||
renderCell: PropTypes.func,
|
||||
itemsPerPage: PropTypes.number,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
totalPages: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
111
Front-End/src/components/TeacherForm.js
Normal file
111
Front-End/src/components/TeacherForm.js
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
nom: teacher.nom || '',
|
||||
prenom: teacher.prenom || '',
|
||||
mail: teacher.mail || '',
|
||||
specialite_id: teacher.specialite_id || 1,
|
||||
classes: teacher.classes || []
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type } = e.target;
|
||||
const newValue = type === 'radio' ? parseInt(value) : value;
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[name]: newValue,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(formData, isNew);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Nom
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nom de l'enseignant"
|
||||
name="nom"
|
||||
value={formData.nom}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Prénom
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Prénom de l'enseignant"
|
||||
name="prenom"
|
||||
value={formData.prenom}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Adresse email
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="email de l'enseignant"
|
||||
name="mail"
|
||||
value={formData.mail}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Spécialités
|
||||
</label>
|
||||
<div className="mt-2 grid grid-cols-1 gap-4">
|
||||
{specialities.map(speciality => (
|
||||
<div key={speciality.id} className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
id={`speciality-${speciality.id}`}
|
||||
name="specialite_id"
|
||||
value={speciality.id}
|
||||
checked={formData.specialite_id === speciality.id}
|
||||
onChange={handleChange}
|
||||
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
|
||||
/>
|
||||
<label htmlFor={`speciality-${speciality.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
|
||||
{speciality.nom}
|
||||
<div
|
||||
className="w-4 h-4 rounded-full ml-2"
|
||||
style={{ backgroundColor: speciality.codeCouleur }}
|
||||
title={speciality.codeCouleur}
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 space-x-4">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
(!formData.nom || !formData.prenom || !formData.mail)
|
||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
}`}
|
||||
disabled={(!formData.nom || !formData.prenom || !formData.mail)}
|
||||
>
|
||||
Soumettre
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeacherForm;
|
||||
94
Front-End/src/components/TeachersSection.js
Normal file
94
Front-End/src/components/TeachersSection.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { School, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
import Modal from '@/components/Modal';
|
||||
import TeacherForm from '@/components/TeacherForm';
|
||||
const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, specialities }) => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [editingTeacher, setEditingTeacher] = useState(null);
|
||||
|
||||
const openEditModal = (teacher) => {
|
||||
setIsOpen(true);
|
||||
setEditingTeacher(teacher);
|
||||
}
|
||||
|
||||
const closeEditModal = () => {
|
||||
setIsOpen(false);
|
||||
setEditingTeacher(null);
|
||||
};
|
||||
|
||||
const handleModalSubmit = (updatedData) => {
|
||||
if (editingTeacher) {
|
||||
handleEdit(editingTeacher.id, updatedData);
|
||||
} else {
|
||||
handleCreate(updatedData);
|
||||
}
|
||||
closeEditModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-4 max-w-7xl ml-0">
|
||||
<h2 className="text-3xl text-gray-800 flex items-center">
|
||||
<School className="w-8 h-8 mr-2" />
|
||||
Enseignants
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
|
||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-gray-200 max-w-7xl ml-0">
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'NOM', transform: (row) => row.nom },
|
||||
{ name: 'PRENOM', transform: (row) => row.prenom },
|
||||
{ name: 'MAIL', transform: (row) => row.mail },
|
||||
{ name: 'SPECIALITE',
|
||||
transform: (row) => (
|
||||
<div key={row.id} className="flex justify-center items-center space-x-2">
|
||||
<span
|
||||
key={row.specialite.id}
|
||||
className="w-4 h-4 rounded-full"
|
||||
style={{ backgroundColor: row.specialite.codeCouleur }}
|
||||
title={row.specialite.nom}
|
||||
></span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ name: 'ACTIONS', transform: (row) => (
|
||||
<DropdownMenu
|
||||
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
|
||||
items={[
|
||||
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) },
|
||||
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
|
||||
]
|
||||
}
|
||||
buttonClassName="text-gray-400 hover:text-gray-600"
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
|
||||
/>
|
||||
)}
|
||||
// { name: 'SPECIALITE', transform: (row) => row.specialite_id },
|
||||
// { name: 'CLASSES', transform: (row) => row.classe },
|
||||
]}
|
||||
data={teachers}
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
title={editingTeacher ? "Modification de l'enseignant" : "Création d'un nouvel enseignant"} ContentComponent={() => (
|
||||
<TeacherForm teacher={editingTeacher || {}} onSubmit={handleModalSubmit} isNew={!editingTeacher} specialities={specialities} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeachersSection;
|
||||
34
Front-End/src/components/ToggleView.js
Normal file
34
Front-End/src/components/ToggleView.js
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { CalendarDays, Calendar, CalendarRange, List } from 'lucide-react';
|
||||
|
||||
const ToggleView = ({ viewType, setViewType }) => {
|
||||
const views = [
|
||||
{ type: 'week', label: 'Semaine', icon: CalendarDays },
|
||||
{ type: 'month', label: 'Mois', icon: Calendar },
|
||||
{ type: 'year', label: 'Année', icon: CalendarRange },
|
||||
{ type: 'planning', label: 'Planning', icon: List }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 p-1 rounded-lg flex gap-1">
|
||||
{views.map(({ type, label, icon: Icon }) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setViewType(type)}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-1.5 rounded-md transition-all
|
||||
${viewType === type
|
||||
? 'bg-emerald-600 text-white shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleView;
|
||||
Reference in New Issue
Block a user