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 généré

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