feat: Harmonisation des fees / ajout de type de réduction / mise à jour

du calcul [#18]
This commit is contained in:
N3WT DE COMPET
2025-01-21 20:39:36 +01:00
parent 8d1a41e269
commit 5462306a60
11 changed files with 169 additions and 194 deletions

View File

@ -73,40 +73,33 @@ class PaymentOptions(models.IntegerChoices):
FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois') FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois')
TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois') TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois')
class DiscountType(models.IntegerChoices):
CURRENCY = 0, 'Currency'
PERCENT = 1, 'Percent'
class FeeType(models.IntegerChoices):
REGISTRATION_FEE = 0, 'Registration Fee'
TUITION_FEE = 1, 'Tuition Fee'
class Discount(models.Model): class Discount(models.Model):
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
amount = models.DecimalField(max_digits=10, decimal_places=2) 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)
def __str__(self): def __str__(self):
return self.name return self.name
class Fee(models.Model): class Fee(models.Model):
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
description = models.TextField(blank=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)
currency = models.CharField(max_length=3, default='EUR') 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)
payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT)
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)
def __str__(self): def __str__(self):
return self.name return self.name
class TuitionFee(models.Model):
name = models.CharField(max_length=255, unique=True)
description = models.TextField(blank=True)
base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
currency = models.CharField(max_length=3, default='EUR')
discounts = models.ManyToManyField('Discount', blank=True)
payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT)
is_active = models.BooleanField(default=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
def clean(self):
if self.validity_end_date <= self.validity_start_date:
raise ValidationError(_('La date de fin de validité doit être après la date de début de validité.'))

View File

@ -1,5 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, TuitionFee, Fee from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee
from Subscriptions.models import RegistrationForm from Subscriptions.models import RegistrationForm
from Subscriptions.serializers import StudentSerializer from Subscriptions.serializers import StudentSerializer
from Auth.serializers import ProfileSerializer from Auth.serializers import ProfileSerializer
@ -208,42 +208,7 @@ class FeeSerializer(serializers.ModelSerializer):
instance.payment_option = validated_data.get('payment_option', instance.payment_option) 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.save() instance.type = validated_data.get('type', instance.type)
# Update discounts if provided
instance.discounts.set(discounts_data)
return instance
class TuitionFeeSerializer(serializers.ModelSerializer):
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True)
class Meta:
model = TuitionFee
fields = '__all__'
def create(self, validated_data):
discounts_data = validated_data.pop('discounts', [])
# Create the TuitionFee instance
tuition_fee = TuitionFee.objects.create(**validated_data)
# Add discounts if provided
tuition_fee.discounts.set(discounts_data)
return tuition_fee
def update(self, instance, validated_data):
discounts_data = validated_data.pop('discounts', [])
# Update the TuitionFee instance
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.save() instance.save()
# Update discounts if provided # Update discounts if provided

View File

@ -11,8 +11,6 @@ from School.views import (
PlanningView, PlanningView,
FeesView, FeesView,
FeeView, FeeView,
TuitionFeesView,
TuitionFeeView,
DiscountsView, DiscountsView,
DiscountView, DiscountView,
) )
@ -34,14 +32,10 @@ urlpatterns = [
re_path(r'^planning$', PlanningView.as_view(), name="planning"), re_path(r'^planning$', PlanningView.as_view(), name="planning"),
re_path(r'^planning/([0-9]+)$', PlanningView.as_view(), name="planning"), re_path(r'^planning/([0-9]+)$', PlanningView.as_view(), name="planning"),
re_path(r'^fees$', FeesView.as_view(), name="fees"), re_path(r'^fees/(?P<_filter>[a-zA-z]+)$', FeesView.as_view(), name="fees"),
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'^tuitionFees$', TuitionFeesView.as_view(), name="tuitionFees"),
re_path(r'^tuitionFee$', TuitionFeeView.as_view(), name="tuitionFee"),
re_path(r'^tuitionFee/([0-9]+)$', TuitionFeeView.as_view(), name="tuitionFee"),
re_path(r'^discounts$', DiscountsView.as_view(), name="discounts"), re_path(r'^discounts$', 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"),

View File

@ -5,8 +5,8 @@ from rest_framework.parsers import JSONParser
from rest_framework.views import APIView from rest_framework.views import APIView
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from .models import Teacher, Speciality, SchoolClass, Planning, Discount, TuitionFee, Fee from .models import Teacher, Speciality, SchoolClass, Planning, Discount, Fee
from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, TuitionFeeSerializer, FeeSerializer from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, FeeSerializer
from N3wtSchool import bdd from N3wtSchool import bdd
from N3wtSchool.bdd import delete_object, getAllObjects, getObject from N3wtSchool.bdd import delete_object, getAllObjects, getObject
@ -267,18 +267,16 @@ class PlanningView(APIView):
@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):
def get(self, request): def get(self, request, _filter, *args, **kwargs):
feesList = Fee.objects.all()
fees_serializer = FeeSerializer(feesList, many=True)
return JsonResponse(fees_serializer.data, safe=False)
def post(self, request): if _filter not in ['registration', 'tuition']:
fee_data = JSONParser().parse(request) return JsonResponse({"error": "Invalid type parameter. Must be 'registration' or 'tuition'."}, safe=False, status=400)
fee_serializer = FeeSerializer(data=fee_data)
if fee_serializer.is_valid(): fee_type_value = 0 if _filter == 'registration' else 1
fee_serializer.save() fees = Fee.objects.filter(type=fee_type_value)
return JsonResponse(fee_serializer.data, safe=False, status=201) fee_serializer = FeeSerializer(fees, many=True)
return JsonResponse(fee_serializer.errors, safe=False, status=400)
return JsonResponse(fee_serializer.data, safe=False, status=200)
@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')
@ -313,46 +311,3 @@ class FeeView(APIView):
def delete(self, request, _id): def delete(self, request, _id):
return delete_object(Fee, _id) return delete_object(Fee, _id)
# Vues pour les frais de scolarité (TuitionFee)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class TuitionFeesView(APIView):
def get(self, request):
tuitionFeesList = TuitionFee.objects.all()
tuitionFees_serializer = TuitionFeeSerializer(tuitionFeesList, many=True)
return JsonResponse(tuitionFees_serializer.data, safe=False)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class TuitionFeeView(APIView):
def get(self, request, _id):
try:
tuitionFee = TuitionFee.objects.get(id=_id)
tuitionFee_serializer = TuitionFeeSerializer(tuitionFee)
return JsonResponse(tuitionFee_serializer.data, safe=False)
except TuitionFee.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=404)
def post(self, request):
tuitionFee_data = JSONParser().parse(request)
tuitionFee_serializer = TuitionFeeSerializer(data=tuitionFee_data)
if tuitionFee_serializer.is_valid():
tuitionFee_serializer.save()
return JsonResponse(tuitionFee_serializer.data, safe=False, status=201)
return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400)
def put(self, request, _id):
tuitionFee_data = JSONParser().parse(request)
try:
tuitionFee = TuitionFee.objects.get(id=_id)
except TuitionFee.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=404)
tuitionFee_serializer = TuitionFeeSerializer(tuitionFee, data=tuitionFee_data, partial=True) # Utilisation de partial=True
if tuitionFee_serializer.is_valid():
tuitionFee_serializer.save()
return JsonResponse(tuitionFee_serializer.data, safe=False)
return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400)
def delete(self, request, _id):
return delete_object(TuitionFee, _id)

View File

@ -1,20 +1,19 @@
'use client' 'use client'
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { School, Calendar, DollarSign } from 'lucide-react'; // Import de l'icône DollarSign
import StructureManagement from '@/components/Structure/Configuration/StructureManagement'; import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'; import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
import FeesManagement from '@/components/Structure/Configuration/FeesManagement'; 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, fetchFees, fetchTuitionFees } from '@/app/lib/schoolAction'; import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchDiscounts, 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 [fees, setFees] = useState([]); const [registrationFees, setRegistrationFees] = useState([]);
const [discounts, setDiscounts] = useState([]); const [discounts, setDiscounts] = useState([]);
const [tuitionFees, setTuitionFees] = useState([]); const [tuitionFees, setTuitionFees] = useState([]);
@ -33,13 +32,13 @@ export default function Page() {
// Fetch data for schedules // Fetch data for schedules
handleSchedules(); handleSchedules();
// Fetch data for fees
handleFees();
// Fetch data for discounts // Fetch data for discounts
handleDiscounts(); handleDiscounts();
// Fetch data for TuitionFee // Fetch data for registration fees
handleRegistrationFees();
// Fetch data for tuition fees
handleTuitionFees(); handleTuitionFees();
}, []); }, []);
@ -75,14 +74,6 @@ export default function Page() {
.catch(error => console.error('Error fetching schedules:', error)); .catch(error => console.error('Error fetching schedules:', error));
}; };
const handleFees = () => {
fetchFees()
.then(data => {
setFees(data);
})
.catch(error => console.error('Error fetching fees:', error));
};
const handleDiscounts = () => { const handleDiscounts = () => {
fetchDiscounts() fetchDiscounts()
.then(data => { .then(data => {
@ -91,6 +82,14 @@ export default function Page() {
.catch(error => console.error('Error fetching discounts:', error)); .catch(error => console.error('Error fetching discounts:', error));
}; };
const handleRegistrationFees = () => {
fetchRegistrationFees()
.then(data => {
setRegistrationFees(data);
})
.catch(error => console.error('Error fetching registration fees:', error));
};
const handleTuitionFees = () => { const handleTuitionFees = () => {
fetchTuitionFees() fetchTuitionFees()
.then(data => { .then(data => {
@ -237,10 +236,10 @@ export default function Page() {
label: 'Tarifications', label: 'Tarifications',
content: ( content: (
<FeesManagement <FeesManagement
fees={fees}
setFees={setFees}
discounts={discounts} discounts={discounts}
setDiscounts={setDiscounts} setDiscounts={setDiscounts}
registrationFees={registrationFees}
setRegistrationFees={setRegistrationFees}
tuitionFees={tuitionFees} tuitionFees={tuitionFees}
setTuitionFees={setTuitionFees} setTuitionFees={setTuitionFees}
handleCreate={handleCreate} handleCreate={handleCreate}

View File

@ -4,8 +4,7 @@ import {
BE_SCHOOL_SCHOOLCLASSES_URL, BE_SCHOOL_SCHOOLCLASSES_URL,
BE_SCHOOL_PLANNINGS_URL, BE_SCHOOL_PLANNINGS_URL,
BE_SCHOOL_FEES_URL, BE_SCHOOL_FEES_URL,
BE_SCHOOL_DISCOUNTS_URL, BE_SCHOOL_DISCOUNTS_URL
BE_SCHOOL_TUITION_FEES_URL
} from '@/utils/Url'; } from '@/utils/Url';
const requestResponseHandler = async (response) => { const requestResponseHandler = async (response) => {
@ -46,12 +45,12 @@ export const fetchDiscounts = () => {
.then(requestResponseHandler) .then(requestResponseHandler)
}; };
export const fetchFees = () => { export const fetchRegistrationFees = () => {
return fetch(`${BE_SCHOOL_FEES_URL}`) return fetch(`${BE_SCHOOL_FEES_URL}/registration`)
.then(requestResponseHandler) .then(requestResponseHandler)
}; };
export const fetchTuitionFees = () => { export const fetchTuitionFees = () => {
return fetch(`${BE_SCHOOL_TUITION_FEES_URL}`) return fetch(`${BE_SCHOOL_FEES_URL}/tuition`)
.then(requestResponseHandler) .then(requestResponseHandler)
}; };

View File

@ -1,10 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Trash, Edit3, Check, X } from 'lucide-react'; import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon } 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, handleCreate, handleEdit, handleDelete }) => { const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, errors }) => {
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,17 +13,24 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete })
const [popupMessage, setPopupMessage] = useState(""); const [popupMessage, setPopupMessage] = useState("");
const handleAddDiscount = () => { const handleAddDiscount = () => {
setNewDiscount({ id: Date.now(), name: '', amount: '', description: '' }); setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discountType: 'amount' });
}; };
const handleRemoveDiscount = (id) => { const handleRemoveDiscount = (id) => {
handleDelete(id) handleDelete(id)
.then(() => {
setDiscounts(prevDiscounts => prevDiscounts.filter(discount => discount.id !== id));
})
.catch(error => {
console.error(error);
});
}; };
const handleSaveNewDiscount = () => { const handleSaveNewDiscount = () => {
if (newDiscount.name && newDiscount.amount) { if (newDiscount.name && newDiscount.amount) {
handleCreate(newDiscount) handleCreate(newDiscount)
.then(() => { .then((createdDiscount) => {
setDiscounts([createdDiscount, ...discounts]);
setNewDiscount(null); setNewDiscount(null);
setLocalErrors({}); setLocalErrors({});
}) })
@ -60,9 +67,29 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete })
} }
}; };
const handleToggleDiscountType = (id, newType) => {
const discount = discounts.find(discount => discount.id === id);
if (!discount) return;
const updatedData = {
...discount,
discount_type: newType
};
handleEdit(id, updatedData)
.then(() => {
setDiscounts(prevDiscounts => prevDiscounts.map(discount => discount.id === id ? { ...discount, discount_type: updatedData.discount_type } : discount));
})
.catch(error => {
console.error(error);
});
};
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
if (editingDiscount) { if (name === 'discountType') {
setDiscountType(value);
} else if (editingDiscount) {
setFormData((prevData) => ({ setFormData((prevData) => ({
...prevData, ...prevData,
[name]: value, [name]: value,
@ -97,13 +124,13 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete })
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 'MONTANT': case 'VALEUR':
return renderInputField('amount', currentData.amount, handleChange, 'Montant'); return renderInputField('amount', currentData.amount, handleChange, discount.discount_type === 0 ? 'Montant' : 'Pourcentage');
case 'DESCRIPTION': case 'DESCRIPTION':
return renderInputField('description', currentData.description, handleChange, 'Description'); return renderInputField('description', currentData.description, handleChange, 'Description');
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 ? handleUpdateDiscount(editingDiscount, formData) : handleSaveNewDiscount())} onClick={() => (isEditing ? handleUpdateDiscount(editingDiscount, formData) : handleSaveNewDiscount())}
@ -127,8 +154,27 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete })
switch (column) { switch (column) {
case 'LIBELLE': case 'LIBELLE':
return discount.name; return discount.name;
case 'MONTANT': case 'VALEUR':
return 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 'ACTIONS': case 'ACTIONS':
@ -168,7 +214,8 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete })
data={newDiscount ? [newDiscount, ...discounts] : discounts} data={newDiscount ? [newDiscount, ...discounts] : discounts}
columns={[ columns={[
{ name: 'LIBELLE', label: 'Libellé' }, { name: 'LIBELLE', label: 'Libellé' },
{ name: 'MONTANT', label: 'Montant' }, { name: 'VALEUR', label: 'Valeur' },
{ name: 'TYPE DE REMISE', label: 'Type de remise' },
{ name: 'DESCRIPTION', label: 'Description' }, { name: 'DESCRIPTION', label: 'Description' },
{ name: 'ACTIONS', label: 'Actions' } { name: 'ACTIONS', label: 'Actions' }
]} ]}

View File

@ -1,29 +1,30 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import FeesSection from './FeesSection'; import RegistrationFeesSection from '@/components/Structure/Configuration/RegistrationFeesSection';
import DiscountsSection from './DiscountsSection'; import DiscountsSection from '@/components/Structure/Configuration/DiscountsSection';
import TuitionFeesSection from './TuitionFeesSection'; import TuitionFeesSection from '@/components/Structure/Configuration/TuitionFeesSection';
import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL, BE_SCHOOL_TUITION_FEE_URL } from '@/utils/Url'; import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL } from '@/utils/Url';
const FeesManagement = ({ fees, setFees, discounts, setDiscounts, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { const FeesManagement = ({ discounts, setDiscounts, registrationFees, setRegistrationFees, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => {
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="p-4 bg-white rounded-lg shadow-md">
<DiscountsSection <DiscountsSection
discounts={discounts} discounts={discounts}
setDiscounts={setDiscounts}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts)} handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)} handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)}
/> />
</div> </div>
<div className="p-4 bg-white rounded-lg shadow-md"> <div className="p-4 bg-white rounded-lg shadow-md">
<FeesSection <RegistrationFeesSection
fees={fees} registrationFees={registrationFees}
setFees={setFees} setRegistrationFees={setRegistrationFees}
discounts={discounts} discounts={discounts}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees)} handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setRegistrationFees)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setRegistrationFees)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)} handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setRegistrationFees)}
/> />
</div> </div>
<div className="p-4 bg-white rounded-lg shadow-md"> <div className="p-4 bg-white rounded-lg shadow-md">
@ -31,10 +32,10 @@ const FeesManagement = ({ fees, setFees, discounts, setDiscounts, tuitionFees, s
tuitionFees={tuitionFees} tuitionFees={tuitionFees}
setTuitionFees={setTuitionFees} setTuitionFees={setTuitionFees}
discounts={discounts} discounts={discounts}
fees={fees} registrationFees={registrationFees}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees)} handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setTuitionFees)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setTuitionFees)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)} handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setTuitionFees)}
/> />
</div> </div>
</div> </div>

View File

@ -1,11 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Trash, Edit3, Check, X } from 'lucide-react'; import { Plus, Trash, Edit3, Check, X, EyeOff, Eye } 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'; import SelectChoice from '@/components/SelectChoice';
const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete }) => { const RegistrationFeesSection = ({ registrationFees, setRegistrationFees, discounts, handleCreate, handleEdit, handleDelete }) => {
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({});
@ -29,8 +29,14 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
const handleSaveNewFee = () => { const handleSaveNewFee = () => {
if (newFee.name && newFee.base_amount) { if (newFee.name && newFee.base_amount) {
handleCreate(newFee) const feeData = {
.then(() => { ...newFee,
type: 0
};
handleCreate(feeData)
.then((createdFee) => {
setRegistrationFees([createdFee, ...registrationFees]);
setNewFee(null); setNewFee(null);
setLocalErrors({}); setLocalErrors({});
}) })
@ -68,7 +74,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
}; };
const handleToggleActive = (id, isActive) => { const handleToggleActive = (id, isActive) => {
const fee = fees.find(fee => fee.id === id); const fee = registrationFees.find(fee => fee.id === id);
if (!fee) return; if (!fee) return;
const updatedData = { const updatedData = {
@ -78,7 +84,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
handleEdit(id, updatedData) handleEdit(id, updatedData)
.then(() => { .then(() => {
setFees(prevFees => prevFees.map(fee => fee.id === id ? { ...fee, is_active: !isActive } : fee)); setRegistrationFees(prevFees => prevFees.map(fee => fee.id === id ? { ...fee, is_active: !isActive } : fee));
}) })
.catch(error => { .catch(error => {
console.error(error); console.error(error);
@ -107,7 +113,14 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
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);
return discount ? sum + parseFloat(discount.amount) : sum; 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); }, 0);
const finalAmount = parseFloat(baseAmount) - totalDiscounts; const finalAmount = parseFloat(baseAmount) - totalDiscounts;
@ -220,7 +233,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
onClick={() => handleToggleActive(fee.id, fee.is_active)} onClick={() => handleToggleActive(fee.id, fee.is_active)}
className={`text-${fee.is_active ? 'gray' : 'green'}-500 hover:text-${fee.is_active ? 'gray' : 'green'}-700`} className={`text-${fee.is_active ? 'gray' : 'green'}-500 hover:text-${fee.is_active ? 'gray' : 'green'}-700`}
> >
{fee.is_active ? <X className="w-5 h-5" /> : <Check className="w-5 h-5" />} {fee.is_active ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
); );
@ -240,7 +253,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
</button> </button>
</div> </div>
<Table <Table
data={newFee ? [newFee, ...fees] : fees} data={newFee ? [newFee, ...registrationFees] : registrationFees}
columns={[ columns={[
{ name: 'LIBELLE', label: 'Libellé' }, { name: 'LIBELLE', label: 'Libellé' },
{ name: 'MONTANT DE BASE', label: 'Montant' }, { name: 'MONTANT DE BASE', label: 'Montant' },
@ -264,4 +277,4 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
); );
}; };
export default FeesSection; export default RegistrationFeesSection;

View File

@ -1,11 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Trash, Edit3, Check, X } from 'lucide-react'; import { Plus, Trash, Edit3, Check, X, EyeOff, Eye } 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'; import SelectChoice from '@/components/SelectChoice';
const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, fees, handleCreate, handleEdit, handleDelete }) => { const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, registrationFees, handleCreate, handleEdit, handleDelete }) => {
const [editingTuitionFee, setEditingTuitionFee] = useState(null); const [editingTuitionFee, setEditingTuitionFee] = useState(null);
const [newTuitionFee, setNewTuitionFee] = useState(null); const [newTuitionFee, setNewTuitionFee] = useState(null);
const [formData, setFormData] = useState({}); const [formData, setFormData] = useState({});
@ -39,7 +39,11 @@ const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, fees, hand
newTuitionFee.base_amount && newTuitionFee.base_amount &&
newTuitionFee.payment_option >= 0 newTuitionFee.payment_option >= 0
) { ) {
handleCreate(newTuitionFee) const tuitionFeeData = {
...newTuitionFee,
type: 1
};
handleCreate(tuitionFeeData)
.then((createdTuitionFee) => { .then((createdTuitionFee) => {
setTuitionFees([createdTuitionFee, ...tuitionFees]); setTuitionFees([createdTuitionFee, ...tuitionFees]);
setNewTuitionFee(null); setNewTuitionFee(null);
@ -151,7 +155,14 @@ const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, fees, hand
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);
return discount ? sum + parseFloat(discount.amount) : sum; 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); }, 0);
const finalAmount = parseFloat(baseAmount) - totalDiscounts; const finalAmount = parseFloat(baseAmount) - totalDiscounts;
@ -238,7 +249,7 @@ const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, fees, hand
onClick={() => handleToggleActive(tuitionFee.id, tuitionFee.is_active)} onClick={() => handleToggleActive(tuitionFee.id, tuitionFee.is_active)}
className={`text-${tuitionFee.is_active ? 'gray' : 'green'}-500 hover:text-${tuitionFee.is_active ? 'gray' : 'green'}-700`} className={`text-${tuitionFee.is_active ? 'gray' : 'green'}-500 hover:text-${tuitionFee.is_active ? 'gray' : 'green'}-700`}
> >
{tuitionFee.is_active ? <X className="w-5 h-5" /> : <Check className="w-5 h-5" />} {tuitionFee.is_active ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
); );

View File

@ -39,8 +39,6 @@ export const BE_SCHOOL_FEE_URL = `${BASE_URL}/School/fee`;
export const BE_SCHOOL_FEES_URL = `${BASE_URL}/School/fees`; export const BE_SCHOOL_FEES_URL = `${BASE_URL}/School/fees`;
export const BE_SCHOOL_DISCOUNT_URL = `${BASE_URL}/School/discount`; export const BE_SCHOOL_DISCOUNT_URL = `${BASE_URL}/School/discount`;
export const BE_SCHOOL_DISCOUNTS_URL = `${BASE_URL}/School/discounts`; export const BE_SCHOOL_DISCOUNTS_URL = `${BASE_URL}/School/discounts`;
export const BE_SCHOOL_TUITION_FEE_URL = `${BASE_URL}/School/tuitionFee`;
export const BE_SCHOOL_TUITION_FEES_URL = `${BASE_URL}/School/tuitionFees`;
// GESTION MESSAGERIE // GESTION MESSAGERIE
export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie` export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie`