feat: Précablage du formulaire dynamique [N3WTS-17]

This commit is contained in:
Luc SORIGNET
2025-11-30 17:24:25 +01:00
parent 7486f6c5ce
commit dd00cba385
41 changed files with 2637 additions and 606 deletions

View File

@ -3,6 +3,7 @@ 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 {
@ -34,6 +35,7 @@ import {
ToggleLeft,
CheckSquare,
FileUp,
PenTool,
} from 'lucide-react';
const FIELD_TYPES_ICON = {
@ -46,6 +48,7 @@ const FIELD_TYPES_ICON = {
checkbox: { icon: CheckSquare },
toggle: { icon: ToggleLeft },
file: { icon: FileUp },
signature: { icon: PenTool },
textarea: { icon: Type },
paragraph: { icon: AlignLeft },
heading1: { icon: Heading1 },
@ -168,15 +171,26 @@ const DraggableFieldItem = ({
);
};
export default function FormTemplateBuilder() {
export default function FormTemplateBuilder({
onSave,
initialData,
groups,
isEditing,
}) {
const [formConfig, setFormConfig] = useState({
id: 0,
title: 'Nouveau formulaire',
id: initialData?.id || 0,
title: initialData?.name || 'Nouveau formulaire',
submitLabel: 'Envoyer',
fields: [],
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: '' });
@ -185,6 +199,19 @@ export default function FormTemplateBuilder() {
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 = () => {
@ -235,7 +262,9 @@ export default function FormTemplateBuilder() {
? undefined
: generateFieldId(data.label || 'field'),
options: ['select', 'radio'].includes(data.type)
? currentField.options
? Array.isArray(currentField.options)
? currentField.options
: []
: undefined,
icon: data.icon || currentField.icon || undefined,
placeholder: data.placeholder || undefined,
@ -345,35 +374,36 @@ export default function FormTemplateBuilder() {
return;
}
if (selectedGroups.length === 0) {
setSaveMessage({
type: 'error',
text: "Sélectionnez au moins un groupe d'inscription",
});
return;
}
setSaving(true);
setSaveMessage({ type: '', text: '' });
try {
// Simulation d'envoi au backend (à remplacer par l'appel API réel)
// const response = await fetch('/api/form-templates', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(formConfig),
// });
const dataToSave = {
name: formConfig.title,
group_ids: selectedGroups,
formMasterData: formConfig,
};
// if (!response.ok) {
// throw new Error('Erreur lors de l\'enregistrement du formulaire');
// }
if (isEditing && initialData) {
dataToSave.id = initialData.id;
}
// const data = await response.json();
// Simulation d'une réponse du backend
await new Promise((resolve) => setTimeout(resolve, 1000));
if (onSave) {
onSave(dataToSave);
}
setSaveMessage({
type: 'success',
text: 'Formulaire enregistré avec succès',
});
// Si le backend renvoie un ID, on peut mettre à jour l'ID du formulaire
// setFormConfig({ ...formConfig, id: data.id });
} catch (error) {
setSaveMessage({
type: 'error',
@ -385,6 +415,13 @@ export default function FormTemplateBuilder() {
}
};
// Fonction pour gérer la sélection d'un type de champ
const handleFieldTypeSelect = (fieldType) => {
setSelectedFieldType(fieldType);
setShowFieldTypeSelector(false);
setShowAddFieldModal(true);
};
return (
<DndProvider backend={HTML5Backend}>
<div className="max-w-6xl mx-auto p-6">
@ -476,6 +513,44 @@ export default function FormTemplateBuilder() {
})
}
/>
{/* Sélecteur de groupes */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
Groupes d'inscription{' '}
<span className="text-red-500">*</span>
</label>
<div className="space-y-2 max-h-32 overflow-y-auto border rounded-md p-2">
{groups && groups.length > 0 ? (
groups.map((group) => (
<label key={group.id} className="flex items-center">
<input
type="checkbox"
checked={selectedGroups.includes(group.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedGroups([
...selectedGroups,
group.id,
]);
} else {
setSelectedGroups(
selectedGroups.filter((id) => id !== group.id)
);
}
}}
className="mr-2 text-blue-600"
/>
<span className="text-sm">{group.name}</span>
</label>
))
) : (
<p className="text-gray-500 text-sm">
Aucun groupe disponible
</p>
)}
</div>
</div>
</div>
{/* Liste des champs */}
@ -487,7 +562,8 @@ export default function FormTemplateBuilder() {
<button
onClick={() => {
setEditingIndex(-1);
setShowAddFieldModal(true);
setSelectedFieldType(null);
setShowFieldTypeSelector(true);
}}
className="p-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors"
title="Ajouter un champ"
@ -504,7 +580,8 @@ export default function FormTemplateBuilder() {
<button
onClick={() => {
setEditingIndex(-1);
setShowAddFieldModal(true);
setSelectedFieldType(null);
setShowFieldTypeSelector(true);
}}
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors inline-flex items-center gap-2"
>
@ -593,11 +670,22 @@ export default function FormTemplateBuilder() {
onClose={() => setShowAddFieldModal(false)}
onSubmit={handleFieldSubmit}
editingField={
editingIndex >= 0 ? formConfig.fields[editingIndex] : null
editingIndex >= 0
? formConfig.fields[editingIndex]
: selectedFieldType
? { type: selectedFieldType.value || selectedFieldType }
: null
}
editingIndex={editingIndex}
/>
{/* Sélecteur de type de champ */}
<FieldTypeSelector
isOpen={showFieldTypeSelector}
onClose={() => setShowFieldTypeSelector(false)}
onSelect={handleFieldTypeSelect}
/>
{/* Bouton flottant pour remonter en haut */}
{showScrollButton && (
<div className="fixed bottom-6 right-6 z-10">