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.conf import settings
from django.utils.translation import gettext_lazy as _ 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 Auth.models import ProfileRole
from Establishment.models import Establishment 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') establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='register_forms')
registration_payment = models.IntegerField(choices=PaymentModeType.choices, null=True, blank=True) registration_payment = models.IntegerField(choices=PaymentModeType.choices, null=True, blank=True)
tuition_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): def __str__(self):
return "RF_" + self.student.last_name + "_" + self.student.first_name 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', '{}') studentForm_data = request.data.get('data', '{}')
print(f'studentForm_data : {studentForm_data}')
try: try:
data = json.loads(studentForm_data) data = json.loads(studentForm_data)
except json.JSONDecodeError: except json.JSONDecodeError:
@ -239,10 +240,16 @@ class RegisterFormWithIdView(APIView):
# Extraire le fichier photo # Extraire le fichier photo
photo_file = request.FILES.get('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 # Ajouter la photo aux données de l'étudiant
if photo_file: if photo_file:
data['student']['photo'] = photo_file data['student']['photo'] = photo_file
if sepa_file:
data['sepa_file'] = sepa_file
# Gérer le champ `_status` # Gérer le champ `_status`
_status = data.pop('status', 0) _status = data.pop('status', 0)
_status = int(_status) _status = int(_status)
@ -307,7 +314,7 @@ class RegisterFormWithIdView(APIView):
elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED:
# Vérifier si le paramètre fusion est activé via l'URL # 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: if fusion:
# Fusion des documents # Fusion des documents
# Récupération des fichiers schoolFileTemplates # Récupération des fichiers schoolFileTemplates

View File

@ -375,8 +375,15 @@ export default function Page({ params: { locale } }) {
return; return;
} }
// Préparer les données JSON
const jsonData = {
status: 7,
};
const formData = new FormData(); 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); formData.append('sepa_file', file);
// Appeler l'API pour uploader le fichier SEPA // Appeler l'API pour uploader le fichier SEPA
@ -868,7 +875,7 @@ export default function Page({ params: { locale } }) {
<img <img
src={`${BASE_URL}${row.student.photo}`} src={`${BASE_URL}${row.student.photo}`}
alt={`${row.student.first_name} ${row.student.last_name}`} 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> </a>
) : ( ) : (

View File

@ -23,10 +23,8 @@ export default function Page() {
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const handleAcceptRF = (data) => { const handleAcceptRF = (data) => {
const { status, fusionParam } = data;
const formData = new FormData(); const formData = new FormData();
formData.append('status', status); // Ajoute le statut formData.append('data', JSON.stringify(data));
formData.append('fusion', fusionParam);
setIsLoading(true); setIsLoading(true);
// Appeler l'API pour mettre à jour le RF // 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 { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { FE_PARENTS_HOME_URL } from '@/utils/Url'; 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'; import logger from '@/utils/logger';
export default function Page() { export default function Page() {
@ -18,7 +18,11 @@ export default function Page() {
const handleSubmit = async (data) => { const handleSubmit = async (data) => {
try { try {
const result = await editRegisterForm(studentId, data, csrfToken); const result = await editRegisterFormWithBinaryFile(
studentId,
data,
csrfToken
);
logger.debug('Success:', result); logger.debug('Success:', result);
router.push(FE_PARENTS_HOME_URL); router.push(FE_PARENTS_HOME_URL);
} catch (error) { } catch (error) {

View File

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

View File

@ -6,6 +6,9 @@ export default function FileUpload({
selectionMessage, selectionMessage,
onFileSelect, onFileSelect,
uploadedFileName, uploadedFileName,
existingFile,
required,
errorMsg,
}) { }) {
const [localFileName, setLocalFileName] = useState(uploadedFileName || ''); const [localFileName, setLocalFileName] = useState(uploadedFileName || '');
const fileInputRef = useRef(null); // Utilisation de useRef pour cibler l'input const fileInputRef = useRef(null); // Utilisation de useRef pour cibler l'input
@ -31,7 +34,10 @@ export default function FileUpload({
return ( return (
<div className="border p-4 rounded-md shadow-md"> <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 <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" 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 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 Déposez votre fichier ici
</p> </p>
<p className="text-sm text-gray-500 mt-2"> <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> </p>
</label> </label>
</div> </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 && ( {localFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm"> <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" /> <CloudUpload className="w-6 h-6 text-emerald-500" />
@ -64,6 +84,9 @@ export default function FileUpload({
</p> </p>
</div> </div>
)} )}
{/* Message d'erreur */}
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div> </div>
); );
} }

View File

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

View File

@ -1,11 +1,14 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import SelectChoice from '@/components/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import RadioList from '@/components/RadioList';
export default function PaymentMethodSelector({ export default function PaymentMethodSelector({
formData, formData,
setFormData, setFormData,
registrationPaymentModes, registrationPaymentModes,
tuitionPaymentModes, tuitionPaymentModes,
registrationPaymentPlans,
tuitionPaymentPlans,
errors, errors,
setIsPageValid, setIsPageValid,
}) { }) {
@ -14,6 +17,7 @@ export default function PaymentMethodSelector({
(field) => getLocalError(field) !== '' (field) => getLocalError(field) !== ''
); );
setIsPageValid(isValid); setIsPageValid(isValid);
console.log('formdata : ', formData);
}, [formData, setIsPageValid]); }, [formData, setIsPageValid]);
const paymentModesOptions = [ const paymentModesOptions = [
@ -23,19 +27,31 @@ export default function PaymentMethodSelector({
{ id: 4, name: 'Espèce' }, { 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) => { const getError = (field) => {
return errors?.student?.[field]?.[0]; return errors?.student?.[field]?.[0];
}; };
const getLocalError = (field) => { const getLocalError = (field) => {
if ( if (
// Student Form
(field === 'registration_payment' && (field === 'registration_payment' &&
(!formData.registration_payment || (!formData.registration_payment ||
String(formData.registration_payment).trim() === '')) || String(formData.registration_payment).trim() === '')) ||
(field === 'tuition_payment' && (field === 'tuition_payment' &&
(!formData.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'; return 'Champs requis';
} }
@ -48,13 +64,12 @@ export default function PaymentMethodSelector({
return ( return (
<> <>
{/* Frais d'inscription */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <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"> <h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
Frais d'inscription Frais d'inscription
</h2> </h2>
{/* Section d'information */}
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100"> <div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
<p className="text-gray-700 text-sm mb-2"> <p className="text-gray-700 text-sm mb-2">
<strong className="text-gray-900">Montant :</strong>{' '} <strong className="text-gray-900">Montant :</strong>{' '}
@ -80,15 +95,42 @@ export default function PaymentMethodSelector({
getLocalError('registration_payment') 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> </div>
{/* Frais de scolarité */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 mt-12"> <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"> <h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
Frais de scolarité Frais de scolarité
</h2> </h2>
{/* Section d'information */}
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100"> <div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
<p className="text-gray-700 text-sm mb-2"> <p className="text-gray-700 text-sm mb-2">
<strong className="text-gray-900">Montant :</strong>{' '} <strong className="text-gray-900">Montant :</strong>{' '}
@ -113,6 +155,26 @@ export default function PaymentMethodSelector({
getError('tuition_payment') || getLocalError('tuition_payment') 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> </div>
</> </>
); );

View File

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

View File

@ -7,9 +7,17 @@ const RadioList = ({
fieldName, fieldName,
icon: Icon, icon: Icon,
className, className,
sectionLabel,
required,
}) => { }) => {
return ( return (
<div className={`mb-4 ${className}`}> <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"> <div className="grid grid-cols-1 gap-4">
{items.map((item) => ( {items.map((item) => (
<div key={item.id} className="flex items-center"> <div key={item.id} className="flex items-center">