Files
n3wt-school/Front-End/src/components/Form/AddFieldModal.js

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&apos;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>
);
}