import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import InputTextIcon from './InputTextIcon';
import FormRenderer from './FormRenderer';
import AddFieldModal from './AddFieldModal';
import FieldTypeSelector from './FieldTypeSelector';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import {
Edit2,
Trash2,
PlusCircle,
Download,
Upload,
GripVertical,
TextCursorInput,
AtSign,
Calendar,
ChevronDown,
Type,
AlignLeft,
Save,
ChevronUp,
Heading1,
Heading2,
Heading3,
Heading4,
Heading5,
Heading6,
Code,
Eye,
EyeOff,
Phone,
Radio,
ToggleLeft,
CheckSquare,
FileUp,
PenTool,
} from 'lucide-react';
const FIELD_TYPES_ICON = {
text: { icon: TextCursorInput },
email: { icon: AtSign },
phone: { icon: Phone },
date: { icon: Calendar },
select: { icon: ChevronDown },
radio: { icon: Radio },
checkbox: { icon: CheckSquare },
toggle: { icon: ToggleLeft },
file: { icon: FileUp },
signature: { icon: PenTool },
textarea: { icon: Type },
paragraph: { icon: AlignLeft },
heading1: { icon: Heading1 },
heading2: { icon: Heading2 },
heading3: { icon: Heading3 },
heading4: { icon: Heading4 },
heading5: { icon: Heading5 },
heading6: { icon: Heading6 },
};
// Type d'item pour le drag and drop
const ItemTypes = {
FIELD: 'field',
};
// Composant pour un champ draggable
const DraggableFieldItem = ({
field,
index,
moveField,
editField,
deleteField,
}) => {
const ref = React.useRef(null);
// Configuration du drag (ce qu'on peut déplacer)
const [{ isDragging }, drag] = useDrag({
type: ItemTypes.FIELD,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
// Configuration du drop (où on peut déposer)
const [, drop] = useDrop({
accept: ItemTypes.FIELD,
hover: (item, monitor) => {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// Ne rien faire si on survole le même élément
if (dragIndex === hoverIndex) {
return;
}
// Déterminer la position de la souris par rapport à l'élément survolé
const hoverBoundingRect = ref.current.getBoundingClientRect();
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Ne pas remplacer si on n'a pas dépassé la moitié de l'élément
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Effectuer le déplacement
moveField(dragIndex, hoverIndex);
// Mettre à jour l'index de l'élément déplacé
item.index = hoverIndex;
},
});
// Combiner drag et drop sur le même élément de référence
drag(drop(ref));
return (
{FIELD_TYPES_ICON[field.type] &&
React.createElement(FIELD_TYPES_ICON[field.type].icon, {
size: 18,
className: 'text-gray-600',
})}
{field.type === 'paragraph'
? 'Paragraphe'
: field.type.startsWith('heading')
? `Titre ${field.type.replace('heading', '')}`
: field.label}
({field.type}){field.required && ' *'}
);
};
export default function FormTemplateBuilder({
onSave,
initialData,
groups,
isEditing,
}) {
const [formConfig, setFormConfig] = useState({
id: initialData?.id || 0,
title: initialData?.name || 'Nouveau formulaire',
submitLabel: 'Envoyer',
fields: initialData?.formMasterData?.fields || [],
});
const [selectedGroups, setSelectedGroups] = useState(
initialData?.groups?.map((g) => g.id) || []
);
const [showAddFieldModal, setShowAddFieldModal] = useState(false);
const [showFieldTypeSelector, setShowFieldTypeSelector] = useState(false);
const [selectedFieldType, setSelectedFieldType] = useState(null);
const [editingIndex, setEditingIndex] = useState(-1);
const [saving, setSaving] = useState(false);
const [saveMessage, setSaveMessage] = useState({ type: '', text: '' });
const [showScrollButton, setShowScrollButton] = useState(false);
const [showJsonSection, setShowJsonSection] = useState(false);
const { reset: resetField } = useForm();
// Initialiser les données du formulaire quand initialData change
useEffect(() => {
if (initialData) {
setFormConfig({
id: initialData.id || 0,
title: initialData.name || 'Nouveau formulaire',
submitLabel: 'Envoyer',
fields: initialData.formMasterData?.fields || [],
});
setSelectedGroups(initialData.groups?.map((g) => g.id) || []);
}
}, [initialData]);
// Gérer l'affichage du bouton de défilement
useEffect(() => {
const handleScroll = () => {
// Afficher le bouton quand on descend d'au moins 300px
setShowScrollButton(window.scrollY > 300);
};
// Ajouter l'écouteur d'événement
window.addEventListener('scroll', handleScroll);
// Nettoyage de l'écouteur lors du démontage du composant
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
// Fonction pour remonter en haut de la page
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};
// Générer un ID unique pour les champs
const generateFieldId = (label) => {
return label
.toLowerCase()
.replace(/[àáâãäå]/g, 'a')
.replace(/[èéêë]/g, 'e')
.replace(/[ìíîï]/g, 'i')
.replace(/[òóôõö]/g, 'o')
.replace(/[ùúûü]/g, 'u')
.replace(/[ç]/g, 'c')
.replace(/[^a-z0-9]/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');
};
// Ajouter ou modifier un champ
const handleFieldSubmit = (data, currentField, editIndex) => {
const isHeadingType = data.type.startsWith('heading');
const isContentTypeOnly = data.type === 'paragraph' || isHeadingType;
const fieldData = {
...data,
id: isContentTypeOnly
? undefined
: generateFieldId(data.label || 'field'),
options: ['select', 'radio'].includes(data.type)
? Array.isArray(currentField.options)
? currentField.options
: []
: undefined,
icon: data.icon || currentField.icon || undefined,
placeholder: data.placeholder || undefined,
text: isContentTypeOnly ? data.text : undefined,
checked: ['checkbox', 'toggle'].includes(data.type)
? currentField.checked
: undefined,
horizontal:
data.type === 'checkbox' ? currentField.horizontal : undefined,
acceptTypes: data.type === 'file' ? currentField.acceptTypes : undefined,
maxSize: data.type === 'file' ? currentField.maxSize : undefined,
validation: ['phone', 'email', 'text'].includes(data.type)
? currentField.validation
: undefined,
};
// Nettoyer les propriétés undefined
Object.keys(fieldData).forEach((key) => {
if (fieldData[key] === undefined || fieldData[key] === '') {
delete fieldData[key];
}
});
const newFields = [...formConfig.fields];
if (editIndex >= 0) {
newFields[editIndex] = fieldData;
} else {
newFields.push(fieldData);
}
setFormConfig({ ...formConfig, fields: newFields });
setEditingIndex(-1);
}; // Modifier un champ existant
const editField = (index) => {
setEditingIndex(index);
setShowAddFieldModal(true);
};
// Supprimer un champ
const deleteField = (index) => {
const newFields = formConfig.fields.filter((_, i) => i !== index);
setFormConfig({ ...formConfig, fields: newFields });
};
// Déplacer un champ
const moveField = (dragIndex, hoverIndex) => {
const newFields = [...formConfig.fields];
const draggedField = newFields[dragIndex];
// Supprimer l'élément déplacé
newFields.splice(dragIndex, 1);
// Insérer l'élément à sa nouvelle position
newFields.splice(hoverIndex, 0, draggedField);
setFormConfig({ ...formConfig, fields: newFields });
};
// Exporter le JSON
const exportJson = () => {
const jsonString = JSON.stringify(formConfig, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `formulaire_${formConfig.title.replace(/\s+/g, '_').toLowerCase()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Importer un JSON
const importJson = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const imported = JSON.parse(e.target.result);
setFormConfig(imported);
} catch (error) {
alert('Erreur lors de l'importation du fichier JSON');
}
};
reader.readAsText(file);
}
};
// Sauvegarder le formulaire (pour le backend)
const saveFormTemplate = async () => {
// Validation basique
if (!formConfig.title.trim()) {
setSaveMessage({
type: 'error',
text: 'Le titre du formulaire est requis',
});
return;
}
if (formConfig.fields.length === 0) {
setSaveMessage({
type: 'error',
text: 'Ajoutez au moins un champ au formulaire',
});
return;
}
if (selectedGroups.length === 0) {
setSaveMessage({
type: 'error',
text: "Sélectionnez au moins un groupe d'inscription",
});
return;
}
setSaving(true);
setSaveMessage({ type: '', text: '' });
try {
const dataToSave = {
name: formConfig.title,
group_ids: selectedGroups,
formMasterData: formConfig,
};
if (isEditing && initialData) {
dataToSave.id = initialData.id;
}
if (onSave) {
onSave(dataToSave);
}
setSaveMessage({
type: 'success',
text: 'Formulaire enregistré avec succès',
});
} catch (error) {
setSaveMessage({
type: 'error',
text:
error.message || "Une erreur est survenue lors de l'enregistrement",
});
} finally {
setSaving(false);
}
};
// Fonction pour gérer la sélection d'un type de champ
const handleFieldTypeSelect = (fieldType) => {
setSelectedFieldType(fieldType);
setShowFieldTypeSelector(false);
setShowAddFieldModal(true);
};
return (
{/* Panel de configuration */}
Configuration du formulaire
{/* Configuration générale */}
Paramètres généraux
{saveMessage.text && (
{saveMessage.text}
)}
setFormConfig({ ...formConfig, title: e.target.value })
}
required
/>
setFormConfig({
...formConfig,
submitLabel: e.target.value,
})
}
/>
{/* Sélecteur de groupes */}
{groups && groups.length > 0 ? (
groups.map((group) => (
))
) : (
Aucun groupe disponible
)}
{/* Liste des champs */}
Champs du formulaire ({formConfig.fields.length})
{formConfig.fields.length === 0 ? (
Aucun champ ajouté
) : (
{formConfig.fields.map((field, index) => (
))}
)}
{/* Actions */}
{/* Les actions ont été déplacées dans la section JSON généré */}
{/* JSON généré */}
{showJsonSection && (
{JSON.stringify(formConfig, null, 2)}
)}
{/* Aperçu */}
Aperçu du formulaire
{formConfig.fields.length > 0 ? (
) : (
Ajoutez des champs pour voir l'aperçu
)}
{/* Modal d'ajout/modification de champ */}
setShowAddFieldModal(false)}
onSubmit={handleFieldSubmit}
editingField={
editingIndex >= 0
? formConfig.fields[editingIndex]
: selectedFieldType
? { type: selectedFieldType.value || selectedFieldType }
: null
}
editingIndex={editingIndex}
/>
{/* Sélecteur de type de champ */}
setShowFieldTypeSelector(false)}
onSelect={handleFieldTypeSelect}
/>
{/* Bouton flottant pour remonter en haut */}
{showScrollButton && (
)}
);
}