mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
467 lines
20 KiB
JavaScript
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'étape suivante.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|