diff --git a/app/components/ComponentItem.vue b/app/components/ComponentItem.vue index 76ca5b2..1f2df0a 100644 --- a/app/components/ComponentItem.vue +++ b/app/components/ComponentItem.vue @@ -25,6 +25,12 @@ {{ component.name }}
+ + Défini dans le catalogue + {{ component.reference }} {{ component.constructeur?.name }} {{ component.prix }}€ @@ -264,8 +270,8 @@ v-for="piece in component.pieces" :key="piece.id" :piece="piece" - :is-edit-mode="isEditMode" - :piece-model-options="pieceModelOptionsProvider(piece)" + :is-edit-mode="isEditMode && !piece.skeletonOnly" + @update="updatePiece" @edit="editPiece" @custom-field-update="updatePieceCustomField" @@ -283,7 +289,7 @@ v-for="subComponent in childComponents" :key="subComponent.id" :component="subComponent" - :is-edit-mode="isEditMode" + :is-edit-mode="isEditMode && !subComponent.skeletonOnly" :collapse-all="collapseAll" :toggle-token="toggleToken" @update="$emit('update', $event)" @@ -358,11 +364,41 @@ const extractStructureCustomFields = (structure) => { } function fieldKeyFromNameAndType(name, type) { - const normalizedName = typeof name === 'string' ? name : '' + const normalizedName = typeof name === 'string' ? name.trim() : '' const normalizedType = typeof type === 'string' ? type : '' return normalizedName ? `${normalizedName}::${normalizedType}` : null } +function deduplicateFieldDefinitions(definitions) { + const result = [] + const seen = new Set() + + ;(Array.isArray(definitions) ? definitions : []).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) + } + result.push(field) + }) + + return result +} + function mergeFieldDefinitionsWithValues(definitions, values) { const definitionList = Array.isArray(definitions) ? definitions : [] const valueList = Array.isArray(values) ? values : [] @@ -458,6 +494,62 @@ function mergeFieldDefinitionsWithValues(definitions, values) { return merged } +function dedupeMergedFields(fields) { + if (!Array.isArray(fields) || fields.length <= 1) { + return Array.isArray(fields) ? fields : [] + } + + const seen = new Map() + const result = [] + + fields.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) { + result.push(field) + return + } + + const existing = seen.get(key) + if (!existing) { + 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) + seen.set(key, existing) + } + }) + + return result +} + const componentDefinitionSources = computed(() => { const requirement = props.component.typeMachineComponentRequirement || {} const type = requirement.typeComposant || props.component.typeComposant || {} @@ -488,13 +580,15 @@ const componentDefinitionSources = computed(() => { } }) - return definitions + return deduplicateFieldDefinitions(definitions) }) const displayedCustomFields = computed(() => - mergeFieldDefinitionsWithValues( - componentDefinitionSources.value, - props.component.customFieldValues, + dedupeMergedFields( + mergeFieldDefinitionsWithValues( + componentDefinitionSources.value, + props.component.customFieldValues, + ), ), ) diff --git a/app/components/ComponentStructureAssignmentNode.vue b/app/components/ComponentStructureAssignmentNode.vue new file mode 100644 index 0000000..50a299a --- /dev/null +++ b/app/components/ComponentStructureAssignmentNode.vue @@ -0,0 +1,312 @@ + + + diff --git a/app/components/PieceItem.vue b/app/components/PieceItem.vue index b6782bd..8b92423 100644 --- a/app/components/PieceItem.vue +++ b/app/components/PieceItem.vue @@ -25,6 +25,12 @@
+ + Défini dans le catalogue + { + 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); + } + result.push(field); + }); + + return result; +} + function mergeFieldDefinitionsWithValues(definitions, values) { const definitionList = Array.isArray(definitions) ? definitions : []; const valueList = Array.isArray(values) ? values : []; @@ -494,6 +527,72 @@ function mergeFieldDefinitionsWithValues(definitions, values) { return merged; } +function dedupeMergedFields(fields) { + if (!Array.isArray(fields) || fields.length <= 1) { + return Array.isArray(fields) ? fields : []; + } + + const seen = new Map(); + const result = []; + + fields.forEach((field) => { + if (!field || typeof field !== 'object') { + return; + } + + const rawName = resolveFieldName(field); + const normalizedName = + typeof rawName === 'string' ? rawName.trim() : ''; + + if (!normalizedName) { + return; + } + + field.type = field.type || 'text'; + + if (typeof field.name === 'string') { + field.name = field.name.trim(); + } else { + field.name = normalizedName; + } + + const fieldId = resolveCustomFieldId(field); + const nameKey = fieldKeyFromNameAndType( + normalizedName, + resolveFieldType(field), + ); + const key = fieldId || nameKey; + + if (!key) { + result.push(field); + return; + } + + const existing = seen.get(key); + if (!existing) { + 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); + seen.set(key, existing); + } + }); + + return result; +} + const pieceDefinitionSources = computed(() => { const requirement = props.piece.typeMachinePieceRequirement || {}; const type = requirement.typePiece || props.piece.typePiece || {}; @@ -528,13 +627,15 @@ const pieceDefinitionSources = computed(() => { } }); - return definitions; + return deduplicateFieldDefinitions(definitions); }); const displayedCustomFields = computed(() => - mergeFieldDefinitionsWithValues( - pieceDefinitionSources.value, - props.piece.customFieldValues, + dedupeMergedFields( + mergeFieldDefinitionsWithValues( + pieceDefinitionSources.value, + props.piece.customFieldValues, + ), ), ); diff --git a/app/composables/useComposants.js b/app/composables/useComposants.js index 290a50d..950b3cb 100644 --- a/app/composables/useComposants.js +++ b/app/composables/useComposants.js @@ -9,52 +9,20 @@ export function useComposants () { const { showSuccess, showError, showInfo } = useToast() const { get, post, patch, delete: del } = useApi() - const loadComposants = async () => { - loading.value = true - try { - const result = await get('/composants') - if (result.success) { - composants.value = result.data - showInfo(`Chargement de ${composants.value.length} composant(s) réussi`) - } - } catch (error) { - console.error('Erreur lors du chargement des composants:', error) - } finally { - loading.value = false - } - } - - const getComposantsByMachine = async (machineId) => { - loading.value = true - try { - const result = await get(`/composants/machine/${machineId}`) - if (result.success) { - return { success: true, data: result.data } - } - return { success: false, error: result.error } - } catch (error) { - console.error('Erreur lors du chargement des composants:', error) - return { success: false, error: error.message } - } finally { - loading.value = false - } - } - - const getComposantHierarchy = async (machineId) => { - loading.value = true - try { - const result = await get(`/composants/hierarchy/${machineId}`) - if (result.success) { - return { success: true, data: result.data } - } - return { success: false, error: result.error } - } catch (error) { - console.error('Erreur lors du chargement de la hiérarchie:', error) - return { success: false, error: error.message } - } finally { - loading.value = false +const loadComposants = async () => { + loading.value = true + try { + const result = await get('/composants') + if (result.success) { + composants.value = result.data + showInfo(`Chargement de ${composants.value.length} composant(s) réussi`) } + } catch (error) { + console.error('Erreur lors du chargement des composants:', error) + } finally { + loading.value = false } +} const createComposant = async (composantData) => { loading.value = true @@ -116,10 +84,6 @@ export function useComposants () { } } - const getComposantById = (id) => { - return composants.value.find(comp => comp.id === id) - } - const getComposants = () => composants.value const isLoading = () => loading.value @@ -127,12 +91,9 @@ export function useComposants () { composants, loading, loadComposants, - getComposantsByMachine, - getComposantHierarchy, createComposant, updateComposant: updateComposantData, deleteComposant, - getComposantById, getComposants, isLoading } diff --git a/app/composables/usePieces.js b/app/composables/usePieces.js index f3e1340..65b541e 100644 --- a/app/composables/usePieces.js +++ b/app/composables/usePieces.js @@ -24,38 +24,6 @@ export function usePieces () { } } - const getPiecesByMachine = async (machineId) => { - loading.value = true - try { - const result = await get(`/pieces/machine/${machineId}`) - if (result.success) { - return { success: true, data: result.data } - } - return { success: false, error: result.error } - } catch (error) { - console.error('Erreur lors du chargement des pièces:', error) - return { success: false, error: error.message } - } finally { - loading.value = false - } - } - - const getPiecesByComposant = async (composantId) => { - loading.value = true - try { - const result = await get(`/pieces/composant/${composantId}`) - if (result.success) { - return { success: true, data: result.data } - } - return { success: false, error: result.error } - } catch (error) { - console.error('Erreur lors du chargement des pièces:', error) - return { success: false, error: error.message } - } finally { - loading.value = false - } - } - const createPiece = async (pieceData) => { loading.value = true try { @@ -116,10 +84,6 @@ export function usePieces () { } } - const getPieceById = (id) => { - return pieces.value.find(piece => piece.id === id) - } - const getPieces = () => pieces.value const isLoading = () => loading.value @@ -127,12 +91,9 @@ export function usePieces () { pieces, loading, loadPieces, - getPiecesByMachine, - getPiecesByComposant, createPiece, updatePiece: updatePieceData, deletePiece, - getPieceById, getPieces, isLoading } diff --git a/app/pages/component/create.vue b/app/pages/component/create.vue index f0c677a..71164e4 100644 --- a/app/pages/component/create.vue +++ b/app/pages/component/create.vue @@ -156,6 +156,45 @@
+
+
+
+

+ Sélection des éléments du squelette +

+

+ Affectez les pièces et sous-composants concrets correspondant à la catégorie choisie. +

+
+ + {{ structureSelectionsComplete ? 'Complet' : structureDataLoading ? 'Chargement…' : 'Incomplet' }} + +
+ +
+ + Chargement du catalogue de pièces et de composants… +
+ +

+ Impossible de générer les emplacements définis par le squelette. +

+
+

Champs personnalisés

@@ -255,12 +294,20 @@ import { computed, onMounted, reactive, ref, watch } from 'vue' import { useRoute, useRouter } from '#imports' import ConstructeurSelect from '~/components/ConstructeurSelect.vue' +import ComponentStructureAssignmentNode, { + type StructureAssignmentNode, +} from '~/components/ComponentStructureAssignmentNode.vue' import { useComponentTypes } from '~/composables/useComponentTypes' import { useComposants } from '~/composables/useComposants' +import { usePieces } from '~/composables/usePieces' import { useToast } from '~/composables/useToast' import { useCustomFields } from '~/composables/useCustomFields' import { formatStructurePreview } from '~/shared/modelUtils' -import type { ComponentModelStructure } from '~/shared/types/inventory' +import type { + ComponentModelPiece, + ComponentModelStructure, + ComponentModelStructureNode, +} from '~/shared/types/inventory' import type { ModelType } from '~/services/modelTypes' interface ComponentCatalogType extends ModelType { @@ -272,7 +319,17 @@ const route = useRoute() const router = useRouter() const { componentTypes, loadComponentTypes, loadingComponentTypes } = useComponentTypes() -const { createComposant } = useComposants() +const { + createComposant, + composants: componentCatalogRef, + loadComposants, + loading: componentsLoading, +} = useComposants() +const { + pieces: pieceCatalogRef, + loadPieces, + loading: piecesLoading, +} = usePieces() const toast = useToast() const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields() @@ -287,6 +344,13 @@ const creationForm = reactive({ }) const lastSuggestedName = ref('') const customFieldInputs = ref([]) +const structureAssignments = ref(null) + +const availablePieces = computed(() => pieceCatalogRef.value ?? []) +const availableComponents = computed(() => componentCatalogRef.value ?? []) +const structureDataLoading = computed( + () => piecesLoading.value || componentsLoading.value, +) watch( () => route.query.typeId, @@ -328,6 +392,7 @@ watch(selectedType, (type) => { if (!type) { clearCreationForm() customFieldInputs.value = [] + structureAssignments.value = null return } if (!creationForm.name || creationForm.name === lastSuggestedName.value) { @@ -335,8 +400,195 @@ watch(selectedType, (type) => { } lastSuggestedName.value = creationForm.name customFieldInputs.value = normalizeCustomFieldInputs(type.structure) + structureAssignments.value = initializeStructureAssignments(type.structure) }) +const extractSubcomponents = ( + definition: ComponentModelStructure | ComponentModelStructureNode | null | undefined, +): ComponentModelStructureNode[] => { + if (!definition || typeof definition !== 'object') { + return [] + } + const raw = Array.isArray((definition as any).subcomponents) + ? (definition as any).subcomponents + : Array.isArray((definition as any).subComponents) + ? (definition as any).subComponents + : [] + return raw.filter( + (item: unknown): item is ComponentModelStructureNode => + !!item && typeof item === 'object', + ) +} + +const extractPiecesFromNode = ( + definition: ComponentModelStructure | ComponentModelStructureNode | null | undefined, +): ComponentModelPiece[] => { + if (!definition || typeof definition !== 'object') { + return [] + } + const raw = Array.isArray((definition as any).pieces) + ? (definition as any).pieces + : [] + return raw.filter( + (item: unknown): item is ComponentModelPiece => + !!item && typeof item === 'object', + ) +} + +const buildAssignmentNode = ( + definition: ComponentModelStructureNode | ComponentModelStructure, + path: string, +): StructureAssignmentNode => { + const pieces = extractPiecesFromNode(definition).map((piece, index) => ({ + path: `${path}:piece-${index}`, + definition: piece, + selectedPieceId: '', + })) + + const subcomponents = extractSubcomponents(definition).map( + (child, index) => buildAssignmentNode(child, `${path}:sub-${index}`), + ) + + return { + path, + definition, + selectedComponentId: '', + pieces, + subcomponents, + } +} + +const initializeStructureAssignments = ( + structure: ComponentModelStructure | null, +): StructureAssignmentNode | null => { + if (!structure || typeof structure !== 'object') { + return null + } + return buildAssignmentNode(structure, 'root') +} + +const hasAssignments = (node: StructureAssignmentNode | null): boolean => { + if (!node) { + return false + } + if (node.pieces.length > 0 || node.subcomponents.length > 0) { + return true + } + return node.subcomponents.some((child) => hasAssignments(child)) +} + +const structureHasRequirements = computed(() => + hasAssignments(structureAssignments.value), +) + +const isAssignmentNodeComplete = ( + node: StructureAssignmentNode, + isRootNode = false, +): boolean => { + const piecesComplete = node.pieces.every( + (piece) => !!piece.selectedPieceId && piece.selectedPieceId.length > 0, + ) + const subcomponentsComplete = node.subcomponents.every( + (child) => + !!child.selectedComponentId && + child.selectedComponentId.length > 0 && + isAssignmentNodeComplete(child, false), + ) + return piecesComplete && subcomponentsComplete && (isRootNode || !!node.selectedComponentId) +} + +const structureSelectionsComplete = computed(() => { + if (!structureHasRequirements.value) { + return true + } + if (structureDataLoading.value) { + return false + } + if (!structureAssignments.value) { + return false + } + return isAssignmentNodeComplete(structureAssignments.value, true) +}) + +const stripNullish = (input: Record) => + Object.fromEntries( + Object.entries(input).filter( + ([, value]) => value !== null && value !== undefined && value !== '', + ), + ) + +const sanitizeStructureDefinition = ( + definition: ComponentModelStructureNode, +) => + stripNullish({ + alias: definition.alias ?? null, + typeComposantId: definition.typeComposantId ?? null, + typeComposantLabel: definition.typeComposantLabel ?? null, + modelId: definition.modelId ?? null, + familyCode: (definition as any).familyCode ?? null, + }) + +const sanitizePieceDefinition = (definition: ComponentModelPiece) => + stripNullish({ + role: (definition as any).role ?? null, + typePieceId: definition.typePieceId ?? null, + typePieceLabel: definition.typePieceLabel ?? null, + reference: definition.reference ?? null, + }) + +const serializeStructureAssignments = ( + root: StructureAssignmentNode | null, +) => { + if (!root) { + return null + } + + const serializeNode = ( + assignment: StructureAssignmentNode, + isRootNode = false, + ): Record => { + const serializedPieces = assignment.pieces + .filter((piece) => !!piece.selectedPieceId) + .map((piece) => + stripNullish({ + path: piece.path, + definition: sanitizePieceDefinition(piece.definition), + selectedPieceId: piece.selectedPieceId, + }), + ) + + const serializedSubcomponents = assignment.subcomponents + .map((child) => serializeNode(child, false)) + .filter((child) => Object.keys(child).length > 0) + + const base: Record = { + path: assignment.path, + definition: sanitizeStructureDefinition(assignment.definition), + } + + if (!isRootNode) { + base.selectedComponentId = assignment.selectedComponentId + } + if (serializedPieces.length) { + base.pieces = serializedPieces + } + if (serializedSubcomponents.length) { + base.subcomponents = serializedSubcomponents + } + + return stripNullish(base) + } + + const serializedRoot = serializeNode(root, true) + if ( + (!serializedRoot.pieces || serializedRoot.pieces.length === 0) && + (!serializedRoot.subcomponents || serializedRoot.subcomponents.length === 0) + ) { + return null + } + return serializedRoot +} + const requiredCustomFieldsFilled = computed(() => customFieldInputs.value.every((field) => { if (!field.required) { @@ -353,6 +605,7 @@ const canSubmit = computed(() => Boolean( selectedType.value && creationForm.name && requiredCustomFieldsFilled.value && + structureSelectionsComplete.value && !submitting.value, )) @@ -421,6 +674,7 @@ const clearCreationForm = () => { creationForm.constructeurId = null creationForm.prix = '' lastSuggestedName.value = '' + structureAssignments.value = null } const submitCreation = async () => { @@ -455,6 +709,19 @@ const submitCreation = async () => { } } + if (structureHasRequirements.value && !structureSelectionsComplete.value) { + toast.showError('Complétez la sélection des pièces et sous-composants.') + return + } + + const serializedStructure = structureHasRequirements.value + ? serializeStructureAssignments(structureAssignments.value) + : null + + if (serializedStructure) { + payload.structure = serializedStructure + } + submitting.value = true try { const result = await createComposant(payload) @@ -473,7 +740,11 @@ const submitCreation = async () => { } onMounted(async () => { - await loadComponentTypes() + await Promise.allSettled([ + loadComponentTypes(), + loadPieces(), + loadComposants(), + ]) }) interface CustomFieldInput { diff --git a/app/pages/machine-skeleton/index.vue b/app/pages/machine-skeleton/index.vue index 070f57f..6934bd7 100644 --- a/app/pages/machine-skeleton/index.vue +++ b/app/pages/machine-skeleton/index.vue @@ -4,14 +4,9 @@
-

- Squelettes de machine -

+

Squelettes de machine

-
@@ -51,23 +46,29 @@
- Voir détails -
@@ -92,52 +93,59 @@ diff --git a/app/pages/machine/[id].vue b/app/pages/machine/[id].vue index a7b0fa7..047914b 100644 --- a/app/pages/machine/[id].vue +++ b/app/pages/machine/[id].vue @@ -2108,27 +2108,83 @@ const mergeCustomFieldValuesWithDefinitions = (valueEntries = [], ...definitionS return result } +const dedupeCustomFieldEntries = (fields) => { + if (!Array.isArray(fields) || fields.length <= 1) { + return Array.isArray(fields) ? fields : [] + } + + const seen = new Set() + const result = [] + + for (const field of fields) { + if (!field) { + continue + } + + field.type = field.type || 'text' + + let normalizedName = + typeof field.name === 'string' ? field.name.trim() : '' + + if (!normalizedName && field.customField?.name) { + normalizedName = String(field.customField.name).trim() + field.name = normalizedName + } else if (typeof field.name === 'string') { + field.name = normalizedName + } + + const key = + field.customFieldId || + field.id || + (normalizedName ? `${normalizedName}::${field.type || 'text'}` : null) + + if (!key && !normalizedName) { + continue + } + + if (key && seen.has(key)) { + continue + } + + if (!normalizedName) { + continue + } + + if (key) { + seen.add(key) + } + if (normalizedName) { + seen.add(`${normalizedName}::${field.type || 'text'}`) + } + result.push(field) + } + + return result +} + const transformCustomFields = (pieces) => { return (pieces || []).map((piece) => { const requirement = piece.typeMachinePieceRequirement || {} const typePiece = requirement.typePiece || piece.typePiece || {} - const customFields = mergeCustomFieldValuesWithDefinitions( - piece.customFieldValues, - piece.customFields, - piece.definition?.customFields, - piece.typePiece?.customFields, - typePiece.customFields, - requirement.typePiece?.customFields, - requirement.customFields, - requirement.definition?.customFields, - getStructureCustomFields(piece.definition?.structure), - getStructureCustomFields(piece.typePiece?.structure), - getStructureCustomFields(typePiece.structure), - getStructureCustomFields(typePiece.pieceSkeleton), - getStructureCustomFields(piece.typePiece?.pieceSkeleton), - getStructureCustomFields(requirement.structure), - getStructureCustomFields(requirement.pieceSkeleton), + const customFields = dedupeCustomFieldEntries( + mergeCustomFieldValuesWithDefinitions( + piece.customFieldValues, + piece.customFields, + piece.definition?.customFields, + piece.typePiece?.customFields, + typePiece.customFields, + requirement.typePiece?.customFields, + requirement.customFields, + requirement.definition?.customFields, + getStructureCustomFields(piece.definition?.structure), + getStructureCustomFields(piece.typePiece?.structure), + getStructureCustomFields(typePiece.structure), + getStructureCustomFields(typePiece.pieceSkeleton), + getStructureCustomFields(piece.typePiece?.pieceSkeleton), + getStructureCustomFields(requirement.structure), + getStructureCustomFields(requirement.pieceSkeleton), + ), ) return { @@ -2151,21 +2207,23 @@ const transformComponentCustomFields = (componentsData) => { const requirement = component.typeMachineComponentRequirement || {} const type = requirement.typeComposant || component.typeComposant || {} - const customFields = mergeCustomFieldValuesWithDefinitions( - component.customFieldValues, - component.customFields, - component.definition?.customFields, - component.typeComposant?.customFields, - type.customFields, - requirement.typeComposant?.customFields, - requirement.customFields, - requirement.definition?.customFields, - getStructureCustomFields(component.definition?.structure), - getStructureCustomFields(component.typeComposant?.structure), - getStructureCustomFields(type.structure), - getStructureCustomFields(type.componentSkeleton), - getStructureCustomFields(requirement.structure), - getStructureCustomFields(requirement.componentSkeleton), + const customFields = dedupeCustomFieldEntries( + mergeCustomFieldValuesWithDefinitions( + component.customFieldValues, + component.customFields, + component.definition?.customFields, + component.typeComposant?.customFields, + type.customFields, + requirement.typeComposant?.customFields, + requirement.customFields, + requirement.definition?.customFields, + getStructureCustomFields(component.definition?.structure), + getStructureCustomFields(component.typeComposant?.structure), + getStructureCustomFields(type.structure), + getStructureCustomFields(type.componentSkeleton), + getStructureCustomFields(requirement.structure), + getStructureCustomFields(requirement.componentSkeleton), + ), ) const pieces = component.pieces @@ -2198,14 +2256,14 @@ const transformComponentCustomFields = (componentsData) => { const syncMachineCustomFields = () => { if (!machine.value) { machineCustomFields.value = [] - return + return } - const merged = mergeCustomFieldValuesWithDefinitions( + const merged = dedupeCustomFieldEntries(mergeCustomFieldValuesWithDefinitions( machine.value.customFieldValues, machine.value.customFields, machine.value.typeMachine?.customFields, - ).map((field) => ({ + )).map((field) => ({ ...field, readOnly: false, })) @@ -2276,210 +2334,271 @@ function mergeComponentTrees(existing = [], updates = []) { } const buildMachineHierarchyFromLinks = (componentLinks = [], pieceLinks = []) => { - const componentMap = new Map() - const componentRoots = [] + const normalizeComponentLinkId = (link) => + resolveIdentifier(link?.id, link?.linkId, link?.machineComponentLinkId) - componentLinks.forEach((link, index) => { - if (!isPlainObject(link)) { - return + const normalizePieceLinkId = (link) => + resolveIdentifier(link?.id, link?.linkId, link?.machinePieceLinkId) + + const createPieceNode = (link, parentComponentName = null) => { + if (!link || typeof link !== 'object') { + return null } - const baseComponent = isPlainObject(link.composant) - ? link.composant - : isPlainObject(link.component) - ? link.component - : isPlainObject(link.targetComponent) - ? link.targetComponent - : {} + const appliedPiece = + (link.piece && typeof link.piece === 'object' && link.piece) || {} + const originalPiece = + (link.originalPiece && typeof link.originalPiece === 'object' && link.originalPiece) || null - const linkId = resolveIdentifier(link.id, link.linkId, link.machineComponentLinkId) + const requirement = + link.typeMachinePieceRequirement || + appliedPiece.typeMachinePieceRequirement || + originalPiece?.typeMachinePieceRequirement || + null - const node = { - ...baseComponent, - machineComponentLink: link, - machineComponentLinkId: linkId, - linkId, - componentLinkId: linkId, - composantId: resolveIdentifier( - baseComponent.composantId, - baseComponent.componentId, - link.composantId, - link.componentId, - baseComponent.id, - ), + const machinePieceLinkId = normalizePieceLinkId(link) + const pieceId = resolveIdentifier(appliedPiece.id, appliedPiece.pieceId, link.pieceId) + + const basePiece = { + ...appliedPiece, + id: appliedPiece.id || pieceId || machinePieceLinkId || `piece-${machinePieceLinkId}`, + pieceId, + name: + link.overrides?.name || + appliedPiece.name || + appliedPiece.definition?.name || + appliedPiece.definition?.role || + originalPiece?.name || + 'Pièce', + reference: + link.overrides?.reference || + appliedPiece.reference || + appliedPiece.definition?.reference || + originalPiece?.reference || + null, + prix: + link.overrides?.prix ?? + appliedPiece.prix ?? + originalPiece?.prix ?? + null, + constructeur: + appliedPiece.constructeur || + originalPiece?.constructeur || + null, + constructeurId: + appliedPiece.constructeurId || + appliedPiece.constructeur?.id || + originalPiece?.constructeurId || + null, + documents: + Array.isArray(appliedPiece.documents) + ? appliedPiece.documents + : Array.isArray(originalPiece?.documents) + ? originalPiece.documents + : [], + typePiece: appliedPiece.typePiece || requirement?.typePiece || null, + typePieceId: + appliedPiece.typePieceId || + appliedPiece.typePiece?.id || + requirement?.typePieceId || + requirement?.typePiece?.id || + null, + typeMachinePieceRequirement: requirement, + typeMachinePieceRequirementId: requirement?.id || null, + requirementId: requirement?.id || null, + overrides: link.overrides || null, + originalPiece, + machinePieceLink: link, + machinePieceLinkId, + linkId: machinePieceLinkId, parentComponentLinkId: resolveIdentifier( link.parentComponentLinkId, link.parentLinkId, link.parentMachineComponentLinkId, - baseComponent.parentComponentLinkId, - baseComponent.parentLinkId, + appliedPiece.parentComponentLinkId, ), - parentComposantId: resolveIdentifier( - baseComponent.parentComposantId, + parentComponentId: resolveIdentifier( + appliedPiece.parentComponentId, link.parentComponentId, ), - parentRequirementId: resolveIdentifier( - baseComponent.parentRequirementId, - link.parentRequirementId, - ), - parentMachineComponentRequirementId: resolveIdentifier( - baseComponent.parentMachineComponentRequirementId, - link.parentMachineComponentRequirementId, - ), - parentMachinePieceRequirementId: resolveIdentifier( - baseComponent.parentMachinePieceRequirementId, - link.parentMachinePieceRequirementId, - ), - typeMachineComponentRequirement: - link.requirement - || link.typeMachineComponentRequirement - || baseComponent.typeMachineComponentRequirement - || null, - typeMachineComponentRequirementId: resolveIdentifier( - link.requirementId, - link.typeMachineComponentRequirementId, - (link.requirement || link.typeMachineComponentRequirement)?.id, - baseComponent.typeMachineComponentRequirementId, - ), - definition: baseComponent.definition || {}, - pieces: [], - subComponents: [], - sousComposants: [], - } - - if (!node.id) { - node.id = resolveIdentifier( - baseComponent.id, - node.composantId, - link.composantId, - link.componentId, - `component-${index}`, - ) - } - - node.requirementId = node.typeMachineComponentRequirementId - node.machineComponentLinkOverrides = link.overrides || null - node.overrides = link.overrides || null - node.definitionOverrides = link.overrides || null - node.subcomponents = node.subComponents - - componentMap.set(node.machineComponentLinkId || node.id, node) - }) - - componentMap.forEach((node) => { - const parentLinkId = resolveIdentifier(node.parentComponentLinkId) - if (parentLinkId && componentMap.has(parentLinkId)) { - const parent = componentMap.get(parentLinkId) - parent.subComponents.push(node) - parent.sousComposants = parent.subComponents - parent.subcomponents = parent.subComponents - node.parentComposantId = resolveIdentifier( - node.parentComposantId, - parent.composantId, - parent.id, - ) - } else { - componentRoots.push(node) - } - }) - - const machinePieces = [] - - pieceLinks.forEach((link, index) => { - if (!isPlainObject(link)) { - return - } - - const basePiece = isPlainObject(link.piece) - ? link.piece - : isPlainObject(link.targetPiece) - ? link.targetPiece - : isPlainObject(link.pieceModel) - ? link.pieceModel - : {} - - const linkId = resolveIdentifier(link.id, link.linkId, link.machinePieceLinkId) - const parentComponentLinkId = resolveIdentifier( - link.parentComponentLinkId, - link.parentLinkId, - link.parentMachineComponentLinkId, - basePiece.parentComponentLinkId, - ) - - const pieceEntry = { - ...basePiece, - id: resolveIdentifier(basePiece.id, link.pieceId, linkId, `piece-${index}`), - pieceId: resolveIdentifier(basePiece.id, link.pieceId), - machinePieceLink: link, - machinePieceLinkId: linkId, - linkId, - parentComponentLinkId, + parentComponentName, parentLinkId: resolveIdentifier( link.parentLinkId, link.parentMachinePieceLinkId, - basePiece.parentLinkId, + appliedPiece.parentLinkId, ), parentPieceLinkId: resolveIdentifier( link.parentPieceLinkId, - basePiece.parentPieceLinkId, + appliedPiece.parentPieceLinkId, ), parentPieceId: resolveIdentifier( - basePiece.parentPieceId, + appliedPiece.parentPieceId, link.parentPieceId, ), - parentComponentId: resolveIdentifier( - basePiece.parentComponentId, + parentMachineComponentRequirementId: resolveIdentifier( + appliedPiece.parentMachineComponentRequirementId, + link.parentMachineComponentRequirementId, + ), + parentMachinePieceRequirementId: resolveIdentifier( + appliedPiece.parentMachinePieceRequirementId, + link.parentMachinePieceRequirementId, + ), + definition: appliedPiece.definition || originalPiece?.definition || {}, + customFields: appliedPiece.customFields || [], + skeletonOnly: !pieceId, + } + + return basePiece + } + + const createComponentNode = (link) => { + if (!link || typeof link !== 'object') { + return null + } + + const appliedComponent = + (link.composant && typeof link.composant === 'object' && link.composant) || {} + const originalComponent = + (link.originalComposant && typeof link.originalComposant === 'object' && link.originalComposant) || null + + const requirement = + link.typeMachineComponentRequirement || + appliedComponent.typeMachineComponentRequirement || + originalComponent?.typeMachineComponentRequirement || + null + + const machineComponentLinkId = normalizeComponentLinkId(link) + const composantId = resolveIdentifier( + appliedComponent.id, + appliedComponent.composantId, + link.composantId, + ) + + const componentName = + link.overrides?.name || + appliedComponent.name || + appliedComponent.definition?.alias || + appliedComponent.definition?.name || + originalComponent?.name || + 'Composant' + + const pieces = Array.isArray(link.pieceLinks) + ? link.pieceLinks.map((pieceLink) => createPieceNode(pieceLink, componentName)).filter(Boolean) + : [] + + const subComponents = Array.isArray(link.childLinks) + ? link.childLinks.map(createComponentNode).filter(Boolean) + : [] + + const baseComponent = { + ...appliedComponent, + id: appliedComponent.id || composantId || machineComponentLinkId || `component-${machineComponentLinkId}`, + composantId, + name: componentName, + reference: + link.overrides?.reference || + appliedComponent.reference || + appliedComponent.definition?.reference || + originalComponent?.reference || + null, + prix: + link.overrides?.prix ?? + appliedComponent.prix ?? + originalComponent?.prix ?? + null, + constructeur: + appliedComponent.constructeur || + originalComponent?.constructeur || + null, + constructeurId: + appliedComponent.constructeurId || + appliedComponent.constructeur?.id || + originalComponent?.constructeurId || + null, + documents: + Array.isArray(appliedComponent.documents) + ? appliedComponent.documents + : Array.isArray(originalComponent?.documents) + ? originalComponent.documents + : [], + typeComposant: + appliedComponent.typeComposant || + requirement?.typeComposant || + null, + typeComposantId: + appliedComponent.typeComposantId || + appliedComponent.typeComposant?.id || + requirement?.typeComposantId || + requirement?.typeComposant?.id || + null, + typeMachineComponentRequirement: requirement, + typeMachineComponentRequirementId: requirement?.id || null, + requirementId: requirement?.id || null, + overrides: link.overrides || null, + machineComponentLinkOverrides: link.overrides || null, + definitionOverrides: link.overrides || null, + originalComposant: originalComponent, + machineComponentLink: link, + machineComponentLinkId, + componentLinkId: machineComponentLinkId, + parentComponentLinkId: resolveIdentifier( + link.parentComponentLinkId, + link.parentLinkId, + link.parentMachineComponentLinkId, + appliedComponent.parentComponentLinkId, + ), + parentComposantId: resolveIdentifier( + appliedComponent.parentComposantId, link.parentComponentId, ), - composantId: resolveIdentifier( - basePiece.composantId, - basePiece.componentId, - link.composantId, - link.componentId, + parentRequirementId: resolveIdentifier( + appliedComponent.parentRequirementId, + link.parentRequirementId, ), - typeMachinePieceRequirement: - link.requirement - || link.typeMachinePieceRequirement - || basePiece.typeMachinePieceRequirement - || null, + parentMachineComponentRequirementId: resolveIdentifier( + appliedComponent.parentMachineComponentRequirementId, + link.parentMachineComponentRequirementId, + ), + parentMachinePieceRequirementId: resolveIdentifier( + appliedComponent.parentMachinePieceRequirementId, + link.parentMachinePieceRequirementId, + ), + definition: appliedComponent.definition || originalComponent?.definition || {}, + customFields: appliedComponent.customFields || [], + pieces, + subComponents, + subcomponents: subComponents, + sousComposants: subComponents, + skeletonOnly: !composantId, } - pieceEntry.typeMachinePieceRequirementId = resolveIdentifier( - link.requirementId, - link.typeMachinePieceRequirementId, - pieceEntry.typeMachinePieceRequirement?.id, - basePiece.typeMachinePieceRequirementId, - ) - pieceEntry.parentMachineComponentRequirementId = resolveIdentifier( - basePiece.parentMachineComponentRequirementId, - link.parentMachineComponentRequirementId, - ) - pieceEntry.parentMachinePieceRequirementId = resolveIdentifier( - basePiece.parentMachinePieceRequirementId, - link.parentMachinePieceRequirementId, - ) - pieceEntry.definition = basePiece.definition || {} - pieceEntry.overrides = link.overrides || null - if (!pieceEntry.name && link.overrides?.name) { - pieceEntry.name = link.overrides.name - } + return baseComponent + } - if (parentComponentLinkId && componentMap.has(parentComponentLinkId)) { - const parent = componentMap.get(parentComponentLinkId) - parent.pieces.push(pieceEntry) - pieceEntry.parentComponentName = parent.name || parent.nom || null - } else { - machinePieces.push(pieceEntry) - } - }) + const rootComponents = (Array.isArray(componentLinks) ? componentLinks : []) + .filter((link) => + !resolveIdentifier( + link?.parentComponentLinkId, + link?.parentLinkId, + link?.parentMachineComponentLinkId, + ), + ) + .map(createComponentNode) + .filter(Boolean) - componentMap.forEach((node) => { - node.sousComposants = node.subComponents - node.subcomponents = node.subComponents - }) + const machinePieces = (Array.isArray(pieceLinks) ? pieceLinks : []) + .filter((link) => + !resolveIdentifier( + link?.parentComponentLinkId, + link?.parentLinkId, + link?.parentMachineComponentLinkId, + ), + ) + .map((link) => createPieceNode(link, null)) + .filter(Boolean) return { - components: componentRoots, + components: rootComponents, machinePieces, } } diff --git a/app/pages/machines/new.vue b/app/pages/machines/new.vue index e53cbb4..2613f7a 100644 --- a/app/pages/machines/new.vue +++ b/app/pages/machines/new.vue @@ -203,18 +203,7 @@ Constructeur : {{ findComponentById(entry.composantId)?.constructeur?.name || findComponentById(entry.composantId)?.constructeurName || "—" }} -
- Machines liées : - {{ formatAssignmentList(getComponentMachineAssignments(findComponentById(entry.composantId))) || 'Aucune' }} -
-
- Ce composant est déjà lié à - {{ formatAssignmentList(getComponentMachineAssignments(findComponentById(entry.composantId))) }}. - La création ajoutera un nouveau lien. -
+ @@ -321,20 +310,7 @@ Constructeur : {{ findPieceById(entry.pieceId)?.constructeur?.name || findPieceById(entry.pieceId)?.constructeurName || "—" }} -
- Machines liées : - {{ formatAssignmentList(getPieceMachineAssignments(findPieceById(entry.pieceId))) || 'Aucune' }} -
-
- Composants liés : - {{ formatAssignmentList(getPieceComponentAssignments(findPieceById(entry.pieceId))) || 'Aucun' }} -
-
- Cette pièce dispose déjà de liaisons existantes. La création ajoutera un nouveau lien. -
+ @@ -1353,18 +1329,8 @@ const machinePreview = computed(() => { issues.push({ message: 'Sélectionner un composant pour chaque entrée.', kind: 'error', anchor: `component-group-${requirement.id}` }) } - normalizedEntries.forEach((entrySummary) => { - if (entrySummary.assignmentLabel) { - issues.push({ - message: `Le composant "${entrySummary.title}" est déjà lié à ${entrySummary.assignmentLabel}.`, - kind: 'warning', - anchor: `component-group-${requirement.id}`, - }) - } - }) - const hasErrors = issues.some(issue => issue.kind === 'error') - const hasWarnings = issues.some(issue => issue.kind === 'warning') || completed < entries.length + const hasWarnings = completed < entries.length const status = hasErrors ? 'error' @@ -1441,25 +1407,8 @@ const machinePreview = computed(() => { issues.push({ message: 'Sélectionner une pièce pour chaque entrée.', kind: 'error', anchor: `piece-group-${requirement.id}` }) } - normalizedEntries.forEach((entrySummary) => { - if (entrySummary.machineAssignmentLabel) { - issues.push({ - message: `La pièce "${entrySummary.title}" est déjà liée aux machines ${entrySummary.machineAssignmentLabel}.`, - kind: 'warning', - anchor: `piece-group-${requirement.id}`, - }) - } - if (entrySummary.componentAssignmentLabel) { - issues.push({ - message: `La pièce "${entrySummary.title}" est déjà rattachée aux composants ${entrySummary.componentAssignmentLabel}.`, - kind: 'warning', - anchor: `piece-group-${requirement.id}`, - }) - } - }) - const hasErrors = issues.some(issue => issue.kind === 'error') - const hasWarnings = issues.some(issue => issue.kind === 'warning') || completed < entries.length + const hasWarnings = completed < entries.length const status = hasErrors ? 'error'