Files
n3wt-school/Front-End/src/components/Inscription/DynamicFormsList.js

467 lines
20 KiB
JavaScript

'use client';
import React, { useState, useEffect } from 'react';
import FormRenderer from '@/components/Form/FormRenderer';
import FileUpload from '@/components/Form/FileUpload';
import { CheckCircle, Hourglass, FileText, Download, Upload, XCircle } 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
* @param {Array} schoolFileTemplates - Liste des templates de formulaires
* @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({
schoolFileTemplates,
existingResponses = {},
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(() => {
// Initialisation complète de formsValidation et formsData pour chaque template
if (schoolFileTemplates && schoolFileTemplates.length > 0) {
// Fusionner avec l'état existant pour préserver les données locales
setFormsData((prevData) => {
const dataState = { ...prevData };
schoolFileTemplates.forEach((tpl) => {
// Ne mettre à jour que si on n'a pas encore de données locales ou si les données du serveur ont changé
const hasLocalData = prevData[tpl.id] && Object.keys(prevData[tpl.id]).length > 0;
const hasServerData = existingResponses && existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0;
if (!hasLocalData && hasServerData) {
// Pas de données locales mais données serveur : utiliser les données serveur
dataState[tpl.id] = existingResponses[tpl.id];
} else if (!hasLocalData && !hasServerData) {
// Pas de données du tout : initialiser à vide
dataState[tpl.id] = {};
}
// Si hasLocalData : on garde les données locales existantes
});
return dataState;
});
// Fusionner avec l'état de validation existant
setFormsValidation((prevValidation) => {
const validationState = { ...prevValidation };
schoolFileTemplates.forEach((tpl) => {
const hasLocalValidation = prevValidation[tpl.id] === true;
const hasServerData = existingResponses && existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0;
if (!hasLocalValidation && hasServerData) {
// Pas validé localement mais données serveur : marquer comme validé
validationState[tpl.id] = true;
} else if (validationState[tpl.id] === undefined) {
// Pas encore initialisé : initialiser à false
validationState[tpl.id] = false;
}
// Si hasLocalValidation : on garde l'état local existant
});
return validationState;
});
}
}, [existingResponses, schoolFileTemplates]);
// Mettre à jour la validation globale quand la validation des formulaires change
useEffect(() => {
// Un document est considéré comme "validé" s'il est validé par l'école OU complété localement OU déjà existant
const allFormsValid = schoolFileTemplates.every(
tpl => tpl.isValidated === true ||
(formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
(existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0)
);
onValidationChange(allFormsValid);
}, [formsData, formsValidation, existingResponses, schoolFileTemplates, onValidationChange]);
/**
* Gère la soumission d'un formulaire individuel
*/
const handleFormSubmit = async (formData, templateId) => {
try {
logger.debug('Soumission du formulaire:', { templateId, formData });
// Sauvegarder les données du formulaire
setFormsData((prev) => ({
...prev,
[templateId]: formData,
}));
// Marquer le formulaire comme complété
setFormsValidation((prev) => ({
...prev,
[templateId]: true,
}));
// Appeler le callback parent
if (onFormSubmit) {
await onFormSubmit(formData, templateId);
}
// Passer au formulaire suivant si disponible
if (currentTemplateIndex < schoolFileTemplates.length - 1) {
setCurrentTemplateIndex(currentTemplateIndex + 1);
}
logger.debug('Formulaire soumis avec succès');
} catch (error) {
logger.error('Erreur lors de la soumission du formulaire:', error);
}
};
/**
* Vérifie si un formulaire est complété
*/
const isFormCompleted = (templateId) => {
return (
formsValidation[templateId] === true ||
(formsData[templateId] &&
Object.keys(formsData[templateId]).length > 0) ||
(existingResponses[templateId] &&
Object.keys(existingResponses[templateId]).length > 0)
);
};
/**
* Obtient l'icône de statut d'un formulaire
*/
const getFormStatusIcon = (templateId, isActive) => {
if (isFormCompleted(templateId)) {
return <CheckCircle className="w-5 h-5 text-green-600" />;
}
if (isActive) {
return <FileText className="w-5 h-5 text-blue-600" />;
}
return <Hourglass className="w-5 h-5 text-gray-400" />;
};
/**
* Obtient le formulaire actuel à afficher
*/
const getCurrentTemplate = () => {
return schoolFileTemplates[currentTemplateIndex];
};
const currentTemplate = getCurrentTemplate();
// Handler d'upload pour formulaire existant
const handleUpload = async (file, selectedFile) => {
if (!file || !selectedFile) return;
try {
const templateId = currentTemplate.id;
if (onFileUpload) {
await onFileUpload(file, selectedFile);
setFormsData((prev) => {
const newData = {
...prev,
[templateId]: { uploaded: true, fileName: file.name },
};
return newData;
});
setFormsValidation((prev) => {
const newValidation = {
...prev,
[templateId]: true,
};
return newValidation;
});
}
} 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 (!schoolFileTemplates || schoolFileTemplates.length === 0) {
return (
<div className="text-center py-8">
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-600 mb-4">Aucun formulaire à compléter</p>
</div>
);
}
return (
<div className="mt-8 mb-4 w-full mx-auto flex gap-8">
{/* Liste des formulaires */}
<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">
Formulaires à compléter
</h3>
<div className="text-sm text-gray-600 mb-4">
{/* Compteur x/y : inclut les documents validés */}
{
schoolFileTemplates.filter(tpl => {
// Validé ou complété localement
return tpl.isValidated === true ||
(formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
(existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0);
}).length
}
{' / '}
{schoolFileTemplates.length} complétés
</div>
{/* Tri des templates par état */}
{(() => {
// Helper pour état
const getState = tpl => {
if (tpl.isValidated === true) return 0; // validé
const isCompletedLocally = !!(
(formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
(existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0)
);
if (isCompletedLocally) return 1; // complété/en attente
return 2; // à compléter/refusé
};
const sortedTemplates = [...schoolFileTemplates].sort((a, b) => {
return getState(a) - getState(b);
});
return (
<ul className="space-y-2">
{sortedTemplates.map((tpl, index) => {
const isActive = schoolFileTemplates[currentTemplateIndex]?.id === tpl.id;
const isValidated = typeof tpl.isValidated === 'boolean' ? tpl.isValidated : undefined;
const isCompletedLocally = !!(
(formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
(existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0)
);
// Statut d'affichage
let statusLabel = '';
let statusColor = '';
let icon = null;
let bgClass = '';
let borderClass = '';
let textClass = '';
let canEdit = true;
if (isValidated === true) {
statusLabel = 'Validé';
statusColor = 'emerald';
icon = <CheckCircle className="w-5 h-5 text-emerald-600" />;
bgClass = 'bg-emerald-50';
borderClass = 'border border-emerald-200';
textClass = 'text-emerald-700';
bgClass = isActive ? 'bg-emerald-200' : bgClass;
borderClass = isActive ? 'border border-emerald-300' : borderClass;
textClass = isActive ? 'text-emerald-900 font-semibold' : textClass;
canEdit = false;
} else if (isValidated === false) {
if (isCompletedLocally) {
statusLabel = 'Complété';
statusColor = 'orange';
icon = <Hourglass className="w-5 h-5 text-orange-400" />;
bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50';
borderClass = isActive ? 'border border-orange-300' : 'border border-orange-200';
textClass = isActive ? 'text-orange-900 font-semibold' : 'text-orange-700';
canEdit = true;
} else {
statusLabel = 'Refusé';
statusColor = 'red';
icon = <XCircle className="w-5 h-5 text-red-500" />;
bgClass = isActive ? 'bg-red-200' : 'bg-red-50';
borderClass = isActive ? 'border border-red-300' : 'border border-red-200';
textClass = isActive ? 'text-red-900 font-semibold' : 'text-red-700';
canEdit = true;
}
} else {
if (isCompletedLocally) {
statusLabel = 'Complété';
statusColor = 'orange';
icon = <Hourglass className="w-5 h-5 text-orange-400" />;
bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50';
borderClass = isActive ? 'border border-orange-300' : 'border border-orange-200';
textClass = isActive ? 'text-orange-900 font-semibold' : 'text-orange-700';
canEdit = true;
} else {
statusLabel = 'À compléter';
statusColor = 'gray';
icon = <Hourglass className="w-5 h-5 text-gray-400" />;
bgClass = isActive ? 'bg-gray-200' : '';
borderClass = isActive ? 'border border-gray-300' : '';
textClass = isActive ? 'text-gray-900 font-semibold' : 'text-gray-600';
canEdit = true;
}
}
return (
<li
key={tpl.id}
className={`flex items-center cursor-pointer p-2 rounded-md transition-colors ${
isActive
? `${bgClass.replace('50', '200')} ${borderClass.replace('200', '300')} ${textClass.replace('700', '900')} font-semibold`
: `${bgClass} ${borderClass} ${textClass}`
}`}
onClick={() => setCurrentTemplateIndex(schoolFileTemplates.findIndex(t => t.id === tpl.id))}
>
<span className="mr-3">{icon}</span>
<div className="flex-1 min-w-0">
<div className="text-sm truncate flex items-center gap-2">
{tpl.formMasterData?.title || tpl.title || tpl.name || 'Formulaire sans nom'}
<span className={`ml-2 px-2 py-0.5 rounded bg-${statusColor}-100 text-${statusColor}-700 text-xs font-semibold`}>
{statusLabel}
</span>
</div>
<div className="text-xs text-gray-500">
{tpl.formMasterData?.fields || tpl.fields
? `${(tpl.formMasterData?.fields || tpl.fields).length} champ(s)`
: 'À compléter'}
</div>
</div>
</li>
);
})}
</ul>
);
})()}
</div>
<div className="w-3/4">
{currentTemplate && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div className="mb-6">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-semibold text-gray-800">
{currentTemplate.name}
</h3>
{/* Label d'état */}
{currentTemplate.isValidated === true ? (
<span className="px-2 py-0.5 rounded bg-emerald-100 text-emerald-700 text-sm font-semibold">Validé</span>
) : ((formsData[currentTemplate.id] && Object.keys(formsData[currentTemplate.id]).length > 0) ||
(existingResponses[currentTemplate.id] && Object.keys(existingResponses[currentTemplate.id]).length > 0)) ? (
<span className="px-2 py-0.5 rounded bg-orange-100 text-orange-700 text-sm font-semibold">Complété</span>
) : (
<span className="px-2 py-0.5 rounded bg-red-100 text-red-700 text-sm font-semibold">Refusé</span>
)}
</div>
<p className="text-sm text-gray-600">
{currentTemplate.formTemplateData?.description ||
currentTemplate.description || ''}
</p>
<div className="text-xs text-gray-500 mt-1">
Formulaire {(() => {
// Trouver l'index du template courant dans la liste triée
const getState = tpl => {
if (tpl.isValidated === true) return 0;
const isCompletedLocally = !!(
(formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
(existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0)
);
if (isCompletedLocally) return 1;
return 2;
};
const sortedTemplates = [...schoolFileTemplates].sort((a, b) => getState(a) - getState(b));
const idx = sortedTemplates.findIndex(tpl => tpl.id === currentTemplate.id);
return idx + 1;
})()} sur {schoolFileTemplates.length}
</div>
</div>
{/* Affichage dynamique ou existant */}
{isDynamicForm(currentTemplate) ? (
<FormRenderer
key={currentTemplate.id}
formConfig={{
id: currentTemplate.id,
title:
currentTemplate.formTemplateData?.title ||
currentTemplate.title ||
currentTemplate.name ||
'Formulaire',
fields:
currentTemplate.formTemplateData?.fields ||
currentTemplate.fields ||
[],
submitLabel:
currentTemplate.formTemplateData?.submitLabel || 'Valider',
}}
initialValues={
formsData[currentTemplate.id] ||
existingResponses[currentTemplate.id] ||
{}
}
onFormSubmit={(formData) =>
handleFormSubmit(formData, currentTemplate.id)
}
// Désactive le bouton suivant si le template est validé
enable={currentTemplate.isValidated !== true}
/>
) : (
// Formulaire existant (PDF, image, etc.)
<div className="flex flex-col items-center gap-6">
{/* Cas validé : affichage en iframe */}
{currentTemplate.isValidated === true && currentTemplate.file && (
<iframe
src={`${BASE_URL}${currentTemplate.file}`}
title={currentTemplate.name}
className="w-full"
style={{ height: '600px', border: 'none' }}
/>
)}
{/* Cas non validé : bouton télécharger + upload */}
{currentTemplate.isValidated !== true && (
<div className="flex flex-col items-center gap-4 w-full">
{/* Bouton télécharger le document source */}
{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>
)}
{/* Composant d'upload */}
{enable && (
<FileUpload
key={currentTemplate.id}
selectionMessage={'Sélectionnez le fichier du document'}
onFileSelect={(file) => handleUpload(file, currentTemplate)}
required
enable={true}
/>
)}
</div>
)}
</div>
)}
</div>
)}
{/* Message de fin */}
{currentTemplateIndex >= schoolFileTemplates.length && (
<div className="text-center py-8">
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-green-600 mb-2">
Tous les formulaires ont été complétés !
</h3>
<p className="text-gray-600">
Vous pouvez maintenant passer à l&apos;étape suivante.
</p>
</div>
)}
</div>
</div>
);
}