mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
chore: application prettier
This commit is contained in:
@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
classeAssocie_id: eleve?.classeAssocie_id || null,
|
||||
});
|
||||
@ -17,10 +16,10 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
|
||||
const handleSubmit = () => {
|
||||
onSubmit({
|
||||
eleve: {
|
||||
...formData
|
||||
...formData,
|
||||
},
|
||||
etat:5
|
||||
});
|
||||
etat: 5,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -30,18 +29,21 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
|
||||
Classes
|
||||
</label>
|
||||
<div className="mt-2 grid grid-cols-1 gap-4">
|
||||
{classes.map(classe => (
|
||||
{classes.map((classe) => (
|
||||
<div key={classe.id} className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
id={`classe-${classe.id}`}
|
||||
name="classeAssocie_id"
|
||||
value={classe.id}
|
||||
checked={formData.classeAssocie_id === classe.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"
|
||||
type="radio"
|
||||
id={`classe-${classe.id}`}
|
||||
name="classeAssocie_id"
|
||||
value={classe.id}
|
||||
checked={formData.classeAssocie_id === classe.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={`classe-${classe.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
|
||||
<label
|
||||
htmlFor={`classe-${classe.id}`}
|
||||
className="ml-2 block text-sm text-gray-900 flex items-center"
|
||||
>
|
||||
{classe.atmosphere_name}
|
||||
</label>
|
||||
</div>
|
||||
@ -50,15 +52,15 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
|
||||
</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.classeAssocie_id )
|
||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
}`}
|
||||
disabled={(!formData.classeAssocie_id)}
|
||||
onClick={handleSubmit}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
!formData.classeAssocie_id
|
||||
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
}`}
|
||||
disabled={!formData.classeAssocie_id}
|
||||
>
|
||||
Associer
|
||||
Associer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -2,11 +2,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">
|
||||
<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}>
|
||||
<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>
|
||||
|
||||
@ -2,28 +2,31 @@ 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 AlertWithModal = ({ title, message, buttonText }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
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>
|
||||
);
|
||||
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;
|
||||
export default AlertWithModal;
|
||||
|
||||
@ -1,37 +1,39 @@
|
||||
|
||||
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}>
|
||||
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 AlphabetLinks = ({ filter, onLetterClick }) => {
|
||||
const [currentLetter, setCurrentLetter] = useState(filter);
|
||||
const alphabet = "*ABCDEFGHIJKLMNOPQRSTUVWXYZ".split('');
|
||||
|
||||
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)}}
|
||||
/>
|
||||
|
||||
|
||||
))}
|
||||
{alphabet.map((letter) => (
|
||||
<AlphabetPaginationNumber
|
||||
key={letter}
|
||||
letter={letter}
|
||||
active={currentLetter === letter}
|
||||
onClick={() => {
|
||||
setCurrentLetter(letter);
|
||||
onLetterClick(letter);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlphabetLinks;
|
||||
export default AlphabetLinks;
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const Button = ({ text, onClick, href, className, primary, icon, disabled}) => {
|
||||
const Button = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className,
|
||||
primary,
|
||||
icon,
|
||||
disabled,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const baseClass = 'px-4 py-2 rounded-md text-white h-8 flex items-center justify-center';
|
||||
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 && !disabled ? primaryClass : secondaryClass} ${className}`;
|
||||
|
||||
@ -6,25 +6,43 @@ 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 {
|
||||
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
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
const { currentDate, setCurrentDate, viewType, setViewType, events, hiddenSchedules } = usePlanning();
|
||||
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 })
|
||||
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
|
||||
label: new Date().getFullYear() - 5 + i,
|
||||
}));
|
||||
|
||||
const handleMonthSelect = (monthIndex) => {
|
||||
@ -39,7 +57,9 @@ const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
|
||||
useEffect(() => {
|
||||
// S'assurer que le filtrage est fait au niveau parent
|
||||
const filtered = events?.filter(event => !hiddenSchedules.includes(event.planning));
|
||||
const filtered = events?.filter(
|
||||
(event) => !hiddenSchedules.includes(event.planning)
|
||||
);
|
||||
setVisibleEvents(filtered);
|
||||
logger.debug('Events filtrés:', filtered); // Debug
|
||||
}, [events, hiddenSchedules]);
|
||||
@ -78,7 +98,10 @@ const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
>
|
||||
Aujourd'hui
|
||||
</button>
|
||||
<button onClick={() => navigateDate('prev')} className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<button
|
||||
onClick={() => navigateDate('prev')}
|
||||
className="p-2 hover:bg-gray-100 rounded-full"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
@ -89,7 +112,11 @@ const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
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 })}
|
||||
{format(
|
||||
currentDate,
|
||||
viewType === 'year' ? 'yyyy' : 'MMMM yyyy',
|
||||
{ locale: fr }
|
||||
)}
|
||||
</h2>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
@ -129,7 +156,10 @@ const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button onClick={() => navigateDate('next')} className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<button
|
||||
onClick={() => navigateDate('next')}
|
||||
className="p-2 hover:bg-gray-100 rounded-full"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
@ -168,7 +198,11 @@ const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
<WeekView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
|
||||
<WeekView
|
||||
onDateClick={onDateClick}
|
||||
onEventClick={onEventClick}
|
||||
events={visibleEvents}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'month' && (
|
||||
@ -179,7 +213,11 @@ const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<MonthView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
|
||||
<MonthView
|
||||
onDateClick={onDateClick}
|
||||
onEventClick={onEventClick}
|
||||
events={visibleEvents}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'year' && (
|
||||
@ -201,7 +239,10 @@ const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<PlanningView onEventClick={onEventClick} events={visibleEvents} />
|
||||
<PlanningView
|
||||
onEventClick={onEventClick}
|
||||
events={visibleEvents}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@ -210,4 +251,4 @@ const Calendar = ({ onDateClick, onEventClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
export default Calendar;
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
import React from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { format, startOfWeek, endOfWeek, eachDayOfInterval, startOfMonth, endOfMonth, isSameMonth, isToday } from 'date-fns';
|
||||
import {
|
||||
format,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
eachDayOfInterval,
|
||||
startOfMonth,
|
||||
endOfMonth,
|
||||
isSameMonth,
|
||||
isToday,
|
||||
} from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getEventsForDate } from '@/utils/events';
|
||||
|
||||
@ -35,7 +44,8 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
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
|
||||
<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' : ''}`}
|
||||
>
|
||||
@ -50,7 +60,7 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
style={{
|
||||
backgroundColor: `${event.color}15`,
|
||||
color: event.color,
|
||||
borderLeft: `2px solid ${event.color}`
|
||||
borderLeft: `2px solid ${event.color}`,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@ -69,8 +79,11 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
<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">
|
||||
{['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>
|
||||
))}
|
||||
@ -83,4 +96,4 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthView;
|
||||
export default MonthView;
|
||||
|
||||
@ -15,17 +15,20 @@ const PlanningView = ({ events, onEventClick }) => {
|
||||
|
||||
// Sinon, créer une entrée pour chaque jour
|
||||
const days = eachDayOfInterval({ start, end });
|
||||
return days.map(day => ({
|
||||
return days.map((day) => ({
|
||||
...event,
|
||||
displayDate: day,
|
||||
isMultiDay: true
|
||||
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));
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.displayDate || a.start) - new Date(b.displayDate || b.start)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white h-full overflow-auto">
|
||||
@ -58,26 +61,29 @@ const PlanningView = ({ events, onEventClick }) => {
|
||||
<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>
|
||||
<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
|
||||
<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')}`}
|
||||
{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">
|
||||
@ -108,4 +114,4 @@ const PlanningView = ({ events, onEventClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanningView;
|
||||
export default PlanningView;
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { format, startOfWeek, addDays, differenceInMinutes, isSameDay } from 'date-fns';
|
||||
import {
|
||||
format,
|
||||
startOfWeek,
|
||||
addDays,
|
||||
differenceInMinutes,
|
||||
isSameDay,
|
||||
} from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getWeekEvents } from '@/utils/events';
|
||||
import { isToday } from 'date-fns';
|
||||
@ -16,7 +22,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
|
||||
|
||||
// Maintenant on peut utiliser weekDays
|
||||
const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date()));
|
||||
const isCurrentWeek = weekDays.some((day) => isSameDay(day, new Date()));
|
||||
|
||||
// Mettre à jour la position de la ligne toutes les minutes
|
||||
useEffect(() => {
|
||||
@ -61,7 +67,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const eventStart = new Date(event.start);
|
||||
const eventEnd = new Date(event.end);
|
||||
|
||||
return dayEvents.filter(otherEvent => {
|
||||
return dayEvents.filter((otherEvent) => {
|
||||
if (otherEvent.id === event.id) return false;
|
||||
const otherStart = new Date(otherEvent.start);
|
||||
const otherEnd = new Date(otherEvent.end);
|
||||
@ -77,7 +83,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
|
||||
// Trouver les événements qui se chevauchent
|
||||
const overlappingEvents = findOverlappingEvents(event, dayEvents);
|
||||
const eventIndex = overlappingEvents.findIndex(e => e.id > event.id) + 1;
|
||||
const eventIndex = overlappingEvents.findIndex((e) => e.id > event.id) + 1;
|
||||
const totalOverlapping = overlappingEvents.length + 1;
|
||||
|
||||
// Calculer la largeur et la position horizontale
|
||||
@ -93,7 +99,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
borderLeft: `3px solid ${event.color}`,
|
||||
borderRadius: '0.25rem',
|
||||
zIndex: 1,
|
||||
transform: `translateY(${startMinutes}rem)`
|
||||
transform: `translateY(${startMinutes}rem)`,
|
||||
};
|
||||
};
|
||||
|
||||
@ -111,14 +117,24 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
}}
|
||||
>
|
||||
<div className="p-1">
|
||||
<div className="font-semibold text-xs truncate" style={{ color: event.color }}>
|
||||
<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
|
||||
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 }}>
|
||||
<div
|
||||
className="text-xs truncate"
|
||||
style={{ color: event.color, opacity: 0.75 }}
|
||||
>
|
||||
{event.location}
|
||||
</div>
|
||||
)}
|
||||
@ -130,7 +146,10 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
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="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
|
||||
@ -142,8 +161,10 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
<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' : ''}`}>
|
||||
<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>
|
||||
@ -158,17 +179,17 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
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 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 => (
|
||||
<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`}
|
||||
@ -188,11 +209,15 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
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 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>
|
||||
);
|
||||
@ -205,4 +230,4 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default WeekView;
|
||||
export default WeekView;
|
||||
|
||||
@ -37,7 +37,7 @@ const YearView = ({ onDateClick }) => {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4 p-4">
|
||||
{months.map(month => (
|
||||
{months.map((month) => (
|
||||
<MonthCard
|
||||
key={month.getTime()}
|
||||
month={month}
|
||||
@ -49,4 +49,4 @@ const YearView = ({ onDateClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default YearView;
|
||||
export default YearView;
|
||||
|
||||
@ -1,13 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
const CheckBox = ({ item, formData, handleChange, fieldName, itemLabelFunc = () => null, labelAttenuated = () => false, horizontal }) => {
|
||||
const CheckBox = ({
|
||||
item,
|
||||
formData,
|
||||
handleChange,
|
||||
fieldName,
|
||||
itemLabelFunc = () => null,
|
||||
labelAttenuated = () => false,
|
||||
horizontal,
|
||||
}) => {
|
||||
const isChecked = formData[fieldName].includes(parseInt(item.id));
|
||||
const isAttenuated = labelAttenuated(item) && !isChecked;
|
||||
return (
|
||||
<div key={item.id} className={`flex ${horizontal ? 'flex-col items-center' : 'flex-row items-center'}`}>
|
||||
<div
|
||||
key={item.id}
|
||||
className={`flex ${horizontal ? 'flex-col items-center' : 'flex-row items-center'}`}
|
||||
>
|
||||
{horizontal && (
|
||||
<label
|
||||
htmlFor={`${fieldName}-${item.id}`}
|
||||
<label
|
||||
htmlFor={`${fieldName}-${item.id}`}
|
||||
className={`block text-sm text-center mb-1 ${isAttenuated ? 'text-gray-300' : 'font-bold text-emerald-600'}`}
|
||||
>
|
||||
{itemLabelFunc(item)}
|
||||
@ -24,8 +35,8 @@ const CheckBox = ({ item, formData, handleChange, fieldName, itemLabelFunc = ()
|
||||
style={{ borderRadius: '6px', outline: 'none', boxShadow: 'none' }}
|
||||
/>
|
||||
{!horizontal && (
|
||||
<label
|
||||
htmlFor={`${fieldName}-${item.id}`}
|
||||
<label
|
||||
htmlFor={`${fieldName}-${item.id}`}
|
||||
className={`block text-sm ${isAttenuated ? 'text-gray-300' : 'font-bold text-emerald-600'}`}
|
||||
>
|
||||
{itemLabelFunc(item)}
|
||||
@ -35,4 +46,4 @@ const CheckBox = ({ item, formData, handleChange, fieldName, itemLabelFunc = ()
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckBox;
|
||||
export default CheckBox;
|
||||
|
||||
@ -11,7 +11,7 @@ const CheckBoxList = ({
|
||||
className,
|
||||
itemLabelFunc = (item) => item.name,
|
||||
labelAttenuated = () => false,
|
||||
horizontal = false // Ajouter l'option horizontal
|
||||
horizontal = false, // Ajouter l'option horizontal
|
||||
}) => {
|
||||
return (
|
||||
<div className={`mb-4 w-full ${className}`}>
|
||||
@ -19,8 +19,10 @@ const CheckBoxList = ({
|
||||
{Icon && <Icon className="w-5 h-5 mr-2" />}
|
||||
{label}
|
||||
</label>
|
||||
<div className={`mt-2 grid ${horizontal ? 'grid-cols-6 gap-2' : 'grid-cols-1 gap-4'}`}>
|
||||
{items.map(item => (
|
||||
<div
|
||||
className={`mt-2 grid ${horizontal ? 'grid-cols-6 gap-2' : 'grid-cols-1 gap-4'}`}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<CheckBox
|
||||
key={`${fieldName}-${item.id}`}
|
||||
item={item}
|
||||
|
||||
@ -5,7 +5,7 @@ import { GraduationCap } from 'lucide-react';
|
||||
const ClasseDetails = ({ classe }) => {
|
||||
if (!classe) return null;
|
||||
|
||||
const nombreElevesInscrits = classe?.eleves?.length||0;
|
||||
const nombreElevesInscrits = classe?.eleves?.length || 0;
|
||||
const capaciteTotale = classe.number_of_students;
|
||||
const pourcentage = Math.round((nombreElevesInscrits / capaciteTotale) * 100);
|
||||
|
||||
@ -42,14 +42,12 @@ const ClasseDetails = ({ classe }) => {
|
||||
{nombreElevesInscrits}/{capaciteTotale}
|
||||
</span>
|
||||
<div className="w-32 bg-gray-200 rounded-full h-6 shadow-inner">
|
||||
<div
|
||||
<div
|
||||
className={`h-full rounded-full ${getColor(pourcentage)}`}
|
||||
style={{ width: `${pourcentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="ml-4 font-bold text-gray-700">
|
||||
{pourcentage}%
|
||||
</span>
|
||||
<span className="ml-4 font-bold text-gray-700">{pourcentage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -60,7 +58,7 @@ const ClasseDetails = ({ classe }) => {
|
||||
columns={[
|
||||
{ name: 'NOM', transform: (row) => row.name },
|
||||
{ name: 'PRENOM', transform: (row) => row.first_name },
|
||||
{ name: 'AGE', transform: (row) => `${row.age}` }
|
||||
{ name: 'AGE', transform: (row) => `${row.age}` },
|
||||
]}
|
||||
data={classe.students}
|
||||
/>
|
||||
|
||||
@ -2,8 +2,8 @@ import React from 'react';
|
||||
|
||||
const LevelLabel = ({ label, index }) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
<div
|
||||
key={index}
|
||||
className={`ml-2 px-3 py-1 rounded-md shadow-sm ${
|
||||
index % 2 === 0 ? 'bg-white' : 'bg-gray-100'
|
||||
} border border-gray-200 text-gray-700`}
|
||||
|
||||
@ -2,13 +2,15 @@ import React from 'react';
|
||||
|
||||
const TeacherLabel = ({ nom, prenom, index }) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
<div
|
||||
key={index}
|
||||
className={`ml-2 px-3 py-1 rounded-md shadow-sm ${
|
||||
index % 2 === 0 ? 'bg-white' : 'bg-gray-100'
|
||||
} border border-gray-200 text-gray-700`}
|
||||
>
|
||||
<span className="font-bold">{nom} {prenom}</span>
|
||||
<span className="font-bold">
|
||||
{nom} {prenom}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,9 +2,17 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Check } from 'lucide-react';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
const DateTab = ({ dates, activeTab, handleDateChange, handleEdit, type, paymentPlanId, resetModifiedDates }) => {
|
||||
const DateTab = ({
|
||||
dates,
|
||||
activeTab,
|
||||
handleDateChange,
|
||||
handleEdit,
|
||||
type,
|
||||
paymentPlanId,
|
||||
resetModifiedDates,
|
||||
}) => {
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [modifiedDates, setModifiedDates] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
@ -16,22 +24,24 @@ const DateTab = ({ dates, activeTab, handleDateChange, handleEdit, type, payment
|
||||
const submit = (updatedData) => {
|
||||
const dataWithType = {
|
||||
...updatedData,
|
||||
type: type
|
||||
type: type,
|
||||
};
|
||||
handleEdit(paymentPlanId, dataWithType)
|
||||
.then(() => {
|
||||
setPopupMessage(`Mise à jour de la date d'échéance effectuée avec succès`);
|
||||
setPopupMessage(
|
||||
`Mise à jour de la date d'échéance effectuée avec succès`
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setModifiedDates({});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDateChangeWithModification = (tab, index, value) => {
|
||||
handleDateChange(tab, index, value);
|
||||
setModifiedDates(prev => ({ ...prev, [`${tab}-${index}`]: true }));
|
||||
setModifiedDates((prev) => ({ ...prev, [`${tab}-${index}`]: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -39,17 +49,30 @@ const DateTab = ({ dates, activeTab, handleDateChange, handleEdit, type, payment
|
||||
<div className="flex flex-col space-y-3">
|
||||
{dates[activeTab]?.map((date, index) => (
|
||||
<div key={index} className="flex items-center space-x-3">
|
||||
<span className="text-emerald-700 font-semibold">Échéance {index + 1}</span>
|
||||
<span className="text-emerald-700 font-semibold">
|
||||
Échéance {index + 1}
|
||||
</span>
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => handleDateChangeWithModification(activeTab, index, e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleDateChangeWithModification(
|
||||
activeTab,
|
||||
index,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="p-2 border border-emerald-300 rounded focus:outline-none focus:ring-2 focus:ring-emerald-500 cursor-pointer"
|
||||
/>
|
||||
{modifiedDates[`${activeTab}-${index}`] && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit({ frequency: dates[activeTab].length, due_dates: dates[activeTab] })}
|
||||
onClick={() =>
|
||||
submit({
|
||||
frequency: dates[activeTab].length,
|
||||
due_dates: dates[activeTab],
|
||||
})
|
||||
}
|
||||
className="text-emerald-500 hover:text-emerald-800"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
@ -71,4 +94,4 @@ const DateTab = ({ dates, activeTab, handleDateChange, handleEdit, type, payment
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTab;
|
||||
export default DateTab;
|
||||
|
||||
@ -2,15 +2,15 @@ import React, { useEffect } from 'react';
|
||||
import { useCookies } from 'react-cookie';
|
||||
|
||||
export default function DjangoCSRFToken({ csrfToken }) {
|
||||
const [cookies, setCookie] = useCookies(['csrftoken']);
|
||||
const [cookies, setCookie] = useCookies(['csrftoken']);
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken && csrfToken !== cookies.csrftoken) {
|
||||
setCookie('csrftoken', csrfToken, { path: '/' });
|
||||
}
|
||||
}, [csrfToken, cookies.csrftoken, setCookie]);
|
||||
useEffect(() => {
|
||||
if (csrfToken && csrfToken !== cookies.csrftoken) {
|
||||
setCookie('csrftoken', csrfToken, { path: '/' });
|
||||
}
|
||||
}, [csrfToken, cookies.csrftoken, setCookie]);
|
||||
|
||||
return (
|
||||
<input type="hidden" value={cookies.csrftoken} name="csrfmiddlewaretoken" />
|
||||
);
|
||||
return (
|
||||
<input type="hidden" value={cookies.csrftoken} name="csrfmiddlewaretoken" />
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,4 +14,4 @@ const DocusealBuilder = ({ onSave, onSend, ...props }) => {
|
||||
return <OriginalDocusealBuilder {...props} />;
|
||||
};
|
||||
|
||||
export default DocusealBuilder;
|
||||
export default DocusealBuilder;
|
||||
|
||||
@ -4,7 +4,6 @@ import { Upload } from 'lucide-react';
|
||||
export default function DraggableFileUpload({ fileName, onFileSelect }) {
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
event.preventDefault();
|
||||
setDragActive(true);
|
||||
@ -39,12 +38,23 @@ export default function DraggableFileUpload({ fileName, onFileSelect }) {
|
||||
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`}
|
||||
style={{ height: '200px' }}
|
||||
>
|
||||
<input type="file" onChange={handleFileChange} className="hidden" id="fileInput" />
|
||||
<label htmlFor="fileInput" className="cursor-pointer flex flex-col items-center">
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
id="fileInput"
|
||||
/>
|
||||
<label
|
||||
htmlFor="fileInput"
|
||||
className="cursor-pointer flex flex-col items-center"
|
||||
>
|
||||
<Upload size={48} className="text-gray-400 mb-2" />
|
||||
<p className="text-center">{fileName || 'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}</p>
|
||||
<p className="text-center">
|
||||
{fileName ||
|
||||
'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,13 +2,23 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const DropdownMenu = ({ buttonContent, items, buttonClassName, menuClassName, dropdownOpen: propDropdownOpen, setDropdownOpen: propSetDropdownOpen }) => {
|
||||
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 isControlled =
|
||||
propDropdownOpen !== undefined && propSetDropdownOpen !== undefined;
|
||||
const actualDropdownOpen = isControlled ? propDropdownOpen : dropdownOpen;
|
||||
const actualSetDropdownOpen = isControlled ? propSetDropdownOpen : setDropdownOpen;
|
||||
const actualSetDropdownOpen = isControlled
|
||||
? propSetDropdownOpen
|
||||
: setDropdownOpen;
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
@ -52,7 +62,10 @@ const DropdownMenu = ({ buttonContent, items, buttonClassName, menuClassName, dr
|
||||
|
||||
return (
|
||||
<div className="relative" ref={menuRef}>
|
||||
<button className={buttonClassName} onClick={() => actualSetDropdownOpen(!actualDropdownOpen)}>
|
||||
<button
|
||||
className={buttonClassName}
|
||||
onClick={() => actualSetDropdownOpen(!actualDropdownOpen)}
|
||||
>
|
||||
{buttonContent}
|
||||
</button>
|
||||
{actualDropdownOpen && (
|
||||
@ -64,4 +77,4 @@ const DropdownMenu = ({ buttonContent, items, buttonClassName, menuClassName, dr
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenu;
|
||||
export default DropdownMenu;
|
||||
|
||||
@ -2,16 +2,22 @@ import { usePlanning } from '@/context/PlanningContext';
|
||||
import { format } from 'date-fns';
|
||||
import React from 'react';
|
||||
|
||||
export default function EventModal({ isOpen, onClose, eventData, setEventData }) {
|
||||
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } = usePlanning();
|
||||
export default function EventModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
eventData,
|
||||
setEventData,
|
||||
}) {
|
||||
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } =
|
||||
usePlanning();
|
||||
|
||||
// S'assurer que planning est défini lors du premier rendu
|
||||
React.useEffect(() => {
|
||||
if (!eventData.planning && schedules.length > 0) {
|
||||
setEventData(prev => ({
|
||||
setEventData((prev) => ({
|
||||
...prev,
|
||||
planning: schedules[0].id,
|
||||
color: schedules[0].color
|
||||
color: schedules[0].color,
|
||||
}));
|
||||
}
|
||||
}, [schedules, eventData.planning]);
|
||||
@ -23,7 +29,7 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
{ value: 'daily', label: 'Quotidienne' },
|
||||
{ value: 'weekly', label: 'Hebdomadaire' },
|
||||
{ value: 'monthly', label: 'Mensuelle' },
|
||||
{ value: 'custom', label: 'Personnalisée' } // Nouvelle option
|
||||
{ value: 'custom', label: 'Personnalisée' }, // Nouvelle option
|
||||
];
|
||||
|
||||
const daysOfWeek = [
|
||||
@ -33,7 +39,7 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
{ value: 4, label: 'Jeu' },
|
||||
{ value: 5, label: 'Ven' },
|
||||
{ value: 6, label: 'Sam' },
|
||||
{ value: 0, label: 'Dim' }
|
||||
{ value: 0, label: 'Dim' },
|
||||
];
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
@ -44,27 +50,30 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSchedule = schedules.find(s => s.id === eventData.planning);
|
||||
const selectedSchedule = schedules.find((s) => s.id === eventData.planning);
|
||||
|
||||
if (eventData.id) {
|
||||
handleUpdateEvent(eventData.id, {
|
||||
...eventData,
|
||||
planning: eventData.planning, // S'assurer que planning est bien défini
|
||||
color: eventData.color || selectedSchedule?.color
|
||||
color: eventData.color || selectedSchedule?.color,
|
||||
});
|
||||
} else {
|
||||
addEvent({
|
||||
...eventData,
|
||||
id: `event-${Date.now()}`,
|
||||
planning: eventData.planning, // S'assurer que planning est bien défini
|
||||
color: eventData.color || selectedSchedule?.color
|
||||
color: eventData.color || selectedSchedule?.color,
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (eventData.id && confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) {
|
||||
if (
|
||||
eventData.id &&
|
||||
confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')
|
||||
) {
|
||||
handleDeleteEvent(eventData.id);
|
||||
onClose();
|
||||
}
|
||||
@ -74,7 +83,7 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
<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'}
|
||||
{eventData.id ? "Modifier l'événement" : 'Nouvel événement'}
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
@ -86,7 +95,9 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
<input
|
||||
type="text"
|
||||
value={eventData.title || ''}
|
||||
onChange={(e) => setEventData({ ...eventData, title: e.target.value })}
|
||||
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
|
||||
/>
|
||||
@ -99,7 +110,9 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
</label>
|
||||
<textarea
|
||||
value={eventData.description || ''}
|
||||
onChange={(e) => setEventData({ ...eventData, description: e.target.value })}
|
||||
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"
|
||||
/>
|
||||
@ -113,17 +126,19 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
<select
|
||||
value={eventData.planning || schedules[0]?.id}
|
||||
onChange={(e) => {
|
||||
const selectedSchedule = schedules.find(s => s.id === e.target.value);
|
||||
const selectedSchedule = schedules.find(
|
||||
(s) => s.id === e.target.value
|
||||
);
|
||||
setEventData({
|
||||
...eventData,
|
||||
planning: e.target.value,
|
||||
color: selectedSchedule?.color || '#10b981'
|
||||
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 => (
|
||||
{schedules.map((schedule) => (
|
||||
<option key={schedule.id} value={schedule.id}>
|
||||
{schedule.name}
|
||||
</option>
|
||||
@ -138,8 +153,14 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
value={eventData.color || schedules.find(s => s.id === eventData.planning)?.color || '#10b981'}
|
||||
onChange={(e) => setEventData({ ...eventData, color: e.target.value })}
|
||||
value={
|
||||
eventData.color ||
|
||||
schedules.find((s) => s.id === eventData.planning)?.color ||
|
||||
'#10b981'
|
||||
}
|
||||
onChange={(e) =>
|
||||
setEventData({ ...eventData, color: e.target.value })
|
||||
}
|
||||
className="w-full h-10 p-1 rounded border"
|
||||
/>
|
||||
</div>
|
||||
@ -151,10 +172,12 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
</label>
|
||||
<select
|
||||
value={eventData.recurrence || 'none'}
|
||||
onChange={(e) => setEventData({ ...eventData, recurrence: e.target.value })}
|
||||
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 => (
|
||||
{recurrenceOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
@ -174,18 +197,22 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
type="number"
|
||||
min="1"
|
||||
value={eventData.customInterval || 1}
|
||||
onChange={(e) => setEventData({
|
||||
...eventData,
|
||||
customInterval: parseInt(e.target.value) || 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
|
||||
})}
|
||||
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>
|
||||
@ -204,14 +231,14 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
Jours de répétition
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{daysOfWeek.map(day => (
|
||||
{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.filter((d) => d !== day.value)
|
||||
: [...days, day.value];
|
||||
setEventData({ ...eventData, selectedDays: newDays });
|
||||
}}
|
||||
@ -237,7 +264,9 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
<input
|
||||
type="date"
|
||||
value={eventData.recurrenceEnd || ''}
|
||||
onChange={(e) => setEventData({ ...eventData, recurrenceEnd: e.target.value })}
|
||||
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>
|
||||
@ -252,7 +281,12 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
<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() })}
|
||||
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
|
||||
/>
|
||||
@ -264,7 +298,12 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
<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() })}
|
||||
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
|
||||
/>
|
||||
@ -279,7 +318,9 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
<input
|
||||
type="text"
|
||||
value={eventData.location || ''}
|
||||
onChange={(e) => setEventData({ ...eventData, location: e.target.value })}
|
||||
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>
|
||||
@ -317,4 +358,4 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,14 +8,14 @@ const FileStatusLabel = ({ status }) => {
|
||||
return {
|
||||
label: 'En attente',
|
||||
className: 'bg-green-50 text-green-600',
|
||||
icon: <Check size={16} className="text-green-600" />
|
||||
icon: <Check size={16} className="text-green-600" />,
|
||||
};
|
||||
case 'pending':
|
||||
default:
|
||||
return {
|
||||
label: 'En attente',
|
||||
className: 'bg-orange-50 text-orange-600',
|
||||
icon: <Clock size={16} className="text-orange-600" />
|
||||
icon: <Clock size={16} className="text-orange-600" />,
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -23,7 +23,9 @@ const FileStatusLabel = ({ status }) => {
|
||||
const { label, className, icon } = getStatusConfig();
|
||||
|
||||
return (
|
||||
<div className={`flex items-center justify-center gap-2 px-3 py-1 rounded-md text-sm font-medium ${className}`}>
|
||||
<div
|
||||
className={`flex items-center justify-center gap-2 px-3 py-1 rounded-md text-sm font-medium ${className}`}
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,9 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
useEffect(() => {
|
||||
fetchRegistrationFileGroups(selectedEstablishmentId).then(data => setGroups(data));
|
||||
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
|
||||
setGroups(data)
|
||||
);
|
||||
|
||||
if (fileToEdit) {
|
||||
setFileName(fileToEdit.name || '');
|
||||
@ -35,7 +37,7 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
|
||||
name: fileName,
|
||||
is_required: isRequired,
|
||||
order: parseInt(order, 10),
|
||||
groupId: selectedGroup || null
|
||||
groupId: selectedGroup || null,
|
||||
});
|
||||
setFile(null);
|
||||
setFileName('');
|
||||
@ -50,7 +52,7 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
|
||||
fileName={fileName}
|
||||
onFileSelect={(selectedFile) => {
|
||||
setFile(selectedFile);
|
||||
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ""));
|
||||
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ''));
|
||||
}}
|
||||
/>
|
||||
<div className="flex mt-2">
|
||||
@ -70,8 +72,8 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
|
||||
/>
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
className={`p-2 rounded-md shadow transition duration-200 ml-2 ${fileName !== "" ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
|
||||
disabled={fileName === ""}
|
||||
className={`p-2 rounded-md shadow transition duration-200 ml-2 ${fileName !== '' ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
|
||||
disabled={fileName === ''}
|
||||
>
|
||||
Ajouter
|
||||
</button>
|
||||
@ -91,11 +93,13 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
|
||||
className="w-full border rounded p-2"
|
||||
>
|
||||
<option value="">Aucun groupe</option>
|
||||
{groups.map(group => (
|
||||
<option key={group.id} value={group.id}>{group.name}</option>
|
||||
{groups.map((group) => (
|
||||
<option key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import Logo from '@/components/Logo';
|
||||
|
||||
export default function Footer ({softwareName, softwareVersion}) {
|
||||
|
||||
return (
|
||||
export default function Footer({ softwareName, softwareVersion }) {
|
||||
return (
|
||||
<footer className="h-16 bg-white border-t border-gray-200 px-8 py-4 flex items-center justify-between">
|
||||
<div className="text-sm font-light">
|
||||
<span>© {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.</span>
|
||||
</div>
|
||||
<div className="text-sm font-light flex items-center justify-between">
|
||||
<div className="text-sm font-light mr-4">{softwareName} - {softwareVersion}</div>
|
||||
<Logo className="w-8 h-8" />
|
||||
<div className="text-sm font-light">
|
||||
<span>
|
||||
© {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm font-light flex items-center justify-between">
|
||||
<div className="text-sm font-light mr-4">
|
||||
{softwareName} - {softwareVersion}
|
||||
</div>
|
||||
<Logo className="w-8 h-8" />
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,31 +2,37 @@ import React from 'react';
|
||||
import { PhoneInput } from 'react-international-phone';
|
||||
import 'react-international-phone/style.css';
|
||||
|
||||
export default function InputPhone({ name, label, value, onChange, errorMsg, className, required }) {
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
export default function InputPhone({
|
||||
name,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
errorMsg,
|
||||
className,
|
||||
required,
|
||||
}) {
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
|
||||
<PhoneInput
|
||||
defaultCountry="fr"
|
||||
value={value}
|
||||
onChange={(phone) => onChange(phone)}
|
||||
inputProps={{
|
||||
name: name,
|
||||
required: required,
|
||||
}}
|
||||
className="!w-full mt-1 !h-[38px]"
|
||||
containerClassName="!w-full !h-[36px] !flex !items-center !rounded-md"
|
||||
inputClassName={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none !rounded-r-md !outline-none items-center !border !border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500` }
|
||||
buttonClassName="!h-[38px] !flex !items-center !justify-center !rounded-l-md !border border-gray-200 !border-r-0"
|
||||
/>
|
||||
<PhoneInput
|
||||
defaultCountry="fr"
|
||||
value={value}
|
||||
onChange={(phone) => onChange(phone)}
|
||||
inputProps={{
|
||||
name: name,
|
||||
required: required,
|
||||
}}
|
||||
className="!w-full mt-1 !h-[38px]"
|
||||
containerClassName="!w-full !h-[36px] !flex !items-center !rounded-md"
|
||||
inputClassName={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none !rounded-r-md !outline-none items-center !border !border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}
|
||||
buttonClassName="!h-[38px] !flex !items-center !justify-center !rounded-l-md !border border-gray-200 !border-r-0"
|
||||
/>
|
||||
|
||||
{errorMsg && (
|
||||
<p className="mt-2 text-sm text-red-600">{errorMsg}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,25 +1,40 @@
|
||||
export default function InputText({name, type, label, value, onChange, errorMsg, placeholder, className, required}) {
|
||||
return (
|
||||
<>
|
||||
<div className={`${className}`}>
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</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"
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default function InputText({
|
||||
name,
|
||||
type,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
errorMsg,
|
||||
placeholder,
|
||||
className,
|
||||
required,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className={`${className}`}>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</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"
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,24 +1,41 @@
|
||||
export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) {
|
||||
return (
|
||||
<>
|
||||
<div className={`${className}`}>
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
|
||||
<div className={`mt-1 flex items-stretch border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
|
||||
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
|
||||
{IconItem && <IconItem />}
|
||||
</span>
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="flex-1 px-3 py-2 block w-full rounded-r-md sm:text-sm border-none focus:ring-0 outline-none"
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default function InputTextIcon({
|
||||
name,
|
||||
type,
|
||||
IconItem,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
errorMsg,
|
||||
placeholder,
|
||||
className,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className={`${className}`}>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<div
|
||||
className={`mt-1 flex items-stretch border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}
|
||||
>
|
||||
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
|
||||
{IconItem && <IconItem />}
|
||||
</span>
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="flex-1 px-3 py-2 block w-full rounded-r-md sm:text-sm border-none focus:ring-0 outline-none"
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Palette } from 'lucide-react';
|
||||
|
||||
const InputTextWithColorIcon = ({ name, textValue, colorValue, onTextChange, onColorChange, placeholder, errorMsg }) => {
|
||||
const InputTextWithColorIcon = ({
|
||||
name,
|
||||
textValue,
|
||||
colorValue,
|
||||
onTextChange,
|
||||
onColorChange,
|
||||
placeholder,
|
||||
errorMsg,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
@ -31,4 +39,4 @@ const InputTextWithColorIcon = ({ name, textValue, colorValue, onTextChange, onC
|
||||
);
|
||||
};
|
||||
|
||||
export default InputTextWithColorIcon;
|
||||
export default InputTextWithColorIcon;
|
||||
|
||||
@ -2,17 +2,19 @@ import React from 'react';
|
||||
import Table from '@/components/Table';
|
||||
|
||||
export default function FilesToSign({ fileTemplates, columns }) {
|
||||
return (
|
||||
<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">Fichiers à remplir</h2>
|
||||
<Table
|
||||
data={fileTemplates}
|
||||
columns={columns}
|
||||
itemsPerPage={5}
|
||||
currentPage={1}
|
||||
totalPages={1}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<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">
|
||||
Fichiers à remplir
|
||||
</h2>
|
||||
<Table
|
||||
data={fileTemplates}
|
||||
columns={columns}
|
||||
itemsPerPage={5}
|
||||
currentPage={1}
|
||||
totalPages={1}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,17 +2,19 @@ import React from 'react';
|
||||
import Table from '@/components/Table';
|
||||
|
||||
export default function FilesToUpload({ fileTemplates, columns }) {
|
||||
return (
|
||||
<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">Fichiers à uploader</h2>
|
||||
<Table
|
||||
data={fileTemplates}
|
||||
columns={columns}
|
||||
itemsPerPage={5}
|
||||
currentPage={1}
|
||||
totalPages={1}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<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">
|
||||
Fichiers à uploader
|
||||
</h2>
|
||||
<Table
|
||||
data={fileTemplates}
|
||||
columns={columns}
|
||||
itemsPerPage={5}
|
||||
currentPage={1}
|
||||
totalPages={1}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,22 +3,29 @@ import React, { useState, useEffect } from 'react';
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Button';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import { fetchRegisterForm, fetchTemplatesFromRegistrationFiles } from '@/app/actions/subscriptionAction';
|
||||
import { downloadTemplate,
|
||||
createRegistrationTemplates,
|
||||
editRegistrationTemplates,
|
||||
deleteRegistrationTemplates
|
||||
import {
|
||||
fetchRegisterForm,
|
||||
fetchTemplatesFromRegistrationFiles,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import {
|
||||
downloadTemplate,
|
||||
createRegistrationTemplates,
|
||||
editRegistrationTemplates,
|
||||
deleteRegistrationTemplates,
|
||||
} from '@/app/actions/registerFileGroupAction';
|
||||
import {
|
||||
fetchRegistrationPaymentModes,
|
||||
fetchTuitionPaymentModes } from '@/app/actions/schoolAction';
|
||||
fetchRegistrationPaymentModes,
|
||||
fetchTuitionPaymentModes,
|
||||
} from '@/app/actions/schoolAction';
|
||||
import { Download, Upload, Trash2, Eye } from 'lucide-react';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import DraggableFileUpload from '@/components/DraggableFileUpload';
|
||||
import Modal from '@/components/Modal';
|
||||
import FileStatusLabel from '@/components/FileStatusLabel';
|
||||
import logger from '@/utils/logger';
|
||||
import StudentInfoForm, { validateStudentInfo } from '@/components/Inscription/StudentInfoForm';
|
||||
import StudentInfoForm, {
|
||||
validateStudentInfo,
|
||||
} from '@/components/Inscription/StudentInfoForm';
|
||||
import FilesToUpload from '@/components/Inscription/FilesToUpload';
|
||||
import { DocusealForm } from '@docuseal/react';
|
||||
|
||||
@ -31,440 +38,498 @@ import { DocusealForm } from '@docuseal/react';
|
||||
* @param {object} errors - Erreurs de validation du formulaire
|
||||
*/
|
||||
export default function InscriptionFormShared({
|
||||
studentId,
|
||||
csrfToken,
|
||||
selectedEstablishmentId,
|
||||
onSubmit,
|
||||
cancelUrl,
|
||||
errors = {} // Nouvelle prop pour les erreurs
|
||||
studentId,
|
||||
csrfToken,
|
||||
selectedEstablishmentId,
|
||||
onSubmit,
|
||||
cancelUrl,
|
||||
errors = {}, // Nouvelle prop pour les erreurs
|
||||
}) {
|
||||
// États pour gérer les données du formulaire
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [formData, setFormData] = useState({
|
||||
id: '',
|
||||
last_name: '',
|
||||
first_name: '',
|
||||
address: '',
|
||||
birth_date: '',
|
||||
birth_place: '',
|
||||
birth_postal_code: '',
|
||||
nationality: '',
|
||||
attending_physician: '',
|
||||
level: '',
|
||||
registration_payment: '',
|
||||
tuition_payment: ''
|
||||
// États pour gérer les données du formulaire
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [formData, setFormData] = useState({
|
||||
id: '',
|
||||
last_name: '',
|
||||
first_name: '',
|
||||
address: '',
|
||||
birth_date: '',
|
||||
birth_place: '',
|
||||
birth_postal_code: '',
|
||||
nationality: '',
|
||||
attending_physician: '',
|
||||
level: '',
|
||||
registration_payment: '',
|
||||
tuition_payment: '',
|
||||
});
|
||||
|
||||
const [guardians, setGuardians] = useState([]);
|
||||
|
||||
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
|
||||
const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]);
|
||||
|
||||
// États pour la gestion des fichiers
|
||||
const [uploadedFiles, setUploadedFiles] = useState([]);
|
||||
const [fileTemplates, setFileTemplates] = useState([]);
|
||||
const [fileGroup, setFileGroup] = useState(null);
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [file, setFile] = useState('');
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [currentTemplateId, setCurrentTemplateId] = useState(null);
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const isCurrentPageValid = () => {
|
||||
if (currentPage === 1) {
|
||||
const isValid = validateStudentInfo(formData);
|
||||
return isValid;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Chargement initial des données
|
||||
// Mettre à jour les données quand initialData change
|
||||
useEffect(() => {
|
||||
if (studentId) {
|
||||
fetchRegisterForm(studentId).then((data) => {
|
||||
logger.debug(data);
|
||||
|
||||
setFormData({
|
||||
id: data?.student?.id || '',
|
||||
last_name: data?.student?.last_name || '',
|
||||
first_name: data?.student?.first_name || '',
|
||||
address: data?.student?.address || '',
|
||||
birth_date: data?.student?.birth_date || '',
|
||||
birth_place: data?.student?.birth_place || '',
|
||||
birth_postal_code: data?.student?.birth_postal_code || '',
|
||||
nationality: data?.student?.nationality || '',
|
||||
attending_physician: data?.student?.attending_physician || '',
|
||||
level: data?.student?.level || '',
|
||||
registration_payment: data?.registration_payment || '',
|
||||
tuition_payment: data?.tuition_payment || '',
|
||||
totalRegistrationFees: data?.totalRegistrationFees,
|
||||
totalTuitionFees: data?.totalTuitionFees,
|
||||
});
|
||||
setGuardians(data?.student?.guardians || []);
|
||||
setUploadedFiles(data.registration_files || []);
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [studentId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplatesFromRegistrationFiles(studentId).then((data) => {
|
||||
setFileTemplates(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [guardians, setGuardians] = useState([]);
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
// Fetch data for registration payment modes
|
||||
handleRegistrationPaymentModes();
|
||||
|
||||
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
|
||||
const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]);
|
||||
// Fetch data for tuition payment modes
|
||||
handleTuitionPaymentModes();
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
// États pour la gestion des fichiers
|
||||
const [uploadedFiles, setUploadedFiles] = useState([]);
|
||||
const [fileTemplates, setFileTemplates] = useState([]);
|
||||
const [fileGroup, setFileGroup] = useState(null);
|
||||
const [fileName, setFileName] = useState("");
|
||||
const [file, setFile] = useState("");
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [currentTemplateId, setCurrentTemplateId] = useState(null);
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const isCurrentPageValid = () => {
|
||||
if (currentPage === 1) {
|
||||
const isValid = validateStudentInfo(formData);
|
||||
return isValid;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Chargement initial des données
|
||||
// Mettre à jour les données quand initialData change
|
||||
useEffect(() => {
|
||||
if (studentId) {
|
||||
fetchRegisterForm(studentId).then((data) => {
|
||||
logger.debug(data);
|
||||
|
||||
setFormData({
|
||||
id: data?.student?.id || '',
|
||||
last_name: data?.student?.last_name || '',
|
||||
first_name: data?.student?.first_name || '',
|
||||
address: data?.student?.address || '',
|
||||
birth_date: data?.student?.birth_date || '',
|
||||
birth_place: data?.student?.birth_place || '',
|
||||
birth_postal_code: data?.student?.birth_postal_code || '',
|
||||
nationality: data?.student?.nationality || '',
|
||||
attending_physician: data?.student?.attending_physician || '',
|
||||
level: data?.student?.level || '',
|
||||
registration_payment: data?.registration_payment || '',
|
||||
tuition_payment: data?.tuition_payment || '',
|
||||
totalRegistrationFees: data?.totalRegistrationFees,
|
||||
totalTuitionFees: data?.totalTuitionFees,
|
||||
});
|
||||
setGuardians(data?.student?.guardians || []);
|
||||
setUploadedFiles(data.registration_files || []);
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [studentId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplatesFromRegistrationFiles(studentId).then((data) => {
|
||||
setFileTemplates(data);
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
// Fetch data for registration payment modes
|
||||
handleRegistrationPaymentModes();
|
||||
|
||||
// Fetch data for tuition payment modes
|
||||
handleTuitionPaymentModes();
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
const handleRegistrationPaymentModes = () => {
|
||||
fetchRegistrationPaymentModes(selectedEstablishmentId)
|
||||
.then(data => {
|
||||
const activePaymentModes = data.filter(mode => mode.is_active === true);
|
||||
setRegistrationPaymentModes(activePaymentModes);
|
||||
})
|
||||
.catch(error => logger.error('Error fetching registration payment modes:', error));
|
||||
};
|
||||
|
||||
const handleTuitionPaymentModes = () => {
|
||||
fetchTuitionPaymentModes(selectedEstablishmentId)
|
||||
.then(data => {
|
||||
const activePaymentModes = data.filter(mode => mode.is_active === true);
|
||||
setTuitionPaymentModes(activePaymentModes);
|
||||
})
|
||||
.catch(error => logger.error('Error fetching tuition payment modes:', error));
|
||||
};
|
||||
|
||||
// Fonctions de gestion du formulaire et des fichiers
|
||||
const updateFormField = (field, value) => {
|
||||
setFormData(prev => ({...prev, [field]: value}));
|
||||
};
|
||||
|
||||
// Gestion du téléversement de fichiers
|
||||
const handleFileUpload = async (file, fileName) => {
|
||||
if (!file || !currentTemplateId || !formData.id) {
|
||||
logger.error('Missing required data for upload');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('name', fileName);
|
||||
data.append('template', currentTemplateId);
|
||||
data.append('register_form', formData.id);
|
||||
|
||||
try {
|
||||
const response = await createRegistrationTemplates(data, csrfToken);
|
||||
if (response) {
|
||||
setUploadedFiles(prev => {
|
||||
const newFiles = prev.filter(f => parseInt(f.template) !== currentTemplateId);
|
||||
return [...newFiles, {
|
||||
name: fileName,
|
||||
template: currentTemplateId,
|
||||
file: response.file
|
||||
}];
|
||||
});
|
||||
|
||||
// Rafraîchir les données du formulaire pour avoir les fichiers à jour
|
||||
if (studentId) {
|
||||
fetchRegisterForm(studentId).then((data) => {
|
||||
setUploadedFiles(data.registration_files || []);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error uploading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Vérification si un fichier est déjà uploadé
|
||||
const isFileUploaded = (templateId) => {
|
||||
return uploadedFiles.find(template =>
|
||||
template.template === templateId
|
||||
const handleRegistrationPaymentModes = () => {
|
||||
fetchRegistrationPaymentModes(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
const activePaymentModes = data.filter(
|
||||
(mode) => mode.is_active === true
|
||||
);
|
||||
};
|
||||
setRegistrationPaymentModes(activePaymentModes);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching registration payment modes:', error)
|
||||
);
|
||||
};
|
||||
|
||||
// Récupération d'un fichier uploadé
|
||||
const getUploadedFile = (templateId) => {
|
||||
return uploadedFiles.find(file => parseInt(file.template) === templateId);
|
||||
};
|
||||
const handleTuitionPaymentModes = () => {
|
||||
fetchTuitionPaymentModes(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
const activePaymentModes = data.filter(
|
||||
(mode) => mode.is_active === true
|
||||
);
|
||||
setTuitionPaymentModes(activePaymentModes);
|
||||
})
|
||||
.catch((error) =>
|
||||
logger.error('Error fetching tuition payment modes:', error)
|
||||
);
|
||||
};
|
||||
|
||||
// Suppression d'un fichier
|
||||
const handleDeleteFile = async (templateId) => {
|
||||
const fileToDelete = getUploadedFile(templateId);
|
||||
if (!fileToDelete) return;
|
||||
// Fonctions de gestion du formulaire et des fichiers
|
||||
const updateFormField = (field, value) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
try {
|
||||
await deleteRegistrationTemplates(fileToDelete.id, csrfToken);
|
||||
setUploadedFiles(prev => prev.filter(f => parseInt(f.template) !== templateId));
|
||||
} catch (error) {
|
||||
logger.error('Error deleting file:', error);
|
||||
}
|
||||
};
|
||||
// Gestion du téléversement de fichiers
|
||||
const handleFileUpload = async (file, fileName) => {
|
||||
if (!file || !currentTemplateId || !formData.id) {
|
||||
logger.error('Missing required data for upload');
|
||||
return;
|
||||
}
|
||||
|
||||
// Soumission du formulaire
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const data ={
|
||||
student: {
|
||||
...formData,
|
||||
guardians
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('name', fileName);
|
||||
data.append('template', currentTemplateId);
|
||||
data.append('register_form', formData.id);
|
||||
|
||||
try {
|
||||
const response = await createRegistrationTemplates(data, csrfToken);
|
||||
if (response) {
|
||||
setUploadedFiles((prev) => {
|
||||
const newFiles = prev.filter(
|
||||
(f) => parseInt(f.template) !== currentTemplateId
|
||||
);
|
||||
return [
|
||||
...newFiles,
|
||||
{
|
||||
name: fileName,
|
||||
template: currentTemplateId,
|
||||
file: response.file,
|
||||
},
|
||||
establishment: selectedEstablishmentId,
|
||||
status:3,
|
||||
tuition_payment:formData.tuition_payment,
|
||||
registration_payment:formData.registration_payment
|
||||
];
|
||||
});
|
||||
|
||||
// Rafraîchir les données du formulaire pour avoir les fichiers à jour
|
||||
if (studentId) {
|
||||
fetchRegisterForm(studentId).then((data) => {
|
||||
setUploadedFiles(data.registration_files || []);
|
||||
});
|
||||
}
|
||||
onSubmit(data);
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error uploading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Soumission du formulaire
|
||||
const handleSave = (e) => {
|
||||
e.preventDefault();
|
||||
const data ={
|
||||
student: {
|
||||
...formData,
|
||||
guardians
|
||||
},
|
||||
establishment: selectedEstablishmentId
|
||||
// Vérification si un fichier est déjà uploadé
|
||||
const isFileUploaded = (templateId) => {
|
||||
return uploadedFiles.find((template) => template.template === templateId);
|
||||
};
|
||||
|
||||
// Récupération d'un fichier uploadé
|
||||
const getUploadedFile = (templateId) => {
|
||||
return uploadedFiles.find((file) => parseInt(file.template) === templateId);
|
||||
};
|
||||
|
||||
// Suppression d'un fichier
|
||||
const handleDeleteFile = async (templateId) => {
|
||||
const fileToDelete = getUploadedFile(templateId);
|
||||
if (!fileToDelete) return;
|
||||
|
||||
try {
|
||||
await deleteRegistrationTemplates(fileToDelete.id, csrfToken);
|
||||
setUploadedFiles((prev) =>
|
||||
prev.filter((f) => parseInt(f.template) !== templateId)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Error deleting file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Soumission du formulaire
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
student: {
|
||||
...formData,
|
||||
guardians,
|
||||
},
|
||||
establishment: selectedEstablishmentId,
|
||||
status: 3,
|
||||
tuition_payment: formData.tuition_payment,
|
||||
registration_payment: formData.registration_payment,
|
||||
};
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
// Soumission du formulaire
|
||||
const handleSave = (e) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
student: {
|
||||
...formData,
|
||||
guardians,
|
||||
},
|
||||
establishment: selectedEstablishmentId,
|
||||
};
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
setCurrentPage(currentPage + 1);
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
setCurrentPage(currentPage - 1);
|
||||
};
|
||||
|
||||
const requiredFileTemplates = fileTemplates;
|
||||
|
||||
// Configuration des colonnes pour le tableau des fichiers
|
||||
const columns = [
|
||||
{ name: 'Nom du fichier', transform: (row) => row.name },
|
||||
{
|
||||
name: 'Fichier à Remplir',
|
||||
transform: (row) => (row.is_required ? 'Oui' : 'Non'),
|
||||
},
|
||||
{
|
||||
name: 'Fichier de référence',
|
||||
transform: (row) =>
|
||||
row.file && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{' '}
|
||||
<a
|
||||
href={`${BASE_URL}${row.file}`}
|
||||
target="_blank"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Download size={16} />
|
||||
</a>{' '}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Statut',
|
||||
transform: (row) =>
|
||||
row.is_required && (
|
||||
<FileStatusLabel
|
||||
status={isFileUploaded(row.id) ? 'sent' : 'pending'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
transform: (row) => {
|
||||
if (!row.is_required) return null;
|
||||
|
||||
const uploadedFile = getUploadedFile(row.id);
|
||||
|
||||
if (uploadedFile) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<a
|
||||
href={`${BASE_URL}${uploadedFile.file}`}
|
||||
target="_blank"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Eye size={16} />
|
||||
</a>
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleDeleteFile(row.id)}
|
||||
type="button"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
setCurrentPage(currentPage + 1);
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
setCurrentPage(currentPage - 1);
|
||||
};
|
||||
return (
|
||||
<button
|
||||
className="text-emerald-500 hover:text-emerald-700"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentTemplateId(row.id);
|
||||
setShowUploadModal(true);
|
||||
}}
|
||||
>
|
||||
<Upload size={16} />
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const requiredFileTemplates = fileTemplates;
|
||||
// Affichage du loader pendant le chargement
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
// Configuration des colonnes pour le tableau des fichiers
|
||||
const columns = [
|
||||
{ name: 'Nom du fichier', transform: (row) => row.name },
|
||||
{ name: 'Fichier à Remplir', transform: (row) => row.is_required ? 'Oui' : 'Non' },
|
||||
{ name: 'Fichier de référence', transform: (row) => row.file && <div className="flex items-center justify-center gap-2"> <a href={`${BASE_URL}${row.file}`} target='_blank' className="text-blue-500 hover:text-blue-700">
|
||||
<Download size={16} />
|
||||
</a> </div>},
|
||||
{ name: 'Statut', transform: (row) =>
|
||||
row.is_required && (
|
||||
<FileStatusLabel
|
||||
status={isFileUploaded(row.id) ? 'sent' : 'pending'}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{ name: 'Actions', transform: (row) => {
|
||||
if (!row.is_required) return null;
|
||||
// Rendu du composant
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
{/* Page 1 : Informations de l'élève et Responsables */}
|
||||
{currentPage === 1 && (
|
||||
<StudentInfoForm
|
||||
formData={formData}
|
||||
updateFormField={updateFormField}
|
||||
guardians={guardians}
|
||||
setGuardians={setGuardians}
|
||||
registrationPaymentModes={registrationPaymentModes}
|
||||
tuitionPaymentModes={tuitionPaymentModes}
|
||||
errors={errors}
|
||||
/>
|
||||
)}
|
||||
|
||||
const uploadedFile = getUploadedFile(row.id);
|
||||
{/* Pages suivantes : Section Fichiers d'inscription */}
|
||||
{currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
{/* Titre du document */}
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
{requiredFileTemplates[currentPage - 2].name ||
|
||||
'Document sans nom'}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
{requiredFileTemplates[currentPage - 2].description ||
|
||||
'Aucune description disponible pour ce document.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
if (uploadedFile) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<a
|
||||
href={`${BASE_URL}${uploadedFile.file}`}
|
||||
target="_blank"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Eye size={16} />
|
||||
</a>
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleDeleteFile(row.id)}
|
||||
type="button"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{/* Affichage du formulaire ou du document */}
|
||||
{requiredFileTemplates[currentPage - 2].file === '' ? (
|
||||
<DocusealForm
|
||||
id="docusealForm"
|
||||
src={
|
||||
'https://docuseal.com/s/' +
|
||||
requiredFileTemplates[currentPage - 2].slug
|
||||
}
|
||||
withDownloadButton={false}
|
||||
onComplete={() => {
|
||||
downloadTemplate(requiredFileTemplates[currentPage - 2].slug)
|
||||
.then((data) => fetch(data))
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const file = new File(
|
||||
[blob],
|
||||
`${requiredFileTemplates[currentPage - 2].name}.pdf`,
|
||||
{ type: blob.type }
|
||||
);
|
||||
const updateData = new FormData();
|
||||
updateData.append('file', file);
|
||||
|
||||
return (
|
||||
<button
|
||||
className="text-emerald-500 hover:text-emerald-700"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentTemplateId(row.id);
|
||||
setShowUploadModal(true);
|
||||
}}
|
||||
>
|
||||
<Upload size={16} />
|
||||
</button>
|
||||
);
|
||||
}},
|
||||
];
|
||||
|
||||
// Affichage du loader pendant le chargement
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
// Rendu du composant
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<DjangoCSRFToken csrfToken={csrfToken}/>
|
||||
{/* Page 1 : Informations de l'élève et Responsables */}
|
||||
{currentPage === 1 && (
|
||||
<StudentInfoForm
|
||||
formData={formData}
|
||||
updateFormField={updateFormField}
|
||||
guardians={guardians}
|
||||
setGuardians={setGuardians}
|
||||
registrationPaymentModes={registrationPaymentModes}
|
||||
tuitionPaymentModes={tuitionPaymentModes}
|
||||
errors={errors}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pages suivantes : Section Fichiers d'inscription */}
|
||||
{currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
{/* Titre du document */}
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
{requiredFileTemplates[currentPage - 2].name || "Document sans nom"}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
{requiredFileTemplates[currentPage - 2].description || "Aucune description disponible pour ce document."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Affichage du formulaire ou du document */}
|
||||
{requiredFileTemplates[currentPage - 2].file === "" ? (
|
||||
<DocusealForm
|
||||
id="docusealForm"
|
||||
src={"https://docuseal.com/s/" + requiredFileTemplates[currentPage - 2].slug}
|
||||
withDownloadButton={false}
|
||||
onComplete={() => {
|
||||
downloadTemplate(requiredFileTemplates[currentPage - 2].slug)
|
||||
.then((data) => fetch(data))
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const file = new File([blob], `${requiredFileTemplates[currentPage - 2].name}.pdf`, { type: blob.type });
|
||||
const updateData = new FormData();
|
||||
updateData.append('file', file);
|
||||
|
||||
return editRegistrationTemplates(requiredFileTemplates[currentPage - 2].id, updateData, csrfToken);
|
||||
})
|
||||
.then((data) => {
|
||||
logger.debug("EDIT TEMPLATE : ", data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("error editing template : ", error);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<iframe
|
||||
src={`${BASE_URL}/${requiredFileTemplates[currentPage - 2].file}`}
|
||||
title="Document Viewer"
|
||||
className="w-full"
|
||||
style={{
|
||||
height: '75vh', // Ajuster la hauteur à 75% de la fenêtre
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dernière page : Section Fichiers parents */}
|
||||
{currentPage === requiredFileTemplates.length + 2 && (
|
||||
<>
|
||||
<FilesToUpload
|
||||
fileTemplates={fileTemplates.filter(template => !template.is_required)}
|
||||
columns={columns}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Boutons de contrôle */}
|
||||
<div className="flex justify-end space-x-4">
|
||||
<Button
|
||||
text="Sauvegarder"
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 rounded-md shadow-sm focus:outline-none bg-orange-500 text-white hover:bg-orange-600"
|
||||
primary
|
||||
name="Save"
|
||||
/>
|
||||
{currentPage > 1 && (
|
||||
<Button text="Précédent" onClick={(e) => { e.preventDefault(); handlePreviousPage(); }} />
|
||||
)}
|
||||
{currentPage < requiredFileTemplates.length + 2 && (
|
||||
<Button
|
||||
text="Suivant"
|
||||
onClick={(e) => { e.preventDefault(); handleNextPage(); }}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
!isCurrentPageValid()
|
||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
}`}
|
||||
disabled={!isCurrentPageValid()}
|
||||
primary
|
||||
name="Next"
|
||||
/>
|
||||
)}
|
||||
{currentPage === requiredFileTemplates.length + 2 && (
|
||||
<Button type="submit" text="Valider" primary />
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{fileTemplates.length > 0 && (
|
||||
<Modal
|
||||
isOpen={showUploadModal}
|
||||
setIsOpen={setShowUploadModal}
|
||||
title="Téléverser un fichier"
|
||||
ContentComponent={() => (
|
||||
<>
|
||||
<DraggableFileUpload
|
||||
className="w-full"
|
||||
fileName={fileName}
|
||||
onFileSelect={(selectedFile) => {
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setFileName(selectedFile.name);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="mt-4 flex justify-center space-x-4">
|
||||
<Button
|
||||
text="Annuler"
|
||||
onClick={() => {
|
||||
setShowUploadModal(false);
|
||||
setCurrentTemplateId(null);
|
||||
setFile(null);
|
||||
setFileName("");
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
text="Valider"
|
||||
onClick={() => {
|
||||
if (file && fileName) {
|
||||
handleFileUpload(file, fileName);
|
||||
setShowUploadModal(false);
|
||||
setCurrentTemplateId(null);
|
||||
setFile(null);
|
||||
setFileName("");
|
||||
}
|
||||
}}
|
||||
primary={true}
|
||||
disabled={!file || !fileName}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
return editRegistrationTemplates(
|
||||
requiredFileTemplates[currentPage - 2].id,
|
||||
updateData,
|
||||
csrfToken
|
||||
);
|
||||
})
|
||||
.then((data) => {
|
||||
logger.debug('EDIT TEMPLATE : ', data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('error editing template : ', error);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<iframe
|
||||
src={`${BASE_URL}/${requiredFileTemplates[currentPage - 2].file}`}
|
||||
title="Document Viewer"
|
||||
className="w-full"
|
||||
style={{
|
||||
height: '75vh', // Ajuster la hauteur à 75% de la fenêtre
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dernière page : Section Fichiers parents */}
|
||||
{currentPage === requiredFileTemplates.length + 2 && (
|
||||
<>
|
||||
<FilesToUpload
|
||||
fileTemplates={fileTemplates.filter(
|
||||
(template) => !template.is_required
|
||||
)}
|
||||
columns={columns}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Boutons de contrôle */}
|
||||
<div className="flex justify-end space-x-4">
|
||||
<Button
|
||||
text="Sauvegarder"
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 rounded-md shadow-sm focus:outline-none bg-orange-500 text-white hover:bg-orange-600"
|
||||
primary
|
||||
name="Save"
|
||||
/>
|
||||
{currentPage > 1 && (
|
||||
<Button
|
||||
text="Précédent"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handlePreviousPage();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{currentPage < requiredFileTemplates.length + 2 && (
|
||||
<Button
|
||||
text="Suivant"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleNextPage();
|
||||
}}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
!isCurrentPageValid()
|
||||
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
}`}
|
||||
disabled={!isCurrentPageValid()}
|
||||
primary
|
||||
name="Next"
|
||||
/>
|
||||
)}
|
||||
{currentPage === requiredFileTemplates.length + 2 && (
|
||||
<Button type="submit" text="Valider" primary />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</form>
|
||||
{fileTemplates.length > 0 && (
|
||||
<Modal
|
||||
isOpen={showUploadModal}
|
||||
setIsOpen={setShowUploadModal}
|
||||
title="Téléverser un fichier"
|
||||
ContentComponent={() => (
|
||||
<>
|
||||
<DraggableFileUpload
|
||||
className="w-full"
|
||||
fileName={fileName}
|
||||
onFileSelect={(selectedFile) => {
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setFileName(selectedFile.name);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="mt-4 flex justify-center space-x-4">
|
||||
<Button
|
||||
text="Annuler"
|
||||
onClick={() => {
|
||||
setShowUploadModal(false);
|
||||
setCurrentTemplateId(null);
|
||||
setFile(null);
|
||||
setFileName('');
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
text="Valider"
|
||||
onClick={() => {
|
||||
if (file && fileName) {
|
||||
handleFileUpload(file, fileName);
|
||||
setShowUploadModal(false);
|
||||
setCurrentTemplateId(null);
|
||||
setFile(null);
|
||||
setFileName('');
|
||||
}
|
||||
}}
|
||||
primary={true}
|
||||
disabled={!file || !fileName}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,34 +1,48 @@
|
||||
import React from 'react';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
|
||||
export default function PaymentMethodSelector({ formData, title, name, updateFormField, selected, paymentModes, paymentModesOptions, amount, getError }) {
|
||||
//console.log(paymentModes)
|
||||
//console.log(selected)
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
{/* Titre */}
|
||||
<h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">{title}</h2>
|
||||
export default function PaymentMethodSelector({
|
||||
formData,
|
||||
title,
|
||||
name,
|
||||
updateFormField,
|
||||
selected,
|
||||
paymentModes,
|
||||
paymentModesOptions,
|
||||
amount,
|
||||
getError,
|
||||
}) {
|
||||
//console.log(paymentModes)
|
||||
//console.log(selected)
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
{/* Titre */}
|
||||
<h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* Section d'information */}
|
||||
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
|
||||
<p className="text-gray-700 text-sm mb-2">
|
||||
<strong className="text-gray-900">Montant :</strong> {amount} €
|
||||
</p>
|
||||
</div>
|
||||
{/* Section d'information */}
|
||||
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
|
||||
<p className="text-gray-700 text-sm mb-2">
|
||||
<strong className="text-gray-900">Montant :</strong> {amount} €
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SelectChoice
|
||||
name={name}
|
||||
label="Mode de Paiement"
|
||||
placeHolder="Sélectionner un mode de paiement"
|
||||
selected={selected || ''}
|
||||
callback={(e) => updateFormField(name, e.target.value)}
|
||||
choices={paymentModes.map((mode) => ({
|
||||
value: mode.mode,
|
||||
label: paymentModesOptions.find(option => option.id === mode.mode)?.name || 'Mode inconnu'
|
||||
}))}
|
||||
required
|
||||
errorMsg={getError('payment_method')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<SelectChoice
|
||||
name={name}
|
||||
label="Mode de Paiement"
|
||||
placeHolder="Sélectionner un mode de paiement"
|
||||
selected={selected || ''}
|
||||
callback={(e) => updateFormField(name, e.target.value)}
|
||||
choices={paymentModes.map((mode) => ({
|
||||
value: mode.mode,
|
||||
label:
|
||||
paymentModesOptions.find((option) => option.id === mode.mode)
|
||||
?.name || 'Mode inconnu',
|
||||
}))}
|
||||
required
|
||||
errorMsg={getError('payment_method')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,112 +5,137 @@ import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Trash2, Plus } from 'lucide-react';
|
||||
|
||||
export default function ResponsableInputFields({guardians, onGuardiansChange, addGuardian, deleteGuardian, errors = []}) {
|
||||
const t = useTranslations('ResponsableInputFields');
|
||||
export default function ResponsableInputFields({
|
||||
guardians,
|
||||
onGuardiansChange,
|
||||
addGuardian,
|
||||
deleteGuardian,
|
||||
errors = [],
|
||||
}) {
|
||||
const t = useTranslations('ResponsableInputFields');
|
||||
|
||||
const getError = (index, field) => {
|
||||
return errors[index]?.[field]?.[0];
|
||||
};
|
||||
const getError = (index, field) => {
|
||||
return errors[index]?.[field]?.[0];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{guardians.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>
|
||||
{guardians.length > 1 && (
|
||||
<Trash2
|
||||
className="w-5 h-5 text-red-500 cursor-pointer hover:text-red-700 transition-colors"
|
||||
onClick={() => deleteGuardian(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{guardians.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>
|
||||
{guardians.length > 1 && (
|
||||
<Trash2
|
||||
className="w-5 h-5 text-red-500 cursor-pointer hover:text-red-700 transition-colors"
|
||||
onClick={() => deleteGuardian(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="idResponsable" value={item.id} />
|
||||
<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.last_name}
|
||||
onChange={(event) => {
|
||||
onGuardiansChange(item.id, 'last_name', event.target.value);
|
||||
}}
|
||||
errorMsg={getError(index, 'last_name')}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
name="prenomResponsable"
|
||||
type="text"
|
||||
label={t('firstname')}
|
||||
value={item.first_name}
|
||||
onChange={(event) => {
|
||||
onGuardiansChange(item.id, 'first_name', event.target.value);
|
||||
}}
|
||||
errorMsg={getError(index, 'first_name')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'>
|
||||
<InputText
|
||||
name="nomResponsable"
|
||||
type="text"
|
||||
label={t('lastname')}
|
||||
value={item.last_name}
|
||||
onChange={(event) => {onGuardiansChange(item.id, "last_name", event.target.value)}}
|
||||
errorMsg={getError(index, 'last_name')}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
name="prenomResponsable"
|
||||
type="text"
|
||||
label={t('firstname')}
|
||||
value={item.first_name}
|
||||
onChange={(event) => {onGuardiansChange(item.id, "first_name", event.target.value)}}
|
||||
errorMsg={getError(index, 'first_name')}
|
||||
required
|
||||
/>
|
||||
</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.associated_profile_email}
|
||||
onChange={(event) => {
|
||||
onGuardiansChange(
|
||||
item.id,
|
||||
'associated_profile_email',
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
required
|
||||
errorMsg={getError(index, 'email')}
|
||||
/>
|
||||
<InputPhone
|
||||
name="telephoneResponsable"
|
||||
label={t('phone')}
|
||||
value={item.phone}
|
||||
onChange={(event) => {
|
||||
onGuardiansChange(item.id, 'phone', event);
|
||||
}}
|
||||
required
|
||||
errorMsg={getError(index, 'phone')}
|
||||
/>
|
||||
</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.associated_profile_email}
|
||||
onChange={(event) => {onGuardiansChange(item.id, "associated_profile_email", event.target.value)}}
|
||||
required
|
||||
errorMsg={getError(index, 'email')}
|
||||
/>
|
||||
<InputPhone
|
||||
name="telephoneResponsable"
|
||||
label={t('phone')}
|
||||
value={item.phone}
|
||||
onChange={(event) => {onGuardiansChange(item.id, "phone", event)}}
|
||||
required
|
||||
errorMsg={getError(index, 'phone')}
|
||||
/>
|
||||
</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.birth_date}
|
||||
onChange={(event) => {
|
||||
onGuardiansChange(item.id, 'birth_date', event.target.value);
|
||||
}}
|
||||
required
|
||||
errorMsg={getError(index, 'birth_date')}
|
||||
/>
|
||||
<InputText
|
||||
name="professionResponsable"
|
||||
type="text"
|
||||
label={t('profession')}
|
||||
value={item.profession}
|
||||
onChange={(event) => {
|
||||
onGuardiansChange(item.id, 'profession', event.target.value);
|
||||
}}
|
||||
required
|
||||
errorMsg={getError(index, 'profession')}
|
||||
/>
|
||||
</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.birth_date}
|
||||
onChange={(event) => {onGuardiansChange(item.id, "birth_date", event.target.value)}}
|
||||
required
|
||||
errorMsg={getError(index, 'birth_date')}
|
||||
/>
|
||||
<InputText
|
||||
name="professionResponsable"
|
||||
type="text"
|
||||
label={t('profession')}
|
||||
value={item.profession}
|
||||
onChange={(event) => {onGuardiansChange(item.id, "profession", event.target.value)}}
|
||||
required
|
||||
errorMsg={getError(index, 'profession')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-4'>
|
||||
<InputText
|
||||
name="adresseResponsable"
|
||||
type="text"
|
||||
label={t('address')}
|
||||
value={item.address}
|
||||
onChange={(event) => {onGuardiansChange(item.id, "address", event.target.value)}}
|
||||
required
|
||||
errorMsg={getError(index, 'address')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Plus
|
||||
className="w-8 h-8 text-green-500 cursor-pointer hover:text-green-700 transition-colors border-2 border-green-500 hover:border-green-700 rounded-full p-1"
|
||||
onClick={(e) => addGuardian(e)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<InputText
|
||||
name="adresseResponsable"
|
||||
type="text"
|
||||
label={t('address')}
|
||||
value={item.address}
|
||||
onChange={(event) => {
|
||||
onGuardiansChange(item.id, 'address', event.target.value);
|
||||
}}
|
||||
required
|
||||
errorMsg={getError(index, 'address')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
))}
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Plus
|
||||
className="w-8 h-8 text-green-500 cursor-pointer hover:text-green-700 transition-colors border-2 border-green-500 hover:border-green-700 rounded-full p-1"
|
||||
onClick={(e) => addGuardian(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,176 +5,190 @@ import ResponsableInputFields from '@/components/Inscription/ResponsableInputFie
|
||||
import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
|
||||
|
||||
const levels = [
|
||||
{ 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'},
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
const paymentModesOptions = [
|
||||
{ id: 1, name: 'Prélèvement SEPA' },
|
||||
{ id: 2, name: 'Virement' },
|
||||
{ id: 3, name: 'Chèque' },
|
||||
{ id: 4, name: 'Espèce' },
|
||||
];
|
||||
{ id: 1, name: 'Prélèvement SEPA' },
|
||||
{ id: 2, name: 'Virement' },
|
||||
{ id: 3, name: 'Chèque' },
|
||||
{ id: 4, name: 'Espèce' },
|
||||
];
|
||||
|
||||
// Fonction de validation pour vérifier les champs requis
|
||||
export function validateStudentInfo(formData) {
|
||||
const requiredFields = [
|
||||
'last_name',
|
||||
'first_name',
|
||||
'nationality',
|
||||
'birth_date',
|
||||
'birth_place',
|
||||
'birth_postal_code',
|
||||
'address',
|
||||
'attending_physician',
|
||||
'level',
|
||||
];
|
||||
const requiredFields = [
|
||||
'last_name',
|
||||
'first_name',
|
||||
'nationality',
|
||||
'birth_date',
|
||||
'birth_place',
|
||||
'birth_postal_code',
|
||||
'address',
|
||||
'attending_physician',
|
||||
'level',
|
||||
];
|
||||
|
||||
const isValid = requiredFields.every((field) => {
|
||||
const value = formData[field];
|
||||
return typeof value === 'string' ? value.trim() !== '' : Boolean(value);
|
||||
});
|
||||
const isValid = requiredFields.every((field) => {
|
||||
const value = formData[field];
|
||||
return typeof value === 'string' ? value.trim() !== '' : Boolean(value);
|
||||
});
|
||||
|
||||
return isValid;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export default function StudentInfoForm({ formData, updateFormField, guardians, setGuardians, registrationPaymentModes, tuitionPaymentModes, errors }) {
|
||||
const getError = (field) => {
|
||||
return errors?.student?.[field]?.[0];
|
||||
};
|
||||
export default function StudentInfoForm({
|
||||
formData,
|
||||
updateFormField,
|
||||
guardians,
|
||||
setGuardians,
|
||||
registrationPaymentModes,
|
||||
tuitionPaymentModes,
|
||||
errors,
|
||||
}) {
|
||||
const getError = (field) => {
|
||||
return errors?.student?.[field]?.[0];
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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="last_name"
|
||||
label="Nom"
|
||||
value={formData.last_name}
|
||||
onChange={(e) => updateFormField('last_name', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('last_name')}
|
||||
/>
|
||||
<InputText
|
||||
name="first_name"
|
||||
label="Prénom"
|
||||
value={formData.first_name}
|
||||
onChange={(e) => updateFormField('first_name', e.target.value)}
|
||||
errorMsg={getError('first_name')}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
name="nationality"
|
||||
label="Nationalité"
|
||||
value={formData.nationality}
|
||||
required
|
||||
onChange={(e) => updateFormField('nationality', e.target.value)}
|
||||
/>
|
||||
<InputText
|
||||
name="birth_date"
|
||||
type="date"
|
||||
label="Date de Naissance"
|
||||
value={formData.birth_date}
|
||||
onChange={(e) => updateFormField('birth_date', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('birth_date')}
|
||||
/>
|
||||
<InputText
|
||||
name="birth_place"
|
||||
label="Lieu de Naissance"
|
||||
value={formData.birth_place}
|
||||
onChange={(e) => updateFormField('birth_place', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('birth_place')}
|
||||
/>
|
||||
<InputText
|
||||
name="birth_postal_code"
|
||||
label="Code Postal de Naissance"
|
||||
value={formData.birth_postal_code}
|
||||
onChange={(e) => updateFormField('birth_postal_code', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('birth_postal_code')}
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<InputText
|
||||
name="address"
|
||||
label="Adresse"
|
||||
value={formData.address}
|
||||
onChange={(e) => updateFormField('address', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('address')}
|
||||
/>
|
||||
</div>
|
||||
<InputText
|
||||
name="attending_physician"
|
||||
label="Médecin Traitant"
|
||||
value={formData.attending_physician}
|
||||
onChange={(e) => updateFormField('attending_physician', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('attending_physician')}
|
||||
/>
|
||||
<SelectChoice
|
||||
name="level"
|
||||
label="Niveau"
|
||||
placeHolder="Sélectionner un niveau"
|
||||
selected={formData.level}
|
||||
callback={(e) => updateFormField('level', e.target.value)}
|
||||
choices={levels}
|
||||
required
|
||||
errorMsg={getError('level')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
guardians={guardians}
|
||||
onGuardiansChange={(id, field, value) => {
|
||||
const updatedGuardians = guardians.map(resp =>
|
||||
resp.id === id ? { ...resp, [field]: value } : resp
|
||||
);
|
||||
setGuardians(updatedGuardians);
|
||||
}}
|
||||
addGuardian={(e) => {
|
||||
e.preventDefault();
|
||||
setGuardians([...guardians, { id: Date.now() }]);
|
||||
}}
|
||||
deleteGuardian={(index) => {
|
||||
const newArray = [...guardians];
|
||||
newArray.splice(index, 1);
|
||||
setGuardians(newArray);
|
||||
}}
|
||||
errors={errors?.student?.guardians || []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PaymentMethodSelector
|
||||
formData={formData}
|
||||
title="Frais d'inscription"
|
||||
name="registration_payment"
|
||||
updateFormField={updateFormField}
|
||||
selected={formData.registration_payment}
|
||||
paymentModes={registrationPaymentModes}
|
||||
paymentModesOptions={paymentModesOptions}
|
||||
amount={formData.totalRegistrationFees}
|
||||
getError={getError}
|
||||
return (
|
||||
<>
|
||||
<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="last_name"
|
||||
label="Nom"
|
||||
value={formData.last_name}
|
||||
onChange={(e) => updateFormField('last_name', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('last_name')}
|
||||
/>
|
||||
<InputText
|
||||
name="first_name"
|
||||
label="Prénom"
|
||||
value={formData.first_name}
|
||||
onChange={(e) => updateFormField('first_name', e.target.value)}
|
||||
errorMsg={getError('first_name')}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
name="nationality"
|
||||
label="Nationalité"
|
||||
value={formData.nationality}
|
||||
required
|
||||
onChange={(e) => updateFormField('nationality', e.target.value)}
|
||||
/>
|
||||
<InputText
|
||||
name="birth_date"
|
||||
type="date"
|
||||
label="Date de Naissance"
|
||||
value={formData.birth_date}
|
||||
onChange={(e) => updateFormField('birth_date', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('birth_date')}
|
||||
/>
|
||||
<InputText
|
||||
name="birth_place"
|
||||
label="Lieu de Naissance"
|
||||
value={formData.birth_place}
|
||||
onChange={(e) => updateFormField('birth_place', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('birth_place')}
|
||||
/>
|
||||
<InputText
|
||||
name="birth_postal_code"
|
||||
label="Code Postal de Naissance"
|
||||
value={formData.birth_postal_code}
|
||||
onChange={(e) =>
|
||||
updateFormField('birth_postal_code', e.target.value)
|
||||
}
|
||||
required
|
||||
errorMsg={getError('birth_postal_code')}
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<InputText
|
||||
name="address"
|
||||
label="Adresse"
|
||||
value={formData.address}
|
||||
onChange={(e) => updateFormField('address', e.target.value)}
|
||||
required
|
||||
errorMsg={getError('address')}
|
||||
/>
|
||||
</div>
|
||||
<InputText
|
||||
name="attending_physician"
|
||||
label="Médecin Traitant"
|
||||
value={formData.attending_physician}
|
||||
onChange={(e) =>
|
||||
updateFormField('attending_physician', e.target.value)
|
||||
}
|
||||
required
|
||||
errorMsg={getError('attending_physician')}
|
||||
/>
|
||||
<SelectChoice
|
||||
name="level"
|
||||
label="Niveau"
|
||||
placeHolder="Sélectionner un niveau"
|
||||
selected={formData.level}
|
||||
callback={(e) => updateFormField('level', e.target.value)}
|
||||
choices={levels}
|
||||
required
|
||||
errorMsg={getError('level')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PaymentMethodSelector
|
||||
formData={formData}
|
||||
title="Frais de scolarité"
|
||||
name="tuition_payment"
|
||||
updateFormField={updateFormField}
|
||||
selected={formData.tuition_payment}
|
||||
paymentModes={tuitionPaymentModes}
|
||||
paymentModesOptions={paymentModesOptions}
|
||||
amount={formData.totalTuitionFees}
|
||||
getError={getError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<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
|
||||
guardians={guardians}
|
||||
onGuardiansChange={(id, field, value) => {
|
||||
const updatedGuardians = guardians.map((resp) =>
|
||||
resp.id === id ? { ...resp, [field]: value } : resp
|
||||
);
|
||||
setGuardians(updatedGuardians);
|
||||
}}
|
||||
addGuardian={(e) => {
|
||||
e.preventDefault();
|
||||
setGuardians([...guardians, { id: Date.now() }]);
|
||||
}}
|
||||
deleteGuardian={(index) => {
|
||||
const newArray = [...guardians];
|
||||
newArray.splice(index, 1);
|
||||
setGuardians(newArray);
|
||||
}}
|
||||
errors={errors?.student?.guardians || []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PaymentMethodSelector
|
||||
formData={formData}
|
||||
title="Frais d'inscription"
|
||||
name="registration_payment"
|
||||
updateFormField={updateFormField}
|
||||
selected={formData.registration_payment}
|
||||
paymentModes={registrationPaymentModes}
|
||||
paymentModesOptions={paymentModesOptions}
|
||||
amount={formData.totalRegistrationFees}
|
||||
getError={getError}
|
||||
/>
|
||||
|
||||
<PaymentMethodSelector
|
||||
formData={formData}
|
||||
title="Frais de scolarité"
|
||||
name="tuition_payment"
|
||||
updateFormField={updateFormField}
|
||||
selected={formData.tuition_payment}
|
||||
paymentModes={tuitionPaymentModes}
|
||||
paymentModesOptions={paymentModesOptions}
|
||||
amount={formData.totalTuitionFees}
|
||||
getError={getError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
'use client'
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { DocusealBuilder } from '@docuseal/react';
|
||||
import Button from '@/components/Button';
|
||||
@ -7,7 +7,14 @@ import { generateToken } from '@/app/actions/registerFileGroupAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { GraduationCap, CloudUpload } from 'lucide-react';
|
||||
|
||||
export default function ValidateSubscription({ studentId, firstName, lastName, paymentMode, file, onAccept }) {
|
||||
export default function ValidateSubscription({
|
||||
studentId,
|
||||
firstName,
|
||||
lastName,
|
||||
paymentMode,
|
||||
file,
|
||||
onAccept,
|
||||
}) {
|
||||
const [token, setToken] = useState(null);
|
||||
const [uploadedFileName, setUploadedFileName] = useState('');
|
||||
const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`);
|
||||
@ -20,7 +27,9 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
|
||||
.then((data) => {
|
||||
setToken(data.token);
|
||||
})
|
||||
.catch((error) => logger.error('Erreur lors de la génération du token:', error));
|
||||
.catch((error) =>
|
||||
logger.error('Erreur lors de la génération du token:', error)
|
||||
);
|
||||
}
|
||||
}, [isSepa]);
|
||||
|
||||
@ -32,23 +41,23 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
|
||||
const handleAccept = () => {
|
||||
const fileInput = document.getElementById('fileInput'); // Récupère l'élément input
|
||||
const file = fileInput?.files[0]; // Récupère le fichier sélectionné
|
||||
|
||||
|
||||
if (!file) {
|
||||
logger.error('Aucun fichier sélectionné pour le champ SEPA.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const data = {
|
||||
status: 7,
|
||||
status: 7,
|
||||
sepa_file: file,
|
||||
};
|
||||
|
||||
|
||||
// Appeler la fonction passée par le parent pour mettre à jour le RF
|
||||
onAccept(data);
|
||||
};
|
||||
|
||||
const handleRefuse = () => {
|
||||
logger.debug('Dossier refusé pour l\'étudiant:', studentId);
|
||||
logger.debug("Dossier refusé pour l'étudiant:", studentId);
|
||||
// Logique pour refuser l'inscription
|
||||
};
|
||||
|
||||
@ -75,10 +84,14 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800">
|
||||
Dossier scolaire de <span className="text-emerald-600">{firstName} {lastName}</span>
|
||||
Dossier scolaire de{' '}
|
||||
<span className="text-emerald-600">
|
||||
{firstName} {lastName}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 italic">
|
||||
Année scolaire {new Date().getFullYear()}-{new Date().getFullYear() + 1}
|
||||
Année scolaire {new Date().getFullYear()}-
|
||||
{new Date().getFullYear() + 1}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -101,45 +114,54 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
|
||||
|
||||
{currentPage === 2 && isSepa && (
|
||||
<div className="border p-4 rounded-md shadow-md">
|
||||
<h3 className="text-lg font-semibold mb-4">Sélection du mandat de pélèvement SEPA</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Sélection du mandat de pélèvement SEPA
|
||||
</h3>
|
||||
<div
|
||||
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
|
||||
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
|
||||
logger.debug('Fichier déposé:', file.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" /> {/* Icône de cloud */}
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
|
||||
logger.debug('Fichier sélectionné:', file.name);
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
id="fileInput"
|
||||
/>
|
||||
<label htmlFor="fileInput" className="text-center text-gray-500">
|
||||
<p className="text-lg font-semibold text-gray-800">Déposez votre fichier ici</p>
|
||||
<p className="text-sm text-gray-500 mt-2">ou cliquez pour sélectionner un fichier PDF</p>
|
||||
</label>
|
||||
</div>
|
||||
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
|
||||
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
|
||||
logger.debug('Fichier déposé:', file.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" />{' '}
|
||||
{/* Icône de cloud */}
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
|
||||
logger.debug('Fichier sélectionné:', file.name);
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
id="fileInput"
|
||||
/>
|
||||
<label htmlFor="fileInput" className="text-center text-gray-500">
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
Déposez votre fichier ici
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
ou cliquez pour sélectionner un fichier PDF
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
{uploadedFileName && (
|
||||
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
|
||||
<CloudUpload className="w-6 h-6 text-emerald-500" />
|
||||
<p className="text-sm font-medium text-gray-800"><span className="font-semibold">{uploadedFileName}</span></p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
|
||||
<CloudUpload className="w-6 h-6 text-emerald-500" />
|
||||
<p className="text-sm font-medium text-gray-800">
|
||||
<span className="font-semibold">{uploadedFileName}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -172,4 +194,4 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,12 @@ import logoImage from '@/img/logo_min.svg'; // Assurez-vous que le chemin vers l
|
||||
const Logo = ({ className }) => {
|
||||
return (
|
||||
<div className={`max-w-[150px] ${className}`}>
|
||||
<Image src={logoImage} alt="Logo" style={{ width: 'auto', height: 'auto'}} priority />
|
||||
<Image
|
||||
src={logoImage}
|
||||
alt="Logo"
|
||||
style={{ width: 'auto', height: 'auto' }}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,36 +1,55 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
|
||||
const Modal = ({ isOpen, setIsOpen, title, ContentComponent, modalClassName }) => {
|
||||
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 p-4">
|
||||
<div className={`inline-block bg-white rounded-lg px-6 py-5 text-left shadow-xl transform transition-all sm:my-8 ${modalClassName ? modalClassName : 'min-w-[500px] m-12 w-max max-h-[80vh] h-full'}`}>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<Dialog.Title className="text-xl font-medium text-gray-900">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-gray-400 hover:text-gray-500 ml-4 focus:outline-none"
|
||||
>
|
||||
<span className="sr-only">Fermer</span>
|
||||
<svg className="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<ContentComponent />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
const Modal = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
title,
|
||||
ContentComponent,
|
||||
modalClassName,
|
||||
}) => {
|
||||
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 p-4">
|
||||
<div
|
||||
className={`inline-block bg-white rounded-lg px-6 py-5 text-left shadow-xl transform transition-all sm:my-8 ${modalClassName ? modalClassName : 'min-w-[500px] m-12 w-max max-h-[80vh] h-full'}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<Dialog.Title className="text-xl font-medium text-gray-900">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-gray-400 hover:text-gray-500 ml-4 focus:outline-none"
|
||||
>
|
||||
<span className="sr-only">Fermer</span>
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<ContentComponent />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
|
||||
@ -1,15 +1,26 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
|
||||
const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg }) => {
|
||||
const MultiSelect = ({
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
selectedOptions,
|
||||
onChange,
|
||||
errorMsg,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const handleSelect = (option) => {
|
||||
const isSelected = selectedOptions.some(selected => selected.id === option.id);
|
||||
const isSelected = selectedOptions.some(
|
||||
(selected) => selected.id === option.id
|
||||
);
|
||||
let newSelectedOptions;
|
||||
if (isSelected) {
|
||||
newSelectedOptions = selectedOptions.filter(selected => selected.id !== option.id);
|
||||
newSelectedOptions = selectedOptions.filter(
|
||||
(selected) => selected.id !== option.id
|
||||
);
|
||||
} else {
|
||||
newSelectedOptions = [...selectedOptions, option];
|
||||
}
|
||||
@ -39,8 +50,11 @@ const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg
|
||||
>
|
||||
{selectedOptions.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1 justify-center items-center">
|
||||
{selectedOptions.map(option => (
|
||||
<span key={option.id} className="bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-sm">
|
||||
{selectedOptions.map((option) => (
|
||||
<span
|
||||
key={option.id}
|
||||
className="bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-sm"
|
||||
>
|
||||
{option.name}
|
||||
</span>
|
||||
))}
|
||||
@ -52,18 +66,24 @@ const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg
|
||||
</button>
|
||||
{isOpen && (
|
||||
<ul className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||
{options.map(option => (
|
||||
{options.map((option) => (
|
||||
<li
|
||||
key={option.id}
|
||||
className={`cursor-pointer select-none relative py-2 pl-3 pr-9 ${
|
||||
selectedOptions.some(selected => selected.id === option.id) ? 'text-white bg-emerald-600' : 'text-gray-900 hover:bg-emerald-100 hover:text-emerald-900'
|
||||
selectedOptions.some((selected) => selected.id === option.id)
|
||||
? 'text-white bg-emerald-600'
|
||||
: 'text-gray-900 hover:bg-emerald-100 hover:text-emerald-900'
|
||||
}`}
|
||||
onClick={() => handleSelect(option)}
|
||||
>
|
||||
<span className={`block truncate ${selectedOptions.some(selected => selected.id === option.id) ? 'font-semibold' : 'font-normal'}`}>
|
||||
<span
|
||||
className={`block truncate ${selectedOptions.some((selected) => selected.id === option.id) ? 'font-semibold' : 'font-normal'}`}
|
||||
>
|
||||
{option.name}
|
||||
</span>
|
||||
{selectedOptions.some(selected => selected.id === option.id) && (
|
||||
{selectedOptions.some(
|
||||
(selected) => selected.id === option.id
|
||||
) && (
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-4 text-white">
|
||||
<Check className="h-5 w-5" />
|
||||
</span>
|
||||
@ -78,4 +98,4 @@ const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
||||
export default MultiSelect;
|
||||
|
||||
@ -1,45 +1,57 @@
|
||||
import React from 'react';
|
||||
import {useTranslations} from 'next-intl';
|
||||
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="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)} />
|
||||
)}
|
||||
{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}>
|
||||
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}>
|
||||
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>
|
||||
);
|
||||
|
||||
@ -8,12 +8,19 @@ const paymentModesOptions = [
|
||||
{ id: 4, name: 'Espèce' },
|
||||
];
|
||||
|
||||
const PaymentModeSelector = ({ paymentModes, setPaymentModes, handleEdit, type }) => {
|
||||
const PaymentModeSelector = ({
|
||||
paymentModes,
|
||||
setPaymentModes,
|
||||
handleEdit,
|
||||
type,
|
||||
}) => {
|
||||
const [activePaymentModes, setActivePaymentModes] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialiser activePaymentModes avec les modes dont is_active est à true
|
||||
const activeModes = paymentModes.filter(mode => mode.is_active).map(mode => mode.mode);
|
||||
const activeModes = paymentModes
|
||||
.filter((mode) => mode.is_active)
|
||||
.map((mode) => mode.mode);
|
||||
setActivePaymentModes(activeModes);
|
||||
}, [paymentModes]);
|
||||
|
||||
@ -24,9 +31,12 @@ const PaymentModeSelector = ({ paymentModes, setPaymentModes, handleEdit, type }
|
||||
: [...prevActiveModes, modeId];
|
||||
|
||||
// Mettre à jour le mode de paiement dans le backend
|
||||
const updatedMode = paymentModes.find(mode => mode.mode === modeId);
|
||||
const updatedMode = paymentModes.find((mode) => mode.mode === modeId);
|
||||
if (updatedMode) {
|
||||
handleEdit(updatedMode.id, { ...updatedMode, is_active: !updatedMode.is_active });
|
||||
handleEdit(updatedMode.id, {
|
||||
...updatedMode,
|
||||
is_active: !updatedMode.is_active,
|
||||
});
|
||||
}
|
||||
|
||||
return newActiveModes;
|
||||
@ -59,4 +69,4 @@ const PaymentModeSelector = ({ paymentModes, setPaymentModes, handleEdit, type }
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentModeSelector;
|
||||
export default PaymentModeSelector;
|
||||
|
||||
@ -12,24 +12,33 @@ const paymentPlansOptions = [
|
||||
{ id: 3, name: '12 fois', frequency: 12 },
|
||||
];
|
||||
|
||||
const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }) => {
|
||||
const PaymentPlanSelector = ({
|
||||
paymentPlans,
|
||||
setPaymentPlans,
|
||||
handleEdit,
|
||||
type,
|
||||
}) => {
|
||||
const [dates, setDates] = useState({});
|
||||
const [selectedFrequency, setSelectedFrequency] = useState(null);
|
||||
const [activeFrequencies, setActiveFrequencies] = useState([]);
|
||||
const [defaultDay, setDefaultDay] = useState('-');
|
||||
const [isDefaultDayModified, setIsDefaultDayModified] = useState(false);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const [resetModifiedDates, setResetModifiedDates] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (paymentPlans && paymentPlans.length > 0) {
|
||||
const activePlans = paymentPlans.filter(plan => plan.is_active);
|
||||
const frequencies = activePlans.map(plan => {
|
||||
const paymentPlanOption = paymentPlansOptions.find(p => p.frequency === plan.frequency);
|
||||
return paymentPlanOption ? paymentPlanOption.id : null;
|
||||
}).filter(id => id !== null);
|
||||
const activePlans = paymentPlans.filter((plan) => plan.is_active);
|
||||
const frequencies = activePlans
|
||||
.map((plan) => {
|
||||
const paymentPlanOption = paymentPlansOptions.find(
|
||||
(p) => p.frequency === plan.frequency
|
||||
);
|
||||
return paymentPlanOption ? paymentPlanOption.id : null;
|
||||
})
|
||||
.filter((id) => id !== null);
|
||||
setActiveFrequencies(frequencies);
|
||||
|
||||
if (activePlans.length > 0) {
|
||||
@ -38,8 +47,10 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
|
||||
}
|
||||
|
||||
const initialDates = {};
|
||||
paymentPlans.forEach(plan => {
|
||||
const paymentPlanOption = paymentPlansOptions.find(p => p.frequency === plan.frequency);
|
||||
paymentPlans.forEach((plan) => {
|
||||
const paymentPlanOption = paymentPlansOptions.find(
|
||||
(p) => p.frequency === plan.frequency
|
||||
);
|
||||
if (paymentPlanOption) {
|
||||
initialDates[paymentPlanOption.id] = plan.due_dates;
|
||||
}
|
||||
@ -55,8 +66,8 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
|
||||
const updateDefaultDay = () => {
|
||||
const currentDates = dates[selectedFrequency];
|
||||
if (currentDates && currentDates.length > 0) {
|
||||
const days = currentDates.map(date => new Date(date).getDate());
|
||||
const allSameDay = days.every(day => day === days[0]);
|
||||
const days = currentDates.map((date) => new Date(date).getDate());
|
||||
const allSameDay = days.every((day) => day === days[0]);
|
||||
if (allSameDay) {
|
||||
setDefaultDay(days[0]);
|
||||
} else {
|
||||
@ -69,32 +80,44 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
|
||||
};
|
||||
|
||||
const handleActivationChange = (value) => {
|
||||
const selectedPlan = paymentPlans.find(plan => plan.frequency === paymentPlansOptions.find(p => p.id === value)?.frequency);
|
||||
const selectedPlan = paymentPlans.find(
|
||||
(plan) =>
|
||||
plan.frequency ===
|
||||
paymentPlansOptions.find((p) => p.id === value)?.frequency
|
||||
);
|
||||
if (!selectedPlan) return;
|
||||
|
||||
const updatedData = {
|
||||
...selectedPlan,
|
||||
is_active: !selectedPlan.is_active
|
||||
is_active: !selectedPlan.is_active,
|
||||
};
|
||||
|
||||
handleEdit(selectedPlan.id, updatedData)
|
||||
.then(() => {
|
||||
setPaymentPlans(prevPlans => prevPlans.map(plan =>
|
||||
plan.id === selectedPlan.id ? { ...plan, is_active: updatedData.is_active } : plan
|
||||
));
|
||||
setActiveFrequencies(prevFrequencies => {
|
||||
setPaymentPlans((prevPlans) =>
|
||||
prevPlans.map((plan) =>
|
||||
plan.id === selectedPlan.id
|
||||
? { ...plan, is_active: updatedData.is_active }
|
||||
: plan
|
||||
)
|
||||
);
|
||||
setActiveFrequencies((prevFrequencies) => {
|
||||
if (updatedData.is_active) {
|
||||
setPopupMessage(`L'option de paiement en ${paymentPlansOptions.find(p => p.id === value).name} a été activée.`);
|
||||
setPopupMessage(
|
||||
`L'option de paiement en ${paymentPlansOptions.find((p) => p.id === value).name} a été activée.`
|
||||
);
|
||||
setPopupVisible(true);
|
||||
return [...prevFrequencies, value];
|
||||
} else {
|
||||
setPopupMessage(`L'option de paiement en ${paymentPlansOptions.find(p => p.id === value).name} a été désactivée.`);
|
||||
setPopupMessage(
|
||||
`L'option de paiement en ${paymentPlansOptions.find((p) => p.id === value).name} a été désactivée.`
|
||||
);
|
||||
setPopupVisible(true);
|
||||
return prevFrequencies.filter(item => item !== value);
|
||||
return prevFrequencies.filter((item) => item !== value);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
@ -106,18 +129,21 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
|
||||
} else {
|
||||
setSelectedFrequency(value);
|
||||
if (!dates[value]) {
|
||||
const frequencyValue = paymentPlansOptions.find(plan => plan.id === value)?.frequency || 1;
|
||||
const newDates = Array(frequencyValue).fill('').map((_, index) => {
|
||||
const newDate = new Date();
|
||||
newDate.setDate(defaultDay);
|
||||
if (value === 1) {
|
||||
newDate.setMonth(newDate.getMonth() + index * 4); // Espacer de 4 mois pour le paiement en 3 fois
|
||||
} else {
|
||||
newDate.setMonth(newDate.getMonth() + index);
|
||||
}
|
||||
return newDate.toISOString().split('T')[0];
|
||||
});
|
||||
setDates(prevDates => ({ ...prevDates, [value]: newDates }));
|
||||
const frequencyValue =
|
||||
paymentPlansOptions.find((plan) => plan.id === value)?.frequency || 1;
|
||||
const newDates = Array(frequencyValue)
|
||||
.fill('')
|
||||
.map((_, index) => {
|
||||
const newDate = new Date();
|
||||
newDate.setDate(defaultDay);
|
||||
if (value === 1) {
|
||||
newDate.setMonth(newDate.getMonth() + index * 4); // Espacer de 4 mois pour le paiement en 3 fois
|
||||
} else {
|
||||
newDate.setMonth(newDate.getMonth() + index);
|
||||
}
|
||||
return newDate.toISOString().split('T')[0];
|
||||
});
|
||||
setDates((prevDates) => ({ ...prevDates, [value]: newDates }));
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -154,30 +180,39 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
|
||||
setTimeout(() => setResetModifiedDates(false), 0);
|
||||
|
||||
// Mettre à jour les dates d'échéance en fonction du jour sélectionné
|
||||
const updatedDates = dates[selectedFrequency].map(date => {
|
||||
const updatedDates = dates[selectedFrequency].map((date) => {
|
||||
const newDate = new Date(date);
|
||||
newDate.setDate(day);
|
||||
return newDate.toISOString().split('T')[0];
|
||||
});
|
||||
setDates(prevDates => ({ ...prevDates, [selectedFrequency]: updatedDates }));
|
||||
setDates((prevDates) => ({
|
||||
...prevDates,
|
||||
[selectedFrequency]: updatedDates,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmitDefaultDay = () => {
|
||||
const selectedPlan = paymentPlans.find(plan => plan.frequency === paymentPlansOptions.find(p => p.id === selectedFrequency)?.frequency);
|
||||
const selectedPlan = paymentPlans.find(
|
||||
(plan) =>
|
||||
plan.frequency ===
|
||||
paymentPlansOptions.find((p) => p.id === selectedFrequency)?.frequency
|
||||
);
|
||||
if (!selectedPlan) return;
|
||||
|
||||
const updatedData = {
|
||||
...selectedPlan,
|
||||
due_dates: dates[selectedFrequency]
|
||||
due_dates: dates[selectedFrequency],
|
||||
};
|
||||
|
||||
handleEdit(selectedPlan.id, updatedData)
|
||||
.then(() => {
|
||||
setPopupMessage(`Mise à jour des dates d'échéances effectuée avec succès`);
|
||||
.then(() => {
|
||||
setPopupMessage(
|
||||
`Mise à jour des dates d'échéances effectuée avec succès`
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setIsDefaultDayModified(false);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
@ -190,7 +225,9 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
|
||||
const renderCell = (row, column) => {
|
||||
switch (column) {
|
||||
case 'OPTIONS':
|
||||
return <span className="text-sm font-medium text-gray-900">{row.name}</span>;
|
||||
return (
|
||||
<span className="text-sm font-medium text-gray-900">{row.name}</span>
|
||||
);
|
||||
case 'ACTIONS':
|
||||
return (
|
||||
<button
|
||||
@ -199,9 +236,17 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
|
||||
e.stopPropagation();
|
||||
handleActivationChange(row.id);
|
||||
}}
|
||||
className={activeFrequencies.includes(row.id) ? 'text-emerald-500 hover:text-emerald-700' : 'text-orange-500 hover:text-orange-700'}
|
||||
className={
|
||||
activeFrequencies.includes(row.id)
|
||||
? 'text-emerald-500 hover:text-emerald-700'
|
||||
: 'text-orange-500 hover:text-orange-700'
|
||||
}
|
||||
>
|
||||
{activeFrequencies.includes(row.id) ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5" />}
|
||||
{activeFrequencies.includes(row.id) ? (
|
||||
<Eye className="w-5 h-5" />
|
||||
) : (
|
||||
<EyeOff className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
default:
|
||||
@ -209,7 +254,11 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
|
||||
}
|
||||
};
|
||||
|
||||
const selectedPaymentPlan = paymentPlans.find(plan => plan.frequency === paymentPlansOptions.find(p => p.id === selectedFrequency)?.frequency);
|
||||
const selectedPaymentPlan = paymentPlans.find(
|
||||
(plan) =>
|
||||
plan.frequency ===
|
||||
paymentPlansOptions.find((p) => p.id === selectedFrequency)?.frequency
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@ -275,4 +324,4 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentPlanSelector;
|
||||
export default PaymentPlanSelector;
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { formatPhoneNumber } from "@/utils/Telephone";
|
||||
import { formatPhoneNumber } from '@/utils/Telephone';
|
||||
|
||||
export function PhoneLabel({phoneNumber}){
|
||||
return (
|
||||
<a className="text-sm font-semibold text-gray-800" href={"tel:"+phoneNumber}>{formatPhoneNumber(phoneNumber)}</a>
|
||||
);
|
||||
}
|
||||
export function PhoneLabel({ phoneNumber }) {
|
||||
return (
|
||||
<a
|
||||
className="text-sm font-semibold text-gray-800"
|
||||
href={'tel:' + phoneNumber}
|
||||
>
|
||||
{formatPhoneNumber(phoneNumber)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = false }) => {
|
||||
const Popup = ({
|
||||
visible,
|
||||
message,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
uniqueConfirmButton = false,
|
||||
}) => {
|
||||
if (!visible) return null;
|
||||
|
||||
// Vérifier si le message est une chaîne de caractères
|
||||
@ -13,17 +19,15 @@ const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = fa
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div className="bg-white p-6 rounded-lg shadow-xl max-w-md w-full">
|
||||
<div className="mb-4">
|
||||
{isStringMessage ? (
|
||||
// Afficher le message sous forme de lignes si c'est une chaîne
|
||||
messageLines.map((line, index) => (
|
||||
<p key={index} className="text-gray-700">
|
||||
{line}
|
||||
</p>
|
||||
))
|
||||
) : (
|
||||
// Sinon, afficher directement le contenu React
|
||||
message
|
||||
)}
|
||||
{isStringMessage
|
||||
? // Afficher le message sous forme de lignes si c'est une chaîne
|
||||
messageLines.map((line, index) => (
|
||||
<p key={index} className="text-gray-700">
|
||||
{line}
|
||||
</p>
|
||||
))
|
||||
: // Sinon, afficher directement le contenu React
|
||||
message}
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
{!uniqueConfirmButton && (
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Trash2, Eye, EyeOff, ToggleLeft, ToggleRight, Info, XCircle } from 'lucide-react';
|
||||
import {
|
||||
Trash2,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
Info,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import Popup from '@/components/Popup';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
@ -32,14 +40,23 @@ const roleTypeToBadgeClass = (roleType) => {
|
||||
}
|
||||
};
|
||||
|
||||
const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeleteProfile, handleDissociateGuardian }) => {
|
||||
const parentProfiles = profileRoles.filter(profileRole => profileRole.role_type === 2);
|
||||
const schoolAdminProfiles = profileRoles.filter(profileRole => profileRole.role_type !== 2);
|
||||
const ProfileDirectory = ({
|
||||
profileRoles,
|
||||
handleActivateProfile,
|
||||
handleDeleteProfile,
|
||||
handleDissociateGuardian,
|
||||
}) => {
|
||||
const parentProfiles = profileRoles.filter(
|
||||
(profileRole) => profileRole.role_type === 2
|
||||
);
|
||||
const schoolAdminProfiles = profileRoles.filter(
|
||||
(profileRole) => profileRole.role_type !== 2
|
||||
);
|
||||
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
|
||||
const [confirmPopupMessage, setConfirmPopupMessage] = useState("");
|
||||
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
|
||||
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
|
||||
const [visibleTooltipId, setVisibleTooltipId] = useState(null);
|
||||
|
||||
@ -49,18 +66,24 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
|
||||
const handleTooltipHide = () => {
|
||||
setVisibleTooltipId(null); // Cacher toutes les tooltips
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmActivateProfile = (profileRole) => {
|
||||
setConfirmPopupMessage(`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`);
|
||||
setConfirmPopupMessage(
|
||||
`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`
|
||||
);
|
||||
setConfirmPopupOnConfirm(() => () => {
|
||||
handleActivateProfile(profileRole)
|
||||
.then(() => {
|
||||
setPopupMessage(`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`);
|
||||
setPopupMessage(
|
||||
`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`
|
||||
);
|
||||
setPopupVisible(true);
|
||||
})
|
||||
.catch(error => {
|
||||
setPopupMessage(`Erreur lors de la ${profileRole.is_active ? 'désactivation' : 'activation'} du profil.`);
|
||||
.catch((error) => {
|
||||
setPopupMessage(
|
||||
`Erreur lors de la ${profileRole.is_active ? 'désactivation' : 'activation'} du profil.`
|
||||
);
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
@ -69,15 +92,15 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
};
|
||||
|
||||
const handleConfirmDeleteProfile = (id) => {
|
||||
setConfirmPopupMessage("Êtes-vous sûr de vouloir supprimer ce profil ?");
|
||||
setConfirmPopupMessage('Êtes-vous sûr de vouloir supprimer ce profil ?');
|
||||
setConfirmPopupOnConfirm(() => () => {
|
||||
handleDeleteProfile(id)
|
||||
.then(() => {
|
||||
setPopupMessage("Le profil a été supprimé avec succès.");
|
||||
setPopupMessage('Le profil a été supprimé avec succès.');
|
||||
setPopupVisible(true);
|
||||
})
|
||||
.catch(error => {
|
||||
setPopupMessage("Erreur lors de la suppression du profil.");
|
||||
.catch((error) => {
|
||||
setPopupMessage('Erreur lors de la suppression du profil.');
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
@ -93,11 +116,11 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
setConfirmPopupOnConfirm(() => () => {
|
||||
handleDissociateGuardian(student.id, profileRole.associated_person?.id)
|
||||
.then(() => {
|
||||
setPopupMessage("Le responsable a été dissocié avec succès.");
|
||||
setPopupMessage('Le responsable a été dissocié avec succès.');
|
||||
setPopupVisible(true);
|
||||
})
|
||||
.catch(error => {
|
||||
setPopupMessage("Erreur lors de la dissociation du responsable.");
|
||||
.catch((error) => {
|
||||
setPopupMessage('Erreur lors de la dissociation du responsable.');
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
@ -108,11 +131,15 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
const parentColumns = [
|
||||
{ name: 'Identifiant', transform: (row) => row.associated_profile_email },
|
||||
{ name: 'Mise à jour', transform: (row) => row.updated_date_formatted },
|
||||
{ name: 'Rôle', transform: (row) => (
|
||||
<span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}>
|
||||
{
|
||||
name: 'Rôle',
|
||||
transform: (row) => (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}
|
||||
>
|
||||
{roleTypeToLabel(row.role_type)}
|
||||
</span>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Utilisateur',
|
||||
@ -121,47 +148,53 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
<span>{row.associated_person?.guardian_name}</span>
|
||||
{row.associated_person && (
|
||||
<div
|
||||
className="relative group"
|
||||
onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne
|
||||
onMouseLeave={handleTooltipHide} // Cacher la tooltip
|
||||
>
|
||||
<button className="relative text-blue-500 hover:text-blue-700 flex items-center justify-center">
|
||||
<div className="w-6 h-6 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center font-bold">
|
||||
{row.associated_person?.students?.length || 0}
|
||||
</div>
|
||||
</button>
|
||||
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
|
||||
<div
|
||||
className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2"
|
||||
>
|
||||
<div className="mb-2">
|
||||
<strong>Elève(s) associé(s):</strong>
|
||||
<div className="flex flex-col justify-center space-y-2 mt-4">
|
||||
{row.associated_person?.students?.map(student => (
|
||||
<div key={student.student_name} className="flex justify-between items-center">
|
||||
<span className="px-2 py-1 rounded-full text-gray-800 whitespace-nowrap inline-block min-w-0 max-w-fit">
|
||||
{student.student_name}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<StatusLabel status={student.registration_status} showDropdown={false} />
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700 flex items-center space-x-1"
|
||||
onClick={() => handleConfirmDissociateGuardian(row, student)}
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
<span className="text-sm">Dissocier</span>
|
||||
</button>
|
||||
className="relative group"
|
||||
onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne
|
||||
onMouseLeave={handleTooltipHide} // Cacher la tooltip
|
||||
>
|
||||
<button className="relative text-blue-500 hover:text-blue-700 flex items-center justify-center">
|
||||
<div className="w-6 h-6 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center font-bold">
|
||||
{row.associated_person?.students?.length || 0}
|
||||
</div>
|
||||
</button>
|
||||
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
|
||||
<div className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2">
|
||||
<div className="mb-2">
|
||||
<strong>Elève(s) associé(s):</strong>
|
||||
<div className="flex flex-col justify-center space-y-2 mt-4">
|
||||
{row.associated_person?.students?.map((student) => (
|
||||
<div
|
||||
key={student.student_name}
|
||||
className="flex justify-between items-center"
|
||||
>
|
||||
<span className="px-2 py-1 rounded-full text-gray-800 whitespace-nowrap inline-block min-w-0 max-w-fit">
|
||||
{student.student_name}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<StatusLabel
|
||||
status={student.registration_status}
|
||||
showDropdown={false}
|
||||
/>
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700 flex items-center space-x-1"
|
||||
onClick={() =>
|
||||
handleConfirmDissociateGuardian(row, student)
|
||||
}
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
<span className="text-sm">Dissocier</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
@ -169,10 +202,18 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className={row.is_active ? 'text-emerald-500 hover:text-emerald-700' : 'text-orange-500 hover:text-orange-700'}
|
||||
className={
|
||||
row.is_active
|
||||
? 'text-emerald-500 hover:text-emerald-700'
|
||||
: 'text-orange-500 hover:text-orange-700'
|
||||
}
|
||||
onClick={() => handleConfirmActivateProfile(row)}
|
||||
>
|
||||
{row.is_active ? <ToggleRight className="w-5 h-5 " /> : <ToggleLeft className="w-5 h-5" />}
|
||||
{row.is_active ? (
|
||||
<ToggleRight className="w-5 h-5 " />
|
||||
) : (
|
||||
<ToggleLeft className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -182,20 +223,26 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const schoolAdminColumns = [
|
||||
{ name: 'Identifiant', transform: (row) => row.associated_profile_email },
|
||||
{ name: 'Mise à jour', transform: (row) => row.updated_date_formatted },
|
||||
{ name: 'Rôle', transform: (row) => (
|
||||
<span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}>
|
||||
{
|
||||
name: 'Rôle',
|
||||
transform: (row) => (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}
|
||||
>
|
||||
{roleTypeToLabel(row.role_type)}
|
||||
</span>
|
||||
)
|
||||
),
|
||||
},
|
||||
{ name: 'Utilisateur', transform: (row) => (
|
||||
{
|
||||
name: 'Utilisateur',
|
||||
transform: (row) => (
|
||||
<div className="flex items-center justify-center space-x-2 relative">
|
||||
<span>{row.associated_person?.teacher_name}</span>
|
||||
{row.associated_person && (
|
||||
@ -210,14 +257,15 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
</div>
|
||||
</button>
|
||||
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
|
||||
<div
|
||||
className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2"
|
||||
>
|
||||
<div className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2">
|
||||
<div className="mb-2">
|
||||
<strong>Classes associées:</strong>
|
||||
<div className="flex flex-wrap justify-center space-x-2 mt-4">
|
||||
{row.associated_person?.classes?.map(classe => (
|
||||
<span key={classe.id} className="px-2 py-1 rounded-full bg-gray-200 text-gray-800">
|
||||
{row.associated_person?.classes?.map((classe) => (
|
||||
<span
|
||||
key={classe.id}
|
||||
className="px-2 py-1 rounded-full bg-gray-200 text-gray-800"
|
||||
>
|
||||
{classe.name}
|
||||
</span>
|
||||
))}
|
||||
@ -226,9 +274,15 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
<div>
|
||||
<strong>Spécialités:</strong>
|
||||
<div className="flex flex-wrap justify-center space-x-2 mt-4">
|
||||
{row.associated_person?.specialities?.map(speciality => (
|
||||
<SpecialityItem key={speciality.name} speciality={speciality} isDraggable={false} />
|
||||
))}
|
||||
{row.associated_person?.specialities?.map(
|
||||
(speciality) => (
|
||||
<SpecialityItem
|
||||
key={speciality.name}
|
||||
speciality={speciality}
|
||||
isDraggable={false}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -236,7 +290,7 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
@ -244,10 +298,18 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className={row.is_active ? 'text-emerald-500 hover:text-emerald-700' : 'text-orange-500 hover:text-orange-700'}
|
||||
className={
|
||||
row.is_active
|
||||
? 'text-emerald-500 hover:text-emerald-700'
|
||||
: 'text-orange-500 hover:text-orange-700'
|
||||
}
|
||||
onClick={() => handleConfirmActivateProfile(row)}
|
||||
>
|
||||
{row.is_active ? <ToggleRight className="w-5 h-5 " /> : <ToggleLeft className="w-5 h-5" />}
|
||||
{row.is_active ? (
|
||||
<ToggleRight className="w-5 h-5 " />
|
||||
) : (
|
||||
<ToggleLeft className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -257,8 +319,8 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@ -268,20 +330,14 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
{parentProfiles.length === 0 ? (
|
||||
<div>Aucun profil trouvé</div>
|
||||
) : (
|
||||
<Table
|
||||
data={parentProfiles}
|
||||
columns={parentColumns}
|
||||
/>
|
||||
<Table data={parentProfiles} columns={parentColumns} />
|
||||
)}
|
||||
</div>
|
||||
<div className="max-h-128 overflow-y-auto border rounded p-4">
|
||||
{schoolAdminProfiles.length === 0 ? (
|
||||
<div>Aucun profil trouvé</div>
|
||||
) : (
|
||||
<Table
|
||||
data={schoolAdminProfiles}
|
||||
columns={schoolAdminColumns}
|
||||
/>
|
||||
<Table data={schoolAdminProfiles} columns={schoolAdminColumns} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -301,4 +357,4 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileDirectory;
|
||||
export default ProfileDirectory;
|
||||
|
||||
@ -5,12 +5,19 @@ import { getRightStr } from '@/utils/rights';
|
||||
import { ChevronDown } from 'lucide-react'; // Import de l'icône
|
||||
|
||||
const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
|
||||
const { establishments, selectedEstablishmentId, setSelectedEstablishmentId, setProfileRole } = useEstablishment();
|
||||
const {
|
||||
establishments,
|
||||
selectedEstablishmentId,
|
||||
setSelectedEstablishmentId,
|
||||
setProfileRole,
|
||||
} = useEstablishment();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
const handleEstablishmentChange = (establishmentId) => {
|
||||
setSelectedEstablishmentId(establishmentId);
|
||||
const role = establishments.find(est => est.id === establishmentId)?.role_type;
|
||||
const role = establishments.find(
|
||||
(est) => est.id === establishmentId
|
||||
)?.role_type;
|
||||
setProfileRole(role);
|
||||
|
||||
if (onEstablishmentChange) {
|
||||
@ -20,11 +27,17 @@ const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
|
||||
};
|
||||
|
||||
// Si on a pas de rôle ou un seul rôle, on n'affiche pas le sélecteur
|
||||
if (!establishments || establishments.length === 0 || establishments.length === 1) {
|
||||
if (
|
||||
!establishments ||
|
||||
establishments.length === 0 ||
|
||||
establishments.length === 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedEstablishment = establishments.find(est => est.id === selectedEstablishmentId);
|
||||
const selectedEstablishment = establishments.find(
|
||||
(est) => est.id === selectedEstablishmentId
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
@ -32,7 +45,9 @@ const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
|
||||
buttonContent={
|
||||
<div className="h-16 flex items-center gap-2 cursor-pointer px-4 bg-white">
|
||||
<div className="flex-1">
|
||||
<div className="font-bold text-left">{getRightStr(selectedEstablishment?.role_type) || ''}</div>
|
||||
<div className="font-bold text-left">
|
||||
{getRightStr(selectedEstablishment?.role_type) || ''}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 text-left">
|
||||
{selectedEstablishment?.name || 'Sélectionnez un établissement'}
|
||||
</div>
|
||||
@ -45,11 +60,13 @@ const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
items={establishments.map(establishment => ({
|
||||
items={establishments.map((establishment) => ({
|
||||
type: 'item',
|
||||
label: (
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{getRightStr(establishment.role_type)}</div>
|
||||
<div className="font-bold">
|
||||
{getRightStr(establishment.role_type)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{establishment.name}</div>
|
||||
</div>
|
||||
),
|
||||
@ -64,4 +81,4 @@ const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSelector;
|
||||
export default ProfileSelector;
|
||||
|
||||
@ -1,157 +1,186 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const Step = ({ number, title, isActive, isValid, isCompleted, onClick }) => {
|
||||
return (
|
||||
<div className="flex-shrink-0 flex justify-center relative mx-4">
|
||||
<div className={`
|
||||
return (
|
||||
<div className="flex-shrink-0 flex justify-center relative mx-4">
|
||||
<div
|
||||
className={`
|
||||
w-8 h-8 rounded-full
|
||||
flex items-center justify-center
|
||||
text-sm font-semibold
|
||||
${isCompleted
|
||||
${
|
||||
isCompleted
|
||||
? 'bg-emerald-600 text-white'
|
||||
: isActive
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}
|
||||
`}>
|
||||
{isCompleted ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
number
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute top-12 left-1/2 -translate-x-1/2">
|
||||
<span className={`
|
||||
`}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
number
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute top-12 left-1/2 -translate-x-1/2">
|
||||
<span
|
||||
className={`
|
||||
text-xs font-medium w-20 text-center block break-words
|
||||
${isActive ? 'text-emerald-600' : 'text-gray-500'}
|
||||
`}>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SpacerStep = ({ isCompleted }) => {
|
||||
return (
|
||||
<div className={`flex-1 h-0.5 ${isCompleted ? 'bg-emerald-600' : 'bg-gray-200'}`} />
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={`flex-1 h-0.5 ${isCompleted ? 'bg-emerald-600' : 'bg-gray-200'}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Dots = () => {
|
||||
return (
|
||||
<div className="text-gray-500 relative flex items-center mx-4">
|
||||
<span>...</span>
|
||||
<div className="absolute top-8 left-1/2 -translate-x-1/2">
|
||||
<span className="text-xs font-medium w-20 text-center block">...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="text-gray-500 relative flex items-center mx-4">
|
||||
<span>...</span>
|
||||
<div className="absolute top-8 left-1/2 -translate-x-1/2">
|
||||
<span className="text-xs font-medium w-20 text-center block">...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProgressStep = ({ steps, stepTitles, currentStep, setStep, isStepValid }) => {
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const [visibleSteps, setVisibleSteps] = useState(steps);
|
||||
const ProgressStep = ({
|
||||
steps,
|
||||
stepTitles,
|
||||
currentStep,
|
||||
setStep,
|
||||
isStepValid,
|
||||
}) => {
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const [visibleSteps, setVisibleSteps] = useState(steps);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setWindowWidth(window.innerWidth);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const handleResize = () => setWindowWidth(window.innerWidth);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateVisibleSteps = () => {
|
||||
const minWidth = 150; // Largeur minimale estimée par étape
|
||||
const maxVisibleSteps = Math.floor(windowWidth / minWidth);
|
||||
useEffect(() => {
|
||||
const calculateVisibleSteps = () => {
|
||||
const minWidth = 150; // Largeur minimale estimée par étape
|
||||
const maxVisibleSteps = Math.floor(windowWidth / minWidth);
|
||||
|
||||
if (maxVisibleSteps >= steps.length) {
|
||||
setVisibleSteps(steps);
|
||||
return;
|
||||
}
|
||||
if (maxVisibleSteps >= steps.length) {
|
||||
setVisibleSteps(steps);
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxVisibleSteps < 4) {
|
||||
// Garder seulement première, dernière et courante
|
||||
let filtered = [steps[0]];
|
||||
if (currentStep > 1 && currentStep < steps.length) {
|
||||
filtered.push('...');
|
||||
filtered.push(steps[currentStep - 1]);
|
||||
}
|
||||
if (currentStep < steps.length) {
|
||||
filtered.push('...');
|
||||
}
|
||||
filtered.push(steps[steps.length - 1]);
|
||||
setVisibleSteps(filtered);
|
||||
} else {
|
||||
// Garder première, dernière, courante et quelques étapes adjacentes
|
||||
let filtered = [steps[0]];
|
||||
if (currentStep > 2) filtered.push('...');
|
||||
if (currentStep > 1 && currentStep < steps.length) {
|
||||
filtered.push(steps[currentStep - 1]);
|
||||
}
|
||||
if (currentStep < steps.length - 1) filtered.push('...');
|
||||
filtered.push(steps[steps.length - 1]);
|
||||
setVisibleSteps(filtered);
|
||||
}
|
||||
};
|
||||
|
||||
calculateVisibleSteps();
|
||||
}, [windowWidth, currentStep, steps]);
|
||||
|
||||
const handleStepClick = (stepIndex) => {
|
||||
// Vérifie si on peut naviguer vers l'étape (toutes les étapes précédentes doivent être valides)
|
||||
const canNavigate = Array.from({ length: stepIndex }, (_, i) => i + 1)
|
||||
.every(step => isStepValid(step));
|
||||
|
||||
if (canNavigate) {
|
||||
setStep(stepIndex + 1);
|
||||
if (maxVisibleSteps < 4) {
|
||||
// Garder seulement première, dernière et courante
|
||||
let filtered = [steps[0]];
|
||||
if (currentStep > 1 && currentStep < steps.length) {
|
||||
filtered.push('...');
|
||||
filtered.push(steps[currentStep - 1]);
|
||||
}
|
||||
if (currentStep < steps.length) {
|
||||
filtered.push('...');
|
||||
}
|
||||
filtered.push(steps[steps.length - 1]);
|
||||
setVisibleSteps(filtered);
|
||||
} else {
|
||||
// Garder première, dernière, courante et quelques étapes adjacentes
|
||||
let filtered = [steps[0]];
|
||||
if (currentStep > 2) filtered.push('...');
|
||||
if (currentStep > 1 && currentStep < steps.length) {
|
||||
filtered.push(steps[currentStep - 1]);
|
||||
}
|
||||
if (currentStep < steps.length - 1) filtered.push('...');
|
||||
filtered.push(steps[steps.length - 1]);
|
||||
setVisibleSteps(filtered);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full py-6">
|
||||
<div className="flex items-center min-h-[100px]">
|
||||
{visibleSteps.map((step, index) => {
|
||||
if (step === '...') {
|
||||
return (
|
||||
<div key={`dots-${index}`} className="flex-1 flex items-center justify-center">
|
||||
<Dots />
|
||||
{index !== visibleSteps.length - 1 && <SpacerStep isCompleted={false} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
calculateVisibleSteps();
|
||||
}, [windowWidth, currentStep, steps]);
|
||||
|
||||
const originalIndex = steps.indexOf(step);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`
|
||||
const handleStepClick = (stepIndex) => {
|
||||
// Vérifie si on peut naviguer vers l'étape (toutes les étapes précédentes doivent être valides)
|
||||
const canNavigate = Array.from(
|
||||
{ length: stepIndex },
|
||||
(_, i) => i + 1
|
||||
).every((step) => isStepValid(step));
|
||||
|
||||
if (canNavigate) {
|
||||
setStep(stepIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full py-6">
|
||||
<div className="flex items-center min-h-[100px]">
|
||||
{visibleSteps.map((step, index) => {
|
||||
if (step === '...') {
|
||||
return (
|
||||
<div
|
||||
key={`dots-${index}`}
|
||||
className="flex-1 flex items-center justify-center"
|
||||
>
|
||||
<Dots />
|
||||
{index !== visibleSteps.length - 1 && (
|
||||
<SpacerStep isCompleted={false} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const originalIndex = steps.indexOf(step);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`
|
||||
flex-1 relative
|
||||
${Array.from({ length: originalIndex + 1 }, (_, i) => i + 1).every(s => isStepValid(s)) ? 'cursor-pointer' : 'cursor-not-allowed'}
|
||||
${Array.from({ length: originalIndex + 1 }, (_, i) => i + 1).every((s) => isStepValid(s)) ? 'cursor-pointer' : 'cursor-not-allowed'}
|
||||
`}
|
||||
onClick={() => handleStepClick(originalIndex)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="w-full flex items-center">
|
||||
<Step
|
||||
number={originalIndex + 1}
|
||||
title={stepTitles ? stepTitles[originalIndex + 1] : step}
|
||||
isActive={currentStep === originalIndex + 1}
|
||||
isCompleted={currentStep > originalIndex + 1}
|
||||
isValid={isStepValid(originalIndex + 1)}
|
||||
/>
|
||||
{index !== visibleSteps.length - 1 && (
|
||||
<SpacerStep isCompleted={currentStep > originalIndex + 1} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
onClick={() => handleStepClick(originalIndex)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="w-full flex items-center">
|
||||
<Step
|
||||
number={originalIndex + 1}
|
||||
title={stepTitles ? stepTitles[originalIndex + 1] : step}
|
||||
isActive={currentStep === originalIndex + 1}
|
||||
isCompleted={currentStep > originalIndex + 1}
|
||||
isValid={isStepValid(originalIndex + 1)}
|
||||
/>
|
||||
{index !== visibleSteps.length - 1 && (
|
||||
<SpacerStep isCompleted={currentStep > originalIndex + 1} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressStep;
|
||||
|
||||
@ -4,37 +4,33 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { FE_USERS_LOGIN_URL, getRedirectUrlFromRole } from '@/utils/Url';
|
||||
|
||||
const ProtectedRoute = ({ children, requiredRight }) => {
|
||||
|
||||
const { user, profileRole } = useEstablishment();
|
||||
const router = useRouter();
|
||||
let hasRequiredRight = false;
|
||||
|
||||
if(requiredRight && Array.isArray(requiredRight) ){
|
||||
if (requiredRight && Array.isArray(requiredRight)) {
|
||||
// Vérifier si l'utilisateur a le droit requis
|
||||
hasRequiredRight = requiredRight.some((right) => profileRole === right);
|
||||
}else{
|
||||
hasRequiredRight = (profileRole === requiredRight);
|
||||
} else {
|
||||
hasRequiredRight = profileRole === requiredRight;
|
||||
}
|
||||
|
||||
// Vérifier si l'utilisateur a au moins un rôle correspondant au requiredRight
|
||||
useEffect(() => {
|
||||
if(user){
|
||||
if (user) {
|
||||
// Vérifier si l'utilisateur a le droit requis mais pas le bon role on le redirige la page d'accueil associé au role
|
||||
if (!hasRequiredRight) {
|
||||
const redirectUrl = getRedirectUrlFromRole(profileRole);
|
||||
router.push(`${redirectUrl}`);
|
||||
}
|
||||
|
||||
}else{
|
||||
} else {
|
||||
// User non authentifié
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
}
|
||||
}, [profileRole]);
|
||||
|
||||
|
||||
|
||||
// Autoriser l'affichage si authentifié et rôle correct
|
||||
return hasRequiredRight ? children : null;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
export default ProtectedRoute;
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { SessionProvider } from "next-auth/react"
|
||||
import { CsrfProvider } from '@/context/CsrfContext'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import { CsrfProvider } from '@/context/CsrfContext';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { EstablishmentProvider } from '@/context/EstablishmentContext';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
|
||||
export default function Providers({ children, messages, locale, session }) {
|
||||
if (!locale) {
|
||||
console.error('Locale non définie dans Providers');
|
||||
@ -25,5 +24,5 @@ export default function Providers({ children, messages, locale, session }) {
|
||||
</CsrfProvider>
|
||||
</DndProvider>
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
const RadioList = ({ items, formData, handleChange, fieldName, icon: Icon, className }) => {
|
||||
const RadioList = ({
|
||||
items,
|
||||
formData,
|
||||
handleChange,
|
||||
fieldName,
|
||||
icon: Icon,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`mb-4 ${className}`}>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{items.map(item => (
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="flex items-center">
|
||||
<input
|
||||
key={`${item.id}-${Math.random()}`}
|
||||
|
||||
@ -3,12 +3,23 @@ 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 {
|
||||
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 [newSchedule, setNewSchedule] = useState({
|
||||
name: '',
|
||||
color: '#10b981',
|
||||
});
|
||||
|
||||
const handleEdit = (schedule) => {
|
||||
setEditingId(schedule.id);
|
||||
@ -19,9 +30,9 @@ export default function ScheduleNavigation() {
|
||||
const handleSave = () => {
|
||||
if (editingId) {
|
||||
updateSchedule(editingId, {
|
||||
...schedules.find(s => s.id === editingId),
|
||||
...schedules.find((s) => s.id === editingId),
|
||||
name: editedName,
|
||||
color: editedColor
|
||||
color: editedColor,
|
||||
});
|
||||
setEditingId(null);
|
||||
}
|
||||
@ -31,7 +42,7 @@ export default function ScheduleNavigation() {
|
||||
if (newSchedule.name) {
|
||||
addSchedule({
|
||||
id: `schedule-${Date.now()}`,
|
||||
...newSchedule
|
||||
...newSchedule,
|
||||
});
|
||||
setIsAddingNew(false);
|
||||
setNewSchedule({ name: '', color: '#10b981' });
|
||||
@ -55,7 +66,9 @@ export default function ScheduleNavigation() {
|
||||
<input
|
||||
type="text"
|
||||
value={newSchedule.name}
|
||||
onChange={(e) => setNewSchedule(prev => ({ ...prev, name: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewSchedule((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
className="w-full p-1 mb-2 border rounded"
|
||||
placeholder="Nom du planning"
|
||||
/>
|
||||
@ -64,7 +77,9 @@ export default function ScheduleNavigation() {
|
||||
<input
|
||||
type="color"
|
||||
value={newSchedule.color}
|
||||
onChange={(e) => setNewSchedule(prev => ({ ...prev, color: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewSchedule((prev) => ({ ...prev, color: e.target.value }))
|
||||
}
|
||||
className="w-8 h-8"
|
||||
/>
|
||||
</div>
|
||||
@ -86,11 +101,13 @@ export default function ScheduleNavigation() {
|
||||
)}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{schedules.map(schedule => (
|
||||
{schedules.map((schedule) => (
|
||||
<li
|
||||
key={schedule.id}
|
||||
className={`p-2 rounded ${
|
||||
selectedSchedule === schedule.id ? 'bg-gray-100' : 'hover:bg-gray-50'
|
||||
selectedSchedule === schedule.id
|
||||
? 'bg-gray-100'
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{editingId === schedule.id ? (
|
||||
@ -135,7 +152,13 @@ export default function ScheduleNavigation() {
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: schedule.color }}
|
||||
/>
|
||||
<span className={hiddenSchedules.includes(schedule.id) ? 'text-gray-400' : ''}>
|
||||
<span
|
||||
className={
|
||||
hiddenSchedules.includes(schedule.id)
|
||||
? 'text-gray-400'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{schedule.name}
|
||||
</span>
|
||||
</div>
|
||||
@ -167,4 +190,4 @@ export default function ScheduleNavigation() {
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,4 +13,4 @@ const SectionTitle = ({ title }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionTitle;
|
||||
export default SectionTitle;
|
||||
|
||||
@ -1,17 +1,34 @@
|
||||
export default function SelectChoice({ type, name, label, required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) {
|
||||
export default function SelectChoice({
|
||||
type,
|
||||
name,
|
||||
label,
|
||||
required,
|
||||
placeHolder,
|
||||
choices,
|
||||
callback,
|
||||
selected,
|
||||
errorMsg,
|
||||
IconItem,
|
||||
disabled = false,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} ${disabled ? '' : 'hover:border-gray-400 focus-within:border-gray-500'}`}>
|
||||
{IconItem &&
|
||||
<div
|
||||
className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} ${disabled ? '' : 'hover:border-gray-400 focus-within:border-gray-500'}`}
|
||||
>
|
||||
{IconItem && (
|
||||
<span className="inline-flex items-center px-3 text-gray-500 text-sm">
|
||||
{<IconItem />}
|
||||
</span>
|
||||
}
|
||||
)}
|
||||
<select
|
||||
className={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md ${disabled ? 'bg-gray-100' : ''}`}
|
||||
type={type}
|
||||
@ -33,4 +50,4 @@ export default function SelectChoice({ type, name, label, required, placeHolder,
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
'use client'
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import ProfileSelector from '@/components/ProfileSelector';
|
||||
@ -7,7 +7,9 @@ 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'
|
||||
active
|
||||
? 'bg-emerald-50 text-emerald-600'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Icon size={20} />
|
||||
@ -56,4 +58,4 @@ function Sidebar({ currentPage, items, onCloseMobile, onEstablishmentChange }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
export default Sidebar;
|
||||
|
||||
@ -6,7 +6,7 @@ const SidebarTabs = ({ tabs }) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex border-b-2 border-gray-200">
|
||||
{tabs.map(tab => (
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`flex-1 p-4 ${activeTab === tab.id ? 'border-b-2 border-emerald-500 text-emerald-500' : 'text-gray-500 hover:text-emerald-500'}`}
|
||||
@ -17,8 +17,11 @@ const SidebarTabs = ({ tabs }) => {
|
||||
))}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{tabs.map(tab => (
|
||||
<div key={tab.id} className={`${activeTab === tab.id ? 'block' : 'hidden'}`}>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`${activeTab === tab.id ? 'block' : 'hidden'}`}
|
||||
>
|
||||
{tab.content}
|
||||
</div>
|
||||
))}
|
||||
@ -27,4 +30,4 @@ const SidebarTabs = ({ tabs }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarTabs;
|
||||
export default SidebarTabs;
|
||||
|
||||
@ -23,30 +23,30 @@ const Slider = ({ min, max, value, onChange }) => {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-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 justify-end">
|
||||
<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 className="flex items-center space-x-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 justify-end">
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -55,7 +55,7 @@ Slider.propTypes = {
|
||||
min: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Slider;
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
// Composant StatCard pour afficher une statistique
|
||||
const StatCard = ({ title, value, icon, color = "blue" }) => (
|
||||
const StatCard = ({ title, value, icon, color = 'blue' }) => (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-gray-500 text-sm font-medium">{title}</h3>
|
||||
<p className="text-2xl font-semibold mt-1">{value}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-full bg-${color}-100`}>
|
||||
{icon}
|
||||
</div>
|
||||
<div className={`p-3 rounded-full bg-${color}-100`}>{icon}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default StatCard;
|
||||
export default StatCard;
|
||||
|
||||
@ -22,25 +22,25 @@ const StatusLabel = ({ status, onChange, showDropdown = true, parent }) => {
|
||||
{ value: 7, label: 'En attente SEPA' },
|
||||
];
|
||||
|
||||
const currentStatus = statusOptions.find(option => option.value === status);
|
||||
const currentStatus = statusOptions.find((option) => option.value === status);
|
||||
|
||||
// Définir les couleurs en fonction du statut
|
||||
const getStatusClass = () => {
|
||||
if (parent) {
|
||||
return (
|
||||
status === 2 && 'bg-orange-50 text-orange-600' ||
|
||||
status === 3 && 'bg-purple-50 text-purple-600' ||
|
||||
status === 7 && 'bg-yellow-50 text-yellow-600'
|
||||
(status === 2 && 'bg-orange-50 text-orange-600') ||
|
||||
(status === 3 && 'bg-purple-50 text-purple-600') ||
|
||||
(status === 7 && 'bg-yellow-50 text-yellow-600')
|
||||
);
|
||||
}
|
||||
return (
|
||||
status === 1 && 'bg-blue-50 text-blue-600' ||
|
||||
status === 2 && 'bg-orange-50 text-orange-600' ||
|
||||
status === 3 && 'bg-purple-50 text-purple-600' ||
|
||||
status === 4 && 'bg-red-50 text-red-600' ||
|
||||
status === 5 && 'bg-green-50 text-green-600' ||
|
||||
status === 6 && 'bg-red-50 text-red-600' ||
|
||||
status === 7 && 'bg-yellow-50 text-yellow-600'
|
||||
(status === 1 && 'bg-blue-50 text-blue-600') ||
|
||||
(status === 2 && 'bg-orange-50 text-orange-600') ||
|
||||
(status === 3 && 'bg-purple-50 text-purple-600') ||
|
||||
(status === 4 && 'bg-red-50 text-red-600') ||
|
||||
(status === 5 && 'bg-green-50 text-green-600') ||
|
||||
(status === 6 && 'bg-red-50 text-red-600') ||
|
||||
(status === 7 && 'bg-yellow-50 text-yellow-600')
|
||||
);
|
||||
};
|
||||
|
||||
@ -51,10 +51,13 @@ const StatusLabel = ({ status, onChange, showDropdown = true, parent }) => {
|
||||
buttonContent={
|
||||
<>
|
||||
{currentStatus ? currentStatus.label : 'Statut inconnu'}
|
||||
<ChevronUp size={16} className={`transform transition-transform duration-200 ${dropdownOpen ? 'rotate-180' : 'rotate-90'}`} />
|
||||
<ChevronUp
|
||||
size={16}
|
||||
className={`transform transition-transform duration-200 ${dropdownOpen ? 'rotate-180' : 'rotate-90'}`}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
items={statusOptions.map(option => ({
|
||||
items={statusOptions.map((option) => ({
|
||||
label: option.label,
|
||||
onClick: () => onChange(option.value),
|
||||
}))}
|
||||
@ -64,7 +67,9 @@ const StatusLabel = ({ status, onChange, showDropdown = true, parent }) => {
|
||||
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 ${getStatusClass()}`}>
|
||||
<div
|
||||
className={`w-[150px] flex items-center justify-center gap-2 px-2 py-2 rounded-md text-sm text-center font-medium ${getStatusClass()}`}
|
||||
>
|
||||
{currentStatus ? currentStatus.label : 'Statut inconnu'}
|
||||
</div>
|
||||
)}
|
||||
@ -72,4 +77,4 @@ const StatusLabel = ({ status, onChange, showDropdown = true, parent }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusLabel;
|
||||
export default StatusLabel;
|
||||
|
||||
@ -1,4 +1,13 @@
|
||||
import { Trash2, Edit3, Plus, ZoomIn, Users, Check, X, Hand } from 'lucide-react';
|
||||
import {
|
||||
Trash2,
|
||||
Edit3,
|
||||
Plus,
|
||||
ZoomIn,
|
||||
Users,
|
||||
Check,
|
||||
X,
|
||||
Hand,
|
||||
} from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import Popup from '@/components/Popup';
|
||||
@ -17,30 +26,40 @@ const ItemTypes = {
|
||||
TEACHER: 'teacher',
|
||||
};
|
||||
|
||||
const TeachersDropZone = ({ classe, handleTeachersChange, teachers, isEditing }) => {
|
||||
const [localTeachers, setLocalTeachers] = useState(classe.teachers_details || []);
|
||||
const TeachersDropZone = ({
|
||||
classe,
|
||||
handleTeachersChange,
|
||||
teachers,
|
||||
isEditing,
|
||||
}) => {
|
||||
const [localTeachers, setLocalTeachers] = useState(
|
||||
classe.teachers_details || []
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
}, [teachers]);
|
||||
useEffect(() => {}, [teachers]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalTeachers(classe.teachers_details || []);
|
||||
}, [classe.teachers_details]);
|
||||
|
||||
useEffect(() => {
|
||||
handleTeachersChange(localTeachers.map(teacher => teacher.id));
|
||||
handleTeachersChange(localTeachers.map((teacher) => teacher.id));
|
||||
}, [localTeachers]);
|
||||
|
||||
const [{ isOver, canDrop }, drop] = useDrop({
|
||||
accept: ItemTypes.TEACHER,
|
||||
drop: (item) => {
|
||||
const teacherDetails = teachers.find(teacher => teacher.id === item.id);
|
||||
const exists = localTeachers.some(teacher => teacher.id === item.id);
|
||||
const teacherDetails = teachers.find((teacher) => teacher.id === item.id);
|
||||
const exists = localTeachers.some((teacher) => teacher.id === item.id);
|
||||
if (!exists) {
|
||||
setLocalTeachers(prevTeachers => {
|
||||
setLocalTeachers((prevTeachers) => {
|
||||
const updatedTeachers = [
|
||||
...prevTeachers,
|
||||
{ id: item.id, last_name: teacherDetails.last_name, first_name: teacherDetails.first_name }
|
||||
{
|
||||
id: item.id,
|
||||
last_name: teacherDetails.last_name,
|
||||
first_name: teacherDetails.first_name,
|
||||
},
|
||||
];
|
||||
return updatedTeachers;
|
||||
});
|
||||
@ -56,26 +75,33 @@ const TeachersDropZone = ({ classe, handleTeachersChange, teachers, isEditing })
|
||||
});
|
||||
|
||||
const handleRemoveTeacher = (id) => {
|
||||
setLocalTeachers(prevTeachers => {
|
||||
const updatedTeachers = prevTeachers.filter(teacher => teacher.id !== id);
|
||||
setLocalTeachers((prevTeachers) => {
|
||||
const updatedTeachers = prevTeachers.filter(
|
||||
(teacher) => teacher.id !== id
|
||||
);
|
||||
return updatedTeachers;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={drop} className={`p-2 rounded-md flex flex-col items-center ${isEditing ? 'border-2 border-dashed border-blue-500 bg-blue-50' : ''} ${
|
||||
isOver && canDrop ? 'border-2 border-solid border-blue-300' : ''
|
||||
}`}
|
||||
<div
|
||||
ref={drop}
|
||||
className={`p-2 rounded-md flex flex-col items-center ${isEditing ? 'border-2 border-dashed border-blue-500 bg-blue-50' : ''} ${
|
||||
isOver && canDrop ? 'border-2 border-solid border-blue-300' : ''
|
||||
}`}
|
||||
>
|
||||
{isEditing && (
|
||||
{isEditing && (
|
||||
<div className="mb-2 text-blue-500 font-semibold flex items-center space-x-2">
|
||||
<Hand className="w-5 h-5" /> {/* Ajoutez l'icône Hand */}
|
||||
<span>Déposez un enseignant ici</span>
|
||||
</div>
|
||||
)}
|
||||
{localTeachers.map((teacher, index) => (
|
||||
<div key={`${teacher.id}-${index}`} className="flex items-center space-x-2 mb-2">
|
||||
<TeacherItem key={teacher.id} teacher={teacher} isDraggable={false}/>
|
||||
<div
|
||||
key={`${teacher.id}-${index}`}
|
||||
className="flex items-center space-x-2 mb-2"
|
||||
>
|
||||
<TeacherItem key={teacher.id} teacher={teacher} isDraggable={false} />
|
||||
{isEditing && (
|
||||
<button
|
||||
type="button"
|
||||
@ -91,15 +117,22 @@ const TeachersDropZone = ({ classe, handleTeachersChange, teachers, isEditing })
|
||||
);
|
||||
};
|
||||
|
||||
const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdit, handleDelete }) => {
|
||||
const ClassesSection = ({
|
||||
classes,
|
||||
setClasses,
|
||||
teachers,
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({});
|
||||
const [editingClass, setEditingClass] = useState(null);
|
||||
const [newClass, setNewClass] = useState(null);
|
||||
const [localErrors, setLocalErrors] = useState({});
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [removePopupVisible, setRemovePopupVisible] = useState(false);
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState("");
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||
const [detailsModalVisible, setDetailsModalVisible] = useState(false);
|
||||
const [selectedClass, setSelectedClass] = useState(null);
|
||||
@ -122,11 +155,15 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
{ id: 9, name: 'CM2', age: 10 },
|
||||
];
|
||||
|
||||
const allNiveaux = [...niveauxPremierCycle, ...niveauxSecondCycle, ...niveauxTroisiemeCycle];
|
||||
const allNiveaux = [
|
||||
...niveauxPremierCycle,
|
||||
...niveauxSecondCycle,
|
||||
...niveauxTroisiemeCycle,
|
||||
];
|
||||
|
||||
const getNiveauxLabels = (levels) => {
|
||||
return levels.map(niveauId => {
|
||||
const niveau = allNiveaux.find(n => n.id === niveauId);
|
||||
return levels.map((niveauId) => {
|
||||
const niveau = allNiveaux.find((n) => n.id === niveauId);
|
||||
return niveau ? niveau.name : niveauId;
|
||||
});
|
||||
};
|
||||
@ -143,7 +180,10 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
const choices = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const year = startYear + i;
|
||||
choices.push({ value: `${year}-${year + 1}`, label: `${year}-${year + 1}` });
|
||||
choices.push({
|
||||
value: `${year}-${year + 1}`,
|
||||
label: `${year}-${year + 1}`,
|
||||
});
|
||||
}
|
||||
return choices;
|
||||
};
|
||||
@ -154,8 +194,25 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
};
|
||||
|
||||
const handleAddClass = () => {
|
||||
setNewClass({ id: Date.now(), atmosphere_name: '', age_range: '', levels: [], number_of_students: '', school_year: '', teachers: [], establishment: ESTABLISHMENT_ID });
|
||||
setFormData({ atmosphere_name: '', age_range: '', levels: [], number_of_students: '', school_year: '', teachers: [], establishment: ESTABLISHMENT_ID });
|
||||
setNewClass({
|
||||
id: Date.now(),
|
||||
atmosphere_name: '',
|
||||
age_range: '',
|
||||
levels: [],
|
||||
number_of_students: '',
|
||||
school_year: '',
|
||||
teachers: [],
|
||||
establishment: ESTABLISHMENT_ID,
|
||||
});
|
||||
setFormData({
|
||||
atmosphere_name: '',
|
||||
age_range: '',
|
||||
levels: [],
|
||||
number_of_students: '',
|
||||
school_year: '',
|
||||
teachers: [],
|
||||
establishment: ESTABLISHMENT_ID,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
@ -175,7 +232,13 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
};
|
||||
|
||||
const handleSaveNewClass = () => {
|
||||
if (newClass.atmosphere_name && newClass.age_range && newClass.levels.length > 0 && newClass.number_of_students && newClass.school_year) {
|
||||
if (
|
||||
newClass.atmosphere_name &&
|
||||
newClass.age_range &&
|
||||
newClass.levels.length > 0 &&
|
||||
newClass.number_of_students &&
|
||||
newClass.school_year
|
||||
) {
|
||||
handleCreate(newClass)
|
||||
.then((createdClass) => {
|
||||
setClasses((prevClasses) => [createdClass, ...classes]);
|
||||
@ -185,21 +248,31 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
.catch((error) => {
|
||||
logger.error('Error:', error.message);
|
||||
if (error.details) {
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupVisible(true);
|
||||
}
|
||||
} else {
|
||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateClass = (id, updatedData) => {
|
||||
if (updatedData.atmosphere_name && updatedData.age_range && updatedData.levels.length > 0 && updatedData.number_of_students && updatedData.school_year) {
|
||||
if (
|
||||
updatedData.atmosphere_name &&
|
||||
updatedData.age_range &&
|
||||
updatedData.levels.length > 0 &&
|
||||
updatedData.number_of_students &&
|
||||
updatedData.school_year
|
||||
) {
|
||||
handleEdit(id, updatedData)
|
||||
.then((updatedClass) => {
|
||||
setClasses((prevClasses) => prevClasses.map((classe) => (classe.id === id ? updatedClass : classe)));
|
||||
setClasses((prevClasses) =>
|
||||
prevClasses.map((classe) =>
|
||||
classe.id === id ? updatedClass : classe
|
||||
)
|
||||
);
|
||||
setEditingClass(null);
|
||||
setFormData({});
|
||||
setLocalErrors({});
|
||||
@ -207,12 +280,12 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
.catch((error) => {
|
||||
logger.error('Error:', error.message);
|
||||
if (error.details) {
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
@ -236,7 +309,7 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
};
|
||||
|
||||
const handleMultiSelectChange = (selectedOptions) => {
|
||||
const levels = selectedOptions.map(option => option.id);
|
||||
const levels = selectedOptions.map((option) => option.id);
|
||||
|
||||
if (editingClass) {
|
||||
setFormData((prevData) => ({
|
||||
@ -277,7 +350,7 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
errorMsg={getError('atmosphere_name')}
|
||||
/>
|
||||
);
|
||||
case 'TRANCHE D\'AGE':
|
||||
case "TRANCHE D'AGE":
|
||||
return (
|
||||
<InputText
|
||||
name="age_range"
|
||||
@ -286,14 +359,20 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
placeholder="Tranche d'âge (ex: 3-6)"
|
||||
errorMsg={getError('age_range')}
|
||||
/>
|
||||
)
|
||||
);
|
||||
case 'NIVEAUX':
|
||||
return (
|
||||
<MultiSelect
|
||||
name="levels"
|
||||
label="Sélection de niveaux"
|
||||
options={allNiveaux}
|
||||
selectedOptions={currentData.levels ? currentData.levels.map(levelId => allNiveaux.find(level => level.id === levelId)) : []}
|
||||
selectedOptions={
|
||||
currentData.levels
|
||||
? currentData.levels.map((levelId) =>
|
||||
allNiveaux.find((level) => level.id === levelId)
|
||||
)
|
||||
: []
|
||||
}
|
||||
onChange={handleMultiSelectChange}
|
||||
errorMsg={getError('levels')}
|
||||
/>
|
||||
@ -308,8 +387,8 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
placeholder="Capacité"
|
||||
errorMsg={getError('number_of_students')}
|
||||
/>
|
||||
)
|
||||
case 'ANNÉE SCOLAIRE' :
|
||||
);
|
||||
case 'ANNÉE SCOLAIRE':
|
||||
return (
|
||||
<SelectChoice
|
||||
type="select"
|
||||
@ -322,24 +401,35 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
IconItem={null}
|
||||
disabled={false}
|
||||
/>
|
||||
)
|
||||
);
|
||||
case 'ENSEIGNANTS':
|
||||
return (
|
||||
<TeachersDropZone classe={currentData} handleTeachersChange={handleTeachersChange} teachers={teachers} isEditing={isEditing || isCreating} />
|
||||
<TeachersDropZone
|
||||
classe={currentData}
|
||||
handleTeachersChange={handleTeachersChange}
|
||||
teachers={teachers}
|
||||
isEditing={isEditing || isCreating}
|
||||
/>
|
||||
);
|
||||
case 'ACTIONS':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? handleUpdateClass(editingClass, formData) : handleSaveNewClass())}
|
||||
onClick={() =>
|
||||
isEditing
|
||||
? handleUpdateClass(editingClass, formData)
|
||||
: handleSaveNewClass()
|
||||
}
|
||||
className="text-green-500 hover:text-green-700"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? setEditingClass(null) : setNewClass(null))}
|
||||
onClick={() =>
|
||||
isEditing ? setEditingClass(null) : setNewClass(null)
|
||||
}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
@ -353,28 +443,34 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
switch (column) {
|
||||
case 'AMBIANCE':
|
||||
return classe.atmosphere_name;
|
||||
case 'TRANCHE D\'AGE':
|
||||
case "TRANCHE D'AGE":
|
||||
return classe.age_range;
|
||||
case 'NIVEAUX':
|
||||
const levelLabels = Array.isArray(classe.levels) ? getNiveauxLabels(classe.levels) : [];
|
||||
const levelLabels = Array.isArray(classe.levels)
|
||||
? getNiveauxLabels(classe.levels)
|
||||
: [];
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center items-center space-x-2">
|
||||
{levelLabels.length > 0
|
||||
? levelLabels.map((label, index) => (
|
||||
<LevelLabel key={index} label={label} index={index} />
|
||||
))
|
||||
: 'Aucun niveau'}
|
||||
? levelLabels.map((label, index) => (
|
||||
<LevelLabel key={index} label={label} index={index} />
|
||||
))
|
||||
: 'Aucun niveau'}
|
||||
</div>
|
||||
);
|
||||
case 'CAPACITE':
|
||||
return classe.number_of_students;
|
||||
case 'ANNÉE SCOLAIRE' :
|
||||
case 'ANNÉE SCOLAIRE':
|
||||
return classe.school_year;
|
||||
case 'ENSEIGNANTS':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2 flex-wrap">
|
||||
{classe.teachers_details.map((teacher) => (
|
||||
<TeacherItem key={teacher.id} teacher={teacher} isDraggable={false} />
|
||||
<TeacherItem
|
||||
key={teacher.id}
|
||||
teacher={teacher}
|
||||
isDraggable={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@ -385,7 +481,9 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingClass(classe.id) || setFormData(classe)}
|
||||
onClick={() =>
|
||||
setEditingClass(classe.id) || setFormData(classe)
|
||||
}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
@ -394,18 +492,29 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRemovePopupVisible(true);
|
||||
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer la classe " + classe.atmosphere_name + ".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
|
||||
setRemovePopupMessage(
|
||||
'Attentions ! \nVous êtes sur le point de supprimer la classe ' +
|
||||
classe.atmosphere_name +
|
||||
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
|
||||
);
|
||||
setRemovePopupOnConfirm(() => () => {
|
||||
handleDelete(classe.id)
|
||||
.then(data => {
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setPopupMessage("La classe " + classe.atmosphere_name + " a été correctement supprimée");
|
||||
setPopupMessage(
|
||||
'La classe ' +
|
||||
classe.atmosphere_name +
|
||||
' a été correctement supprimée'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
setPopupMessage("Erreur lors de la suppression de la classe " + classe.atmosphere_name);
|
||||
setPopupMessage(
|
||||
'Erreur lors de la suppression de la classe ' +
|
||||
classe.atmosphere_name
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
});
|
||||
@ -431,14 +540,14 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ name: 'AMBIANCE', label: 'Nom d\'ambiance' },
|
||||
{ name: 'TRANCHE D\'AGE', label: 'Tranche d\'âge' },
|
||||
{ name: 'AMBIANCE', label: "Nom d'ambiance" },
|
||||
{ name: "TRANCHE D'AGE", label: "Tranche d'âge" },
|
||||
{ name: 'NIVEAUX', label: 'Niveaux' },
|
||||
{ name: 'CAPACITE', label: 'Capacité max' },
|
||||
{ name: 'ANNÉE SCOLAIRE', label: 'Année scolaire' },
|
||||
{ name: 'ENSEIGNANTS', label: 'Enseignants' },
|
||||
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||
{ name: 'ACTIONS', label: 'Actions' }
|
||||
{ name: 'ACTIONS', label: 'Actions' },
|
||||
];
|
||||
|
||||
return (
|
||||
@ -449,7 +558,11 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
<Users className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Classes</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddClass} className="text-emerald-500 hover:text-emerald-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddClass}
|
||||
className="text-emerald-500 hover:text-emerald-700"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
@ -460,7 +573,9 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
/>
|
||||
<Popup
|
||||
visible={detailsModalVisible}
|
||||
message={selectedClass ? <ClasseDetails classe={selectedClass} /> : null}
|
||||
message={
|
||||
selectedClass ? <ClasseDetails classe={selectedClass} /> : null
|
||||
}
|
||||
onConfirm={() => setDetailsModalVisible(false)}
|
||||
onCancel={() => setDetailsModalVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
@ -483,4 +598,4 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
);
|
||||
};
|
||||
|
||||
export default ClassesSection;
|
||||
export default ClassesSection;
|
||||
|
||||
@ -1,15 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Calendar } from 'lucide-react';
|
||||
|
||||
const DateRange = ({ nameStart, nameEnd, valueStart, valueEnd, onChange, label }) => {
|
||||
const DateRange = ({
|
||||
nameStart,
|
||||
nameEnd,
|
||||
valueStart,
|
||||
valueEnd,
|
||||
onChange,
|
||||
label,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-4 mt-4 p-4 border rounded-md shadow-sm bg-white">
|
||||
<label className="block text-lg font-medium text-gray-700 mb-2">{label}</label>
|
||||
<label className="block text-lg font-medium text-gray-700 mb-2">
|
||||
{label}
|
||||
</label>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 items-center">
|
||||
<div className="relative flex items-center">
|
||||
<span className="mr-2">Du</span>
|
||||
<Calendar className="w-5 h-5 text-emerald-500 absolute top-3 left-16" />
|
||||
<input
|
||||
<input
|
||||
type="date"
|
||||
name={nameStart}
|
||||
value={valueStart}
|
||||
@ -21,7 +30,7 @@ const DateRange = ({ nameStart, nameEnd, valueStart, valueEnd, onChange, label }
|
||||
<div className="relative flex items-center">
|
||||
<span className="mr-2">Au</span>
|
||||
<Calendar className="w-5 h-5 text-emerald-500 absolute top-3 left-16" />
|
||||
<input
|
||||
<input
|
||||
type="date"
|
||||
name={nameEnd}
|
||||
value={valueEnd}
|
||||
|
||||
@ -4,7 +4,13 @@ import DateRange from '@/components/Structure/Configuration/DateRange';
|
||||
import TimeRange from '@/components/Structure/Configuration/TimeRange';
|
||||
import CheckBoxList from '@/components/CheckBoxList';
|
||||
|
||||
const PlanningConfiguration = ({ formData, handleChange, handleTimeChange, handleJoursChange, typeEmploiDuTemps }) => {
|
||||
const PlanningConfiguration = ({
|
||||
formData,
|
||||
handleChange,
|
||||
handleTimeChange,
|
||||
handleJoursChange,
|
||||
typeEmploiDuTemps,
|
||||
}) => {
|
||||
const daysOfWeek = [
|
||||
{ id: 1, name: 'lun' },
|
||||
{ id: 2, name: 'mar' },
|
||||
@ -14,13 +20,15 @@ const PlanningConfiguration = ({ formData, handleChange, handleTimeChange, handl
|
||||
{ id: 6, name: 'sam' },
|
||||
];
|
||||
|
||||
const isLabelAttenuated = (item) => {
|
||||
return !formData.opening_days.includes(parseInt(item.id));
|
||||
const isLabelAttenuated = (item) => {
|
||||
return !formData.opening_days.includes(parseInt(item.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<label className="mt-6 block text-2xl font-medium text-gray-700">Emploi du temps</label>
|
||||
<label className="mt-6 block text-2xl font-medium text-gray-700">
|
||||
Emploi du temps
|
||||
</label>
|
||||
|
||||
<div className="flex justify-between space-x-4 items-start">
|
||||
<div className="w-1/2">
|
||||
@ -41,7 +49,7 @@ const PlanningConfiguration = ({ formData, handleChange, handleTimeChange, handl
|
||||
onStartChange={(e) => handleTimeChange(e, 0)}
|
||||
onEndChange={(e) => handleTimeChange(e, 1)}
|
||||
/>
|
||||
|
||||
|
||||
{/* CheckBoxList */}
|
||||
<CheckBoxList
|
||||
items={daysOfWeek}
|
||||
|
||||
@ -8,17 +8,22 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, handleEdit, handleDelete }) => {
|
||||
|
||||
const SpecialitiesSection = ({
|
||||
specialities,
|
||||
setSpecialities,
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}) => {
|
||||
const [newSpeciality, setNewSpeciality] = useState(null);
|
||||
const [editingSpeciality, setEditingSpeciality] = useState(null);
|
||||
const [formData, setFormData] = useState({});
|
||||
const [localErrors, setLocalErrors] = useState({});
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
|
||||
const [removePopupVisible, setRemovePopupVisible] = useState(false);
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState("");
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||
|
||||
// Récupération des messages d'erreur
|
||||
@ -33,16 +38,17 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
|
||||
const handleRemoveSpeciality = (id) => {
|
||||
return handleDelete(id)
|
||||
.then(() => {
|
||||
setSpecialities(prevSpecialities => prevSpecialities.filter(speciality => speciality.id !== id));
|
||||
setSpecialities((prevSpecialities) =>
|
||||
prevSpecialities.filter((speciality) => speciality.id !== id)
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveNewSpeciality = () => {
|
||||
if (
|
||||
newSpeciality.name) {
|
||||
if (newSpeciality.name) {
|
||||
handleCreate(newSpeciality)
|
||||
.then((createdSpeciality) => {
|
||||
setSpecialities([createdSpeciality, ...specialities]);
|
||||
@ -52,34 +58,37 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
|
||||
.catch((error) => {
|
||||
logger.error('Error:', error.message);
|
||||
if (error.details) {
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateSpeciality = (id, updatedSpeciality) => {
|
||||
if (
|
||||
updatedSpeciality.name) {
|
||||
if (updatedSpeciality.name) {
|
||||
handleEdit(id, updatedSpeciality)
|
||||
.then((updatedSpeciality) => {
|
||||
setSpecialities(specialities.map(speciality => speciality.id === id ? updatedSpeciality : speciality));
|
||||
setSpecialities(
|
||||
specialities.map((speciality) =>
|
||||
speciality.id === id ? updatedSpeciality : speciality
|
||||
)
|
||||
);
|
||||
setEditingSpeciality(null);
|
||||
setLocalErrors({});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error:', error.message);
|
||||
if (error.details) {
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
@ -129,14 +138,22 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? handleUpdateSpeciality(editingSpeciality, formData) : handleSaveNewSpeciality())}
|
||||
onClick={() =>
|
||||
isEditing
|
||||
? handleUpdateSpeciality(editingSpeciality, formData)
|
||||
: handleSaveNewSpeciality()
|
||||
}
|
||||
className="text-green-500 hover:text-green-700"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? setEditingSpeciality(null) : setNewSpeciality(null))}
|
||||
onClick={() =>
|
||||
isEditing
|
||||
? setEditingSpeciality(null)
|
||||
: setNewSpeciality(null)
|
||||
}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
@ -149,9 +166,7 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
|
||||
} else {
|
||||
switch (column) {
|
||||
case 'LIBELLE':
|
||||
return (
|
||||
<SpecialityItem key={speciality.id} speciality={speciality} />
|
||||
);
|
||||
return <SpecialityItem key={speciality.id} speciality={speciality} />;
|
||||
case 'MISE A JOUR':
|
||||
return speciality.updated_date_formatted;
|
||||
case 'ACTIONS':
|
||||
@ -159,7 +174,9 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingSpeciality(speciality.id) || setFormData(speciality)}
|
||||
onClick={() =>
|
||||
setEditingSpeciality(speciality.id) || setFormData(speciality)
|
||||
}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
@ -168,18 +185,29 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRemovePopupVisible(true);
|
||||
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer la spécialité " + speciality.name + ".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
|
||||
setRemovePopupMessage(
|
||||
'Attentions ! \nVous êtes sur le point de supprimer la spécialité ' +
|
||||
speciality.name +
|
||||
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
|
||||
);
|
||||
setRemovePopupOnConfirm(() => () => {
|
||||
handleRemoveSpeciality(speciality.id)
|
||||
.then(data => {
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setPopupMessage("La spécialité " + speciality.name + " a été correctement supprimée");
|
||||
setPopupMessage(
|
||||
'La spécialité ' +
|
||||
speciality.name +
|
||||
' a été correctement supprimée'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
setPopupMessage("Erreur lors de la suppression de la spécialité " + speciality.name);
|
||||
setPopupMessage(
|
||||
'Erreur lors de la suppression de la spécialité ' +
|
||||
speciality.name
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
});
|
||||
@ -198,10 +226,10 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ name: 'LIBELLE', label: 'Libellé' },
|
||||
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||
{ name: 'ACTIONS', label: 'Actions' }
|
||||
];
|
||||
{ name: 'LIBELLE', label: 'Libellé' },
|
||||
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||
{ name: 'ACTIONS', label: 'Actions' },
|
||||
];
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
@ -211,7 +239,11 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
|
||||
<BookOpen className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Spécialités</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddSpeciality} className="text-emerald-500 hover:text-emerald-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddSpeciality}
|
||||
className="text-emerald-500 hover:text-emerald-700"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -9,29 +9,32 @@ const lightenColor = (color, percent) => {
|
||||
const num = parseInt(color.slice(1), 16),
|
||||
amt = Math.round(2.55 * percent),
|
||||
R = (num >> 16) + amt,
|
||||
G = (num >> 8 & 0x00FF) + amt,
|
||||
B = (num & 0x0000FF) + amt;
|
||||
return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`;
|
||||
G = ((num >> 8) & 0x00ff) + amt,
|
||||
B = (num & 0x0000ff) + amt;
|
||||
return `#${(0x1000000 + (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + (B < 255 ? (B < 1 ? 0 : B) : 255)).toString(16).slice(1).toUpperCase()}`;
|
||||
};
|
||||
|
||||
const darkenColor = (color, percent) => {
|
||||
const num = parseInt(color.slice(1), 16),
|
||||
amt = Math.round(2.55 * percent),
|
||||
R = (num >> 16) - amt,
|
||||
G = (num >> 8 & 0x00FF) - amt,
|
||||
B = (num & 0x0000FF) - amt;
|
||||
return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`;
|
||||
G = ((num >> 8) & 0x00ff) - amt,
|
||||
B = (num & 0x0000ff) - amt;
|
||||
return `#${(0x1000000 + (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + (B < 255 ? (B < 1 ? 0 : B) : 255)).toString(16).slice(1).toUpperCase()}`;
|
||||
};
|
||||
|
||||
const SpecialityItem = ({ speciality, isDraggable = true }) => {
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
type: ItemTypes.SPECIALITY,
|
||||
item: { id: speciality.id, name: speciality.name },
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
const [{ isDragging }, drag] = useDrag(
|
||||
() => ({
|
||||
type: ItemTypes.SPECIALITY,
|
||||
item: { id: speciality.id, name: speciality.name },
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => isDraggable,
|
||||
}),
|
||||
canDrag: () => isDraggable,
|
||||
}), [isDraggable]);
|
||||
[isDraggable]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -40,10 +43,12 @@ const SpecialityItem = ({ speciality, isDraggable = true }) => {
|
||||
isDragging ? 'opacity-30' : 'opacity-100'
|
||||
} ${isDraggable ? 'cursor-grabbing hover:shadow-lg hover:scale-105' : ''}`}
|
||||
style={{
|
||||
backgroundColor: isDragging ? lightenColor(speciality.color_code, 30) : speciality.color_code,
|
||||
backgroundColor: isDragging
|
||||
? lightenColor(speciality.color_code, 30)
|
||||
: speciality.color_code,
|
||||
border: `1px solid ${darkenColor(speciality.color_code, 20)}`,
|
||||
color: 'white',
|
||||
boxShadow: isDraggable ? '0 2px 4px rgba(0, 0, 0, 0.1)' : 'none'
|
||||
boxShadow: isDraggable ? '0 2px 4px rgba(0, 0, 0, 0.1)' : 'none',
|
||||
}}
|
||||
>
|
||||
{speciality.name}
|
||||
@ -51,4 +56,4 @@ const SpecialityItem = ({ speciality, isDraggable = true }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecialityItem;
|
||||
export default SpecialityItem;
|
||||
|
||||
@ -3,9 +3,24 @@ import SpecialitiesSection from '@/components/Structure/Configuration/Specialiti
|
||||
import TeachersSection from '@/components/Structure/Configuration/TeachersSection';
|
||||
import ClassesSection from '@/components/Structure/Configuration/ClassesSection';
|
||||
import { ClassesProvider } from '@/context/ClassesContext';
|
||||
import { BE_SCHOOL_SPECIALITIES_URL, BE_SCHOOL_TEACHERS_URL, BE_SCHOOL_SCHOOLCLASSES_URL } from '@/utils/Url';
|
||||
import {
|
||||
BE_SCHOOL_SPECIALITIES_URL,
|
||||
BE_SCHOOL_TEACHERS_URL,
|
||||
BE_SCHOOL_SCHOOLCLASSES_URL,
|
||||
} from '@/utils/Url';
|
||||
|
||||
const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, profiles, handleCreate, handleEdit, handleDelete }) => {
|
||||
const StructureManagement = ({
|
||||
specialities,
|
||||
setSpecialities,
|
||||
teachers,
|
||||
setTeachers,
|
||||
classes,
|
||||
setClasses,
|
||||
profiles,
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}) => {
|
||||
return (
|
||||
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-8">
|
||||
<ClassesProvider>
|
||||
@ -13,9 +28,24 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
|
||||
<SpecialitiesSection
|
||||
specialities={specialities}
|
||||
setSpecialities={setSpecialities}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_SPECIALITIES_URL}`, newData, setSpecialities)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SPECIALITIES_URL}`, id, updatedData, setSpecialities)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SPECIALITIES_URL}`, id, setSpecialities)}
|
||||
handleCreate={(newData) =>
|
||||
handleCreate(
|
||||
`${BE_SCHOOL_SPECIALITIES_URL}`,
|
||||
newData,
|
||||
setSpecialities
|
||||
)
|
||||
}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_SPECIALITIES_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setSpecialities
|
||||
)
|
||||
}
|
||||
handleDelete={(id) =>
|
||||
handleDelete(`${BE_SCHOOL_SPECIALITIES_URL}`, id, setSpecialities)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-4/5 p-4 bg-white rounded-lg shadow-md">
|
||||
@ -24,9 +54,20 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
|
||||
setTeachers={setTeachers}
|
||||
specialities={specialities}
|
||||
profiles={profiles}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_TEACHERS_URL}`, newData, setTeachers)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TEACHERS_URL}`, id, updatedData, setTeachers)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHERS_URL}`, id, setTeachers)}
|
||||
handleCreate={(newData) =>
|
||||
handleCreate(`${BE_SCHOOL_TEACHERS_URL}`, newData, setTeachers)
|
||||
}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_TEACHERS_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setTeachers
|
||||
)
|
||||
}
|
||||
handleDelete={(id) =>
|
||||
handleDelete(`${BE_SCHOOL_TEACHERS_URL}`, id, setTeachers)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full p-4 bg-white rounded-lg shadow-md">
|
||||
@ -34,9 +75,24 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
|
||||
classes={classes}
|
||||
setClasses={setClasses}
|
||||
teachers={teachers}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_SCHOOLCLASSES_URL}`, newData, setClasses)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SCHOOLCLASSES_URL}`, id, updatedData, setClasses)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SCHOOLCLASSES_URL}`, id, setClasses)}
|
||||
handleCreate={(newData) =>
|
||||
handleCreate(
|
||||
`${BE_SCHOOL_SCHOOLCLASSES_URL}`,
|
||||
newData,
|
||||
setClasses
|
||||
)
|
||||
}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_SCHOOLCLASSES_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setClasses
|
||||
)
|
||||
}
|
||||
handleDelete={(id) =>
|
||||
handleDelete(`${BE_SCHOOL_SCHOOLCLASSES_URL}`, id, setClasses)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ClassesProvider>
|
||||
|
||||
@ -4,9 +4,9 @@ const TabsStructure = ({ activeTab, setActiveTab, tabs }) => {
|
||||
return (
|
||||
<div className="flex justify-center items-center w-full">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`tab px-4 py-2 mx-2 flex items-center justify-center space-x-2 ${activeTab === tab.id ? 'bg-emerald-600 text-white shadow-lg' : 'bg-emerald-200 text-emerald-600'} rounded-full`}
|
||||
className={`tab px-4 py-2 mx-2 flex items-center justify-center space-x-2 ${activeTab === tab.id ? 'bg-emerald-600 text-white shadow-lg' : 'bg-emerald-200 text-emerald-600'} rounded-full`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
<tab.icon className="w-5 h-5" />
|
||||
@ -18,6 +18,3 @@ const TabsStructure = ({ activeTab, setActiveTab, tabs }) => {
|
||||
};
|
||||
|
||||
export default TabsStructure;
|
||||
|
||||
|
||||
|
||||
|
||||
@ -6,14 +6,20 @@ const ItemTypes = {
|
||||
};
|
||||
|
||||
const TeacherItem = ({ teacher, isDraggable = true }) => {
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
type: ItemTypes.TEACHER,
|
||||
item: { id: teacher.id, name: `${teacher.last_name} ${teacher.first_name}` },
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
const [{ isDragging }, drag] = useDrag(
|
||||
() => ({
|
||||
type: ItemTypes.TEACHER,
|
||||
item: {
|
||||
id: teacher.id,
|
||||
name: `${teacher.last_name} ${teacher.first_name}`,
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => isDraggable,
|
||||
}),
|
||||
canDrag: () => isDraggable,
|
||||
}), [isDraggable]);
|
||||
[isDraggable]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -22,9 +28,13 @@ const TeacherItem = ({ teacher, isDraggable = true }) => {
|
||||
isDragging ? 'opacity-30' : 'opacity-100'
|
||||
} ${isDraggable ? 'cursor-grabbing hover:shadow-lg hover:scale-105' : ''}`}
|
||||
style={{
|
||||
backgroundColor: isDragging ? '#d1d5db' : isDraggable ? '#10b981' : '#a7f3d0', // Change background color based on dragging state and draggable state
|
||||
backgroundColor: isDragging
|
||||
? '#d1d5db'
|
||||
: isDraggable
|
||||
? '#10b981'
|
||||
: '#a7f3d0', // Change background color based on dragging state and draggable state
|
||||
border: isDraggable ? '1px solid #10b981' : '1px solid #a7f3d0', // Add a border
|
||||
boxShadow: isDraggable ? '0 2px 4px rgba(0, 0, 0, 0.1)' : 'none' // Add a shadow if draggable
|
||||
boxShadow: isDraggable ? '0 2px 4px rgba(0, 0, 0, 0.1)' : 'none', // Add a shadow if draggable
|
||||
}}
|
||||
>
|
||||
{teacher.last_name} {teacher.first_name}
|
||||
@ -32,4 +42,4 @@ const TeacherItem = ({ teacher, isDraggable = true }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TeacherItem;
|
||||
export default TeacherItem;
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Plus, Edit3, Trash2, GraduationCap, Check, X, Hand, Search } from 'lucide-react';
|
||||
import {
|
||||
Plus,
|
||||
Edit3,
|
||||
Trash2,
|
||||
GraduationCap,
|
||||
Check,
|
||||
X,
|
||||
Hand,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import Popup from '@/components/Popup';
|
||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
||||
@ -18,30 +27,46 @@ const ItemTypes = {
|
||||
SPECIALITY: 'speciality',
|
||||
};
|
||||
|
||||
const SpecialitiesDropZone = ({ teacher, handleSpecialitiesChange, specialities, isEditing }) => {
|
||||
const [localSpecialities, setLocalSpecialities] = useState(teacher.specialities_details || []);
|
||||
const SpecialitiesDropZone = ({
|
||||
teacher,
|
||||
handleSpecialitiesChange,
|
||||
specialities,
|
||||
isEditing,
|
||||
}) => {
|
||||
const [localSpecialities, setLocalSpecialities] = useState(
|
||||
teacher.specialities_details || []
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
}, [specialities]);
|
||||
useEffect(() => {}, [specialities]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSpecialities(teacher.specialities_details || []);
|
||||
}, [teacher.specialities_details]);
|
||||
|
||||
useEffect(() => {
|
||||
handleSpecialitiesChange(localSpecialities.map(speciality => speciality.id));
|
||||
handleSpecialitiesChange(
|
||||
localSpecialities.map((speciality) => speciality.id)
|
||||
);
|
||||
}, [localSpecialities]);
|
||||
|
||||
const [{ isOver, canDrop }, drop] = useDrop({
|
||||
accept: ItemTypes.SPECIALITY,
|
||||
drop: (item) => {
|
||||
const specialityDetails = specialities.find(speciality => speciality.id === item.id);
|
||||
const exists = localSpecialities.some(speciality => speciality.id === item.id);
|
||||
const specialityDetails = specialities.find(
|
||||
(speciality) => speciality.id === item.id
|
||||
);
|
||||
const exists = localSpecialities.some(
|
||||
(speciality) => speciality.id === item.id
|
||||
);
|
||||
if (!exists) {
|
||||
setLocalSpecialities(prevSpecialities => {
|
||||
setLocalSpecialities((prevSpecialities) => {
|
||||
const updatedSpecialities = [
|
||||
...prevSpecialities,
|
||||
{ id: item.id, name: specialityDetails.name, color_code: specialityDetails.color_code }
|
||||
{
|
||||
id: item.id,
|
||||
name: specialityDetails.name,
|
||||
color_code: specialityDetails.color_code,
|
||||
},
|
||||
];
|
||||
return updatedSpecialities;
|
||||
});
|
||||
@ -57,26 +82,37 @@ const SpecialitiesDropZone = ({ teacher, handleSpecialitiesChange, specialities,
|
||||
});
|
||||
|
||||
const handleRemoveSpeciality = (id) => {
|
||||
setLocalSpecialities(prevSpecialities => {
|
||||
const updatedSpecialities = prevSpecialities.filter(speciality => speciality.id !== id);
|
||||
setLocalSpecialities((prevSpecialities) => {
|
||||
const updatedSpecialities = prevSpecialities.filter(
|
||||
(speciality) => speciality.id !== id
|
||||
);
|
||||
return updatedSpecialities;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={drop} className={`p-2 rounded-md flex flex-col items-center ${isEditing ? 'border-2 border-dashed border-blue-500 bg-blue-50' : ''} ${
|
||||
isOver && canDrop ? 'border-2 border-solid border-blue-300' : ''
|
||||
}`}
|
||||
<div
|
||||
ref={drop}
|
||||
className={`p-2 rounded-md flex flex-col items-center ${isEditing ? 'border-2 border-dashed border-blue-500 bg-blue-50' : ''} ${
|
||||
isOver && canDrop ? 'border-2 border-solid border-blue-300' : ''
|
||||
}`}
|
||||
>
|
||||
{isEditing && (
|
||||
{isEditing && (
|
||||
<div className="mb-2 text-blue-500 font-semibold flex items-center space-x-2">
|
||||
<Hand className="w-5 h-5" /> {/* Ajoutez l'icône Hand */}
|
||||
<span>Déposez une spécialité ici</span>
|
||||
</div>
|
||||
)}
|
||||
{localSpecialities.map((speciality, index) => (
|
||||
<div key={`${speciality.id}-${index}`} className="flex items-center space-x-2 mb-2">
|
||||
<SpecialityItem key={speciality.id} speciality={speciality} isDraggable={false}/>
|
||||
<div
|
||||
key={`${speciality.id}-${index}`}
|
||||
className="flex items-center space-x-2 mb-2"
|
||||
>
|
||||
<SpecialityItem
|
||||
key={speciality.id}
|
||||
speciality={speciality}
|
||||
isDraggable={false}
|
||||
/>
|
||||
{isEditing && (
|
||||
<button
|
||||
type="button"
|
||||
@ -92,37 +128,45 @@ const SpecialitiesDropZone = ({ teacher, handleSpecialitiesChange, specialities,
|
||||
);
|
||||
};
|
||||
|
||||
const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handleCreate, handleEdit, handleDelete }) => {
|
||||
const TeachersSection = ({
|
||||
teachers,
|
||||
setTeachers,
|
||||
specialities,
|
||||
profiles,
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}) => {
|
||||
const csrfToken = useCsrfToken();
|
||||
const [editingTeacher, setEditingTeacher] = useState(null);
|
||||
const [newTeacher, setNewTeacher] = useState(null);
|
||||
const [formData, setFormData] = useState({});
|
||||
const [localErrors, setLocalErrors] = useState({});
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
|
||||
const [removePopupVisible, setRemovePopupVisible] = useState(false);
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState("");
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||
|
||||
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
|
||||
const [confirmPopupMessage, setConfirmPopupMessage] = useState("");
|
||||
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
|
||||
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
|
||||
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
const handleEmailChange = (e) => {
|
||||
const email = e.target.value;
|
||||
|
||||
|
||||
// Vérifier si l'email correspond à un profil existant
|
||||
const existingProfile = profiles.find((profile) => profile.email === email);
|
||||
|
||||
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
associated_profile_email: email,
|
||||
existingProfileId: existingProfile ? existingProfile.id : null,
|
||||
}));
|
||||
|
||||
|
||||
if (newTeacher) {
|
||||
setNewTeacher((prevData) => ({
|
||||
...prevData,
|
||||
@ -133,7 +177,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
};
|
||||
|
||||
const handleCancelConfirmation = () => {
|
||||
setConfirmPopupVisible(false);
|
||||
setConfirmPopupVisible(false);
|
||||
};
|
||||
|
||||
// Récupération des messages d'erreur
|
||||
@ -142,22 +186,41 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
};
|
||||
|
||||
const handleAddTeacher = () => {
|
||||
setNewTeacher({ id: Date.now(), last_name: '', first_name: '', associated_profile_email: '', specialities: [], role_type: 0 });
|
||||
setFormData({ last_name: '', first_name: '', associated_profile_email: '', specialities: [], role_type: 0});
|
||||
setNewTeacher({
|
||||
id: Date.now(),
|
||||
last_name: '',
|
||||
first_name: '',
|
||||
associated_profile_email: '',
|
||||
specialities: [],
|
||||
role_type: 0,
|
||||
});
|
||||
setFormData({
|
||||
last_name: '',
|
||||
first_name: '',
|
||||
associated_profile_email: '',
|
||||
specialities: [],
|
||||
role_type: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveTeacher = (id) => {
|
||||
return handleDelete(id)
|
||||
.then(() => {
|
||||
setTeachers(prevTeachers => prevTeachers.filter(teacher => teacher.id !== id));
|
||||
setTeachers((prevTeachers) =>
|
||||
prevTeachers.filter((teacher) => teacher.id !== id)
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveNewTeacher = () => {
|
||||
if (formData.last_name && formData.first_name && formData.associated_profile_email) {
|
||||
if (
|
||||
formData.last_name &&
|
||||
formData.first_name &&
|
||||
formData.associated_profile_email
|
||||
) {
|
||||
const data = {
|
||||
last_name: formData.last_name,
|
||||
first_name: formData.first_name,
|
||||
@ -177,7 +240,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
},
|
||||
specialities: formData.specialities || [],
|
||||
};
|
||||
|
||||
|
||||
handleCreate(data)
|
||||
.then((createdTeacher) => {
|
||||
setTeachers([createdTeacher, ...teachers]);
|
||||
@ -192,7 +255,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
@ -202,17 +265,24 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
const currentTeacher = teachers.find((teacher) => teacher.id === id);
|
||||
|
||||
// Vérifier si l'email correspond à un profil existant
|
||||
const existingProfile = profiles.find((profile) => profile.email === currentTeacher.associated_profile_email);
|
||||
const existingProfile = profiles.find(
|
||||
(profile) => profile.email === currentTeacher.associated_profile_email
|
||||
);
|
||||
|
||||
// Vérifier si l'email a été modifié
|
||||
const isEmailModified = currentTeacher
|
||||
? currentTeacher.associated_profile_email !== updatedData.associated_profile_email
|
||||
: true;
|
||||
|
||||
? currentTeacher.associated_profile_email !==
|
||||
updatedData.associated_profile_email
|
||||
: true;
|
||||
|
||||
// Mettre à jour existingProfileId en fonction de l'email
|
||||
updatedData.existingProfileId = existingProfile ? existingProfile.id : null;
|
||||
|
||||
if (updatedData.last_name && updatedData.first_name && updatedData.associated_profile_email) {
|
||||
if (
|
||||
updatedData.last_name &&
|
||||
updatedData.first_name &&
|
||||
updatedData.associated_profile_email
|
||||
) {
|
||||
const data = {
|
||||
last_name: updatedData.last_name,
|
||||
first_name: updatedData.first_name,
|
||||
@ -238,7 +308,9 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
handleEdit(id, data)
|
||||
.then((updatedTeacher) => {
|
||||
setTeachers((prevTeachers) =>
|
||||
prevTeachers.map((teacher) => (teacher.id === id ? { ...teacher, ...updatedTeacher } : teacher))
|
||||
prevTeachers.map((teacher) =>
|
||||
teacher.id === id ? { ...teacher, ...updatedTeacher } : teacher
|
||||
)
|
||||
);
|
||||
setEditingTeacher(null);
|
||||
setFormData({});
|
||||
@ -251,7 +323,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
@ -347,7 +419,12 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
);
|
||||
case 'SPECIALITES':
|
||||
return (
|
||||
<SpecialitiesDropZone teacher={currentData} handleSpecialitiesChange={handleSpecialitiesChange} specialities={specialities} isEditing={isEditing || isCreating} />
|
||||
<SpecialitiesDropZone
|
||||
teacher={currentData}
|
||||
handleSpecialitiesChange={handleSpecialitiesChange}
|
||||
specialities={specialities}
|
||||
isEditing={isEditing || isCreating}
|
||||
/>
|
||||
);
|
||||
case 'ADMINISTRATEUR':
|
||||
return (
|
||||
@ -364,14 +441,20 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? handleUpdateTeacher(editingTeacher, formData) : handleSaveNewTeacher())}
|
||||
onClick={() =>
|
||||
isEditing
|
||||
? handleUpdateTeacher(editingTeacher, formData)
|
||||
: handleSaveNewTeacher()
|
||||
}
|
||||
className="text-green-500 hover:text-green-700"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? setEditingTeacher(null) : setNewTeacher(null))}
|
||||
onClick={() =>
|
||||
isEditing ? setEditingTeacher(null) : setNewTeacher(null)
|
||||
}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
@ -384,33 +467,43 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
} else {
|
||||
switch (column) {
|
||||
case 'NOM - PRENOM':
|
||||
return (
|
||||
<TeacherItem key={teacher.id} teacher={teacher} />
|
||||
);
|
||||
return <TeacherItem key={teacher.id} teacher={teacher} />;
|
||||
case 'EMAIL':
|
||||
return teacher.associated_profile_email;
|
||||
case 'SPECIALITES':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2 flex-wrap">
|
||||
{teacher.specialities_details.map((speciality) => (
|
||||
<SpecialityItem key={speciality.id} speciality={speciality} isDraggable={false} />
|
||||
<SpecialityItem
|
||||
key={speciality.id}
|
||||
speciality={speciality}
|
||||
isDraggable={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
case 'ADMINISTRATEUR':
|
||||
if (teacher.associated_profile_email) {
|
||||
const badgeClass = teacher.role_type === 1 ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600';
|
||||
const label = teacher.role_type === 1 ? 'OUI' : 'NON';
|
||||
return (
|
||||
<div key={teacher.id} className="flex justify-center items-center space-x-2">
|
||||
<span className={`px-3 py-1 rounded-full font-bold ${badgeClass}`}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <i>Non définie</i>;
|
||||
};
|
||||
if (teacher.associated_profile_email) {
|
||||
const badgeClass =
|
||||
teacher.role_type === 1
|
||||
? 'bg-red-100 text-red-600'
|
||||
: 'bg-blue-100 text-blue-600';
|
||||
const label = teacher.role_type === 1 ? 'OUI' : 'NON';
|
||||
return (
|
||||
<div
|
||||
key={teacher.id}
|
||||
className="flex justify-center items-center space-x-2"
|
||||
>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full font-bold ${badgeClass}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <i>Non définie</i>;
|
||||
}
|
||||
case 'MISE A JOUR':
|
||||
return teacher.updated_date_formatted;
|
||||
case 'ACTIONS':
|
||||
@ -427,18 +520,35 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRemovePopupVisible(true);
|
||||
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer l'enseignant " + teacher.last_name + " " + teacher.first_name + ".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
|
||||
setRemovePopupMessage(
|
||||
"Attentions ! \nVous êtes sur le point de supprimer l'enseignant " +
|
||||
teacher.last_name +
|
||||
' ' +
|
||||
teacher.first_name +
|
||||
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
|
||||
);
|
||||
setRemovePopupOnConfirm(() => () => {
|
||||
handleRemoveTeacher(teacher.id)
|
||||
.then(data => {
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setPopupMessage("L'enseignant " + teacher.last_name + " " + teacher.first_name + " a été correctement supprimé");
|
||||
setPopupMessage(
|
||||
"L'enseignant " +
|
||||
teacher.last_name +
|
||||
' ' +
|
||||
teacher.first_name +
|
||||
' a été correctement supprimé'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
setPopupMessage("Erreur lors de la suppression de l'enseignant " + teacher.last_name + " " + teacher.first_name);
|
||||
setPopupMessage(
|
||||
"Erreur lors de la suppression de l'enseignant " +
|
||||
teacher.last_name +
|
||||
' ' +
|
||||
teacher.first_name
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
});
|
||||
@ -462,7 +572,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
{ name: 'SPECIALITES', label: 'Spécialités' },
|
||||
{ name: 'ADMINISTRATEUR', label: 'Profil' },
|
||||
{ name: 'MISE A JOUR', label: 'Mise à jour' },
|
||||
{ name: 'ACTIONS', label: 'Actions' }
|
||||
{ name: 'ACTIONS', label: 'Actions' },
|
||||
];
|
||||
|
||||
return (
|
||||
@ -473,7 +583,11 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
<GraduationCap className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Enseignants</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddTeacher} className="text-emerald-500 hover:text-emerald-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddTeacher}
|
||||
className="text-emerald-500 hover:text-emerald-700"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,23 +1,37 @@
|
||||
import React from 'react';
|
||||
import Table from '@/components/Table';
|
||||
|
||||
const TeachersSelectionConfiguration = ({ formData, teachers, handleTeacherSelection, selectedTeachers }) => {
|
||||
const TeachersSelectionConfiguration = ({
|
||||
formData,
|
||||
teachers,
|
||||
handleTeacherSelection,
|
||||
selectedTeachers,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-4" style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||
<label className="mt-6 block text-2xl font-medium text-gray-700 mb-2">Enseignants</label>
|
||||
<label className={`block text-sm font-medium mb-4`}>Sélection : <span className={`${formData.teachers.length !== 0 ? 'text-emerald-400' : 'text-red-300'}`}>{formData.teachers.length}</span></label>
|
||||
<label className="mt-6 block text-2xl font-medium text-gray-700 mb-2">
|
||||
Enseignants
|
||||
</label>
|
||||
<label className={`block text-sm font-medium mb-4`}>
|
||||
Sélection :{' '}
|
||||
<span
|
||||
className={`${formData.teachers.length !== 0 ? 'text-emerald-400' : 'text-red-300'}`}
|
||||
>
|
||||
{formData.teachers.length}
|
||||
</span>
|
||||
</label>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
name: 'Nom',
|
||||
{
|
||||
name: 'Nom',
|
||||
transform: (row) => row.last_name,
|
||||
},
|
||||
{
|
||||
name: 'Prénom',
|
||||
{
|
||||
name: 'Prénom',
|
||||
transform: (row) => row.first_name,
|
||||
},
|
||||
// {
|
||||
// name: 'Spécialités',
|
||||
// {
|
||||
// name: 'Spécialités',
|
||||
// transform: (row) => (
|
||||
// <div className="flex flex-wrap items-center">
|
||||
// {row.specialites.map(specialite => (
|
||||
|
||||
@ -5,7 +5,9 @@ const TimeRange = ({ startTime, endTime, onStartChange, onEndChange }) => {
|
||||
<div className="mb-4">
|
||||
<div className="flex space-x-4">
|
||||
<div className="w-1/2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Heure de début</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Heure de début
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="startTime"
|
||||
@ -15,7 +17,9 @@ const TimeRange = ({ startTime, endTime, onStartChange, onEndChange }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Heure de fin</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Heure de fin
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
name="endTime"
|
||||
|
||||
@ -1,15 +1,29 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
|
||||
import { fetchRegistrationFileGroups, createRegistrationTemplates, cloneTemplate, generateToken } from '@/app/actions/registerFileGroupAction';
|
||||
import {
|
||||
fetchRegistrationFileGroups,
|
||||
createRegistrationTemplates,
|
||||
cloneTemplate,
|
||||
generateToken,
|
||||
} from '@/app/actions/registerFileGroupAction';
|
||||
import { DocusealBuilder } from '@docuseal/react';
|
||||
import logger from '@/utils/logger';
|
||||
import { BE_DOCUSEAL_GET_JWT, BASE_URL, FE_API_DOCUSEAL_GENERATE_TOKEN } from '@/utils/Url';
|
||||
import {
|
||||
BE_DOCUSEAL_GET_JWT,
|
||||
BASE_URL,
|
||||
FE_API_DOCUSEAL_GENERATE_TOKEN,
|
||||
} from '@/utils/Url';
|
||||
import Button from '@/components/Button'; // Import du composant Button
|
||||
import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
|
||||
export default function FileUpload({ handleCreateTemplateMaster, handleEditTemplateMaster, fileToEdit = null, onSuccess }) {
|
||||
export default function FileUpload({
|
||||
handleCreateTemplateMaster,
|
||||
handleEditTemplateMaster,
|
||||
fileToEdit = null,
|
||||
onSuccess,
|
||||
}) {
|
||||
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
|
||||
const [order, setOrder] = useState(0);
|
||||
const [groups, setGroups] = useState([]);
|
||||
@ -24,7 +38,9 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
useEffect(() => {
|
||||
fetchRegistrationFileGroups(selectedEstablishmentId).then(data => setGroups(data));
|
||||
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
|
||||
setGroups(data)
|
||||
);
|
||||
|
||||
if (fileToEdit) {
|
||||
setUploadedFileName(fileToEdit.name || '');
|
||||
@ -40,7 +56,9 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
|
||||
.then((data) => {
|
||||
setToken(data.token);
|
||||
})
|
||||
.catch((error) => console.error('Erreur lors de la génération du token:', error));
|
||||
.catch((error) =>
|
||||
console.error('Erreur lors de la génération du token:', error)
|
||||
);
|
||||
}, [fileToEdit]);
|
||||
|
||||
const handleFileNameChange = (event) => {
|
||||
@ -49,14 +67,14 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
|
||||
|
||||
const handleGroupChange = (selectedGroups) => {
|
||||
setSelectedGroups(selectedGroups);
|
||||
|
||||
const details = selectedGroups.flatMap(group =>
|
||||
group.registration_forms.flatMap(form =>
|
||||
form.guardians.map(guardian => ({
|
||||
|
||||
const details = selectedGroups.flatMap((group) =>
|
||||
group.registration_forms.flatMap((form) =>
|
||||
form.guardians.map((guardian) => ({
|
||||
email: guardian.associated_profile_email,
|
||||
last_name: form.last_name,
|
||||
first_name: form.first_name,
|
||||
registration_form: form.student_id
|
||||
registration_form: form.student_id,
|
||||
}))
|
||||
)
|
||||
);
|
||||
@ -65,9 +83,9 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
|
||||
|
||||
const handleLoad = (detail) => {
|
||||
const templateId = detail?.id;
|
||||
logger.debug('loading template id : ', detail)
|
||||
logger.debug('loading template id : ', detail);
|
||||
setTemplateMaster(detail);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = (detail) => {
|
||||
logger.debug('Uploaded file detail:', detail);
|
||||
@ -75,54 +93,57 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
|
||||
};
|
||||
|
||||
const handleChange = (detail) => {
|
||||
logger.debug(detail)
|
||||
logger.debug(detail);
|
||||
setUploadedFileName(detail.name);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (data) => {
|
||||
const is_required = (data.fields.length > 0)
|
||||
const is_required = data.fields.length > 0;
|
||||
if (fileToEdit) {
|
||||
logger.debug('Modification du template master:', templateMaster?.id);
|
||||
handleEditTemplateMaster({
|
||||
name: uploadedFileName,
|
||||
group_ids: selectedGroups.map(group => group.id),
|
||||
group_ids: selectedGroups.map((group) => group.id),
|
||||
id: templateMaster?.id,
|
||||
is_required: is_required
|
||||
is_required: is_required,
|
||||
});
|
||||
} else {
|
||||
logger.debug('Création du template master:', templateMaster?.id);
|
||||
handleCreateTemplateMaster({
|
||||
name: uploadedFileName,
|
||||
group_ids: selectedGroups.map(group => group.id),
|
||||
group_ids: selectedGroups.map((group) => group.id),
|
||||
id: templateMaster?.id,
|
||||
is_required: is_required
|
||||
is_required: is_required,
|
||||
});
|
||||
|
||||
|
||||
guardianDetails.forEach((guardian, index) => {
|
||||
logger.debug('creation du clone avec required : ', is_required)
|
||||
logger.debug('creation du clone avec required : ', is_required);
|
||||
cloneTemplate(templateMaster?.id, guardian.email, is_required)
|
||||
.then(clonedDocument => {
|
||||
.then((clonedDocument) => {
|
||||
// Sauvegarde des templates clonés dans la base de données
|
||||
const data = {
|
||||
name: `${uploadedFileName}_${guardian.first_name}_${guardian.last_name}`,
|
||||
slug: clonedDocument.slug,
|
||||
id: clonedDocument.id,
|
||||
master: templateMaster?.id,
|
||||
registration_form: guardian.registration_form
|
||||
registration_form: guardian.registration_form,
|
||||
};
|
||||
createRegistrationTemplates(data, csrfToken)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
logger.debug('Template enregistré avec succès:', response);
|
||||
onSuccess();
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('Erreur lors de l\'enregistrement du template:', error);
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
"Erreur lors de l'enregistrement du template:",
|
||||
error
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// Logique pour envoyer chaque template au submitter
|
||||
logger.debug('Sending template to:', guardian.email);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error('Error during cloning or sending:', error);
|
||||
});
|
||||
});
|
||||
@ -144,10 +165,10 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
|
||||
</div>
|
||||
<div className="col-span-8">
|
||||
{token && (
|
||||
<DocusealBuilder
|
||||
token={token}
|
||||
<DocusealBuilder
|
||||
token={token}
|
||||
headers={{
|
||||
'Authorization': `Bearer ${token}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
}}
|
||||
withSendButton={false}
|
||||
withSaveButton={false}
|
||||
@ -168,4 +189,4 @@ export default function FileUpload({ handleCreateTemplateMaster, handleEditTempl
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Plus, Download, Edit3, Trash2, FolderPlus, Signature } from 'lucide-react';
|
||||
import {
|
||||
Plus,
|
||||
Download,
|
||||
Edit3,
|
||||
Trash2,
|
||||
FolderPlus,
|
||||
Signature,
|
||||
} from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
import Table from '@/components/Table';
|
||||
import FileUpload from '@/components/Structure/Files/FileUpload';
|
||||
@ -13,12 +20,15 @@ import {
|
||||
createRegistrationTemplateMaster,
|
||||
editRegistrationTemplateMaster,
|
||||
deleteRegistrationTemplateMaster,
|
||||
fetchRegistrationTemplates
|
||||
fetchRegistrationTemplates,
|
||||
} from '@/app/actions/registerFileGroupAction';
|
||||
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function FilesGroupsManagement({ csrfToken, selectedEstablishmentId }) {
|
||||
export default function FilesGroupsManagement({
|
||||
csrfToken,
|
||||
selectedEstablishmentId,
|
||||
}) {
|
||||
const [templateMasters, setTemplateMasters] = useState([]);
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
@ -32,13 +42,19 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
|
||||
const handleReloadTemplates = () => {
|
||||
setReloadTemplates(true);
|
||||
}
|
||||
};
|
||||
|
||||
const transformFileData = (file, groups) => {
|
||||
const groupInfos = file.groups.map(groupId => groups.find(g => g.id === groupId) || { id: groupId, name: 'Groupe inconnu' });
|
||||
const groupInfos = file.groups.map(
|
||||
(groupId) =>
|
||||
groups.find((g) => g.id === groupId) || {
|
||||
id: groupId,
|
||||
name: 'Groupe inconnu',
|
||||
}
|
||||
);
|
||||
return {
|
||||
...file,
|
||||
groups: groupInfos
|
||||
groups: groupInfos,
|
||||
};
|
||||
};
|
||||
|
||||
@ -47,58 +63,75 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
Promise.all([
|
||||
fetchRegistrationTemplateMaster(),
|
||||
fetchRegistrationFileGroups(selectedEstablishmentId),
|
||||
fetchRegistrationTemplates()
|
||||
]).then(([filesTemplateMasters, groupsData, filesTemplates]) => {
|
||||
setGroups(groupsData);
|
||||
setTemplates(filesTemplates);
|
||||
// Transformer chaque fichier pour inclure les informations complètes du groupe
|
||||
const transformedFiles = filesTemplateMasters.map(file => transformFileData(file, groupsData));
|
||||
setTemplateMasters(transformedFiles);
|
||||
}).catch(err => {
|
||||
console.log(err.message);
|
||||
}).finally(() => {
|
||||
setReloadTemplates(false);
|
||||
});
|
||||
fetchRegistrationTemplates(),
|
||||
])
|
||||
.then(([filesTemplateMasters, groupsData, filesTemplates]) => {
|
||||
setGroups(groupsData);
|
||||
setTemplates(filesTemplates);
|
||||
// Transformer chaque fichier pour inclure les informations complètes du groupe
|
||||
const transformedFiles = filesTemplateMasters.map((file) =>
|
||||
transformFileData(file, groupsData)
|
||||
);
|
||||
setTemplateMasters(transformedFiles);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setReloadTemplates(false);
|
||||
});
|
||||
}
|
||||
}, [reloadTemplates, selectedEstablishmentId]);
|
||||
|
||||
const deleteTemplateMaster = (templateMaster) => {
|
||||
// Supprimer les clones associés via l'API DocuSeal
|
||||
const removeClonesPromises = templates
|
||||
.filter(template => template.master === templateMaster.id)
|
||||
.map(template => removeTemplate(template.id));
|
||||
.filter((template) => template.master === templateMaster.id)
|
||||
.map((template) => removeTemplate(template.id));
|
||||
|
||||
// Ajouter la suppression du master à la liste des promesses
|
||||
removeClonesPromises.push(removeTemplate(templateMaster.id));
|
||||
|
||||
// Attendre que toutes les suppressions dans DocuSeal soient terminées
|
||||
Promise.all(removeClonesPromises)
|
||||
.then(responses => {
|
||||
const allSuccessful = responses.every(response => response.ok);
|
||||
.then((responses) => {
|
||||
const allSuccessful = responses.every((response) => response.ok);
|
||||
if (allSuccessful) {
|
||||
logger.debug('Master et clones supprimés avec succès de DocuSeal.');
|
||||
|
||||
// Supprimer le template master de la base de données
|
||||
deleteRegistrationTemplateMaster(templateMaster.id, csrfToken)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
setTemplateMasters(templateMasters.filter(fichier => fichier.id !== templateMaster.id));
|
||||
setTemplateMasters(
|
||||
templateMasters.filter(
|
||||
(fichier) => fichier.id !== templateMaster.id
|
||||
)
|
||||
);
|
||||
alert('Fichier supprimé avec succès.');
|
||||
} else {
|
||||
alert('Erreur lors de la suppression du fichier dans la base de données.');
|
||||
alert(
|
||||
'Erreur lors de la suppression du fichier dans la base de données.'
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error deleting file from database:', error);
|
||||
alert('Erreur lors de la suppression du fichier dans la base de données.');
|
||||
alert(
|
||||
'Erreur lors de la suppression du fichier dans la base de données.'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
alert('Erreur lors de la suppression du master ou des clones dans DocuSeal.');
|
||||
alert(
|
||||
'Erreur lors de la suppression du master ou des clones dans DocuSeal.'
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error removing template from DocuSeal:', error);
|
||||
alert('Erreur lors de la suppression du master ou des clones dans DocuSeal.');
|
||||
alert(
|
||||
'Erreur lors de la suppression du master ou des clones dans DocuSeal.'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -107,22 +140,24 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId
|
||||
templateId,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return response.json().then((err) => {
|
||||
throw new Error(err.message);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => { throw new Error(err.message); });
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error removing template:', error);
|
||||
throw error;
|
||||
});
|
||||
.catch((error) => {
|
||||
console.error('Error removing template:', error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const editTemplateMaster = (file) => {
|
||||
@ -131,72 +166,76 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateTemplateMaster = ({name, group_ids, id, is_required}) => {
|
||||
const handleCreateTemplateMaster = ({ name, group_ids, id, is_required }) => {
|
||||
const data = {
|
||||
name: name,
|
||||
id: id,
|
||||
groups: group_ids,
|
||||
is_required: is_required
|
||||
is_required: is_required,
|
||||
};
|
||||
logger.debug(data);
|
||||
|
||||
|
||||
createRegistrationTemplateMaster(data, csrfToken)
|
||||
.then(data => {
|
||||
// Transformer le nouveau fichier avec les informations du groupe
|
||||
const transformedFile = transformFileData(data, groups);
|
||||
setTemplateMasters(prevFiles => [...prevFiles, transformedFile]);
|
||||
setIsModalOpen(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error uploading file:', error);
|
||||
});
|
||||
.then((data) => {
|
||||
// Transformer le nouveau fichier avec les informations du groupe
|
||||
const transformedFile = transformFileData(data, groups);
|
||||
setTemplateMasters((prevFiles) => [...prevFiles, transformedFile]);
|
||||
setIsModalOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error uploading file:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditTemplateMaster = ({name, group_ids, id, is_required}) => {
|
||||
const handleEditTemplateMaster = ({ name, group_ids, id, is_required }) => {
|
||||
const data = {
|
||||
name: name,
|
||||
id: id,
|
||||
groups: group_ids,
|
||||
is_required: is_required
|
||||
is_required: is_required,
|
||||
};
|
||||
logger.debug(data);
|
||||
|
||||
editRegistrationTemplateMaster(id, data, csrfToken)
|
||||
.then(data => {
|
||||
// Transformer le fichier mis à jour avec les informations du groupe
|
||||
const transformedFile = transformFileData(data, groups);
|
||||
setTemplateMasters(prevFichiers =>
|
||||
prevFichiers.map(f => f.id === id ? transformedFile : f)
|
||||
);
|
||||
setIsModalOpen(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error editing file:', error);
|
||||
alert('Erreur lors de la modification du fichier');
|
||||
});
|
||||
.then((data) => {
|
||||
// Transformer le fichier mis à jour avec les informations du groupe
|
||||
const transformedFile = transformFileData(data, groups);
|
||||
setTemplateMasters((prevFichiers) =>
|
||||
prevFichiers.map((f) => (f.id === id ? transformedFile : f))
|
||||
);
|
||||
setIsModalOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error editing file:', error);
|
||||
alert('Erreur lors de la modification du fichier');
|
||||
});
|
||||
};
|
||||
|
||||
const handleGroupSubmit = (groupData) => {
|
||||
if (groupToEdit) {
|
||||
editRegistrationFileGroup(groupToEdit.id, groupData, csrfToken)
|
||||
.then(updatedGroup => {
|
||||
setGroups(groups.map(group => group.id === groupToEdit.id ? updatedGroup : group));
|
||||
.then((updatedGroup) => {
|
||||
setGroups(
|
||||
groups.map((group) =>
|
||||
group.id === groupToEdit.id ? updatedGroup : group
|
||||
)
|
||||
);
|
||||
setGroupToEdit(null);
|
||||
setIsGroupModalOpen(false);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error handling group:', error);
|
||||
alert('Erreur lors de l\'opération sur le groupe');
|
||||
alert("Erreur lors de l'opération sur le groupe");
|
||||
});
|
||||
} else {
|
||||
createRegistrationFileGroup(groupData, csrfToken)
|
||||
.then(newGroup => {
|
||||
.then((newGroup) => {
|
||||
setGroups([...groups, newGroup]);
|
||||
setIsGroupModalOpen(false);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error handling group:', error);
|
||||
alert('Erreur lors de l\'opération sur le groupe');
|
||||
alert("Erreur lors de l'opération sur le groupe");
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -208,9 +247,13 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
|
||||
const handleGroupDelete = (groupId) => {
|
||||
// Vérifier si des templateMasters utilisent ce groupe
|
||||
const filesInGroup = templateMasters.filter(file => file.group && file.group.id === groupId);
|
||||
const filesInGroup = templateMasters.filter(
|
||||
(file) => file.group && file.group.id === groupId
|
||||
);
|
||||
if (filesInGroup.length > 0) {
|
||||
alert('Impossible de supprimer ce groupe car il contient des templateMasters. Veuillez d\'abord retirer tous les templateMasters de ce groupe.');
|
||||
alert(
|
||||
"Impossible de supprimer ce groupe car il contient des templateMasters. Veuillez d'abord retirer tous les templateMasters de ce groupe."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -223,54 +266,88 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors de la suppression du groupe.');
|
||||
}
|
||||
setGroups(groups.filter(group => group.id !== groupId));
|
||||
setGroups(groups.filter((group) => group.id !== groupId));
|
||||
alert('Groupe supprimé avec succès.');
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error deleting group:', error);
|
||||
alert(error.message || 'Erreur lors de la suppression du groupe. Vérifiez qu\'aucune inscription n\'utilise ce groupe.');
|
||||
alert(
|
||||
error.message ||
|
||||
"Erreur lors de la suppression du groupe. Vérifiez qu'aucune inscription n'utilise ce groupe."
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const filteredFiles = templateMasters.filter(file => {
|
||||
const filteredFiles = templateMasters.filter((file) => {
|
||||
if (!selectedGroup) return true;
|
||||
return file.groups && file.groups.some(group => group.id === parseInt(selectedGroup));
|
||||
return (
|
||||
file.groups &&
|
||||
file.groups.some((group) => group.id === parseInt(selectedGroup))
|
||||
);
|
||||
});
|
||||
|
||||
const columnsFiles = [
|
||||
{ name: 'Nom du fichier', transform: (row) => row.name },
|
||||
{ name: 'Groupes', transform: (row) => row.groups && row.groups.length > 0 ? row.groups.map(group => group.name).join(', ') : 'Aucun' },
|
||||
{ name: 'Actions', transform: (row) => (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{row.file && (
|
||||
<a href={`${BASE_URL}${row.file}`} target='_blank' className="text-blue-500 hover:text-blue-700">
|
||||
<Download className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
<button onClick={() => editTemplateMaster(row)} className="text-blue-500 hover:text-blue-700">
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button onClick={() => deleteTemplateMaster(row)} className="text-red-500 hover:text-red-700">
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
name: 'Groupes',
|
||||
transform: (row) =>
|
||||
row.groups && row.groups.length > 0
|
||||
? row.groups.map((group) => group.name).join(', ')
|
||||
: 'Aucun',
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
transform: (row) => (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{row.file && (
|
||||
<a
|
||||
href={`${BASE_URL}${row.file}`}
|
||||
target="_blank"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
onClick={() => editTemplateMaster(row)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteTemplateMaster(row)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const columnsGroups = [
|
||||
{ name: 'Nom du groupe', transform: (row) => row.name },
|
||||
{ name: 'Description', transform: (row) => row.description },
|
||||
{ name: 'Actions', transform: (row) => (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button onClick={() => handleGroupEdit(row)} className="text-blue-500 hover:text-blue-700">
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button onClick={() => handleGroupDelete(row.id)} className="text-red-500 hover:text-red-700">
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
name: 'Actions',
|
||||
transform: (row) => (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={() => handleGroupEdit(row)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGroupDelete(row.id)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@ -292,12 +369,16 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
onSuccess={handleReloadTemplates}
|
||||
/>
|
||||
)}
|
||||
modalClassName='w-4/5 h-4/5'
|
||||
modalClassName="w-4/5 h-4/5"
|
||||
/>
|
||||
<Modal
|
||||
isOpen={isGroupModalOpen}
|
||||
setIsOpen={setIsGroupModalOpen}
|
||||
title={groupToEdit ? "Modifier le groupe" : "Ajouter un groupe de templateMasters"}
|
||||
title={
|
||||
groupToEdit
|
||||
? 'Modifier le groupe'
|
||||
: 'Ajouter un groupe de templateMasters'
|
||||
}
|
||||
ContentComponent={() => (
|
||||
<RegistrationFileGroupForm
|
||||
onSubmit={handleGroupSubmit}
|
||||
@ -334,12 +415,17 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
onChange={(e) => setSelectedGroup(e.target.value)}
|
||||
>
|
||||
<option value="">Tous les groupes</option>
|
||||
{groups.map(group => (
|
||||
<option key={group.id} value={group.id}>{group.name}</option>
|
||||
{groups.map((group) => (
|
||||
<option key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => { setIsModalOpen(true); setIsEditing(false); }}
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
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" />
|
||||
|
||||
@ -53,4 +53,4 @@ export default function RegistrationFileGroupForm({ onSubmit, initialData }) {
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,17 +8,19 @@ export default function RegistrationFileGroupList() {
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
useEffect(() => {
|
||||
fetchRegistrationFileGroups(selectedEstablishmentId).then(data => setGroups(data));
|
||||
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
|
||||
setGroups(data)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Groupes de fichiers d'inscription</h2>
|
||||
<ul>
|
||||
{groups.map(group => (
|
||||
{groups.map((group) => (
|
||||
<li key={group.id}>{group.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,17 +7,27 @@ const ClassesInformation = ({ selectedClass, isPastYear }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`w-full p-6 shadow-lg rounded-full border relative ${isPastYear ? 'bg-gray-200 border-gray-600' : 'bg-emerald-200 border-emerald-500'}`}>
|
||||
<div className={`border-b pb-4 ${isPastYear ? 'border-gray-600' : 'border-emerald-500'}`}>
|
||||
<p className="text-gray-700 text-center"><strong>{selectedClass.age_range} ans</strong></p>
|
||||
<div
|
||||
className={`w-full p-6 shadow-lg rounded-full border relative ${isPastYear ? 'bg-gray-200 border-gray-600' : 'bg-emerald-200 border-emerald-500'}`}
|
||||
>
|
||||
<div
|
||||
className={`border-b pb-4 ${isPastYear ? 'border-gray-600' : 'border-emerald-500'}`}
|
||||
>
|
||||
<p className="text-gray-700 text-center">
|
||||
<strong>{selectedClass.age_range} ans</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`border-b pb-4 ${isPastYear ? 'border-gray-600' : 'border-emerald-500'}`}>
|
||||
<div
|
||||
className={`border-b pb-4 ${isPastYear ? 'border-gray-600' : 'border-emerald-500'}`}
|
||||
>
|
||||
<div className="flex flex-wrap justify-center space-x-4">
|
||||
{selectedClass.teachers.map((teacher) => (
|
||||
<div key={teacher.id} className="relative group mt-4">
|
||||
<TeacherLabel nom={teacher.nom} prenom={teacher.prenom} />
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-full mb-2 w-max px-4 py-2 text-white bg-gray-800 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<p className="text-sm">{teacher.nom} {teacher.prenom}</p>
|
||||
<p className="text-sm">
|
||||
{teacher.nom} {teacher.prenom}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -5,17 +5,21 @@ import logger from '@/utils/logger';
|
||||
const ClassesList = ({ classes, onClassSelect, selectedClassId }) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentSchoolYearStart = currentMonth >= 8 ? currentYear : currentYear - 1;
|
||||
const currentSchoolYearStart =
|
||||
currentMonth >= 8 ? currentYear : currentYear - 1;
|
||||
|
||||
const handleClassClick = (classe) => {
|
||||
logger.debug(`Classe sélectionnée: ${classe.atmosphere_name}, Année scolaire: ${classe.school_year}`);
|
||||
logger.debug(
|
||||
`Classe sélectionnée: ${classe.atmosphere_name}, Année scolaire: ${classe.school_year}`
|
||||
);
|
||||
onClassSelect(classe);
|
||||
};
|
||||
|
||||
const categorizedClasses = classes.reduce((acc, classe) => {
|
||||
const { school_year } = classe;
|
||||
const [startYear] = school_year.split('-').map(Number);
|
||||
const category = startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes';
|
||||
const category =
|
||||
startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes';
|
||||
|
||||
if (!acc[category]) {
|
||||
acc[category] = [];
|
||||
@ -45,8 +49,12 @@ const ClassesList = ({ classes, onClassSelect, selectedClassId }) => {
|
||||
onClick={() => handleClassClick(classe)}
|
||||
style={{ maxWidth: '400px' }}
|
||||
>
|
||||
<div className="flex-1 text-sm font-medium">{classe.atmosphere_name}</div>
|
||||
<div className="flex-1 text-sm font-medium">{classe.school_year}</div>
|
||||
<div className="flex-1 text-sm font-medium">
|
||||
{classe.atmosphere_name}
|
||||
</div>
|
||||
<div className="flex-1 text-sm font-medium">
|
||||
{classe.school_year}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -63,8 +71,12 @@ const ClassesList = ({ classes, onClassSelect, selectedClassId }) => {
|
||||
onClick={() => handleClassClick(classe)}
|
||||
style={{ maxWidth: '400px' }}
|
||||
>
|
||||
<div className="flex-1 text-sm font-medium">{classe.atmosphere_name}</div>
|
||||
<div className="flex-1 text-sm font-medium">{classe.school_year}</div>
|
||||
<div className="flex-1 text-sm font-medium">
|
||||
{classe.atmosphere_name}
|
||||
</div>
|
||||
<div className="flex-1 text-sm font-medium">
|
||||
{classe.school_year}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -5,11 +5,11 @@ import { UserIcon } from 'lucide-react'; // Assure-toi d'importer l'icône que t
|
||||
const DraggableSpeciality = ({ speciality }) => {
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
type: 'SPECIALITY',
|
||||
item: {
|
||||
id: speciality.id,
|
||||
name: speciality.nom,
|
||||
color: speciality.codeCouleur,
|
||||
teachers: speciality.teachers
|
||||
item: {
|
||||
id: speciality.id,
|
||||
name: speciality.nom,
|
||||
color: speciality.codeCouleur,
|
||||
teachers: speciality.teachers,
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
@ -21,7 +21,11 @@ const DraggableSpeciality = ({ speciality }) => {
|
||||
ref={drag}
|
||||
key={speciality.id}
|
||||
className={`relative flex items-center px-4 py-2 rounded-full font-bold text-white text-center shadow-lg cursor-pointer transition-transform duration-200 ease-in-out transform ${isDragging ? 'opacity-50 scale-95' : 'scale-100 hover:scale-105 hover:shadow-xl'}`}
|
||||
style={{ backgroundColor: speciality.codeCouleur, minWidth: '200px', maxWidth: '400px' }}
|
||||
style={{
|
||||
backgroundColor: speciality.codeCouleur,
|
||||
minWidth: '200px',
|
||||
maxWidth: '400px',
|
||||
}}
|
||||
title={speciality.nom}
|
||||
>
|
||||
{speciality.nom}
|
||||
|
||||
@ -4,28 +4,33 @@ import PropTypes from 'prop-types';
|
||||
|
||||
// Définition du composant DropTargetCell
|
||||
const DropTargetCell = ({ day, hour, courses, onDrop, onClick }) => {
|
||||
const [{ isOver, canDrop }, drop] = useDrop(() => ({
|
||||
accept: 'SPECIALITY',
|
||||
drop: (item) => onDrop(item, hour, day),
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
const [{ isOver, canDrop }, drop] = useDrop(
|
||||
() => ({
|
||||
accept: 'SPECIALITY',
|
||||
drop: (item) => onDrop(item, hour, day),
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
}),
|
||||
}),
|
||||
}), [hour, day]);
|
||||
[hour, day]
|
||||
);
|
||||
|
||||
const isColorDark = (color) => {
|
||||
if (!color) return false;
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
return (r * 0.299 + g * 0.587 + b * 0.114) < 150;
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 < 150;
|
||||
};
|
||||
|
||||
const isToday = (someDate) => {
|
||||
const today = new Date();
|
||||
return someDate.getDate() === today.getDate() &&
|
||||
someDate.getMonth() === today.getMonth() &&
|
||||
someDate.getFullYear() === today.getFullYear();
|
||||
return (
|
||||
someDate.getDate() === today.getDate() &&
|
||||
someDate.getMonth() === today.getMonth() &&
|
||||
someDate.getFullYear() === today.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
// Vérifie si c'est une heure pleine
|
||||
@ -38,19 +43,32 @@ const DropTargetCell = ({ day, hour, courses, onDrop, onClick }) => {
|
||||
${isToday(new Date(day)) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}
|
||||
hover:bg-emerald-100 h-10 border-b
|
||||
${isFullHour ? 'border-emerald-200' : 'border-gray-300'}
|
||||
${isOver && canDrop ? 'bg-emerald-200' : ''}`} // Ajouté pour indiquer le drop
|
||||
style={{ display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
width: '100%' }}
|
||||
${isOver && canDrop ? 'bg-emerald-200' : ''}`} // Ajouté pour indiquer le drop
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{courses.map(course => (
|
||||
<div key={course.matiere}
|
||||
className="flex flex-row items-center justify-center gap-2"
|
||||
style={{ backgroundColor: course.color, color: isColorDark(course.color) ? '#E5E5E5' : '#333333', width: '100%', height: '100%' }}>
|
||||
<div style={{ textAlign: 'center', fontWeight: 'bold' }}>{course.matiere}</div>
|
||||
<div style={{ fontStyle: 'italic', textAlign: 'center' }}>{course.teachers.join(', ')}</div>
|
||||
{courses.map((course) => (
|
||||
<div
|
||||
key={course.matiere}
|
||||
className="flex flex-row items-center justify-center gap-2"
|
||||
style={{
|
||||
backgroundColor: course.color,
|
||||
color: isColorDark(course.color) ? '#E5E5E5' : '#333333',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center', fontWeight: 'bold' }}>
|
||||
{course.matiere}
|
||||
</div>
|
||||
<div style={{ fontStyle: 'italic', textAlign: 'center' }}>
|
||||
{course.teachers.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -8,11 +8,21 @@ import { useClasses } from '@/context/ClassesContext';
|
||||
import { Calendar } from 'lucide-react';
|
||||
import SpecialityEventModal from '@/components/Structure/Planning/SpecialityEventModal'; // Assurez-vous du bon chemin d'importation
|
||||
|
||||
const PlanningClassView = ({ schedule, onDrop, selectedLevel, handleUpdatePlanning, classe }) => {
|
||||
const PlanningClassView = ({
|
||||
schedule,
|
||||
onDrop,
|
||||
selectedLevel,
|
||||
handleUpdatePlanning,
|
||||
classe,
|
||||
}) => {
|
||||
const { formData } = useClasseForm();
|
||||
const { determineInitialPeriod } = useClasses();
|
||||
|
||||
const [currentPeriod, setCurrentPeriod] = useState(schedule?.emploiDuTemps ? determineInitialPeriod(schedule.emploiDuTemps) : null);
|
||||
const [currentPeriod, setCurrentPeriod] = useState(
|
||||
schedule?.emploiDuTemps
|
||||
? determineInitialPeriod(schedule.emploiDuTemps)
|
||||
: null
|
||||
);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedCell, setSelectedCell] = useState(null);
|
||||
const [existingEvent, setExistingEvent] = useState(null);
|
||||
@ -36,36 +46,53 @@ const PlanningClassView = ({ schedule, onDrop, selectedLevel, handleUpdatePlanni
|
||||
);
|
||||
}
|
||||
|
||||
const emploiDuTemps = schedule.emploiDuTemps[currentPeriod] || schedule.emploiDuTemps;
|
||||
const emploiDuTemps =
|
||||
schedule.emploiDuTemps[currentPeriod] || schedule.emploiDuTemps;
|
||||
const joursOuverture = Object.keys(emploiDuTemps);
|
||||
const currentWeekDays = joursOuverture
|
||||
.map(day => {
|
||||
switch(day.toLowerCase()) {
|
||||
case 'lundi': return 1;
|
||||
case 'mardi': return 2;
|
||||
case 'mercredi': return 3;
|
||||
case 'jeudi': return 4;
|
||||
case 'vendredi': return 5;
|
||||
case 'samedi': return 6;
|
||||
case 'dimanche': return 7;
|
||||
default: return 0;
|
||||
}
|
||||
.map((day) => {
|
||||
switch (day.toLowerCase()) {
|
||||
case 'lundi':
|
||||
return 1;
|
||||
case 'mardi':
|
||||
return 2;
|
||||
case 'mercredi':
|
||||
return 3;
|
||||
case 'jeudi':
|
||||
return 4;
|
||||
case 'vendredi':
|
||||
return 5;
|
||||
case 'samedi':
|
||||
return 6;
|
||||
case 'dimanche':
|
||||
return 7;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a - b) // Trier les jours dans l'ordre croissant
|
||||
.map(day => addDays(startOfWeek(new Date(), { weekStartsOn: 1 }), day - 1)); // Calculer les dates à partir du lundi
|
||||
.map((day) =>
|
||||
addDays(startOfWeek(new Date(), { weekStartsOn: 1 }), day - 1)
|
||||
); // Calculer les dates à partir du lundi
|
||||
|
||||
const getFilteredEvents = (day, time, level) => {
|
||||
const [hour, minute] = time.split(':').map(Number);
|
||||
const startTime = hour + minute / 60; // Convertir l'heure en fraction d'heure
|
||||
|
||||
return emploiDuTemps[day.toLowerCase()]?.filter(event => {
|
||||
return (
|
||||
emploiDuTemps[day.toLowerCase()]?.filter((event) => {
|
||||
const [eventHour, eventMinute] = event.heure.split(':').map(Number);
|
||||
const eventStartTime = eventHour + eventMinute / 60;
|
||||
const eventEndTime = eventStartTime + parseFloat(event.duree);
|
||||
|
||||
|
||||
// Filtrer en fonction du selectedLevel
|
||||
return schedule.niveau === level && startTime >= eventStartTime && startTime < eventEndTime;
|
||||
}) || [];
|
||||
return (
|
||||
schedule.niveau === level &&
|
||||
startTime >= eventStartTime &&
|
||||
startTime < eventEndTime
|
||||
);
|
||||
}) || []
|
||||
);
|
||||
};
|
||||
|
||||
const handleCellClick = (hour, day) => {
|
||||
@ -78,10 +105,14 @@ const PlanningClassView = ({ schedule, onDrop, selectedLevel, handleUpdatePlanni
|
||||
|
||||
const renderTimeSlots = () => {
|
||||
const timeSlots = [];
|
||||
|
||||
for (let hour = parseInt(formData.time_range[0], 10); hour <= parseInt(formData.time_range[1], 10); hour++) {
|
||||
|
||||
for (
|
||||
let hour = parseInt(formData.time_range[0], 10);
|
||||
hour <= parseInt(formData.time_range[1], 10);
|
||||
hour++
|
||||
) {
|
||||
const hourString = hour.toString().padStart(2, '0');
|
||||
|
||||
|
||||
timeSlots.push(
|
||||
<React.Fragment key={`${hourString}:00-${Math.random()}`}>
|
||||
<div className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
|
||||
@ -95,14 +126,22 @@ const PlanningClassView = ({ schedule, onDrop, selectedLevel, handleUpdatePlanni
|
||||
<DropTargetCell
|
||||
hour={`${hourString}:00`}
|
||||
day={day}
|
||||
courses={getFilteredEvents(day, `${hourString}:00`, selectedLevel)}
|
||||
courses={getFilteredEvents(
|
||||
day,
|
||||
`${hourString}:00`,
|
||||
selectedLevel
|
||||
)}
|
||||
onDrop={onDrop}
|
||||
onClick={(hour, day) => handleCellClick(hour, day)}
|
||||
/>
|
||||
<DropTargetCell
|
||||
hour={`${hourString}:30`}
|
||||
day={day}
|
||||
courses={getFilteredEvents(day, `${hourString}:30`, selectedLevel)}
|
||||
courses={getFilteredEvents(
|
||||
day,
|
||||
`${hourString}:30`,
|
||||
selectedLevel
|
||||
)}
|
||||
onDrop={onDrop}
|
||||
onClick={(hour, day) => handleCellClick(hour, day)}
|
||||
/>
|
||||
@ -124,46 +163,77 @@ const PlanningClassView = ({ schedule, onDrop, selectedLevel, handleUpdatePlanni
|
||||
</h2>
|
||||
{schedule.emploiDuTemps.S1 && schedule.emploiDuTemps.S2 && (
|
||||
<div>
|
||||
<button onClick={() => setCurrentPeriod('S1')} className={`px-4 py-2 ${currentPeriod === 'S1' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||
<button
|
||||
onClick={() => setCurrentPeriod('S1')}
|
||||
className={`px-4 py-2 ${currentPeriod === 'S1' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
|
||||
>
|
||||
Semestre 1
|
||||
</button>
|
||||
<button onClick={() => setCurrentPeriod('S2')} className={`px-4 py-2 ${currentPeriod === 'S2' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||
<button
|
||||
onClick={() => setCurrentPeriod('S2')}
|
||||
className={`px-4 py-2 ${currentPeriod === 'S2' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
|
||||
>
|
||||
Semestre 2
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{schedule.emploiDuTemps.T1 && schedule.emploiDuTemps.T2 && schedule.emploiDuTemps.T3 && (
|
||||
<div>
|
||||
<button onClick={() => setCurrentPeriod('T1')} className={`px-4 py-2 ${currentPeriod === 'T1' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||
Trimestre 1
|
||||
</button>
|
||||
<button onClick={() => setCurrentPeriod('T2')} className={`px-4 py-2 ${currentPeriod === 'T2' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||
Trimestre 2
|
||||
</button>
|
||||
<button onClick={() => setCurrentPeriod('T3')} className={`px-4 py-2 ${currentPeriod === 'T3' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}>
|
||||
Trimestre 3
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{schedule.emploiDuTemps.T1 &&
|
||||
schedule.emploiDuTemps.T2 &&
|
||||
schedule.emploiDuTemps.T3 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setCurrentPeriod('T1')}
|
||||
className={`px-4 py-2 ${currentPeriod === 'T1' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
|
||||
>
|
||||
Trimestre 1
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPeriod('T2')}
|
||||
className={`px-4 py-2 ${currentPeriod === 'T2' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
|
||||
>
|
||||
Trimestre 2
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPeriod('T3')}
|
||||
className={`px-4 py-2 ${currentPeriod === 'T3' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
|
||||
>
|
||||
Trimestre 3
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
|
||||
{/* En-tête des jours */}
|
||||
<div className="grid w-full" style={{ gridTemplateColumns: `2.5rem repeat(${currentWeekDays.length}, 1fr)` }}>
|
||||
<div
|
||||
className="grid w-full"
|
||||
style={{
|
||||
gridTemplateColumns: `2.5rem repeat(${currentWeekDays.length}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
<div className="bg-gray-50 h-14"></div>
|
||||
{currentWeekDays.map((date, index) => (
|
||||
<div
|
||||
key={`${date}-${index}`}
|
||||
className="p-3 text-center bg-emerald-100 text-emerald-800 border-r border-emerald-200">
|
||||
className="p-3 text-center bg-emerald-100 text-emerald-800 border-r border-emerald-200"
|
||||
>
|
||||
<div className="text font-semibold">
|
||||
{format(date, 'EEEE', { locale: fr })}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Contenu du planning */}
|
||||
<div className="flex-1 overflow-y-auto relative" style={{ maxHeight: 'calc(100vh - 300px)' }}>
|
||||
<div className="grid bg-white relative" style={{ gridTemplateColumns: `2.5rem repeat(${currentWeekDays.length}, 1fr)` }}>
|
||||
<div
|
||||
className="flex-1 overflow-y-auto relative"
|
||||
style={{ maxHeight: 'calc(100vh - 300px)' }}
|
||||
>
|
||||
<div
|
||||
className="grid bg-white relative"
|
||||
style={{
|
||||
gridTemplateColumns: `2.5rem repeat(${currentWeekDays.length}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{renderTimeSlots()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
@ -16,14 +16,17 @@ import logger from '@/utils/logger';
|
||||
const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentSchoolYearStart = currentMonth >= 8 ? currentYear : currentYear - 1;
|
||||
const currentSchoolYearStart =
|
||||
currentMonth >= 8 ? currentYear : currentYear - 1;
|
||||
|
||||
const [selectedClass, setSelectedClass] = useState(null);
|
||||
const [selectedLevel, setSelectedLevel] = useState('');
|
||||
const [schedule, setSchedule] = useState(null);
|
||||
|
||||
const { getNiveauxTabs } = useClasses();
|
||||
const niveauxLabels = Array.isArray(selectedClass?.levels) ? getNiveauxTabs(selectedClass.levels) : [];
|
||||
const niveauxLabels = Array.isArray(selectedClass?.levels)
|
||||
? getNiveauxTabs(selectedClass.levels)
|
||||
: [];
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const handleOpenModal = () => setIsModalOpen(true);
|
||||
@ -36,14 +39,18 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
|
||||
setSelectedLevel(niveau);
|
||||
|
||||
const currentPlanning = selectedClass.plannings_read?.find(planning => planning.niveau === niveau);
|
||||
const currentPlanning = selectedClass.plannings_read?.find(
|
||||
(planning) => planning.niveau === niveau
|
||||
);
|
||||
setSchedule(currentPlanning ? currentPlanning.planning : {});
|
||||
}
|
||||
}, [selectedClass, niveauxLabels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedClass && selectedLevel) {
|
||||
const currentPlanning = selectedClass.plannings_read?.find(planning => planning.niveau === selectedLevel);
|
||||
const currentPlanning = selectedClass.plannings_read?.find(
|
||||
(planning) => planning.niveau === selectedLevel
|
||||
);
|
||||
setSchedule(currentPlanning ? currentPlanning.planning : {});
|
||||
}
|
||||
}, [selectedClass, selectedLevel]);
|
||||
@ -53,21 +60,28 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
};
|
||||
|
||||
const handleClassSelect = (classId) => {
|
||||
const selectedClasse = categorizedClasses['Actives'].find(classe => classe.id === classId);
|
||||
const selectedClasse = categorizedClasses['Actives'].find(
|
||||
(classe) => classe.id === classId
|
||||
);
|
||||
setSelectedClass(selectedClasse);
|
||||
setSelectedLevel('');
|
||||
};
|
||||
|
||||
const onDrop = (item, hour, day) => {
|
||||
const { id, name, color, teachers } = item;
|
||||
const newSchedule = { ...schedule, emploiDuTemps: schedule.emploiDuTemps || {} };
|
||||
const newSchedule = {
|
||||
...schedule,
|
||||
emploiDuTemps: schedule.emploiDuTemps || {},
|
||||
};
|
||||
|
||||
if (!newSchedule.emploiDuTemps[day]) {
|
||||
newSchedule.emploiDuTemps[day] = [];
|
||||
}
|
||||
const courseTime = `${hour.toString().padStart(2, '0')}:00`;
|
||||
|
||||
const existingCourseIndex = newSchedule.emploiDuTemps[day].findIndex(course => course.heure === courseTime);
|
||||
const existingCourseIndex = newSchedule.emploiDuTemps[day].findIndex(
|
||||
(course) => course.heure === courseTime
|
||||
);
|
||||
|
||||
const newCourse = {
|
||||
duree: '1',
|
||||
@ -84,12 +98,14 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
}
|
||||
|
||||
// Mettre à jour scheduleRef
|
||||
setSchedule(newSchedule)
|
||||
setSchedule(newSchedule);
|
||||
|
||||
// Utiliser `handleUpdatePlanning` pour mettre à jour le planning du niveau de la classe
|
||||
const planningId = selectedClass.plannings_read.find(planning => planning.niveau === selectedLevel)?.planning.id;
|
||||
const planningId = selectedClass.plannings_read.find(
|
||||
(planning) => planning.niveau === selectedLevel
|
||||
)?.planning.id;
|
||||
if (planningId) {
|
||||
logger.debug('newSchedule : ', newSchedule)
|
||||
logger.debug('newSchedule : ', newSchedule);
|
||||
handleUpdatePlanning(BE_SCHOOL_PLANNINGS_URL, planningId, newSchedule);
|
||||
}
|
||||
};
|
||||
@ -97,7 +113,8 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
const categorizedClasses = classes.reduce((acc, classe) => {
|
||||
const { school_year } = classe;
|
||||
const [startYear] = school_year.split('-').map(Number);
|
||||
const category = startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes';
|
||||
const category =
|
||||
startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes';
|
||||
|
||||
if (!acc[category]) {
|
||||
acc[category] = [];
|
||||
@ -111,7 +128,6 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="p-4 bg-gray-100 border-b">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
|
||||
{/* Colonne Classes */}
|
||||
<div className="p-4 bg-gray-50 rounded-lg shadow-inner">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
@ -120,17 +136,17 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
Classes
|
||||
</h2>
|
||||
</div>
|
||||
{categorizedClasses['Actives'] &&
|
||||
{categorizedClasses['Actives'] && (
|
||||
<TabsStructure
|
||||
activeTab={selectedClass?.id}
|
||||
setActiveTab={handleClassSelect}
|
||||
tabs={categorizedClasses['Actives'].map(classe => ({
|
||||
tabs={categorizedClasses['Actives'].map((classe) => ({
|
||||
id: classe.id,
|
||||
title: classe.atmosphere_name,
|
||||
icon: Users
|
||||
icon: Users,
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Colonne Niveaux */}
|
||||
@ -141,9 +157,13 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
Niveaux
|
||||
</h2>
|
||||
</div>
|
||||
{niveauxLabels &&
|
||||
<TabsStructure activeTab={selectedLevel} setActiveTab={handleLevelSelect} tabs={niveauxLabels} />
|
||||
}
|
||||
{niveauxLabels && (
|
||||
<TabsStructure
|
||||
activeTab={selectedLevel}
|
||||
setActiveTab={handleLevelSelect}
|
||||
tabs={niveauxLabels}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Colonne Spécialités */}
|
||||
@ -154,9 +174,10 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
Spécialités
|
||||
</h2>
|
||||
</div>
|
||||
<SpecialitiesList teachers={selectedClass ? selectedClass.teachers : []} />
|
||||
<SpecialitiesList
|
||||
teachers={selectedClass ? selectedClass.teachers : []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -171,7 +192,13 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
className="flex-1 relative"
|
||||
>
|
||||
<ClasseFormProvider initialClasse={selectedClass || {}}>
|
||||
<PlanningClassView schedule={schedule} onDrop={onDrop} selectedLevel={selectedLevel} handleUpdatePlanning={handleUpdatePlanning} classe={selectedClass} />
|
||||
<PlanningClassView
|
||||
schedule={schedule}
|
||||
onDrop={onDrop}
|
||||
selectedLevel={selectedLevel}
|
||||
handleUpdatePlanning={handleUpdatePlanning}
|
||||
classe={selectedClass}
|
||||
/>
|
||||
</ClasseFormProvider>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
@ -179,7 +206,6 @@ const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
|
||||
</DndProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default ScheduleManagement;
|
||||
|
||||
@ -17,5 +17,3 @@ const SpecialitiesList = ({ teachers }) => {
|
||||
};
|
||||
|
||||
export default SpecialitiesList;
|
||||
|
||||
|
||||
|
||||
@ -6,7 +6,14 @@ import { BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url';
|
||||
import { BookOpen, Users } from 'lucide-react';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, handleUpdatePlanning, classe }) => {
|
||||
const SpecialityEventModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
selectedCell,
|
||||
existingEvent,
|
||||
handleUpdatePlanning,
|
||||
classe,
|
||||
}) => {
|
||||
const { formData, setFormData } = useClasseForm();
|
||||
const { groupSpecialitiesBySubject } = useClasses();
|
||||
const [selectedSpeciality, setSelectedSpeciality] = useState('');
|
||||
@ -15,7 +22,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
specialiteId: '',
|
||||
teacherId: '',
|
||||
start: '',
|
||||
duration: '1h'
|
||||
duration: '1h',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -25,7 +32,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
specialiteId: '',
|
||||
teacherId: '',
|
||||
start: '',
|
||||
duration: '1h'
|
||||
duration: '1h',
|
||||
});
|
||||
setSelectedSpeciality('');
|
||||
setSelectedTeacher('');
|
||||
@ -42,10 +49,10 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
setSelectedTeacher(existingEvent.teacherId);
|
||||
} else {
|
||||
// Mode création
|
||||
setEventData(prev => ({
|
||||
setEventData((prev) => ({
|
||||
...prev,
|
||||
start: selectedCell.hour,
|
||||
duration: '1h'
|
||||
duration: '1h',
|
||||
}));
|
||||
setSelectedSpeciality('');
|
||||
setSelectedTeacher('');
|
||||
@ -69,17 +76,21 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
}
|
||||
|
||||
// Transformer eventData pour correspondre au format du planning
|
||||
const selectedTeacherData = formData.teachers.find(teacher => teacher.id === parseInt(eventData.teacherId, 10));
|
||||
const selectedTeacherData = formData.teachers.find(
|
||||
(teacher) => teacher.id === parseInt(eventData.teacherId, 10)
|
||||
);
|
||||
const newCourse = {
|
||||
color: '#FF0000', // Vous pouvez définir la couleur de manière dynamique si nécessaire
|
||||
teachers: selectedTeacherData ? [`${selectedTeacherData.nom} ${selectedTeacherData.prenom}`] : [],
|
||||
teachers: selectedTeacherData
|
||||
? [`${selectedTeacherData.nom} ${selectedTeacherData.prenom}`]
|
||||
: [],
|
||||
heure: `${eventData.start}:00`,
|
||||
duree: eventData.duration.replace('h', ''), // Supposons que '1h' signifie 1
|
||||
matiere: 'GROUPE'
|
||||
matiere: 'GROUPE',
|
||||
};
|
||||
|
||||
// Mettre à jour le planning
|
||||
const updatedPlannings = classe.plannings_read.map(planning => {
|
||||
const updatedPlannings = classe.plannings_read.map((planning) => {
|
||||
if (planning.niveau === selectedCell.selectedLevel) {
|
||||
const newEmploiDuTemps = { ...planning.emploiDuTemps };
|
||||
|
||||
@ -88,7 +99,9 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
}
|
||||
|
||||
const courseTime = newCourse.heure;
|
||||
const existingCourseIndex = newEmploiDuTemps[selectedCell.day].findIndex(course => course.heure === courseTime);
|
||||
const existingCourseIndex = newEmploiDuTemps[
|
||||
selectedCell.day
|
||||
].findIndex((course) => course.heure === courseTime);
|
||||
|
||||
if (existingCourseIndex !== -1) {
|
||||
newEmploiDuTemps[selectedCell.day][existingCourseIndex] = newCourse;
|
||||
@ -98,31 +111,37 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
|
||||
return {
|
||||
...planning,
|
||||
emploiDuTemps: newEmploiDuTemps
|
||||
emploiDuTemps: newEmploiDuTemps,
|
||||
};
|
||||
}
|
||||
return planning;
|
||||
});
|
||||
|
||||
const updatedPlanning = updatedPlannings.find(planning => planning.niveau === selectedCell.selectedLevel);
|
||||
const updatedPlanning = updatedPlannings.find(
|
||||
(planning) => planning.niveau === selectedCell.selectedLevel
|
||||
);
|
||||
|
||||
setFormData(prevFormData => ({
|
||||
setFormData((prevFormData) => ({
|
||||
...prevFormData,
|
||||
plannings: updatedPlannings
|
||||
plannings: updatedPlannings,
|
||||
}));
|
||||
|
||||
// Appeler handleUpdatePlanning avec les arguments appropriés
|
||||
const planningId = updatedPlanning ? updatedPlanning.planning.id : null;
|
||||
logger.debug("id : ", planningId)
|
||||
logger.debug('id : ', planningId);
|
||||
if (planningId) {
|
||||
handleUpdatePlanning(BE_SCHOOL_PLANNINGS_URL, planningId, updatedPlanning.emploiDuTemps);
|
||||
handleUpdatePlanning(
|
||||
BE_SCHOOL_PLANNINGS_URL,
|
||||
planningId,
|
||||
updatedPlanning.emploiDuTemps
|
||||
);
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const filteredTeachers = selectedSpeciality
|
||||
? formData.teachers.filter(teacher =>
|
||||
? formData.teachers.filter((teacher) =>
|
||||
teacher.specialites.includes(parseInt(selectedSpeciality, 10))
|
||||
)
|
||||
: formData.teachers;
|
||||
@ -132,9 +151,9 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
setSelectedSpeciality(specialityId);
|
||||
|
||||
// Mettre à jour eventData
|
||||
setEventData(prev => ({
|
||||
setEventData((prev) => ({
|
||||
...prev,
|
||||
specialiteId: specialityId
|
||||
specialiteId: specialityId,
|
||||
}));
|
||||
};
|
||||
|
||||
@ -143,9 +162,9 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
setSelectedTeacher(teacherId);
|
||||
|
||||
// Mettre à jour eventData
|
||||
setEventData(prev => ({
|
||||
setEventData((prev) => ({
|
||||
...prev,
|
||||
teacherId: teacherId
|
||||
teacherId: teacherId,
|
||||
}));
|
||||
};
|
||||
|
||||
@ -153,7 +172,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
<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'}
|
||||
{eventData.id ? "Modifier l'événement" : 'Nouvel événement'}
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
@ -165,10 +184,12 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
selected={selectedSpeciality}
|
||||
choices={[
|
||||
{ value: '', label: 'Sélectionner une spécialité' },
|
||||
...groupSpecialitiesBySubject(formData.teachers).map((speciality) => ({
|
||||
value: speciality.id,
|
||||
label: speciality.nom
|
||||
}))
|
||||
...groupSpecialitiesBySubject(formData.teachers).map(
|
||||
(speciality) => ({
|
||||
value: speciality.id,
|
||||
label: speciality.nom,
|
||||
})
|
||||
),
|
||||
]}
|
||||
callback={handleSpecialityChange}
|
||||
IconItem={BookOpen}
|
||||
@ -182,11 +203,11 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
placeHolder="Enseignants"
|
||||
selected={selectedTeacher}
|
||||
choices={[
|
||||
{ value: '', label: 'Sélectionner un enseignant'},
|
||||
...filteredTeachers.map(teacher => ({
|
||||
{ value: '', label: 'Sélectionner un enseignant' },
|
||||
...filteredTeachers.map((teacher) => ({
|
||||
value: teacher.id,
|
||||
label: `${teacher.nom} ${teacher.prenom}`
|
||||
}))
|
||||
label: `${teacher.nom} ${teacher.prenom}`,
|
||||
})),
|
||||
]}
|
||||
callback={handleTeacherChange}
|
||||
IconItem={Users}
|
||||
@ -202,10 +223,12 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
|
||||
<input
|
||||
type="text"
|
||||
value={eventData.duration}
|
||||
onChange={(e) => setEventData(prev => ({
|
||||
...prev,
|
||||
duration: e.target.value
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setEventData((prev) => ({
|
||||
...prev,
|
||||
duration: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
|
||||
import {
|
||||
Plus,
|
||||
Trash2,
|
||||
Edit3,
|
||||
Check,
|
||||
X,
|
||||
Percent,
|
||||
EuroIcon,
|
||||
Tag,
|
||||
} from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import Popup from '@/components/Popup';
|
||||
import CheckBox from '@/components/CheckBox';
|
||||
@ -7,27 +16,47 @@ import InputText from '@/components/InputText';
|
||||
import logger from '@/utils/logger';
|
||||
import { ESTABLISHMENT_ID } from '@/utils/Url';
|
||||
|
||||
const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedDiscounts, handleDiscountSelection }) => {
|
||||
const DiscountsSection = ({
|
||||
discounts,
|
||||
setDiscounts,
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
type,
|
||||
subscriptionMode = false,
|
||||
selectedDiscounts,
|
||||
handleDiscountSelection,
|
||||
}) => {
|
||||
const [editingDiscount, setEditingDiscount] = useState(null);
|
||||
const [newDiscount, setNewDiscount] = useState(null);
|
||||
const [formData, setFormData] = useState({});
|
||||
const [localErrors, setLocalErrors] = useState({});
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [removePopupVisible, setRemovePopupVisible] = useState(false);
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState("");
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||
|
||||
const handleAddDiscount = () => {
|
||||
setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discount_type: 0, type: type, establishment: ESTABLISHMENT_ID });
|
||||
setNewDiscount({
|
||||
id: Date.now(),
|
||||
name: '',
|
||||
amount: '',
|
||||
description: '',
|
||||
discount_type: 0,
|
||||
type: type,
|
||||
establishment: ESTABLISHMENT_ID,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveDiscount = (id) => {
|
||||
return handleDelete(id)
|
||||
.then(() => {
|
||||
setDiscounts(prevDiscounts => prevDiscounts.filter(discount => discount.id !== id));
|
||||
setDiscounts((prevDiscounts) =>
|
||||
prevDiscounts.filter((discount) => discount.id !== id)
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
};
|
||||
@ -40,7 +69,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
setNewDiscount(null);
|
||||
setLocalErrors({});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
if (error && typeof error === 'object') {
|
||||
setLocalErrors(error);
|
||||
} else {
|
||||
@ -48,7 +77,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis");
|
||||
setPopupMessage('Tous les champs doivent être remplis');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
@ -60,7 +89,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
setEditingDiscount(null);
|
||||
setLocalErrors({});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
if (error && typeof error === 'object') {
|
||||
setLocalErrors(error);
|
||||
} else {
|
||||
@ -68,25 +97,31 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis");
|
||||
setPopupMessage('Tous les champs doivent être remplis');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleDiscountType = (id) => {
|
||||
const discount = discounts.find(discount => discount.id === id);
|
||||
const discount = discounts.find((discount) => discount.id === id);
|
||||
if (!discount) return;
|
||||
|
||||
const updatedData = {
|
||||
...discount,
|
||||
discount_type: discount.discount_type === 0 ? 1 : 0
|
||||
discount_type: discount.discount_type === 0 ? 1 : 0,
|
||||
};
|
||||
|
||||
handleEdit(id, updatedData)
|
||||
.then(() => {
|
||||
setDiscounts(prevDiscounts => prevDiscounts.map(discount => discount.id === id ? { ...discount, discount_type: updatedData.discount_type } : discount));
|
||||
setDiscounts((prevDiscounts) =>
|
||||
prevDiscounts.map((discount) =>
|
||||
discount.id === id
|
||||
? { ...discount, discount_type: updatedData.discount_type }
|
||||
: discount
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
};
|
||||
@ -129,7 +164,13 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
|
||||
errorMsg={
|
||||
localErrors &&
|
||||
localErrors[field] &&
|
||||
Array.isArray(localErrors[field])
|
||||
? localErrors[field][0]
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -143,36 +184,61 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
if (isEditing || isCreating) {
|
||||
switch (column) {
|
||||
case 'LIBELLE':
|
||||
return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction');
|
||||
return renderInputField(
|
||||
'name',
|
||||
currentData.name,
|
||||
handleChange,
|
||||
'Libellé de la réduction'
|
||||
);
|
||||
case 'REMISE':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
{renderInputField('amount', currentData.amount, handleChange,'Montant')}
|
||||
{renderInputField(
|
||||
'amount',
|
||||
currentData.amount,
|
||||
handleChange,
|
||||
'Montant'
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggleDiscountTypeEdition(discount.id)}
|
||||
className="flex justify-center items-center text-emerald-500 hover:text-emerald-700 ml-2"
|
||||
>
|
||||
{currentData.discount_type === 0 ? <EuroIcon className="w-5 h-5" /> : <Percent className="w-5 h-5" />}
|
||||
{currentData.discount_type === 0 ? (
|
||||
<EuroIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<Percent className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
case 'DESCRIPTION':
|
||||
return renderInputField('description', currentData.description, handleChange, 'Description');
|
||||
return renderInputField(
|
||||
'description',
|
||||
currentData.description,
|
||||
handleChange,
|
||||
'Description'
|
||||
);
|
||||
case 'ACTIONS':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? handleUpdateDiscount(editingDiscount, formData) : handleSaveNewDiscount())}
|
||||
onClick={() =>
|
||||
isEditing
|
||||
? handleUpdateDiscount(editingDiscount, formData)
|
||||
: handleSaveNewDiscount()
|
||||
}
|
||||
className="text-green-500 hover:text-green-700"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? setEditingDiscount(null) : setNewDiscount(null))}
|
||||
onClick={() =>
|
||||
isEditing ? setEditingDiscount(null) : setNewDiscount(null)
|
||||
}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
@ -187,7 +253,9 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
case 'LIBELLE':
|
||||
return discount.name;
|
||||
case 'REMISE':
|
||||
return discount.discount_type === 0 ? `${discount.amount} €` : `${discount.amount} %`;
|
||||
return discount.discount_type === 0
|
||||
? `${discount.amount} €`
|
||||
: `${discount.amount} %`;
|
||||
case 'DESCRIPTION':
|
||||
return discount.description;
|
||||
case 'MISE A JOUR':
|
||||
@ -200,11 +268,17 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
onClick={() => handleToggleDiscountType(discount.id)}
|
||||
className="flex justify-center items-center text-emerald-500 hover:text-emerald-700"
|
||||
>
|
||||
{discount.discount_type === 0 ? <EuroIcon className="w-5 h-5" /> : <Percent className="w-5 h-5" />}
|
||||
{discount.discount_type === 0 ? (
|
||||
<EuroIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<Percent className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingDiscount(discount.id) || setFormData(discount)}
|
||||
onClick={() =>
|
||||
setEditingDiscount(discount.id) || setFormData(discount)
|
||||
}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
@ -213,18 +287,22 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRemovePopupVisible(true);
|
||||
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer un tarif personnalisé.\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
|
||||
setRemovePopupMessage(
|
||||
"Attentions ! \nVous êtes sur le point de supprimer un tarif personnalisé.\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
|
||||
);
|
||||
setRemovePopupOnConfirm(() => () => {
|
||||
handleRemoveDiscount(discount.id)
|
||||
.then(data => {
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setPopupMessage("Réduction correctement supprimé");
|
||||
setPopupMessage('Réduction correctement supprimé');
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
setPopupMessage("Erreur lors de la suppression de la réduction");
|
||||
setPopupMessage(
|
||||
'Erreur lors de la suppression de la réduction'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
});
|
||||
@ -236,17 +314,17 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
case '':
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<CheckBox
|
||||
item={discount}
|
||||
formData={{ selectedDiscounts }}
|
||||
handleChange={() => handleDiscountSelection(discount.id)}
|
||||
fieldName="selectedDiscounts"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case '':
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<CheckBox
|
||||
item={discount}
|
||||
formData={{ selectedDiscounts }}
|
||||
handleChange={() => handleDiscountSelection(discount.id)}
|
||||
fieldName="selectedDiscounts"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -254,19 +332,19 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
};
|
||||
|
||||
const columns = subscriptionMode
|
||||
? [
|
||||
{ name: 'LIBELLE', label: 'Libellé' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
{ name: 'REMISE', label: 'Remise' },
|
||||
{ name: '', label: 'Sélection' }
|
||||
]
|
||||
: [
|
||||
{ name: 'LIBELLE', label: 'Libellé' },
|
||||
{ name: 'REMISE', label: 'Remise' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||
{ name: 'ACTIONS', label: 'Actions' }
|
||||
];
|
||||
? [
|
||||
{ name: 'LIBELLE', label: 'Libellé' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
{ name: 'REMISE', label: 'Remise' },
|
||||
{ name: '', label: 'Sélection' },
|
||||
]
|
||||
: [
|
||||
{ name: 'LIBELLE', label: 'Libellé' },
|
||||
{ name: 'REMISE', label: 'Remise' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||
{ name: 'ACTIONS', label: 'Actions' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@ -276,7 +354,11 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
<Tag className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Liste des réductions</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddDiscount}
|
||||
className="text-emerald-500 hover:text-emerald-700"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
@ -285,7 +367,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
data={newDiscount ? [newDiscount, ...discounts] : discounts}
|
||||
columns={columns}
|
||||
renderCell={renderDiscountCell}
|
||||
defaultTheme='bg-yellow-100'
|
||||
defaultTheme="bg-yellow-100"
|
||||
/>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
@ -304,4 +386,4 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscountsSection;
|
||||
export default DiscountsSection;
|
||||
|
||||
@ -3,41 +3,47 @@ import FeesSection from '@/components/Structure/Tarification/FeesSection';
|
||||
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
|
||||
import PaymentPlanSelector from '@/components/PaymentPlanSelector';
|
||||
import PaymentModeSelector from '@/components/PaymentModeSelector';
|
||||
import { BE_SCHOOL_FEES_URL, BE_SCHOOL_DISCOUNTS_URL, BE_SCHOOL_PAYMENT_PLANS_URL, BE_SCHOOL_PAYMENT_MODES_URL } from '@/utils/Url';
|
||||
|
||||
const FeesManagement = ({ registrationDiscounts,
|
||||
setRegistrationDiscounts,
|
||||
tuitionDiscounts,
|
||||
setTuitionDiscounts,
|
||||
registrationFees,
|
||||
setRegistrationFees,
|
||||
tuitionFees,
|
||||
setTuitionFees,
|
||||
registrationPaymentPlans,
|
||||
setRegistrationPaymentPlans,
|
||||
tuitionPaymentPlans,
|
||||
setTuitionPaymentPlans,
|
||||
registrationPaymentModes,
|
||||
setRegistrationPaymentModes,
|
||||
tuitionPaymentModes,
|
||||
setTuitionPaymentModes,
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete }) => {
|
||||
import {
|
||||
BE_SCHOOL_FEES_URL,
|
||||
BE_SCHOOL_DISCOUNTS_URL,
|
||||
BE_SCHOOL_PAYMENT_PLANS_URL,
|
||||
BE_SCHOOL_PAYMENT_MODES_URL,
|
||||
} from '@/utils/Url';
|
||||
|
||||
const FeesManagement = ({
|
||||
registrationDiscounts,
|
||||
setRegistrationDiscounts,
|
||||
tuitionDiscounts,
|
||||
setTuitionDiscounts,
|
||||
registrationFees,
|
||||
setRegistrationFees,
|
||||
tuitionFees,
|
||||
setTuitionFees,
|
||||
registrationPaymentPlans,
|
||||
setRegistrationPaymentPlans,
|
||||
tuitionPaymentPlans,
|
||||
setTuitionPaymentPlans,
|
||||
registrationPaymentModes,
|
||||
setRegistrationPaymentModes,
|
||||
tuitionPaymentModes,
|
||||
setTuitionPaymentModes,
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}) => {
|
||||
const handleDiscountDelete = (id, type) => {
|
||||
if (type === 0) {
|
||||
setRegistrationFees(prevFees =>
|
||||
prevFees.map(fee => ({
|
||||
setRegistrationFees((prevFees) =>
|
||||
prevFees.map((fee) => ({
|
||||
...fee,
|
||||
discounts: fee.discounts.filter(discountId => discountId !== id)
|
||||
discounts: fee.discounts.filter((discountId) => discountId !== id),
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
setTuitionFees(prevFees =>
|
||||
prevFees.map(fee => ({
|
||||
setTuitionFees((prevFees) =>
|
||||
prevFees.map((fee) => ({
|
||||
...fee,
|
||||
discounts: fee.discounts.filter(discountId => discountId !== id)
|
||||
discounts: fee.discounts.filter((discountId) => discountId !== id),
|
||||
}))
|
||||
);
|
||||
}
|
||||
@ -46,16 +52,33 @@ const FeesManagement = ({ registrationDiscounts,
|
||||
return (
|
||||
<div className="w-full mx-auto p-2 mt-6 space-y-6">
|
||||
<div className="bg-white p-2 rounded-lg shadow-md">
|
||||
<h2 className="text-2xl font-semibold mb-4">Frais d'inscription</h2>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Frais d'inscription
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<FeesSection
|
||||
fees={registrationFees}
|
||||
setFees={setRegistrationFees}
|
||||
discounts={registrationDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setRegistrationFees)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEES_URL}`, id, updatedData, setRegistrationFees)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setRegistrationFees)}
|
||||
handleCreate={(newData) =>
|
||||
handleCreate(
|
||||
`${BE_SCHOOL_FEES_URL}`,
|
||||
newData,
|
||||
setRegistrationFees
|
||||
)
|
||||
}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_FEES_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setRegistrationFees
|
||||
)
|
||||
}
|
||||
handleDelete={(id) =>
|
||||
handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setRegistrationFees)
|
||||
}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
@ -63,9 +86,28 @@ const FeesManagement = ({ registrationDiscounts,
|
||||
<DiscountsSection
|
||||
discounts={registrationDiscounts}
|
||||
setDiscounts={setRegistrationDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNTS_URL}`, newData, setRegistrationDiscounts)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNTS_URL}`, id, updatedData, setRegistrationDiscounts)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNTS_URL}`, id, setRegistrationDiscounts)}
|
||||
handleCreate={(newData) =>
|
||||
handleCreate(
|
||||
`${BE_SCHOOL_DISCOUNTS_URL}`,
|
||||
newData,
|
||||
setRegistrationDiscounts
|
||||
)
|
||||
}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_DISCOUNTS_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setRegistrationDiscounts
|
||||
)
|
||||
}
|
||||
handleDelete={(id) =>
|
||||
handleDelete(
|
||||
`${BE_SCHOOL_DISCOUNTS_URL}`,
|
||||
id,
|
||||
setRegistrationDiscounts
|
||||
)
|
||||
}
|
||||
onDiscountDelete={(id) => handleDiscountDelete(id, 0)}
|
||||
type={0}
|
||||
/>
|
||||
@ -74,7 +116,14 @@ const FeesManagement = ({ registrationDiscounts,
|
||||
<PaymentPlanSelector
|
||||
paymentPlans={registrationPaymentPlans}
|
||||
setPaymentPlans={setRegistrationPaymentPlans}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_PLANS_URL}`, id, updatedData, setRegistrationPaymentPlans)}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_PAYMENT_PLANS_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setRegistrationPaymentPlans
|
||||
)
|
||||
}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
@ -82,23 +131,41 @@ const FeesManagement = ({ registrationDiscounts,
|
||||
<PaymentModeSelector
|
||||
paymentModes={registrationPaymentModes}
|
||||
setPaymentModes={setRegistrationPaymentModes}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_MODES_URL}`, id, updatedData, setRegistrationPaymentModes)}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_PAYMENT_MODES_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setRegistrationPaymentModes
|
||||
)
|
||||
}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-2 rounded-lg shadow-md">
|
||||
<h2 className="text-2xl font-semibold mb-4">Frais de scolarité</h2>
|
||||
<h2 className="text-2xl font-semibold mb-4">Frais de scolarité</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<FeesSection
|
||||
fees={tuitionFees}
|
||||
setFees={setTuitionFees}
|
||||
discounts={tuitionDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setTuitionFees)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEES_URL}`, id, updatedData, setTuitionFees)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setTuitionFees)}
|
||||
handleCreate={(newData) =>
|
||||
handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setTuitionFees)
|
||||
}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_FEES_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setTuitionFees
|
||||
)
|
||||
}
|
||||
handleDelete={(id) =>
|
||||
handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setTuitionFees)
|
||||
}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
@ -106,9 +173,28 @@ const FeesManagement = ({ registrationDiscounts,
|
||||
<DiscountsSection
|
||||
discounts={tuitionDiscounts}
|
||||
setDiscounts={setTuitionDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNTS_URL}`, newData, setTuitionDiscounts)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNTS_URL}`, id, updatedData, setTuitionDiscounts)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNTS_URL}`, id, setTuitionDiscounts)}
|
||||
handleCreate={(newData) =>
|
||||
handleCreate(
|
||||
`${BE_SCHOOL_DISCOUNTS_URL}`,
|
||||
newData,
|
||||
setTuitionDiscounts
|
||||
)
|
||||
}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_DISCOUNTS_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setTuitionDiscounts
|
||||
)
|
||||
}
|
||||
handleDelete={(id) =>
|
||||
handleDelete(
|
||||
`${BE_SCHOOL_DISCOUNTS_URL}`,
|
||||
id,
|
||||
setTuitionDiscounts
|
||||
)
|
||||
}
|
||||
onDiscountDelete={(id) => handleDiscountDelete(id, 1)}
|
||||
type={1}
|
||||
/>
|
||||
@ -117,7 +203,14 @@ const FeesManagement = ({ registrationDiscounts,
|
||||
<PaymentPlanSelector
|
||||
paymentPlans={tuitionPaymentPlans}
|
||||
setPaymentPlans={setTuitionPaymentPlans}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_PLANS_URL}`, id, updatedData, setRegistrationPaymentPlans)}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_PAYMENT_PLANS_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setRegistrationPaymentPlans
|
||||
)
|
||||
}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
@ -125,7 +218,14 @@ const FeesManagement = ({ registrationDiscounts,
|
||||
<PaymentModeSelector
|
||||
paymentModes={tuitionPaymentModes}
|
||||
setPaymentModes={setTuitionPaymentModes}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_MODES_URL}`, id, updatedData, setTuitionPaymentModes)}
|
||||
handleEdit={(id, updatedData) =>
|
||||
handleEdit(
|
||||
`${BE_SCHOOL_PAYMENT_MODES_URL}`,
|
||||
id,
|
||||
updatedData,
|
||||
setTuitionPaymentModes
|
||||
)
|
||||
}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
@ -135,4 +235,4 @@ const FeesManagement = ({ registrationDiscounts,
|
||||
);
|
||||
};
|
||||
|
||||
export default FeesManagement;
|
||||
export default FeesManagement;
|
||||
|
||||
@ -1,5 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard, BookOpen } from 'lucide-react';
|
||||
import {
|
||||
Plus,
|
||||
Trash2,
|
||||
Edit3,
|
||||
Check,
|
||||
X,
|
||||
EyeOff,
|
||||
Eye,
|
||||
CreditCard,
|
||||
BookOpen,
|
||||
} from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import Popup from '@/components/Popup';
|
||||
import CheckBox from '@/components/CheckBox';
|
||||
@ -8,17 +18,29 @@ import logger from '@/utils/logger';
|
||||
|
||||
import { ESTABLISHMENT_ID } from '@/utils/Url';
|
||||
|
||||
const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedFees, handleFeeSelection }) => {
|
||||
const FeesSection = ({
|
||||
fees,
|
||||
setFees,
|
||||
discounts,
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
type,
|
||||
subscriptionMode = false,
|
||||
selectedFees,
|
||||
handleFeeSelection,
|
||||
}) => {
|
||||
const [editingFee, setEditingFee] = useState(null);
|
||||
const [newFee, setNewFee] = useState(null);
|
||||
const [formData, setFormData] = useState({});
|
||||
const [localErrors, setLocalErrors] = useState({});
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [removePopupVisible, setRemovePopupVisible] = useState(false);
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState("");
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||
const labelTypeFrais = (type === 0 ? 'Frais d\'inscription' : 'Frais de scolarité');
|
||||
const labelTypeFrais =
|
||||
type === 0 ? "Frais d'inscription" : 'Frais de scolarité';
|
||||
|
||||
// Récupération des messages d'erreur
|
||||
const getError = (field) => {
|
||||
@ -26,23 +48,31 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
};
|
||||
|
||||
const handleAddFee = () => {
|
||||
setNewFee({ id: Date.now(), name: '', base_amount: '', description: '', validity_start_date: '', validity_end_date: '', discounts: [], type: type, establishment: ESTABLISHMENT_ID });
|
||||
setNewFee({
|
||||
id: Date.now(),
|
||||
name: '',
|
||||
base_amount: '',
|
||||
description: '',
|
||||
validity_start_date: '',
|
||||
validity_end_date: '',
|
||||
discounts: [],
|
||||
type: type,
|
||||
establishment: ESTABLISHMENT_ID,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFee = (id) => {
|
||||
return handleDelete(id)
|
||||
.then(() => {
|
||||
setFees(prevFees => prevFees.filter(fee => fee.id !== id));
|
||||
setFees((prevFees) => prevFees.filter((fee) => fee.id !== id));
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveNewFee = () => {
|
||||
if (
|
||||
newFee.name &&
|
||||
newFee.base_amount) {
|
||||
if (newFee.name && newFee.base_amount) {
|
||||
handleCreate(newFee)
|
||||
.then((createdFee) => {
|
||||
setFees([createdFee, ...fees]);
|
||||
@ -52,53 +82,55 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
.catch((error) => {
|
||||
logger.error('Error:', error.message);
|
||||
if (error.details) {
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateFee = (id, updatedFee) => {
|
||||
if (
|
||||
updatedFee.name &&
|
||||
updatedFee.base_amount) {
|
||||
if (updatedFee.name && updatedFee.base_amount) {
|
||||
handleEdit(id, updatedFee)
|
||||
.then((updatedFee) => {
|
||||
setFees(fees.map(fee => fee.id === id ? updatedFee : fee));
|
||||
setFees(fees.map((fee) => (fee.id === id ? updatedFee : fee)));
|
||||
setEditingFee(null);
|
||||
setLocalErrors({});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error:', error.message);
|
||||
if (error.details) {
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
logger.error('Form errors:', error.details);
|
||||
setLocalErrors(error.details);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupMessage('Tous les champs doivent être remplis et valides');
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleActive = (id, isActive) => {
|
||||
const fee = fees.find(fee => fee.id === id);
|
||||
const fee = fees.find((fee) => fee.id === id);
|
||||
if (!fee) return;
|
||||
|
||||
const updatedData = {
|
||||
is_active: !isActive,
|
||||
discounts: fee.discounts
|
||||
discounts: fee.discounts,
|
||||
};
|
||||
|
||||
handleEdit(id, updatedData)
|
||||
.then(() => {
|
||||
setFees(prevFees => prevFees.map(fee => fee.id === id ? { ...fee, is_active: !isActive } : fee));
|
||||
setFees((prevFees) =>
|
||||
prevFees.map((fee) =>
|
||||
fee.id === id ? { ...fee, is_active: !isActive } : fee
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
};
|
||||
@ -107,7 +139,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
const { name, value } = e.target;
|
||||
let parsedValue = value;
|
||||
if (name === 'discounts') {
|
||||
parsedValue = value.split(',').map(v => parseInt(v, 10));
|
||||
parsedValue = value.split(',').map((v) => parseInt(v, 10));
|
||||
}
|
||||
if (editingFee) {
|
||||
setFormData((prevData) => ({
|
||||
@ -145,24 +177,45 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
if (isEditing || isCreating) {
|
||||
switch (column) {
|
||||
case 'NOM':
|
||||
return renderInputField('name', currentData.name, handleChange, 'Nom des frais');
|
||||
return renderInputField(
|
||||
'name',
|
||||
currentData.name,
|
||||
handleChange,
|
||||
'Nom des frais'
|
||||
);
|
||||
case 'MONTANT':
|
||||
return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base');
|
||||
return renderInputField(
|
||||
'base_amount',
|
||||
currentData.base_amount,
|
||||
handleChange,
|
||||
'Montant de base'
|
||||
);
|
||||
case 'DESCRIPTION':
|
||||
return renderInputField('description', currentData.description, handleChange, 'Description');
|
||||
return renderInputField(
|
||||
'description',
|
||||
currentData.description,
|
||||
handleChange,
|
||||
'Description'
|
||||
);
|
||||
case 'ACTIONS':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? handleUpdateFee(editingFee, formData) : handleSaveNewFee())}
|
||||
onClick={() =>
|
||||
isEditing
|
||||
? handleUpdateFee(editingFee, formData)
|
||||
: handleSaveNewFee()
|
||||
}
|
||||
className="text-green-500 hover:text-green-700"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? setEditingFee(null) : setNewFee(null))}
|
||||
onClick={() =>
|
||||
isEditing ? setEditingFee(null) : setNewFee(null)
|
||||
}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
@ -190,7 +243,11 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
onClick={() => handleToggleActive(fee.id, fee.is_active)}
|
||||
className={`text-${fee.is_active ? 'green' : 'orange'}-500 hover:text-${fee.is_active ? 'green' : 'orange'}-700`}
|
||||
>
|
||||
{fee.is_active ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5" />}
|
||||
{fee.is_active ? (
|
||||
<Eye className="w-5 h-5" />
|
||||
) : (
|
||||
<EyeOff className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -203,18 +260,26 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRemovePopupVisible(true);
|
||||
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer un " + labelTypeFrais + ".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
|
||||
setRemovePopupMessage(
|
||||
'Attentions ! \nVous êtes sur le point de supprimer un ' +
|
||||
labelTypeFrais +
|
||||
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
|
||||
);
|
||||
setRemovePopupOnConfirm(() => () => {
|
||||
handleRemoveFee(fee.id)
|
||||
.then(data => {
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setPopupMessage(labelTypeFrais + " correctement supprimé");
|
||||
setPopupMessage(
|
||||
labelTypeFrais + ' correctement supprimé'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
setPopupMessage("Erreur lors de la suppression du " + labelTypeFrais);
|
||||
setPopupMessage(
|
||||
'Erreur lors de la suppression du ' + labelTypeFrais
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRemovePopupVisible(false);
|
||||
});
|
||||
@ -248,29 +313,33 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
{ name: 'NOM', label: 'Nom' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
{ name: 'MONTANT', label: 'Montant de base' },
|
||||
{ name: '', label: 'Sélection' }
|
||||
{ name: '', label: 'Sélection' },
|
||||
]
|
||||
: [
|
||||
{ name: 'NOM', label: 'Nom' },
|
||||
{ name: 'MONTANT', label: 'Montant de base' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||
{ name: 'ACTIONS', label: 'Actions' }
|
||||
{ name: 'ACTIONS', label: 'Actions' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!subscriptionMode && (
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center mb-4">
|
||||
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Liste des frais</h2>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center mb-4">
|
||||
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Liste des frais</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddFee}
|
||||
className="text-emerald-500 hover:text-emerald-700"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddFee} className="text-emerald-500 hover:text-emerald-700">
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<Table
|
||||
data={newFee ? [newFee, ...fees] : fees}
|
||||
columns={columns}
|
||||
@ -293,4 +362,4 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
);
|
||||
};
|
||||
|
||||
export default FeesSection;
|
||||
export default FeesSection;
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
const Tab = ({ text, active, count, onClick}) => (
|
||||
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'
|
||||
}`}>
|
||||
active
|
||||
? 'text-emerald-600 border-b-2 border-emerald-600'
|
||||
: 'text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
export default Tab;
|
||||
export default Tab;
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const TabContent = ({ children, isActive }) => (
|
||||
<div className={`${isActive ? 'block' : 'hidden'}`}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={`${isActive ? 'block' : 'hidden'}`}>{children}</div>
|
||||
);
|
||||
|
||||
export default TabContent;
|
||||
export default TabContent;
|
||||
|
||||
@ -2,7 +2,19 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Pagination from '@/components/Pagination'; // Correction du chemin d'importatio,
|
||||
|
||||
const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange, onRowClick, selectedRows, isSelectable = false, defaultTheme='bg-emerald-50' }) => {
|
||||
const Table = ({
|
||||
data,
|
||||
columns,
|
||||
renderCell,
|
||||
itemsPerPage = 0,
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
onRowClick,
|
||||
selectedRows,
|
||||
isSelectable = false,
|
||||
defaultTheme = 'bg-emerald-50',
|
||||
}) => {
|
||||
const handlePageChange = (newPage) => {
|
||||
onPageChange(newPage);
|
||||
};
|
||||
@ -13,7 +25,10 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
|
||||
<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">
|
||||
<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>
|
||||
))}
|
||||
@ -21,8 +36,8 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={rowIndex}
|
||||
<tr
|
||||
key={rowIndex}
|
||||
className={`
|
||||
${isSelectable ? 'cursor-pointer' : ''}
|
||||
${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''}
|
||||
@ -31,8 +46,13 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
|
||||
onClick={() => isSelectable && onRowClick && onRowClick(row)}
|
||||
>
|
||||
{columns.map((column, colIndex) => (
|
||||
<td key={colIndex} className={`py-2 px-4 border-b border-gray-200 text-center text-sm ${selectedRows?.includes(row.id) ? 'text-white' : 'text-gray-700'}`} >
|
||||
{renderCell ? renderCell(row, column.name) : column.transform(row)}
|
||||
<td
|
||||
key={colIndex}
|
||||
className={`py-2 px-4 border-b border-gray-200 text-center text-sm ${selectedRows?.includes(row.id) ? 'text-white' : 'text-gray-700'}`}
|
||||
>
|
||||
{renderCell
|
||||
? renderCell(row, column.name)
|
||||
: column.transform(row)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@ -52,10 +72,12 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
|
||||
|
||||
Table.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
transform: PropTypes.func.isRequired,
|
||||
})).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,
|
||||
|
||||
@ -6,7 +6,7 @@ const ToggleView = ({ viewType, setViewType }) => {
|
||||
{ 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 }
|
||||
{ type: 'planning', label: 'Planning', icon: List },
|
||||
];
|
||||
|
||||
return (
|
||||
@ -17,9 +17,10 @@ const ToggleView = ({ viewType, setViewType }) => {
|
||||
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'
|
||||
${
|
||||
viewType === type
|
||||
? 'bg-emerald-600 text-white shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@ -31,4 +32,4 @@ const ToggleView = ({ viewType, setViewType }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleView;
|
||||
export default ToggleView;
|
||||
|
||||
@ -27,4 +27,4 @@ Tooltip.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
export default Tooltip;
|
||||
|
||||
Reference in New Issue
Block a user