diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 7c7f609..f549d77 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -96,7 +96,6 @@ class Fee(models.Model): name = models.CharField(max_length=255, unique=True) base_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) description = models.TextField(blank=True) - discounts = models.ManyToManyField('Discount', blank=True) is_active = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) type = models.IntegerField(choices=FeeType.choices, default=FeeType.REGISTRATION_FEE) diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index a4e2a90..7c63f88 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,8 +1,5 @@ from rest_framework import serializers 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 N3wtSchool import settings, bdd from django.utils import timezone @@ -187,41 +184,12 @@ class DiscountSerializer(serializers.ModelSerializer): return local_time.strftime("%d-%m-%Y %H:%M") class FeeSerializer(serializers.ModelSerializer): - discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) updated_at_formatted = serializers.SerializerMethodField() class Meta: model = Fee 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): utc_time = timezone.localtime(obj.updated_at) local_tz = pytz.timezone(settings.TZ_APPLI) diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index b183107..fe7d2af 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -4,7 +4,7 @@ from django.conf import settings from django.utils.translation import gettext_lazy as _ from Auth.models import Profile -from School.models import SchoolClass +from School.models import SchoolClass, Fee, Discount from datetime import datetime @@ -204,6 +204,12 @@ class RegistrationForm(models.Model): registration_file = models.FileField(upload_to=settings.DOCUMENT_DIR, 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): return "RF_" + self.student.last_name + "_" + self.student.first_name diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index b4a996c..2e916a7 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers 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.serializers import ProfileSerializer from GestionMessagerie.models import Messagerie @@ -133,6 +134,9 @@ class RegistrationFormSerializer(serializers.ModelSerializer): status_label = serializers.SerializerMethodField() formatted_last_update = serializers.SerializerMethodField() 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: model = RegistrationForm fields = '__all__' @@ -140,11 +144,19 @@ class RegistrationFormSerializer(serializers.ModelSerializer): def create(self, validated_data): student_data = validated_data.pop('student') 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) + + # 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 def update(self, instance, validated_data): student_data = validated_data.pop('student', None) + fees_data = validated_data.pop('fees', []) + discounts_data = validated_data.pop('discounts', []) if student_data: student = instance.student StudentSerializer.update(StudentSerializer(), student, student_data) @@ -156,6 +168,10 @@ class RegistrationFormSerializer(serializers.ModelSerializer): pass 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 def get_status_label(self, obj): diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 545a112..e76e5ed 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import StructureManagement from '@/components/Structure/Configuration/StructureManagement'; 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 useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 5ddb2da..641d223 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -32,7 +32,13 @@ import { fetchStudents, 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 { @@ -75,6 +81,11 @@ export default function Page({ params: { locale } }) { const [isEditing, setIsEditing] = useState(false); 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 openModal = () => { @@ -151,6 +162,7 @@ const registerFormArchivedDataHandler = (data) => { } } } + // TODO: revoir le système de pagination et de UseEffect useEffect(() => { @@ -195,7 +207,27 @@ const registerFormArchivedDataHandler = (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 { setTimeout(() => { setRegistrationFormsDataPending(mockFicheInscription); @@ -321,6 +353,8 @@ useEffect(()=>{ const createRF = (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) { const selectedGuardiansIds = updatedData.selectedGuardians.map(guardianId => guardianId) @@ -330,7 +364,9 @@ useEffect(()=>{ last_name: updatedData.studentLastName, first_name: updatedData.studentFirstName, }, - idGuardians: selectedGuardiansIds + idGuardians: selectedGuardiansIds, + fees: selectedRegistrationFeesIds, + discounts: selectedRegistrationDiscountsIds }; createRegisterForm(data,csrfToken) @@ -379,7 +415,9 @@ useEffect(()=>{ } ], sibling: [] - } + }, + fees: selectedRegistrationFeesIds, + discounts: selectedRegistrationDiscountsIds }; createRegisterForm(data,csrfToken) @@ -784,6 +822,10 @@ const handleFileUpload = ({file, name, is_required, order}) => { size='sm:w-1/4' ContentComponent={() => ( )} diff --git a/Front-End/src/components/CheckBox.js b/Front-End/src/components/CheckBox.js new file mode 100644 index 0000000..3818736 --- /dev/null +++ b/Front-End/src/components/CheckBox.js @@ -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 ( +
+ {horizontal && ( + + )} + + {!horizontal && ( + + )} +
+ ); +}; + +export default CheckBox; \ No newline at end of file diff --git a/Front-End/src/components/CheckBoxList.js b/Front-End/src/components/CheckBoxList.js index 9514f99..8ff7a8c 100644 --- a/Front-End/src/components/CheckBoxList.js +++ b/Front-End/src/components/CheckBoxList.js @@ -1,4 +1,5 @@ import React from 'react'; +import CheckBox from '@/components/CheckBox'; const CheckBoxList = ({ items, @@ -12,10 +13,6 @@ const CheckBoxList = ({ labelAttenuated = () => false, horizontal = false // Ajouter l'option horizontal }) => { - const handleCheckboxChange = (e) => { - handleChange(e); - }; - return (
- {items.map(item => { - const isChecked = formData[fieldName].includes(parseInt(item.id)); - const isAttenuated = labelAttenuated(item) && !isChecked; - return ( -
- {horizontal && ( - - )} - - {!horizontal && ( - - )} -
- ); - })} + {items.map(item => ( + + ))}
); diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index 9414a19..8895f33 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -1,10 +1,13 @@ -import { useState } from 'react'; -import { User, Mail, Phone, UserCheck } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { User, Mail, Phone, UserCheck, DollarSign, Percent } from 'lucide-react'; import InputTextIcon from '@/components/InputTextIcon'; import ToggleSwitch from '@/components/ToggleSwitch'; 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({ studentLastName: '', studentFirstName: '', @@ -12,14 +15,26 @@ const InscriptionForm = ( { students, onSubmit }) => { guardianPhone: '', selectedGuardians: [], 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 [existingGuardians, setExistingGuardians] = useState([]); + const [totalRegistrationAmount, setTotalRegistrationAmount] = useState(0); 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 = () => { setFormData({ ...formData, autoMail: !formData.autoMail }); }; @@ -39,7 +54,7 @@ const InscriptionForm = ( { students, onSubmit }) => { }; const prevStep = () => { - if (step > 1) { + if (step >= 1) { setStep(step - 1); } }; @@ -66,8 +81,122 @@ const InscriptionForm = ( { students, onSubmit }) => { 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 (
+ {step === 0 && ( +
+

Frais d'inscription

+ {registrationFees.length > 0 ? ( + <> +
+ +
+

Réductions

+
+ {registrationDiscounts.length > 0 ? ( + + ) : ( +

+ Information + Aucune réduction n'a été créée sur les frais d'inscription. +

+ )} +
+ MONTANT TOTAL + }, + { + name: 'TOTAL', + transform: () => {totalRegistrationAmount} € + } + ]} + defaultTheme='bg-cyan-100' + /> + + ) : ( +

+ Attention! + Aucun frais d'inscription n'a été créé. +

+ )} + + + )} + {step === 1 && (

Nouvel élève

@@ -270,7 +399,7 @@ const InscriptionForm = ( { students, onSubmit }) => { )}
- {step > 1 && ( + {step >= 1 && (
); + case '': + return ( +
+ handleDiscountSelection(discount.id)} + fieldName="selectedDiscounts" + /> +
+ ); default: 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 (
-
-
- -

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

+ {!subscriptionMode && ( +
+
+ +

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

+
+
- -
+ )}
diff --git a/Front-End/src/components/Structure/Configuration/FeesManagement.js b/Front-End/src/components/Structure/Tarification/FeesManagement.js similarity index 95% rename from Front-End/src/components/Structure/Configuration/FeesManagement.js rename to Front-End/src/components/Structure/Tarification/FeesManagement.js index bbaba49..d83ac9e 100644 --- a/Front-End/src/components/Structure/Configuration/FeesManagement.js +++ b/Front-End/src/components/Structure/Tarification/FeesManagement.js @@ -1,6 +1,6 @@ import React from 'react'; -import FeesSection from '@/components/Structure/Configuration/FeesSection'; -import DiscountsSection from '@/components/Structure/Configuration/DiscountsSection'; +import FeesSection from '@/components/Structure/Tarification/FeesSection'; +import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection'; 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 }) => { diff --git a/Front-End/src/components/Structure/Configuration/FeesSection.js b/Front-End/src/components/Structure/Tarification/FeesSection.js similarity index 86% rename from Front-End/src/components/Structure/Configuration/FeesSection.js rename to Front-End/src/components/Structure/Tarification/FeesSection.js index cbfe960..25669ab 100644 --- a/Front-End/src/components/Structure/Configuration/FeesSection.js +++ b/Front-End/src/components/Structure/Tarification/FeesSection.js @@ -1,10 +1,11 @@ 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 InputTextIcon from '@/components/InputTextIcon'; 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 [newFee, setNewFee] = useState(null); const [formData, setFormData] = useState({}); @@ -122,24 +123,6 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl ); - 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 isEditing = editingFee === fee.id; const isCreating = newFee && newFee.id === fee.id; @@ -211,14 +194,41 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl ); + case '': + return ( +
+ handleFeeSelection(fee.id)} + fieldName="selectedFees" + /> +
+ ); default: 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 (
+ {!subscriptionMode && (
@@ -228,15 +238,10 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
+ )}