mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Ajout des composants manquant dans le FormTemplateBuilder [N3WTS-17]
This commit is contained in:
@ -4,7 +4,7 @@ import Tab from '@/components/Tab';
|
|||||||
import TabContent from '@/components/TabContent';
|
import TabContent from '@/components/TabContent';
|
||||||
import Button from '@/components/Form/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import InputText from '@/components/Form/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import CheckBox from '@/components/CheckBox'; // Import du composant CheckBox
|
import CheckBox from '@/components/Form/CheckBox'; // Import du composant CheckBox
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import {
|
import {
|
||||||
fetchSmtpSettings,
|
fetchSmtpSettings,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import logger from '@/utils/logger';
|
|||||||
import { useClasses } from '@/context/ClassesContext';
|
import { useClasses } from '@/context/ClassesContext';
|
||||||
import Button from '@/components/Form/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import SelectChoice from '@/components/Form/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import {
|
import {
|
||||||
fetchAbsences,
|
fetchAbsences,
|
||||||
createAbsences,
|
createAbsences,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import FeesSection from '@/components/Structure/Tarification/FeesSection';
|
|||||||
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
|
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
|
||||||
import SectionTitle from '@/components/SectionTitle';
|
import SectionTitle from '@/components/SectionTitle';
|
||||||
import InputPhone from '@/components/Form/InputPhone';
|
import InputPhone from '@/components/Form/InputPhone';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import RadioList from '@/components/Form/RadioList';
|
import RadioList from '@/components/Form/RadioList';
|
||||||
import SelectChoice from '@/components/Form/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import InputTextIcon from './InputTextIcon';
|
import InputTextIcon from './InputTextIcon';
|
||||||
import SelectChoice from './SelectChoice';
|
import SelectChoice from './SelectChoice';
|
||||||
@ -16,23 +16,69 @@ export default function AddFieldModal({
|
|||||||
}) {
|
}) {
|
||||||
const isEditing = editingIndex >= 0;
|
const isEditing = editingIndex >= 0;
|
||||||
|
|
||||||
const [currentField, setCurrentField] = useState(
|
const [currentField, setCurrentField] = useState({
|
||||||
editingField || {
|
id: '',
|
||||||
id: '',
|
label: '',
|
||||||
label: '',
|
type: 'text',
|
||||||
type: 'text',
|
required: false,
|
||||||
required: false,
|
icon: '',
|
||||||
icon: '',
|
options: [],
|
||||||
options: [],
|
text: '',
|
||||||
text: '',
|
placeholder: '',
|
||||||
placeholder: '',
|
acceptTypes: '',
|
||||||
}
|
maxSize: 5, // 5MB par défaut
|
||||||
);
|
checked: false,
|
||||||
|
validation: {
|
||||||
|
pattern: '',
|
||||||
|
minLength: '',
|
||||||
|
maxLength: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [showIconPicker, setShowIconPicker] = useState(false);
|
const [showIconPicker, setShowIconPicker] = useState(false);
|
||||||
const [newOption, setNewOption] = useState('');
|
const [newOption, setNewOption] = useState('');
|
||||||
|
|
||||||
const { control, handleSubmit, reset } = useForm();
|
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,
|
||||||
|
validation: {
|
||||||
|
pattern: '',
|
||||||
|
minLength: '',
|
||||||
|
maxLength: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
validation: defaultValues.validation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOpen, editingField, reset]);
|
||||||
|
|
||||||
// Ajouter une option au select
|
// Ajouter une option au select
|
||||||
const addOption = () => {
|
const addOption = () => {
|
||||||
@ -326,6 +372,195 @@ export default function AddFieldModal({
|
|||||||
</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">
|
||||||
|
{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' && (
|
||||||
|
<>
|
||||||
|
<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 === '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">
|
<div className="flex gap-2 mt-6">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const CheckBox = ({
|
|||||||
handleChange,
|
handleChange,
|
||||||
fieldName,
|
fieldName,
|
||||||
itemLabelFunc = () => null,
|
itemLabelFunc = () => null,
|
||||||
horizontal,
|
horizontal = false,
|
||||||
}) => {
|
}) => {
|
||||||
// Vérifier si formData[fieldName] est un tableau ou une valeur booléenne
|
// Vérifier si formData[fieldName] est un tableau ou une valeur booléenne
|
||||||
const isChecked = Array.isArray(formData[fieldName])
|
const isChecked = Array.isArray(formData[fieldName])
|
||||||
@ -22,7 +22,7 @@ const CheckBox = ({
|
|||||||
{horizontal && (
|
{horizontal && (
|
||||||
<label
|
<label
|
||||||
htmlFor={`${fieldName}-${item.id}`}
|
htmlFor={`${fieldName}-${item.id}`}
|
||||||
className="block text-sm text-center mb-1 font-medium text-gray-700"
|
className="block text-sm text-center mb-1 font-medium text-gray-700 cursor-pointer"
|
||||||
>
|
>
|
||||||
{itemLabelFunc(item)}
|
{itemLabelFunc(item)}
|
||||||
</label>
|
</label>
|
||||||
@ -40,7 +40,7 @@ const CheckBox = ({
|
|||||||
{!horizontal && (
|
{!horizontal && (
|
||||||
<label
|
<label
|
||||||
htmlFor={`${fieldName}-${item.id}`}
|
htmlFor={`${fieldName}-${item.id}`}
|
||||||
className="block text-sm text-center mb-1 font-medium text-gray-700"
|
className="block text-sm font-medium text-gray-700 cursor-pointer"
|
||||||
>
|
>
|
||||||
{itemLabelFunc(item)}
|
{itemLabelFunc(item)}
|
||||||
</label>
|
</label>
|
||||||
@ -6,6 +6,11 @@ import * as LucideIcons from 'lucide-react';
|
|||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import DjangoCSRFToken from '../DjangoCSRFToken';
|
import DjangoCSRFToken from '../DjangoCSRFToken';
|
||||||
import WisiwigTextArea from './WisiwigTextArea';
|
import WisiwigTextArea from './WisiwigTextArea';
|
||||||
|
import RadioList from './RadioList';
|
||||||
|
import CheckBox from './CheckBox';
|
||||||
|
import ToggleSwitch from './ToggleSwitch';
|
||||||
|
import InputPhone from './InputPhone';
|
||||||
|
import FileUpload from './FileUpload';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Récupère une icône Lucide par son nom.
|
* Récupère une icône Lucide par son nom.
|
||||||
@ -60,6 +65,9 @@ const formConfigTest = {
|
|||||||
export default function FormRenderer({
|
export default function FormRenderer({
|
||||||
formConfig = formConfigTest,
|
formConfig = formConfigTest,
|
||||||
csrfToken,
|
csrfToken,
|
||||||
|
onFormSubmit = (data) => {
|
||||||
|
alert(JSON.stringify(data, null, 2));
|
||||||
|
}, // Callback de soumission personnalisé (optionnel)
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -68,19 +76,103 @@ export default function FormRenderer({
|
|||||||
reset,
|
reset,
|
||||||
} = useForm();
|
} = useForm();
|
||||||
|
|
||||||
const onSubmit = (data) => {
|
// Fonction utilitaire pour envoyer les données au backend
|
||||||
|
const sendFormDataToBackend = async (formData) => {
|
||||||
|
try {
|
||||||
|
// Cette fonction peut être remplacée par votre propre implémentation
|
||||||
|
// Exemple avec fetch:
|
||||||
|
const response = await fetch('/api/submit-form', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
// Les en-têtes sont automatiquement définis pour FormData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
logger.debug('Envoi réussi:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Erreur lors de l'envoi:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
logger.debug('=== DÉBUT onSubmit ===');
|
logger.debug('=== DÉBUT onSubmit ===');
|
||||||
logger.debug('Réponses :', data);
|
logger.debug('Réponses :', data);
|
||||||
|
|
||||||
const formattedData = {
|
try {
|
||||||
//TODO: idDossierInscriptions: 123,
|
// Vérifier si nous avons des fichiers dans les données
|
||||||
formId: formConfig.id,
|
const hasFiles = Object.keys(data).some((key) => {
|
||||||
responses: { ...data },
|
return (
|
||||||
};
|
data[key] instanceof FileList ||
|
||||||
|
(data[key] && data[key][0] instanceof File)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasFiles) {
|
||||||
|
// Utiliser FormData pour l'envoi de fichiers
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Ajouter l'ID du formulaire
|
||||||
|
formData.append('formId', formConfig.id.toString());
|
||||||
|
|
||||||
|
// Traiter chaque champ et ses valeurs
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
const value = data[key];
|
||||||
|
|
||||||
|
if (
|
||||||
|
value instanceof FileList ||
|
||||||
|
(value && value[0] instanceof File)
|
||||||
|
) {
|
||||||
|
// Gérer les champs de type fichier
|
||||||
|
if (value.length > 0) {
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
formData.append(`files.${key}`, value[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Gérer les autres types de champs
|
||||||
|
formData.append(
|
||||||
|
`data.${key}`,
|
||||||
|
value !== undefined ? value.toString() : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onFormSubmit) {
|
||||||
|
// Utiliser le callback personnalisé si fourni
|
||||||
|
await onFormSubmit(formData, true);
|
||||||
|
} else {
|
||||||
|
// Sinon, utiliser la fonction par défaut
|
||||||
|
await sendFormDataToBackend(formData);
|
||||||
|
alert('Formulaire avec fichier(s) envoyé avec succès');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pas de fichier, on peut utiliser JSON
|
||||||
|
const formattedData = {
|
||||||
|
formId: formConfig.id,
|
||||||
|
responses: { ...data },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (onFormSubmit) {
|
||||||
|
// Utiliser le callback personnalisé si fourni
|
||||||
|
await onFormSubmit(formattedData, false);
|
||||||
|
} else {
|
||||||
|
// Afficher un message pour démonstration
|
||||||
|
alert('Données reçues : ' + JSON.stringify(formattedData, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(); // Réinitialiser le formulaire après soumission
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la soumission du formulaire:', error);
|
||||||
|
alert(`Erreur lors de l'envoi du formulaire: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: ENVOYER LES DONNÉES AU BACKEND
|
|
||||||
alert('Données reçues : ' + JSON.stringify(formattedData, null, 2));
|
|
||||||
reset(); // Réinitialiser le formulaire après soumission
|
|
||||||
logger.debug('=== FIN onSubmit ===');
|
logger.debug('=== FIN onSubmit ===');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,7 +224,14 @@ export default function FormRenderer({
|
|||||||
<Controller
|
<Controller
|
||||||
name={field.id}
|
name={field.id}
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: field.required }}
|
rules={{
|
||||||
|
required: field.required,
|
||||||
|
pattern: field.validation?.pattern
|
||||||
|
? new RegExp(field.validation.pattern)
|
||||||
|
: undefined,
|
||||||
|
minLength: field.validation?.minLength,
|
||||||
|
maxLength: field.validation?.maxLength,
|
||||||
|
}}
|
||||||
render={({ field: { onChange, value, name } }) => (
|
render={({ field: { onChange, value, name } }) => (
|
||||||
<InputTextIcon
|
<InputTextIcon
|
||||||
label={field.label}
|
label={field.label}
|
||||||
@ -153,6 +252,29 @@ export default function FormRenderer({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{field.type === 'phone' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<InputPhone
|
||||||
|
label={field.label}
|
||||||
|
required={field.required}
|
||||||
|
name={name}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
errorMsg={
|
||||||
|
errors[field.id]
|
||||||
|
? field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{field.type === 'select' && (
|
{field.type === 'select' && (
|
||||||
<Controller
|
<Controller
|
||||||
name={field.id}
|
name={field.id}
|
||||||
@ -178,6 +300,111 @@ export default function FormRenderer({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{field.type === 'radio' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<RadioList
|
||||||
|
items={field.options.map((option, idx) => ({
|
||||||
|
id: idx,
|
||||||
|
label: option,
|
||||||
|
}))}
|
||||||
|
formData={{
|
||||||
|
[field.id]: value
|
||||||
|
? field.options.findIndex((o) => o === value)
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
|
handleChange={(e) =>
|
||||||
|
onChange(field.options[parseInt(e.target.value)])
|
||||||
|
}
|
||||||
|
fieldName={field.id}
|
||||||
|
sectionLabel={field.label}
|
||||||
|
required={field.required}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'checkbox' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
defaultValue={field.checked || false}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<div>
|
||||||
|
<CheckBox
|
||||||
|
item={{ id: field.id, label: field.label }}
|
||||||
|
formData={{ [field.id]: value || false }}
|
||||||
|
handleChange={(e) => onChange(e.target.checked)}
|
||||||
|
fieldName={field.id}
|
||||||
|
itemLabelFunc={(item) => item.label}
|
||||||
|
horizontal={field.horizontal || false}
|
||||||
|
/>
|
||||||
|
{errors[field.id] && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'toggle' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
defaultValue={field.checked || false}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<div>
|
||||||
|
<ToggleSwitch
|
||||||
|
name={field.id}
|
||||||
|
label={field.label + (field.required ? ' *' : '')}
|
||||||
|
checked={value || false}
|
||||||
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
{errors[field.id] && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'file' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<FileUpload
|
||||||
|
selectionMessage={field.label}
|
||||||
|
required={field.required}
|
||||||
|
uploadedFileName={value ? value[0]?.name : null}
|
||||||
|
onFileSelect={(file) => {
|
||||||
|
// Créer un objet de type FileList similaire pour la compatibilité
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.items.add(file);
|
||||||
|
onChange(dataTransfer.files);
|
||||||
|
}}
|
||||||
|
errorMsg={
|
||||||
|
errors[field.id]
|
||||||
|
? field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{field.type === 'textarea' && (
|
{field.type === 'textarea' && (
|
||||||
<Controller
|
<Controller
|
||||||
name={field.id}
|
name={field.id}
|
||||||
|
|||||||
@ -29,13 +29,23 @@ import {
|
|||||||
Code,
|
Code,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
|
Phone,
|
||||||
|
Radio,
|
||||||
|
ToggleLeft,
|
||||||
|
CheckSquare,
|
||||||
|
FileUp,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const FIELD_TYPES_ICON = {
|
const FIELD_TYPES_ICON = {
|
||||||
text: { icon: TextCursorInput },
|
text: { icon: TextCursorInput },
|
||||||
email: { icon: AtSign },
|
email: { icon: AtSign },
|
||||||
|
phone: { icon: Phone },
|
||||||
date: { icon: Calendar },
|
date: { icon: Calendar },
|
||||||
select: { icon: ChevronDown },
|
select: { icon: ChevronDown },
|
||||||
|
radio: { icon: Radio },
|
||||||
|
checkbox: { icon: CheckSquare },
|
||||||
|
toggle: { icon: ToggleLeft },
|
||||||
|
file: { icon: FileUp },
|
||||||
textarea: { icon: Type },
|
textarea: { icon: Type },
|
||||||
paragraph: { icon: AlignLeft },
|
paragraph: { icon: AlignLeft },
|
||||||
heading1: { icon: Heading1 },
|
heading1: { icon: Heading1 },
|
||||||
@ -224,10 +234,22 @@ export default function FormTemplateBuilder() {
|
|||||||
id: isContentTypeOnly
|
id: isContentTypeOnly
|
||||||
? undefined
|
? undefined
|
||||||
: generateFieldId(data.label || 'field'),
|
: generateFieldId(data.label || 'field'),
|
||||||
options: data.type === 'select' ? currentField.options : undefined,
|
options: ['select', 'radio'].includes(data.type)
|
||||||
|
? currentField.options
|
||||||
|
: undefined,
|
||||||
icon: data.icon || currentField.icon || undefined,
|
icon: data.icon || currentField.icon || undefined,
|
||||||
placeholder: data.placeholder || undefined,
|
placeholder: data.placeholder || undefined,
|
||||||
text: isContentTypeOnly ? data.text : 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
|
// Nettoyer les propriétés undefined
|
||||||
@ -247,9 +269,7 @@ export default function FormTemplateBuilder() {
|
|||||||
|
|
||||||
setFormConfig({ ...formConfig, fields: newFields });
|
setFormConfig({ ...formConfig, fields: newFields });
|
||||||
setEditingIndex(-1);
|
setEditingIndex(-1);
|
||||||
};
|
}; // Modifier un champ existant
|
||||||
|
|
||||||
// Modifier un champ existant
|
|
||||||
const editField = (index) => {
|
const editField = (index) => {
|
||||||
setEditingIndex(index);
|
setEditingIndex(index);
|
||||||
setShowAddFieldModal(true);
|
setShowAddFieldModal(true);
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
export const FIELD_TYPES = [
|
export const FIELD_TYPES = [
|
||||||
{ value: 'text', label: 'Texte' },
|
{ value: 'text', label: 'Texte' },
|
||||||
{ value: 'email', label: 'Email' },
|
{ value: 'email', label: 'Email' },
|
||||||
|
{ value: 'phone', label: 'Téléphone' },
|
||||||
{ value: 'date', label: 'Date' },
|
{ value: 'date', label: 'Date' },
|
||||||
{ value: 'select', label: 'Liste déroulante' },
|
{ value: 'select', label: 'Liste déroulante' },
|
||||||
|
{ value: 'radio', label: 'Boutons radio' },
|
||||||
|
{ value: 'checkbox', label: 'Case à cocher' },
|
||||||
|
{ value: 'toggle', label: 'Interrupteur' },
|
||||||
|
{ value: 'file', label: 'Upload de fichier' },
|
||||||
{ value: 'textarea', label: 'Zone de texte riche' },
|
{ value: 'textarea', label: 'Zone de texte riche' },
|
||||||
{ value: 'paragraph', label: 'Paragraphe' },
|
{ value: 'paragraph', label: 'Paragraphe' },
|
||||||
{ value: 'heading1', label: 'Titre 1' },
|
{ value: 'heading1', label: 'Titre 1' },
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import Table from '@/components/Table';
|
|||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
|
|
||||||
const paymentPlansOptions = [
|
const paymentPlansOptions = [
|
||||||
{ id: 1, name: '1 fois', frequency: 1 },
|
{ id: 1, name: '1 fois', frequency: 1 },
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import TreeView from '@/components/Structure/Competencies/TreeView';
|
|||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import { Award, CheckCircle } from 'lucide-react';
|
import { Award, CheckCircle } from 'lucide-react';
|
||||||
import SelectChoice from '@/components/Form/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import Button from '@/components/Form/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
|
import { Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import InputText from '@/components/Form/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react';
|
import { Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import InputText from '@/components/Form/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
|
|||||||
Reference in New Issue
Block a user