diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 937b1e5..7c7f609 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -86,6 +86,8 @@ class Discount(models.Model): amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) description = models.TextField(blank=True) discount_type = models.IntegerField(choices=DiscountType.choices, default=DiscountType.CURRENCY) + type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE) + updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name @@ -94,11 +96,9 @@ class Fee(models.Model): name = models.CharField(max_length=255, unique=True) base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) description = models.TextField(blank=True) - payment_option = models.IntegerField(choices=PaymentOptions.choices, default=PaymentOptions.SINGLE_PAYMENT) discounts = models.ManyToManyField('Discount', blank=True) is_active = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) - currency = models.CharField(max_length=3, default='EUR') type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE) def __str__(self): diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index 0aaccc1..a4e2a90 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -175,12 +175,20 @@ class SchoolClassSerializer(serializers.ModelSerializer): return local_time.strftime("%d-%m-%Y %H:%M") class DiscountSerializer(serializers.ModelSerializer): + updated_at_formatted = serializers.SerializerMethodField() class Meta: model = Discount fields = '__all__' + def get_updated_at_formatted(self, obj): + utc_time = timezone.localtime(obj.updated_at) + local_tz = pytz.timezone(settings.TZ_APPLI) + local_time = utc_time.astimezone(local_tz) + return local_time.strftime("%d-%m-%Y %H:%M") + class FeeSerializer(serializers.ModelSerializer): discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) + updated_at_formatted = serializers.SerializerMethodField() class Meta: model = Fee @@ -204,8 +212,6 @@ class FeeSerializer(serializers.ModelSerializer): instance.name = validated_data.get('name', instance.name) instance.description = validated_data.get('description', instance.description) instance.base_amount = validated_data.get('base_amount', instance.base_amount) - instance.currency = validated_data.get('currency', instance.currency) - instance.payment_option = validated_data.get('payment_option', instance.payment_option) instance.is_active = validated_data.get('is_active', instance.is_active) instance.updated_at = validated_data.get('updated_at', instance.updated_at) instance.type = validated_data.get('type', instance.type) @@ -214,4 +220,10 @@ class FeeSerializer(serializers.ModelSerializer): # Update discounts if provided instance.discounts.set(discounts_data) - return instance \ No newline at end of file + return instance + + def get_updated_at_formatted(self, obj): + utc_time = timezone.localtime(obj.updated_at) + local_tz = pytz.timezone(settings.TZ_APPLI) + local_time = utc_time.astimezone(local_tz) + return local_time.strftime("%d-%m-%Y %H:%M") \ No newline at end of file diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index cb04f28..6cd2009 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -36,7 +36,7 @@ urlpatterns = [ re_path(r'^fee$', FeeView.as_view(), name="fee"), re_path(r'^fee/([0-9]+)$', FeeView.as_view(), name="fee"), - re_path(r'^discounts$', DiscountsView.as_view(), name="discounts"), + re_path(r'^discounts/(?P<_filter>[a-zA-z]+)$$', DiscountsView.as_view(), name="discounts"), re_path(r'^discount$', DiscountView.as_view(), name="discount"), re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"), ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index 35b648a..86bbc89 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -65,57 +65,6 @@ class SpecialityView(APIView): def delete(self, request, _id): return delete_object(Speciality, _id) -# Vues pour les réductions (Discount) -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class DiscountsView(APIView): - def get(self, request): - discountsList = Discount.objects.all() - discounts_serializer = DiscountSerializer(discountsList, many=True) - return JsonResponse(discounts_serializer.data, safe=False) - - def post(self, request): - discount_data = JSONParser().parse(request) - discount_serializer = DiscountSerializer(data=discount_data) - if discount_serializer.is_valid(): - discount_serializer.save() - return JsonResponse(discount_serializer.data, safe=False, status=201) - return JsonResponse(discount_serializer.errors, safe=False, status=400) - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class DiscountView(APIView): - def get(self, request, _id): - try: - discount = Discount.objects.get(id=_id) - discount_serializer = DiscountSerializer(discount) - return JsonResponse(discount_serializer.data, safe=False) - except Discount.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=404) - - def post(self, request): - discount_data = JSONParser().parse(request) - discount_serializer = DiscountSerializer(data=discount_data) - if discount_serializer.is_valid(): - discount_serializer.save() - return JsonResponse(discount_serializer.data, safe=False, status=201) - return JsonResponse(discount_serializer.errors, safe=False, status=400) - - def put(self, request, _id): - discount_data = JSONParser().parse(request) - try: - discount = Discount.objects.get(id=_id) - except Discount.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=404) - discount_serializer = DiscountSerializer(discount, data=discount_data, partial=True) # Utilisation de partial=True - if discount_serializer.is_valid(): - discount_serializer.save() - return JsonResponse(discount_serializer.data, safe=False) - return JsonResponse(discount_serializer.errors, safe=False, status=400) - - def delete(self, request, _id): - return delete_object(Discount, _id) - class TeachersView(APIView): def get(self, request): teachersList=getAllObjects(Teacher) @@ -263,7 +212,55 @@ class PlanningView(APIView): return JsonResponse(planning_serializer.errors, safe=False) -# Vues pour les frais (Fee) +# Vues pour les réductions (Discount) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountsView(APIView): + def get(self, request, _filter, *args, **kwargs): + + if _filter not in ['registration', 'tuition']: + return JsonResponse({"error": "Invalid type parameter. Must be 'registration' or 'tuition'."}, safe=False, status=400) + + discount_type_value = 0 if _filter == 'registration' else 1 + discounts = Discount.objects.filter(type=discount_type_value) + discounts_serializer = DiscountSerializer(discounts, many=True) + + return JsonResponse(discounts_serializer.data, safe=False, status=200) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountView(APIView): + def get(self, request, _id): + try: + discount = Discount.objects.get(id=_id) + discount_serializer = DiscountSerializer(discount) + return JsonResponse(discount_serializer.data, safe=False) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + discount_data = JSONParser().parse(request) + discount_serializer = DiscountSerializer(data=discount_data) + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False, status=201) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + discount_data = JSONParser().parse(request) + try: + discount = Discount.objects.get(id=_id) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + discount_serializer = DiscountSerializer(discount, data=discount_data, partial=True) # Utilisation de partial=True + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(Discount, _id) + @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class FeesView(APIView): diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 0d893e6..545a112 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -6,15 +6,16 @@ import FeesManagement from '@/components/Structure/Configuration/FeesManagement' import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; -import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchDiscounts, fetchRegistrationFees, fetchTuitionFees } from '@/app/lib/schoolAction'; +import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchRegistrationDiscounts, fetchTuitionDiscounts, fetchRegistrationFees, fetchTuitionFees } from '@/app/lib/schoolAction'; import SidebarTabs from '@/components/SidebarTabs'; export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); + const [registrationDiscounts, setRegistrationDiscounts] = useState([]); + const [tuitionDiscounts, setTuitionDiscounts] = useState([]); const [registrationFees, setRegistrationFees] = useState([]); - const [discounts, setDiscounts] = useState([]); const [tuitionFees, setTuitionFees] = useState([]); const csrfToken = useCsrfToken(); @@ -32,8 +33,11 @@ export default function Page() { // Fetch data for schedules handleSchedules(); - // Fetch data for discounts - handleDiscounts(); + // Fetch data for registration discounts + handleRegistrationDiscounts(); + + // Fetch data for tuition discounts + handleTuitionDiscounts(); // Fetch data for registration fees handleRegistrationFees(); @@ -74,12 +78,20 @@ export default function Page() { .catch(error => console.error('Error fetching schedules:', error)); }; - const handleDiscounts = () => { - fetchDiscounts() + const handleRegistrationDiscounts = () => { + fetchRegistrationDiscounts() .then(data => { - setDiscounts(data); + setRegistrationDiscounts(data); }) - .catch(error => console.error('Error fetching discounts:', error)); + .catch(error => console.error('Error fetching registration discounts:', error)); + }; + + const handleTuitionDiscounts = () => { + fetchTuitionDiscounts() + .then(data => { + setTuitionDiscounts(data); + }) + .catch(error => console.error('Error fetching tuition discounts:', error)); }; const handleRegistrationFees = () => { @@ -236,8 +248,10 @@ export default function Page() { label: 'Tarifications', content: ( { .then(requestResponseHandler) }; -export const fetchDiscounts = () => { - return fetch(`${BE_SCHOOL_DISCOUNTS_URL}`) +export const fetchRegistrationDiscounts = () => { + return fetch(`${BE_SCHOOL_DISCOUNTS_URL}/registration`) + .then(requestResponseHandler) +}; + +export const fetchTuitionDiscounts = () => { + return fetch(`${BE_SCHOOL_DISCOUNTS_URL}/tuition`) .then(requestResponseHandler) }; diff --git a/Front-End/src/components/Structure/Configuration/DiscountsSection.js b/Front-End/src/components/Structure/Configuration/DiscountsSection.js index f6c48d6..3c19301 100644 --- a/Front-End/src/components/Structure/Configuration/DiscountsSection.js +++ b/Front-End/src/components/Structure/Configuration/DiscountsSection.js @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon } from 'lucide-react'; +import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; -const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, errors }) => { +const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type }) => { const [editingDiscount, setEditingDiscount] = useState(null); const [newDiscount, setNewDiscount] = useState(null); const [formData, setFormData] = useState({}); @@ -13,7 +13,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h const [popupMessage, setPopupMessage] = useState(""); const handleAddDiscount = () => { - setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discountType: 'amount' }); + setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discount_type: 0, type: type }); }; const handleRemoveDiscount = (id) => { @@ -67,13 +67,13 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h } }; - const handleToggleDiscountType = (id, newType) => { + const handleToggleDiscountType = (id) => { const discount = discounts.find(discount => discount.id === id); if (!discount) return; const updatedData = { ...discount, - discount_type: newType + discount_type: discount.discount_type === 0 ? 1 : 0 }; handleEdit(id, updatedData) @@ -87,9 +87,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h const handleChange = (e) => { const { name, value } = e.target; - if (name === 'discountType') { - setDiscountType(value); - } else if (editingDiscount) { + if (editingDiscount) { setFormData((prevData) => ({ ...prevData, [name]: value, @@ -124,8 +122,8 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h switch (column) { case 'LIBELLE': return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction'); - case 'VALEUR': - return renderInputField('amount', currentData.amount, handleChange, discount.discount_type === 0 ? 'Montant' : 'Pourcentage'); + case 'REMISE': + return renderInputField('amount', currentData.amount, handleChange,'Montant'); case 'DESCRIPTION': return renderInputField('description', currentData.description, handleChange, 'Description'); case 'ACTIONS': @@ -154,32 +152,22 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h switch (column) { case 'LIBELLE': return discount.name; - case 'VALEUR': + case 'REMISE': return discount.discount_type === 0 ? `${discount.amount} €` : `${discount.amount} %`; - case 'TYPE DE REMISE': - return ( -
- - -
- ); case 'DESCRIPTION': return discount.description; + case 'MISE A JOUR': + return discount.updated_at_formatted; case 'ACTIONS': return (
+ @@ -214,12 +205,13 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h data={newDiscount ? [newDiscount, ...discounts] : discounts} columns={[ { name: 'LIBELLE', label: 'Libellé' }, - { name: 'VALEUR', label: 'Valeur' }, - { name: 'TYPE DE REMISE', label: 'Type de remise' }, + { name: 'REMISE', label: 'Valeur' }, { name: 'DESCRIPTION', label: 'Description' }, + { name: 'MISE A JOUR', label: 'date mise à jour' }, { name: 'ACTIONS', label: 'Actions' } ]} renderCell={renderDiscountCell} + defaultTheme='bg-yellow-100' /> { +const FeesManagement = ({ registrationDiscounts, setRegistrationDiscounts, tuitionDiscounts, setTuitionDiscounts, registrationFees, setRegistrationFees, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { + + const handleDiscountDelete = (id, type) => { + if (type === 0) { + setRegistrationFees(prevFees => + prevFees.map(fee => ({ + ...fee, + discounts: fee.discounts.filter(discountId => discountId !== id) + })) + ); + } else { + setTuitionFees(prevFees => + prevFees.map(fee => ({ + ...fee, + discounts: fee.discounts.filter(discountId => discountId !== id) + })) + ); + } + }; return (
-
- handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)} - /> +
+
+ handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setRegistrationFees)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setRegistrationFees)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setRegistrationFees)} + type={0} + /> +
+
+ handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setRegistrationDiscounts)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setRegistrationDiscounts)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setRegistrationDiscounts)} + onDiscountDelete={(id) => handleDiscountDelete(id, 0)} + type={0} + /> +
-
- handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setRegistrationFees)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setRegistrationFees)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setRegistrationFees)} - /> -
-
- handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setTuitionFees)} - handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setTuitionFees)} - handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setTuitionFees)} - /> +
+
+ handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setTuitionFees)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setTuitionFees)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setTuitionFees)} + type={1} + /> +
+
+ handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setTuitionDiscounts)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setTuitionDiscounts)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setTuitionDiscounts)} + onDiscountDelete={(id) => handleDiscountDelete(id, 1)} + type={1} + /> +
); diff --git a/Front-End/src/components/Structure/Configuration/RegistrationFeesSection.js b/Front-End/src/components/Structure/Configuration/FeesSection.js similarity index 61% rename from Front-End/src/components/Structure/Configuration/RegistrationFeesSection.js rename to Front-End/src/components/Structure/Configuration/FeesSection.js index f8a9886..cbfe960 100644 --- a/Front-End/src/components/Structure/Configuration/RegistrationFeesSection.js +++ b/Front-End/src/components/Structure/Configuration/FeesSection.js @@ -1,11 +1,10 @@ import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, EyeOff, Eye } from 'lucide-react'; +import { Plus, Trash, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react'; import Table from '@/components/Table'; import InputTextIcon from '@/components/InputTextIcon'; import Popup from '@/components/Popup'; -import SelectChoice from '@/components/SelectChoice'; -const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discounts, handleCreate, handleEdit, handleDelete }) => { +const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type }) => { const [editingFee, setEditingFee] = useState(null); const [newFee, setNewFee] = useState(null); const [formData, setFormData] = useState({}); @@ -13,30 +12,27 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou 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 handleAddFee = () => { - setNewFee({ id: Date.now(), name: '', base_amount: '', description: '' }); + setNewFee({ id: Date.now(), name: '', base_amount: '', description: '', validity_start_date: '', validity_end_date: '', discounts: [], type: type }); }; const handleRemoveFee = (id) => { - handleDelete(id); + handleDelete(id) + .then(() => { + setFees(prevFees => prevFees.filter(fee => fee.id !== id)); + }) + .catch(error => { + console.error(error); + }); }; const handleSaveNewFee = () => { - if (newFee.name && newFee.base_amount) { - const feeData = { - ...newFee, - type: 0 - }; - - handleCreate(feeData) + if ( + newFee.name && + newFee.base_amount) { + handleCreate(newFee) .then((createdFee) => { - setRegistrationFees([createdFee, ...registrationFees]); + setFees([createdFee, ...fees]); setNewFee(null); setLocalErrors({}); }) @@ -48,15 +44,18 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou } }); } else { - setPopupMessage("Tous les champs doivent être remplis"); + 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(() => { + .then((updatedFee) => { + setFees(fees.map(fee => fee.id === id ? updatedFee : fee)); setEditingFee(null); setLocalErrors({}); }) @@ -68,13 +67,13 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou } }); } else { - setPopupMessage("Tous les champs doivent être remplis"); + setPopupMessage("Tous les champs doivent être remplis et valides"); setPopupVisible(true); } }; const handleToggleActive = (id, isActive) => { - const fee = registrationFees.find(fee => fee.id === id); + const fee = fees.find(fee => fee.id === id); if (!fee) return; const updatedData = { @@ -84,7 +83,7 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou handleEdit(id, updatedData) .then(() => { - setRegistrationFees(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 => { console.error(error); @@ -110,6 +109,19 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou } }; + const renderInputField = (field, value, onChange, placeholder) => ( +
+ +
+ ); + const calculateFinalAmount = (baseAmount, discountIds) => { const totalDiscounts = discountIds.reduce((sum, discountId) => { const discount = discounts.find(d => d.id === discountId); @@ -128,32 +140,6 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou return finalAmount.toFixed(2); }; - const renderInputField = (field, value, onChange, placeholder) => ( -
- -
- ); - - const renderSelectField = (field, value, options, callback, label) => ( -
- -
- ); - const renderFeeCell = (fee, column) => { const isEditing = editingFee === fee.id; const isCreating = newFee && newFee.id === fee.id; @@ -161,19 +147,15 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou if (isEditing || isCreating) { switch (column) { - case 'LIBELLE': - return renderInputField('name', currentData.name, handleChange, 'Libellé du frais'); - case 'MONTANT DE BASE': - return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant'); + case 'NOM': + return renderInputField('name', currentData.name, handleChange, 'Nom des frais'); + case 'MONTANT': + return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base'); case 'DESCRIPTION': return renderInputField('description', currentData.description, handleChange, 'Description'); - 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 ( -
+
-
); default: @@ -244,28 +218,27 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou }; return ( - <> -
-
-

Frais d'inscription

- +
+
+
+ +

{type === 0 ? 'Frais d\'inscription' : 'Frais de scolarité'}

- + +
setPopupVisible(false)} uniqueConfirmButton={true} /> - + ); }; -export default RegistrationFeesSection; \ No newline at end of file +export default FeesSection; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js b/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js deleted file mode 100644 index 34dc882..0000000 --- a/Front-End/src/components/Structure/Configuration/TuitionFeesSection.js +++ /dev/null @@ -1,294 +0,0 @@ -import React, { useState } from 'react'; -import { Plus, Trash, Edit3, Check, X, EyeOff, Eye } from 'lucide-react'; -import Table from '@/components/Table'; -import InputTextIcon from '@/components/InputTextIcon'; -import Popup from '@/components/Popup'; -import SelectChoice from '@/components/SelectChoice'; - -const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, registrationFees, handleCreate, handleEdit, handleDelete }) => { - 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) - .then(() => { - setTuitionFees(prevTuitionFees => prevTuitionFees.filter(fee => fee.id !== id)); - }) - .catch(error => { - console.error(error); - }); - }; - - const handleSaveNewTuitionFee = () => { - if ( - newTuitionFee.name && - newTuitionFee.base_amount && - newTuitionFee.payment_option >= 0 - ) { - const tuitionFeeData = { - ...newTuitionFee, - type: 1 - }; - handleCreate(tuitionFeeData) - .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 - ) { - 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 handleToggleActive = (id, isActive) => { - const tuitionFee = tuitionFees.find(tuitionFee => tuitionFee.id === id); - if (!tuitionFee) return; - - const updatedData = { - is_active: !isActive, - discounts: tuitionFee.discounts - }; - - handleEdit(id, updatedData) - .then(() => { - setFees(prevTuitionFees => prevTuitionFees.map(tuitionFee => tuitionFee.id === id ? { ...tuitionFee, is_active: !isActive } : tuitionFee)); - }) - .catch(error => { - console.error(error); - }); - }; - - 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) => ( -
- -
- ); - - const renderSelectField = (field, value, options, callback, label) => ( -
- -
- ); - - const calculateFinalAmount = (baseAmount, discountIds) => { - const totalDiscounts = discountIds.reduce((sum, discountId) => { - const discount = discounts.find(d => d.id === discountId); - if (discount) { - if (discount.discount_type === 0) { // Currency - return sum + parseFloat(discount.amount); - } else if (discount.discount_type === 1) { // Percent - return sum + (parseFloat(baseAmount) * parseFloat(discount.amount) / 100); - } - } - return sum; - }, 0); - - const finalAmount = parseFloat(baseAmount) - 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 '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 ( -
- - -
- ); - 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 '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 ( -
- - - -
- ); - default: - return null; - } - } - }; - - return ( -
-
-

Frais de scolarité

- -
-
- setPopupVisible(false)} - onCancel={() => setPopupVisible(false)} - uniqueConfirmButton={true} - /> - - ); -}; - -export default TuitionFeesSection; \ No newline at end of file diff --git a/Front-End/src/components/Table.js b/Front-End/src/components/Table.js index d22d378..116c98b 100644 --- a/Front-End/src/components/Table.js +++ b/Front-End/src/components/Table.js @@ -2,7 +2,7 @@ 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 }) => { +const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange, onRowClick, selectedRows, isSelectable = false, defaultTheme='bg-emerald-50' }) => { const handlePageChange = (newPage) => { onPageChange(newPage); }; @@ -25,7 +25,7 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total key={rowIndex} className={` ${isSelectable ? 'cursor-pointer' : ''} - ${selectedRows?.includes(row.id) ? 'bg-emerald-500 text-white' : rowIndex % 2 === 0 ? 'bg-emerald-50' : ''} + ${selectedRows?.includes(row.id) ? 'bg-emerald-500 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''} ${isSelectable ? 'hover:bg-emerald-600' : ''} `} onClick={() => isSelectable && onRowClick && onRowClick(row)}