mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
467 lines
16 KiB
JavaScript
467 lines
16 KiB
JavaScript
import logger from '@/utils/logger';
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
import { useEffect } from 'react';
|
|
import SelectChoice from './SelectChoice';
|
|
import InputTextIcon from './InputTextIcon';
|
|
import * as LucideIcons from 'lucide-react';
|
|
import Button from './Button';
|
|
import DjangoCSRFToken from '../DjangoCSRFToken';
|
|
import WisiwigTextArea from './WisiwigTextArea';
|
|
import RadioList from './RadioList';
|
|
import CheckBox from './CheckBox';
|
|
import ToggleSwitch from './ToggleSwitch';
|
|
import InputPhone from './InputPhone';
|
|
import FileUpload from './FileUpload';
|
|
import SignatureField from './SignatureField';
|
|
|
|
/*
|
|
* Récupère une icône Lucide par son nom.
|
|
*/
|
|
export function getIcon(name) {
|
|
if (Object.keys(LucideIcons).includes(name)) {
|
|
const Icon = LucideIcons[name];
|
|
return Icon ?? null;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default function FormRenderer({
|
|
formConfig,
|
|
csrfToken,
|
|
initialValues = {},
|
|
onFormSubmit = (data) => {
|
|
alert(JSON.stringify(data, null, 2));
|
|
}, // Callback de soumission personnalisé (optionnel)
|
|
}) {
|
|
const {
|
|
handleSubmit,
|
|
control,
|
|
formState: { errors },
|
|
reset,
|
|
} = useForm({ defaultValues: initialValues });
|
|
|
|
// Réinitialiser le formulaire quand les valeurs initiales changent
|
|
useEffect(() => {
|
|
if (initialValues && Object.keys(initialValues).length > 0) {
|
|
reset(initialValues);
|
|
}
|
|
}, [initialValues, reset]);
|
|
|
|
// Fonction utilitaire pour envoyer les données au backend
|
|
const sendFormDataToBackend = async (formData) => {
|
|
try {
|
|
// Cette fonction peut être remplacée par votre propre implémentation
|
|
// Exemple avec fetch:
|
|
const response = await fetch('/api/submit-form', {
|
|
method: 'POST',
|
|
body: formData,
|
|
// Les en-têtes sont automatiquement définis pour FormData
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
logger.debug('Envoi réussi:', result);
|
|
return result;
|
|
} catch (error) {
|
|
logger.error("Erreur lors de l'envoi:", error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const onSubmit = async (data) => {
|
|
logger.debug('=== DÉBUT onSubmit ===');
|
|
logger.debug('Réponses :', data);
|
|
|
|
try {
|
|
// Vérifier si nous avons des fichiers dans les données
|
|
const hasFiles = Object.keys(data).some((key) => {
|
|
return (
|
|
data[key] instanceof FileList ||
|
|
(data[key] && data[key][0] instanceof File) ||
|
|
(typeof data[key] === 'string' && data[key].startsWith('data:image'))
|
|
);
|
|
});
|
|
|
|
if (hasFiles) {
|
|
// Utiliser FormData pour l'envoi de fichiers
|
|
const formData = new FormData();
|
|
|
|
// Ajouter l'ID du formulaire
|
|
formData.append('formId', (formConfig?.id || 'unknown').toString());
|
|
|
|
// Traiter chaque champ et ses valeurs
|
|
Object.keys(data).forEach((key) => {
|
|
const value = data[key];
|
|
|
|
if (
|
|
value instanceof FileList ||
|
|
(value && value[0] instanceof File)
|
|
) {
|
|
// Gérer les champs de type fichier
|
|
if (value.length > 0) {
|
|
for (let i = 0; i < value.length; i++) {
|
|
formData.append(`files.${key}`, value[i]);
|
|
}
|
|
}
|
|
} else if (
|
|
typeof value === 'string' &&
|
|
value.startsWith('data:image')
|
|
) {
|
|
// Gérer les signatures (SVG ou images base64)
|
|
if (value.includes('svg+xml')) {
|
|
// Gérer les signatures SVG
|
|
const svgData = value.split(',')[1];
|
|
const svgBlob = new Blob([atob(svgData)], {
|
|
type: 'image/svg+xml',
|
|
});
|
|
formData.append(`files.${key}`, svgBlob, `signature_${key}.svg`);
|
|
} else {
|
|
// Gérer les images base64 classiques
|
|
const byteString = atob(value.split(',')[1]);
|
|
const ab = new ArrayBuffer(byteString.length);
|
|
const ia = new Uint8Array(ab);
|
|
for (let i = 0; i < byteString.length; i++) {
|
|
ia[i] = byteString.charCodeAt(i);
|
|
}
|
|
const blob = new Blob([ab], { type: 'image/png' });
|
|
formData.append(`files.${key}`, blob, `signature_${key}.png`);
|
|
}
|
|
} else {
|
|
// Gérer les autres types de champs
|
|
formData.append(
|
|
`data.${key}`,
|
|
value !== undefined ? value.toString() : ''
|
|
);
|
|
}
|
|
});
|
|
|
|
if (onFormSubmit) {
|
|
// Utiliser le callback personnalisé si fourni
|
|
await onFormSubmit(formData, true);
|
|
} else {
|
|
// Sinon, utiliser la fonction par défaut
|
|
await sendFormDataToBackend(formData);
|
|
alert('Formulaire avec fichier(s) envoyé avec succès');
|
|
}
|
|
} else {
|
|
// Pas de fichier, on peut utiliser JSON
|
|
const formattedData = {
|
|
formId: formConfig?.id || 'unknown',
|
|
responses: { ...data },
|
|
};
|
|
|
|
if (onFormSubmit) {
|
|
// Utiliser le callback personnalisé si fourni
|
|
await onFormSubmit(formattedData, false);
|
|
} else {
|
|
// Afficher un message pour démonstration
|
|
alert('Données reçues : ' + JSON.stringify(formattedData, null, 2));
|
|
}
|
|
}
|
|
|
|
reset(); // Réinitialiser le formulaire après soumission
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la soumission du formulaire:', error);
|
|
alert(`Erreur lors de l'envoi du formulaire: ${error.message}`);
|
|
}
|
|
|
|
logger.debug('=== FIN onSubmit ===');
|
|
};
|
|
|
|
const onError = (errors) => {
|
|
logger.error('=== ERREURS DE VALIDATION ===');
|
|
logger.error('Erreurs :', errors);
|
|
alert('Erreurs de validation : ' + JSON.stringify(errors, null, 2));
|
|
};
|
|
|
|
return (
|
|
<form
|
|
onSubmit={handleSubmit(onSubmit, onError)}
|
|
className="max-w-md mx-auto"
|
|
>
|
|
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
|
|
<h2 className="text-2xl font-bold text-center mb-4">
|
|
{formConfig?.title || 'Formulaire'}
|
|
</h2>
|
|
|
|
{(formConfig?.fields || []).map((field) => (
|
|
<div
|
|
key={field.id || `field-${Math.random().toString(36).substr(2, 9)}`}
|
|
className="flex flex-col mt-4"
|
|
>
|
|
{field.type === 'heading1' && (
|
|
<h1 className="text-3xl font-bold mb-3">{field.text}</h1>
|
|
)}
|
|
{field.type === 'heading2' && (
|
|
<h2 className="text-2xl font-bold mb-3">{field.text}</h2>
|
|
)}
|
|
{field.type === 'heading3' && (
|
|
<h3 className="text-xl font-bold mb-2">{field.text}</h3>
|
|
)}
|
|
{field.type === 'heading4' && (
|
|
<h4 className="text-lg font-bold mb-2">{field.text}</h4>
|
|
)}
|
|
{field.type === 'heading5' && (
|
|
<h5 className="text-base font-bold mb-1">{field.text}</h5>
|
|
)}
|
|
{field.type === 'heading6' && (
|
|
<h6 className="text-sm font-bold mb-1">{field.text}</h6>
|
|
)}
|
|
|
|
{field.type === 'paragraph' && <p className="mb-4">{field.text}</p>}
|
|
|
|
{(field.type === 'text' ||
|
|
field.type === 'email' ||
|
|
field.type === 'date') && (
|
|
<Controller
|
|
name={field.id}
|
|
control={control}
|
|
rules={{
|
|
required: field.required,
|
|
pattern: field.validation?.pattern
|
|
? new RegExp(field.validation.pattern)
|
|
: undefined,
|
|
minLength: field.validation?.minLength,
|
|
maxLength: field.validation?.maxLength,
|
|
}}
|
|
render={({ field: { onChange, value, name } }) => (
|
|
<InputTextIcon
|
|
label={field.label}
|
|
required={field.required}
|
|
IconItem={field.icon ? getIcon(field.icon) : null}
|
|
type={field.type}
|
|
name={name}
|
|
value={value || ''}
|
|
onChange={onChange}
|
|
errorMsg={
|
|
errors[field.id]
|
|
? field.required
|
|
? `${field.label} est requis`
|
|
: 'Champ invalide'
|
|
: ''
|
|
}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
{field.type === 'phone' && (
|
|
<Controller
|
|
name={field.id}
|
|
control={control}
|
|
rules={{ required: field.required }}
|
|
render={({ field: { onChange, value, name } }) => (
|
|
<InputPhone
|
|
label={field.label}
|
|
required={field.required}
|
|
name={name}
|
|
value={value || ''}
|
|
onChange={onChange}
|
|
errorMsg={
|
|
errors[field.id]
|
|
? field.required
|
|
? `${field.label} est requis`
|
|
: 'Champ invalide'
|
|
: ''
|
|
}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
{field.type === 'select' && (
|
|
<Controller
|
|
name={field.id}
|
|
control={control}
|
|
rules={{ required: field.required }}
|
|
render={({ field: { onChange, value, name } }) => (
|
|
<SelectChoice
|
|
label={field.label}
|
|
required={field.required}
|
|
name={name}
|
|
selected={value || ''}
|
|
callback={onChange}
|
|
choices={field.options.map((e) => ({ label: e, value: e }))}
|
|
placeHolder={`Sélectionner ${field.label.toLowerCase()}`}
|
|
errorMsg={
|
|
errors[field.id]
|
|
? field.required
|
|
? `${field.label} est requis`
|
|
: 'Champ invalide'
|
|
: ''
|
|
}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
{field.type === 'radio' && (
|
|
<Controller
|
|
name={field.id}
|
|
control={control}
|
|
rules={{ required: field.required }}
|
|
render={({ field: { onChange, value, name } }) => (
|
|
<RadioList
|
|
items={field.options.map((option, idx) => ({
|
|
id: idx,
|
|
label: option,
|
|
}))}
|
|
formData={{
|
|
[field.id]: value
|
|
? field.options.findIndex((o) => o === value)
|
|
: '',
|
|
}}
|
|
handleChange={(e) =>
|
|
onChange(field.options[parseInt(e.target.value)])
|
|
}
|
|
fieldName={field.id}
|
|
sectionLabel={field.label}
|
|
required={field.required}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
{field.type === 'checkbox' && (
|
|
<Controller
|
|
name={field.id}
|
|
control={control}
|
|
defaultValue={field.checked || false}
|
|
rules={{ required: field.required }}
|
|
render={({ field: { onChange, value, name } }) => (
|
|
<div>
|
|
<CheckBox
|
|
item={{ id: field.id, label: field.label }}
|
|
formData={{ [field.id]: value || false }}
|
|
handleChange={(e) => onChange(e.target.checked)}
|
|
fieldName={field.id}
|
|
itemLabelFunc={(item) => item.label}
|
|
horizontal={field.horizontal || false}
|
|
/>
|
|
{errors[field.id] && (
|
|
<p className="text-red-500 text-sm mt-1">
|
|
{field.required
|
|
? `${field.label} est requis`
|
|
: 'Champ invalide'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
/>
|
|
)}
|
|
{field.type === 'toggle' && (
|
|
<Controller
|
|
name={field.id}
|
|
control={control}
|
|
defaultValue={field.checked || false}
|
|
rules={{ required: field.required }}
|
|
render={({ field: { onChange, value, name } }) => (
|
|
<div>
|
|
<ToggleSwitch
|
|
name={field.id}
|
|
label={field.label + (field.required ? ' *' : '')}
|
|
checked={value || false}
|
|
onChange={(e) => onChange(e.target.checked)}
|
|
/>
|
|
{errors[field.id] && (
|
|
<p className="text-red-500 text-sm mt-1">
|
|
{field.required
|
|
? `${field.label} est requis`
|
|
: 'Champ invalide'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
/>
|
|
)}
|
|
{field.type === 'file' && (
|
|
<Controller
|
|
name={field.id}
|
|
control={control}
|
|
rules={{ required: field.required }}
|
|
render={({ field: { onChange, value } }) => (
|
|
<FileUpload
|
|
selectionMessage={field.label}
|
|
required={field.required}
|
|
uploadedFileName={value ? value[0]?.name : null}
|
|
onFileSelect={(file) => {
|
|
// Créer un objet de type FileList similaire pour la compatibilité
|
|
const dataTransfer = new DataTransfer();
|
|
dataTransfer.items.add(file);
|
|
onChange(dataTransfer.files);
|
|
}}
|
|
errorMsg={
|
|
errors[field.id]
|
|
? field.required
|
|
? `${field.label} est requis`
|
|
: 'Champ invalide'
|
|
: ''
|
|
}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
{field.type === 'textarea' && (
|
|
<Controller
|
|
name={field.id}
|
|
control={control}
|
|
rules={{ required: field.required }}
|
|
render={({ field: { onChange, value } }) => (
|
|
<WisiwigTextArea
|
|
label={field.label}
|
|
placeholder={field.placeholder}
|
|
value={value || ''}
|
|
onChange={onChange}
|
|
required={field.required}
|
|
errorMsg={
|
|
errors[field.id]
|
|
? field.required
|
|
? `${field.label} est requis`
|
|
: 'Champ invalide'
|
|
: ''
|
|
}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
{field.type === 'signature' && (
|
|
<Controller
|
|
name={field.id}
|
|
control={control}
|
|
rules={{ required: field.required }}
|
|
render={({ field: { onChange, value } }) => (
|
|
<div>
|
|
<SignatureField
|
|
label={field.label}
|
|
required={field.required}
|
|
value={value || ''}
|
|
onChange={onChange}
|
|
backgroundColor={field.backgroundColor || '#ffffff'}
|
|
penColor={field.penColor || '#000000'}
|
|
penWidth={field.penWidth || 2}
|
|
/>
|
|
{errors[field.id] && (
|
|
<p className="text-red-500 text-sm mt-1">
|
|
{field.required
|
|
? `${field.label} est requis`
|
|
: 'Champ invalide'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
<div className="form-group-submit mt-4">
|
|
<Button
|
|
type="submit"
|
|
primary
|
|
text={formConfig?.submitLabel || 'Envoyer'}
|
|
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
|
|
/>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|