mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Ajout des frais d'inscription lors de la création d'un RF [#18]
This commit is contained in:
@ -1,227 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Trash, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type }) => {
|
||||
const [editingDiscount, setEditingDiscount] = useState(null);
|
||||
const [newDiscount, setNewDiscount] = useState(null);
|
||||
const [formData, setFormData] = useState({});
|
||||
const [localErrors, setLocalErrors] = useState({});
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
|
||||
const handleAddDiscount = () => {
|
||||
setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discount_type: 0, type: type });
|
||||
};
|
||||
|
||||
const handleRemoveDiscount = (id) => {
|
||||
handleDelete(id)
|
||||
.then(() => {
|
||||
setDiscounts(prevDiscounts => prevDiscounts.filter(discount => discount.id !== id));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveNewDiscount = () => {
|
||||
if (newDiscount.name && newDiscount.amount) {
|
||||
handleCreate(newDiscount)
|
||||
.then((createdDiscount) => {
|
||||
setDiscounts([createdDiscount, ...discounts]);
|
||||
setNewDiscount(null);
|
||||
setLocalErrors({});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error && typeof error === 'object') {
|
||||
setLocalErrors(error);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis");
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateDiscount = (id, updatedDiscount) => {
|
||||
if (updatedDiscount.name && updatedDiscount.amount) {
|
||||
handleEdit(id, updatedDiscount)
|
||||
.then(() => {
|
||||
setEditingDiscount(null);
|
||||
setLocalErrors({});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error && typeof error === 'object') {
|
||||
setLocalErrors(error);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis");
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleDiscountType = (id) => {
|
||||
const discount = discounts.find(discount => discount.id === id);
|
||||
if (!discount) return;
|
||||
|
||||
const updatedData = {
|
||||
...discount,
|
||||
discount_type: discount.discount_type === 0 ? 1 : 0
|
||||
};
|
||||
|
||||
handleEdit(id, updatedData)
|
||||
.then(() => {
|
||||
setDiscounts(prevDiscounts => prevDiscounts.map(discount => discount.id === id ? { ...discount, discount_type: updatedData.discount_type } : discount));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
if (editingDiscount) {
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
} else if (newDiscount) {
|
||||
setNewDiscount((prevData) => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const renderInputField = (field, value, onChange, placeholder) => (
|
||||
<div>
|
||||
<InputTextIcon
|
||||
name={field}
|
||||
type={field === 'amount' ? 'number' : 'text'}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderDiscountCell = (discount, column) => {
|
||||
const isEditing = editingDiscount === discount.id;
|
||||
const isCreating = newDiscount && newDiscount.id === discount.id;
|
||||
const currentData = isEditing ? formData : newDiscount;
|
||||
|
||||
if (isEditing || isCreating) {
|
||||
switch (column) {
|
||||
case 'LIBELLE':
|
||||
return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction');
|
||||
case 'REMISE':
|
||||
return renderInputField('amount', currentData.amount, handleChange,'Montant');
|
||||
case 'DESCRIPTION':
|
||||
return renderInputField('description', currentData.description, handleChange, 'Description');
|
||||
case 'ACTIONS':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? handleUpdateDiscount(editingDiscount, formData) : handleSaveNewDiscount())}
|
||||
className="text-green-500 hover:text-green-700"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? setEditingDiscount(null) : setNewDiscount(null))}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
switch (column) {
|
||||
case 'LIBELLE':
|
||||
return discount.name;
|
||||
case 'REMISE':
|
||||
return discount.discount_type === 0 ? `${discount.amount} €` : `${discount.amount} %`;
|
||||
case 'DESCRIPTION':
|
||||
return discount.description;
|
||||
case 'MISE A JOUR':
|
||||
return discount.updated_at_formatted;
|
||||
case 'ACTIONS':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggleDiscountType(discount.id)}
|
||||
className="flex justify-center items-center text-emerald-500 hover:text-emerald-700"
|
||||
>
|
||||
{discount.discount_type === 0 ? <EuroIcon className="w-5 h-5" /> : <Percent className="w-5 h-5" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingDiscount(discount.id) || setFormData(discount)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveDiscount(discount.id)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<Trash className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
</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' }
|
||||
]}
|
||||
renderCell={renderDiscountCell}
|
||||
defaultTheme='bg-yellow-100'
|
||||
/>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscountsSection;
|
||||
@ -1,80 +0,0 @@
|
||||
import React from 'react';
|
||||
import FeesSection from '@/components/Structure/Configuration/FeesSection';
|
||||
import DiscountsSection from '@/components/Structure/Configuration/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 }) => {
|
||||
|
||||
const handleDiscountDelete = (id, type) => {
|
||||
if (type === 0) {
|
||||
setRegistrationFees(prevFees =>
|
||||
prevFees.map(fee => ({
|
||||
...fee,
|
||||
discounts: fee.discounts.filter(discountId => discountId !== id)
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
setTuitionFees(prevFees =>
|
||||
prevFees.map(fee => ({
|
||||
...fee,
|
||||
discounts: fee.discounts.filter(discountId => discountId !== id)
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<FeesSection
|
||||
fees={registrationFees}
|
||||
setFees={setRegistrationFees}
|
||||
discounts={registrationDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setRegistrationFees)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setRegistrationFees)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setRegistrationFees)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<DiscountsSection
|
||||
discounts={registrationDiscounts}
|
||||
setDiscounts={setRegistrationDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setRegistrationDiscounts)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setRegistrationDiscounts)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setRegistrationDiscounts)}
|
||||
onDiscountDelete={(id) => handleDiscountDelete(id, 0)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<FeesSection
|
||||
fees={tuitionFees}
|
||||
setFees={setTuitionFees}
|
||||
discounts={tuitionDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setTuitionFees)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setTuitionFees)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setTuitionFees)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<DiscountsSection
|
||||
discounts={tuitionDiscounts}
|
||||
setDiscounts={setTuitionDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setTuitionDiscounts)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setTuitionDiscounts)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setTuitionDiscounts)}
|
||||
onDiscountDelete={(id) => handleDiscountDelete(id, 1)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeesManagement;
|
||||
@ -1,253 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Trash, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type }) => {
|
||||
const [editingFee, setEditingFee] = useState(null);
|
||||
const [newFee, setNewFee] = useState(null);
|
||||
const [formData, setFormData] = useState({});
|
||||
const [localErrors, setLocalErrors] = useState({});
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState("");
|
||||
|
||||
const handleAddFee = () => {
|
||||
setNewFee({ id: Date.now(), name: '', base_amount: '', description: '', validity_start_date: '', validity_end_date: '', discounts: [], type: type });
|
||||
};
|
||||
|
||||
const handleRemoveFee = (id) => {
|
||||
handleDelete(id)
|
||||
.then(() => {
|
||||
setFees(prevFees => prevFees.filter(fee => fee.id !== id));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveNewFee = () => {
|
||||
if (
|
||||
newFee.name &&
|
||||
newFee.base_amount) {
|
||||
handleCreate(newFee)
|
||||
.then((createdFee) => {
|
||||
setFees([createdFee, ...fees]);
|
||||
setNewFee(null);
|
||||
setLocalErrors({});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error && typeof error === 'object') {
|
||||
setLocalErrors(error);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateFee = (id, updatedFee) => {
|
||||
if (
|
||||
updatedFee.name &&
|
||||
updatedFee.base_amount) {
|
||||
handleEdit(id, updatedFee)
|
||||
.then((updatedFee) => {
|
||||
setFees(fees.map(fee => fee.id === id ? updatedFee : fee));
|
||||
setEditingFee(null);
|
||||
setLocalErrors({});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error && typeof error === 'object') {
|
||||
setLocalErrors(error);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||
setPopupVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
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 { name, value } = e.target;
|
||||
let parsedValue = value;
|
||||
if (name === 'discounts') {
|
||||
parsedValue = value.split(',').map(v => parseInt(v, 10));
|
||||
}
|
||||
if (editingFee) {
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
[name]: parsedValue,
|
||||
}));
|
||||
} else if (newFee) {
|
||||
setNewFee((prevData) => ({
|
||||
...prevData,
|
||||
[name]: parsedValue,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const renderInputField = (field, value, onChange, placeholder) => (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<InputTextIcon
|
||||
name={field}
|
||||
type={field === 'base_amount' ? 'number' : 'text'}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
|
||||
/>
|
||||
</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;
|
||||
const currentData = isEditing ? formData : newFee;
|
||||
|
||||
if (isEditing || isCreating) {
|
||||
switch (column) {
|
||||
case 'NOM':
|
||||
return renderInputField('name', currentData.name, handleChange, 'Nom des frais');
|
||||
case 'MONTANT':
|
||||
return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base');
|
||||
case 'DESCRIPTION':
|
||||
return renderInputField('description', currentData.description, handleChange, 'Description');
|
||||
case 'ACTIONS':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? handleUpdateFee(editingFee, formData) : handleSaveNewFee())}
|
||||
className="text-green-500 hover:text-green-700"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (isEditing ? setEditingFee(null) : setNewFee(null))}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
switch (column) {
|
||||
case 'NOM':
|
||||
return fee.name;
|
||||
case 'MONTANT':
|
||||
return fee.base_amount + ' €';
|
||||
case 'MISE A JOUR':
|
||||
return fee.updated_at_formatted;
|
||||
case 'DESCRIPTION':
|
||||
return fee.description;
|
||||
case 'ACTIONS':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggleActive(fee.id, fee.is_active)}
|
||||
className={`text-${fee.is_active ? 'green' : 'orange'}-500 hover:text-${fee.is_active ? 'green' : 'orange'}-700`}
|
||||
>
|
||||
{fee.is_active ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingFee(fee.id) || setFormData(fee)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFee(fee.id)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<Trash className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<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" />
|
||||
<h2 className="text-xl font-semibold">{type === 0 ? 'Frais d\'inscription' : 'Frais de scolarité'}</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddFee} className="text-emerald-500 hover:text-emerald-700">
|
||||
<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' }
|
||||
]}
|
||||
renderCell={renderFeeCell}
|
||||
/>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeesSection;
|
||||
Reference in New Issue
Block a user