mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Sortie des calculs des montants totaux de la partie configuration + revue du rendu [#18]
This commit is contained in:
@ -86,6 +86,8 @@ class Discount(models.Model):
|
|||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
discount_type = models.IntegerField(choices=DiscountType.choices, default=DiscountType.CURRENCY)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -94,11 +96,9 @@ class Fee(models.Model):
|
|||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255, unique=True)
|
||||||
base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
payment_option = models.IntegerField(choices=PaymentOptions.choices, default=PaymentOptions.SINGLE_PAYMENT)
|
|
||||||
discounts = models.ManyToManyField('Discount', blank=True)
|
discounts = models.ManyToManyField('Discount', blank=True)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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)
|
type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@ -175,12 +175,20 @@ class SchoolClassSerializer(serializers.ModelSerializer):
|
|||||||
return local_time.strftime("%d-%m-%Y %H:%M")
|
return local_time.strftime("%d-%m-%Y %H:%M")
|
||||||
|
|
||||||
class DiscountSerializer(serializers.ModelSerializer):
|
class DiscountSerializer(serializers.ModelSerializer):
|
||||||
|
updated_at_formatted = serializers.SerializerMethodField()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Discount
|
model = Discount
|
||||||
fields = '__all__'
|
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):
|
class FeeSerializer(serializers.ModelSerializer):
|
||||||
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True)
|
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True)
|
||||||
|
updated_at_formatted = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Fee
|
model = Fee
|
||||||
@ -204,8 +212,6 @@ class FeeSerializer(serializers.ModelSerializer):
|
|||||||
instance.name = validated_data.get('name', instance.name)
|
instance.name = validated_data.get('name', instance.name)
|
||||||
instance.description = validated_data.get('description', instance.description)
|
instance.description = validated_data.get('description', instance.description)
|
||||||
instance.base_amount = validated_data.get('base_amount', instance.base_amount)
|
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.is_active = validated_data.get('is_active', instance.is_active)
|
||||||
instance.updated_at = validated_data.get('updated_at', instance.updated_at)
|
instance.updated_at = validated_data.get('updated_at', instance.updated_at)
|
||||||
instance.type = validated_data.get('type', instance.type)
|
instance.type = validated_data.get('type', instance.type)
|
||||||
@ -215,3 +221,9 @@ class FeeSerializer(serializers.ModelSerializer):
|
|||||||
instance.discounts.set(discounts_data)
|
instance.discounts.set(discounts_data)
|
||||||
|
|
||||||
return instance
|
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")
|
||||||
@ -36,7 +36,7 @@ urlpatterns = [
|
|||||||
re_path(r'^fee$', FeeView.as_view(), name="fee"),
|
re_path(r'^fee$', FeeView.as_view(), name="fee"),
|
||||||
re_path(r'^fee/([0-9]+)$', 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$', DiscountView.as_view(), name="discount"),
|
||||||
re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"),
|
re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"),
|
||||||
]
|
]
|
||||||
@ -65,57 +65,6 @@ class SpecialityView(APIView):
|
|||||||
def delete(self, request, _id):
|
def delete(self, request, _id):
|
||||||
return delete_object(Speciality, _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):
|
class TeachersView(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
teachersList=getAllObjects(Teacher)
|
teachersList=getAllObjects(Teacher)
|
||||||
@ -263,7 +212,55 @@ class PlanningView(APIView):
|
|||||||
return JsonResponse(planning_serializer.errors, safe=False)
|
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(csrf_protect, name='dispatch')
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||||
class FeesView(APIView):
|
class FeesView(APIView):
|
||||||
|
|||||||
@ -6,15 +6,16 @@ import FeesManagement from '@/components/Structure/Configuration/FeesManagement'
|
|||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||||
import { ClassesProvider } from '@/context/ClassesContext';
|
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';
|
import SidebarTabs from '@/components/SidebarTabs';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [specialities, setSpecialities] = useState([]);
|
const [specialities, setSpecialities] = useState([]);
|
||||||
const [classes, setClasses] = useState([]);
|
const [classes, setClasses] = useState([]);
|
||||||
const [teachers, setTeachers] = useState([]);
|
const [teachers, setTeachers] = useState([]);
|
||||||
|
const [registrationDiscounts, setRegistrationDiscounts] = useState([]);
|
||||||
|
const [tuitionDiscounts, setTuitionDiscounts] = useState([]);
|
||||||
const [registrationFees, setRegistrationFees] = useState([]);
|
const [registrationFees, setRegistrationFees] = useState([]);
|
||||||
const [discounts, setDiscounts] = useState([]);
|
|
||||||
const [tuitionFees, setTuitionFees] = useState([]);
|
const [tuitionFees, setTuitionFees] = useState([]);
|
||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
@ -32,8 +33,11 @@ export default function Page() {
|
|||||||
// Fetch data for schedules
|
// Fetch data for schedules
|
||||||
handleSchedules();
|
handleSchedules();
|
||||||
|
|
||||||
// Fetch data for discounts
|
// Fetch data for registration discounts
|
||||||
handleDiscounts();
|
handleRegistrationDiscounts();
|
||||||
|
|
||||||
|
// Fetch data for tuition discounts
|
||||||
|
handleTuitionDiscounts();
|
||||||
|
|
||||||
// Fetch data for registration fees
|
// Fetch data for registration fees
|
||||||
handleRegistrationFees();
|
handleRegistrationFees();
|
||||||
@ -74,12 +78,20 @@ export default function Page() {
|
|||||||
.catch(error => console.error('Error fetching schedules:', error));
|
.catch(error => console.error('Error fetching schedules:', error));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDiscounts = () => {
|
const handleRegistrationDiscounts = () => {
|
||||||
fetchDiscounts()
|
fetchRegistrationDiscounts()
|
||||||
.then(data => {
|
.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 = () => {
|
const handleRegistrationFees = () => {
|
||||||
@ -236,8 +248,10 @@ export default function Page() {
|
|||||||
label: 'Tarifications',
|
label: 'Tarifications',
|
||||||
content: (
|
content: (
|
||||||
<FeesManagement
|
<FeesManagement
|
||||||
discounts={discounts}
|
registrationDiscounts={registrationDiscounts}
|
||||||
setDiscounts={setDiscounts}
|
setRegistrationDiscounts={setRegistrationDiscounts}
|
||||||
|
tuitionDiscounts={tuitionDiscounts}
|
||||||
|
setTuitionDiscounts={setTuitionDiscounts}
|
||||||
registrationFees={registrationFees}
|
registrationFees={registrationFees}
|
||||||
setRegistrationFees={setRegistrationFees}
|
setRegistrationFees={setRegistrationFees}
|
||||||
tuitionFees={tuitionFees}
|
tuitionFees={tuitionFees}
|
||||||
|
|||||||
@ -40,8 +40,13 @@ export const fetchSchedules = () => {
|
|||||||
.then(requestResponseHandler)
|
.then(requestResponseHandler)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchDiscounts = () => {
|
export const fetchRegistrationDiscounts = () => {
|
||||||
return fetch(`${BE_SCHOOL_DISCOUNTS_URL}`)
|
return fetch(`${BE_SCHOOL_DISCOUNTS_URL}/registration`)
|
||||||
|
.then(requestResponseHandler)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTuitionDiscounts = () => {
|
||||||
|
return fetch(`${BE_SCHOOL_DISCOUNTS_URL}/tuition`)
|
||||||
.then(requestResponseHandler)
|
.then(requestResponseHandler)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
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 Table from '@/components/Table';
|
||||||
import InputTextIcon from '@/components/InputTextIcon';
|
import InputTextIcon from '@/components/InputTextIcon';
|
||||||
import Popup from '@/components/Popup';
|
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 [editingDiscount, setEditingDiscount] = useState(null);
|
||||||
const [newDiscount, setNewDiscount] = useState(null);
|
const [newDiscount, setNewDiscount] = useState(null);
|
||||||
const [formData, setFormData] = useState({});
|
const [formData, setFormData] = useState({});
|
||||||
@ -13,7 +13,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
|||||||
const [popupMessage, setPopupMessage] = useState("");
|
const [popupMessage, setPopupMessage] = useState("");
|
||||||
|
|
||||||
const handleAddDiscount = () => {
|
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) => {
|
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);
|
const discount = discounts.find(discount => discount.id === id);
|
||||||
if (!discount) return;
|
if (!discount) return;
|
||||||
|
|
||||||
const updatedData = {
|
const updatedData = {
|
||||||
...discount,
|
...discount,
|
||||||
discount_type: newType
|
discount_type: discount.discount_type === 0 ? 1 : 0
|
||||||
};
|
};
|
||||||
|
|
||||||
handleEdit(id, updatedData)
|
handleEdit(id, updatedData)
|
||||||
@ -87,9 +87,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
|||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
if (name === 'discountType') {
|
if (editingDiscount) {
|
||||||
setDiscountType(value);
|
|
||||||
} else if (editingDiscount) {
|
|
||||||
setFormData((prevData) => ({
|
setFormData((prevData) => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
@ -124,8 +122,8 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
|||||||
switch (column) {
|
switch (column) {
|
||||||
case 'LIBELLE':
|
case 'LIBELLE':
|
||||||
return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction');
|
return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction');
|
||||||
case 'VALEUR':
|
case 'REMISE':
|
||||||
return renderInputField('amount', currentData.amount, handleChange, discount.discount_type === 0 ? 'Montant' : 'Pourcentage');
|
return renderInputField('amount', currentData.amount, handleChange,'Montant');
|
||||||
case 'DESCRIPTION':
|
case 'DESCRIPTION':
|
||||||
return renderInputField('description', currentData.description, handleChange, 'Description');
|
return renderInputField('description', currentData.description, handleChange, 'Description');
|
||||||
case 'ACTIONS':
|
case 'ACTIONS':
|
||||||
@ -154,32 +152,22 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
|||||||
switch (column) {
|
switch (column) {
|
||||||
case 'LIBELLE':
|
case 'LIBELLE':
|
||||||
return discount.name;
|
return discount.name;
|
||||||
case 'VALEUR':
|
case 'REMISE':
|
||||||
return discount.discount_type === 0 ? `${discount.amount} €` : `${discount.amount} %`;
|
return discount.discount_type === 0 ? `${discount.amount} €` : `${discount.amount} %`;
|
||||||
case 'TYPE DE REMISE':
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center space-y-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleToggleDiscountType(discount.id, 0)}
|
|
||||||
className={`text-${discount.discount_type === 0 ? 'emerald' : 'gray'}-500 hover:text-${discount.discount_type === 0 ? 'emerald' : 'gray'}-700`}
|
|
||||||
>
|
|
||||||
<EuroIcon className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleToggleDiscountType(discount.id, 1)}
|
|
||||||
className={`text-${discount.discount_type === 1 ? 'emerald' : 'gray'}-500 hover:text-${discount.discount_type === 1 ? 'emerald' : 'gray'}-700`}
|
|
||||||
>
|
|
||||||
<Percent className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 'DESCRIPTION':
|
case 'DESCRIPTION':
|
||||||
return discount.description;
|
return discount.description;
|
||||||
|
case 'MISE A JOUR':
|
||||||
|
return discount.updated_at_formatted;
|
||||||
case 'ACTIONS':
|
case 'ACTIONS':
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleToggleDiscountType(discount.id)}
|
||||||
|
className="flex justify-center items-center text-emerald-500 hover:text-emerald-700"
|
||||||
|
>
|
||||||
|
{discount.discount_type === 0 ? <EuroIcon className="w-5 h-5" /> : <Percent className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditingDiscount(discount.id) || setFormData(discount)}
|
onClick={() => setEditingDiscount(discount.id) || setFormData(discount)}
|
||||||
@ -205,7 +193,10 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-xl font-semibold">Réductions</h2>
|
<div className="flex items-center mb-4">
|
||||||
|
<Tag className="w-6 h-6 text-emerald-500 mr-2" />
|
||||||
|
<h2 className="text-xl font-semibold">Réductions {type === 0 ? 'd\'inscription' : 'de scolarité'}</h2>
|
||||||
|
</div>
|
||||||
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700">
|
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700">
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -214,12 +205,13 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
|||||||
data={newDiscount ? [newDiscount, ...discounts] : discounts}
|
data={newDiscount ? [newDiscount, ...discounts] : discounts}
|
||||||
columns={[
|
columns={[
|
||||||
{ name: 'LIBELLE', label: 'Libellé' },
|
{ name: 'LIBELLE', label: 'Libellé' },
|
||||||
{ name: 'VALEUR', label: 'Valeur' },
|
{ name: 'REMISE', label: 'Valeur' },
|
||||||
{ name: 'TYPE DE REMISE', label: 'Type de remise' },
|
|
||||||
{ name: 'DESCRIPTION', label: 'Description' },
|
{ name: 'DESCRIPTION', label: 'Description' },
|
||||||
|
{ name: 'MISE A JOUR', label: 'date mise à jour' },
|
||||||
{ name: 'ACTIONS', label: 'Actions' }
|
{ name: 'ACTIONS', label: 'Actions' }
|
||||||
]}
|
]}
|
||||||
renderCell={renderDiscountCell}
|
renderCell={renderDiscountCell}
|
||||||
|
defaultTheme='bg-yellow-100'
|
||||||
/>
|
/>
|
||||||
<Popup
|
<Popup
|
||||||
visible={popupVisible}
|
visible={popupVisible}
|
||||||
|
|||||||
@ -1,43 +1,78 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import RegistrationFeesSection from '@/components/Structure/Configuration/RegistrationFeesSection';
|
import FeesSection from '@/components/Structure/Configuration/FeesSection';
|
||||||
import DiscountsSection from '@/components/Structure/Configuration/DiscountsSection';
|
import DiscountsSection from '@/components/Structure/Configuration/DiscountsSection';
|
||||||
import TuitionFeesSection from '@/components/Structure/Configuration/TuitionFeesSection';
|
|
||||||
import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL } from '@/utils/Url';
|
import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL } from '@/utils/Url';
|
||||||
|
|
||||||
const FeesManagement = ({ discounts, setDiscounts, registrationFees, setRegistrationFees, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => {
|
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 (
|
return (
|
||||||
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-6">
|
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-6">
|
||||||
<div className="p-4 bg-white rounded-lg shadow-md">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<DiscountsSection
|
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||||
discounts={discounts}
|
<FeesSection
|
||||||
setDiscounts={setDiscounts}
|
fees={registrationFees}
|
||||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts)}
|
setFees={setRegistrationFees}
|
||||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts)}
|
discounts={registrationDiscounts}
|
||||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 bg-white rounded-lg shadow-md">
|
|
||||||
<RegistrationFeesSection
|
|
||||||
registrationFees={registrationFees}
|
|
||||||
setRegistrationFees={setRegistrationFees}
|
|
||||||
discounts={discounts}
|
|
||||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setRegistrationFees)}
|
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setRegistrationFees)}
|
||||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setRegistrationFees)}
|
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setRegistrationFees)}
|
||||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setRegistrationFees)}
|
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setRegistrationFees)}
|
||||||
|
type={0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-white rounded-lg shadow-md">
|
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||||
<TuitionFeesSection
|
<DiscountsSection
|
||||||
tuitionFees={tuitionFees}
|
discounts={registrationDiscounts}
|
||||||
setTuitionFees={setTuitionFees}
|
setDiscounts={setRegistrationDiscounts}
|
||||||
discounts={discounts}
|
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setRegistrationDiscounts)}
|
||||||
registrationFees={registrationFees}
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||||
|
<FeesSection
|
||||||
|
fees={tuitionFees}
|
||||||
|
setFees={setTuitionFees}
|
||||||
|
discounts={tuitionDiscounts}
|
||||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setTuitionFees)}
|
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setTuitionFees)}
|
||||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setTuitionFees)}
|
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setTuitionFees)}
|
||||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setTuitionFees)}
|
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setTuitionFees)}
|
||||||
|
type={1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||||
|
<DiscountsSection
|
||||||
|
discounts={tuitionDiscounts}
|
||||||
|
setDiscounts={setTuitionDiscounts}
|
||||||
|
handleCreate={(newData) => 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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
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 Table from '@/components/Table';
|
||||||
import InputTextIcon from '@/components/InputTextIcon';
|
import InputTextIcon from '@/components/InputTextIcon';
|
||||||
import Popup from '@/components/Popup';
|
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 [editingFee, setEditingFee] = useState(null);
|
||||||
const [newFee, setNewFee] = useState(null);
|
const [newFee, setNewFee] = useState(null);
|
||||||
const [formData, setFormData] = useState({});
|
const [formData, setFormData] = useState({});
|
||||||
@ -13,30 +12,27 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
const [popupMessage, setPopupMessage] = useState("");
|
const [popupMessage, setPopupMessage] = useState("");
|
||||||
|
|
||||||
const paymentOptions = [
|
|
||||||
{ value: 0, label: '1 fois' },
|
|
||||||
{ value: 1, label: '4 fois' },
|
|
||||||
{ value: 2, label: '10 fois' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleAddFee = () => {
|
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) => {
|
const handleRemoveFee = (id) => {
|
||||||
handleDelete(id);
|
handleDelete(id)
|
||||||
|
.then(() => {
|
||||||
|
setFees(prevFees => prevFees.filter(fee => fee.id !== id));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveNewFee = () => {
|
const handleSaveNewFee = () => {
|
||||||
if (newFee.name && newFee.base_amount) {
|
if (
|
||||||
const feeData = {
|
newFee.name &&
|
||||||
...newFee,
|
newFee.base_amount) {
|
||||||
type: 0
|
handleCreate(newFee)
|
||||||
};
|
|
||||||
|
|
||||||
handleCreate(feeData)
|
|
||||||
.then((createdFee) => {
|
.then((createdFee) => {
|
||||||
setRegistrationFees([createdFee, ...registrationFees]);
|
setFees([createdFee, ...fees]);
|
||||||
setNewFee(null);
|
setNewFee(null);
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
})
|
})
|
||||||
@ -48,15 +44,18 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setPopupMessage("Tous les champs doivent être remplis");
|
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||||
setPopupVisible(true);
|
setPopupVisible(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateFee = (id, updatedFee) => {
|
const handleUpdateFee = (id, updatedFee) => {
|
||||||
if (updatedFee.name && updatedFee.base_amount) {
|
if (
|
||||||
|
updatedFee.name &&
|
||||||
|
updatedFee.base_amount) {
|
||||||
handleEdit(id, updatedFee)
|
handleEdit(id, updatedFee)
|
||||||
.then(() => {
|
.then((updatedFee) => {
|
||||||
|
setFees(fees.map(fee => fee.id === id ? updatedFee : fee));
|
||||||
setEditingFee(null);
|
setEditingFee(null);
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
})
|
})
|
||||||
@ -68,13 +67,13 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setPopupMessage("Tous les champs doivent être remplis");
|
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||||
setPopupVisible(true);
|
setPopupVisible(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleActive = (id, isActive) => {
|
const handleToggleActive = (id, isActive) => {
|
||||||
const fee = registrationFees.find(fee => fee.id === id);
|
const fee = fees.find(fee => fee.id === id);
|
||||||
if (!fee) return;
|
if (!fee) return;
|
||||||
|
|
||||||
const updatedData = {
|
const updatedData = {
|
||||||
@ -84,7 +83,7 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
|
|
||||||
handleEdit(id, updatedData)
|
handleEdit(id, updatedData)
|
||||||
.then(() => {
|
.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 => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -110,6 +109,19 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 calculateFinalAmount = (baseAmount, discountIds) => {
|
const calculateFinalAmount = (baseAmount, discountIds) => {
|
||||||
const totalDiscounts = discountIds.reduce((sum, discountId) => {
|
const totalDiscounts = discountIds.reduce((sum, discountId) => {
|
||||||
const discount = discounts.find(d => d.id === discountId);
|
const discount = discounts.find(d => d.id === discountId);
|
||||||
@ -128,32 +140,6 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
return finalAmount.toFixed(2);
|
return finalAmount.toFixed(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderInputField = (field, value, onChange, placeholder) => (
|
|
||||||
<div>
|
|
||||||
<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 renderSelectField = (field, value, options, callback, label) => (
|
|
||||||
<div className="flex justify-center items-center h-full">
|
|
||||||
<SelectChoice
|
|
||||||
name={field}
|
|
||||||
selected={value}
|
|
||||||
options={options}
|
|
||||||
callback={callback}
|
|
||||||
placeHolder={label}
|
|
||||||
choices={options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderFeeCell = (fee, column) => {
|
const renderFeeCell = (fee, column) => {
|
||||||
const isEditing = editingFee === fee.id;
|
const isEditing = editingFee === fee.id;
|
||||||
const isCreating = newFee && newFee.id === fee.id;
|
const isCreating = newFee && newFee.id === fee.id;
|
||||||
@ -161,19 +147,15 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
|
|
||||||
if (isEditing || isCreating) {
|
if (isEditing || isCreating) {
|
||||||
switch (column) {
|
switch (column) {
|
||||||
case 'LIBELLE':
|
case 'NOM':
|
||||||
return renderInputField('name', currentData.name, handleChange, 'Libellé du frais');
|
return renderInputField('name', currentData.name, handleChange, 'Nom des frais');
|
||||||
case 'MONTANT DE BASE':
|
case 'MONTANT':
|
||||||
return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant');
|
return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base');
|
||||||
case 'DESCRIPTION':
|
case 'DESCRIPTION':
|
||||||
return renderInputField('description', currentData.description, handleChange, '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':
|
case 'ACTIONS':
|
||||||
return (
|
return (
|
||||||
<div className="flex space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => (isEditing ? handleUpdateFee(editingFee, formData) : handleSaveNewFee())}
|
onClick={() => (isEditing ? handleUpdateFee(editingFee, formData) : handleSaveNewFee())}
|
||||||
@ -195,25 +177,24 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (column) {
|
switch (column) {
|
||||||
case 'LIBELLE':
|
case 'NOM':
|
||||||
return fee.name;
|
return fee.name;
|
||||||
case 'MONTANT DE BASE':
|
case 'MONTANT':
|
||||||
return fee.base_amount + ' €';
|
return fee.base_amount + ' €';
|
||||||
|
case 'MISE A JOUR':
|
||||||
|
return fee.updated_at_formatted;
|
||||||
case 'DESCRIPTION':
|
case 'DESCRIPTION':
|
||||||
return fee.description;
|
return fee.description;
|
||||||
case 'OPTIONS DE PAIEMENT':
|
|
||||||
return paymentOptions.find(option => option.value === fee.payment_option)?.label || '';
|
|
||||||
case 'REMISES':
|
|
||||||
const discountNames = fee.discounts
|
|
||||||
.map(discountId => discounts.find(discount => discount.id === discountId)?.name)
|
|
||||||
.filter(name => name)
|
|
||||||
.join(', ');
|
|
||||||
return discountNames;
|
|
||||||
case 'MONTANT FINAL':
|
|
||||||
return calculateFinalAmount(fee.base_amount, fee.discounts) + ' €';
|
|
||||||
case 'ACTIONS':
|
case 'ACTIONS':
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleToggleActive(fee.id, fee.is_active)}
|
||||||
|
className={`text-${fee.is_active ? 'green' : 'orange'}-500 hover:text-${fee.is_active ? 'green' : 'orange'}-700`}
|
||||||
|
>
|
||||||
|
{fee.is_active ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditingFee(fee.id) || setFormData(fee)}
|
onClick={() => setEditingFee(fee.id) || setFormData(fee)}
|
||||||
@ -228,13 +209,6 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
>
|
>
|
||||||
<Trash className="w-5 h-5" />
|
<Trash className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleToggleActive(fee.id, fee.is_active)}
|
|
||||||
className={`text-${fee.is_active ? 'gray' : 'green'}-500 hover:text-${fee.is_active ? 'gray' : 'green'}-700`}
|
|
||||||
>
|
|
||||||
{fee.is_active ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@ -244,28 +218,27 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-xl font-semibold">Frais d'inscription</h2>
|
<div className="flex items-center mb-4">
|
||||||
|
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" />
|
||||||
|
<h2 className="text-xl font-semibold">{type === 0 ? 'Frais d\'inscription' : 'Frais de scolarité'}</h2>
|
||||||
|
</div>
|
||||||
<button type="button" onClick={handleAddFee} className="text-emerald-500 hover:text-emerald-700">
|
<button type="button" onClick={handleAddFee} className="text-emerald-500 hover:text-emerald-700">
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
data={newFee ? [newFee, ...registrationFees] : registrationFees}
|
data={newFee ? [newFee, ...fees] : fees}
|
||||||
columns={[
|
columns={[
|
||||||
{ name: 'LIBELLE', label: 'Libellé' },
|
{ name: 'NOM', label: 'Nom' },
|
||||||
{ name: 'MONTANT DE BASE', label: 'Montant' },
|
{ name: 'MONTANT', label: 'Montant de base' },
|
||||||
{ name: 'DESCRIPTION', label: 'Description' },
|
{ name: 'DESCRIPTION', label: 'Description' },
|
||||||
{ name: 'OPTIONS DE PAIEMENT', label: 'Options de paiement' },
|
{ name: 'MISE A JOUR', label: 'date mise à jour' },
|
||||||
{ name: 'REMISES', label: 'Remises' },
|
|
||||||
{ name: 'MONTANT FINAL', label: 'Montant final' },
|
|
||||||
{ name: 'ACTIONS', label: 'Actions' }
|
{ name: 'ACTIONS', label: 'Actions' }
|
||||||
]}
|
]}
|
||||||
renderCell={renderFeeCell}
|
renderCell={renderFeeCell}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<Popup
|
<Popup
|
||||||
visible={popupVisible}
|
visible={popupVisible}
|
||||||
message={popupMessage}
|
message={popupMessage}
|
||||||
@ -273,8 +246,8 @@ const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discou
|
|||||||
onCancel={() => setPopupVisible(false)}
|
onCancel={() => setPopupVisible(false)}
|
||||||
uniqueConfirmButton={true}
|
uniqueConfirmButton={true}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RegistrationFeesSection;
|
export default FeesSection;
|
||||||
@ -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) => (
|
|
||||||
<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 renderSelectField = (field, value, options, callback, label) => (
|
|
||||||
<div className="flex justify-center items-center h-full">
|
|
||||||
<SelectChoice
|
|
||||||
name={field}
|
|
||||||
selected={value}
|
|
||||||
options={options}
|
|
||||||
callback={callback}
|
|
||||||
placeHolder={label}
|
|
||||||
choices={options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<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 '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>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleToggleActive(tuitionFee.id, tuitionFee.is_active)}
|
|
||||||
className={`text-${tuitionFee.is_active ? 'gray' : 'green'}-500 hover:text-${tuitionFee.is_active ? 'gray' : 'green'}-700`}
|
|
||||||
>
|
|
||||||
{tuitionFee.is_active ? <EyeOff className="w-5 h-5" /> : <Eye 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: '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;
|
|
||||||
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Pagination from '@/components/Pagination'; // Correction du chemin d'importatio,
|
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) => {
|
const handlePageChange = (newPage) => {
|
||||||
onPageChange(newPage);
|
onPageChange(newPage);
|
||||||
};
|
};
|
||||||
@ -25,7 +25,7 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
|
|||||||
key={rowIndex}
|
key={rowIndex}
|
||||||
className={`
|
className={`
|
||||||
${isSelectable ? 'cursor-pointer' : ''}
|
${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' : ''}
|
${isSelectable ? 'hover:bg-emerald-600' : ''}
|
||||||
`}
|
`}
|
||||||
onClick={() => isSelectable && onRowClick && onRowClick(row)}
|
onClick={() => isSelectable && onRowClick && onRowClick(row)}
|
||||||
|
|||||||
Reference in New Issue
Block a user