feat: Ajout de la fratrie [#27]

This commit is contained in:
N3WT DE COMPET
2025-05-02 17:44:35 +02:00
parent 1ced4a1069
commit 4a382d523c
8 changed files with 279 additions and 30 deletions

View File

@ -1,6 +1,7 @@
import React, { useState, useRef } from 'react';
import { CloudUpload } from 'lucide-react';
import logger from '@/utils/logger';
import { BASE_URL } from '@/utils/Url';
export default function FileUpload({
selectionMessage,
@ -69,9 +70,18 @@ export default function FileUpload({
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800">
<span className="font-semibold">
{typeof existingFile === 'string'
? existingFile.split('/').pop()
: existingFile?.name || 'Fichier inconnu'}
{typeof existingFile === 'string' ? (
<a
href={`${BASE_URL}${existingFile}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700 underline"
>
{existingFile.split('/').pop()}
</a>
) : (
existingFile?.name || 'Fichier inconnu'
)}
</span>
</p>
</div>

View File

@ -23,6 +23,7 @@ import FilesToUpload from '@/components/Inscription/FilesToUpload';
import { DocusealForm } from '@docuseal/react';
import StudentInfoForm from '@/components/Inscription/StudentInfoForm';
import ResponsableInputFields from '@/components/Inscription/ResponsableInputFields';
import SiblingInputFields from '@/components/Inscription/SiblingInputFields';
import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
import ProgressStep from '@/components/ProgressStep';
import { CheckCircle, Hourglass } from 'lucide-react';
@ -58,6 +59,7 @@ export default function InscriptionFormShared({
photo: '',
});
const [guardians, setGuardians] = useState([]);
const [siblings, setSiblings] = useState([]);
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]);
@ -75,6 +77,7 @@ export default function InscriptionFormShared({
const [isPage3Valid, setIsPage3Valid] = useState(false);
const [isPage4Valid, setIsPage4Valid] = useState(false);
const [isPage5Valid, setIsPage5Valid] = useState(false);
const [isPage6Valid, setIsPage6Valid] = useState(false);
const [hasInteracted, setHasInteracted] = useState(false);
@ -103,7 +106,7 @@ export default function InscriptionFormShared({
);
// Mettre à jour isPage4Valid en fonction de cette condition
setIsPage4Valid(allSigned);
setIsPage5Valid(allSigned);
if (allSigned) {
setCurrentTemplateIndex(0);
@ -116,8 +119,8 @@ export default function InscriptionFormShared({
(template) => template.file !== null
);
// Mettre à jour isPage5Valid en fonction de cette condition
setIsPage5Valid(allUploaded);
// Mettre à jour isPage6Valid en fonction de cette condition
setIsPage6Valid(allUploaded);
}, [parentFileTemplates]);
const handleTemplateSigned = (index) => {
@ -333,7 +336,7 @@ export default function InscriptionFormShared({
.then((response) => {
logger.debug('Fichier supprimé avec succès dans la base :', response);
setIsPage5Valid(false);
setIsPage6Valid(false);
// Mettre à jour l'état local pour refléter la suppression
setUploadedFiles((prev) =>
@ -374,6 +377,7 @@ export default function InscriptionFormShared({
student: {
...formData,
guardians: guardians,
siblings: siblings,
},
establishment: selectedEstablishmentId,
status: isSepaPayment ? 8 : 3,
@ -383,8 +387,6 @@ export default function InscriptionFormShared({
registration_payment_plan: formData.registration_payment_plan,
};
console.log('jsonData : ', jsonData);
// Créer un objet FormData
const formDataToSend = new FormData();
@ -411,14 +413,16 @@ export default function InscriptionFormShared({
const stepTitles = {
1: 'Elève',
2: 'Responsables légaux',
3: 'Modalités de paiement',
4: 'Formulaires à signer',
5: 'Pièces à fournir',
3: 'Frères et soeurs',
4: 'Modalités de paiement',
5: 'Formulaires à signer',
6: 'Pièces à fournir',
};
const steps = [
'Élève',
'Responsable',
'Fratrie',
'Paiement',
'Formulaires',
'Documents parent',
@ -436,6 +440,8 @@ export default function InscriptionFormShared({
return isPage4Valid;
case 5:
return isPage5Valid;
case 6:
return isPage6Valid;
default:
return false;
}
@ -464,6 +470,7 @@ export default function InscriptionFormShared({
setFormData={setFormData}
guardians={guardians}
setGuardians={setGuardians}
setSiblings={setSiblings}
errors={errors}
setIsPageValid={setIsPage1Valid}
hasInteracted={hasInteracted}
@ -481,8 +488,19 @@ export default function InscriptionFormShared({
/>
)}
{/* Page 3 : Informations sur les modalités de paiement */}
{/* Étape 3 : Frères et Sœurs */}
{currentPage === 3 && (
<SiblingInputFields
siblings={siblings}
setSiblings={setSiblings}
setFormData={setFormData}
errors={errors.siblings || []}
setIsPageValid={setIsPage3Valid}
/>
)}
{/* Page 4 : Informations sur les modalités de paiement */}
{currentPage === 4 && (
<>
<PaymentMethodSelector
formData={formData}
@ -492,13 +510,13 @@ export default function InscriptionFormShared({
registrationPaymentPlans={registrationPaymentPlans}
tuitionPaymentPlans={tuitionPaymentPlans}
errors={errors}
setIsPageValid={setIsPage3Valid}
setIsPageValid={setIsPage4Valid}
/>
</>
)}
{/* Pages suivantes : Section Fichiers d'inscription */}
{currentPage === 4 && (
{/* Page 5 : Section Fichiers d'inscription */}
{currentPage === 5 && (
<div className="mt-8 mb-4 w-full mx-auto flex gap-8">
{/* Liste des états de signature */}
<div className="w-1/4 bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-200">
@ -613,7 +631,8 @@ export default function InscriptionFormShared({
(currentPage === 1 && !isPage1Valid) ||
(currentPage === 2 && !isPage2Valid) ||
(currentPage === 3 && !isPage3Valid) ||
(currentPage === 4 && !isPage4Valid)
(currentPage === 4 && !isPage4Valid) ||
(currentPage === 5 && !isPage5Valid)
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
@ -621,7 +640,8 @@ export default function InscriptionFormShared({
(currentPage === 1 && !isPage1Valid) ||
(currentPage === 2 && !isPage2Valid) ||
(currentPage === 3 && !isPage3Valid) ||
(currentPage === 4 && !isPage4Valid)
(currentPage === 4 && !isPage4Valid) ||
(currentPage === 5 && !isPage5Valid)
}
primary
name="Next"
@ -633,11 +653,11 @@ export default function InscriptionFormShared({
text="Valider"
primary
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
currentPage === 5 && !isPage5Valid
currentPage === 6 && !isPage6Valid
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
disabled={currentPage === 5 && !isPage5Valid}
disabled={currentPage === 6 && !isPage6Valid}
/>
)}
</div>

View File

@ -0,0 +1,157 @@
import InputText from '@/components/InputText';
import React, { useEffect } from 'react';
import { Trash2, Plus, Users } from 'lucide-react';
import SectionHeader from '@/components/SectionHeader';
export default function SiblingInputFields({
siblings,
setSiblings,
setFormData,
errors,
setIsPageValid,
}) {
useEffect(() => {
// Si siblings est null ou undefined, le valoriser à un tableau vide
if (!siblings || siblings.length === 0) {
setSiblings([]);
}
// Synchroniser siblings avec formData
setFormData((prevFormData) => ({
...prevFormData,
siblings: siblings, // Mettre à jour siblings dans formData
}));
const isValid = siblings.every((sibling, index) => {
return !Object.keys(sibling).some(
(field) => getLocalError(index, field) !== ''
);
});
setIsPageValid(isValid);
}, [siblings, setSiblings, setFormData, setIsPageValid]);
const getError = (index, field) => {
return errors[index]?.[field]?.[0];
};
const getLocalError = (index, field) => {
if (
(field === 'last_name' &&
(!siblings[index].last_name ||
siblings[index].last_name.trim() === '')) ||
(field === 'first_name' &&
(!siblings[index].first_name ||
siblings[index].first_name.trim() === '')) ||
(field === 'birth_date' &&
(!siblings[index].birth_date ||
siblings[index].birth_date.trim() === ''))
) {
return 'Champs requis';
}
return '';
};
const onSiblingsChange = (id, field, value) => {
const updatedSiblings = siblings.map((sibling) => {
if (sibling.id === id) {
return { ...sibling, [field]: value };
}
return sibling;
});
setSiblings(updatedSiblings);
};
const addSibling = () => {
setSiblings([
...siblings,
{
last_name: '',
first_name: '',
birth_date: '',
},
]);
};
const deleteSibling = (index) => {
const updatedSiblings = siblings.filter((_, i) => i !== index);
setSiblings(updatedSiblings);
};
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<SectionHeader
icon={Users}
title={`Frères et Sœurs`}
description={`Ajoutez les informations des frères et sœurs`}
/>
{siblings.map((item, index) => (
<div className="p-6" key={index}>
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold">Frère/Sœur {index + 1}</h3>
<Trash2
className="w-5 h-5 text-red-500 cursor-pointer hover:text-red-700 transition-colors"
onClick={() => deleteSibling(index)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<InputText
name="last_name"
type="text"
label="Nom"
value={item.last_name}
onChange={(event) => {
onSiblingsChange(item.id, 'last_name', event.target.value);
}}
errorMsg={
getError(index, 'last_name') ||
getLocalError(index, 'last_name')
}
required
/>
<InputText
name="first_name"
type="text"
label="Prénom"
value={item.first_name}
onChange={(event) => {
onSiblingsChange(item.id, 'first_name', event.target.value);
}}
errorMsg={
getError(index, 'first_name') ||
getLocalError(index, 'first_name')
}
required
/>
</div>
<div className="grid grid-cols-1 gap-4">
<InputText
name="birth_date"
type="date"
label="Date de Naissance"
value={item.birth_date}
onChange={(event) => {
onSiblingsChange(item.id, 'birth_date', event.target.value);
}}
errorMsg={
getError(index, 'birth_date') ||
getLocalError(index, 'birth_date')
}
required
/>
</div>
</div>
))}
<div className="flex justify-center">
<Plus
className="w-8 h-8 text-green-500 cursor-pointer hover:text-green-700 transition-colors border-2 border-green-500 hover:border-green-700 rounded-full p-1"
onClick={addSibling}
/>
</div>
</div>
);
}

View File

@ -7,6 +7,7 @@ import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
import { User } from 'lucide-react';
import FileUpload from '@/components/FileUpload';
import { BASE_URL } from '@/utils/Url';
const levels = [
{ value: '1', label: 'TPS - Très Petite Section' },
@ -25,6 +26,7 @@ export default function StudentInfoForm({
formData,
setFormData,
setGuardians,
setSiblings,
errors,
setIsPageValid,
hasInteracted,
@ -37,9 +39,11 @@ export default function StudentInfoForm({
fetchRegisterForm(studentId).then((data) => {
logger.debug(data);
const photoPath = data?.student?.photo || null;
setFormData({
id: data?.student?.id || '',
photo: data?.student?.photo || null,
photo: photoPath,
last_name: data?.student?.last_name || '',
first_name: data?.student?.first_name || '',
address: data?.student?.address || '',
@ -56,7 +60,29 @@ export default function StudentInfoForm({
totalRegistrationFees: data?.totalRegistrationFees,
totalTuitionFees: data?.totalTuitionFees,
});
setGuardians(data?.student?.guardians || []);
setSiblings(data?.student?.siblings || []);
// Convertir la photo en fichier binaire si elle est un chemin ou une URL
if (photoPath && typeof photoPath === 'string') {
fetch(`${BASE_URL}${photoPath}`)
.then((response) => {
if (!response.ok) {
throw new Error('Erreur lors de la récupération de la photo.');
}
return response.blob();
})
.then((blob) => {
const file = new File([blob], photoPath.split('/').pop(), {
type: blob.type,
});
handlePhotoUpload(file); // Utiliser handlePhotoUpload pour valoriser la photo
})
.catch((error) => {
logger.error('Erreur lors de la conversion de la photo :', error);
});
}
});
setIsLoading(false);