feat: Ajout des frais d'inscription lors de la création d'un RF [#18]

This commit is contained in:
N3WT DE COMPET
2025-01-25 16:40:08 +01:00
parent 799e1c6717
commit ece23deb19
12 changed files with 333 additions and 136 deletions

View File

@ -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';

View File

@ -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={() => (
<InscriptionForm students={students}
registrationDiscounts={registrationDiscounts}
tuitionDiscounts={tuitionDiscounts}
registrationFees={registrationFees}
tuitionFees={tuitionFees}
onSubmit={createRF}
/>
)}

View 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;

View File

@ -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 (
<div className={`mb-4 ${className}`}>
<label className="block text-sm font-medium text-gray-700 flex items-center">
@ -23,45 +20,18 @@ const CheckBoxList = ({
{label}
</label>
<div className={`mt-2 grid ${horizontal ? 'grid-cols-6 gap-2' : 'grid-cols-1 gap-4'}`}>
{items.map(item => {
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
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>
);
})}
{items.map(item => (
<CheckBox
key={item.id}
item={item}
formData={formData}
handleChange={handleChange}
fieldName={fieldName}
itemLabelFunc={itemLabelFunc}
labelAttenuated={labelAttenuated}
horizontal={horizontal}
/>
))}
</div>
</div>
);

View File

@ -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 (
<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 && (
<div>
<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">
{step > 1 && (
{step >= 1 && (
<Button text="Précédent"
onClick={prevStep}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md shadow-sm hover:bg-gray-400 focus:outline-none"

View File

@ -3,8 +3,9 @@ import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-rea
import Table from '@/components/Table';
import InputTextIcon from '@/components/InputTextIcon';
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 [newDiscount, setNewDiscount] = useState(null);
const [formData, setFormData] = useState({});
@ -154,7 +155,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
return discount.name;
case 'REMISE':
return discount.discount_type === 0 ? `${discount.amount}` : `${discount.amount} %`;
case 'DESCRIPTION':
case 'DESCRIPTION':
return discount.description;
case 'MISE A JOUR':
return discount.updated_at_formatted;
@ -184,32 +185,54 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
</button>
</div>
);
case '':
return (
<div className="flex justify-center">
<CheckBox
item={discount}
formData={{ selectedDiscounts }}
handleChange={() => handleDiscountSelection(discount.id)}
fieldName="selectedDiscounts"
/>
</div>
);
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 (
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<Tag className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Réductions {type === 0 ? 'd\'inscription' : 'de scolarité'}</h2>
{!subscriptionMode && (
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<Tag className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Réductions {type === 0 ? 'd\'inscription' : 'de scolarité'}</h2>
</div>
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
)}
<Table
data={newDiscount ? [newDiscount, ...discounts] : discounts}
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' }
]}
columns={columns}
renderCell={renderDiscountCell}
defaultTheme='bg-yellow-100'
/>

View File

@ -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 }) => {

View File

@ -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
</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 isEditing = editingFee === fee.id;
const isCreating = newFee && newFee.id === fee.id;
@ -211,14 +194,41 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
</button>
</div>
);
case '':
return (
<div className="flex justify-center">
<CheckBox
item={fee}
formData={{ selectedFees }}
handleChange={() => handleFeeSelection(fee.id)}
fieldName="selectedFees"
/>
</div>
);
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 (
<div className="space-y-4">
{!subscriptionMode && (
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<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" />
</button>
</div>
)}
<Table
data={newFee ? [newFee, ...fees] : fees}
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' }
]}
columns={columns}
renderCell={renderFeeCell}
/>
<Popup