feat: WIP finalisation partie signature des parents [N3WTS-17]

This commit is contained in:
N3WT DE COMPET
2026-02-13 17:06:21 +01:00
parent abb4b525b2
commit 9dff32b388
7 changed files with 255 additions and 726 deletions

View File

@ -1,8 +1,10 @@
'use client';
import React, { useState, useEffect } from 'react';
import FormRenderer from '@/components/Form/FormRenderer';
import { CheckCircle, Hourglass, FileText } from 'lucide-react';
import FileUpload from '@/components/Form/FileUpload';
import { CheckCircle, Hourglass, FileText, Download, Upload } from 'lucide-react';
import logger from '@/utils/logger';
import { BASE_URL } from '@/utils/Url';
/**
* Composant pour afficher et gérer les formulaires dynamiques d'inscription
@ -10,6 +12,7 @@ import logger from '@/utils/logger';
* @param {Object} existingResponses - Réponses déjà sauvegardées
* @param {Function} onFormSubmit - Callback appelé quand un formulaire est soumis
* @param {Boolean} enable - Si les formulaires sont modifiables
* @param {Function} onFileUpload - Callback appelé quand un fichier est sélectionné
*/
export default function DynamicFormsList({
schoolFileMasters,
@ -17,10 +20,12 @@ export default function DynamicFormsList({
onFormSubmit,
enable = true,
onValidationChange,
onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent)
}) {
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
const [formsData, setFormsData] = useState({});
const [formsValidation, setFormsValidation] = useState({});
const fileInputRefs = React.useRef({});
// Initialiser les données avec les réponses existantes
useEffect(() => {
@ -138,6 +143,27 @@ export default function DynamicFormsList({
return schoolFileMasters[currentTemplateIndex];
};
// Handler d'upload pour formulaire existant
const handleUpload = async (file, selectedFile) => {
if (!file || !selectedFile) return;
try {
if (onFileUpload) {
await onFileUpload(file, selectedFile);
setFormsValidation((prev) => ({
...prev,
[selectedFile.id]: true,
}));
}
} catch (error) {
logger.error('Erreur lors de l\'upload du fichier :', error);
}
};
const isDynamicForm = (template) =>
template.formTemplateData &&
Array.isArray(template.formTemplateData.fields) &&
template.formTemplateData.fields.length > 0;
if (!schoolFileMasters || schoolFileMasters.length === 0) {
return (
<div className="text-center py-8">
@ -223,13 +249,13 @@ export default function DynamicFormsList({
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div className="mb-6">
<h3 className="text-xl font-semibold text-gray-800 mb-2">
{currentTemplate.formMasterData?.title ||
{currentTemplate.formTemplateData?.title ||
currentTemplate.title ||
currentTemplate.name ||
'Formulaire sans nom'}
</h3>
<p className="text-sm text-gray-600">
{currentTemplate.formMasterData?.description ||
{currentTemplate.formTemplateData?.description ||
currentTemplate.description ||
'Veuillez compléter ce formulaire pour continuer votre inscription.'}
</p>
@ -239,39 +265,57 @@ export default function DynamicFormsList({
</div>
</div>
{/* Vérifier si le formulaire maître a des données de configuration */}
{(currentTemplate.formMasterData?.fields &&
currentTemplate.formMasterData.fields.length > 0) ||
(currentTemplate.fields && currentTemplate.fields.length > 0) ? (
{/* Affichage dynamique ou existant */}
{isDynamicForm(currentTemplate) ? (
<FormRenderer
key={currentTemplate.id}
formConfig={{
id: currentTemplate.id,
title:
currentTemplate.formMasterData?.title ||
currentTemplate.formTemplateData?.title ||
currentTemplate.title ||
currentTemplate.name ||
'Formulaire',
fields:
currentTemplate.formMasterData?.fields ||
currentTemplate.formTemplateData?.fields ||
currentTemplate.fields ||
[],
submitLabel:
currentTemplate.formMasterData?.submitLabel || 'Valider',
currentTemplate.formTemplateData?.submitLabel || 'Valider',
}}
onFormSubmit={(formData) =>
handleFormSubmit(formData, currentTemplate.id)
}
/>
) : (
<div className="text-center py-8">
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-600">
Ce formulaire n&apos;est pas encore configuré.
</p>
<p className="text-sm text-gray-500 mt-2">
Contactez l&apos;administration pour plus d&apos;informations.
</p>
// Formulaire existant (PDF, image, etc.)
<div className="flex flex-col items-center gap-6">
<div className="flex flex-col items-center gap-2">
<FileText className="w-16 h-16 text-gray-400" />
<div className="text-lg font-semibold text-gray-700">
{currentTemplate.name}
</div>
{currentTemplate.file && (
<a
href={`${BASE_URL}${currentTemplate.file}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
download
>
<Download className="w-5 h-5" />
Télécharger le document
</a>
)}
</div>
{enable && (
<FileUpload
selectionMessage="Sélectionnez le fichier du document"
onFileSelect={(file) => handleUpload(file, currentTemplate)}
required
enable
/>
)}
</div>
)}
</div>

View File

@ -5,15 +5,12 @@ 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,
@ -22,7 +19,7 @@ import {
fetchTuitionPaymentPlans,
} from '@/app/actions/schoolAction';
import { fetchProfiles } from '@/app/actions/authAction';
import { BASE_URL, FE_PARENTS_HOME_URL } from '@/utils/Url';
import { FE_PARENTS_HOME_URL } from '@/utils/Url';
import logger from '@/utils/logger';
import FilesToUpload from '@/components/Inscription/FilesToUpload';
import DynamicFormsList from '@/components/Inscription/DynamicFormsList';
@ -32,7 +29,6 @@ import ResponsableInputFields from '@/components/Inscription/ResponsableInputFie
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';
/**
@ -47,7 +43,6 @@ export default function InscriptionFormShared({
studentId,
csrfToken,
selectedEstablishmentId,
apiDocuseal,
onSubmit,
errors = {}, // Nouvelle prop pour les erreurs
enable = true,
@ -82,7 +77,7 @@ export default function InscriptionFormShared({
const [parentFileTemplates, setParentFileTemplates] = useState([]);
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
const [formResponses, setFormResponses] = useState({});
const [currentPage, setCurrentPage] = useState(1);
const [currentPage, setCurrentPage] = useState(5);
const [isPage1Valid, setIsPage1Valid] = useState(false);
const [isPage2Valid, setIsPage2Valid] = useState(false);
@ -283,8 +278,8 @@ export default function InscriptionFormShared({
});
// Trouver le template correspondant pour récupérer sa configuration
const currentTemplate = schoolFileMasters.find(
(master) => master.id === templateId
const currentTemplate = schoolFileTemplates.find(
(template) => template.id === templateId
);
if (!currentTemplate) {
throw new Error(`Template avec l'ID ${templateId} non trouvé`);
@ -294,17 +289,16 @@ export default function InscriptionFormShared({
const formTemplateData = {
id: currentTemplate.id,
title:
currentTemplate.formMasterData?.title ||
currentTemplate.formTemplateData?.title ||
currentTemplate.title ||
currentTemplate.name ||
'Formulaire',
fields: (
currentTemplate.formMasterData?.fields ||
currentTemplate.formTemplateData?.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 }
: {}),
@ -315,8 +309,8 @@ export default function InscriptionFormShared({
? { value: formData[field.id] || '' }
: {}),
})),
submitLabel: currentTemplate.formMasterData?.submitLabel || 'Valider',
responses: formData, // Garder aussi les réponses brutes pour facilité d'accès
submitLabel: currentTemplate.formTemplateData?.submitLabel || 'Valider',
responses: formData,
};
// Sauvegarder les réponses du formulaire via l'API RegistrationSchoolFileTemplate
@ -331,18 +325,37 @@ export default function InscriptionFormShared({
);
logger.debug("Réponse de l'API:", result);
// Mettre à jour l'état local des réponses
// Prendre en compte la réponse du back pour mettre à jour les réponses locales
let newResponses = formData;
if (
result &&
result.data &&
result.data.formTemplateData &&
result.data.formTemplateData.responses &&
result.data.formTemplateData.responses.responses
) {
// Si la structure responses.responses existe, on la prend
newResponses = result.data.formTemplateData.responses.responses;
} else if (
result &&
result.data &&
result.data.formTemplateData &&
result.data.formTemplateData.responses
) {
// Sinon, on prend responses directement
newResponses = result.data.formTemplateData.responses;
}
setFormResponses((prev) => ({
...prev,
[templateId]: formData,
[templateId]: newResponses,
}));
// 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
setSchoolFileTemplates((prevTemplates) => {
return prevTemplates.map((template) =>
template.id === templateId
? { ...template, completed: true, responses: newResponses }
: template
);
});
@ -354,7 +367,6 @@ export default function InscriptionFormShared({
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);
}
@ -370,6 +382,56 @@ export default function InscriptionFormShared({
useEffect(() => {
fetchSchoolFileTemplatesFromRegistrationFiles(studentId).then((data) => {
setSchoolFileTemplates(data);
// Récupérer les réponses existantes pour chaque template
const fetchAllResponses = async () => {
const responsesMap = {};
for (const template of data) {
if (template.id) {
try {
const templateData = await fetchFormResponses(template.id);
if (templateData && templateData.formTemplateData) {
if (templateData.formTemplateData.responses) {
responsesMap[template.id] = templateData.formTemplateData.responses;
} else {
// 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[template.id] = responses;
}
}
} catch (error) {
logger.debug(
`Pas de données existantes pour le template ${template.id}:`,
error
);
}
}
}
setFormResponses(responsesMap);
};
fetchAllResponses();
});
fetchParentFileTemplatesFromRegistrationFiles(studentId).then((data) => {
@ -392,66 +454,6 @@ export default function InscriptionFormShared({
.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();
@ -464,7 +466,7 @@ export default function InscriptionFormShared({
// Fetch data for tuition payment plans
handleTuitionnPaymentPlans();
}
}, [selectedEstablishmentId]);
}, [studentId, selectedEstablishmentId]);
const handleRegistrationPaymentModes = () => {
fetchRegistrationPaymentModes(selectedEstablishmentId)
@ -514,10 +516,22 @@ export default function InscriptionFormShared({
);
}
const updateData = new FormData();
updateData.append('file', file);
// Générer le nom du fichier : <nom_template>.<extension d'origine>
let extension = '';
if (file.name && file.name.lastIndexOf('.') !== -1) {
extension = file.name.substring(file.name.lastIndexOf('.'));
}
// Nettoyer le nom du template pour éviter les caractères spéciaux
const cleanName = (selectedFile.name || 'document')
.replace(/[^a-zA-Z0-9_\-]/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
const finalFileName = `${cleanName}${extension}`;
return editRegistrationParentFileTemplates(
const updateData = new FormData();
updateData.append('file', file, finalFileName);
return editRegistrationSchoolFileTemplates(
selectedFile.id,
updateData,
csrfToken
@ -528,11 +542,10 @@ export default function InscriptionFormShared({
setUploadedFiles((prev) => {
const updatedFiles = prev.map((uploadedFile) =>
uploadedFile.id === selectedFile.id
? { ...uploadedFile, fileName: response.data.file } // Met à jour le fichier téléversé
? { ...uploadedFile, fileName: response.data.file }
: uploadedFile
);
// Si le fichier n'existe pas encore, l'ajouter
if (!updatedFiles.find((file) => file.id === selectedFile.id)) {
updatedFiles.push({
id: selectedFile.id,
@ -552,11 +565,11 @@ export default function InscriptionFormShared({
)
);
return response; // Retourner la réponse pour signaler le succès
return response;
})
.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
throw error;
});
};
@ -587,7 +600,7 @@ export default function InscriptionFormShared({
setUploadedFiles((prev) =>
prev.map((uploadedFile) =>
uploadedFile.id === templateId
? { ...uploadedFile, fileName: null, fileUrl: null } // Réinitialiser les champs liés au fichier
? { ...uploadedFile, fileName: null, fileUrl: null }
: uploadedFile
)
);
@ -786,11 +799,12 @@ export default function InscriptionFormShared({
{/* Page 5 : Formulaires dynamiques d'inscription */}
{currentPage === 5 && (
<DynamicFormsList
schoolFileMasters={schoolFileMasters}
schoolFileMasters={schoolFileTemplates}
existingResponses={formResponses}
onFormSubmit={handleDynamicFormSubmit}
onValidationChange={handleDynamicFormsValidationChange}
enable={enable}
onFileUpload={handleFileUpload}
/>
)}