diff --git a/app/components/machine/MachineCustomFieldDefEditor.vue b/app/components/machine/MachineCustomFieldDefEditor.vue
new file mode 100644
index 0000000..98eccad
--- /dev/null
+++ b/app/components/machine/MachineCustomFieldDefEditor.vue
@@ -0,0 +1,124 @@
+
+
+
+
+ Définitions des champs personnalisés
+
+
+
+
+
+ Aucun champ personnalisé défini. Cliquez sur « Ajouter » pour en créer un.
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ Obligatoire
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/machine/MachineInfoCard.vue b/app/components/machine/MachineInfoCard.vue
index 8606bc8..d9fd55d 100644
--- a/app/components/machine/MachineInfoCard.vue
+++ b/app/components/machine/MachineInfoCard.vue
@@ -151,18 +151,36 @@
+
+
+
+
diff --git a/app/composables/useMachineCustomFieldDefs.ts b/app/composables/useMachineCustomFieldDefs.ts
new file mode 100644
index 0000000..a4e9a94
--- /dev/null
+++ b/app/composables/useMachineCustomFieldDefs.ts
@@ -0,0 +1,327 @@
+import { reactive, ref } from 'vue'
+import { useApi } from '~/composables/useApi'
+import { useToast } from '~/composables/useToast'
+
+// --- Types ---
+
+export type MachineFieldType = 'text' | 'number' | 'select' | 'boolean' | 'date'
+
+export interface MachineCustomFieldEditorField {
+ uid: string
+ serverId?: string
+ name: string
+ type: MachineFieldType
+ required: boolean
+ optionsText: string
+ orderIndex: number
+}
+
+interface InitialDef {
+ id: string
+ name: string
+ type: string
+ required: boolean
+ options?: string[]
+ orderIndex: number
+ defaultValue?: unknown
+}
+
+interface Deps {
+ machineId: string
+ initialDefs: InitialDef[]
+ onSaved: () => void | Promise
+}
+
+// --- Helpers ---
+
+let uidCounter = 0
+const createUid = (): string => {
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
+ return crypto.randomUUID()
+ }
+ uidCounter += 1
+ return `mcf-${Date.now().toString(36)}-${uidCounter}`
+}
+
+const normalizeLineEndings = (value: string): string =>
+ value.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
+
+const toEditorField = (def: InitialDef, index: number): MachineCustomFieldEditorField => ({
+ uid: createUid(),
+ serverId: def.id,
+ name: def.name || '',
+ type: (def.type || 'text') as MachineFieldType,
+ required: Boolean(def.required),
+ optionsText: normalizeLineEndings(
+ Array.isArray(def.options) ? def.options.join('\n') : '',
+ ),
+ orderIndex: typeof def.orderIndex === 'number' ? def.orderIndex : index,
+})
+
+const hydrateFields = (defs: InitialDef[]): MachineCustomFieldEditorField[] =>
+ defs
+ .map((def, index) => toEditorField(def, index))
+ .sort((a, b) => a.orderIndex - b.orderIndex)
+ .map((field, index) => ({ ...field, orderIndex: index }))
+
+const buildSnapshot = (defs: InitialDef[]): Map => {
+ const map = new Map()
+ for (const def of defs) {
+ map.set(def.id, def)
+ }
+ return map
+}
+
+const applyOrderIndex = (
+ list: MachineCustomFieldEditorField[],
+): MachineCustomFieldEditorField[] =>
+ list.map((field, index) => ({ ...field, orderIndex: index }))
+
+const parseOptions = (optionsText: string): string[] =>
+ normalizeLineEndings(optionsText)
+ .split('\n')
+ .map(o => o.trim())
+ .filter(o => o.length > 0)
+
+// --- Composable ---
+
+export function useMachineCustomFieldDefs(deps: Deps) {
+ const { apiCall } = useApi()
+ const { showSuccess, showError } = useToast()
+
+ // --- State ---
+
+ const fields = ref(hydrateFields(deps.initialDefs))
+ const initialSnapshot = ref