diff --git a/app/components/common/StructureSkeletonPreview.vue b/app/components/common/StructureSkeletonPreview.vue
new file mode 100644
index 0000000..6db7e15
--- /dev/null
+++ b/app/components/common/StructureSkeletonPreview.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
Squelette sélectionné
+
+ {{ description }}
+
+
+
{{ previewBadge }}
+
+
+
+
+ Consulter le détail du squelette
+
+
+
+
+
Champs personnalisés
+
+ -
+
+ {{ field.name || field.key }}
+
+
+ Type : {{ field.type || 'text' }} • Obligatoire
+
+ • Options : {{ field.options.join(', ') }}
+
+
+ • Défaut : {{ field.defaultValue }}
+
+
+
+
+
+
+
+
+
Champs personnalisés
+
+ -
+ {{ field.name }}
+ : {{ field.value }}
+
+
+
+
+
+
+
Pièces imposées
+
+ -
+ {{ resolvePieceLabelFn(piece) }}
+
+
+
+
+
+
+
Produits imposés
+
+ -
+ {{ resolveProductLabelFn(product) }}
+
+
+
+
+
+
+
Sous-composants
+
+ -
+ {{ resolveSubcomponentLabelFn(subcomponent) }}
+
+
+
+
+
+
+ Ce squelette ne définit pas encore de pièces, sous-composants ou valeurs par défaut.
+
+
+
+
+ Ce squelette ne définit pas encore de champs personnalisés.
+
+
+
+
+
+
+
diff --git a/app/pages/component/[id]/edit.vue b/app/pages/component/[id]/edit.vue
index f86c46e..65c6ad1 100644
--- a/app/pages/component/[id]/edit.vue
+++ b/app/pages/component/[id]/edit.vue
@@ -138,91 +138,17 @@
-
-
-
-
Squelette sélectionné
-
- {{ selectedType.description || 'Ce squelette définit la structure et les contraintes du composant.' }}
-
-
-
{{ formatStructurePreview(selectedTypeStructure) }}
-
-
-
-
- Consulter le détail du squelette
-
-
-
-
Champs personnalisés
-
- -
-
- {{ field.name || field.key }}
-
-
- Type : {{ field.type || 'text' }} • Obligatoire
-
- • Options : {{ field.options.join(', ') }}
-
-
- • Défaut : {{ field.defaultValue }}
-
-
-
-
-
-
-
-
Pièces imposées
-
- -
- {{ resolvePieceLabel(piece) }}
-
-
-
-
-
-
Produits imposés
-
- -
- {{ resolveProductLabel(product) }}
-
-
-
-
-
-
Sous-composants
-
- -
- {{ resolveSubcomponentLabel(subcomponent) }}
-
-
-
-
-
- Ce squelette ne définit pas encore de pièces, sous-composants ou valeurs par défaut.
-
-
-
-
+
-
-
-
-
Squelette sélectionné
-
- {{ selectedType.description || 'Ce squelette définit la structure et les contraintes du composant.' }}
-
-
-
{{ formatStructurePreview(selectedTypeStructure) }}
-
-
-
-
- Consulter le détail du squelette
-
-
-
-
Champs personnalisés
-
- -
-
- {{ field.name || field.key }}
-
-
- Type : {{ field.type || 'text' }} • Obligatoire
-
- • Options : {{ field.options.join(', ') }}
-
-
- • Défaut : {{ field.defaultValue }}
-
-
-
-
-
-
-
-
Pièces imposées
-
- -
- {{ resolvePieceLabel(piece) }}
-
-
-
-
-
-
Produits imposés
-
- -
- {{ resolveProductLabel(product) }}
-
-
-
-
-
-
Sous-composants
-
- -
- {{ resolveSubcomponentLabel(subcomponent) }}
-
-
-
-
-
-
+
(typeof route.query.typeId === 'string' ? route.query.typeId : '')
-const selectedTypeId = ref(initialTypeId.value)
+const selectedTypeId = ref(typeof route.query.typeId === 'string' ? route.query.typeId : '')
const submitting = ref(false)
const creationForm = reactive({
name: '' as string,
@@ -370,27 +311,14 @@ const structureDataLoading = computed(
)
const fetchedPieceTypeMap = ref>({})
-const pieceTypeLabelMap = computed(() => ({
- ...Object.fromEntries(
- (pieceTypes.value || [])
- .filter((type: any) => type?.id)
- .map((type: any) => [type.id, type.name || type.code || '']),
- ),
- ...fetchedPieceTypeMap.value,
-}))
+const pieceTypeLabelMap = computed(() =>
+ buildTypeLabelMap(pieceTypes.value, fetchedPieceTypeMap.value),
+)
const productTypeLabelMap = computed(() =>
- Object.fromEntries(
- (productTypes.value || [])
- .filter((type: any) => type?.id)
- .map((type: any) => [type.id, type.name || type.code || '']),
- ),
+ buildTypeLabelMap(productTypes.value),
)
const componentTypeLabelMap = computed(() =>
- Object.fromEntries(
- (componentTypes.value || [])
- .filter((type: any) => type?.id)
- .map((type: any) => [type.id, type.name || type.code || '']),
- ),
+ buildTypeLabelMap(componentTypes.value),
)
watch(
@@ -707,69 +635,11 @@ const canSubmit = computed(() => Boolean(
!submitting.value,
))
-const getStructureCustomFields = (structure: ComponentModelStructure | null) => {
- return Array.isArray(structure?.customFields) ? structure.customFields : []
-}
+const resolvePieceLabel = (piece: Record) =>
+ _resolvePieceLabel(piece, pieceTypeLabelMap.value)
-const getStructurePieces = (structure: ComponentModelStructure | null) => {
- return Array.isArray(structure?.pieces) ? structure.pieces : []
-}
-
-const getStructureProducts = (structure: ComponentModelStructure | null) => {
- return Array.isArray(structure?.products) ? structure.products : []
-}
-
-const getStructureSubcomponents = (structure: ComponentModelStructure | null) => {
- if (Array.isArray(structure?.subcomponents)) {
- return structure.subcomponents
- }
- const legacy = (structure as any)?.subComponents
- return Array.isArray(legacy) ? legacy : []
-}
-
-const resolvePieceLabel = (piece: Record) => {
- const parts: string[] = []
- if (piece.role) {
- parts.push(piece.role)
- }
- if (piece.typePiece?.name) {
- parts.push(piece.typePiece.name)
- } else if (piece.typePieceLabel) {
- parts.push(piece.typePieceLabel)
- } else if (piece.typePieceId && pieceTypeLabelMap.value[piece.typePieceId]) {
- parts.push(pieceTypeLabelMap.value[piece.typePieceId])
- } else if (piece.typePiece?.code) {
- parts.push(`Famille ${piece.typePiece.code}`)
- } else if (piece.familyCode) {
- parts.push(`Famille ${piece.familyCode}`)
- } else if (piece.typePieceId) {
- parts.push(`#${piece.typePieceId}`)
- }
- return parts.length ? parts.join(' • ') : 'Pièce'
-}
-
-const fetchPieceTypeNames = async (ids: string[]) => {
- const missing = ids.filter((id) => id && !pieceTypeLabelMap.value[id])
- if (!missing.length) {
- return
- }
- const results = await Promise.allSettled(
- missing.map((id) => get(`/model_types/${id}`)),
- )
- const next = { ...fetchedPieceTypeMap.value }
- results.forEach((result, index) => {
- const key = missing[index]
- if (!key || result.status !== 'fulfilled') {
- return
- }
- const data = result.value?.data
- const name = data?.name || data?.code
- if (name) {
- next[key] = name
- }
- })
- fetchedPieceTypeMap.value = next
-}
+const resolveProductLabel = (product: Record) =>
+ _resolveProductLabel(product, productTypeLabelMap.value)
watch(
selectedTypeStructure,
@@ -780,56 +650,17 @@ watch(
if (!ids.length) {
return
}
- fetchPieceTypeNames(Array.from(new Set(ids))).catch(() => {})
+ fetchModelTypeNames(Array.from(new Set(ids)), pieceTypeLabelMap.value, get)
+ .then((additions) => {
+ if (Object.keys(additions).length) {
+ fetchedPieceTypeMap.value = { ...fetchedPieceTypeMap.value, ...additions }
+ }
+ })
+ .catch(() => {})
},
{ immediate: true },
)
-const resolveProductLabel = (product: Record) => {
- const parts: string[] = []
- if (product.role) {
- parts.push(product.role)
- }
- if (product.typeProduct?.name) {
- parts.push(product.typeProduct.name)
- } else if (product.typeProductLabel) {
- parts.push(product.typeProductLabel)
- } else if (product.typeProduct?.code) {
- parts.push(`Catégorie ${product.typeProduct.code}`)
- } else if (product.familyCode) {
- parts.push(`Catégorie ${product.familyCode}`)
- } else if (product.typeProductId) {
- parts.push(`#${product.typeProductId}`)
- }
- return parts.length ? parts.join(' • ') : 'Produit'
-}
-
-const resolveSubcomponentLabel = (node: Record) => {
- const parts: string[] = []
- if (node.alias) {
- parts.push(node.alias)
- }
- if (node.typeComposant?.name) {
- parts.push(node.typeComposant.name)
- } else if (node.typeComposantLabel) {
- parts.push(node.typeComposantLabel)
- } else if (node.familyCode) {
- parts.push(node.familyCode)
- } else if (node.typeComposantId) {
- parts.push(`#${node.typeComposantId}`)
- }
-
- const childCount = Array.isArray(node.subcomponents)
- ? node.subcomponents.length
- : Array.isArray(node.subComponents)
- ? node.subComponents.length
- : 0
- if (childCount) {
- parts.push(`${childCount} sous-composant(s)`)
- }
- return parts.length ? parts.join(' • ') : 'Sous-composant'
-}
-
const clearCreationForm = () => {
creationForm.name = ''
creationForm.description = ''
@@ -903,7 +734,13 @@ const submitCreation = async () => {
try {
const result = await createComposant(payload)
if (result.success) {
- await saveCustomFieldValues(result.data)
+ const createdComponent = result.data as Record
+ await _saveCustomFieldValues(
+ 'composant',
+ createdComponent.id,
+ [createdComponent?.typeComposant?.customFields],
+ { customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
+ )
if (selectedDocuments.value.length && result.data?.id) {
uploadingDocuments.value = true
const uploadResult = await uploadDocuments(
@@ -941,274 +778,4 @@ onMounted(async () => {
loadProductTypes(),
])
})
-
-interface CustomFieldInput {
- id: string | null
- name: string
- type: string
- required: boolean
- options: string[]
- value: string
- customFieldId: string | null
- customFieldValueId: string | null
- orderIndex: number
-}
-
-
-const normalizeCustomFieldInputs = (structure: ComponentModelStructure | null): CustomFieldInput[] => {
- if (!structure || typeof structure !== 'object') {
- return []
- }
- const fields = Array.isArray(structure.customFields) ? structure.customFields : []
- return fields
- .map((field, index) => normalizeCustomField(field, index))
- .filter((field): field is CustomFieldInput => field !== null)
- .sort((a, b) => a.orderIndex - b.orderIndex)
-}
-
-const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => {
- if (!rawField || typeof rawField !== 'object') {
- return null
- }
- const name = resolveFieldName(rawField)
- if (!name) {
- return null
- }
- const type = resolveFieldType(rawField)
- const required = resolveRequiredFlag(rawField)
- const options = resolveOptions(rawField)
- const defaultSource = resolveDefaultValue(rawField)
- const value = formatDefaultValue(type, defaultSource)
- const id = typeof rawField.id === 'string' ? rawField.id : null
- const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
- const customFieldValueId = typeof rawField.customFieldValueId === 'string'
- ? rawField.customFieldValueId
- : null
- const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex
- return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex }
-}
-
-const resolveFieldName = (field: any): string => {
- if (typeof field?.name === 'string' && field.name.trim()) {
- return field.name.trim()
- }
- if (typeof field?.key === 'string' && field.key.trim()) {
- return field.key.trim()
- }
- if (typeof field?.label === 'string' && field.label.trim()) {
- return field.label.trim()
- }
- return ''
-}
-
-const resolveFieldType = (field: any): string => {
- const allowed = ['text', 'number', 'select', 'boolean', 'date']
- const rawType =
- typeof field?.type === 'string'
- ? field.type
- : typeof field?.value?.type === 'string'
- ? field.value.type
- : ''
- const value = rawType.toLowerCase()
- return allowed.includes(value) ? value : 'text'
-}
-
-const resolveDefaultValue = (field: any): any => {
- if (!field || typeof field !== 'object') {
- return null
- }
- if (field.defaultValue !== undefined && field.defaultValue !== null) {
- return field.defaultValue
- }
- if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') {
- return field.value
- }
- if (field.default !== undefined && field.default !== null) {
- return field.default
- }
- if (field.value && typeof field.value === 'object') {
- if ((field.value as any).defaultValue !== undefined && (field.value as any).defaultValue !== null) {
- return (field.value as any).defaultValue
- }
- if ((field.value as any).value !== undefined && (field.value as any).value !== null && typeof (field.value as any).value !== 'object') {
- return (field.value as any).value
- }
- }
- return null
-}
-
-const formatDefaultValue = (type: string, defaultValue: any): string => {
- if (defaultValue === null || defaultValue === undefined) {
- return ''
- }
- if (typeof defaultValue === 'object') {
- if (defaultValue === null) {
- return ''
- }
- if ('defaultValue' in (defaultValue as Record)) {
- return formatDefaultValue(type, (defaultValue as Record).defaultValue)
- }
- if ('value' in (defaultValue as Record)) {
- return formatDefaultValue(type, (defaultValue as Record).value)
- }
- return ''
- }
- if (type === 'boolean') {
- const normalized = String(defaultValue).toLowerCase()
- if (normalized === 'true' || normalized === '1') {
- return 'true'
- }
- if (normalized === 'false' || normalized === '0') {
- return 'false'
- }
- return ''
- }
- return String(defaultValue)
-}
-
-const resolveRequiredFlag = (field: any): boolean => {
- if (typeof field?.required === 'boolean') {
- return field.required
- }
- const nestedRequired = field?.value?.required
- if (typeof nestedRequired === 'boolean') {
- return nestedRequired
- }
- if (typeof nestedRequired === 'string') {
- const normalized = nestedRequired.toLowerCase()
- return normalized === 'true' || normalized === '1'
- }
- return false
-}
-
-const resolveOptions = (field: any): string[] => {
- const sources = [field?.options, field?.value?.options, field?.value?.choices]
- for (const source of sources) {
- if (Array.isArray(source)) {
- const mapped = source
- .map((option: unknown) => {
- if (option === null || option === undefined) {
- return ''
- }
- if (typeof option === 'string') {
- return option.trim()
- }
- if (typeof option === 'object') {
- const record = option as Record
- const keys = ['value', 'label', 'name']
- for (const key of keys) {
- const candidate = record[key]
- if (typeof candidate === 'string' && candidate.trim().length > 0) {
- return candidate.trim()
- }
- }
- }
- const fallback = String(option).trim()
- return fallback === '[object Object]' ? '' : fallback
- })
- .filter((option) => option.length > 0)
- if (mapped.length) {
- return mapped
- }
- }
- }
- return []
-}
-
-const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
- customFieldName: field.name,
- customFieldType: field.type,
- customFieldRequired: field.required,
- customFieldOptions: field.options,
-})
-
-const saveCustomFieldValues = async (createdComponent: any) => {
- if (!createdComponent || !createdComponent.id) {
- return
- }
-
- const definitionMap = new Map()
- const registerDefinitions = (fields: any[]) => {
- if (!Array.isArray(fields)) {
- return
- }
- fields.forEach((field) => {
- if (!field || typeof field !== 'object') {
- return
- }
- const name = typeof field.name === 'string' ? field.name : null
- const id = typeof field.id === 'string' ? field.id : null
- if (name && id && !definitionMap.has(name)) {
- definitionMap.set(name, id)
- }
- })
- }
-
- registerDefinitions(createdComponent?.typeComposant?.customFields)
-
- const resolveDefinitionId = (field: CustomFieldInput) => {
- if (field.customFieldId) {
- return field.customFieldId
- }
- if (field.id) {
- return field.id
- }
- return definitionMap.get(field.name) ?? null
- }
-
- for (const field of customFieldInputs.value) {
- if (!shouldPersistField(field)) {
- continue
- }
-
- const definitionId = resolveDefinitionId(field)
- const metadata = definitionId ? undefined : buildCustomFieldMetadata(field)
- const value = formatValueForPersistence(field)
-
- if (field.customFieldValueId) {
- const result = await updateCustomFieldValue(field.customFieldValueId, { value })
- if (!result.success) {
- toast.showError(`Impossible de mettre à jour le champ personnalisé "${field.name}"`)
- } else if (definitionId && !field.customFieldId) {
- field.customFieldId = definitionId
- }
- continue
- }
-
- const result = await upsertCustomFieldValue(
- definitionId,
- 'composant',
- createdComponent.id,
- value,
- metadata,
- )
-
- if (!result.success) {
- toast.showError(`Impossible d'enregistrer le champ personnalisé "${field.name}"`)
- } else {
- const createdValue = result.data
- if (createdValue?.id) {
- field.customFieldValueId = createdValue.id
- }
- const resolvedId = createdValue?.customField?.id || definitionId
- if (resolvedId) {
- field.customFieldId = resolvedId
- }
- }
- }
-}
-
-const shouldPersistField = (field: CustomFieldInput) => {
- if (field.type === 'boolean') {
- return field.value === 'true' || field.value === 'false'
- }
- return toFieldString(field.value).trim() !== ''
-}
-
-const formatValueForPersistence = (field: CustomFieldInput) => {
- if (field.type === 'boolean') {
- return field.value === 'true' ? 'true' : 'false'
- }
- return toFieldString(field.value).trim()
-}
diff --git a/app/pages/pieces/[id]/edit.vue b/app/pages/pieces/[id]/edit.vue
index 3e5a326..3d3605b 100644
--- a/app/pages/pieces/[id]/edit.vue
+++ b/app/pages/pieces/[id]/edit.vue
@@ -182,38 +182,13 @@
-
-
-
-
Squelette sélectionné
-
- {{ selectedType?.description || 'Ce squelette définit la structure et les champs personnalisés de la pièce.' }}
-
-
-
{{ formatPieceStructurePreview(resolvedStructure) }}
-
-
-
-
- Consulter le détail du squelette
-
-
-
-
Champs personnalisés
-
- -
- {{ field.name }}
- : {{ field.value }}
-
-
-
-
-
- Ce squelette ne définit pas encore de champs personnalisés.
-
-
-
-
+
@@ -457,9 +432,6 @@ const selectedType = computed(() => {
const getStructureProducts = (structure: PieceModelStructure | null) =>
Array.isArray(structure?.products) ? structure.products : []
-const getStructureCustomFields = (structure: PieceModelStructure | null) =>
- Array.isArray(structure?.customFields) ? structure.customFields : []
-
const structureProducts = computed(() =>
getStructureProducts(resolvedStructure.value),
)
diff --git a/app/pages/pieces/create.vue b/app/pages/pieces/create.vue
index 9ccc3e4..d6cb88d 100644
--- a/app/pages/pieces/create.vue
+++ b/app/pages/pieces/create.vue
@@ -153,38 +153,13 @@
-
-
-
-
Squelette sélectionné
-
- {{ selectedType.description || 'Ce squelette définit la structure et les champs personnalisés de la pièce.' }}
-
-
-
{{ formatPieceStructurePreview(selectedType.structure) }}
-
-
-
-
- Consulter le détail du squelette
-
-
-
-
Champs personnalisés
-
- -
- {{ field.name }}
- : {{ field.value }}
-
-
-
-
-
- Ce squelette ne définit pas encore de champs personnalisés.
-
-
-
-
+
@@ -328,9 +303,6 @@ const selectedType = computed(() => {
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
})
-const getStructureCustomFields = (structure: PieceModelStructure | null) =>
- Array.isArray(structure?.customFields) ? structure.customFields : []
-
const getStructureProducts = (structure: PieceModelStructure | null) =>
Array.isArray(structure?.products) ? structure.products : []
diff --git a/app/shared/utils/structureDisplayUtils.ts b/app/shared/utils/structureDisplayUtils.ts
new file mode 100644
index 0000000..9988af5
--- /dev/null
+++ b/app/shared/utils/structureDisplayUtils.ts
@@ -0,0 +1,157 @@
+/**
+ * Shared helpers for displaying component/machine structure skeleton details.
+ *
+ * Extracted from pages/component/create.vue and pages/component/[id]/edit.vue
+ * where these functions were duplicated verbatim.
+ */
+
+// ---------------------------------------------------------------------------
+// Structure accessors
+// ---------------------------------------------------------------------------
+
+type StructureLike = Record | null
+
+export const getStructureCustomFields = (structure: StructureLike): any[] => {
+ return Array.isArray(structure?.customFields) ? structure.customFields : []
+}
+
+export const getStructurePieces = (structure: StructureLike): any[] => {
+ return Array.isArray(structure?.pieces) ? structure.pieces : []
+}
+
+export const getStructureProducts = (structure: StructureLike): any[] => {
+ return Array.isArray(structure?.products) ? structure.products : []
+}
+
+export const getStructureSubcomponents = (structure: StructureLike): any[] => {
+ if (Array.isArray(structure?.subcomponents)) {
+ return structure.subcomponents
+ }
+ const legacy = (structure as any)?.subComponents
+ return Array.isArray(legacy) ? legacy : []
+}
+
+// ---------------------------------------------------------------------------
+// Label resolvers
+// ---------------------------------------------------------------------------
+
+export const resolvePieceLabel = (
+ piece: Record,
+ labelMap: Record = {},
+): string => {
+ const parts: string[] = []
+ if (piece.role) {
+ parts.push(piece.role)
+ }
+ if (piece.typePiece?.name) {
+ parts.push(piece.typePiece.name)
+ } else if (piece.typePieceLabel) {
+ parts.push(piece.typePieceLabel)
+ } else if (piece.typePieceId && labelMap[piece.typePieceId]) {
+ parts.push(labelMap[piece.typePieceId]!)
+ } else if (piece.typePiece?.code) {
+ parts.push(`Famille ${piece.typePiece.code}`)
+ } else if (piece.familyCode) {
+ parts.push(`Famille ${piece.familyCode}`)
+ } else if (piece.typePieceId) {
+ parts.push(`#${piece.typePieceId}`)
+ }
+ return parts.length ? parts.join(' • ') : 'Pièce'
+}
+
+export const resolveProductLabel = (
+ product: Record,
+ labelMap: Record = {},
+): string => {
+ const parts: string[] = []
+ if (product.role) {
+ parts.push(product.role)
+ }
+ if (product.typeProduct?.name) {
+ parts.push(product.typeProduct.name)
+ } else if (product.typeProductLabel) {
+ parts.push(product.typeProductLabel)
+ } else if (product.typeProductId && labelMap[product.typeProductId]) {
+ parts.push(labelMap[product.typeProductId]!)
+ } else if (product.typeProduct?.code) {
+ parts.push(`Catégorie ${product.typeProduct.code}`)
+ } else if (product.familyCode) {
+ parts.push(`Catégorie ${product.familyCode}`)
+ } else if (product.typeProductId) {
+ parts.push(`#${product.typeProductId}`)
+ }
+ return parts.length ? parts.join(' • ') : 'Produit'
+}
+
+export const resolveSubcomponentLabel = (node: Record): string => {
+ const parts: string[] = []
+ if (node.alias) {
+ parts.push(node.alias)
+ }
+ if (node.typeComposant?.name) {
+ parts.push(node.typeComposant.name)
+ } else if (node.typeComposantLabel) {
+ parts.push(node.typeComposantLabel)
+ } else if (node.familyCode) {
+ parts.push(node.familyCode)
+ } else if (node.typeComposantId) {
+ parts.push(`#${node.typeComposantId}`)
+ }
+
+ const childCount = Array.isArray(node.subcomponents)
+ ? node.subcomponents.length
+ : Array.isArray(node.subComponents)
+ ? node.subComponents.length
+ : 0
+ if (childCount) {
+ parts.push(`${childCount} sous-composant(s)`)
+ }
+ return parts.length ? parts.join(' • ') : 'Sous-composant'
+}
+
+// ---------------------------------------------------------------------------
+// Generic model type name fetcher (replaces fetchPieceTypeNames / fetchProductTypeNames)
+// ---------------------------------------------------------------------------
+
+export const fetchModelTypeNames = async (
+ ids: string[],
+ existingMap: Record,
+ get: (url: string) => Promise<{ success?: boolean; data?: any }>,
+): Promise> => {
+ const missing = ids.filter((id) => id && !existingMap[id])
+ if (!missing.length) {
+ return {}
+ }
+ const results = await Promise.allSettled(
+ missing.map((id) => get(`/model_types/${id}`)),
+ )
+ const additions: Record = {}
+ results.forEach((result, index) => {
+ const key = missing[index]
+ if (!key || result.status !== 'fulfilled') {
+ return
+ }
+ const data = result.value?.data
+ const name = data?.name || data?.code
+ if (name) {
+ additions[key] = name
+ }
+ })
+ return additions
+}
+
+// ---------------------------------------------------------------------------
+// Type label map builder
+// ---------------------------------------------------------------------------
+
+export const buildTypeLabelMap = (
+ types: any[],
+ fetchedOverrides: Record = {},
+): Record => ({
+ ...Object.fromEntries(
+ (types || [])
+ .filter((type: any) => type?.id)
+ .map((type: any) => [type.id, type.name || type.code || '']),
+ ),
+ ...fetchedOverrides,
+})