mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Ajout des frais d'inscription lors de la création d'un RF [#18]
This commit is contained in:
@ -96,7 +96,6 @@ 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)
|
||||||
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)
|
||||||
type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE)
|
type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE)
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee
|
from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee
|
||||||
from Subscriptions.models import RegistrationForm
|
|
||||||
from Subscriptions.serializers import StudentSerializer
|
|
||||||
from Auth.serializers import ProfileSerializer
|
|
||||||
from Auth.models import Profile
|
from Auth.models import Profile
|
||||||
from N3wtSchool import settings, bdd
|
from N3wtSchool import settings, bdd
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -187,41 +184,12 @@ class DiscountSerializer(serializers.ModelSerializer):
|
|||||||
return local_time.strftime("%d-%m-%Y %H:%M")
|
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)
|
|
||||||
updated_at_formatted = serializers.SerializerMethodField()
|
updated_at_formatted = serializers.SerializerMethodField()
|
||||||
|
|
||||||
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.is_active = validated_data.get('is_active', instance.is_active)
|
|
||||||
instance.updated_at = validated_data.get('updated_at', instance.updated_at)
|
|
||||||
instance.type = validated_data.get('type', instance.type)
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
# Update discounts if provided
|
|
||||||
instance.discounts.set(discounts_data)
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def get_updated_at_formatted(self, obj):
|
def get_updated_at_formatted(self, obj):
|
||||||
utc_time = timezone.localtime(obj.updated_at)
|
utc_time = timezone.localtime(obj.updated_at)
|
||||||
local_tz = pytz.timezone(settings.TZ_APPLI)
|
local_tz = pytz.timezone(settings.TZ_APPLI)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from django.conf import settings
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from Auth.models import Profile
|
from Auth.models import Profile
|
||||||
from School.models import SchoolClass
|
from School.models import SchoolClass, Fee, Discount
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@ -204,6 +204,12 @@ class RegistrationForm(models.Model):
|
|||||||
registration_file = models.FileField(upload_to=settings.DOCUMENT_DIR, default="", blank=True)
|
registration_file = models.FileField(upload_to=settings.DOCUMENT_DIR, default="", blank=True)
|
||||||
associated_rf = models.CharField(max_length=200, default="", blank=True)
|
associated_rf = models.CharField(max_length=200, default="", blank=True)
|
||||||
|
|
||||||
|
# Many-to-Many Relationship
|
||||||
|
fees = models.ManyToManyField(Fee, blank=True, related_name='register_forms')
|
||||||
|
|
||||||
|
# Many-to-Many Relationship
|
||||||
|
discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "RF_" + self.student.last_name + "_" + self.student.first_name
|
return "RF_" + self.student.last_name + "_" + self.student.first_name
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee
|
from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee
|
||||||
from School.models import SchoolClass
|
from School.models import SchoolClass, Fee, Discount
|
||||||
|
from School.serializers import FeeSerializer, DiscountSerializer
|
||||||
from Auth.models import Profile
|
from Auth.models import Profile
|
||||||
from Auth.serializers import ProfileSerializer
|
from Auth.serializers import ProfileSerializer
|
||||||
from GestionMessagerie.models import Messagerie
|
from GestionMessagerie.models import Messagerie
|
||||||
@ -133,6 +134,9 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
|
|||||||
status_label = serializers.SerializerMethodField()
|
status_label = serializers.SerializerMethodField()
|
||||||
formatted_last_update = serializers.SerializerMethodField()
|
formatted_last_update = serializers.SerializerMethodField()
|
||||||
registration_files = RegistrationFileSerializer(many=True, required=False)
|
registration_files = RegistrationFileSerializer(many=True, required=False)
|
||||||
|
fees = serializers.PrimaryKeyRelatedField(queryset=Fee.objects.all(), many=True, required=False)
|
||||||
|
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RegistrationForm
|
model = RegistrationForm
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@ -140,11 +144,19 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
student_data = validated_data.pop('student')
|
student_data = validated_data.pop('student')
|
||||||
student = StudentSerializer.create(StudentSerializer(), student_data)
|
student = StudentSerializer.create(StudentSerializer(), student_data)
|
||||||
|
fees_data = validated_data.pop('fees', [])
|
||||||
|
discounts_data = validated_data.pop('discounts', [])
|
||||||
registrationForm = RegistrationForm.objects.create(student=student, **validated_data)
|
registrationForm = RegistrationForm.objects.create(student=student, **validated_data)
|
||||||
|
|
||||||
|
# Associer les IDs des objets Fee et Discount au RegistrationForm
|
||||||
|
registrationForm.fees.set([fee.id for fee in fees_data])
|
||||||
|
registrationForm.discounts.set([discount.id for discount in discounts_data])
|
||||||
return registrationForm
|
return registrationForm
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
student_data = validated_data.pop('student', None)
|
student_data = validated_data.pop('student', None)
|
||||||
|
fees_data = validated_data.pop('fees', [])
|
||||||
|
discounts_data = validated_data.pop('discounts', [])
|
||||||
if student_data:
|
if student_data:
|
||||||
student = instance.student
|
student = instance.student
|
||||||
StudentSerializer.update(StudentSerializer(), student, student_data)
|
StudentSerializer.update(StudentSerializer(), student, student_data)
|
||||||
@ -156,6 +168,10 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
|
|||||||
pass
|
pass
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
# Associer les IDs des objets Fee et Discount au RegistrationForm
|
||||||
|
instance.fees.set([fee.id for fee in fees_data])
|
||||||
|
instance.discounts.set([discount.id for discount in discounts_data])
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def get_status_label(self, obj):
|
def get_status_label(self, obj):
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
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/Tarification/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';
|
||||||
|
|||||||
@ -32,7 +32,13 @@ import {
|
|||||||
fetchStudents,
|
fetchStudents,
|
||||||
editRegisterForm } from "@/app/lib/subscriptionAction"
|
editRegisterForm } from "@/app/lib/subscriptionAction"
|
||||||
|
|
||||||
import { fetchClasses } from '@/app/lib/schoolAction';
|
import {
|
||||||
|
fetchClasses,
|
||||||
|
fetchRegistrationDiscounts,
|
||||||
|
fetchTuitionDiscounts,
|
||||||
|
fetchRegistrationFees,
|
||||||
|
fetchTuitionFees } from '@/app/lib/schoolAction';
|
||||||
|
|
||||||
import { createProfile } from '@/app/lib/authAction';
|
import { createProfile } from '@/app/lib/authAction';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -75,6 +81,11 @@ export default function Page({ params: { locale } }) {
|
|||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [fileToEdit, setFileToEdit] = useState(null);
|
const [fileToEdit, setFileToEdit] = useState(null);
|
||||||
|
|
||||||
|
const [registrationDiscounts, setRegistrationDiscounts] = useState([]);
|
||||||
|
const [tuitionDiscounts, setTuitionDiscounts] = useState([]);
|
||||||
|
const [registrationFees, setRegistrationFees] = useState([]);
|
||||||
|
const [tuitionFees, setTuitionFees] = useState([]);
|
||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
|
|
||||||
const openModal = () => {
|
const openModal = () => {
|
||||||
@ -151,6 +162,7 @@ const registerFormArchivedDataHandler = (data) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: revoir le système de pagination et de UseEffect
|
// TODO: revoir le système de pagination et de UseEffect
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -195,7 +207,27 @@ const registerFormArchivedDataHandler = (data) => {
|
|||||||
|
|
||||||
setFichiers(data)
|
setFichiers(data)
|
||||||
})
|
})
|
||||||
.catch((err)=>{ err = err.message; console.log(err);});
|
.catch((err)=>{ err = err.message; console.log(err);})
|
||||||
|
fetchRegistrationDiscounts()
|
||||||
|
.then(data => {
|
||||||
|
setRegistrationDiscounts(data);
|
||||||
|
})
|
||||||
|
.catch(requestErrorHandler)
|
||||||
|
fetchTuitionDiscounts()
|
||||||
|
.then(data => {
|
||||||
|
setTuitionDiscounts(data);
|
||||||
|
})
|
||||||
|
.catch(requestErrorHandler)
|
||||||
|
fetchRegistrationFees()
|
||||||
|
.then(data => {
|
||||||
|
setRegistrationFees(data);
|
||||||
|
})
|
||||||
|
.catch(requestErrorHandler)
|
||||||
|
fetchTuitionFees()
|
||||||
|
.then(data => {
|
||||||
|
setTuitionFees(data);
|
||||||
|
})
|
||||||
|
.catch(requestErrorHandler);
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setRegistrationFormsDataPending(mockFicheInscription);
|
setRegistrationFormsDataPending(mockFicheInscription);
|
||||||
@ -321,6 +353,8 @@ useEffect(()=>{
|
|||||||
|
|
||||||
const createRF = (updatedData) => {
|
const createRF = (updatedData) => {
|
||||||
console.log('createRF updatedData:', updatedData);
|
console.log('createRF updatedData:', updatedData);
|
||||||
|
const selectedRegistrationFeesIds = updatedData.selectedRegistrationFees.map(feeId => feeId)
|
||||||
|
const selectedRegistrationDiscountsIds = updatedData.selectedRegistrationDiscounts.map(discountId => discountId)
|
||||||
|
|
||||||
if (updatedData.selectedGuardians.length !== 0) {
|
if (updatedData.selectedGuardians.length !== 0) {
|
||||||
const selectedGuardiansIds = updatedData.selectedGuardians.map(guardianId => guardianId)
|
const selectedGuardiansIds = updatedData.selectedGuardians.map(guardianId => guardianId)
|
||||||
@ -330,7 +364,9 @@ useEffect(()=>{
|
|||||||
last_name: updatedData.studentLastName,
|
last_name: updatedData.studentLastName,
|
||||||
first_name: updatedData.studentFirstName,
|
first_name: updatedData.studentFirstName,
|
||||||
},
|
},
|
||||||
idGuardians: selectedGuardiansIds
|
idGuardians: selectedGuardiansIds,
|
||||||
|
fees: selectedRegistrationFeesIds,
|
||||||
|
discounts: selectedRegistrationDiscountsIds
|
||||||
};
|
};
|
||||||
|
|
||||||
createRegisterForm(data,csrfToken)
|
createRegisterForm(data,csrfToken)
|
||||||
@ -379,7 +415,9 @@ useEffect(()=>{
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
sibling: []
|
sibling: []
|
||||||
}
|
},
|
||||||
|
fees: selectedRegistrationFeesIds,
|
||||||
|
discounts: selectedRegistrationDiscountsIds
|
||||||
};
|
};
|
||||||
|
|
||||||
createRegisterForm(data,csrfToken)
|
createRegisterForm(data,csrfToken)
|
||||||
@ -784,6 +822,10 @@ const handleFileUpload = ({file, name, is_required, order}) => {
|
|||||||
size='sm:w-1/4'
|
size='sm:w-1/4'
|
||||||
ContentComponent={() => (
|
ContentComponent={() => (
|
||||||
<InscriptionForm students={students}
|
<InscriptionForm students={students}
|
||||||
|
registrationDiscounts={registrationDiscounts}
|
||||||
|
tuitionDiscounts={tuitionDiscounts}
|
||||||
|
registrationFees={registrationFees}
|
||||||
|
tuitionFees={tuitionFees}
|
||||||
onSubmit={createRF}
|
onSubmit={createRF}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
39
Front-End/src/components/CheckBox.js
Normal file
39
Front-End/src/components/CheckBox.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const CheckBox = ({ item, formData, handleChange, fieldName, itemLabelFunc = () => null, labelAttenuated = () => false, horizontal }) => {
|
||||||
|
const isChecked = formData[fieldName].includes(parseInt(item.id));
|
||||||
|
const isAttenuated = labelAttenuated(item) && !isChecked;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.id} className={`flex ${horizontal ? 'flex-col items-center' : 'flex-row items-center'}`}>
|
||||||
|
{horizontal && (
|
||||||
|
<label
|
||||||
|
htmlFor={`${fieldName}-${item.id}`}
|
||||||
|
className={`block text-sm text-center mb-1 ${isAttenuated ? 'text-gray-300' : 'font-bold text-emerald-600'}`}
|
||||||
|
>
|
||||||
|
{itemLabelFunc(item)}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`${fieldName}-${item.id}`}
|
||||||
|
name={fieldName}
|
||||||
|
value={item.id}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={`form-checkbox h-4 w-4 rounded-mg text-emerald-600 hover:ring-emerald-400 checked:bg-emerald-600 hover:border-emerald-500 hover:bg-emerald-500 cursor-pointer ${horizontal ? 'mt-1' : 'mr-2'}`}
|
||||||
|
style={{ borderRadius: '6px', outline: 'none', boxShadow: 'none' }}
|
||||||
|
/>
|
||||||
|
{!horizontal && (
|
||||||
|
<label
|
||||||
|
htmlFor={`${fieldName}-${item.id}`}
|
||||||
|
className={`block text-sm ${isAttenuated ? 'text-gray-300' : 'font-bold text-emerald-600'}`}
|
||||||
|
>
|
||||||
|
{itemLabelFunc(item)}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckBox;
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import CheckBox from '@/components/CheckBox';
|
||||||
|
|
||||||
const CheckBoxList = ({
|
const CheckBoxList = ({
|
||||||
items,
|
items,
|
||||||
@ -12,10 +13,6 @@ const CheckBoxList = ({
|
|||||||
labelAttenuated = () => false,
|
labelAttenuated = () => false,
|
||||||
horizontal = false // Ajouter l'option horizontal
|
horizontal = false // Ajouter l'option horizontal
|
||||||
}) => {
|
}) => {
|
||||||
const handleCheckboxChange = (e) => {
|
|
||||||
handleChange(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`mb-4 ${className}`}>
|
<div className={`mb-4 ${className}`}>
|
||||||
<label className="block text-sm font-medium text-gray-700 flex items-center">
|
<label className="block text-sm font-medium text-gray-700 flex items-center">
|
||||||
@ -23,45 +20,18 @@ const CheckBoxList = ({
|
|||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<div className={`mt-2 grid ${horizontal ? 'grid-cols-6 gap-2' : 'grid-cols-1 gap-4'}`}>
|
<div className={`mt-2 grid ${horizontal ? 'grid-cols-6 gap-2' : 'grid-cols-1 gap-4'}`}>
|
||||||
{items.map(item => {
|
{items.map(item => (
|
||||||
const isChecked = formData[fieldName].includes(parseInt(item.id));
|
<CheckBox
|
||||||
const isAttenuated = labelAttenuated(item) && !isChecked;
|
key={item.id}
|
||||||
return (
|
item={item}
|
||||||
<div key={item.id} className={`flex ${horizontal ? 'flex-col items-center' : 'flex-row items-center'}`}>
|
formData={formData}
|
||||||
{horizontal && (
|
handleChange={handleChange}
|
||||||
<label
|
fieldName={fieldName}
|
||||||
htmlFor={`${fieldName}-${item.id}`}
|
itemLabelFunc={itemLabelFunc}
|
||||||
className={`block text-sm text-center mb-1 ${
|
labelAttenuated={labelAttenuated}
|
||||||
isAttenuated ? 'text-gray-300' : 'font-bold text-emerald-600'
|
horizontal={horizontal}
|
||||||
}`}
|
/>
|
||||||
>
|
))}
|
||||||
{itemLabelFunc(item)}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
key={`${item.id}-${Math.random()}`}
|
|
||||||
type="checkbox"
|
|
||||||
id={`${fieldName}-${item.id}`}
|
|
||||||
name={fieldName}
|
|
||||||
value={item.id}
|
|
||||||
checked={isChecked}
|
|
||||||
onChange={handleCheckboxChange}
|
|
||||||
className={`form-checkbox h-4 w-4 rounded-mg text-emerald-600 hover:ring-emerald-400 checked:bg-emerald-600 hover:border-emerald-500 hover:bg-emerald-500 cursor-pointer ${horizontal ? 'mt-1' : 'mr-2'}`}
|
|
||||||
style={{ borderRadius: '6px', outline: 'none', boxShadow: 'none' }}
|
|
||||||
/>
|
|
||||||
{!horizontal && (
|
|
||||||
<label
|
|
||||||
htmlFor={`${fieldName}-${item.id}`}
|
|
||||||
className={`block text-sm ${
|
|
||||||
isAttenuated ? 'text-gray-300' : 'font-bold text-emerald-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{itemLabelFunc(item)}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { User, Mail, Phone, UserCheck } from 'lucide-react';
|
import { User, Mail, Phone, UserCheck, DollarSign, Percent } from 'lucide-react';
|
||||||
import InputTextIcon from '@/components/InputTextIcon';
|
import InputTextIcon from '@/components/InputTextIcon';
|
||||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
import ToggleSwitch from '@/components/ToggleSwitch';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import FeesSection from '@/components/Structure/Tarification/FeesSection';
|
||||||
|
import DiscountsSection from '../Structure/Tarification/DiscountsSection';
|
||||||
|
|
||||||
const InscriptionForm = ( { students, onSubmit }) => {
|
const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, onSubmit }) => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
studentLastName: '',
|
studentLastName: '',
|
||||||
studentFirstName: '',
|
studentFirstName: '',
|
||||||
@ -12,14 +15,26 @@ const InscriptionForm = ( { students, onSubmit }) => {
|
|||||||
guardianPhone: '',
|
guardianPhone: '',
|
||||||
selectedGuardians: [],
|
selectedGuardians: [],
|
||||||
responsableType: 'new',
|
responsableType: 'new',
|
||||||
autoMail: false
|
autoMail: false,
|
||||||
|
selectedRegistrationDiscounts: [],
|
||||||
|
selectedRegistrationFees: registrationFees.map(fee => fee.id)
|
||||||
});
|
});
|
||||||
|
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(0);
|
||||||
const [selectedStudent, setSelectedEleve] = useState('');
|
const [selectedStudent, setSelectedEleve] = useState('');
|
||||||
const [existingGuardians, setExistingGuardians] = useState([]);
|
const [existingGuardians, setExistingGuardians] = useState([]);
|
||||||
|
const [totalRegistrationAmount, setTotalRegistrationAmount] = useState(0);
|
||||||
const maxStep = 4
|
const maxStep = 4
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Calcul du montant total lors de l'initialisation
|
||||||
|
const initialTotalAmount = calculateFinalRegistrationAmount(
|
||||||
|
registrationFees.map(fee => fee.id),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
setTotalRegistrationAmount(initialTotalAmount);
|
||||||
|
}, [registrationDiscounts, registrationFees]);
|
||||||
|
|
||||||
const handleToggleChange = () => {
|
const handleToggleChange = () => {
|
||||||
setFormData({ ...formData, autoMail: !formData.autoMail });
|
setFormData({ ...formData, autoMail: !formData.autoMail });
|
||||||
};
|
};
|
||||||
@ -39,7 +54,7 @@ const InscriptionForm = ( { students, onSubmit }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const prevStep = () => {
|
const prevStep = () => {
|
||||||
if (step > 1) {
|
if (step >= 1) {
|
||||||
setStep(step - 1);
|
setStep(step - 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -66,8 +81,122 @@ const InscriptionForm = ( { students, onSubmit }) => {
|
|||||||
onSubmit(formData);
|
onSubmit(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleFeeSelection = (feeId) => {
|
||||||
|
setFormData((prevData) => {
|
||||||
|
const selectedRegistrationFees = prevData.selectedRegistrationFees.includes(feeId)
|
||||||
|
? prevData.selectedRegistrationFees.filter(id => id !== feeId)
|
||||||
|
: [...prevData.selectedRegistrationFees, feeId];
|
||||||
|
const finalAmount = calculateFinalRegistrationAmount(selectedRegistrationFees, prevData.selectedRegistrationDiscounts);
|
||||||
|
setTotalRegistrationAmount(finalAmount);
|
||||||
|
return { ...prevData, selectedRegistrationFees };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDiscountSelection = (discountId) => {
|
||||||
|
setFormData((prevData) => {
|
||||||
|
const selectedRegistrationDiscounts = prevData.selectedRegistrationDiscounts.includes(discountId)
|
||||||
|
? prevData.selectedRegistrationDiscounts.filter(id => id !== discountId)
|
||||||
|
: [...prevData.selectedRegistrationDiscounts, discountId];
|
||||||
|
const finalAmount = calculateFinalRegistrationAmount(prevData.selectedRegistrationFees, selectedRegistrationDiscounts);
|
||||||
|
setTotalRegistrationAmount(finalAmount);
|
||||||
|
return { ...prevData, selectedRegistrationDiscounts };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateFinalRegistrationAmount = (selectedRegistrationFees, selectedRegistrationDiscounts) => {
|
||||||
|
const totalFees = selectedRegistrationFees.reduce((sum, feeId) => {
|
||||||
|
const fee = registrationFees.find(f => f.id === feeId);
|
||||||
|
if (fee && !isNaN(parseFloat(fee.base_amount))) {
|
||||||
|
return sum + parseFloat(fee.base_amount);
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
console.log(totalFees);
|
||||||
|
|
||||||
|
const totalDiscounts = selectedRegistrationDiscounts.reduce((sum, discountId) => {
|
||||||
|
const discount = registrationDiscounts.find(d => d.id === discountId);
|
||||||
|
if (discount) {
|
||||||
|
if (discount.discount_type === 0 && !isNaN(parseFloat(discount.amount))) { // Currency
|
||||||
|
return sum + parseFloat(discount.amount);
|
||||||
|
} else if (discount.discount_type === 1 && !isNaN(parseFloat(discount.amount))) { // Percent
|
||||||
|
return sum + (totalFees * parseFloat(discount.amount) / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const finalAmount = totalFees - totalDiscounts;
|
||||||
|
|
||||||
|
return finalAmount.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLabelAttenuated = (item) => {
|
||||||
|
return !formData.selectedRegistrationDiscounts.includes(parseInt(item.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLabelFunction = (item) => {
|
||||||
|
return item.name + ' : ' + item.amount
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 mt-8">
|
<div className="space-y-4 mt-8">
|
||||||
|
{step === 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-l font-bold mb-4">Frais d'inscription</h2>
|
||||||
|
{registrationFees.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="mb-4">
|
||||||
|
<FeesSection
|
||||||
|
fees={registrationFees}
|
||||||
|
type={0}
|
||||||
|
subscriptionMode={true}
|
||||||
|
selectedFees={formData.selectedRegistrationFees}
|
||||||
|
handleFeeSelection={handleFeeSelection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-l font-bold mb-4">Réductions</h2>
|
||||||
|
<div className="mb-4">
|
||||||
|
{registrationDiscounts.length > 0 ? (
|
||||||
|
<DiscountsSection
|
||||||
|
discounts={registrationDiscounts}
|
||||||
|
type={0}
|
||||||
|
subscriptionMode={true}
|
||||||
|
selectedDiscounts={formData.selectedRegistrationDiscounts}
|
||||||
|
handleDiscountSelection={handleDiscountSelection}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="bg-orange-100 border border-orange-400 text-orange-700 px-4 py-3 rounded relative" role="alert">
|
||||||
|
<strong className="font-bold">Information</strong>
|
||||||
|
<span className="block sm:inline"> Aucune réduction n'a été créée sur les frais d'inscription.</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
data={[ {id: 1}]}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
name: 'LIBELLE',
|
||||||
|
transform: () => <span>MONTANT TOTAL</span>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TOTAL',
|
||||||
|
transform: () => <b>{totalRegistrationAmount} €</b>
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
defaultTheme='bg-cyan-100'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
|
||||||
|
<strong className="font-bold">Attention!</strong>
|
||||||
|
<span className="block sm:inline"> Aucun frais d'inscription n'a été créé.</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-l font-bold mb-4">Nouvel élève</h2>
|
<h2 className="text-l font-bold mb-4">Nouvel élève</h2>
|
||||||
@ -270,7 +399,7 @@ const InscriptionForm = ( { students, onSubmit }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end mt-4 space-x-4">
|
<div className="flex justify-end mt-4 space-x-4">
|
||||||
{step > 1 && (
|
{step >= 1 && (
|
||||||
<Button text="Précédent"
|
<Button text="Précédent"
|
||||||
onClick={prevStep}
|
onClick={prevStep}
|
||||||
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md shadow-sm hover:bg-gray-400 focus:outline-none"
|
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md shadow-sm hover:bg-gray-400 focus:outline-none"
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-rea
|
|||||||
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 CheckBox from '@/components/CheckBox';
|
||||||
|
|
||||||
const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type }) => {
|
const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedDiscounts, handleDiscountSelection }) => {
|
||||||
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({});
|
||||||
@ -154,7 +155,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
|||||||
return discount.name;
|
return discount.name;
|
||||||
case 'REMISE':
|
case 'REMISE':
|
||||||
return discount.discount_type === 0 ? `${discount.amount} €` : `${discount.amount} %`;
|
return discount.discount_type === 0 ? `${discount.amount} €` : `${discount.amount} %`;
|
||||||
case 'DESCRIPTION':
|
case 'DESCRIPTION':
|
||||||
return discount.description;
|
return discount.description;
|
||||||
case 'MISE A JOUR':
|
case 'MISE A JOUR':
|
||||||
return discount.updated_at_formatted;
|
return discount.updated_at_formatted;
|
||||||
@ -184,32 +185,54 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case '':
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<CheckBox
|
||||||
|
item={discount}
|
||||||
|
formData={{ selectedDiscounts }}
|
||||||
|
handleChange={() => handleDiscountSelection(discount.id)}
|
||||||
|
fieldName="selectedDiscounts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const columns = subscriptionMode
|
||||||
|
? [
|
||||||
|
{ name: 'LIBELLE', label: 'Libellé' },
|
||||||
|
{ name: 'DESCRIPTION', label: 'Description' },
|
||||||
|
{ name: 'REMISE', label: 'Remise' },
|
||||||
|
{ name: '', label: 'Sélection' }
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ name: 'LIBELLE', label: 'Libellé' },
|
||||||
|
{ name: 'REMISE', label: 'Remise' },
|
||||||
|
{ name: 'DESCRIPTION', label: 'Description' },
|
||||||
|
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||||
|
{ name: 'ACTIONS', label: 'Actions' }
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
{!subscriptionMode && (
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex justify-between items-center">
|
||||||
<Tag className="w-6 h-6 text-emerald-500 mr-2" />
|
<div className="flex items-center mb-4">
|
||||||
<h2 className="text-xl font-semibold">Réductions {type === 0 ? 'd\'inscription' : 'de scolarité'}</h2>
|
<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">
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700">
|
)}
|
||||||
<Plus className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Table
|
<Table
|
||||||
data={newDiscount ? [newDiscount, ...discounts] : discounts}
|
data={newDiscount ? [newDiscount, ...discounts] : discounts}
|
||||||
columns={[
|
columns={columns}
|
||||||
{ name: 'LIBELLE', label: 'Libellé' },
|
|
||||||
{ name: 'REMISE', label: 'Valeur' },
|
|
||||||
{ name: 'DESCRIPTION', label: 'Description' },
|
|
||||||
{ name: 'MISE A JOUR', label: 'date mise à jour' },
|
|
||||||
{ name: 'ACTIONS', label: 'Actions' }
|
|
||||||
]}
|
|
||||||
renderCell={renderDiscountCell}
|
renderCell={renderDiscountCell}
|
||||||
defaultTheme='bg-yellow-100'
|
defaultTheme='bg-yellow-100'
|
||||||
/>
|
/>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FeesSection from '@/components/Structure/Configuration/FeesSection';
|
import FeesSection from '@/components/Structure/Tarification/FeesSection';
|
||||||
import DiscountsSection from '@/components/Structure/Configuration/DiscountsSection';
|
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
|
||||||
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 = ({ registrationDiscounts, setRegistrationDiscounts, tuitionDiscounts, setTuitionDiscounts, registrationFees, setRegistrationFees, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => {
|
const FeesManagement = ({ registrationDiscounts, setRegistrationDiscounts, tuitionDiscounts, setTuitionDiscounts, registrationFees, setRegistrationFees, tuitionFees, setTuitionFees, handleCreate, handleEdit, handleDelete }) => {
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Plus, Trash, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react';
|
import { Plus, Trash, Edit3, Check, X, EyeOff, Eye, CreditCard, BookOpen } 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 CheckBox from '@/components/CheckBox';
|
||||||
|
|
||||||
const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type }) => {
|
const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedFees, handleFeeSelection }) => {
|
||||||
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({});
|
||||||
@ -122,24 +123,6 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
|||||||
</div>
|
</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 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;
|
||||||
@ -211,14 +194,41 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case '':
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<CheckBox
|
||||||
|
item={fee}
|
||||||
|
formData={{ selectedFees }}
|
||||||
|
handleChange={() => handleFeeSelection(fee.id)}
|
||||||
|
fieldName="selectedFees"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const columns = subscriptionMode
|
||||||
|
? [
|
||||||
|
{ name: 'NOM', label: 'Nom' },
|
||||||
|
{ name: 'DESCRIPTION', label: 'Description' },
|
||||||
|
{ name: 'MONTANT', label: 'Montant de base' },
|
||||||
|
{ name: '', label: 'Sélection' }
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ name: 'NOM', label: 'Nom' },
|
||||||
|
{ name: 'MONTANT', label: 'Montant de base' },
|
||||||
|
{ name: 'DESCRIPTION', label: 'Description' },
|
||||||
|
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||||
|
{ name: 'ACTIONS', label: 'Actions' }
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{!subscriptionMode && (
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" />
|
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" />
|
||||||
@ -228,15 +238,10 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
|||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<Table
|
<Table
|
||||||
data={newFee ? [newFee, ...fees] : fees}
|
data={newFee ? [newFee, ...fees] : fees}
|
||||||
columns={[
|
columns={columns}
|
||||||
{ name: 'NOM', label: 'Nom' },
|
|
||||||
{ name: 'MONTANT', label: 'Montant de base' },
|
|
||||||
{ name: 'DESCRIPTION', label: 'Description' },
|
|
||||||
{ name: 'MISE A JOUR', label: 'date mise à jour' },
|
|
||||||
{ name: 'ACTIONS', label: 'Actions' }
|
|
||||||
]}
|
|
||||||
renderCell={renderFeeCell}
|
renderCell={renderFeeCell}
|
||||||
/>
|
/>
|
||||||
<Popup
|
<Popup
|
||||||
Reference in New Issue
Block a user