Files
n3wt-school/Front-End/src/components/Inscription/InscriptionFormShared.js
N3WT DE COMPET 170f7c4fa8 fix: Correction URL
chore: Ajout de notifications
2025-05-30 21:11:47 +02:00

706 lines
23 KiB
JavaScript

// Import des dépendances nécessaires
import React, { useState, useEffect } from 'react';
import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import {
fetchSchoolFileTemplatesFromRegistrationFiles,
fetchParentFileTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction';
import {
downloadTemplate,
editRegistrationSchoolFileTemplates,
editRegistrationParentFileTemplates,
} from '@/app/actions/registerFileGroupAction';
import {
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes,
fetchRegistrationPaymentPlans,
fetchTuitionPaymentPlans,
} from '@/app/actions/schoolAction';
import { fetchProfiles } from '@/app/actions/authAction';
import { BASE_URL, FE_PARENTS_HOME_URL } from '@/utils/Url';
import logger from '@/utils/logger';
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';
import { useRouter } from 'next/navigation';
/**
* Composant de formulaire d'inscription partagé
* @param {string} studentId - ID de l'étudiant
* @param {string} csrfToken - Token CSRF pour la sécurité
* @param {function} onSubmit - Fonction de soumission du formulaire
* @param {string} cancelUrl - URL de redirection en cas d'annulation
* @param {object} errors - Erreurs de validation du formulaire
*/
export default function InscriptionFormShared({
studentId,
csrfToken,
selectedEstablishmentId,
apiDocuseal,
onSubmit,
errors = {}, // Nouvelle prop pour les erreurs
enable = true,
}) {
// États pour gérer les données du formulaire
const [formData, setFormData] = useState({
id: '',
photo: null,
last_name: '',
first_name: '',
gender: '',
address: '',
birth_date: '',
birth_place: '',
birth_postal_code: '',
nationality: '',
attending_physician: '',
level: '',
photo: '',
});
const [guardians, setGuardians] = useState([]);
const [siblings, setSiblings] = 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([]);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFileTemplates, setParentFileTemplates] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [isPage1Valid, setIsPage1Valid] = useState(false);
const [isPage2Valid, setIsPage2Valid] = useState(false);
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);
// État pour suivre l'index du fichier en cours
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
const [profiles, setProfiles] = useState([]);
const router = useRouter();
// Mettre à jour les états en fonction de la valeur de `enable`
useEffect(() => {
if (!enable) {
setIsPage1Valid(true);
setIsPage2Valid(true);
setIsPage3Valid(true);
setIsPage4Valid(true);
setIsPage5Valid(true);
setIsPage6Valid(true);
}
}, [enable]);
useEffect(() => {
// Trouver le premier template non signé
const firstUnsignedIndex = schoolFileTemplates.findIndex(
(template) => template.file === null
);
// Mettre à jour l'index du template actuel
if (firstUnsignedIndex !== -1) {
setCurrentTemplateIndex(firstUnsignedIndex);
} else {
// Si tous les templates sont signés, définir un index hors limites
setCurrentTemplateIndex(0);
}
}, [schoolFileTemplates]);
useEffect(() => {
// Vérifier si tous les templates ont leur champ "file" différent de null
const allSigned = schoolFileTemplates.every(
(template) => template.file !== null
);
// Mettre à jour isPage4Valid en fonction de cette condition
setIsPage5Valid(allSigned);
if (allSigned) {
setCurrentTemplateIndex(0);
}
}, [schoolFileTemplates]);
useEffect(() => {
// Vérifier si tous les documents avec is_required = true ont leur champ "file" différent de null
const allRequiredUploaded = parentFileTemplates
.filter((template) => template.is_required) // Ne garder que les documents requis
.every((template) => template.file !== null); // Vérifier que chaque fichier requis est uploadé
// Mettre à jour isPage6Valid en fonction de cette condition
setIsPage6Valid(allRequiredUploaded);
logger.debug(allRequiredUploaded);
}, [parentFileTemplates]);
const handleTemplateSigned = (index) => {
const template = schoolFileTemplates[index];
if (!template) {
logger.error("Template introuvable pour l'index donné.");
return;
}
// Télécharger le template
downloadTemplate(template.slug, selectedEstablishmentId, apiDocuseal)
.then((downloadUrl) => fetch(downloadUrl))
.then((response) => {
if (!response.ok) {
throw new Error('Erreur lors du téléchargement du fichier.');
}
return response.blob();
})
.then((blob) => {
const file = new File([blob], `${template.name}.pdf`, {
type: blob.type,
});
// Préparer les données pour la mise à jour
const updateData = new FormData();
updateData.append('file', file);
// Mettre à jour le template via l'API
return editRegistrationSchoolFileTemplates(
template.id,
updateData,
csrfToken
);
})
.then((updatedTemplate) => {
logger.debug('Template mis à jour avec succès :', updatedTemplate);
// Mettre à jour l'état local de schoolFileTemplates
setSchoolFileTemplates((prevTemplates) => {
const updatedTemplates = prevTemplates.map((t, i) =>
i === index ? { ...t, file: updatedTemplate.data.file } : t
);
logger.debug(
'État schoolFileTemplates mis à jour :',
updatedTemplates
);
return updatedTemplates;
});
})
.catch((error) => {
logger.error('Erreur lors de la mise à jour du template :', error);
});
};
useEffect(() => {
fetchSchoolFileTemplatesFromRegistrationFiles(studentId).then((data) => {
setSchoolFileTemplates(data);
});
fetchParentFileTemplatesFromRegistrationFiles(studentId).then((data) => {
setParentFileTemplates(data);
// Initialiser uploadedFiles avec uniquement les fichiers dont `file` n'est pas null
const filteredFiles = data
.filter((item) => item.file !== null)
.map((item) => ({
id: item.id,
fileName: item.file,
}));
setUploadedFiles(filteredFiles);
});
fetchProfiles()
.then((data) => {
setProfiles(data);
})
.catch((error) =>
logger.error('Error fetching profiles : ', error)
);
if (selectedEstablishmentId) {
// Fetch data for registration payment modes
handleRegistrationPaymentModes();
// Fetch data for tuition payment modes
handleTuitionPaymentModes();
// Fetch data for registration payment plans
handleRegistrationPaymentPlans();
// Fetch data for tuition payment plans
handleTuitionnPaymentPlans();
}
}, [selectedEstablishmentId]);
const handleRegistrationPaymentModes = () => {
fetchRegistrationPaymentModes(selectedEstablishmentId)
.then((data) => {
setRegistrationPaymentModes(data);
})
.catch((error) =>
logger.error('Error fetching registration payment modes:', error)
);
};
const handleTuitionPaymentModes = () => {
fetchTuitionPaymentModes(selectedEstablishmentId)
.then((data) => {
setTuitionPaymentModes(data);
})
.catch((error) =>
logger.error('Error fetching tuition payment modes:', error)
);
};
const handleRegistrationPaymentPlans = () => {
fetchRegistrationPaymentPlans(selectedEstablishmentId)
.then((data) => {
setRegistrationPaymentPlans(data);
})
.catch((error) =>
logger.error('Error fetching registration payment plans:', error)
);
};
const handleTuitionnPaymentPlans = () => {
fetchTuitionPaymentPlans(selectedEstablishmentId)
.then((data) => {
setTuitionPaymentPlans(data);
})
.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.');
return Promise.reject(
new Error('Données manquantes pour le téléversement.')
);
}
const updateData = new FormData();
updateData.append('file', file);
return editRegistrationParentFileTemplates(
selectedFile.id,
updateData,
csrfToken
)
.then((response) => {
logger.debug('Template mis à jour avec succès :', response);
setUploadedFiles((prev) => {
const updatedFiles = prev.map((uploadedFile) =>
uploadedFile.id === selectedFile.id
? { ...uploadedFile, fileName: response.data.file } // Met à jour le fichier téléversé
: uploadedFile
);
// Si le fichier n'existe pas encore, l'ajouter
if (!updatedFiles.find((file) => file.id === selectedFile.id)) {
updatedFiles.push({
id: selectedFile.id,
fileName: response.data.file,
});
}
return updatedFiles;
});
// Mettre à jour parentFileTemplates
setParentFileTemplates((prevTemplates) =>
prevTemplates.map((template) =>
template.id === selectedFile.id
? { ...template, file: response.data.file }
: template
)
);
return response; // Retourner la réponse pour signaler le succès
})
.catch((error) => {
logger.error('Erreur lors de la mise à jour du fichier :', error);
throw error; // Relancer l'erreur pour que l'appelant puisse la capturer
});
};
const handleDeleteFile = (templateId) => {
const fileToDelete = uploadedFiles.find(
(file) => parseInt(file.id) === templateId && file.fileName
);
if (!fileToDelete) {
logger.error('Aucun fichier trouvé pour suppression.');
return;
}
// Créer un FormData avec un champ vide pour "file"
const updateData = new FormData();
updateData.append('file', ''); // Envoyer chaine vide pour indiquer qu'aucun fichier n'est uploadé
return editRegistrationParentFileTemplates(
templateId,
updateData,
csrfToken
)
.then((response) => {
logger.debug('Fichier supprimé avec succès dans la base :', response);
setIsPage6Valid(false);
// Mettre à jour l'état local pour refléter la suppression
setUploadedFiles((prev) =>
prev.map((uploadedFile) =>
uploadedFile.id === templateId
? { ...uploadedFile, fileName: null, fileUrl: null } // Réinitialiser les champs liés au fichier
: uploadedFile
)
);
// Mettre à jour l'état local pour refléter la suppression dans parentFileTemplates
setParentFileTemplates((prevTemplates) =>
prevTemplates.map((template) =>
template.id === templateId ? { ...template, file: null } : template
)
);
return response;
})
.catch((error) => {
logger.error(
'Erreur lors de la suppression du fichier dans la base :',
error
);
throw error;
});
};
// Soumission du formulaire
const handleSubmit = (e) => {
e.preventDefault();
// Vérifier si le mode de paiement sélectionné est un prélèvement SEPA
const isSepaPayment = formData.isSepa === 1;
// Préparer les données JSON
const jsonData = {
student: {
...formData,
guardians: guardians,
siblings: siblings.map(({ id, ...rest }) =>
id && typeof id === 'string' && id.startsWith('temp-')
? rest
: { id, ...rest }
), // Supprimer les IDs temporaires
},
establishment: selectedEstablishmentId,
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,
};
// Créer un objet FormData
const formDataToSend = new FormData();
// Ajouter les données JSON sous forme de chaîne
formDataToSend.append('data', JSON.stringify(jsonData));
// Ajouter la photo si elle est présente
if (formData.photo) {
formDataToSend.append('photo', formData.photo);
}
logger.debug('submit : ', jsonData);
// Appeler la fonction onSubmit avec les données FormData
onSubmit(formDataToSend);
};
const handleNextPage = () => {
setCurrentPage(currentPage + 1);
};
const handlePreviousPage = () => {
setCurrentPage(currentPage - 1);
};
const stepTitles = {
1: 'Elève',
2: 'Responsables légaux',
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',
];
const isStepValid = (stepNumber) => {
switch (stepNumber) {
case 1:
return isPage1Valid;
case 2:
return isPage2Valid;
case 3:
return isPage3Valid;
case 4:
return isPage4Valid;
case 5:
return isPage5Valid;
case 6:
return isPage6Valid;
default:
return false;
}
};
// Rendu du composant
return (
<div className="mx-auto p-6">
<DjangoCSRFToken csrfToken={csrfToken} />
<ProgressStep
steps={steps}
stepTitles={stepTitles}
currentStep={currentPage}
setStep={setCurrentPage}
isStepValid={isStepValid}
/>
<div className="flex-1 h-full mt-12 ">
{/* Page 1 : Informations sur l'élève */}
{currentPage === 1 && (
<StudentInfoForm
studentId={studentId}
formData={formData}
setFormData={setFormData}
guardians={guardians}
setGuardians={setGuardians}
setSiblings={setSiblings}
errors={errors}
setIsPageValid={setIsPage1Valid}
hasInteracted={hasInteracted}
setHasInteracted={setHasInteracted}
enable={enable}
/>
)}
{/* Page 2 : Informations sur les responsables légaux */}
{currentPage === 2 && (
<ResponsableInputFields
guardians={guardians}
setGuardians={setGuardians}
profiles={profiles}
errors={errors}
setIsPageValid={setIsPage2Valid}
enable={enable}
/>
)}
{/* Étape 3 : Frères et Sœurs */}
{currentPage === 3 && (
<SiblingInputFields
siblings={siblings}
setSiblings={setSiblings}
setFormData={setFormData}
errors={errors.siblings || []}
setIsPageValid={setIsPage3Valid}
enable={enable}
/>
)}
{/* Page 4 : Informations sur les modalités de paiement */}
{currentPage === 4 && (
<>
<PaymentMethodSelector
formData={formData}
setFormData={setFormData}
registrationPaymentModes={registrationPaymentModes}
tuitionPaymentModes={tuitionPaymentModes}
registrationPaymentPlans={registrationPaymentPlans}
tuitionPaymentPlans={tuitionPaymentPlans}
errors={errors}
setIsPageValid={setIsPage4Valid}
enable={enable}
/>
</>
)}
{/* 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">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
Documents
</h3>
<ul className="space-y-2">
{schoolFileTemplates.map((template, index) => (
<li
key={template.id}
className={`flex items-center cursor-pointer ${
index === currentTemplateIndex
? 'text-blue-600 font-bold'
: template.file !== null
? 'text-green-600'
: 'text-gray-600'
}`}
onClick={() => setCurrentTemplateIndex(index)} // Mettre à jour l'index du template actuel
>
<span className="mr-2">
{template.file !== null ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<Hourglass className="w-5 h-5 text-gray-600" />
)}
</span>
{template.name || 'Document sans nom'}
</li>
))}
</ul>
</div>
{/* Affichage du fichier actuel */}
<div className="w-3/4">
{currentTemplateIndex < schoolFileTemplates.length && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
{schoolFileTemplates[currentTemplateIndex].name ||
'Document sans nom'}
</h3>
<p className="text-sm text-gray-500 mb-4">
{schoolFileTemplates[currentTemplateIndex].description ||
'Aucune description disponible pour ce document.'}
</p>
{schoolFileTemplates[currentTemplateIndex].file === null ? (
<DocusealForm
key={schoolFileTemplates[currentTemplateIndex].slug}
id="docusealForm"
src={`https://docuseal.com/s/${schoolFileTemplates[currentTemplateIndex].slug}`}
withDownloadButton={false}
withTitle={false}
onComplete={() =>
handleTemplateSigned(currentTemplateIndex)
}
/>
) : (
<iframe
src={`${BASE_URL}${schoolFileTemplates[currentTemplateIndex].file}`}
title="Document Viewer"
className="w-full"
style={{
height: '75vh',
border: 'none',
}}
/>
)}
</div>
)}
{/* Message de fin */}
{currentTemplateIndex >= schoolFileTemplates.length && (
<div className="text-center text-green-600 font-semibold">
Tous les formulaires ont été signés avec succès !
</div>
)}
</div>
</div>
)}
{/* Dernière page : Section Fichiers parents */}
{currentPage === 6 && (
<FilesToUpload
parentFileTemplates={parentFileTemplates}
uploadedFiles={uploadedFiles}
onFileUpload={handleFileUpload}
onFileDelete={handleDeleteFile}
enable={enable}
/>
)}
</div>
{/* Boutons de contrôle */}
<div className="flex justify-center space-x-4 mt-12">
{enable ? (
<>
{currentPage > 1 && (
<Button
text="Précédent"
onClick={(e) => {
e.preventDefault();
handlePreviousPage();
}}
primary
/>
)}
{currentPage < steps.length ? (
<Button
text="Suivant"
onClick={(e) => {
e.preventDefault();
handleNextPage();
}}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(currentPage === 1 && !isPage1Valid) ||
(currentPage === 2 && !isPage2Valid) ||
(currentPage === 3 && !isPage3Valid) ||
(currentPage === 4 && !isPage4Valid) ||
(currentPage === 5 && !isPage5Valid)
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
disabled={
(currentPage === 1 && !isPage1Valid) ||
(currentPage === 2 && !isPage2Valid) ||
(currentPage === 3 && !isPage3Valid) ||
(currentPage === 4 && !isPage4Valid) ||
(currentPage === 5 && !isPage5Valid)
}
primary
name="Next"
/>
) : (
<Button
text="Valider"
onClick={(e) => {
e.preventDefault();
handleSubmit(e);
}}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
currentPage === 6 && !isPage6Valid
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
disabled={currentPage === 6 && !isPage6Valid}
primary
/>
)}
</>
) : (
<Button
onClick={() => router.push(FE_PARENTS_HOME_URL)}
text="Quitter"
primary
className="bg-emerald-500 text-white hover:bg-emerald-600"
/>
)}
</div>
</div>
);
}