// Import des dépendances nécessaires import React, { useState, useEffect } from 'react'; import Button from '@/components/Form/Button'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import { fetchSchoolFileTemplatesFromRegistrationFiles, fetchParentFileTemplatesFromRegistrationFiles, fetchRegistrationSchoolFileMasters, saveFormResponses, fetchFormResponses, autoSaveRegisterForm, } 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 DynamicFormsList from '@/components/Inscription/DynamicFormsList'; import AutoSaveIndicator from '@/components/AutoSaveIndicator'; 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 [schoolFileMasters, setSchoolFileMasters] = useState([]); const [formResponses, setFormResponses] = 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 [isSaving, setIsSaving] = useState(false); const [lastSaved, setLastSaved] = useState(null); const [autoSaveEnabled, setAutoSaveEnabled] = useState(true); 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 formulaires maîtres sont complétés const allCompleted = schoolFileMasters.length === 0 || schoolFileMasters.every((master) => master.completed === true); // Mettre à jour isPage5Valid en fonction de cette condition setIsPage5Valid(allCompleted); if (allCompleted) { setCurrentTemplateIndex(0); } }, [schoolFileMasters]); 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]); // Auto-sauvegarde périodique (toutes les 30 secondes) useEffect(() => { if (!enable || !autoSaveEnabled) return; const interval = setInterval(() => { autoSave(); }, 30000); // 30 secondes return () => clearInterval(interval); }, [enable, autoSaveEnabled, formData, guardians, siblings]); // Auto-sauvegarde quand les données changent (avec debounce) useEffect(() => { if (!enable || !autoSaveEnabled) return; const timeout = setTimeout(() => { autoSave(); }, 2000); // Attendre 2 secondes après le dernier changement return () => clearTimeout(timeout); }, [formData, guardians, siblings]); /** * Fonction d'auto-sauvegarde qui sauvegarde les données en cours */ const autoSave = async () => { if (!autoSaveEnabled || !studentId || isSaving) { return; } try { setIsSaving(true); logger.debug('Auto-sauvegarde en cours...', { studentId, formDataKeys: Object.keys(formData), paymentFields: { registration_payment: formData.registration_payment, registration_payment_plan: formData.registration_payment_plan, tuition_payment: formData.tuition_payment, tuition_payment_plan: formData.tuition_payment_plan, }, guardians: guardians.length, siblings: siblings.length, currentPage, }); // Fonction helper pour nettoyer les données avant sauvegarde const cleanDataForAutoSave = (data) => { const cleaned = {}; Object.keys(data).forEach((key) => { const value = data[key]; // Garder seulement les valeurs non-vides et valides if (value !== null && value !== undefined && value !== '') { // Pour les dates, vérifier le format if (key === 'birth_date' && value) { // Vérifier que la date est dans un format valide const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (dateRegex.test(value)) { cleaned[key] = value; } } // Pour les codes postaux, vérifier que c'est un nombre else if (key === 'birth_postal_code' && value) { if (!isNaN(value) && value.toString().trim() !== '') { cleaned[key] = parseInt(value); } } // Pour les champs de paiement, toujours les inclure s'ils ont une valeur else if (key.includes('payment') && value) { cleaned[key] = value; } // Pour les autres champs, garder la valeur si elle n'est pas vide else if (value.toString().trim() !== '') { cleaned[key] = value; } } }); return cleaned; }; // Préparer les données à sauvegarder avec nettoyage const cleanedFormData = cleanDataForAutoSave(formData); const dataToSave = { student: cleanedFormData, guardians: guardians.filter( (guardian) => guardian && (guardian.first_name || guardian.last_name || guardian.email) ), siblings: siblings.filter( (sibling) => sibling && (sibling.first_name || sibling.last_name) ), currentPage: currentPage, }; // Utiliser la fonction d'auto-save dédiée await autoSaveRegisterForm(studentId, dataToSave, csrfToken); setLastSaved(new Date()); logger.debug('Auto-sauvegarde réussie'); } catch (error) { logger.error("Erreur lors de l'auto-sauvegarde:", error); // Ne pas afficher d'erreur à l'utilisateur pour l'auto-save } finally { setIsSaving(false); } }; /** * Gère la sauvegarde à chaque changement d'étape */ const saveStepData = async () => { await autoSave(); }; /** * Gère la soumission d'un formulaire dynamique */ const handleDynamicFormSubmit = async (formData, templateId) => { try { logger.debug('Soumission du formulaire dynamique:', { templateId, formData, csrfToken: !!csrfToken, }); // Trouver le template correspondant pour récupérer sa configuration const currentTemplate = schoolFileMasters.find( (master) => master.id === templateId ); if (!currentTemplate) { throw new Error(`Template avec l'ID ${templateId} non trouvé`); } // Construire la structure complète avec la configuration et les réponses const formTemplateData = { id: currentTemplate.id, title: currentTemplate.formMasterData?.title || currentTemplate.title || currentTemplate.name || 'Formulaire', fields: ( currentTemplate.formMasterData?.fields || currentTemplate.fields || [] ).map((field) => ({ ...field, // Ajouter la réponse de l'utilisateur selon le type de champ ...(field.type === 'checkbox' ? { checked: formData[field.id] || false } : {}), ...(field.type === 'radio' ? { selected: formData[field.id] } : {}), ...(field.type === 'text' || field.type === 'textarea' || field.type === 'email' ? { value: formData[field.id] || '' } : {}), })), submitLabel: currentTemplate.formMasterData?.submitLabel || 'Valider', responses: formData, // Garder aussi les réponses brutes pour facilité d'accès }; // Sauvegarder les réponses du formulaire via l'API RegistrationSchoolFileTemplate logger.debug('Appel API saveFormResponses avec:', { templateId, formTemplateData, }); const result = await saveFormResponses( templateId, formTemplateData, csrfToken ); logger.debug("Réponse de l'API:", result); // Mettre à jour l'état local des réponses setFormResponses((prev) => ({ ...prev, [templateId]: formData, })); // Mettre à jour l'état local pour indiquer que le formulaire est complété setSchoolFileMasters((prevMasters) => { return prevMasters.map((master) => master.id === templateId ? { ...master, completed: true, responses: formData } : master ); }); logger.debug('Formulaire dynamique sauvegardé avec succès'); return Promise.resolve(); } catch (error) { logger.error('Erreur lors de la soumission du formulaire dynamique:', { templateId, error: error.message, stack: error.stack, }); // Afficher l'erreur à l'utilisateur alert(`Erreur lors de la sauvegarde du formulaire: ${error.message}`); return Promise.reject(error); } }; /** * Gère les changements de validation des formulaires dynamiques */ const handleDynamicFormsValidationChange = (isValid) => { setIsPage5Valid(isValid); }; 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 school file masters fetchRegistrationSchoolFileMasters(selectedEstablishmentId) .then(async (data) => { logger.debug('School file masters fetched:', data); setSchoolFileMasters(data); // Récupérer les données existantes de chaque template const responsesMap = {}; for (const master of data) { if (master.id) { try { const templateData = await fetchFormResponses(master.id); if (templateData && templateData.formTemplateData) { // Si on a les réponses brutes sauvegardées, les utiliser if (templateData.formTemplateData.responses) { responsesMap[master.id] = templateData.formTemplateData.responses; } else { // Sinon, extraire les réponses depuis les champs const responses = {}; if (templateData.formTemplateData.fields) { templateData.formTemplateData.fields.forEach((field) => { if ( field.type === 'checkbox' && field.checked !== undefined ) { responses[field.id] = field.checked; } else if ( field.type === 'radio' && field.selected !== undefined ) { responses[field.id] = field.selected; } else if ( (field.type === 'text' || field.type === 'textarea' || field.type === 'email') && field.value !== undefined ) { responses[field.id] = field.value; } }); } responsesMap[master.id] = responses; } } } catch (error) { logger.debug( `Pas de données existantes pour le template ${master.id}:`, error ); // Ce n'est pas critique si un template n'a pas de données } } } setFormResponses(responsesMap); }) .catch((error) => logger.error('Error fetching school file masters:', error) ); // 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 = async () => { // Sauvegarder avant de passer à l'étape suivante await saveStepData(); setHasInteracted(false); setCurrentPage(currentPage + 1); }; const handlePreviousPage = async () => { // Sauvegarder avant de revenir à l'étape précédente await saveStepData(); 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 (
{/* Indicateur de sauvegarde automatique */} {enable && ( setAutoSaveEnabled(!autoSaveEnabled)} /> )}
{/* Page 1 : Informations sur l'élève */} {currentPage === 1 && ( )} {/* Page 2 : Informations sur les responsables légaux */} {currentPage === 2 && ( )} {/* Étape 3 : Frères et Sœurs */} {currentPage === 3 && ( )} {/* Page 4 : Informations sur les modalités de paiement */} {currentPage === 4 && ( <> )} {/* Page 5 : Formulaires dynamiques d'inscription */} {currentPage === 5 && ( )} {/* Dernière page : Section Fichiers parents */} {currentPage === 6 && ( )}
{/* Boutons de contrôle */}
{enable ? ( <> {currentPage > 1 && (
); }