mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Mise à jour du modèle (possibilité d'associer une réduciton à un
frais d'inscription [#18]
This commit is contained in:
@ -68,6 +68,11 @@ class Planning(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Planning for {self.level} of {self.school_class.atmosphere_name}'
|
return f'Planning for {self.level} of {self.school_class.atmosphere_name}'
|
||||||
|
|
||||||
|
class PaymentOptions(models.IntegerChoices):
|
||||||
|
SINGLE_PAYMENT = 0, _('Paiement en une seule fois')
|
||||||
|
FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois')
|
||||||
|
TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois')
|
||||||
|
|
||||||
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)
|
||||||
@ -78,25 +83,23 @@ class Discount(models.Model):
|
|||||||
|
|
||||||
class Fee(models.Model):
|
class Fee(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)
|
|
||||||
description = models.TextField(blank=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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class TuitionFee(models.Model):
|
class TuitionFee(models.Model):
|
||||||
class PaymentOptions(models.IntegerChoices):
|
|
||||||
SINGLE_PAYMENT = 0, _('Paiement en une seule fois')
|
|
||||||
FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois')
|
|
||||||
TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois')
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255, unique=True)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
base_amount = models.DecimalField(max_digits=10, decimal_places=2)
|
base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||||
currency = models.CharField(max_length=3, default='EUR')
|
currency = models.CharField(max_length=3, default='EUR')
|
||||||
discounts = models.ManyToManyField('Discount', blank=True)
|
discounts = models.ManyToManyField('Discount', blank=True)
|
||||||
validity_start_date = models.DateField()
|
|
||||||
validity_end_date = models.DateField()
|
|
||||||
payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT)
|
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)
|
||||||
@ -107,16 +110,3 @@ class TuitionFee(models.Model):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
if self.validity_end_date <= self.validity_start_date:
|
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é.'))
|
raise ValidationError(_('La date de fin de validité doit être après la date de début de validité.'))
|
||||||
|
|
||||||
def calculate_final_amount(self):
|
|
||||||
amount = self.base_amount
|
|
||||||
|
|
||||||
# Apply fees (supplements and taxes)
|
|
||||||
# for fee in self.fees.all():
|
|
||||||
# amount += fee.amount
|
|
||||||
|
|
||||||
# Apply discounts
|
|
||||||
for discount in self.discounts.all():
|
|
||||||
amount -= discount.amount
|
|
||||||
|
|
||||||
return amount
|
|
||||||
|
|||||||
@ -180,21 +180,48 @@ class DiscountSerializer(serializers.ModelSerializer):
|
|||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
class FeeSerializer(serializers.ModelSerializer):
|
class FeeSerializer(serializers.ModelSerializer):
|
||||||
|
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Fee
|
model = Fee
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
discounts_data = validated_data.pop('discounts', [])
|
||||||
|
|
||||||
|
# Create the Fee instance
|
||||||
|
fee = Fee.objects.create(**validated_data)
|
||||||
|
|
||||||
|
# Add discounts if provided
|
||||||
|
fee.discounts.set(discounts_data)
|
||||||
|
|
||||||
|
return fee
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
discounts_data = validated_data.pop('discounts', [])
|
||||||
|
|
||||||
|
# Update the Fee 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()
|
||||||
|
|
||||||
|
# Update discounts if provided
|
||||||
|
instance.discounts.set(discounts_data)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
class TuitionFeeSerializer(serializers.ModelSerializer):
|
class TuitionFeeSerializer(serializers.ModelSerializer):
|
||||||
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True)
|
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True)
|
||||||
final_amount = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TuitionFee
|
model = TuitionFee
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
def get_final_amount(self, obj):
|
|
||||||
return obj.calculate_final_amount()
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
discounts_data = validated_data.pop('discounts', [])
|
discounts_data = validated_data.pop('discounts', [])
|
||||||
|
|
||||||
@ -202,8 +229,7 @@ class TuitionFeeSerializer(serializers.ModelSerializer):
|
|||||||
tuition_fee = TuitionFee.objects.create(**validated_data)
|
tuition_fee = TuitionFee.objects.create(**validated_data)
|
||||||
|
|
||||||
# Add discounts if provided
|
# Add discounts if provided
|
||||||
for discount in discounts_data:
|
tuition_fee.discounts.set(discounts_data)
|
||||||
tuition_fee.discounts.add(discount)
|
|
||||||
|
|
||||||
return tuition_fee
|
return tuition_fee
|
||||||
|
|
||||||
@ -215,14 +241,12 @@ class TuitionFeeSerializer(serializers.ModelSerializer):
|
|||||||
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.currency = validated_data.get('currency', instance.currency)
|
||||||
instance.validity_start_date = validated_data.get('validity_start_date', instance.validity_start_date)
|
|
||||||
instance.validity_end_date = validated_data.get('validity_end_date', instance.validity_end_date)
|
|
||||||
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.save()
|
instance.save()
|
||||||
|
|
||||||
# Update discounts if provided
|
# Update discounts if provided
|
||||||
if discounts_data:
|
|
||||||
instance.discounts.set(discounts_data)
|
instance.discounts.set(discounts_data)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
@ -99,7 +99,7 @@ export default function Page() {
|
|||||||
.catch(error => console.error('Error fetching tuition fees', error));
|
.catch(error => console.error('Error fetching tuition fees', error));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = (url, newData, setDatas, setErrors) => {
|
const handleCreate = (url, newData, setDatas) => {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -119,17 +119,15 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setDatas(prevState => [...prevState, data]);
|
setDatas(prevState => [...prevState, data]);
|
||||||
setErrors({});
|
|
||||||
return data;
|
return data;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
setErrors(error);
|
|
||||||
console.error('Error creating data:', error);
|
console.error('Error creating data:', error);
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (url, id, updatedData, setDatas, setErrors) => {
|
const handleEdit = (url, id, updatedData, setDatas) => {
|
||||||
return fetch(`${url}/${id}`, {
|
return fetch(`${url}/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
@ -149,18 +147,16 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setDatas(prevState => prevState.map(item => item.id === id ? data : item));
|
setDatas(prevState => prevState.map(item => item.id === id ? data : item));
|
||||||
setErrors({});
|
|
||||||
return data;
|
return data;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
setErrors(error);
|
|
||||||
console.error('Error editing data:', error);
|
console.error('Error editing data:', error);
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (url, id, setDatas) => {
|
const handleDelete = (url, id, setDatas) => {
|
||||||
fetch(`${url}/${id}`, {
|
return fetch(`${url}/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -168,12 +164,24 @@ export default function Page() {
|
|||||||
},
|
},
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return response.json().then(errorData => {
|
||||||
|
throw errorData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setDatas(prevState => prevState.filter(item => item.id !== id));
|
setDatas(prevState => prevState.filter(item => item.id !== id));
|
||||||
|
return data;
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error deleting data:', error));
|
.catch(error => {
|
||||||
|
console.error('Error deleting data:', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatePlanning = (url, planningId, updatedData) => {
|
const handleUpdatePlanning = (url, planningId, updatedData) => {
|
||||||
fetch(`${url}/${planningId}`, {
|
fetch(`${url}/${planningId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) {
|
export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`mb-4 ${className}`}>
|
<div className={`${className}`}>
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
|
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
|
||||||
<div className={`mt-1 flex items-stretch border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
|
<div className={`mt-1 flex items-stretch border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
|
||||||
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
|
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
|
||||||
|
|||||||
@ -4,7 +4,7 @@ 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, errors }) => {
|
const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete }) => {
|
||||||
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({});
|
||||||
@ -17,7 +17,7 @@ const DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete, e
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveDiscount = (id) => {
|
const handleRemoveDiscount = (id) => {
|
||||||
handleDelete(id);
|
handleDelete(id)
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveNewDiscount = () => {
|
const handleSaveNewDiscount = () => {
|
||||||
|
|||||||
@ -2,41 +2,42 @@ import React, { useState } from 'react';
|
|||||||
import FeesSection from './FeesSection';
|
import FeesSection from './FeesSection';
|
||||||
import DiscountsSection from './DiscountsSection';
|
import DiscountsSection from './DiscountsSection';
|
||||||
import TuitionFeesSection from './TuitionFeesSection';
|
import TuitionFeesSection from './TuitionFeesSection';
|
||||||
import { TuitionFeesProvider } from '@/context/TuitionFeesContext';
|
|
||||||
import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL, BE_SCHOOL_TUITION_FEE_URL } from '@/utils/Url';
|
import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL, BE_SCHOOL_TUITION_FEE_URL } from '@/utils/Url';
|
||||||
|
|
||||||
const FeesManagement = ({ fees, setFees, discounts, setDiscounts, setTuitionFees, handleCreate, handleEdit, handleDelete }) => {
|
const FeesManagement = ({ fees, setFees, discounts, setDiscounts, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => {
|
||||||
const [errors, setErrors] = useState({});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TuitionFeesProvider>
|
|
||||||
<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">
|
|
||||||
<FeesSection
|
|
||||||
fees={fees}
|
|
||||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees, setErrors)}
|
|
||||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees, setErrors)}
|
|
||||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)}
|
|
||||||
errors
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 bg-white rounded-lg shadow-md">
|
<div className="p-4 bg-white rounded-lg shadow-md">
|
||||||
<DiscountsSection
|
<DiscountsSection
|
||||||
discounts={discounts}
|
discounts={discounts}
|
||||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts, setErrors)}
|
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts)}
|
||||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts, setErrors)}
|
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">
|
||||||
|
<FeesSection
|
||||||
|
fees={fees}
|
||||||
|
setFees={setFees}
|
||||||
|
discounts={discounts}
|
||||||
|
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees)}
|
||||||
|
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees)}
|
||||||
|
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="p-4 bg-white rounded-lg shadow-md">
|
<div className="p-4 bg-white rounded-lg shadow-md">
|
||||||
<TuitionFeesSection
|
<TuitionFeesSection
|
||||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees, setErrors)}
|
tuitionFees={tuitionFees}
|
||||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees, setErrors)}
|
setTuitionFees={setTuitionFees}
|
||||||
|
discounts={discounts}
|
||||||
|
fees={fees}
|
||||||
|
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees)}
|
||||||
|
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees)}
|
||||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)}
|
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TuitionFeesProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import { Plus, Trash, Edit3, Check, X } 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 FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) => {
|
const FeesSection = ({ fees, setFees, 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({});
|
||||||
@ -12,8 +13,14 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) =
|
|||||||
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: '', amount: '', description: '' });
|
setNewFee({ id: Date.now(), name: '', base_amount: '', description: '' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFee = (id) => {
|
const handleRemoveFee = (id) => {
|
||||||
@ -21,7 +28,7 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveNewFee = () => {
|
const handleSaveNewFee = () => {
|
||||||
if (newFee.name && newFee.amount) {
|
if (newFee.name && newFee.base_amount) {
|
||||||
handleCreate(newFee)
|
handleCreate(newFee)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setNewFee(null);
|
setNewFee(null);
|
||||||
@ -41,7 +48,7 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateFee = (id, updatedFee) => {
|
const handleUpdateFee = (id, updatedFee) => {
|
||||||
if (updatedFee.name && updatedFee.amount) {
|
if (updatedFee.name && updatedFee.base_amount) {
|
||||||
handleEdit(id, updatedFee)
|
handleEdit(id, updatedFee)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setEditingFee(null);
|
setEditingFee(null);
|
||||||
@ -60,26 +67,59 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleActive = (id, isActive) => {
|
||||||
|
const fee = fees.find(fee => fee.id === id);
|
||||||
|
if (!fee) return;
|
||||||
|
|
||||||
|
const updatedData = {
|
||||||
|
is_active: !isActive,
|
||||||
|
discounts: fee.discounts
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEdit(id, updatedData)
|
||||||
|
.then(() => {
|
||||||
|
setFees(prevFees => prevFees.map(fee => fee.id === id ? { ...fee, is_active: !isActive } : fee));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
let parsedValue = value;
|
||||||
|
if (name === 'discounts') {
|
||||||
|
parsedValue = value.split(',').map(v => parseInt(v, 10));
|
||||||
|
}
|
||||||
if (editingFee) {
|
if (editingFee) {
|
||||||
setFormData((prevData) => ({
|
setFormData((prevData) => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
[name]: value,
|
[name]: parsedValue,
|
||||||
}));
|
}));
|
||||||
} else if (newFee) {
|
} else if (newFee) {
|
||||||
setNewFee((prevData) => ({
|
setNewFee((prevData) => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
[name]: value,
|
[name]: parsedValue,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calculateFinalAmount = (baseAmount, discountIds) => {
|
||||||
|
const totalDiscounts = discountIds.reduce((sum, discountId) => {
|
||||||
|
const discount = discounts.find(d => d.id === discountId);
|
||||||
|
return discount ? sum + parseFloat(discount.amount) : sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const finalAmount = parseFloat(baseAmount) - totalDiscounts;
|
||||||
|
|
||||||
|
return finalAmount.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
const renderInputField = (field, value, onChange, placeholder) => (
|
const renderInputField = (field, value, onChange, placeholder) => (
|
||||||
<div>
|
<div>
|
||||||
<InputTextIcon
|
<InputTextIcon
|
||||||
name={field}
|
name={field}
|
||||||
type={field === 'amount' ? 'number' : 'text'}
|
type={field === 'base_amount' ? 'number' : 'text'}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@ -88,6 +128,19 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) =
|
|||||||
</div>
|
</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;
|
||||||
@ -97,10 +150,14 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) =
|
|||||||
switch (column) {
|
switch (column) {
|
||||||
case 'LIBELLE':
|
case 'LIBELLE':
|
||||||
return renderInputField('name', currentData.name, handleChange, 'Libellé du frais');
|
return renderInputField('name', currentData.name, handleChange, 'Libellé du frais');
|
||||||
case 'MONTANT':
|
case 'MONTANT DE BASE':
|
||||||
return renderInputField('amount', currentData.amount, handleChange, 'Montant');
|
return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant');
|
||||||
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 space-x-2">
|
||||||
@ -127,10 +184,20 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) =
|
|||||||
switch (column) {
|
switch (column) {
|
||||||
case 'LIBELLE':
|
case 'LIBELLE':
|
||||||
return fee.name;
|
return fee.name;
|
||||||
case 'MONTANT':
|
case 'MONTANT DE BASE':
|
||||||
return fee.amount + ' €';
|
return fee.base_amount + ' €';
|
||||||
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">
|
||||||
@ -148,6 +215,13 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) =
|
|||||||
>
|
>
|
||||||
<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 ? <X className="w-5 h-5" /> : <Check className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@ -169,8 +243,11 @@ const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) =
|
|||||||
data={newFee ? [newFee, ...fees] : fees}
|
data={newFee ? [newFee, ...fees] : fees}
|
||||||
columns={[
|
columns={[
|
||||||
{ name: 'LIBELLE', label: 'Libellé' },
|
{ name: 'LIBELLE', label: 'Libellé' },
|
||||||
{ name: 'MONTANT', label: 'Montant' },
|
{ name: 'MONTANT DE BASE', label: 'Montant' },
|
||||||
{ name: 'DESCRIPTION', label: 'Description' },
|
{ 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' }
|
{ name: 'ACTIONS', label: 'Actions' }
|
||||||
]}
|
]}
|
||||||
renderCell={renderFeeCell}
|
renderCell={renderFeeCell}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Plus, Trash, Edit3, Check, X, Calendar } from 'lucide-react';
|
import { Plus, Trash, Edit3, Check, X } 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';
|
||||||
import { useTuitionFees } from '@/context/TuitionFeesContext';
|
|
||||||
|
|
||||||
const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) => {
|
const TuitionFeesSection = ({ tuitionFees, setTuitionFees, discounts, fees, handleCreate, handleEdit, handleDelete }) => {
|
||||||
const { fees, tuitionFees, setTuitionFees, discounts } = useTuitionFees();
|
|
||||||
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({});
|
||||||
@ -26,18 +24,20 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveTuitionFee = (id) => {
|
const handleRemoveTuitionFee = (id) => {
|
||||||
handleDelete(id);
|
handleDelete(id)
|
||||||
setTuitionFees(tuitionFees.filter(fee => fee.id !== id));
|
.then(() => {
|
||||||
|
setTuitionFees(prevTuitionFees => prevTuitionFees.filter(fee => fee.id !== id));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveNewTuitionFee = () => {
|
const handleSaveNewTuitionFee = () => {
|
||||||
if (
|
if (
|
||||||
newTuitionFee.name &&
|
newTuitionFee.name &&
|
||||||
newTuitionFee.base_amount &&
|
newTuitionFee.base_amount &&
|
||||||
newTuitionFee.payment_option >= 0 &&
|
newTuitionFee.payment_option >= 0
|
||||||
newTuitionFee.validity_start_date &&
|
|
||||||
newTuitionFee.validity_end_date &&
|
|
||||||
new Date(newTuitionFee.validity_start_date) <= new Date(newTuitionFee.validity_end_date)
|
|
||||||
) {
|
) {
|
||||||
handleCreate(newTuitionFee)
|
handleCreate(newTuitionFee)
|
||||||
.then((createdTuitionFee) => {
|
.then((createdTuitionFee) => {
|
||||||
@ -62,10 +62,7 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
|
|||||||
if (
|
if (
|
||||||
updatedTuitionFee.name &&
|
updatedTuitionFee.name &&
|
||||||
updatedTuitionFee.base_amount &&
|
updatedTuitionFee.base_amount &&
|
||||||
updatedTuitionFee.payment_option >= 0 &&
|
updatedTuitionFee.payment_option >= 0
|
||||||
updatedTuitionFee.validity_start_date &&
|
|
||||||
updatedTuitionFee.validity_end_date &&
|
|
||||||
new Date(updatedTuitionFee.validity_start_date) <= new Date(updatedTuitionFee.validity_end_date)
|
|
||||||
) {
|
) {
|
||||||
handleEdit(id, updatedTuitionFee)
|
handleEdit(id, updatedTuitionFee)
|
||||||
.then((updatedFee) => {
|
.then((updatedFee) => {
|
||||||
@ -86,6 +83,24 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 handleChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
let parsedValue = value;
|
let parsedValue = value;
|
||||||
@ -120,24 +135,11 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderDateField = (field, value, onChange) => (
|
|
||||||
<div className="relative flex items-center justify-center h-full">
|
|
||||||
<Calendar className="w-5 h-5 text-emerald-500 absolute left-3" />
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name={field}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
className="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderSelectField = (field, value, options, callback, label) => (
|
const renderSelectField = (field, value, options, callback, label) => (
|
||||||
<div className="flex justify-center items-center h-full">
|
<div className="flex justify-center items-center h-full">
|
||||||
<SelectChoice
|
<SelectChoice
|
||||||
name={field}
|
name={field}
|
||||||
value={value}
|
selected={value}
|
||||||
options={options}
|
options={options}
|
||||||
callback={callback}
|
callback={callback}
|
||||||
placeHolder={label}
|
placeHolder={label}
|
||||||
@ -147,14 +149,12 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
|
|||||||
);
|
);
|
||||||
|
|
||||||
const calculateFinalAmount = (baseAmount, discountIds) => {
|
const calculateFinalAmount = (baseAmount, discountIds) => {
|
||||||
const totalFees = fees.reduce((sum, fee) => sum + parseFloat(fee.amount), 0);
|
|
||||||
|
|
||||||
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;
|
return discount ? sum + parseFloat(discount.amount) : sum;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const finalAmount = parseFloat(baseAmount) + totalFees - totalDiscounts;
|
const finalAmount = parseFloat(baseAmount) - totalDiscounts;
|
||||||
|
|
||||||
return finalAmount.toFixed(2);
|
return finalAmount.toFixed(2);
|
||||||
};
|
};
|
||||||
@ -172,10 +172,6 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
|
|||||||
return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base');
|
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 'DATE DE DEBUT':
|
|
||||||
return renderDateField('validity_start_date', currentData.validity_start_date, handleChange);
|
|
||||||
case 'DATE DE FIN':
|
|
||||||
return renderDateField('validity_end_date', currentData.validity_end_date, handleChange);
|
|
||||||
case 'OPTIONS DE PAIEMENT':
|
case 'OPTIONS DE PAIEMENT':
|
||||||
return renderSelectField('payment_option', currentData.payment_option, paymentOptions, handleChange, 'Options de paiement');
|
return renderSelectField('payment_option', currentData.payment_option, paymentOptions, handleChange, 'Options de paiement');
|
||||||
case 'REMISES':
|
case 'REMISES':
|
||||||
@ -210,10 +206,6 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
|
|||||||
return tuitionFee.base_amount + ' €';
|
return tuitionFee.base_amount + ' €';
|
||||||
case 'DESCRIPTION':
|
case 'DESCRIPTION':
|
||||||
return tuitionFee.description;
|
return tuitionFee.description;
|
||||||
case 'DATE DE DEBUT':
|
|
||||||
return tuitionFee.validity_start_date;
|
|
||||||
case 'DATE DE FIN':
|
|
||||||
return tuitionFee.validity_end_date;
|
|
||||||
case 'OPTIONS DE PAIEMENT':
|
case 'OPTIONS DE PAIEMENT':
|
||||||
return paymentOptions.find(option => option.value === tuitionFee.payment_option)?.label || '';
|
return paymentOptions.find(option => option.value === tuitionFee.payment_option)?.label || '';
|
||||||
case 'REMISES':
|
case 'REMISES':
|
||||||
@ -241,6 +233,13 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
|
|||||||
>
|
>
|
||||||
<Trash className="w-5 h-5" />
|
<Trash className="w-5 h-5" />
|
||||||
</button>
|
</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 ? <X className="w-5 h-5" /> : <Check className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@ -263,8 +262,6 @@ const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors })
|
|||||||
{ name: 'NOM', label: 'Nom' },
|
{ name: 'NOM', label: 'Nom' },
|
||||||
{ name: 'MONTANT DE BASE', label: 'Montant de base' },
|
{ name: 'MONTANT DE BASE', label: 'Montant de base' },
|
||||||
{ name: 'DESCRIPTION', label: 'Description' },
|
{ name: 'DESCRIPTION', label: 'Description' },
|
||||||
{ name: 'DATE DE DEBUT', label: 'Date de début' },
|
|
||||||
{ name: 'DATE DE FIN', label: 'Date de fin' },
|
|
||||||
{ name: 'OPTIONS DE PAIEMENT', label: 'Options de paiement' },
|
{ name: 'OPTIONS DE PAIEMENT', label: 'Options de paiement' },
|
||||||
{ name: 'REMISES', label: 'Remises' },
|
{ name: 'REMISES', label: 'Remises' },
|
||||||
{ name: 'MONTANT FINAL', label: 'Montant final' },
|
{ name: 'MONTANT FINAL', label: 'Montant final' },
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
import React, { createContext, useState, useEffect, useContext } from 'react';
|
|
||||||
import { fetchTuitionFees, fetchFees, fetchDiscounts } from '@/app/lib/schoolAction';
|
|
||||||
|
|
||||||
const TuitionFeesContext = createContext();
|
|
||||||
|
|
||||||
export const useTuitionFees = () => useContext(TuitionFeesContext);
|
|
||||||
|
|
||||||
export const TuitionFeesProvider = ({ children }) => {
|
|
||||||
const [tuitionFees, setTuitionFees] = useState([]);
|
|
||||||
const [fees, setFees] = useState([]);
|
|
||||||
const [discounts, setDiscounts] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchTuitionFees().then(data => setTuitionFees(data));
|
|
||||||
fetchFees().then(data => setFees(data));
|
|
||||||
fetchDiscounts().then(data => setDiscounts(data));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TuitionFeesContext.Provider value={{ tuitionFees, setTuitionFees, fees, setFees, discounts, setDiscounts }}>
|
|
||||||
{children}
|
|
||||||
</TuitionFeesContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user