Replace ~2900 lines across 9 files with ~400 lines in 2 files: - shared/utils/customFields.ts (types + pure helpers) - composables/useCustomFieldInputs.ts (reactive composable) Migrated all consumers: - Backend: add defaultValue to API Platform serialization groups - Standalone pages: component edit/create, piece edit/create, product edit/create/detail - Machine page: MachineCustomFieldsCard, MachineInfoCard, useMachineDetailCustomFields - Hierarchy: ComponentItem, PieceItem - Shared: CustomFieldDisplay, CustomFieldInputGrid - Category editor: componentStructure.ts Deleted: - entityCustomFieldLogic.ts (335 lines) - customFieldUtils.ts (440 lines) - customFieldFormUtils.ts (404 lines) - useEntityCustomFields.ts (181 lines) - customFieldFormUtils.test.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
222 lines
8.9 KiB
Vue
222 lines
8.9 KiB
Vue
<template>
|
|
<div class="card bg-base-100 shadow-lg">
|
|
<div class="card-body space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="card-title">Champs personnalisés</h2>
|
|
<p class="text-xs text-gray-500">
|
|
Champs personnalisés propres à cette machine.
|
|
</p>
|
|
</div>
|
|
<span v-if="visibleCustomFields.length" class="badge badge-outline">
|
|
{{ visibleCustomFields.length }} champ{{ visibleCustomFields.length > 1 ? 's' : '' }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- View mode: display values -->
|
|
<template v-if="!isEditMode">
|
|
<div v-if="visibleCustomFields.length" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div
|
|
v-for="field in visibleCustomFields"
|
|
:key="field.customFieldValueId || field.id || field.name"
|
|
class="form-control"
|
|
>
|
|
<label class="label">
|
|
<span class="label-text text-sm">{{ field.name }}</span>
|
|
</label>
|
|
<div class="input input-bordered input-sm bg-base-200">
|
|
{{ formatValueForDisplay(field) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-xs text-gray-500">
|
|
Aucun champ personnalisé défini pour cette machine.
|
|
</p>
|
|
</template>
|
|
|
|
<!-- Edit mode: definition management + value editing -->
|
|
<template v-else>
|
|
<p v-if="!customFields.length" class="text-xs text-gray-500">
|
|
Aucun champ personnalisé défini.
|
|
</p>
|
|
|
|
<div v-else class="space-y-3">
|
|
<div
|
|
v-for="(field, index) in customFields"
|
|
:key="field.id || field.name || 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">
|
|
<!-- Definition fields -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
|
<input
|
|
:value="field.name"
|
|
type="text"
|
|
class="input input-bordered input-sm"
|
|
placeholder="Nom du champ"
|
|
@blur="handleDefinitionUpdate(field, 'name', ($event.target as HTMLInputElement).value)"
|
|
/>
|
|
<select
|
|
:value="field.type || 'text'"
|
|
class="select select-bordered select-sm"
|
|
@change="handleDefinitionUpdate(field, 'type', ($event.target as HTMLSelectElement).value)"
|
|
>
|
|
<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 class="flex items-center gap-2 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
class="checkbox checkbox-sm"
|
|
:checked="!!field.required"
|
|
@change="handleDefinitionUpdate(field, 'required', ($event.target as HTMLInputElement).checked)"
|
|
/>
|
|
Obligatoire
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Options for select type -->
|
|
<textarea
|
|
v-if="(field.type || 'text') === 'select'"
|
|
:value="field.optionsText || (Array.isArray(field.options) ? field.options.join('\n') : '')"
|
|
class="textarea textarea-bordered textarea-sm h-20 w-full"
|
|
placeholder="Option 1 Option 2"
|
|
@blur="handleOptionsUpdate(field, ($event.target as HTMLTextAreaElement).value)"
|
|
></textarea>
|
|
|
|
<!-- Value editing -->
|
|
<div class="pt-1 border-t border-base-200">
|
|
<label class="label py-0">
|
|
<span class="label-text text-xs text-base-content/60">Valeur</span>
|
|
</label>
|
|
<input
|
|
v-if="!field.type || field.type === 'text'"
|
|
:value="field.value ?? ''"
|
|
type="text"
|
|
class="input input-bordered input-sm w-full"
|
|
placeholder="Valeur..."
|
|
@input="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).value)"
|
|
@blur="$emit('update-custom-field', field)"
|
|
/>
|
|
<input
|
|
v-else-if="field.type === 'number'"
|
|
:value="field.value ?? ''"
|
|
type="number"
|
|
class="input input-bordered input-sm w-full"
|
|
placeholder="Valeur..."
|
|
@input="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).value)"
|
|
@blur="$emit('update-custom-field', field)"
|
|
/>
|
|
<select
|
|
v-else-if="field.type === 'select'"
|
|
:value="field.value ?? ''"
|
|
class="select select-bordered select-sm w-full"
|
|
@change="onSelectChange(field, ($event.target as HTMLSelectElement).value)"
|
|
>
|
|
<option value="">Sélectionner...</option>
|
|
<option
|
|
v-for="option in field.options"
|
|
:key="option"
|
|
:value="option"
|
|
>
|
|
{{ option }}
|
|
</option>
|
|
</select>
|
|
<label
|
|
v-else-if="field.type === 'boolean'"
|
|
class="flex items-center gap-3 cursor-pointer py-1"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
class="toggle toggle-primary toggle-sm"
|
|
:checked="String(field.value).toLowerCase() === 'true'"
|
|
@change="onBooleanChange(field, ($event.target as HTMLInputElement).checked)"
|
|
>
|
|
<span
|
|
class="text-sm"
|
|
:class="String(field.value).toLowerCase() === 'true' ? 'text-success font-medium' : 'text-base-content/60'"
|
|
>
|
|
{{ String(field.value).toLowerCase() === 'true' ? 'Oui' : 'Non' }}
|
|
</span>
|
|
</label>
|
|
<input
|
|
v-else-if="field.type === 'date'"
|
|
:value="field.value ?? ''"
|
|
type="date"
|
|
class="input input-bordered input-sm w-full"
|
|
@input="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).value)"
|
|
@blur="$emit('update-custom-field', field)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm btn-square flex-shrink-0 text-error"
|
|
title="Supprimer ce champ"
|
|
@click="$emit('delete-field', field.id || field.customFieldId)"
|
|
>
|
|
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm md:btn-md btn-primary"
|
|
@click="$emit('add-field')"
|
|
>
|
|
Ajouter un champ personnalisé
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import IconLucideTrash from '~icons/lucide/trash'
|
|
import { formatValueForDisplay } from '~/shared/utils/customFields'
|
|
|
|
defineProps<{
|
|
customFields: any[]
|
|
visibleCustomFields: any[]
|
|
isEditMode: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'set-custom-field-value': [field: any, value: unknown]
|
|
'update-custom-field': [field: any]
|
|
'add-field': []
|
|
'delete-field': [fieldId: string]
|
|
'update-field-definition': [fieldId: string, data: Record<string, unknown>]
|
|
}>()
|
|
|
|
const handleDefinitionUpdate = (field: any, key: string, value: unknown) => {
|
|
const fieldId = field.id || field.customFieldId
|
|
if (!fieldId) return
|
|
emit('update-field-definition', fieldId, { ...field, [key]: value })
|
|
}
|
|
|
|
const handleOptionsUpdate = (field: any, raw: string) => {
|
|
const fieldId = field.id || field.customFieldId
|
|
if (!fieldId) return
|
|
const options = raw.split('\n').map((o: string) => o.trim()).filter((o: string) => o.length > 0)
|
|
emit('update-field-definition', fieldId, { ...field, options })
|
|
}
|
|
|
|
const onSelectChange = (field: any, value: string) => {
|
|
emit('set-custom-field-value', field, value)
|
|
emit('update-custom-field', field)
|
|
}
|
|
|
|
const onBooleanChange = (field: any, checked: boolean) => {
|
|
emit('set-custom-field-value', field, checked ? 'true' : 'false')
|
|
emit('update-custom-field', field)
|
|
}
|
|
</script>
|