mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-06-04 13:26:11 +00:00
745 lines
26 KiB
JavaScript
745 lines
26 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
import InputTextIcon from './InputTextIcon';
|
|
import SelectChoice from './SelectChoice';
|
|
import Button from './Button';
|
|
import FileUpload from './FileUpload';
|
|
import IconSelector from './IconSelector';
|
|
import * as LucideIcons from 'lucide-react';
|
|
import { FIELD_TYPES } from './FormTypes';
|
|
import FIELD_TYPES_WITH_ICONS from './FieldTypesWithIcons';
|
|
|
|
export default function AddFieldModal({
|
|
isOpen,
|
|
onClose,
|
|
onSubmit,
|
|
editingField = null,
|
|
editingIndex = -1,
|
|
hasMasterFile = false,
|
|
onMasterFileUpload,
|
|
}) {
|
|
const isEditing = editingIndex >= 0;
|
|
|
|
const [currentField, setCurrentField] = useState({
|
|
id: '',
|
|
label: '',
|
|
type: 'text',
|
|
required: false,
|
|
icon: '',
|
|
options: [],
|
|
text: '',
|
|
placeholder: '',
|
|
acceptTypes: '',
|
|
maxSize: 5, // 5MB par défaut
|
|
checked: false,
|
|
masterFileToUpload: null,
|
|
validation: {
|
|
pattern: '',
|
|
minLength: '',
|
|
maxLength: '',
|
|
},
|
|
});
|
|
|
|
const [showIconPicker, setShowIconPicker] = useState(false);
|
|
const [newOption, setNewOption] = useState('');
|
|
|
|
const { control, handleSubmit, reset, setValue } = useForm();
|
|
|
|
// Mettre à jour l'état et les valeurs du formulaire lorsque editingField change
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
const defaultValues = editingField || {
|
|
id: '',
|
|
label: '',
|
|
type: 'text',
|
|
required: false,
|
|
icon: '',
|
|
options: [],
|
|
text: '',
|
|
placeholder: '',
|
|
acceptTypes: '',
|
|
maxSize: 5,
|
|
checked: false,
|
|
masterFileToUpload: null,
|
|
signatureData: '',
|
|
backgroundColor: '#ffffff',
|
|
penColor: '#000000',
|
|
penWidth: 2,
|
|
validation: {
|
|
pattern: '',
|
|
minLength: '',
|
|
maxLength: '',
|
|
},
|
|
};
|
|
|
|
// Si un type a été présélectionné depuis le sélecteur de type
|
|
if (editingField && !isEditing) {
|
|
// S'assurer que le type est correctement défini
|
|
if (typeof editingField.type === 'string') {
|
|
defaultValues.type = editingField.type;
|
|
} else if (editingField.value) {
|
|
defaultValues.type = editingField.value;
|
|
}
|
|
}
|
|
|
|
setCurrentField(defaultValues);
|
|
|
|
// Réinitialiser le formulaire avec les valeurs de l'élément à éditer
|
|
reset({
|
|
type: defaultValues.type,
|
|
label: defaultValues.label,
|
|
placeholder: defaultValues.placeholder,
|
|
required: defaultValues.required,
|
|
icon: defaultValues.icon,
|
|
text: defaultValues.text,
|
|
acceptTypes: defaultValues.acceptTypes,
|
|
maxSize: defaultValues.maxSize,
|
|
checked: defaultValues.checked,
|
|
signatureData: defaultValues.signatureData,
|
|
backgroundColor: defaultValues.backgroundColor,
|
|
penColor: defaultValues.penColor,
|
|
penWidth: defaultValues.penWidth,
|
|
validation: defaultValues.validation,
|
|
});
|
|
}
|
|
}, [isOpen, editingField, reset, isEditing]);
|
|
|
|
// Ajouter une option au select
|
|
const addOption = (e) => {
|
|
// Arrêter la propagation de l'événement pour éviter que le clic n'atteigne l'arrière-plan
|
|
if (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
if (newOption.trim()) {
|
|
// Vérifie si options existe et est un tableau, sinon initialise comme tableau vide
|
|
const currentOptions = Array.isArray(currentField.options)
|
|
? currentField.options
|
|
: [];
|
|
|
|
setCurrentField({
|
|
...currentField,
|
|
options: [...currentOptions, newOption.trim()],
|
|
});
|
|
setNewOption('');
|
|
}
|
|
};
|
|
|
|
// Supprimer une option du select
|
|
const removeOption = (index) => {
|
|
// Vérifie si options existe et est un tableau, sinon initialise comme tableau vide
|
|
const currentOptions = Array.isArray(currentField.options)
|
|
? currentField.options
|
|
: [];
|
|
const newOptions = currentOptions.filter((_, i) => i !== index);
|
|
|
|
setCurrentField({ ...currentField, options: newOptions });
|
|
};
|
|
|
|
// Sélectionner une icône
|
|
const selectIcon = (iconName) => {
|
|
setCurrentField({ ...currentField, icon: iconName });
|
|
|
|
// Mettre à jour la valeur dans le formulaire
|
|
const iconField = control._fields.icon;
|
|
if (iconField && iconField.onChange) {
|
|
iconField.onChange(iconName);
|
|
}
|
|
};
|
|
|
|
const handleFieldSubmit = (data) => {
|
|
onSubmit(data, currentField, editingIndex);
|
|
onClose();
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg p-6 max-w-xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="font-headline text-xl font-semibold">
|
|
{isEditing ? 'Modifier le champ' : 'Ajouter un champ'}
|
|
</h3>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-500 hover:text-gray-700 text-xl"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit(handleFieldSubmit)} className="space-y-4">
|
|
<Controller
|
|
name="type"
|
|
control={control}
|
|
defaultValue={currentField.type}
|
|
render={({ field: { onChange, value } }) => (
|
|
<SelectChoice
|
|
label="Type de champ"
|
|
name="type"
|
|
selected={value}
|
|
callback={(e) => {
|
|
const newType = e.target.value;
|
|
onChange(newType);
|
|
|
|
// Assurons-nous que les options restent un tableau si on sélectionne select ou radio
|
|
let updatedOptions = currentField.options;
|
|
|
|
// Si options n'existe pas ou n'est pas un tableau, initialiser comme tableau vide
|
|
if (!updatedOptions || !Array.isArray(updatedOptions)) {
|
|
updatedOptions = [];
|
|
}
|
|
|
|
setCurrentField({
|
|
...currentField,
|
|
type: newType,
|
|
options: updatedOptions,
|
|
});
|
|
}}
|
|
choices={FIELD_TYPES_WITH_ICONS}
|
|
placeHolder="Sélectionner un type"
|
|
required
|
|
showIcons={true}
|
|
customSelect={true}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
{![
|
|
'paragraph',
|
|
'heading1',
|
|
'heading2',
|
|
'heading3',
|
|
'heading4',
|
|
'heading5',
|
|
'heading6',
|
|
].includes(currentField.type) && (
|
|
<>
|
|
<Controller
|
|
name="label"
|
|
control={control}
|
|
defaultValue={currentField.label}
|
|
render={({ field: { onChange, value } }) => (
|
|
<InputTextIcon
|
|
label="Label du champ"
|
|
name="label"
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
setCurrentField({
|
|
...currentField,
|
|
label: e.target.value,
|
|
});
|
|
}}
|
|
required
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
name="placeholder"
|
|
control={control}
|
|
defaultValue={currentField.placeholder}
|
|
render={({ field: { onChange, value } }) => (
|
|
<InputTextIcon
|
|
label="Placeholder (optionnel)"
|
|
name="placeholder"
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
setCurrentField({
|
|
...currentField,
|
|
placeholder: e.target.value,
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex items-center">
|
|
<Controller
|
|
name="required"
|
|
control={control}
|
|
defaultValue={currentField.required}
|
|
render={({ field: { onChange, value } }) => (
|
|
<input
|
|
type="checkbox"
|
|
id="required"
|
|
checked={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.checked);
|
|
setCurrentField({
|
|
...currentField,
|
|
required: e.target.checked,
|
|
});
|
|
}}
|
|
className="mr-2"
|
|
/>
|
|
)}
|
|
/>
|
|
<label htmlFor="required">Champ obligatoire</label>
|
|
</div>
|
|
|
|
{(currentField.type === 'text' ||
|
|
currentField.type === 'email' ||
|
|
currentField.type === 'date') && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Icône (optionnel)
|
|
</label>
|
|
<Controller
|
|
name="icon"
|
|
control={control}
|
|
defaultValue={currentField.icon}
|
|
render={({ field: { onChange } }) => (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 flex items-center gap-2 p-3 border border-gray-300 rounded-md bg-gray-50">
|
|
{currentField.icon &&
|
|
LucideIcons[currentField.icon] ? (
|
|
<>
|
|
{React.createElement(
|
|
LucideIcons[currentField.icon],
|
|
{
|
|
size: 20,
|
|
className: 'text-gray-600',
|
|
}
|
|
)}
|
|
<span className="text-sm text-gray-700">
|
|
{currentField.icon}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className="text-sm text-gray-500">
|
|
Aucune icône sélectionnée
|
|
</span>
|
|
)}
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
text="Choisir"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
setShowIconPicker(true);
|
|
}}
|
|
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
|
/>
|
|
{currentField.icon && (
|
|
<Button
|
|
type="button"
|
|
text="✕"
|
|
onClick={() => {
|
|
onChange('');
|
|
setCurrentField({ ...currentField, icon: '' });
|
|
}}
|
|
className="px-2 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{[
|
|
'paragraph',
|
|
'heading1',
|
|
'heading2',
|
|
'heading3',
|
|
'heading4',
|
|
'heading5',
|
|
'heading6',
|
|
].includes(currentField.type) && (
|
|
<Controller
|
|
name="text"
|
|
control={control}
|
|
defaultValue={currentField.text}
|
|
render={({ field: { onChange, value } }) => (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
{currentField.type === 'paragraph'
|
|
? 'Texte du paragraphe'
|
|
: 'Texte du titre'}
|
|
</label>
|
|
<textarea
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
setCurrentField({
|
|
...currentField,
|
|
text: e.target.value,
|
|
});
|
|
}}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
rows={3}
|
|
required
|
|
/>
|
|
</div>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{currentField.type === 'select' && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Options de la liste
|
|
</label>
|
|
<div className="flex gap-2 mb-2">
|
|
<input
|
|
type="text"
|
|
value={newOption}
|
|
onChange={(e) => setNewOption(e.target.value)}
|
|
placeholder="Nouvelle option"
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
onKeyPress={(e) =>
|
|
e.key === 'Enter' && (e.preventDefault(), addOption())
|
|
}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
text="Ajouter"
|
|
onClick={addOption}
|
|
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
{Array.isArray(currentField.options) &&
|
|
currentField.options.map((option, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between bg-gray-50 p-2 rounded"
|
|
>
|
|
<span>{option}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeOption(index)}
|
|
className="text-red-500 hover:text-red-700"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{currentField.type === 'radio' && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Options des boutons radio
|
|
</label>
|
|
<div className="flex gap-2 mb-2">
|
|
<input
|
|
type="text"
|
|
value={newOption}
|
|
onChange={(e) => setNewOption(e.target.value)}
|
|
placeholder="Nouvelle option"
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
onKeyPress={(e) =>
|
|
e.key === 'Enter' && (e.preventDefault(), addOption())
|
|
}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
text="Ajouter"
|
|
onClick={addOption}
|
|
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
{Array.isArray(currentField.options) &&
|
|
currentField.options.map((option, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between bg-gray-50 p-2 rounded"
|
|
>
|
|
<span>{option}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeOption(index)}
|
|
className="text-red-500 hover:text-red-700"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{currentField.type === 'phone' && (
|
|
<Controller
|
|
name="validation.pattern"
|
|
control={control}
|
|
defaultValue={currentField.validation?.pattern || ''}
|
|
render={({ field: { onChange, value } }) => (
|
|
<InputTextIcon
|
|
label="Format de téléphone (optionnel, exemple: ^\\+?[0-9]{10,15}$)"
|
|
name="phonePattern"
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
setCurrentField({
|
|
...currentField,
|
|
validation: {
|
|
...currentField.validation,
|
|
pattern: e.target.value,
|
|
},
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{currentField.type === 'file' && (
|
|
<>
|
|
<div className="rounded border border-gray-200 bg-gray-50 p-3">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Document PDF du formulaire{' '}
|
|
<span className="text-red-500">*</span>
|
|
</label>
|
|
<FileUpload
|
|
selectionMessage="Uploader le PDF à afficher dans l'aperçu"
|
|
onFileSelect={(file) => {
|
|
setCurrentField((prev) => ({
|
|
...prev,
|
|
masterFileToUpload: file,
|
|
}));
|
|
if (onMasterFileUpload) {
|
|
onMasterFileUpload(file);
|
|
}
|
|
}}
|
|
enable
|
|
/>
|
|
{!hasMasterFile && !currentField.masterFileToUpload && (
|
|
<p className="text-xs text-red-500 mt-2">
|
|
Uploadez un document avant d'ajouter ce type de champ.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<Controller
|
|
name="acceptTypes"
|
|
control={control}
|
|
defaultValue={currentField.acceptTypes || ''}
|
|
render={({ field: { onChange, value } }) => (
|
|
<InputTextIcon
|
|
label="Types de fichiers acceptés (ex: .pdf,.jpg,.png)"
|
|
name="acceptTypes"
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
setCurrentField({
|
|
...currentField,
|
|
acceptTypes: e.target.value,
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="maxSize"
|
|
control={control}
|
|
defaultValue={currentField.maxSize || 5}
|
|
render={({ field: { onChange, value } }) => (
|
|
<InputTextIcon
|
|
label="Taille maximale (MB)"
|
|
name="maxSize"
|
|
type="number"
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
setCurrentField({
|
|
...currentField,
|
|
maxSize: parseInt(e.target.value) || 5,
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{currentField.type === 'signature' && (
|
|
<>
|
|
<Controller
|
|
name="backgroundColor"
|
|
control={control}
|
|
defaultValue={currentField.backgroundColor || '#ffffff'}
|
|
render={({ field: { onChange, value } }) => (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Couleur de fond
|
|
</label>
|
|
<input
|
|
type="color"
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
setCurrentField({
|
|
...currentField,
|
|
backgroundColor: e.target.value,
|
|
});
|
|
}}
|
|
className="w-full h-10 border border-gray-300 rounded-md cursor-pointer"
|
|
/>
|
|
</div>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="penColor"
|
|
control={control}
|
|
defaultValue={currentField.penColor || '#000000'}
|
|
render={({ field: { onChange, value } }) => (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Couleur du stylo
|
|
</label>
|
|
<input
|
|
type="color"
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
setCurrentField({
|
|
...currentField,
|
|
penColor: e.target.value,
|
|
});
|
|
}}
|
|
className="w-full h-10 border border-gray-300 rounded-md cursor-pointer"
|
|
/>
|
|
</div>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="penWidth"
|
|
control={control}
|
|
defaultValue={currentField.penWidth || 2}
|
|
render={({ field: { onChange, value } }) => (
|
|
<InputTextIcon
|
|
label="Épaisseur du stylo (px)"
|
|
name="penWidth"
|
|
type="number"
|
|
min="1"
|
|
max="10"
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(parseInt(e.target.value));
|
|
setCurrentField({
|
|
...currentField,
|
|
penWidth: parseInt(e.target.value) || 2,
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{currentField.type === 'checkbox' && (
|
|
<>
|
|
<div className="flex items-center mt-2">
|
|
<Controller
|
|
name="checked"
|
|
control={control}
|
|
defaultValue={currentField.checked || false}
|
|
render={({ field: { onChange, value } }) => (
|
|
<input
|
|
type="checkbox"
|
|
id="defaultChecked"
|
|
checked={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.checked);
|
|
setCurrentField({
|
|
...currentField,
|
|
checked: e.target.checked,
|
|
});
|
|
}}
|
|
className="mr-2"
|
|
/>
|
|
)}
|
|
/>
|
|
<label htmlFor="defaultChecked">Coché par défaut</label>
|
|
</div>
|
|
<div className="flex items-center mt-2">
|
|
<Controller
|
|
name="horizontal"
|
|
control={control}
|
|
defaultValue={currentField.horizontal || false}
|
|
render={({ field: { onChange, value } }) => (
|
|
<input
|
|
type="checkbox"
|
|
id="horizontal"
|
|
checked={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.checked);
|
|
setCurrentField({
|
|
...currentField,
|
|
horizontal: e.target.checked,
|
|
});
|
|
}}
|
|
className="mr-2"
|
|
/>
|
|
)}
|
|
/>
|
|
<label htmlFor="horizontal">Label au-dessus (horizontal)</label>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{currentField.type === 'toggle' && (
|
|
<div className="flex items-center mt-2">
|
|
<Controller
|
|
name="checked"
|
|
control={control}
|
|
defaultValue={currentField.checked || false}
|
|
render={({ field: { onChange, value } }) => (
|
|
<input
|
|
type="checkbox"
|
|
id="defaultToggled"
|
|
checked={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.checked);
|
|
setCurrentField({
|
|
...currentField,
|
|
checked: e.target.checked,
|
|
});
|
|
}}
|
|
className="mr-2"
|
|
/>
|
|
)}
|
|
/>
|
|
<label htmlFor="defaultToggled">Activé par défaut</label>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-2 mt-6">
|
|
<Button
|
|
type="submit"
|
|
text={isEditing ? 'Modifier' : 'Ajouter'}
|
|
className="px-4 py-2 bg-primary text-white rounded hover:bg-secondary"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
text="Annuler"
|
|
onClick={onClose}
|
|
className="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600"
|
|
/>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Sélecteur d'icônes - déplacé en dehors du formulaire */}
|
|
<IconSelector
|
|
isOpen={showIconPicker}
|
|
onClose={() => setShowIconPicker(false)}
|
|
onSelect={selectIcon}
|
|
selectedIcon={currentField.icon}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|