mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Ajout de la configuration des tarifs de l'école [#18]
This commit is contained in:
committed by
Luc SORIGNET
parent
147a70135d
commit
5a0e65bb75
@ -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">
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
190
Front-End/src/components/Structure/Configuration/FeesSection.js
Normal file
190
Front-End/src/components/Structure/Configuration/FeesSection.js
Normal 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;
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
Reference in New Issue
Block a user