mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
573 lines
18 KiB
JavaScript
573 lines
18 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,
|
|
} from '@/app/actions/schoolAction';
|
|
import { BASE_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 PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
|
|
import ProgressStep from '@/components/ProgressStep';
|
|
import { CheckCircle, Loader2 } from 'lucide-react';
|
|
|
|
/**
|
|
* 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,
|
|
onSubmit,
|
|
errors = {}, // Nouvelle prop pour les erreurs
|
|
}) {
|
|
// États pour gérer les données du formulaire
|
|
const [formData, setFormData] = useState({
|
|
id: '',
|
|
last_name: '',
|
|
first_name: '',
|
|
address: '',
|
|
birth_date: '',
|
|
birth_place: '',
|
|
birth_postal_code: '',
|
|
nationality: '',
|
|
attending_physician: '',
|
|
level: '',
|
|
registration_payment: '',
|
|
tuition_payment: '',
|
|
});
|
|
|
|
const [guardians, setGuardians] = useState([]);
|
|
|
|
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
|
|
const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]);
|
|
|
|
// États pour la gestion des fichiers
|
|
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
|
|
const [parentFileTemplates, setParentFileTemplates] = useState([]);
|
|
const [currentPage, setCurrentPage] = useState(5);
|
|
|
|
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 [hasInteracted, setHasInteracted] = useState(false);
|
|
|
|
// État pour suivre l'index du fichier en cours
|
|
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
|
|
|
|
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(schoolFileTemplates.length);
|
|
}
|
|
}, [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
|
|
setIsPage4Valid(allSigned);
|
|
|
|
if (allSigned) {
|
|
setCurrentTemplateIndex(0);
|
|
}
|
|
}, [schoolFileTemplates]);
|
|
|
|
useEffect(() => {
|
|
// Vérifier si tous les parentFileTemplates ont leur champ "file" différent de null
|
|
const allUploaded = parentFileTemplates.every(
|
|
(template) => template.file !== null
|
|
);
|
|
|
|
// Mettre à jour isPage5Valid en fonction de cette condition
|
|
setIsPage5Valid(allUploaded);
|
|
}, [parentFileTemplates]);
|
|
|
|
const handleTemplateSigned = (index) => {
|
|
const template = schoolFileTemplates[index];
|
|
|
|
if (!template) {
|
|
logger.error("Template introuvable pour l'index donné.");
|
|
return;
|
|
}
|
|
|
|
downloadTemplate(template.slug)
|
|
.then((data) => fetch(data))
|
|
.then((response) => response.blob())
|
|
.then((blob) => {
|
|
const file = new File([blob], `${template.name}.pdf`, {
|
|
type: blob.type,
|
|
});
|
|
const updateData = new FormData();
|
|
updateData.append('file', file);
|
|
|
|
return editRegistrationSchoolFileTemplates(
|
|
template.id,
|
|
updateData,
|
|
csrfToken
|
|
);
|
|
})
|
|
.then((data) => {
|
|
logger.debug('Template mis à jour avec succès :', data);
|
|
|
|
// Mettre à jour l'état local de schoolFileTemplates
|
|
setSchoolFileTemplates((prevTemplates) =>
|
|
prevTemplates.map((t, i) =>
|
|
i === index ? { ...t, file: data.file } : t
|
|
)
|
|
);
|
|
})
|
|
.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);
|
|
});
|
|
|
|
if (selectedEstablishmentId) {
|
|
// Fetch data for registration payment modes
|
|
handleRegistrationPaymentModes();
|
|
|
|
// Fetch data for tuition payment modes
|
|
handleTuitionPaymentModes();
|
|
}
|
|
}, [selectedEstablishmentId]);
|
|
|
|
const handleRegistrationPaymentModes = () => {
|
|
fetchRegistrationPaymentModes(selectedEstablishmentId)
|
|
.then((data) => {
|
|
const activePaymentModes = data.filter(
|
|
(mode) => mode.is_active === true
|
|
);
|
|
setRegistrationPaymentModes(activePaymentModes);
|
|
})
|
|
.catch((error) =>
|
|
logger.error('Error fetching registration payment modes:', error)
|
|
);
|
|
};
|
|
|
|
const handleTuitionPaymentModes = () => {
|
|
fetchTuitionPaymentModes(selectedEstablishmentId)
|
|
.then((data) => {
|
|
const activePaymentModes = data.filter(
|
|
(mode) => mode.is_active === true
|
|
);
|
|
setTuitionPaymentModes(activePaymentModes);
|
|
})
|
|
.catch((error) =>
|
|
logger.error('Error fetching tuition payment modes:', 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);
|
|
|
|
setIsPage5Valid(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();
|
|
const data = {
|
|
student: {
|
|
...formData,
|
|
guardians,
|
|
},
|
|
establishment: selectedEstablishmentId,
|
|
status: 3,
|
|
tuition_payment: formData.tuition_payment,
|
|
registration_payment: formData.registration_payment,
|
|
};
|
|
onSubmit(data);
|
|
};
|
|
|
|
const handleNextPage = () => {
|
|
setCurrentPage(currentPage + 1);
|
|
};
|
|
|
|
const handlePreviousPage = () => {
|
|
setCurrentPage(currentPage - 1);
|
|
};
|
|
|
|
const stepTitles = {
|
|
1: 'Elève',
|
|
2: 'Responsables légaux',
|
|
3: 'Modalités de paiement',
|
|
4: 'Formulaires à signer',
|
|
5: 'Pièces à fournir',
|
|
};
|
|
|
|
const steps = [
|
|
'Élève',
|
|
'Responsable',
|
|
'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;
|
|
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 overflow-y-auto mt-12 "
|
|
style={{ maxHeight: 'calc(100vh - 400px)' }}
|
|
>
|
|
{/* Page 1 : Informations sur l'élève */}
|
|
{currentPage === 1 && (
|
|
<StudentInfoForm
|
|
studentId={studentId}
|
|
formData={formData}
|
|
setFormData={setFormData}
|
|
guardians={guardians}
|
|
setGuardians={setGuardians}
|
|
errors={errors}
|
|
setIsPageValid={setIsPage1Valid}
|
|
hasInteracted={hasInteracted}
|
|
setHasInteracted={setHasInteracted}
|
|
/>
|
|
)}
|
|
|
|
{/* Page 2 : Informations sur les responsables légaux */}
|
|
{currentPage === 2 && (
|
|
<ResponsableInputFields
|
|
guardians={guardians}
|
|
setGuardians={setGuardians}
|
|
errors={errors}
|
|
setIsPageValid={setIsPage2Valid}
|
|
/>
|
|
)}
|
|
|
|
{/* Page 3 : Informations sur les modalités de paiement */}
|
|
{currentPage === 3 && (
|
|
<>
|
|
<PaymentMethodSelector
|
|
formData={formData}
|
|
setFormData={setFormData}
|
|
registrationPaymentModes={registrationPaymentModes}
|
|
tuitionPaymentModes={tuitionPaymentModes}
|
|
errors={errors}
|
|
setIsPageValid={setIsPage3Valid}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{/* Pages suivantes : Section Fichiers d'inscription */}
|
|
{currentPage === 4 && (
|
|
<div className="mt-8 mb-4 w-4/5 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" />
|
|
) : (
|
|
<Loader2 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 === 5 && (
|
|
<FilesToUpload
|
|
parentFileTemplates={parentFileTemplates}
|
|
uploadedFiles={uploadedFiles}
|
|
onFileUpload={handleFileUpload}
|
|
onFileDelete={handleDeleteFile}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Boutons de contrôle */}
|
|
<div className="flex justify-center space-x-4 mt-12">
|
|
{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)
|
|
? '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)
|
|
}
|
|
primary
|
|
name="Next"
|
|
/>
|
|
)}
|
|
{currentPage === steps.length && (
|
|
<Button
|
|
onClick={handleSubmit}
|
|
text="Valider"
|
|
primary
|
|
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
|
currentPage === 5 && !isPage5Valid
|
|
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
|
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
|
}`}
|
|
disabled={currentPage === 5 && !isPage5Valid}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|