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:
Luc SORIGNET
2024-11-18 10:02:58 +01:00
committed by N3WT DE COMPET
commit af0cd1c840
228 changed files with 22694 additions and 0 deletions

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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" />
);
}

View 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;

View 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>
);
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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;

View 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>
);
}

View 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>
);
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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>
);
}

View 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>
</>
)
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -0,0 +1,9 @@
import React from 'react';
const TabContent = ({ children, isActive }) => (
<div className={`${isActive ? 'block' : 'hidden'}`}>
{children}
</div>
);
export default TabContent;

View 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;

View 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;

View 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;

View 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;