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 @@ + + + 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, +})