refactor(frontend) : extract CustomFieldDisplay shared component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 14:37:43 +01:00
parent b696b5aa1f
commit a3fde7a191
3 changed files with 189 additions and 229 deletions

View File

@@ -213,79 +213,12 @@
</div>
<!-- Custom Fields Display - Editable or Read-only -->
<div v-if="displayedCustomFields.length" class="mt-4 pt-4 border-t border-gray-200">
<h4 class="font-semibold text-sm text-gray-700 mb-3">
Champs personnalisés
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="(field, index) in displayedCustomFields"
:key="resolveFieldKey(field, index)"
class="form-control"
>
<label class="label">
<span class="label-text text-sm">{{ resolveFieldName(field) }}</span>
<span v-if="resolveFieldRequired(field)" class="label-text-alt text-error">*</span>
</label>
<template v-if="isEditMode && !resolveFieldReadOnly(field)">
<input
v-if="resolveFieldType(field) === 'text'"
v-model="field.value"
type="text"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@blur="updateComponentCustomField(field)"
>
<input
v-else-if="resolveFieldType(field) === 'number'"
v-model="field.value"
type="number"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@blur="updateComponentCustomField(field)"
>
<select
v-else-if="resolveFieldType(field) === 'select'"
v-model="field.value"
class="select select-bordered select-sm"
:required="resolveFieldRequired(field)"
@change="updateComponentCustomField(field)"
>
<option value="">
Sélectionner...
</option>
<option v-for="option in resolveFieldOptions(field)" :key="option" :value="option">
{{ option }}
</option>
</select>
<div v-else-if="resolveFieldType(field) === 'boolean'" class="flex items-center gap-2">
<input
v-model="field.value"
type="checkbox"
class="checkbox checkbox-sm"
true-value="true"
false-value="false"
@change="updateComponentCustomField(field)"
>
<span class="text-sm">{{ String(field.value).toLowerCase() === 'true' ? 'Oui' : 'Non' }}</span>
</div>
<input
v-else-if="resolveFieldType(field) === 'date'"
v-model="field.value"
type="date"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@blur="updateComponentCustomField(field)"
>
</template>
<template v-else>
<div class="input input-bordered input-sm bg-base-200">
{{ formatFieldDisplayValue(field) }}
</div>
</template>
</div>
</div>
</div>
<CommonCustomFieldDisplay
:fields="displayedCustomFields"
:is-edit-mode="isEditMode"
:columns="2"
@field-blur="updateComponentCustomField"
/>
<div class="mt-4 pt-4 border-t border-gray-200 space-y-3">
<div class="flex items-center justify-between">
@@ -381,15 +314,6 @@ import {
documentIcon,
downloadDocument,
} from '~/shared/utils/documentDisplayUtils'
import {
resolveFieldKey,
resolveFieldName,
resolveFieldType,
resolveFieldOptions,
resolveFieldRequired,
resolveFieldReadOnly,
formatFieldDisplayValue,
} from '~/shared/utils/entityCustomFieldLogic'
import { useEntityDocuments } from '~/composables/useEntityDocuments'
import { useEntityProductDisplay } from '~/composables/useEntityProductDisplay'
import { useEntityCustomFields } from '~/composables/useEntityCustomFields'

View File

@@ -234,143 +234,12 @@
</div>
<!-- Champs personnalisés de la pièce -->
<div
v-if="displayedCustomFields.length"
class="mt-4 pt-4 border-t border-gray-200"
>
<h5 class="text-sm font-medium text-gray-700 mb-3">
Champs personnalisés
</h5>
<div class="space-y-3">
<div
v-for="(field, index) in displayedCustomFields"
:key="resolveFieldKey(field, index)"
class="form-control"
>
<label class="label">
<span class="label-text text-sm">{{
resolveFieldName(field)
}}</span>
<span
v-if="resolveFieldRequired(field)"
class="label-text-alt text-error"
>*</span
>
</label>
<!-- Mode édition -->
<template v-if="isEditMode && !resolveFieldReadOnly(field)">
<!-- Champ de type TEXT -->
<input
v-if="resolveFieldType(field) === 'text'"
:value="field.value ?? ''"
type="text"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@input="
setCustomFieldValue(
resolveFieldId(field),
$event.target.value,
field
)
"
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
/>
<!-- Champ de type NUMBER -->
<input
v-else-if="resolveFieldType(field) === 'number'"
:value="field.value ?? ''"
type="number"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@input="
setCustomFieldValue(
resolveFieldId(field),
$event.target.value,
field
)
"
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
/>
<!-- Champ de type SELECT -->
<select
v-else-if="resolveFieldType(field) === 'select'"
:value="field.value ?? ''"
class="select select-bordered select-sm"
:required="resolveFieldRequired(field)"
@change="
(event) =>
setCustomFieldValue(
resolveFieldId(field),
event.target.value,
field
)
"
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
>
<option value="">Sélectionner...</option>
<option
v-for="option in resolveFieldOptions(field)"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
<!-- Champ de type BOOLEAN -->
<div
v-else-if="resolveFieldType(field) === 'boolean'"
class="flex items-center gap-2"
>
<input
:value="field.value ?? ''"
type="checkbox"
class="checkbox checkbox-sm"
:checked="String(field.value).toLowerCase() === 'true'"
@change="
setCustomFieldValue(
resolveFieldId(field),
$event.target.checked ? 'true' : 'false',
field
)
"
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
/>
<span class="text-sm">{{
String(field.value).toLowerCase() === "true" ? "Oui" : "Non"
}}</span>
</div>
<!-- Champ de type DATE -->
<input
v-else-if="resolveFieldType(field) === 'date'"
:value="field.value ?? ''"
type="date"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@input="
setCustomFieldValue(
resolveFieldId(field),
$event.target.value,
field
)
"
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
/>
</template>
<!-- Mode lecture seule -->
<template v-else>
<div class="input input-bordered input-sm bg-base-200">
{{ formatFieldDisplayValue(field) }}
</div>
</template>
</div>
</div>
</div>
<CommonCustomFieldDisplay
:fields="displayedCustomFields"
:is-edit-mode="isEditMode"
@field-input="handleCustomFieldInput"
@field-blur="handleCustomFieldBlur"
/>
<div class="mt-4 pt-4 border-t border-gray-200 space-y-3">
<div class="flex items-center justify-between">
@@ -434,14 +303,8 @@ import {
downloadDocument,
} from '~/shared/utils/documentDisplayUtils'
import {
resolveFieldKey,
resolveFieldId,
resolveFieldName,
resolveFieldType,
resolveFieldOptions,
resolveFieldRequired,
resolveFieldReadOnly,
formatFieldDisplayValue,
} from '~/shared/utils/entityCustomFieldLogic'
import { useEntityDocuments } from '~/composables/useEntityDocuments'
import { useEntityProductDisplay } from '~/composables/useEntityProductDisplay'
@@ -595,16 +458,16 @@ const handleProductChange = async (value) => {
updatePiece()
}
// --- Custom field local helpers ---
const setCustomFieldValue = (fieldValueId, value, field) => {
// --- Custom field event handlers ---
const handleCustomFieldInput = (field, value) => {
if (resolveFieldReadOnly(field)) return
if (field && typeof field === 'object') field.value = value
const fieldValueId = resolveFieldId(field)
if (!fieldValueId) return
const fieldValue = props.piece.customFieldValues?.find((fv) => fv.id === fieldValueId)
if (fieldValue) fieldValue.value = value
}
const updateCustomFieldValue = async (_fieldValueId, field) => {
const handleCustomFieldBlur = async (field) => {
await updateCustomField(field)
const cfId = field?.customFieldId || field?.customField?.id || null
if (cfId || field?.customFieldValueId) {

View File

@@ -0,0 +1,173 @@
<template>
<div
v-if="fields.length"
class="mt-4 pt-4 border-t border-gray-200"
>
<h5 class="text-sm font-medium text-gray-700 mb-3">
Champs personnalisés
</h5>
<div :class="layoutClass">
<div
v-for="(field, index) in fields"
:key="resolveFieldKey(field, index)"
class="form-control"
>
<label class="label">
<span class="label-text text-sm">{{
resolveFieldName(field)
}}</span>
<span
v-if="resolveFieldRequired(field)"
class="label-text-alt text-error"
>*</span>
</label>
<!-- Mode édition -->
<template v-if="isEditMode && !resolveFieldReadOnly(field)">
<!-- Champ de type TEXT -->
<input
v-if="resolveFieldType(field) === 'text'"
:value="field.value ?? ''"
type="text"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@input="onInput(field, ($event.target as HTMLInputElement).value)"
@blur="onBlur(field)"
>
<!-- Champ de type NUMBER -->
<input
v-else-if="resolveFieldType(field) === 'number'"
:value="field.value ?? ''"
type="number"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@input="onInput(field, ($event.target as HTMLInputElement).value)"
@blur="onBlur(field)"
>
<!-- Champ de type SELECT -->
<select
v-else-if="resolveFieldType(field) === 'select'"
:value="field.value ?? ''"
class="select select-bordered select-sm"
:required="resolveFieldRequired(field)"
@change="onInput(field, ($event.target as HTMLSelectElement).value)"
@blur="onBlur(field)"
>
<option value="">
Sélectionner...
</option>
<option
v-for="option in resolveFieldOptions(field)"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
<!-- Champ de type BOOLEAN -->
<div
v-else-if="resolveFieldType(field) === 'boolean'"
class="flex items-center gap-2"
>
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="String(field.value).toLowerCase() === 'true'"
@change="onBooleanChange(field, ($event.target as HTMLInputElement).checked)"
>
<span class="text-sm">{{
String(field.value).toLowerCase() === "true" ? "Oui" : "Non"
}}</span>
</div>
<!-- Champ de type DATE -->
<input
v-else-if="resolveFieldType(field) === 'date'"
:value="field.value ?? ''"
type="date"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@input="onInput(field, ($event.target as HTMLInputElement).value)"
@blur="onBlur(field)"
>
<!-- Champ de type TEXTAREA -->
<textarea
v-else-if="resolveFieldType(field) === 'textarea'"
:value="field.value ?? ''"
class="textarea textarea-bordered textarea-sm"
:required="resolveFieldRequired(field)"
@input="onInput(field, ($event.target as HTMLTextAreaElement).value)"
@blur="onBlur(field)"
/>
<!-- Fallback: input text -->
<input
v-else
:value="field.value ?? ''"
type="text"
class="input input-bordered input-sm"
:required="resolveFieldRequired(field)"
@input="onInput(field, ($event.target as HTMLInputElement).value)"
@blur="onBlur(field)"
>
</template>
<!-- Mode lecture seule -->
<template v-else>
<div class="input input-bordered input-sm bg-base-200">
{{ formatFieldDisplayValue(field) }}
</div>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
resolveFieldKey,
resolveFieldName,
resolveFieldType,
resolveFieldOptions,
resolveFieldRequired,
resolveFieldReadOnly,
formatFieldDisplayValue,
} from '~/shared/utils/entityCustomFieldLogic'
const props = defineProps<{
fields: any[]
isEditMode: boolean
columns?: 1 | 2
}>()
const emit = defineEmits<{
'field-input': [field: any, value: string]
'field-blur': [field: any]
}>()
const layoutClass = computed(() =>
props.columns === 2
? 'grid grid-cols-1 md:grid-cols-2 gap-4'
: 'space-y-3',
)
function onInput(field: any, value: string) {
field.value = value
emit('field-input', field, value)
}
function onBooleanChange(field: any, checked: boolean) {
const value = checked ? 'true' : 'false'
field.value = value
emit('field-input', field, value)
emit('field-blur', field)
}
function onBlur(field: any) {
emit('field-blur', field)
}
</script>