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 @@
+
+
+
+
+
+ {{ requirementLabel }}
+
+
+ {{ requirementDescription }}
+
+
+
+
+
+ Sélectionner un composant
+
+
+
+ {{ componentOptions.length ? 'Choisir un composant compatible' : 'Aucun composant disponible' }}
+
+
+ {{ formatComponentOption(component) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ describePieceRequirement(pieceAssignment.definition) }}
+
+
+ Aucune pièce disponible pour cette famille.
+
+
+
+
+
+ {{ getPieceOptions(pieceAssignment.definition).length ? 'Choisir une pièce' : 'Sélection impossible' }}
+
+
+ {{ formatPieceOption(piece) }}
+
+
+
+
+
+
+
+
+
+
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
-
+
Créer un type
@@ -51,23 +46,29 @@
- {{ type.componentRequirements?.length || 0 }} famille(s) de composants
+ {{ type.componentRequirements?.length || 0 }} famille(s) de
+ composants
- {{ type.pieceRequirements?.length || 0 }} groupe(s) de pièces
+ {{ type.pieceRequirements?.length || 0 }} groupe(s) de
+ pièces
-
+
Supprimer
Voir détails
-
- Utiliser
-
@@ -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'