feat: Ajout des payementPlans dans le formulaire / ajout de la photo

This commit is contained in:
N3WT DE COMPET
2025-05-01 20:44:57 +02:00
parent 5851341235
commit d37aed5f64
11 changed files with 182 additions and 21 deletions

View File

@ -3,7 +3,7 @@ from django.utils.timezone import now
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from School.models import SchoolClass, Fee, Discount, PaymentModeType
from School.models import SchoolClass, Fee, Discount, PaymentModeType, PaymentPlanType
from Auth.models import ProfileRole
from Establishment.models import Establishment
@ -229,6 +229,8 @@ class RegistrationForm(models.Model):
establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='register_forms')
registration_payment = models.IntegerField(choices=PaymentModeType.choices, null=True, blank=True)
tuition_payment = models.IntegerField(choices=PaymentModeType.choices, null=True, blank=True)
registration_payment_plan = models.IntegerField(choices=PaymentPlanType.choices, null=True, blank=True)
tuition_payment_plan = models.IntegerField(choices=PaymentPlanType.choices, null=True, blank=True)
def __str__(self):
return "RF_" + self.student.last_name + "_" + self.student.first_name

View File

@ -231,6 +231,7 @@ class RegisterFormWithIdView(APIView):
"""
studentForm_data = request.data.get('data', '{}')
print(f'studentForm_data : {studentForm_data}')
try:
data = json.loads(studentForm_data)
except json.JSONDecodeError:
@ -239,10 +240,16 @@ class RegisterFormWithIdView(APIView):
# Extraire le fichier photo
photo_file = request.FILES.get('photo')
# Extraire le fichier photo
sepa_file = request.FILES.get('sepa_file')
# Ajouter la photo aux données de l'étudiant
if photo_file:
data['student']['photo'] = photo_file
if sepa_file:
data['sepa_file'] = sepa_file
# Gérer le champ `_status`
_status = data.pop('status', 0)
_status = int(_status)
@ -307,7 +314,7 @@ class RegisterFormWithIdView(APIView):
elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED:
# Vérifier si le paramètre fusion est activé via l'URL
fusion = studentForm_data.get('fusion', False)
fusion = data.get('fusion', False)
if fusion:
# Fusion des documents
# Récupération des fichiers schoolFileTemplates

View File

@ -375,8 +375,15 @@ export default function Page({ params: { locale } }) {
return;
}
// Préparer les données JSON
const jsonData = {
status: 7,
};
const formData = new FormData();
formData.append('status', 7);
// Ajouter les données JSON sous forme de chaîne
formData.append('data', JSON.stringify(jsonData));
formData.append('sepa_file', file);
// Appeler l'API pour uploader le fichier SEPA
@ -868,7 +875,7 @@ export default function Page({ params: { locale } }) {
<img
src={`${BASE_URL}${row.student.photo}`}
alt={`${row.student.first_name} ${row.student.last_name}`}
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer"
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer rounded-full"
/>
</a>
) : (

View File

@ -23,10 +23,8 @@ export default function Page() {
const csrfToken = useCsrfToken();
const handleAcceptRF = (data) => {
const { status, fusionParam } = data;
const formData = new FormData();
formData.append('status', status); // Ajoute le statut
formData.append('fusion', fusionParam);
formData.append('data', JSON.stringify(data));
setIsLoading(true);
// Appeler l'API pour mettre à jour le RF

View File

@ -5,7 +5,7 @@ import { useSearchParams, useRouter } from 'next/navigation';
import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext';
import { FE_PARENTS_HOME_URL } from '@/utils/Url';
import { editRegisterForm } from '@/app/actions/subscriptionAction';
import { editRegisterFormWithBinaryFile } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
export default function Page() {
@ -18,7 +18,11 @@ export default function Page() {
const handleSubmit = async (data) => {
try {
const result = await editRegisterForm(studentId, data, csrfToken);
const result = await editRegisterFormWithBinaryFile(
studentId,
data,
csrfToken
);
logger.debug('Success:', result);
router.push(FE_PARENTS_HOME_URL);
} catch (error) {

View File

@ -66,9 +66,12 @@ export default function ParentHomePage() {
return;
}
const jsonData = {
status: 3,
};
const formData = new FormData();
formData.append('data', JSON.stringify(jsonData));
formData.append('sepa_file', uploadedFile); // Ajoute le fichier SEPA
formData.append('status', 3); // Statut à envoyer
editRegisterFormWithBinaryFile(uploadingStudentId, formData, csrfToken)
.then((response) => {

View File

@ -6,6 +6,9 @@ export default function FileUpload({
selectionMessage,
onFileSelect,
uploadedFileName,
existingFile,
required,
errorMsg,
}) {
const [localFileName, setLocalFileName] = useState(uploadedFileName || '');
const fileInputRef = useRef(null); // Utilisation de useRef pour cibler l'input
@ -31,7 +34,10 @@ export default function FileUpload({
return (
<div className="border p-4 rounded-md shadow-md">
<h3 className="text-lg font-semibold mb-4">{`${selectionMessage}`}</h3>
<h3 className="text-lg font-semibold mb-4">
{`${selectionMessage}`}
{required && <span className="text-red-500 ml-1">*</span>}
</h3>
<div
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
onClick={() => fileInputRef.current.click()} // Utilisation de la référence pour ouvrir l'explorateur
@ -52,10 +58,24 @@ export default function FileUpload({
Déposez votre fichier ici
</p>
<p className="text-sm text-gray-500 mt-2">
ou cliquez pour sélectionner un fichier PDF
ou cliquez pour sélectionner un fichier
</p>
</label>
</div>
{/* Affichage du fichier existant */}
{existingFile && !localFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800">
<span className="font-semibold">
{existingFile.split('/').pop()}
</span>
</p>
</div>
)}
{/* Affichage du fichier sélectionné */}
{localFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
@ -64,6 +84,9 @@ export default function FileUpload({
</p>
</div>
)}
{/* Message d'erreur */}
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
);
}

View File

@ -14,6 +14,8 @@ import {
import {
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes,
fetchRegistrationPaymentPlans,
fetchTuitionPaymentPlans,
} from '@/app/actions/schoolAction';
import { BASE_URL } from '@/utils/Url';
import logger from '@/utils/logger';
@ -53,13 +55,14 @@ export default function InscriptionFormShared({
nationality: '',
attending_physician: '',
level: '',
registration_payment: '',
tuition_payment: '',
photo: '',
});
const [guardians, setGuardians] = useState([]);
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]);
const [registrationPaymentPlans, setRegistrationPaymentPlans] = useState([]);
const [tuitionPaymentPlans, setTuitionPaymentPlans] = useState([]);
// États pour la gestion des fichiers
const [uploadedFiles, setUploadedFiles] = useState([]);
@ -194,6 +197,12 @@ export default function InscriptionFormShared({
// Fetch data for tuition payment modes
handleTuitionPaymentModes();
// Fetch data for registration payment plans
handleRegistrationPaymentPlans();
// Fetch data for tuition payment plans
handleTuitionnPaymentPlans();
}
}, [selectedEstablishmentId]);
@ -223,6 +232,32 @@ export default function InscriptionFormShared({
);
};
const handleRegistrationPaymentPlans = () => {
fetchRegistrationPaymentPlans(selectedEstablishmentId)
.then((data) => {
const activePaymentPlans = data.filter(
(mode) => mode.is_active === true
);
setRegistrationPaymentPlans(activePaymentPlans);
})
.catch((error) =>
logger.error('Error fetching registration payment plans:', error)
);
};
const handleTuitionnPaymentPlans = () => {
fetchTuitionPaymentPlans(selectedEstablishmentId)
.then((data) => {
const activePaymentPlans = data.filter(
(mode) => mode.is_active === true
);
setTuitionPaymentPlans(activePaymentPlans);
})
.catch((error) =>
logger.error('Error fetching registration tuition plans:', error)
);
};
const handleFileUpload = (file, selectedFile) => {
if (!file || !selectedFile) {
logger.error('Données manquantes pour le téléversement.');
@ -344,8 +379,12 @@ export default function InscriptionFormShared({
status: isSepaPayment ? 8 : 3,
tuition_payment: formData.tuition_payment,
registration_payment: formData.registration_payment,
tuition_payment_plan: formData.tuition_payment_plan,
registration_payment_plan: formData.registration_payment_plan,
};
console.log('jsonData : ', jsonData);
// Créer un objet FormData
const formDataToSend = new FormData();
@ -450,6 +489,8 @@ export default function InscriptionFormShared({
setFormData={setFormData}
registrationPaymentModes={registrationPaymentModes}
tuitionPaymentModes={tuitionPaymentModes}
registrationPaymentPlans={registrationPaymentPlans}
tuitionPaymentPlans={tuitionPaymentPlans}
errors={errors}
setIsPageValid={setIsPage3Valid}
/>

View File

@ -1,11 +1,14 @@
import React, { useEffect } from 'react';
import SelectChoice from '@/components/SelectChoice';
import RadioList from '@/components/RadioList';
export default function PaymentMethodSelector({
formData,
setFormData,
registrationPaymentModes,
tuitionPaymentModes,
registrationPaymentPlans,
tuitionPaymentPlans,
errors,
setIsPageValid,
}) {
@ -14,6 +17,7 @@ export default function PaymentMethodSelector({
(field) => getLocalError(field) !== ''
);
setIsPageValid(isValid);
console.log('formdata : ', formData);
}, [formData, setIsPageValid]);
const paymentModesOptions = [
@ -23,19 +27,31 @@ export default function PaymentMethodSelector({
{ id: 4, name: 'Espèce' },
];
const paymentPlansOptions = [
{ id: 1, name: '1 fois' },
{ id: 3, name: '3 fois' },
{ id: 10, name: '10 fois' },
{ id: 12, name: '12 fois' },
];
const getError = (field) => {
return errors?.student?.[field]?.[0];
};
const getLocalError = (field) => {
if (
// Student Form
(field === 'registration_payment' &&
(!formData.registration_payment ||
String(formData.registration_payment).trim() === '')) ||
(field === 'tuition_payment' &&
(!formData.tuition_payment ||
String(formData.tuition_payment).trim() === ''))
String(formData.tuition_payment).trim() === '')) ||
(field === 'registration_payment_plan' &&
(!formData.registration_payment_plan ||
String(formData.registration_payment_plan).trim() === '')) ||
(field === 'tuition_payment_plan' &&
(!formData.tuition_payment_plan ||
String(formData.tuition_payment_plan).trim() === ''))
) {
return 'Champs requis';
}
@ -48,13 +64,12 @@ export default function PaymentMethodSelector({
return (
<>
{/* Frais d'inscription */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre */}
<h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
Frais d'inscription
</h2>
{/* Section d'information */}
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
<p className="text-gray-700 text-sm mb-2">
<strong className="text-gray-900">Montant :</strong>{' '}
@ -80,15 +95,42 @@ export default function PaymentMethodSelector({
getLocalError('registration_payment')
}
/>
<RadioList
sectionLabel="Choisissez une option"
required
items={paymentPlansOptions
.filter((option) =>
registrationPaymentPlans.some(
(plan) => plan.frequency === option.id
)
)
.map((option) => ({
id: option.id,
label: option.name,
}))}
formData={{
...formData,
registration_payment_plan: parseInt(
formData.registration_payment_plan,
10
), // S'assurer que la valeur est un entier
}}
handleChange={(e) => {
const value = parseInt(e.target.value, 10);
onChange('registration_payment_plan', value); // Convertir la valeur en entier
}}
fieldName="registration_payment_plan"
className="mt-4"
/>
</div>
{/* Frais de scolarité */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 mt-12">
{/* Titre */}
<h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
Frais de scolarité
</h2>
{/* Section d'information */}
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
<p className="text-gray-700 text-sm mb-2">
<strong className="text-gray-900">Montant :</strong>{' '}
@ -113,6 +155,26 @@ export default function PaymentMethodSelector({
getError('tuition_payment') || getLocalError('tuition_payment')
}
/>
<RadioList
sectionLabel="Choisissez une option"
items={paymentPlansOptions
.filter((option) =>
tuitionPaymentPlans.some((plan) => plan.frequency === option.id)
)
.map((option) => ({
id: option.id,
label: option.name,
}))}
formData={formData}
handleChange={(e) => onChange('tuition_payment_plan', e.target.value)}
fieldName="tuition_payment_plan"
className="mt-4"
errorMsg={
getError('tuition_payment_plan') ||
getLocalError('tuition_payment_plan')
}
/>
</div>
</>
);

View File

@ -51,6 +51,8 @@ export default function StudentInfoForm({
level: data?.student?.level || '',
registration_payment: data?.registration_payment || '',
tuition_payment: data?.tuition_payment || '',
registration_payment_plan: data?.registration_payment_plan || '',
tuition_payment_plan: data?.tuition_payment_plan || '',
totalRegistrationFees: data?.totalRegistrationFees,
totalTuitionFees: data?.totalTuitionFees,
});
@ -96,7 +98,8 @@ export default function StudentInfoForm({
(!formData.attending_physician ||
formData.attending_physician.trim() === '')) ||
(field === 'level' &&
(!formData.level || String(formData.level).trim() === ''))
(!formData.level || String(formData.level).trim() === '')) ||
(field === 'photo' && !formData.photo)
) {
return 'Champs requis';
}
@ -230,6 +233,9 @@ export default function StudentInfoForm({
<FileUpload
selectionMessage="Sélectionnez une photo à uploader"
onFileSelect={(file) => handlePhotoUpload(file)}
existingFile={formData.photo}
required
errorMsg={getError('photo') || getLocalError('photo')}
/>
</div>
</>

View File

@ -7,9 +7,17 @@ const RadioList = ({
fieldName,
icon: Icon,
className,
sectionLabel,
required,
}) => {
return (
<div className={`mb-4 ${className}`}>
{sectionLabel && (
<h3 className="text-lg font-semibold text-gray-800 mb-2">
{sectionLabel}
{required && <span className="text-red-500 ml-1">*</span>}
</h3>
)}
<div className="grid grid-cols-1 gap-4">
{items.map((item) => (
<div key={item.id} className="flex items-center">