feat: Ajout de la configuration des tarifs de l'école [#18]

This commit is contained in:
N3WT DE COMPET
2025-01-19 21:00:58 +01:00
committed by Luc SORIGNET
parent 147a70135d
commit 5a0e65bb75
45 changed files with 2089 additions and 376 deletions

View File

@ -6,8 +6,8 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) =
<>
<div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div className={`flex items-center border-2 border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500 h-8 w-1/3`}>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full">
<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">
<Palette className="w-5 h-5" />
</span>
<input
@ -16,7 +16,7 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) =
name={name}
value={value}
onChange={onChange}
className="flex-1 block rounded-r-md sm:text-sm border-none focus:ring-0 outline-none h-full p-0 w-8 cursor-pointer"
className="flex-1 h-8 w-full sm:text-sm border-none focus:ring-0 outline-none rounded-r-md cursor-pointer"
/>
</div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}

View File

@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react';
import { isValidPhoneNumber } from 'react-phone-number-input';
export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className }) {
const inputRef = useRef(null);
@ -19,12 +19,12 @@ export default function InputPhone({ name, label, value, onChange, errorMsg, pla
<>
<div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div className={`flex items-center border-2 border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500 h-8`}>
<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="tel"
name={name}
ref={inputRef}
className="flex-1 pl-2 block w-full sm:text-sm focus:ring-0 h-full rounded-md border-none outline-none"
className="flex-1 px-3 py-2 block w-full sm:text-sm focus:ring-0 rounded-md border-none outline-none"
value={typeof value === 'string' ? value : ''}
onChange={handleChange}
placeholder={placeholder}

View File

@ -1,8 +1,11 @@
export default function InputText({name, type, label, value, onChange, errorMsg, placeholder,className}) {
export default function InputText({name, type, label, value, onChange, errorMsg, placeholder, className, required}) {
return (
<>
<div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<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}
@ -12,6 +15,7 @@ export default function InputText({name, type, label, value, onChange, errorMsg,
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>}

View File

@ -1,11 +1,10 @@
export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) {
return (
<>
<div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div className={`flex items-center border-2 border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500 h-8`}>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full">
<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
@ -15,7 +14,7 @@ export default function InputTextIcon({name, type, IconItem, label, value, onCha
name={name}
value={value}
onChange={onChange}
className="flex-1 pl-2 block w-full rounded-r-md sm:text-sm border-none focus:ring-0 outline-none h-full"
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>}

View File

@ -5,8 +5,12 @@ import ResponsableInputFields from '@/components/Inscription/ResponsableInputFie
import Loader from '@/components/Loader';
import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import FileUpload from '@/app/[locale]/admin/subscriptions/components/FileUpload';
import Table from '@/components/Table';
import { fetchRegisterFormFileTemplate, createRegistrationFormFile } from '@/app/lib/subscriptionAction';
import { Download, Upload } from 'lucide-react';
import { BASE_URL } from '@/utils/Url';
import DraggableFileUpload from '@/app/[locale]/admin/subscriptions/components/DraggableFileUpload';
import Modal from '@/components/Modal';
const levels = [
{ value:'1', label: 'TPS - Très Petite Section'},
@ -20,7 +24,8 @@ export default function InscriptionFormShared({
csrfToken,
onSubmit,
cancelUrl,
isLoading = false
isLoading = false,
errors = {} // Nouvelle prop pour les erreurs
}) {
const [formData, setFormData] = useState(() => ({
@ -41,6 +46,11 @@ export default function InscriptionFormShared({
);
const [uploadedFiles, setUploadedFiles] = useState([]);
const [fileTemplates, setFileTemplates] = useState([]);
const [fileName, setFileName] = useState("");
const [file, setFile] = useState("");
const [showUploadModal, setShowUploadModal] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState(null);
// Mettre à jour les données quand initialData change
useEffect(() => {
@ -58,6 +68,9 @@ export default function InscriptionFormShared({
level: initialData.level || ''
});
setGuardians(initialData.guardians || []);
fetchRegisterFormFileTemplate().then((data) => {
setFileTemplates(data);
});
}
}, [initialData]);
@ -65,8 +78,22 @@ export default function InscriptionFormShared({
setFormData(prev => ({...prev, [field]: value}));
};
const handleFileUpload = (file, fileName) => {
setUploadedFiles([...uploadedFiles, { file, fileName }]);
const handleFileUpload = async (file, fileName) => {
const data = new FormData();
data.append('file', file);
data.append('name',fileName);
data.append('template', currentTemplateId);
data.append('register_form', formData.id);
try {
await createRegistrationFormFile(data, csrfToken);
// Optionnellement, rafraîchir la liste des fichiers
fetchRegisterFormFileTemplate().then((data) => {
setFileTemplates(data);
});
} catch (error) {
console.error('Error uploading file:', error);
}
};
const handleSubmit = (e) => {
@ -80,12 +107,31 @@ export default function InscriptionFormShared({
onSubmit(data);
};
const getError = (field) => {
return errors?.student?.[field]?.[0];
};
const getGuardianError = (index, field) => {
return errors?.student?.guardians?.[index]?.[field]?.[0];
};
const columns = [
{ name: 'Nom du fichier', transform: (row) => row.last_name },
{ 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: 'Actions', transform: (row) => (
<a href={URL.createObjectURL(row.fichier)} target='_blank' className="text-blue-500 hover:text-blue-700">
Télécharger
</a>
<div className="flex items-center justify-center gap-2">
{row.is_required &&
<button className="text-emerald-500 hover:text-emerald-700" type="button" onClick={() => {
setCurrentTemplateId(row.id);
setShowUploadModal(true);
}}>
<Upload size={16} />
</button>
}
</div>
) },
];
@ -105,12 +151,14 @@ export default function InscriptionFormShared({
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
@ -126,18 +174,22 @@ export default function InscriptionFormShared({
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)}
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
@ -145,6 +197,7 @@ export default function InscriptionFormShared({
label="Adresse"
value={formData.address}
onChange={(e) => updateFormField('address', e.target.value)}
errorMsg={getError('address')}
/>
</div>
<InputText
@ -152,14 +205,17 @@ export default function InscriptionFormShared({
label="Médecin Traitant"
value={formData.attending_physician}
onChange={(e) => updateFormField('attending_physician', e.target.value)}
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>
@ -184,6 +240,7 @@ export default function InscriptionFormShared({
newArray.splice(index, 1);
setGuardians(newArray);
}}
errors={errors?.student?.guardians || []}
/>
</div>
@ -191,14 +248,13 @@ export default function InscriptionFormShared({
<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={uploadedFiles}
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
<FileUpload onFileUpload={handleFileUpload} />
</div>
{/* Boutons de contrôle */}
@ -207,6 +263,44 @@ export default function InscriptionFormShared({
<Button type="submit" text="Valider" primary />
</div>
</form>
<Modal
isOpen={showUploadModal}
setIsOpen={setShowUploadModal}
title="Téléverser un fichier"
ContentComponent={() => (
<>
<DraggableFileUpload
className="w-full"
fileName={fileName}
onFileSelect={(selectedFile) => {
setFile(selectedFile);
setFileName(selectedFile.name);
}}
>
<input type="hidden" name="template" value={currentTemplateId} />
<input type="hidden" name="register_form" value={formData.id} />
</DraggableFileUpload>
<div className="mt-4 flex justify-center space-x-4">
<Button
text="Annuler"
onClick={() => {
setShowUploadModal(false);
setCurrentTemplateId(null);
}}
/>
<Button
text="Valider"
onClick={() => {
setShowUploadModal(false);
handleFileUpload(file, fileName);
setCurrentTemplateId(null);
}}
primary={true}
/>
</div>
</>
)}
/>
</div>
);
}

View File

@ -5,8 +5,12 @@ import React from 'react';
import { useTranslations } from 'next-intl';
import 'react-phone-number-input/style.css'
export default function ResponsableInputFields({guardians, onGuardiansChange, addGuardian, deleteGuardian}) {
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];
};
return (
<div className="space-y-8">
@ -33,6 +37,8 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
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"
@ -40,6 +46,8 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('firstname')}
value={item.first_name}
onChange={(event) => {onGuardiansChange(item.id, "first_name", event.target.value)}}
errorMsg={getError(index, 'first_name')}
required
/>
</div>
@ -50,12 +58,14 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('email')}
value={item.email}
onChange={(event) => {onGuardiansChange(item.id, "email", event.target.value)}}
errorMsg={getError(index, 'email')}
/>
<InputPhone
name="telephoneResponsable"
label={t('phone')}
value={item.phone}
onChange={(event) => {onGuardiansChange(item.id, "phone", event)}}
errorMsg={getError(index, 'phone')}
/>
</div>
@ -66,6 +76,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('birthdate')}
value={item.birth_date}
onChange={(event) => {onGuardiansChange(item.id, "birth_date", event.target.value)}}
errorMsg={getError(index, 'birth_date')}
/>
<InputText
name="professionResponsable"
@ -73,6 +84,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('profession')}
value={item.profession}
onChange={(event) => {onGuardiansChange(item.id, "profession", event.target.value)}}
errorMsg={getError(index, 'profession')}
/>
</div>
@ -83,6 +95,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('address')}
value={item.address}
onChange={(event) => {onGuardiansChange(item.id, "address", event.target.value)}}
errorMsg={getError(index, 'address')}
/>
</div>
</div>

View File

@ -1,16 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom';
const Popup = ({ visible, message, onConfirm, onCancel }) => {
const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = false }) => {
if (!visible) return null;
return ReactDOM.createPortal(
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white p-6 rounded-md shadow-md">
<p className="mb-4">{message}</p>
<div className="flex justify-end gap-4">
<button className="px-4 py-2 bg-gray-200 rounded-md" onClick={onCancel}>Annuler</button>
<button className="px-4 py-2 bg-emerald-500 text-white rounded-md" onClick={onConfirm}>Confirmer</button>
<div className={`flex ${uniqueConfirmButton ? 'justify-center' : 'justify-end'} gap-4`}>
{!uniqueConfirmButton && (
<button className="px-4 py-2 bg-gray-200 rounded-md" onClick={onCancel}>Annuler</button>
)}
<button className="px-4 py-2 bg-emerald-500 text-white rounded-md" onClick={onConfirm}>
{uniqueConfirmButton ? 'Compris !' : 'Confirmer'}
</button>
</div>
</div>
</div>,

View File

@ -0,0 +1,21 @@
import React, { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import useLocalStorage from '@/hooks/useLocalStorage';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
const ProtectedRoute = ({ children }) => {
const router = useRouter();
const [userId] = useLocalStorage("userId", '');
useEffect(() => {
if (!userId) {
// Rediriger vers la page de login si l'utilisateur n'est pas connecté
router.push(FE_USERS_LOGIN_URL);
}
}, [userId, router]);
// Afficher les enfants seulement si l'utilisateur est connecté
return userId ? children : null;
};
export default ProtectedRoute;

View File

@ -1,34 +1,36 @@
export default function SelectChoice({ type, name, label, choices, callback, selected, error, IconItem, disabled = false }) {
export default function SelectChoice({ type, name, label,required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) {
return (
<>
<div className="mb-4">
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div
className={`flex items-center border-2 rounded-md ${disabled ? 'border-gray-200' : 'border-gray-200 hover:border-gray-400 focus-within:border-gray-500'} h-8 mt-2`}
>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full">
{IconItem && <IconItem />}
</span>
<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 &&
<span className="inline-flex items-center px-3 text-gray-500 text-sm">
{<IconItem />}
</span>
}
<select
className={`mt-1 block w-full px-2 py-0 text-base rounded-r-md sm:text-sm border-none focus:ring-0 outline-none cursor-pointer ${disabled ? 'bg-gray-100' : ''}`}
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' : ''} ${selected === "" ? 'italic' : ''}`}
type={type}
id={name}
name={name}
value={selected}
onChange={callback}
disabled={disabled} // Ajout de l'attribut disabled avec une valeur par défaut de false
disabled={disabled}
>
<option value="" className="italic">{placeHolder?.toLowerCase()}</option>
{choices.map(({ value, label }, index) => (
<option key={value} value={value} className={value === '' ? 'italic' : ''}>
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
</>
);
}
}

View File

@ -0,0 +1,30 @@
import React, { useState } from 'react';
const SidebarTabs = ({ tabs }) => {
const [activeTab, setActiveTab] = useState(tabs[0].id);
return (
<div className="w-full">
<div className="flex border-b-2 border-gray-200">
{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'}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
<div className="p-4">
{tabs.map(tab => (
<div key={tab.id} className={`${activeTab === tab.id ? 'block' : 'hidden'}`}>
{tab.content}
</div>
))}
</div>
</div>
);
};
export default SidebarTabs;

View File

@ -34,15 +34,15 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
setFormData(prevState => {
const updatedTimes = [...prevState.time_range];
updatedTimes[index] = value;
const updatedFormData = {
...prevState,
time_range: updatedTimes,
};
const existingPlannings = prevState.plannings || [];
updatedFormData.plannings = updatePlannings(updatedFormData, existingPlannings);
return updatedFormData;
});
};
@ -50,24 +50,24 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
const handleJoursChange = (e) => {
const { value, checked } = e.target;
const dayId = parseInt(value, 10);
setFormData((prevState) => {
const updatedJoursOuverture = checked
? [...prevState.opening_days, dayId]
: prevState.opening_days.filter((id) => id !== dayId);
const updatedFormData = {
...prevState,
opening_days: updatedJoursOuverture,
};
const existingPlannings = prevState.plannings || [];
updatedFormData.plannings = updatePlannings(updatedFormData, existingPlannings);
return updatedFormData;
});
};
const handleChange = (e) => {
e.preventDefault();
const { name, value, type, checked } = e.target;
@ -78,8 +78,8 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
let newState = { ...prevState };
if (type === 'checkbox') {
const newValues = checked
? [...(prevState[name] || []), parseInt(value)]
const newValues = checked
? [...(prevState[name] || []), parseInt(value)]
: (prevState[name] || []).filter(v => v !== parseInt(value));
newState[name] = newValues;
} else if (name === 'age_range') {
@ -117,14 +117,14 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
return (
<div className="h-[80vh] overflow-y-auto">
<form onSubmit={handleSubmit} className="space-y-4 mt-8">
<div className="flex justify-between space-x-4">
{/* Section Ambiance */}
<div className="w-1/2 space-y-4">
<label className="block text-lg font-medium text-gray-700">Ambiance <i>(optionnel)</i></label>
<div className="space-y-4">
<div>
<InputTextIcon
<InputTextIcon
name="atmosphere_name"
type="text"
IconItem={Users}
@ -135,7 +135,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
/>
</div>
<div>
<InputTextIcon
<InputTextIcon
name="age_range"
type="text"
IconItem={Maximize2}
@ -185,7 +185,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
<div className="w-1/2 space-y-4">
<label className="block text-lg font-medium text-gray-700">Capacité</label>
<div className="space-y-4">
<InputTextIcon
<InputTextIcon
name="number_of_students"
type="number"
IconItem={UserPlus}
@ -201,9 +201,9 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
<div className="w-1/2 space-y-4">
<label className="block text-lg font-medium text-gray-700">Année scolaire</label>
<div className="space-y-4">
<SelectChoice
<SelectChoice
name="school_year"
placeholder="Sélectionner l'année scolaire"
placeHolder="Sélectionner l'année scolaire"
selected={formData.school_year}
callback={handleChange}
choices={schoolYears}
@ -215,19 +215,19 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
</div>
{/* Section Enseignants */}
<TeachersSelectionConfiguration formData={formData}
<TeachersSelectionConfiguration formData={formData}
teachers={teachers}
handleTeacherSelection={handleTeacherSelection}
handleTeacherSelection={handleTeacherSelection}
selectedTeachers={selectedTeachers}
/>
{/* Section Emploi du temps */}
<PlanningConfiguration formData={formData}
handleChange={handleChange}
handleTimeChange={handleTimeChange}
<PlanningConfiguration formData={formData}
handleChange={handleChange}
handleTimeChange={handleTimeChange}
handleJoursChange={handleJoursChange}
typeEmploiDuTemps={typeEmploiDuTemps}
typeEmploiDuTemps={typeEmploiDuTemps}
/>
<div className="flex justify-end mt-4 space-x-4">

View File

@ -1,4 +1,4 @@
import { Users, Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
import { Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
@ -49,11 +49,8 @@ const ClassesSection = ({ classes, teachers, handleCreate, handleEdit, handleDel
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<Users className="w-8 h-8 mr-2" />
Classes
</h2>
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-xl font-bold mb-4">Gestion des classes</h2>
<button
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"

View File

@ -0,0 +1,188 @@
import React, { useState } from 'react';
import { Plus, Trash, Edit3, Check, X } from 'lucide-react';
import Table from '@/components/Table';
import InputTextIcon from '@/components/InputTextIcon';
import Popup from '@/components/Popup';
const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete, errors }) => {
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 handleAddDiscount = () => {
setNewDiscount({ id: Date.now(), name: '', amount: '', description: '' });
};
const handleRemoveDiscount = (id) => {
handleDelete(id);
};
const handleSaveNewDiscount = () => {
if (newDiscount.name && newDiscount.amount) {
handleCreate(newDiscount)
.then(() => {
setNewDiscount(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis");
setPopupVisible(true);
}
};
const handleUpdateDiscount = (id, updatedDiscount) => {
if (updatedDiscount.name && updatedDiscount.amount) {
handleEdit(id, updatedDiscount)
.then(() => {
setEditingDiscount(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis");
setPopupVisible(true);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
if (editingDiscount) {
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
} else if (newDiscount) {
setNewDiscount((prevData) => ({
...prevData,
[name]: value,
}));
}
};
const renderInputField = (field, value, onChange, placeholder) => (
<div>
<InputTextIcon
name={field}
type={field === 'amount' ? 'number' : 'text'}
value={value}
onChange={onChange}
placeholder={placeholder}
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
/>
</div>
);
const renderDiscountCell = (discount, column) => {
const isEditing = editingDiscount === discount.id;
const isCreating = newDiscount && newDiscount.id === discount.id;
const currentData = isEditing ? formData : newDiscount;
if (isEditing || isCreating) {
switch (column) {
case 'LIBELLE':
return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction');
case 'MONTANT':
return renderInputField('amount', currentData.amount, handleChange, 'Montant');
case 'DESCRIPTION':
return renderInputField('description', currentData.description, handleChange, 'Description');
case 'ACTIONS':
return (
<div className="flex space-x-2">
<button
type="button"
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))}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
} else {
switch (column) {
case 'LIBELLE':
return discount.name;
case 'MONTANT':
return discount.amount + ' €';
case 'DESCRIPTION':
return discount.description;
case 'ACTIONS':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => setEditingDiscount(discount.id) || setFormData(discount)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => handleRemoveDiscount(discount.id)}
className="text-red-500 hover:text-red-700"
>
<Trash className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Réductions</h2>
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<Table
data={newDiscount ? [newDiscount, ...discounts] : discounts}
columns={[
{ name: 'LIBELLE', label: 'Libellé' },
{ name: 'MONTANT', label: 'Montant' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'ACTIONS', label: 'Actions' }
]}
renderCell={renderDiscountCell}
/>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
</div>
);
};
export default DiscountsSection;

View File

@ -0,0 +1,43 @@
import React, { useState } from 'react';
import FeesSection from './FeesSection';
import DiscountsSection from './DiscountsSection';
import TuitionFeesSection from './TuitionFeesSection';
import { TuitionFeesProvider } from '@/context/TuitionFeesContext';
import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL, BE_SCHOOL_TUITION_FEE_URL } from '@/utils/Url';
const FeesManagement = ({ fees, setFees, discounts, setDiscounts, setTuitionFees, handleCreate, handleEdit, handleDelete }) => {
const [errors, setErrors] = useState({});
return (
<TuitionFeesProvider>
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-6">
<div className="p-4 bg-white rounded-lg shadow-md">
<FeesSection
fees={fees}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees, setErrors)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees, setErrors)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)}
errors
/>
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<DiscountsSection
discounts={discounts}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts, setErrors)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts, setErrors)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)}
/>
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<TuitionFeesSection
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees, setErrors)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees, setErrors)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)}
/>
</div>
</div>
</TuitionFeesProvider>
);
};
export default FeesManagement;

View File

@ -0,0 +1,190 @@
import React, { useState } from 'react';
import { Plus, Trash, Edit3, Check, X } from 'lucide-react';
import Table from '@/components/Table';
import InputTextIcon from '@/components/InputTextIcon';
import Popup from '@/components/Popup';
const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) => {
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 handleAddFee = () => {
setNewFee({ id: Date.now(), name: '', amount: '', description: '' });
};
const handleRemoveFee = (id) => {
handleDelete(id);
};
const handleSaveNewFee = () => {
if (newFee.name && newFee.amount) {
handleCreate(newFee)
.then(() => {
setNewFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis");
setPopupVisible(true);
}
};
const handleUpdateFee = (id, updatedFee) => {
if (updatedFee.name && updatedFee.amount) {
handleEdit(id, updatedFee)
.then(() => {
setEditingFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis");
setPopupVisible(true);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
if (editingFee) {
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
} else if (newFee) {
setNewFee((prevData) => ({
...prevData,
[name]: value,
}));
}
};
const renderInputField = (field, value, onChange, placeholder) => (
<div>
<InputTextIcon
name={field}
type={field === 'amount' ? 'number' : 'text'}
value={value}
onChange={onChange}
placeholder={placeholder}
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
/>
</div>
);
const renderFeeCell = (fee, column) => {
const isEditing = editingFee === fee.id;
const isCreating = newFee && newFee.id === fee.id;
const currentData = isEditing ? formData : newFee;
if (isEditing || isCreating) {
switch (column) {
case 'LIBELLE':
return renderInputField('name', currentData.name, handleChange, 'Libellé du frais');
case 'MONTANT':
return renderInputField('amount', currentData.amount, handleChange, 'Montant');
case 'DESCRIPTION':
return renderInputField('description', currentData.description, handleChange, 'Description');
case 'ACTIONS':
return (
<div className="flex space-x-2">
<button
type="button"
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))}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
} else {
switch (column) {
case 'LIBELLE':
return fee.name;
case 'MONTANT':
return fee.amount + ' €';
case 'DESCRIPTION':
return fee.description;
case 'ACTIONS':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => setEditingFee(fee.id) || setFormData(fee)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => handleRemoveFee(fee.id)}
className="text-red-500 hover:text-red-700"
>
<Trash className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
return (
<>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Frais d'inscription</h2>
<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={[
{ name: 'LIBELLE', label: 'Libellé' },
{ name: 'MONTANT', label: 'Montant' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'ACTIONS', label: 'Actions' }
]}
renderCell={renderFeeCell}
/>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
</>
);
};
export default FeesSection;

View File

@ -1,4 +1,4 @@
import { BookOpen, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
@ -33,10 +33,7 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-4xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<BookOpen className="w-8 h-8 mr-2" />
Spécialités
</h2>
<h2 className="text-xl font-bold mb-4">Gestion des spécialités</h2>
<button
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
@ -52,8 +49,8 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel
transform: (row) => (
<div
className="inline-block px-3 py-1 rounded-full font-bold text-white"
style={{ backgroundColor: row. color_code }}
title={row. color_code}
style={{ backgroundColor: row.color_code }}
title={row.color_code}
>
<span className="font-bold text-white">{row.name.toUpperCase()}</span>
</div>

View File

@ -3,42 +3,41 @@ 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_SPECIALITY_URL,
BE_SCHOOL_TEACHER_URL,
BE_SCHOOL_SCHOOLCLASS_URL } from '@/utils/Url';
import { BE_SCHOOL_SPECIALITY_URL, BE_SCHOOL_TEACHER_URL, BE_SCHOOL_SCHOOLCLASS_URL } from '@/utils/Url';
const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, handleCreate, handleEdit, handleDelete }) => {
return (
<div className='p-8'>
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-6">
<ClassesProvider>
<SpecialitiesSection
specialities={specialities}
setSpecialities={setSpecialities}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_SPECIALITY_URL}`, newData, setSpecialities)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SPECIALITY_URL}`, id, updatedData, setSpecialities)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SPECIALITY_URL}`, id, setSpecialities)}
/>
<TeachersSection
teachers={teachers}
specialities={specialities}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_TEACHER_URL}`, newData, setTeachers)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TEACHER_URL}`, id, updatedData, setTeachers)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHER_URL}`, id, setTeachers)}
/>
<ClassesSection
classes={classes}
teachers={teachers}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_SCHOOLCLASS_URL}`, newData, setClasses)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, updatedData, setClasses)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, setClasses)}
/>
<div className="p-4 bg-white rounded-lg shadow-md">
<SpecialitiesSection
specialities={specialities}
setSpecialities={setSpecialities}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_SPECIALITY_URL}`, newData, setSpecialities)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SPECIALITY_URL}`, id, updatedData, setSpecialities)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SPECIALITY_URL}`, id, setSpecialities)}
/>
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<TeachersSection
teachers={teachers}
specialities={specialities}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_TEACHER_URL}`, newData, setTeachers)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TEACHER_URL}`, id, updatedData, setTeachers)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHER_URL}`, id, setTeachers)}
/>
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<ClassesSection
classes={classes}
teachers={teachers}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_SCHOOLCLASS_URL}`, newData, setClasses)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, updatedData, setClasses)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, setClasses)}
/>
</div>
</ClassesProvider>
</div>
);
};

View File

@ -1,4 +1,4 @@
import { GraduationCap, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
@ -76,11 +76,8 @@ const TeachersSection = ({ teachers, specialities , handleCreate, handleEdit, ha
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<GraduationCap className="w-8 h-8 mr-2" />
Enseignants
</h2>
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-xl font-bold mb-4">Gestion des enseignants</h2>
<button
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"

View File

@ -0,0 +1,286 @@
import React, { useState } from 'react';
import { Plus, Trash, Edit3, Check, X, Calendar } from 'lucide-react';
import Table from '@/components/Table';
import InputTextIcon from '@/components/InputTextIcon';
import Popup from '@/components/Popup';
import SelectChoice from '@/components/SelectChoice';
import { useTuitionFees } from '@/context/TuitionFeesContext';
const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) => {
const { fees, tuitionFees, setTuitionFees, discounts } = useTuitionFees();
const [editingTuitionFee, setEditingTuitionFee] = useState(null);
const [newTuitionFee, setNewTuitionFee] = useState(null);
const [formData, setFormData] = useState({});
const [localErrors, setLocalErrors] = useState({});
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const paymentOptions = [
{ value: 0, label: '1 fois' },
{ value: 1, label: '4 fois' },
{ value: 2, label: '10 fois' }
];
const handleAddTuitionFee = () => {
setNewTuitionFee({ id: Date.now(), name: '', base_amount: '', description: '', validity_start_date: '', validity_end_date: '', payment_option: '', discounts: [] });
};
const handleRemoveTuitionFee = (id) => {
handleDelete(id);
setTuitionFees(tuitionFees.filter(fee => fee.id !== id));
};
const handleSaveNewTuitionFee = () => {
if (
newTuitionFee.name &&
newTuitionFee.base_amount &&
newTuitionFee.payment_option >= 0 &&
newTuitionFee.validity_start_date &&
newTuitionFee.validity_end_date &&
new Date(newTuitionFee.validity_start_date) <= new Date(newTuitionFee.validity_end_date)
) {
handleCreate(newTuitionFee)
.then((createdTuitionFee) => {
setTuitionFees([createdTuitionFee, ...tuitionFees]);
setNewTuitionFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis et valides");
setPopupVisible(true);
}
};
const handleUpdateTuitionFee = (id, updatedTuitionFee) => {
if (
updatedTuitionFee.name &&
updatedTuitionFee.base_amount &&
updatedTuitionFee.payment_option >= 0 &&
updatedTuitionFee.validity_start_date &&
updatedTuitionFee.validity_end_date &&
new Date(updatedTuitionFee.validity_start_date) <= new Date(updatedTuitionFee.validity_end_date)
) {
handleEdit(id, updatedTuitionFee)
.then((updatedFee) => {
setTuitionFees(tuitionFees.map(fee => fee.id === id ? updatedFee : fee));
setEditingTuitionFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis et valides");
setPopupVisible(true);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
let parsedValue = value;
if (name === 'payment_option') {
parsedValue = parseInt(value, 10);
} else if (name === 'discounts') {
parsedValue = value.split(',').map(v => parseInt(v, 10));
}
if (editingTuitionFee) {
setFormData((prevData) => ({
...prevData,
[name]: parsedValue,
}));
} else if (newTuitionFee) {
setNewTuitionFee((prevData) => ({
...prevData,
[name]: parsedValue,
}));
}
};
const renderInputField = (field, value, onChange, placeholder) => (
<div className="flex justify-center items-center h-full">
<InputTextIcon
name={field}
type={field === 'base_amount' ? 'number' : 'text'}
value={value}
onChange={onChange}
placeholder={placeholder}
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
/>
</div>
);
const renderDateField = (field, value, onChange) => (
<div className="relative flex items-center justify-center h-full">
<Calendar className="w-5 h-5 text-emerald-500 absolute left-3" />
<input
type="date"
name={field}
value={value}
onChange={onChange}
className="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500"
/>
</div>
);
const renderSelectField = (field, value, options, callback, label) => (
<div className="flex justify-center items-center h-full">
<SelectChoice
name={field}
value={value}
options={options}
callback={callback}
placeHolder={label}
choices={options}
/>
</div>
);
const calculateFinalAmount = (baseAmount, discountIds) => {
const totalFees = fees.reduce((sum, fee) => sum + parseFloat(fee.amount), 0);
const totalDiscounts = discountIds.reduce((sum, discountId) => {
const discount = discounts.find(d => d.id === discountId);
return discount ? sum + parseFloat(discount.amount) : sum;
}, 0);
const finalAmount = parseFloat(baseAmount) + totalFees - totalDiscounts;
return finalAmount.toFixed(2);
};
const renderTuitionFeeCell = (tuitionFee, column) => {
const isEditing = editingTuitionFee === tuitionFee.id;
const isCreating = newTuitionFee && newTuitionFee.id === tuitionFee.id;
const currentData = isEditing ? formData : newTuitionFee;
if (isEditing || isCreating) {
switch (column) {
case 'NOM':
return renderInputField('name', currentData.name, handleChange, 'Nom des frais de scolarité');
case 'MONTANT DE BASE':
return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base');
case 'DESCRIPTION':
return renderInputField('description', currentData.description, handleChange, 'Description');
case 'DATE DE DEBUT':
return renderDateField('validity_start_date', currentData.validity_start_date, handleChange);
case 'DATE DE FIN':
return renderDateField('validity_end_date', currentData.validity_end_date, handleChange);
case 'OPTIONS DE PAIEMENT':
return renderSelectField('payment_option', currentData.payment_option, paymentOptions, handleChange, 'Options de paiement');
case 'REMISES':
return renderSelectField('discounts', currentData.discounts, discounts.map(discount => ({ value: discount.id, label: discount.name })), handleChange, 'Remises');
case 'ACTIONS':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => (isEditing ? handleUpdateTuitionFee(editingTuitionFee, formData) : handleSaveNewTuitionFee())}
className="text-green-500 hover:text-green-700"
>
<Check className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => (isEditing ? setEditingTuitionFee(null) : setNewTuitionFee(null))}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
} else {
switch (column) {
case 'NOM':
return tuitionFee.name;
case 'MONTANT DE BASE':
return tuitionFee.base_amount + ' €';
case 'DESCRIPTION':
return tuitionFee.description;
case 'DATE DE DEBUT':
return tuitionFee.validity_start_date;
case 'DATE DE FIN':
return tuitionFee.validity_end_date;
case 'OPTIONS DE PAIEMENT':
return paymentOptions.find(option => option.value === tuitionFee.payment_option)?.label || '';
case 'REMISES':
const discountNames = tuitionFee.discounts
.map(discountId => discounts.find(discount => discount.id === discountId)?.name)
.filter(name => name)
.join(', ');
return discountNames;
case 'MONTANT FINAL':
return calculateFinalAmount(tuitionFee.base_amount, tuitionFee.discounts) + ' €';
case 'ACTIONS':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => setEditingTuitionFee(tuitionFee.id) || setFormData(tuitionFee)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => handleRemoveTuitionFee(tuitionFee.id)}
className="text-red-500 hover:text-red-700"
>
<Trash className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Frais de scolarité</h2>
<button type="button" onClick={handleAddTuitionFee} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<Table
data={newTuitionFee ? [newTuitionFee, ...tuitionFees] : tuitionFees}
columns={[
{ name: 'NOM', label: 'Nom' },
{ name: 'MONTANT DE BASE', label: 'Montant de base' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'DATE DE DEBUT', label: 'Date de début' },
{ name: 'DATE DE FIN', label: 'Date de fin' },
{ name: 'OPTIONS DE PAIEMENT', label: 'Options de paiement' },
{ name: 'REMISES', label: 'Remises' },
{ name: 'MONTANT FINAL', label: 'Montant final' },
{ name: 'ACTIONS', label: 'Actions' }
]}
renderCell={renderTuitionFeeCell}
/>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
</div>
);
};
export default TuitionFeesSection;

View File

@ -160,7 +160,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
<div>
<SelectChoice
name="specialites"
label="Spécialités"
placeHolder="Spécialités"
selected={selectedSpeciality}
choices={[
{ value: '', label: 'Sélectionner une spécialité' },
@ -178,7 +178,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
<div>
<SelectChoice
name="teachers"
label="Enseignants"
placeHolder="Enseignants"
selected={selectedTeacher}
choices={[
{ value: '', label: 'Sélectionner un enseignant'},

View File

@ -14,17 +14,17 @@ const ToggleSwitch = ({ label, checked, onChange }) => {
<div className="flex items-center mt-4">
<label className="mr-2 text-gray-600">{label}</label>
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input
type="checkbox"
name="toggle"
id="toggle"
<input
type="checkbox"
name="toggle"
id="toggle"
checked={checked}
onChange={handleChange}
className="hover:text-emerald-500 absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer border-emerald-500 checked:right-0 checked:border-emerald-500 checked:bg-emerald-500 hover:border-emerald-500 hover:bg-emerald-500 focus:outline-none focus:ring-0"
ref={inputRef} // Reference to the input element
/>
<label
htmlFor="toggle"
<label
htmlFor="toggle"
className={`toggle-label block overflow-hidden h-6 rounded-full cursor-pointer transition-colors duration-200 ${checked ? 'bg-emerald-300' : 'bg-gray-300'}`}
></label>
</div>