Files
Inventory_frontend/app/components/PieceModelStructureEditor.vue
2025-09-26 11:29:47 +02:00

209 lines
5.8 KiB
Vue

<template>
<div class="space-y-4">
<section class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold">
Champs personnalisés
</h3>
<button type="button" class="btn btn-outline btn-xs" @click="addField">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<p v-if="!localFields.length" class="text-xs text-gray-500">
Aucun champ personnalisé n'a encore été défini.
</p>
<div v-else class="space-y-2">
<div
v-for="(field, index) in localFields"
:key="`custom-field-${index}`"
class="border border-base-200 rounded-md p-3 space-y-2"
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<input
v-model="field.name"
type="text"
class="input input-bordered input-xs"
placeholder="Nom du champ"
>
<select v-model="field.type" class="select select-bordered select-xs">
<option value="text">
Texte
</option>
<option value="number">
Nombre
</option>
<option value="select">
Liste
</option>
<option value="boolean">
Oui/Non
</option>
<option value="date">
Date
</option>
</select>
</div>
<div class="flex items-center gap-2 text-xs">
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs">
Obligatoire
</div>
<textarea
v-if="field.type === 'select'"
v-model="field.optionsText"
class="textarea textarea-bordered textarea-xs h-20"
placeholder="Option 1&#10;Option 2"
/>
</div>
<button
type="button"
class="btn btn-error btn-xs btn-square"
@click="removeField(index)"
>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
import { computed, reactive, watch } from 'vue'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideTrash from '~icons/lucide/trash'
const props = defineProps({
modelValue: {
type: Object,
default: () => ({ customFields: [] })
}
})
const emit = defineEmits(['update:modelValue'])
const ensureArray = value => (Array.isArray(value) ? value : [])
const clone = (input, fallback = {}) => {
try {
return JSON.parse(JSON.stringify(input ?? fallback))
} catch (error) {
return JSON.parse(JSON.stringify(fallback))
}
}
const extractRest = (structure = {}) => {
if (!structure || typeof structure !== 'object') {
return {}
}
return Object.fromEntries(
Object.entries(structure).filter(([key]) => key !== 'customFields')
)
}
const toEditorField = (input = {}) => ({
name: typeof input.name === 'string' ? input.name : '',
type: typeof input.type === 'string' && input.type ? input.type : 'text',
required: Boolean(input.required),
optionsText: Array.isArray(input.options)
? input.options.join('\n')
: typeof input.optionsText === 'string'
? input.optionsText
: ''
})
const hydrateFields = (structure = {}) => ensureArray(structure.customFields).map(toEditorField)
const localState = reactive({
fields: hydrateFields(props.modelValue)
})
const extraState = reactive({
rest: clone(extractRest(props.modelValue))
})
const localFields = computed({
get: () => localState.fields,
set: (value) => {
localState.fields = ensureArray(value).map(toEditorField)
}
})
const normalizeFields = (fields) => {
return ensureArray(fields)
.map((field) => {
const name = typeof field.name === 'string' ? field.name.trim() : ''
if (!name) {
return null
}
const type = field.type || 'text'
const required = Boolean(field.required)
let options
if (type === 'select') {
const raw = typeof field.optionsText === 'string' ? field.optionsText : ''
const parsed = raw
.split(/\r?\n/)
.map(option => option.trim())
.filter(option => option.length > 0)
options = parsed.length > 0 ? parsed : undefined
}
const normalized = { name, type, required }
if (options) {
normalized.options = options
}
return normalized
})
.filter(Boolean)
}
let lastEmitted = JSON.stringify({
...clone(extraState.rest, {}),
customFields: normalizeFields(props.modelValue?.customFields)
})
const emitUpdate = () => {
const customFields = normalizeFields(localFields.value)
const payload = {
...clone(extraState.rest, {}),
customFields
}
const serialized = JSON.stringify(payload)
if (serialized !== lastEmitted) {
lastEmitted = serialized
emit('update:modelValue', payload)
}
}
watch(
() => props.modelValue,
(value) => {
localFields.value = hydrateFields(value)
extraState.rest = clone(extractRest(value), {})
lastEmitted = JSON.stringify({
...clone(extraState.rest, {}),
customFields: normalizeFields(value?.customFields)
})
},
{ deep: true }
)
watch(localFields, emitUpdate, { deep: true })
const addField = () => {
localFields.value = [...localFields.value, toEditorField()]
}
const removeField = (index) => {
localFields.value = localFields.value.filter((_, i) => i !== index)
}
</script>