refactor(components): extract shared entity utilities and simplify item components (F1.3, F1.4)
Extract 3 entity composables (useEntityCustomFields, useEntityDocuments, useEntityProductDisplay) and entityCustomFieldLogic utility shared across ComponentItem (1336→585 LOC) and PieceItem (1588→740 LOC). Improve type safety in edit/create pages with explicit casts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -401,7 +401,7 @@
|
|||||||
:key="piece.id"
|
:key="piece.id"
|
||||||
:piece="piece"
|
:piece="piece"
|
||||||
:is-edit-mode="isEditMode && !piece.skeletonOnly"
|
:is-edit-mode="isEditMode && !piece.skeletonOnly"
|
||||||
|
|
||||||
@update="updatePiece"
|
@update="updatePiece"
|
||||||
@edit="editPiece"
|
@edit="editPiece"
|
||||||
@custom-field-update="updatePieceCustomField"
|
@custom-field-update="updatePieceCustomField"
|
||||||
@@ -437,200 +437,98 @@ import { ref, watch, computed } from 'vue'
|
|||||||
import PieceItem from './PieceItem.vue'
|
import PieceItem from './PieceItem.vue'
|
||||||
import DocumentUpload from './DocumentUpload.vue'
|
import DocumentUpload from './DocumentUpload.vue'
|
||||||
import ConstructeurSelect from './ConstructeurSelect.vue'
|
import ConstructeurSelect from './ConstructeurSelect.vue'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
|
||||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
|
||||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||||
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
||||||
import { useCustomFields } from '~/composables/useCustomFields'
|
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
|
||||||
import { useToast } from '~/composables/useToast'
|
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import {
|
import {
|
||||||
formatConstructeurContact as formatConstructeurContactSummary,
|
formatConstructeurContact as formatConstructeurContactSummary,
|
||||||
resolveConstructeurs,
|
resolveConstructeurs,
|
||||||
uniqueConstructeurIds,
|
uniqueConstructeurIds,
|
||||||
} from '~/shared/constructeurUtils'
|
} from '~/shared/constructeurUtils'
|
||||||
|
import {
|
||||||
|
formatSize,
|
||||||
|
shouldInlinePdf,
|
||||||
|
documentPreviewSrc,
|
||||||
|
documentThumbnailClass,
|
||||||
|
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'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
component: { type: Object, required: true },
|
||||||
type: Object,
|
isEditMode: { type: Boolean, default: false },
|
||||||
required: true
|
collapseAll: { type: Boolean, default: true },
|
||||||
},
|
toggleToken: { type: Number, default: 0 },
|
||||||
isEditMode: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
collapseAll: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
toggleToken: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits(['update', 'edit-piece', 'custom-field-update'])
|
||||||
'update',
|
|
||||||
'edit-piece',
|
|
||||||
'custom-field-update'
|
|
||||||
])
|
|
||||||
|
|
||||||
|
// --- Shared composables ---
|
||||||
|
const {
|
||||||
|
documents: componentDocuments,
|
||||||
|
selectedFiles,
|
||||||
|
uploadingDocuments,
|
||||||
|
loadingDocuments,
|
||||||
|
previewDocument,
|
||||||
|
previewVisible,
|
||||||
|
openPreview,
|
||||||
|
closePreview,
|
||||||
|
ensureDocumentsLoaded,
|
||||||
|
handleFilesAdded,
|
||||||
|
removeDocument,
|
||||||
|
} = useEntityDocuments({ entity: () => props.component, entityType: 'composant' })
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayProduct,
|
||||||
|
displayProductName,
|
||||||
|
productInfoRows,
|
||||||
|
productDocuments,
|
||||||
|
} = useEntityProductDisplay({ entity: () => props.component })
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayedCustomFields,
|
||||||
|
updateCustomField: updateComponentCustomField,
|
||||||
|
} = useEntityCustomFields({ entity: () => props.component, entityType: 'composant' })
|
||||||
|
|
||||||
|
// --- Collapse state ---
|
||||||
const isCollapsed = ref(true)
|
const isCollapsed = ref(true)
|
||||||
const selectedFiles = ref([])
|
|
||||||
const uploadingDocuments = ref(false)
|
watch(
|
||||||
const loadingDocuments = ref(false)
|
() => props.toggleToken,
|
||||||
const documentsLoaded = ref(!!(props.component.documents && props.component.documents.length))
|
() => {
|
||||||
const componentDocuments = computed(() => props.component.documents || [])
|
isCollapsed.value = props.collapseAll
|
||||||
const documentIcon = doc => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
if (!isCollapsed.value) ensureDocumentsLoaded()
|
||||||
const previewDocument = ref(null)
|
},
|
||||||
const previewVisible = ref(false)
|
{ immediate: true },
|
||||||
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
)
|
||||||
const shouldInlinePdf = (document) => {
|
|
||||||
if (!document || !isPdfDocument(document) || !document.path) {
|
const toggleCollapse = () => {
|
||||||
return false
|
isCollapsed.value = !isCollapsed.value
|
||||||
}
|
if (!isCollapsed.value) ensureDocumentsLoaded()
|
||||||
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const appendPdfViewerParams = (src) => {
|
|
||||||
if (!src || src.startsWith('data:')) {
|
|
||||||
return src || ''
|
|
||||||
}
|
|
||||||
if (src.includes('#')) {
|
|
||||||
return `${src}&toolbar=0&navpanes=0`
|
|
||||||
}
|
|
||||||
return `${src}#toolbar=0&navpanes=0`
|
|
||||||
}
|
|
||||||
const documentPreviewSrc = (document) => {
|
|
||||||
if (!document?.path) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
if (isPdfDocument(document)) {
|
|
||||||
return appendPdfViewerParams(document.path)
|
|
||||||
}
|
|
||||||
return document.path
|
|
||||||
}
|
|
||||||
const documentThumbnailClass = (document) => {
|
|
||||||
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
|
||||||
return 'h-24 w-20'
|
|
||||||
}
|
|
||||||
return 'h-16 w-16'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Child components ---
|
||||||
const childComponents = computed(() => {
|
const childComponents = computed(() => {
|
||||||
const list = props.component.subcomponents || props.component.subComponents || []
|
const list = props.component.subcomponents || props.component.subComponents || []
|
||||||
return Array.isArray(list) ? list : []
|
return Array.isArray(list) ? list : []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Constructeurs ---
|
||||||
const { constructeurs } = useConstructeurs()
|
const { constructeurs } = useConstructeurs()
|
||||||
|
|
||||||
const buildProductDisplay = (product) => {
|
|
||||||
if (!product || typeof product !== 'object') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const suppliers = Array.isArray(product.constructeurs)
|
|
||||||
? product.constructeurs
|
|
||||||
.map((constructeur) => constructeur?.name)
|
|
||||||
.filter((name) => typeof name === 'string' && name.trim().length > 0)
|
|
||||||
.join(', ')
|
|
||||||
: product.supplierLabel || null
|
|
||||||
|
|
||||||
const priceValue =
|
|
||||||
product.supplierPrice ??
|
|
||||||
product.price ??
|
|
||||||
product.priceLabel ??
|
|
||||||
product.priceDisplay ??
|
|
||||||
null
|
|
||||||
|
|
||||||
let price = null
|
|
||||||
if (priceValue !== null && priceValue !== undefined) {
|
|
||||||
const parsed = Number(priceValue)
|
|
||||||
if (!Number.isNaN(parsed)) {
|
|
||||||
price = currencyFormatter.format(parsed)
|
|
||||||
} else if (typeof priceValue === 'string' && priceValue.trim().length > 0) {
|
|
||||||
price = priceValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name:
|
|
||||||
product.name ||
|
|
||||||
product.label ||
|
|
||||||
product.reference ||
|
|
||||||
product.productName ||
|
|
||||||
null,
|
|
||||||
reference: product.reference || null,
|
|
||||||
category: product.typeProduct?.name || product.category || null,
|
|
||||||
suppliers,
|
|
||||||
price,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayProduct = computed(() => {
|
|
||||||
const explicit = props.component.product || null
|
|
||||||
const normalized = buildProductDisplay(explicit)
|
|
||||||
if (normalized) {
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
const fallback = props.component.__productDisplay
|
|
||||||
if (fallback) {
|
|
||||||
return {
|
|
||||||
name: fallback.name || null,
|
|
||||||
reference: fallback.reference || null,
|
|
||||||
category: fallback.category || null,
|
|
||||||
suppliers: fallback.suppliers || null,
|
|
||||||
price: fallback.price || null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayProductName = computed(() => {
|
|
||||||
if (displayProduct.value?.name) {
|
|
||||||
return displayProduct.value.name
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
props.component.product?.name ||
|
|
||||||
props.component.productName ||
|
|
||||||
props.component.productLabel ||
|
|
||||||
null
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayProductCategory = computed(() => displayProduct.value?.category || null)
|
|
||||||
const displayProductReference = computed(() => displayProduct.value?.reference || null)
|
|
||||||
const displayProductSuppliers = computed(() => displayProduct.value?.suppliers || null)
|
|
||||||
const displayProductPrice = computed(() => displayProduct.value?.price || null)
|
|
||||||
|
|
||||||
const productInfoRows = computed(() => {
|
|
||||||
if (!displayProduct.value) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const rows = []
|
|
||||||
if (displayProductReference.value) {
|
|
||||||
rows.push({ label: 'Référence', value: displayProductReference.value })
|
|
||||||
}
|
|
||||||
if (displayProductPrice.value) {
|
|
||||||
rows.push({ label: 'Prix indicatif', value: displayProductPrice.value })
|
|
||||||
}
|
|
||||||
if (displayProductSuppliers.value) {
|
|
||||||
rows.push({ label: 'Fournisseur(s)', value: displayProductSuppliers.value })
|
|
||||||
}
|
|
||||||
if (displayProductCategory.value) {
|
|
||||||
rows.push({ label: 'Catégorie', value: displayProductCategory.value })
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
})
|
|
||||||
|
|
||||||
const productDocuments = computed(() => {
|
|
||||||
const product = props.component.product
|
|
||||||
return Array.isArray(product?.documents) ? product.documents : []
|
|
||||||
})
|
|
||||||
|
|
||||||
const componentConstructeurIds = computed(() =>
|
const componentConstructeurIds = computed(() =>
|
||||||
uniqueConstructeurIds(
|
uniqueConstructeurIds(
|
||||||
props.component,
|
props.component,
|
||||||
@@ -651,332 +549,8 @@ const componentConstructeursDisplay = computed(() =>
|
|||||||
const formatConstructeurContact = (constructeur) =>
|
const formatConstructeurContact = (constructeur) =>
|
||||||
formatConstructeurContactSummary(constructeur)
|
formatConstructeurContactSummary(constructeur)
|
||||||
|
|
||||||
const extractStructureCustomFields = (structure) => {
|
|
||||||
if (!structure || typeof structure !== 'object') {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const customFields = structure.customFields
|
|
||||||
return Array.isArray(customFields) ? customFields : []
|
|
||||||
}
|
|
||||||
|
|
||||||
function fieldKeyFromNameAndType(name, type) {
|
|
||||||
const normalizedName =
|
|
||||||
typeof name === 'string' ? name.trim().toLowerCase() : ''
|
|
||||||
const normalizedType =
|
|
||||||
typeof type === 'string' ? type.trim().toLowerCase() : ''
|
|
||||||
return normalizedName ? `${normalizedName}::${normalizedType}` : null
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveOrderIndex(field) {
|
|
||||||
if (!field || typeof field !== 'object') {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (typeof field.orderIndex === 'number') {
|
|
||||||
return field.orderIndex
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
field.customField &&
|
|
||||||
typeof field.customField.orderIndex === 'number'
|
|
||||||
) {
|
|
||||||
return field.customField.orderIndex
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function deduplicateFieldDefinitions(definitions) {
|
|
||||||
const result = []
|
|
||||||
const seen = new Set()
|
|
||||||
|
|
||||||
const orderedDefinitions = (Array.isArray(definitions)
|
|
||||||
? definitions.slice()
|
|
||||||
: []
|
|
||||||
).sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
|
||||||
|
|
||||||
orderedDefinitions.forEach((field) => {
|
|
||||||
if (!field || typeof field !== 'object') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const id =
|
|
||||||
field.id ??
|
|
||||||
field.customFieldId ??
|
|
||||||
field.customField?.id ??
|
|
||||||
null
|
|
||||||
const nameKey = fieldKeyFromNameAndType(field.name, field.type)
|
|
||||||
if (!id && !nameKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const key = id || nameKey
|
|
||||||
if (key && seen.has(key)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (key) {
|
|
||||||
seen.add(key)
|
|
||||||
}
|
|
||||||
field.orderIndex = resolveOrderIndex(field)
|
|
||||||
result.push(field)
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeFieldDefinitionsWithValues(definitions, values) {
|
|
||||||
const definitionList = Array.isArray(definitions) ? definitions : []
|
|
||||||
const valueList = Array.isArray(values) ? values : []
|
|
||||||
|
|
||||||
const valueMap = new Map()
|
|
||||||
valueList.forEach((entry) => {
|
|
||||||
if (!entry || typeof entry !== 'object') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const fieldId = entry.customField?.id ?? entry.customFieldId ?? null
|
|
||||||
if (fieldId) {
|
|
||||||
valueMap.set(fieldId, entry)
|
|
||||||
}
|
|
||||||
const nameKey = fieldKeyFromNameAndType(entry.customField?.name, entry.customField?.type)
|
|
||||||
if (nameKey) {
|
|
||||||
valueMap.set(nameKey, entry)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const merged = definitionList.map((field) => {
|
|
||||||
if (!field || typeof field !== 'object') {
|
|
||||||
return field
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldId = ensureCustomFieldId(field)
|
|
||||||
const nameKey = fieldKeyFromNameAndType(resolveFieldName(field), resolveFieldType(field))
|
|
||||||
|
|
||||||
const matchedValue =
|
|
||||||
(fieldId ? valueMap.get(fieldId) : undefined) ??
|
|
||||||
(nameKey ? valueMap.get(nameKey) : undefined)
|
|
||||||
|
|
||||||
if (!matchedValue) {
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
value: field?.value ?? '',
|
|
||||||
orderIndex: resolveOrderIndex(field),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedOrder = Math.min(
|
|
||||||
resolveOrderIndex(field),
|
|
||||||
resolveOrderIndex(matchedValue.customField),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
customFieldValueId: matchedValue.id ?? field.customFieldValueId ?? null,
|
|
||||||
customFieldId:
|
|
||||||
matchedValue.customField?.id ??
|
|
||||||
matchedValue.customFieldId ??
|
|
||||||
fieldId ??
|
|
||||||
null,
|
|
||||||
customField: matchedValue.customField ?? field.customField ?? null,
|
|
||||||
value: matchedValue.value ?? field.value ?? '',
|
|
||||||
orderIndex: resolvedOrder,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
valueList.forEach((entry) => {
|
|
||||||
if (!entry || typeof entry !== 'object') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldId = entry.customField?.id ?? entry.customFieldId ?? null
|
|
||||||
const nameKey = fieldKeyFromNameAndType(entry.customField?.name, entry.customField?.type)
|
|
||||||
|
|
||||||
const exists = merged.some((field) => {
|
|
||||||
if (!field || typeof field !== 'object') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (field.customFieldValueId && field.customFieldValueId === entry.id) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const existingId = ensureCustomFieldId(field)
|
|
||||||
if (fieldId && existingId && existingId === fieldId) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (!fieldId && nameKey) {
|
|
||||||
return (
|
|
||||||
fieldKeyFromNameAndType(resolveFieldName(field), resolveFieldType(field)) === nameKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
merged.push({
|
|
||||||
customFieldValueId: entry.id ?? null,
|
|
||||||
customFieldId: fieldId,
|
|
||||||
name: entry.customField?.name ?? '',
|
|
||||||
type: entry.customField?.type ?? 'text',
|
|
||||||
required: entry.customField?.required ?? false,
|
|
||||||
options: entry.customField?.options ?? [],
|
|
||||||
value: entry.value ?? '',
|
|
||||||
customField: entry.customField ?? null,
|
|
||||||
orderIndex: resolveOrderIndex(entry.customField),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return merged.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
function dedupeMergedFields(fields) {
|
|
||||||
if (!Array.isArray(fields) || fields.length <= 1) {
|
|
||||||
return Array.isArray(fields)
|
|
||||||
? fields.slice().sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Map()
|
|
||||||
const result = []
|
|
||||||
|
|
||||||
const orderedFields = fields
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
|
||||||
|
|
||||||
orderedFields.forEach((field) => {
|
|
||||||
if (!field || typeof field !== 'object') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawName = resolveFieldName(field)
|
|
||||||
const normalizedName = typeof rawName === 'string' ? rawName.trim() : ''
|
|
||||||
if (!normalizedName) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
field.name = normalizedName
|
|
||||||
field.type = resolveFieldType(field)
|
|
||||||
|
|
||||||
const fieldId = ensureCustomFieldId(field)
|
|
||||||
const nameKey = fieldKeyFromNameAndType(normalizedName, field.type)
|
|
||||||
const key = fieldId || nameKey
|
|
||||||
|
|
||||||
if (!key) {
|
|
||||||
field.orderIndex = resolveOrderIndex(field)
|
|
||||||
result.push(field)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = seen.get(key)
|
|
||||||
if (!existing) {
|
|
||||||
field.orderIndex = resolveOrderIndex(field)
|
|
||||||
seen.set(key, field)
|
|
||||||
result.push(field)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingHasValue =
|
|
||||||
existing.value !== undefined &&
|
|
||||||
existing.value !== null &&
|
|
||||||
String(existing.value).trim().length > 0
|
|
||||||
|
|
||||||
const incomingHasValue =
|
|
||||||
field.value !== undefined &&
|
|
||||||
field.value !== null &&
|
|
||||||
String(field.value).trim().length > 0
|
|
||||||
|
|
||||||
if (!existingHasValue && incomingHasValue) {
|
|
||||||
Object.assign(existing, field)
|
|
||||||
existing.orderIndex = Math.min(
|
|
||||||
resolveOrderIndex(existing),
|
|
||||||
resolveOrderIndex(field),
|
|
||||||
)
|
|
||||||
seen.set(key, existing)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return result.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentDefinitionSources = computed(() => {
|
|
||||||
const requirement = props.component.typeMachineComponentRequirement || {}
|
|
||||||
const type = requirement.typeComposant || props.component.typeComposant || {}
|
|
||||||
|
|
||||||
const definitions = []
|
|
||||||
const pushFields = (collection) => {
|
|
||||||
if (Array.isArray(collection)) {
|
|
||||||
definitions.push(...collection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pushFields(props.component.customFields)
|
|
||||||
pushFields(props.component.definition?.customFields)
|
|
||||||
pushFields(type.customFields)
|
|
||||||
pushFields(requirement.customFields)
|
|
||||||
pushFields(requirement.definition?.customFields)
|
|
||||||
|
|
||||||
;[
|
|
||||||
props.component.definition?.structure,
|
|
||||||
type.structure,
|
|
||||||
type.componentSkeleton,
|
|
||||||
requirement.structure,
|
|
||||||
requirement.componentSkeleton,
|
|
||||||
].forEach((structure) => {
|
|
||||||
const fields = extractStructureCustomFields(structure)
|
|
||||||
if (fields.length) {
|
|
||||||
definitions.push(...fields)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return deduplicateFieldDefinitions(definitions)
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayedCustomFields = computed(() =>
|
|
||||||
dedupeMergedFields(
|
|
||||||
mergeFieldDefinitionsWithValues(
|
|
||||||
componentDefinitionSources.value,
|
|
||||||
props.component.customFieldValues,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const candidateCustomFields = computed(() => {
|
|
||||||
const map = new Map()
|
|
||||||
const register = (collection) => {
|
|
||||||
if (!Array.isArray(collection)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
collection.forEach((item) => {
|
|
||||||
if (!item || typeof item !== 'object') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const id = item.id || item.customFieldId
|
|
||||||
const name = typeof item.name === 'string' ? item.name : null
|
|
||||||
const key = id || (name ? `${name}::${item.type ?? ''}` : null)
|
|
||||||
if (!key || map.has(key)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
map.set(key, item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
register(props.component.customFieldValues?.map((value) => value?.customField))
|
|
||||||
register(componentDefinitionSources.value)
|
|
||||||
|
|
||||||
return Array.from(map.values())
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
candidateCustomFields,
|
|
||||||
() => {
|
|
||||||
displayedCustomFields.value.forEach((field) => ensureCustomFieldId(field))
|
|
||||||
},
|
|
||||||
{ immediate: true, deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
displayedCustomFields,
|
|
||||||
(fields) => {
|
|
||||||
(fields || []).forEach((field) => ensureCustomFieldId(field))
|
|
||||||
},
|
|
||||||
{ immediate: true, deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleConstructeurChange = async (value) => {
|
const handleConstructeurChange = async (value) => {
|
||||||
const ids = uniqueConstructeurIds(value)
|
const ids = uniqueConstructeurIds(value)
|
||||||
|
|
||||||
props.component.constructeurIds = [...ids]
|
props.component.constructeurIds = [...ids]
|
||||||
props.component.constructeurId = null
|
props.component.constructeurId = null
|
||||||
props.component.constructeur = null
|
props.component.constructeur = null
|
||||||
@@ -985,42 +559,10 @@ const handleConstructeurChange = async (value) => {
|
|||||||
constructeurs.value,
|
constructeurs.value,
|
||||||
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
||||||
)
|
)
|
||||||
|
|
||||||
await updateComponent()
|
await updateComponent()
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uploadDocuments, deleteDocument, loadDocumentsByComponent } = useDocuments()
|
// --- Update / Event forwarding ---
|
||||||
const {
|
|
||||||
updateCustomFieldValue: updateComponentCustomFieldValueApi,
|
|
||||||
upsertCustomFieldValue: upsertComponentCustomFieldValue,
|
|
||||||
} = useCustomFields()
|
|
||||||
const { showSuccess, showError } = useToast()
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.toggleToken,
|
|
||||||
() => {
|
|
||||||
isCollapsed.value = props.collapseAll
|
|
||||||
if (!isCollapsed.value) {
|
|
||||||
ensureDocumentsLoaded()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.component.documents,
|
|
||||||
(docs) => {
|
|
||||||
documentsLoaded.value = !!(docs && docs.length)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const toggleCollapse = () => {
|
|
||||||
isCollapsed.value = !isCollapsed.value
|
|
||||||
if (!isCollapsed.value) {
|
|
||||||
ensureDocumentsLoaded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateComponent = () => {
|
const updateComponent = () => {
|
||||||
emit('update', {
|
emit('update', {
|
||||||
...props.component,
|
...props.component,
|
||||||
@@ -1028,216 +570,6 @@ const updateComponent = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveFieldKey(field, index) {
|
|
||||||
return field?.id
|
|
||||||
?? field?.customFieldValueId
|
|
||||||
?? field?.customFieldId
|
|
||||||
?? field?.name
|
|
||||||
?? `field-${index}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFieldId(field) {
|
|
||||||
return field?.customFieldValueId ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFieldName(field) {
|
|
||||||
return field?.name ?? 'Champ'
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFieldType(field) {
|
|
||||||
return field?.type ?? 'text'
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFieldOptions(field) {
|
|
||||||
return field?.options ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFieldRequired(field) {
|
|
||||||
return !!field?.required
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFieldReadOnly(field) {
|
|
||||||
return !!field?.readOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCustomFieldMetadata(field) {
|
|
||||||
return {
|
|
||||||
customFieldName: resolveFieldName(field),
|
|
||||||
customFieldType: resolveFieldType(field),
|
|
||||||
customFieldRequired: resolveFieldRequired(field),
|
|
||||||
customFieldOptions: resolveFieldOptions(field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveCustomFieldId(field) {
|
|
||||||
return field?.customFieldId ?? field?.id ?? field?.customField?.id ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureCustomFieldId(field) {
|
|
||||||
const existingId = resolveCustomFieldId(field)
|
|
||||||
if (existingId) {
|
|
||||||
return existingId
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = resolveFieldName(field)
|
|
||||||
if (!name || name === 'Champ') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const matches = candidateCustomFields.value.filter((candidate) => {
|
|
||||||
if (!candidate || typeof candidate !== 'object') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const candidateId = candidate.id || candidate.customFieldId
|
|
||||||
if (candidateId && (candidateId === field?.id || candidateId === field?.customFieldId)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return typeof candidate.name === 'string' && candidate.name === name
|
|
||||||
})
|
|
||||||
|
|
||||||
if (matches.length) {
|
|
||||||
const withId = matches.find((candidate) => candidate?.id || candidate?.customFieldId) || matches[0]
|
|
||||||
const id = withId?.id || withId?.customFieldId || null
|
|
||||||
if (id) {
|
|
||||||
field.customFieldId = id
|
|
||||||
}
|
|
||||||
if (!field.customField && typeof withId === 'object') {
|
|
||||||
field.customField = withId
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
candidateCustomFields,
|
|
||||||
() => {
|
|
||||||
displayedCustomFields.value.forEach((field) => ensureCustomFieldId(field))
|
|
||||||
},
|
|
||||||
{ immediate: true, deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
displayedCustomFields,
|
|
||||||
(fields) => {
|
|
||||||
(fields || []).forEach((field) => ensureCustomFieldId(field))
|
|
||||||
},
|
|
||||||
{ immediate: true, deep: true }
|
|
||||||
)
|
|
||||||
const formatFieldDisplayValue = (field) => {
|
|
||||||
const type = resolveFieldType(field)
|
|
||||||
const rawValue = field?.value ?? ''
|
|
||||||
if (type === 'boolean') {
|
|
||||||
const normalized = String(rawValue).toLowerCase()
|
|
||||||
if (normalized === 'true') return 'Oui'
|
|
||||||
if (normalized === 'false') return 'Non'
|
|
||||||
}
|
|
||||||
return rawValue || 'Non défini'
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateComponentCustomField = async (field) => {
|
|
||||||
if (!field || resolveFieldReadOnly(field)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldValueId = resolveFieldId(field)
|
|
||||||
if (fieldValueId) {
|
|
||||||
const result = await updateComponentCustomFieldValueApi(fieldValueId, { value: field.value ?? '' })
|
|
||||||
if (result.success) {
|
|
||||||
const existingValue = props.component.customFieldValues?.find((value) => value.id === fieldValueId)
|
|
||||||
if (existingValue?.customField?.id) {
|
|
||||||
field.customFieldId = existingValue.customField.id
|
|
||||||
field.customField = existingValue.customField
|
|
||||||
}
|
|
||||||
showSuccess(`Champ "${resolveFieldName(field)}" mis à jour avec succès`)
|
|
||||||
} else {
|
|
||||||
showError(`Erreur lors de la mise à jour du champ "${resolveFieldName(field)}"`)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const customFieldId = ensureCustomFieldId(field)
|
|
||||||
const fieldName = resolveFieldName(field)
|
|
||||||
if (!props.component?.id) {
|
|
||||||
showError('Impossible de créer la valeur pour ce champ de composant')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!customFieldId && (!fieldName || fieldName === 'Champ')) {
|
|
||||||
showError('Impossible de créer la valeur pour ce champ de composant')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = customFieldId ? undefined : buildCustomFieldMetadata(field)
|
|
||||||
|
|
||||||
const result = await upsertComponentCustomFieldValue(
|
|
||||||
customFieldId,
|
|
||||||
'composant',
|
|
||||||
props.component.id,
|
|
||||||
field.value ?? '',
|
|
||||||
metadata,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
const newValue = result.data
|
|
||||||
if (newValue?.id) {
|
|
||||||
field.customFieldValueId = newValue.id
|
|
||||||
field.value = newValue.value ?? field.value ?? ''
|
|
||||||
if (newValue.customField?.id) {
|
|
||||||
field.customFieldId = newValue.customField.id
|
|
||||||
field.customField = newValue.customField
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(props.component.customFieldValues)) {
|
|
||||||
const index = props.component.customFieldValues.findIndex((value) => value.id === newValue.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
props.component.customFieldValues.splice(index, 1, newValue)
|
|
||||||
} else {
|
|
||||||
props.component.customFieldValues.push(newValue)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
props.component.customFieldValues = [newValue]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showSuccess(`Champ "${resolveFieldName(field)}" créé avec succès`)
|
|
||||||
|
|
||||||
const definitions = Array.isArray(props.component.customFields)
|
|
||||||
? [...props.component.customFields]
|
|
||||||
: []
|
|
||||||
const fieldIdentifier = ensureCustomFieldId(field)
|
|
||||||
const existingIndex = definitions.findIndex((definition) => {
|
|
||||||
const definitionId = ensureCustomFieldId(definition)
|
|
||||||
if (fieldIdentifier && definitionId) {
|
|
||||||
return definitionId === fieldIdentifier
|
|
||||||
}
|
|
||||||
return definition?.name === resolveFieldName(field)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updatedDefinition = {
|
|
||||||
...(existingIndex !== -1 ? definitions[existingIndex] : {}),
|
|
||||||
customFieldValueId: field.customFieldValueId,
|
|
||||||
customFieldId: fieldIdentifier,
|
|
||||||
name: resolveFieldName(field),
|
|
||||||
type: resolveFieldType(field),
|
|
||||||
required: resolveFieldRequired(field),
|
|
||||||
options: resolveFieldOptions(field),
|
|
||||||
value: field.value ?? '',
|
|
||||||
customField: field.customField ?? null,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
definitions.splice(existingIndex, 1, updatedDefinition)
|
|
||||||
} else {
|
|
||||||
definitions.push(updatedDefinition)
|
|
||||||
}
|
|
||||||
|
|
||||||
props.component.customFields = definitions
|
|
||||||
} else {
|
|
||||||
showError(`Erreur lors de la sauvegarde du champ "${resolveFieldName(field)}"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePiece = (updatedPiece) => {
|
const updatePiece = (updatedPiece) => {
|
||||||
emit('edit-piece', updatedPiece)
|
emit('edit-piece', updatedPiece)
|
||||||
}
|
}
|
||||||
@@ -1250,87 +582,4 @@ const updatePieceCustomField = (fieldUpdate) => {
|
|||||||
emit('custom-field-update', fieldUpdate)
|
emit('custom-field-update', fieldUpdate)
|
||||||
emit('edit-piece', { ...fieldUpdate, type: 'custom-field-update' })
|
emit('edit-piece', { ...fieldUpdate, type: 'custom-field-update' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureDocumentsLoaded = async () => {
|
|
||||||
if (documentsLoaded.value || !props.component?.id) { return }
|
|
||||||
await refreshDocuments()
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshDocuments = async () => {
|
|
||||||
loadingDocuments.value = true
|
|
||||||
try {
|
|
||||||
const result = await loadDocumentsByComponent(props.component.id, { updateStore: false })
|
|
||||||
if (result.success) {
|
|
||||||
props.component.documents = result.data || []
|
|
||||||
documentsLoaded.value = true
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loadingDocuments.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFilesAdded = async (files) => {
|
|
||||||
if (!files.length || !props.component?.id) { return }
|
|
||||||
uploadingDocuments.value = true
|
|
||||||
try {
|
|
||||||
const result = await uploadDocuments(
|
|
||||||
{
|
|
||||||
files,
|
|
||||||
context: { composantId: props.component.id }
|
|
||||||
},
|
|
||||||
{ updateStore: false }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
const newDocs = result.data || []
|
|
||||||
props.component.documents = [...newDocs, ...(props.component.documents || [])]
|
|
||||||
documentsLoaded.value = true
|
|
||||||
selectedFiles.value = []
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
uploadingDocuments.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeDocument = async (documentId) => {
|
|
||||||
if (!documentId) { return }
|
|
||||||
const result = await deleteDocument(documentId, { updateStore: false })
|
|
||||||
if (result.success) {
|
|
||||||
props.component.documents = (props.component.documents || []).filter(doc => doc.id !== documentId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadDocument = (doc) => {
|
|
||||||
if (!doc?.path) { return }
|
|
||||||
|
|
||||||
if (doc.path.startsWith('data:')) {
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = doc.path
|
|
||||||
link.download = doc.filename || doc.name || 'document'
|
|
||||||
link.click()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open(doc.path, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
const openPreview = (doc) => {
|
|
||||||
if (!canPreviewDocument(doc)) { return }
|
|
||||||
previewDocument.value = doc
|
|
||||||
previewVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const closePreview = () => {
|
|
||||||
previewVisible.value = false
|
|
||||||
previewDocument.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatSize = (size) => {
|
|
||||||
if (size === undefined || size === null) { return '—' }
|
|
||||||
if (size === 0) { return '0 B' }
|
|
||||||
const units = ['B', 'KB', 'MB', 'GB']
|
|
||||||
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
|
|
||||||
const formatted = size / Math.pow(1024, index)
|
|
||||||
return `${formatted.toFixed(1)} ${units[index]}`
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -511,6 +511,10 @@ const reorderFields = (from: number, to: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [moved] = list.splice(from, 1)
|
const [moved] = list.splice(from, 1)
|
||||||
|
if (!moved) {
|
||||||
|
resetDragState()
|
||||||
|
return
|
||||||
|
}
|
||||||
list.splice(to, 0, moved)
|
list.splice(to, 0, moved)
|
||||||
fields.value = applyOrderIndex(list)
|
fields.value = applyOrderIndex(list)
|
||||||
resetDragState()
|
resetDragState()
|
||||||
|
|||||||
181
app/composables/useEntityCustomFields.ts
Normal file
181
app/composables/useEntityCustomFields.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* Reactive custom field management for entity items (ComponentItem, PieceItem).
|
||||||
|
*
|
||||||
|
* Wraps the pure logic from entityCustomFieldLogic.ts with Vue reactivity,
|
||||||
|
* watchers, and API calls for updating/upserting custom field values.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
import { useCustomFields } from '~/composables/useCustomFields'
|
||||||
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import {
|
||||||
|
buildDefinitionSources,
|
||||||
|
buildCandidateCustomFields,
|
||||||
|
mergeFieldDefinitionsWithValues,
|
||||||
|
dedupeMergedFields,
|
||||||
|
ensureCustomFieldId,
|
||||||
|
resolveFieldId,
|
||||||
|
resolveFieldName,
|
||||||
|
resolveFieldType,
|
||||||
|
resolveFieldReadOnly,
|
||||||
|
resolveCustomFieldId,
|
||||||
|
buildCustomFieldMetadata,
|
||||||
|
} from '~/shared/utils/entityCustomFieldLogic'
|
||||||
|
|
||||||
|
export interface EntityCustomFieldsDeps {
|
||||||
|
entity: () => any
|
||||||
|
entityType: 'composant' | 'piece'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEntityCustomFields(deps: EntityCustomFieldsDeps) {
|
||||||
|
const { entity, entityType } = deps
|
||||||
|
const {
|
||||||
|
updateCustomFieldValue: updateCustomFieldValueApi,
|
||||||
|
upsertCustomFieldValue,
|
||||||
|
} = useCustomFields()
|
||||||
|
const { showSuccess, showError } = useToast()
|
||||||
|
|
||||||
|
const definitionSources = computed(() =>
|
||||||
|
buildDefinitionSources(entity(), entityType),
|
||||||
|
)
|
||||||
|
|
||||||
|
const displayedCustomFields = computed(() =>
|
||||||
|
dedupeMergedFields(
|
||||||
|
mergeFieldDefinitionsWithValues(
|
||||||
|
definitionSources.value,
|
||||||
|
entity().customFieldValues,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const candidateCustomFields = computed(() =>
|
||||||
|
buildCandidateCustomFields(entity(), definitionSources.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watchers to ensure field IDs are resolved
|
||||||
|
watch(
|
||||||
|
candidateCustomFields,
|
||||||
|
() => {
|
||||||
|
const candidates = candidateCustomFields.value
|
||||||
|
;(displayedCustomFields.value || []).forEach((field: any) => {
|
||||||
|
if (field) ensureCustomFieldId(field, candidates)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
displayedCustomFields,
|
||||||
|
(fields) => {
|
||||||
|
const candidates = candidateCustomFields.value
|
||||||
|
;(fields || []).forEach((field: any) => {
|
||||||
|
if (field) ensureCustomFieldId(field, candidates)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateCustomField = async (field: any) => {
|
||||||
|
if (!field || resolveFieldReadOnly(field)) return
|
||||||
|
|
||||||
|
const e = entity()
|
||||||
|
const fieldValueId = resolveFieldId(field)
|
||||||
|
|
||||||
|
// Update existing field value
|
||||||
|
if (fieldValueId) {
|
||||||
|
const result: any = await updateCustomFieldValueApi(fieldValueId, { value: field.value ?? '' })
|
||||||
|
if (result.success) {
|
||||||
|
const existingValue = e.customFieldValues?.find((v: any) => v.id === fieldValueId)
|
||||||
|
if (existingValue?.customField?.id) {
|
||||||
|
field.customFieldId = existingValue.customField.id
|
||||||
|
field.customField = existingValue.customField
|
||||||
|
}
|
||||||
|
showSuccess(`Champ "${resolveFieldName(field)}" mis à jour avec succès`)
|
||||||
|
} else {
|
||||||
|
showError(`Erreur lors de la mise à jour du champ "${resolveFieldName(field)}"`)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new field value
|
||||||
|
const customFieldId = ensureCustomFieldId(field, candidateCustomFields.value)
|
||||||
|
const fieldName = resolveFieldName(field)
|
||||||
|
if (!e?.id) {
|
||||||
|
showError(`Impossible de créer la valeur pour ce champ`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!customFieldId && (!fieldName || fieldName === 'Champ')) {
|
||||||
|
showError(`Impossible de créer la valeur pour ce champ`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = customFieldId ? undefined : buildCustomFieldMetadata(field)
|
||||||
|
const result: any = await upsertCustomFieldValue(
|
||||||
|
customFieldId,
|
||||||
|
entityType,
|
||||||
|
e.id,
|
||||||
|
field.value ?? '',
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const newValue = result.data
|
||||||
|
if (newValue?.id) {
|
||||||
|
field.customFieldValueId = newValue.id
|
||||||
|
field.value = newValue.value ?? field.value ?? ''
|
||||||
|
if (newValue.customField?.id) {
|
||||||
|
field.customFieldId = newValue.customField.id
|
||||||
|
field.customField = newValue.customField
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(e.customFieldValues)) {
|
||||||
|
const index = e.customFieldValues.findIndex((v: any) => v.id === newValue.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
e.customFieldValues.splice(index, 1, newValue)
|
||||||
|
} else {
|
||||||
|
e.customFieldValues.push(newValue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e.customFieldValues = [newValue]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showSuccess(`Champ "${resolveFieldName(field)}" créé avec succès`)
|
||||||
|
|
||||||
|
// Update definitions list
|
||||||
|
const definitions = Array.isArray(e.customFields) ? [...e.customFields] : []
|
||||||
|
const fieldIdentifier = ensureCustomFieldId(field, candidateCustomFields.value)
|
||||||
|
const existingIndex = definitions.findIndex((definition: any) => {
|
||||||
|
const definitionId = resolveCustomFieldId(definition)
|
||||||
|
if (fieldIdentifier && definitionId) return definitionId === fieldIdentifier
|
||||||
|
return definition?.name === resolveFieldName(field)
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedDefinition = {
|
||||||
|
...(existingIndex !== -1 ? definitions[existingIndex] : {}),
|
||||||
|
customFieldValueId: field.customFieldValueId,
|
||||||
|
customFieldId: fieldIdentifier,
|
||||||
|
name: resolveFieldName(field),
|
||||||
|
type: resolveFieldType(field),
|
||||||
|
required: field.required ?? false,
|
||||||
|
options: field.options ?? [],
|
||||||
|
value: field.value ?? '',
|
||||||
|
customField: field.customField ?? null,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
definitions.splice(existingIndex, 1, updatedDefinition)
|
||||||
|
} else {
|
||||||
|
definitions.push(updatedDefinition)
|
||||||
|
}
|
||||||
|
e.customFields = definitions
|
||||||
|
} else {
|
||||||
|
showError(`Erreur lors de la sauvegarde du champ "${resolveFieldName(field)}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayedCustomFields,
|
||||||
|
candidateCustomFields,
|
||||||
|
updateCustomField,
|
||||||
|
}
|
||||||
|
}
|
||||||
122
app/composables/useEntityDocuments.ts
Normal file
122
app/composables/useEntityDocuments.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Reactive document management for entity items (ComponentItem, PieceItem).
|
||||||
|
*
|
||||||
|
* Handles document CRUD operations, preview modal state, and lazy loading.
|
||||||
|
* Display helpers (formatSize, shouldInlinePdf, etc.) are imported from
|
||||||
|
* shared/utils/documentDisplayUtils.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
|
|
||||||
|
export interface EntityDocumentsDeps {
|
||||||
|
entity: () => any
|
||||||
|
entityType: 'composant' | 'piece'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEntityDocuments(deps: EntityDocumentsDeps) {
|
||||||
|
const { entity, entityType } = deps
|
||||||
|
const { uploadDocuments, deleteDocument } = useDocuments()
|
||||||
|
|
||||||
|
const loadDocumentsFn = entityType === 'composant'
|
||||||
|
? useDocuments().loadDocumentsByComponent
|
||||||
|
: useDocuments().loadDocumentsByPiece
|
||||||
|
|
||||||
|
const selectedFiles = ref<File[]>([])
|
||||||
|
const uploadingDocuments = ref(false)
|
||||||
|
const loadingDocuments = ref(false)
|
||||||
|
const documentsLoaded = ref(!!(entity().documents && entity().documents.length))
|
||||||
|
|
||||||
|
const documents = computed(() => entity().documents || [])
|
||||||
|
|
||||||
|
// Preview modal state
|
||||||
|
const previewDocument = ref<any>(null)
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
|
||||||
|
const openPreview = (doc: any) => {
|
||||||
|
if (!canPreviewDocument(doc)) return
|
||||||
|
previewDocument.value = doc
|
||||||
|
previewVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePreview = () => {
|
||||||
|
previewVisible.value = false
|
||||||
|
previewDocument.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document watchers
|
||||||
|
watch(
|
||||||
|
() => entity().documents,
|
||||||
|
(docs: any) => {
|
||||||
|
documentsLoaded.value = !!(docs && docs.length)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// CRUD operations
|
||||||
|
const refreshDocuments = async () => {
|
||||||
|
const e = entity()
|
||||||
|
if (!e?.id) return
|
||||||
|
loadingDocuments.value = true
|
||||||
|
try {
|
||||||
|
const result: any = await loadDocumentsFn(e.id, { updateStore: false })
|
||||||
|
if (result.success) {
|
||||||
|
e.documents = result.data || []
|
||||||
|
documentsLoaded.value = true
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loadingDocuments.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureDocumentsLoaded = async () => {
|
||||||
|
if (documentsLoaded.value || !entity()?.id) return
|
||||||
|
await refreshDocuments()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilesAdded = async (files: File[]) => {
|
||||||
|
const e = entity()
|
||||||
|
if (!files.length || !e?.id) return
|
||||||
|
uploadingDocuments.value = true
|
||||||
|
try {
|
||||||
|
const contextKey = entityType === 'composant' ? 'composantId' : 'pieceId'
|
||||||
|
const result: any = await uploadDocuments(
|
||||||
|
{ files, context: { [contextKey]: e.id } } as any,
|
||||||
|
{ updateStore: false } as any,
|
||||||
|
)
|
||||||
|
if (result.success) {
|
||||||
|
const newDocs = result.data || []
|
||||||
|
e.documents = [...newDocs, ...(e.documents || [])]
|
||||||
|
documentsLoaded.value = true
|
||||||
|
selectedFiles.value = []
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
uploadingDocuments.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeDocument = async (documentId: string) => {
|
||||||
|
if (!documentId) return
|
||||||
|
const result: any = await deleteDocument(documentId, { updateStore: false } as any)
|
||||||
|
if (result.success) {
|
||||||
|
const e = entity()
|
||||||
|
e.documents = (e.documents || []).filter((doc: any) => doc.id !== documentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
documents,
|
||||||
|
selectedFiles,
|
||||||
|
uploadingDocuments,
|
||||||
|
loadingDocuments,
|
||||||
|
documentsLoaded,
|
||||||
|
previewDocument,
|
||||||
|
previewVisible,
|
||||||
|
openPreview,
|
||||||
|
closePreview,
|
||||||
|
refreshDocuments,
|
||||||
|
ensureDocumentsLoaded,
|
||||||
|
handleFilesAdded,
|
||||||
|
removeDocument,
|
||||||
|
}
|
||||||
|
}
|
||||||
103
app/composables/useEntityProductDisplay.ts
Normal file
103
app/composables/useEntityProductDisplay.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Reactive product display for entity items (ComponentItem, PieceItem).
|
||||||
|
*
|
||||||
|
* Resolves product information from entity.product, entity.__productDisplay,
|
||||||
|
* or a selectedProduct ref, and produces display-ready computed properties.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, type Ref } from 'vue'
|
||||||
|
|
||||||
|
const currencyFormatter = new Intl.NumberFormat('fr-FR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR',
|
||||||
|
currencyDisplay: 'narrowSymbol',
|
||||||
|
})
|
||||||
|
|
||||||
|
function buildProductDisplay(product: any) {
|
||||||
|
if (!product || typeof product !== 'object') return null
|
||||||
|
|
||||||
|
const suppliers = Array.isArray(product.constructeurs)
|
||||||
|
? product.constructeurs
|
||||||
|
.map((c: any) => c?.name)
|
||||||
|
.filter((name: any) => typeof name === 'string' && name.trim().length > 0)
|
||||||
|
.join(', ')
|
||||||
|
: product.supplierLabel || null
|
||||||
|
|
||||||
|
const priceValue = product.supplierPrice ?? product.price ?? product.priceLabel ?? product.priceDisplay ?? null
|
||||||
|
let price: string | null = null
|
||||||
|
if (priceValue !== null && priceValue !== undefined) {
|
||||||
|
const parsed = Number(priceValue)
|
||||||
|
if (!Number.isNaN(parsed)) {
|
||||||
|
price = currencyFormatter.format(parsed)
|
||||||
|
} else if (typeof priceValue === 'string' && priceValue.trim().length > 0) {
|
||||||
|
price = priceValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: product.name || product.label || product.reference || product.productName || null,
|
||||||
|
reference: product.reference || null,
|
||||||
|
category: product.typeProduct?.name || product.category || null,
|
||||||
|
suppliers,
|
||||||
|
price,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityProductDisplayDeps {
|
||||||
|
entity: () => any
|
||||||
|
selectedProduct?: Ref<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEntityProductDisplay(deps: EntityProductDisplayDeps) {
|
||||||
|
const { entity, selectedProduct } = deps
|
||||||
|
|
||||||
|
const displayProduct = computed(() => {
|
||||||
|
// Priority: selectedProduct (for PieceItem) → entity.product → entity.__productDisplay
|
||||||
|
if (selectedProduct?.value) {
|
||||||
|
const normalized = buildProductDisplay(selectedProduct.value)
|
||||||
|
if (normalized) return normalized
|
||||||
|
}
|
||||||
|
const explicit = entity().product || null
|
||||||
|
const normalized = buildProductDisplay(explicit)
|
||||||
|
if (normalized) return normalized
|
||||||
|
const fallback = entity().__productDisplay
|
||||||
|
if (fallback) {
|
||||||
|
return {
|
||||||
|
name: fallback.name || null,
|
||||||
|
reference: fallback.reference || null,
|
||||||
|
category: fallback.category || null,
|
||||||
|
suppliers: fallback.suppliers || null,
|
||||||
|
price: fallback.price || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayProductName = computed(() => {
|
||||||
|
if (displayProduct.value?.name) return displayProduct.value.name
|
||||||
|
const e = entity()
|
||||||
|
return e.product?.name || e.productName || e.productLabel || null
|
||||||
|
})
|
||||||
|
|
||||||
|
const productInfoRows = computed(() => {
|
||||||
|
if (!displayProduct.value) return []
|
||||||
|
const rows: { label: string; value: string }[] = []
|
||||||
|
if (displayProduct.value.reference) rows.push({ label: 'Référence', value: displayProduct.value.reference })
|
||||||
|
if (displayProduct.value.price) rows.push({ label: 'Prix indicatif', value: displayProduct.value.price })
|
||||||
|
if (displayProduct.value.suppliers) rows.push({ label: 'Fournisseur(s)', value: displayProduct.value.suppliers })
|
||||||
|
if (displayProduct.value.category) rows.push({ label: 'Catégorie', value: displayProduct.value.category })
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
|
const productDocuments = computed(() => {
|
||||||
|
const product = selectedProduct?.value || entity().product || null
|
||||||
|
return Array.isArray(product?.documents) ? product.documents : []
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayProduct,
|
||||||
|
displayProductName,
|
||||||
|
productInfoRows,
|
||||||
|
productDocuments,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useHead, useRoute, useRouter } from '#imports'
|
import { useHead, useRoute, useRouter } from '#imports'
|
||||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||||
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
||||||
|
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
||||||
import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard'
|
import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ const loadCategory = async () => {
|
|||||||
code: response.code,
|
code: response.code,
|
||||||
category: response.category,
|
category: response.category,
|
||||||
notes: response.notes ?? response.description ?? '',
|
notes: response.notes ?? response.description ?? '',
|
||||||
structure: response.structure ?? undefined,
|
structure: (response.structure as ComponentModelStructure | null) ?? undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadLinkedCount(id)
|
await loadLinkedCount(id)
|
||||||
|
|||||||
@@ -558,7 +558,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
historyActionLabel,
|
historyActionLabel,
|
||||||
formatHistoryDate,
|
formatHistoryDate,
|
||||||
formatHistoryValue,
|
|
||||||
historyDiffEntries as _historyDiffEntries,
|
historyDiffEntries as _historyDiffEntries,
|
||||||
} from '~/shared/utils/historyDisplayUtils'
|
} from '~/shared/utils/historyDisplayUtils'
|
||||||
|
|
||||||
@@ -709,7 +708,7 @@ const refreshDocuments = async () => {
|
|||||||
try {
|
try {
|
||||||
const result = await loadDocumentsByComponent(component.value.id, { updateStore: false })
|
const result = await loadDocumentsByComponent(component.value.id, { updateStore: false })
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
componentDocuments.value = result.data || []
|
componentDocuments.value = Array.isArray(result.data) ? result.data : result.data ? [result.data] : []
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loadingDocuments.value = false
|
loadingDocuments.value = false
|
||||||
@@ -851,8 +850,8 @@ const submitEdition = async () => {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
const result = await updateComposant(component.value.id, payload)
|
const result = await updateComposant(component.value.id, payload)
|
||||||
if (result.success) {
|
if (result.success && result.data) {
|
||||||
const updatedComponent = result.data
|
const updatedComponent = result.data as Record<string, any>
|
||||||
await _saveCustomFieldValues(
|
await _saveCustomFieldValues(
|
||||||
'composant',
|
'composant',
|
||||||
updatedComponent.id,
|
updatedComponent.id,
|
||||||
@@ -925,13 +924,14 @@ const fetchPieceTypeNames = async (ids: string[]) => {
|
|||||||
)
|
)
|
||||||
const next = { ...fetchedPieceTypeMap.value }
|
const next = { ...fetchedPieceTypeMap.value }
|
||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
if (result.status !== 'fulfilled') {
|
const key = missing[index]
|
||||||
|
if (!key || result.status !== 'fulfilled') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data = result.value?.data
|
const data = result.value?.data
|
||||||
const name = data?.name || data?.code
|
const name = data?.name || data?.code
|
||||||
if (name) {
|
if (name) {
|
||||||
next[missing[index]] = name
|
next[key] = name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
fetchedPieceTypeMap.value = next
|
fetchedPieceTypeMap.value = next
|
||||||
@@ -968,13 +968,14 @@ const fetchProductTypeNames = async (ids: string[]) => {
|
|||||||
)
|
)
|
||||||
const next = { ...fetchedProductTypeMap.value }
|
const next = { ...fetchedProductTypeMap.value }
|
||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
if (result.status !== 'fulfilled') {
|
const key = missing[index]
|
||||||
|
if (!key || result.status !== 'fulfilled') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data = result.value?.data
|
const data = result.value?.data
|
||||||
const name = data?.name || data?.code
|
const name = data?.name || data?.code
|
||||||
if (name) {
|
if (name) {
|
||||||
next[missing[index]] = name
|
next[key] = name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
fetchedProductTypeMap.value = next
|
fetchedProductTypeMap.value = next
|
||||||
|
|||||||
@@ -813,13 +813,14 @@ const fetchPieceTypeNames = async (ids: string[]) => {
|
|||||||
)
|
)
|
||||||
const next = { ...fetchedPieceTypeMap.value }
|
const next = { ...fetchedPieceTypeMap.value }
|
||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
if (result.status !== 'fulfilled') {
|
const key = missing[index]
|
||||||
|
if (!key || result.status !== 'fulfilled') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data = result.value?.data
|
const data = result.value?.data
|
||||||
const name = data?.name || data?.code
|
const name = data?.name || data?.code
|
||||||
if (name) {
|
if (name) {
|
||||||
next[missing[index]] = name
|
next[key] = name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
fetchedPieceTypeMap.value = next
|
fetchedPieceTypeMap.value = next
|
||||||
@@ -1144,7 +1145,7 @@ const resolveOptions = (field: any): string[] => {
|
|||||||
return option.trim()
|
return option.trim()
|
||||||
}
|
}
|
||||||
if (typeof option === 'object') {
|
if (typeof option === 'object') {
|
||||||
const record = option || {}
|
const record = option as Record<string, unknown>
|
||||||
const keys = ['value', 'label', 'name']
|
const keys = ['value', 'label', 'name']
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const candidate = record[key]
|
const candidate = record[key]
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useHead, useRoute, useRouter } from '#imports'
|
import { useHead, useRoute, useRouter } from '#imports'
|
||||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||||
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
||||||
|
import type { PieceModelStructure } from '~/shared/types/inventory'
|
||||||
import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard'
|
import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ const loadCategory = async () => {
|
|||||||
code: response.code,
|
code: response.code,
|
||||||
category: response.category,
|
category: response.category,
|
||||||
notes: response.notes ?? response.description ?? '',
|
notes: response.notes ?? response.description ?? '',
|
||||||
structure: response.structure ?? undefined,
|
structure: (response.structure as PieceModelStructure | null) ?? undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadLinkedCount(id)
|
await loadLinkedCount(id)
|
||||||
|
|||||||
@@ -503,7 +503,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
historyActionLabel,
|
historyActionLabel,
|
||||||
formatHistoryDate,
|
formatHistoryDate,
|
||||||
formatHistoryValue,
|
|
||||||
historyDiffEntries as _historyDiffEntries,
|
historyDiffEntries as _historyDiffEntries,
|
||||||
} from '~/shared/utils/historyDisplayUtils'
|
} from '~/shared/utils/historyDisplayUtils'
|
||||||
|
|
||||||
@@ -626,7 +625,7 @@ const refreshDocuments = async () => {
|
|||||||
try {
|
try {
|
||||||
const result = await loadDocumentsByPiece(piece.value.id, { updateStore: false })
|
const result = await loadDocumentsByPiece(piece.value.id, { updateStore: false })
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
pieceDocuments.value = result.data || []
|
pieceDocuments.value = Array.isArray(result.data) ? result.data : result.data ? [result.data] : []
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loadingDocuments.value = false
|
loadingDocuments.value = false
|
||||||
@@ -776,9 +775,9 @@ const loadPieceTypeDetails = async (currentPiece: any) => {
|
|||||||
const type = await getModelType(typeId)
|
const type = await getModelType(typeId)
|
||||||
if (type && typeof type === 'object') {
|
if (type && typeof type === 'object') {
|
||||||
pieceTypeDetails.value = type
|
pieceTypeDetails.value = type
|
||||||
refreshCustomFieldInputs(type.structure ?? null, currentPiece?.customFieldValues ?? null)
|
refreshCustomFieldInputs((type.structure as PieceModelStructure | null) ?? null, currentPiece?.customFieldValues ?? null)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
pieceTypeDetails.value = null
|
pieceTypeDetails.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -898,8 +897,8 @@ const submitEdition = async () => {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
const result = await updatePiece(piece.value.id, payload)
|
const result = await updatePiece(piece.value.id, payload)
|
||||||
if (result.success) {
|
if (result.success && result.data) {
|
||||||
const updatedPiece = result.data
|
const updatedPiece = result.data as Record<string, any>
|
||||||
await _saveCustomFieldValues(
|
await _saveCustomFieldValues(
|
||||||
'piece',
|
'piece',
|
||||||
updatedPiece.id,
|
updatedPiece.id,
|
||||||
|
|||||||
@@ -543,22 +543,23 @@ const submitCreation = async () => {
|
|||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
const result = await createPiece(payload)
|
const result = await createPiece(payload)
|
||||||
if (result.success) {
|
if (result.success && result.data) {
|
||||||
|
const createdPiece = result.data as Record<string, any>
|
||||||
await _saveCustomFieldValues(
|
await _saveCustomFieldValues(
|
||||||
'piece',
|
'piece',
|
||||||
result.data.id,
|
createdPiece.id,
|
||||||
[
|
[
|
||||||
result.data?.typePiece?.pieceCustomFields,
|
createdPiece?.typePiece?.pieceCustomFields,
|
||||||
result.data?.typeMachinePieceRequirement?.typePiece?.pieceCustomFields,
|
createdPiece?.typeMachinePieceRequirement?.typePiece?.pieceCustomFields,
|
||||||
],
|
],
|
||||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||||
)
|
)
|
||||||
if (selectedDocuments.value.length && result.data?.id) {
|
if (selectedDocuments.value.length && createdPiece.id) {
|
||||||
uploadingDocuments.value = true
|
uploadingDocuments.value = true
|
||||||
const uploadResult = await uploadDocuments(
|
const uploadResult = await uploadDocuments(
|
||||||
{
|
{
|
||||||
files: selectedDocuments.value,
|
files: selectedDocuments.value,
|
||||||
context: { pieceId: result.data.id },
|
context: { pieceId: createdPiece.id },
|
||||||
},
|
},
|
||||||
{ updateStore: false },
|
{ updateStore: false },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useHead, useRoute, useRouter } from '#imports'
|
import { useHead, useRoute, useRouter } from '#imports'
|
||||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||||
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
||||||
|
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||||
import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard'
|
import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ const loadCategory = async () => {
|
|||||||
code: response.code,
|
code: response.code,
|
||||||
category: response.category,
|
category: response.category,
|
||||||
notes: response.notes ?? response.description ?? '',
|
notes: response.notes ?? response.description ?? '',
|
||||||
structure: response.structure ?? undefined,
|
structure: (response.structure as ProductModelStructure | null) ?? undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadLinkedCount(id)
|
await loadLinkedCount(id)
|
||||||
|
|||||||
@@ -421,7 +421,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
historyActionLabel,
|
historyActionLabel,
|
||||||
formatHistoryDate,
|
formatHistoryDate,
|
||||||
formatHistoryValue,
|
|
||||||
historyDiffEntries as _historyDiffEntries,
|
historyDiffEntries as _historyDiffEntries,
|
||||||
} from '~/shared/utils/historyDisplayUtils'
|
} from '~/shared/utils/historyDisplayUtils'
|
||||||
|
|
||||||
@@ -518,9 +517,9 @@ const loadProduct = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = await getProduct(id)
|
const result = await getProduct(id)
|
||||||
if (result.success) {
|
if (result.success && result.data) {
|
||||||
product.value = result.data
|
product.value = result.data
|
||||||
productDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
productDocuments.value = Array.isArray(result.data.documents) ? result.data.documents : []
|
||||||
await loadProductType()
|
await loadProductType()
|
||||||
const customValues = await getCustomFieldValuesByEntity('product', result.data.id)
|
const customValues = await getCustomFieldValuesByEntity('product', result.data.id)
|
||||||
if (customValues.success && Array.isArray(customValues.data)) {
|
if (customValues.success && Array.isArray(customValues.data)) {
|
||||||
|
|||||||
349
app/shared/utils/entityCustomFieldLogic.ts
Normal file
349
app/shared/utils/entityCustomFieldLogic.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
/**
|
||||||
|
* Pure functions for custom field resolution, merging, and deduplication.
|
||||||
|
*
|
||||||
|
* Extracted from ComponentItem.vue and PieceItem.vue which had ~350 LOC
|
||||||
|
* of identical custom field logic duplicated between them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Field key / identity helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function fieldKeyFromNameAndType(name: unknown, type: unknown): string | null {
|
||||||
|
const normalizedName = typeof name === 'string' ? name.trim().toLowerCase() : ''
|
||||||
|
const normalizedType = typeof type === 'string' ? type.trim().toLowerCase() : ''
|
||||||
|
return normalizedName ? `${normalizedName}::${normalizedType}` : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveOrderIndex(field: any): number {
|
||||||
|
if (!field || typeof field !== 'object') return 0
|
||||||
|
if (typeof field.orderIndex === 'number') return field.orderIndex
|
||||||
|
if (field.customField && typeof field.customField.orderIndex === 'number') return field.customField.orderIndex
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Field accessors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function resolveFieldKey(field: any, index: number): string {
|
||||||
|
return field?.id ?? field?.customFieldValueId ?? field?.customFieldId ?? field?.name ?? `field-${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFieldId(field: any): string | null {
|
||||||
|
return field?.customFieldValueId ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFieldName(field: any): string {
|
||||||
|
return field?.name ?? 'Champ'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFieldType(field: any): string {
|
||||||
|
return field?.type ?? 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFieldOptions(field: any): string[] {
|
||||||
|
return field?.options ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFieldRequired(field: any): boolean {
|
||||||
|
return !!field?.required
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFieldReadOnly(field: any): boolean {
|
||||||
|
return !!field?.readOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCustomFieldId(field: any): string | null {
|
||||||
|
return field?.customFieldId ?? field?.id ?? field?.customField?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCustomFieldMetadata(field: any) {
|
||||||
|
return {
|
||||||
|
customFieldName: resolveFieldName(field),
|
||||||
|
customFieldType: resolveFieldType(field),
|
||||||
|
customFieldRequired: resolveFieldRequired(field),
|
||||||
|
customFieldOptions: resolveFieldOptions(field),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFieldDisplayValue(field: any): string {
|
||||||
|
const type = resolveFieldType(field)
|
||||||
|
const rawValue = field?.value ?? ''
|
||||||
|
if (type === 'boolean') {
|
||||||
|
const normalized = String(rawValue).toLowerCase()
|
||||||
|
if (normalized === 'true') return 'Oui'
|
||||||
|
if (normalized === 'false') return 'Non'
|
||||||
|
}
|
||||||
|
return rawValue || 'Non défini'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Custom field ID resolution against candidate pool
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function ensureCustomFieldId(field: any, candidateFields: any[]): string | null {
|
||||||
|
const existingId = resolveCustomFieldId(field)
|
||||||
|
if (existingId) return existingId
|
||||||
|
|
||||||
|
const name = resolveFieldName(field)
|
||||||
|
if (!name || name === 'Champ') return null
|
||||||
|
|
||||||
|
const matches = candidateFields.filter((candidate) => {
|
||||||
|
if (!candidate || typeof candidate !== 'object') return false
|
||||||
|
const candidateId = candidate.id || candidate.customFieldId
|
||||||
|
if (candidateId && (candidateId === field?.id || candidateId === field?.customFieldId)) return true
|
||||||
|
return typeof candidate.name === 'string' && candidate.name === name
|
||||||
|
})
|
||||||
|
|
||||||
|
if (matches.length) {
|
||||||
|
const withId = matches.find((c) => c?.id || c?.customFieldId) || matches[0]
|
||||||
|
const id = withId?.id || withId?.customFieldId || null
|
||||||
|
if (id) field.customFieldId = id
|
||||||
|
if (!field.customField && typeof withId === 'object') field.customField = withId
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Structure extraction
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function extractStructureCustomFields(structure: any): any[] {
|
||||||
|
if (!structure || typeof structure !== 'object') return []
|
||||||
|
const customFields = structure.customFields
|
||||||
|
return Array.isArray(customFields) ? customFields : []
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Deduplication & merge
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function deduplicateFieldDefinitions(definitions: any[]): any[] {
|
||||||
|
const result: any[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
const orderedDefinitions = (Array.isArray(definitions) ? definitions.slice() : [])
|
||||||
|
.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||||
|
|
||||||
|
orderedDefinitions.forEach((field) => {
|
||||||
|
if (!field || typeof field !== 'object') return
|
||||||
|
const id = field.id ?? field.customFieldId ?? field.customField?.id ?? null
|
||||||
|
const nameKey = fieldKeyFromNameAndType(field.name, field.type)
|
||||||
|
const key = id || nameKey
|
||||||
|
if (key && seen.has(key)) return
|
||||||
|
if (key) seen.add(key)
|
||||||
|
field.orderIndex = resolveOrderIndex(field)
|
||||||
|
result.push(field)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeFieldDefinitionsWithValues(definitions: any[], values: any[]): any[] {
|
||||||
|
const definitionList = Array.isArray(definitions) ? definitions : []
|
||||||
|
const valueList = Array.isArray(values) ? values : []
|
||||||
|
|
||||||
|
const valueMap = new Map<string, any>()
|
||||||
|
valueList.forEach((entry) => {
|
||||||
|
if (!entry || typeof entry !== 'object') return
|
||||||
|
const fieldId = entry.customField?.id ?? entry.customFieldId ?? null
|
||||||
|
if (fieldId) valueMap.set(fieldId, entry)
|
||||||
|
const nameKey = fieldKeyFromNameAndType(entry.customField?.name, entry.customField?.type)
|
||||||
|
if (nameKey) valueMap.set(nameKey, entry)
|
||||||
|
})
|
||||||
|
|
||||||
|
const merged = definitionList.map((field) => {
|
||||||
|
if (!field || typeof field !== 'object') return field
|
||||||
|
|
||||||
|
const fieldId = resolveCustomFieldId(field)
|
||||||
|
const nameKey = fieldKeyFromNameAndType(resolveFieldName(field), resolveFieldType(field))
|
||||||
|
const matchedValue = (fieldId ? valueMap.get(fieldId) : undefined) ?? (nameKey ? valueMap.get(nameKey) : undefined)
|
||||||
|
|
||||||
|
if (!matchedValue) {
|
||||||
|
return { ...field, value: field?.value ?? '', orderIndex: resolveOrderIndex(field) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedOrder = Math.min(resolveOrderIndex(field), resolveOrderIndex(matchedValue.customField))
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
customFieldValueId: matchedValue.id ?? field.customFieldValueId ?? null,
|
||||||
|
customFieldId: matchedValue.customField?.id ?? matchedValue.customFieldId ?? fieldId ?? null,
|
||||||
|
customField: matchedValue.customField ?? field.customField ?? null,
|
||||||
|
value: matchedValue.value ?? field.value ?? '',
|
||||||
|
orderIndex: resolvedOrder,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
valueList.forEach((entry) => {
|
||||||
|
if (!entry || typeof entry !== 'object') return
|
||||||
|
|
||||||
|
const fieldId = entry.customField?.id ?? entry.customFieldId ?? null
|
||||||
|
const nameKey = fieldKeyFromNameAndType(entry.customField?.name, entry.customField?.type)
|
||||||
|
|
||||||
|
const exists = merged.some((field) => {
|
||||||
|
if (!field || typeof field !== 'object') return false
|
||||||
|
if (field.customFieldValueId && field.customFieldValueId === entry.id) return true
|
||||||
|
const existingId = resolveCustomFieldId(field)
|
||||||
|
if (fieldId && existingId && existingId === fieldId) return true
|
||||||
|
if (!fieldId && nameKey) {
|
||||||
|
return fieldKeyFromNameAndType(resolveFieldName(field), resolveFieldType(field)) === nameKey
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
merged.push({
|
||||||
|
customFieldValueId: entry.id ?? null,
|
||||||
|
customFieldId: fieldId,
|
||||||
|
name: entry.customField?.name ?? '',
|
||||||
|
type: entry.customField?.type ?? 'text',
|
||||||
|
required: entry.customField?.required ?? false,
|
||||||
|
options: entry.customField?.options ?? [],
|
||||||
|
value: entry.value ?? '',
|
||||||
|
customField: entry.customField ?? null,
|
||||||
|
orderIndex: resolveOrderIndex(entry.customField),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return merged.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dedupeMergedFields(fields: any[]): any[] {
|
||||||
|
if (!Array.isArray(fields) || fields.length <= 1) {
|
||||||
|
return Array.isArray(fields)
|
||||||
|
? fields.slice().sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Map<string, any>()
|
||||||
|
const result: any[] = []
|
||||||
|
|
||||||
|
const orderedFields = fields.slice().sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||||
|
|
||||||
|
orderedFields.forEach((field) => {
|
||||||
|
if (!field || typeof field !== 'object') return
|
||||||
|
|
||||||
|
const rawName = resolveFieldName(field)
|
||||||
|
const normalizedName = typeof rawName === 'string' ? rawName.trim() : ''
|
||||||
|
if (!normalizedName) return
|
||||||
|
|
||||||
|
field.name = normalizedName
|
||||||
|
field.type = field.type || resolveFieldType(field)
|
||||||
|
|
||||||
|
const fieldId = resolveCustomFieldId(field)
|
||||||
|
const nameKey = fieldKeyFromNameAndType(normalizedName, field.type)
|
||||||
|
const key = fieldId || nameKey
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
field.orderIndex = resolveOrderIndex(field)
|
||||||
|
result.push(field)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = seen.get(key)
|
||||||
|
if (!existing) {
|
||||||
|
field.orderIndex = resolveOrderIndex(field)
|
||||||
|
seen.set(key, field)
|
||||||
|
result.push(field)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingHasValue = existing.value !== undefined && existing.value !== null && String(existing.value).trim().length > 0
|
||||||
|
const incomingHasValue = field.value !== undefined && field.value !== null && String(field.value).trim().length > 0
|
||||||
|
|
||||||
|
if (!existingHasValue && incomingHasValue) {
|
||||||
|
Object.assign(existing, field)
|
||||||
|
existing.orderIndex = Math.min(resolveOrderIndex(existing), resolveOrderIndex(field))
|
||||||
|
seen.set(key, existing)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Definition sources builder
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function buildDefinitionSources(entity: any, entityType: 'composant' | 'piece'): any[] {
|
||||||
|
const definitions: any[] = []
|
||||||
|
const pushFields = (collection: any) => {
|
||||||
|
if (Array.isArray(collection)) definitions.push(...collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityType === 'composant') {
|
||||||
|
const requirement = entity.typeMachineComponentRequirement || {}
|
||||||
|
const type = requirement.typeComposant || entity.typeComposant || {}
|
||||||
|
|
||||||
|
pushFields(entity.customFields)
|
||||||
|
pushFields(entity.definition?.customFields)
|
||||||
|
pushFields(type.customFields)
|
||||||
|
pushFields(requirement.customFields)
|
||||||
|
pushFields(requirement.definition?.customFields)
|
||||||
|
|
||||||
|
;[
|
||||||
|
entity.definition?.structure,
|
||||||
|
type.structure,
|
||||||
|
type.componentSkeleton,
|
||||||
|
requirement.structure,
|
||||||
|
requirement.componentSkeleton,
|
||||||
|
].forEach((structure) => {
|
||||||
|
const fields = extractStructureCustomFields(structure)
|
||||||
|
if (fields.length) definitions.push(...fields)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const requirement = entity.typeMachinePieceRequirement || {}
|
||||||
|
const type = requirement.typePiece || entity.typePiece || {}
|
||||||
|
|
||||||
|
pushFields(entity.customFields)
|
||||||
|
pushFields(entity.definition?.customFields)
|
||||||
|
pushFields(entity.typePiece?.customFields)
|
||||||
|
pushFields(type.customFields)
|
||||||
|
pushFields(requirement.typePiece?.customFields)
|
||||||
|
pushFields(requirement.customFields)
|
||||||
|
pushFields(requirement.definition?.customFields)
|
||||||
|
|
||||||
|
;[
|
||||||
|
entity.definition?.structure,
|
||||||
|
entity.typePiece?.structure,
|
||||||
|
type.structure,
|
||||||
|
type.pieceSkeleton,
|
||||||
|
entity.typePiece?.pieceSkeleton,
|
||||||
|
requirement.structure,
|
||||||
|
requirement.pieceSkeleton,
|
||||||
|
].forEach((structure) => {
|
||||||
|
const fields = extractStructureCustomFields(structure)
|
||||||
|
if (fields.length) definitions.push(...fields)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return deduplicateFieldDefinitions(definitions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Candidate fields builder
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function buildCandidateCustomFields(entity: any, definitionSources: any[]): any[] {
|
||||||
|
const map = new Map<string, any>()
|
||||||
|
const register = (collection: any[]) => {
|
||||||
|
if (!Array.isArray(collection)) return
|
||||||
|
collection.forEach((item) => {
|
||||||
|
if (!item || typeof item !== 'object') return
|
||||||
|
const id = item.id || item.customFieldId
|
||||||
|
const name = typeof item.name === 'string' ? item.name : null
|
||||||
|
const key = id || (name ? `${name}::${item.type ?? ''}` : null)
|
||||||
|
if (!key || map.has(key)) return
|
||||||
|
map.set(key, item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
register((entity.customFieldValues || []).map((value: any) => value?.customField))
|
||||||
|
register(definitionSources)
|
||||||
|
|
||||||
|
return Array.from(map.values())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user