feat: improve machine component hierarchy handling

This commit is contained in:
Matthieu
2025-10-13 09:01:19 +02:00
parent 95c2a82689
commit 06ae0ca7aa
9 changed files with 1184 additions and 408 deletions

View File

@@ -25,6 +25,12 @@
{{ component.name }}
</h3>
<div class="flex flex-wrap gap-2 mt-2">
<span
v-if="component.skeletonOnly"
class="badge badge-warning badge-sm"
>
Défini dans le catalogue
</span>
<span v-if="component.reference" class="badge badge-outline badge-sm">{{ component.reference }}</span>
<span v-if="component.constructeur" class="badge badge-outline badge-sm">{{ component.constructeur?.name }}</span>
<span v-if="component.prix" class="badge badge-primary badge-sm">{{ component.prix }}</span>
@@ -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,
),
),
)

View File

@@ -0,0 +1,312 @@
<template>
<div :class="wrapperClass">
<section v-if="!isRoot" class="rounded-lg border border-base-200 bg-base-100 p-4 space-y-3">
<div class="space-y-1">
<h4 class="text-sm font-semibold text-base-content">
{{ requirementLabel }}
</h4>
<p class="text-xs text-base-content/70">
{{ requirementDescription }}
</p>
</div>
<div class="form-control">
<label class="label">
<span class="label-text text-xs">Sélectionner un composant</span>
</label>
<select
v-model="assignment.selectedComponentId"
class="select select-bordered select-sm"
>
<option value="">
{{ componentOptions.length ? 'Choisir un composant compatible' : 'Aucun composant disponible' }}
</option>
<option
v-for="component in componentOptions"
:key="component.id"
:value="component.id"
>
{{ formatComponentOption(component) }}
</option>
</select>
</div>
</section>
<section v-if="assignment.pieces.length" class="rounded-lg border border-dashed border-base-300 bg-base-200/40 p-4 space-y-4">
<header class="space-y-1">
<h4 class="text-sm font-semibold text-base-content">
{{ isRoot ? 'Pièces requises par le squelette' : 'Pièces associées à ce sous-composant' }}
</h4>
<p class="text-xs text-base-content/70">
Sélectionnez les pièces concrètes à associer pour chaque emplacement.
</p>
</header>
<div
v-for="pieceAssignment in assignment.pieces"
:key="pieceAssignment.path"
class="rounded-md border border-base-200 bg-base-100 p-3 space-y-2"
>
<div class="space-y-1">
<p class="text-xs font-medium text-base-content">
{{ describePieceRequirement(pieceAssignment.definition) }}
</p>
<p v-if="!getPieceOptions(pieceAssignment.definition).length" class="text-[11px] text-error">
Aucune pièce disponible pour cette famille.
</p>
</div>
<select
v-model="pieceAssignment.selectedPieceId"
class="select select-bordered select-xs"
>
<option value="">
{{ getPieceOptions(pieceAssignment.definition).length ? 'Choisir une pièce' : 'Sélection impossible' }}
</option>
<option
v-for="piece in getPieceOptions(pieceAssignment.definition)"
:key="piece.id"
:value="piece.id"
>
{{ formatPieceOption(piece) }}
</option>
</select>
</div>
</section>
<section v-if="assignment.subcomponents.length" class="space-y-4">
<header class="space-y-1">
<h4 class="text-sm font-semibold text-base-content">
{{ isRoot ? 'Sous-composants définis par le squelette' : 'Sous-composants imbriqués' }}
</h4>
<p class="text-xs text-base-content/70">
Choisissez un composant existant pour chaque sous-niveau requis.
</p>
</header>
<ComponentStructureAssignmentNode
v-for="subAssignment in assignment.subcomponents"
:key="subAssignment.path"
:assignment="subAssignment"
:pieces="pieces"
:components="components"
:depth="depth + 1"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, watch } from 'vue';
import type {
ComponentModelPiece,
ComponentModelStructureNode,
} from '~/shared/types/inventory';
interface ComponentOption {
id: string;
name?: string | null;
reference?: string | null;
typeComposantId?: string | null;
typeComposant?: {
id: string;
name?: string | null;
code?: string | null;
} | null;
}
interface PieceOption {
id: string;
name?: string | null;
reference?: string | null;
typePieceId?: string | null;
typePiece?: {
id: string;
name?: string | null;
code?: string | null;
} | null;
}
export interface StructurePieceAssignment {
path: string;
definition: ComponentModelPiece;
selectedPieceId: string;
}
export interface StructureAssignmentNode {
path: string;
definition: ComponentModelStructureNode;
selectedComponentId: string;
pieces: StructurePieceAssignment[];
subcomponents: StructureAssignmentNode[];
}
const props = withDefaults(
defineProps<{
assignment: StructureAssignmentNode;
pieces: PieceOption[] | null;
components: ComponentOption[] | null;
depth?: number;
}>(),
{
depth: 0,
pieces: () => [],
components: () => [],
},
);
const depth = computed(() => props.depth ?? 0);
const isRoot = computed(() => depth.value === 0);
const wrapperClass = computed(() =>
depth.value === 0 ? 'space-y-6' : 'space-y-6 border-l border-base-300 pl-4',
);
const componentOptions = computed(() => {
if (isRoot.value) {
return [];
}
const definition = props.assignment.definition || {};
const requiredTypeId =
definition.typeComposantId || definition.modelId || null;
const requiredFamilyCode = definition.familyCode || null;
return (props.components || []).filter((component) => {
if (!component || typeof component !== 'object') {
return false;
}
if (requiredTypeId) {
return component.typeComposantId === requiredTypeId;
}
if (requiredFamilyCode) {
return (
component.typeComposant?.code === requiredFamilyCode ||
component.typeComposantId === requiredFamilyCode
);
}
return true;
});
});
watch(
componentOptions,
(options) => {
if (isRoot.value) {
return;
}
const hasMatch = options.some(
(component) => component.id === props.assignment.selectedComponentId,
);
if (!hasMatch) {
props.assignment.selectedComponentId = '';
}
},
{ immediate: true },
);
const formatComponentOption = (component: ComponentOption) => {
const name = component.name || 'Composant sans nom';
const reference = component.reference ? ` • Ref. ${component.reference}` : '';
const typeLabel =
component.typeComposant?.name || component.typeComposant?.code || '';
const typed =
typeLabel && component.typeComposant?.name !== name
? ` (${typeLabel})`
: '';
return `${name}${typed}${reference}`;
};
const describePieceRequirement = (definition: ComponentModelPiece) => {
const parts: string[] = [];
if (definition.role) {
parts.push(definition.role);
}
if (definition.typePieceLabel) {
parts.push(definition.typePieceLabel);
} else if ((definition as any).typePiece?.name) {
parts.push((definition as any).typePiece.name);
} else if (definition.familyCode) {
parts.push(definition.familyCode);
}
return parts.length ? parts.join(' • ') : 'Pièce du squelette';
};
const requirementLabel = computed(() => {
const definition = props.assignment.definition || {};
const alias = definition.alias || definition.typeComposantLabel;
if (alias) {
return alias;
}
if (definition.typeComposant?.name) {
return definition.typeComposant.name;
}
if (definition.familyCode) {
return `Famille ${definition.familyCode}`;
}
return 'Sous-composant';
});
const requirementDescription = computed(() => {
const definition = props.assignment.definition || {};
const family =
definition.typeComposantLabel ||
definition.typeComposant?.name ||
definition.familyCode;
if (family) {
return `Doit appartenir à la famille "${family}".`;
}
return 'Sélectionnez un composant enfant conforme à cette position.';
});
const getPieceOptions = (definition: ComponentModelPiece) => {
const requiredTypeId =
definition.typePieceId ||
(definition as any).typePiece?.id ||
definition.familyCode ||
null;
return (props.pieces || []).filter((piece) => {
if (!piece || typeof piece !== 'object') {
return false;
}
if (!requiredTypeId) {
return true;
}
if (definition.typePieceId || (definition as any).typePiece?.id) {
return (
piece.typePieceId === requiredTypeId ||
piece.typePiece?.id === requiredTypeId
);
}
if (definition.familyCode) {
return (
piece.typePiece?.code === requiredTypeId ||
piece.typePieceId === requiredTypeId
);
}
return false;
});
};
const formatPieceOption = (piece: PieceOption) => {
const name = piece.name || 'Pièce';
const reference = piece.reference ? ` • Ref. ${piece.reference}` : '';
return `${name}${reference}`;
};
watch(
() => [props.pieces, props.assignment.pieces],
() => {
for (const pieceAssignment of props.assignment.pieces) {
const options = getPieceOptions(pieceAssignment.definition);
if (
pieceAssignment.selectedPieceId &&
!options.some((piece) => piece.id === pieceAssignment.selectedPieceId)
) {
pieceAssignment.selectedPieceId = '';
}
}
},
{ deep: true, immediate: true },
);
</script>

View File

@@ -25,6 +25,12 @@
</div>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs">
<span
v-if="piece.skeletonOnly"
class="badge badge-warning badge-sm"
>
Défini dans le catalogue
</span>
<span
v-if="piece.typeMachinePieceRequirement"
class="badge badge-outline badge-sm"
@@ -387,6 +393,33 @@ function fieldKeyFromNameAndType(name, 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);
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,
),
),
);

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -156,6 +156,45 @@
</details>
</div>
<div
v-if="structureHasRequirements"
class="space-y-4 rounded-lg border border-primary/30 bg-primary/5 p-4"
>
<div class="flex items-start justify-between gap-4">
<div>
<h2 class="font-semibold text-base-content">
Sélection des éléments du squelette
</h2>
<p class="text-xs text-base-content/70">
Affectez les pièces et sous-composants concrets correspondant à la catégorie choisie.
</p>
</div>
<span
class="badge"
:class="structureSelectionsComplete ? 'badge-success' : structureDataLoading ? 'badge-info' : 'badge-warning'"
>
{{ structureSelectionsComplete ? 'Complet' : structureDataLoading ? 'Chargement…' : 'Incomplet' }}
</span>
</div>
<div
v-if="structureDataLoading"
class="flex items-center gap-3 rounded-md border border-base-200 bg-base-100 p-3 text-sm text-base-content/70"
>
<span class="loading loading-spinner loading-sm" aria-hidden="true"></span>
Chargement du catalogue de pièces et de composants
</div>
<ComponentStructureAssignmentNode
v-else-if="structureAssignments"
:assignment="structureAssignments"
:pieces="availablePieces"
:components="availableComponents"
/>
<p v-else class="text-xs text-error">
Impossible de générer les emplacements définis par le squelette.
</p>
</div>
<div v-if="customFieldInputs.length" class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
<header class="space-y-1">
<h2 class="font-semibold text-base-content">Champs personnalisés</h2>
@@ -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<CustomFieldInput[]>([])
const structureAssignments = ref<StructureAssignmentNode | null>(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<string, any>) =>
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<string, any> => {
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<string, any> = {
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 {

View File

@@ -4,14 +4,9 @@
<div class="my-8">
<!-- Header with Add Button -->
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-800">
Squelettes de machine
</h2>
<h2 class="text-2xl font-bold text-gray-800">Squelettes de machine</h2>
<NuxtLink to="/machine-skeleton/new" class="btn btn-primary">
<IconLucidePlus
class="w-5 h-5 mr-2"
aria-hidden="true"
/>
<IconLucidePlus class="w-5 h-5 mr-2" aria-hidden="true" />
Créer un type
</NuxtLink>
</div>
@@ -51,23 +46,29 @@
<div class="space-y-2 text-sm text-gray-500">
<div class="flex items-center gap-2">
<IconLucidePackage class="w-4 h-4" aria-hidden="true" />
<span>{{ type.componentRequirements?.length || 0 }} famille(s) de composants</span>
<span
>{{ type.componentRequirements?.length || 0 }} famille(s) de
composants</span
>
</div>
<div class="flex items-center gap-2">
<IconLucideLayoutGrid class="w-4 h-4" aria-hidden="true" />
<span>{{ type.pieceRequirements?.length || 0 }} groupe(s) de pièces</span>
<span
>{{ type.pieceRequirements?.length || 0 }} groupe(s) de
pièces</span
>
</div>
</div>
<div class="card-actions justify-end mt-4">
<button class="btn btn-sm btn-error" @click.stop="confirmDeleteType(type)">
<button
class="btn btn-sm btn-error"
@click.stop="confirmDeleteType(type)"
>
Supprimer
</button>
<NuxtLink :to="`/type/${type.id}`" class="btn btn-sm btn-outline">
Voir détails
</NuxtLink>
<button class="btn btn-sm btn-primary">
Utiliser
</button>
</div>
</div>
</div>
@@ -92,52 +93,59 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
import { useToast } from '~/composables/useToast'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucidePackage from '~icons/lucide/package'
import IconLucideLayoutGrid from '~icons/lucide/layout-grid'
import { ref, computed, onMounted } from "vue";
import { useMachineTypesApi } from "~/composables/useMachineTypesApi";
import { useToast } from "~/composables/useToast";
import IconLucidePlus from "~icons/lucide/plus";
import IconLucidePackage from "~icons/lucide/package";
import IconLucideLayoutGrid from "~icons/lucide/layout-grid";
const { machineTypes, loading, loadMachineTypes, deleteMachineType } = useMachineTypesApi()
const { machineTypes, loading, loadMachineTypes, deleteMachineType } =
useMachineTypesApi();
const categories = ref([
'Toutes',
'Production',
'Transformation',
'Manutention',
'Traitement',
'Contrôle'
])
"Toutes",
"Production",
"Transformation",
"Manutention",
"Traitement",
"Contrôle",
]);
const selectedCategory = ref('Toutes')
const selectedCategory = ref("Toutes");
const filteredTypes = computed(() => {
if (selectedCategory.value === 'Toutes') {
return machineTypes.value
if (selectedCategory.value === "Toutes") {
return machineTypes.value;
}
return machineTypes.value.filter(type => type.category === selectedCategory.value)
})
return machineTypes.value.filter(
(type) => type.category === selectedCategory.value
);
});
const confirmDeleteType = async (type) => {
const { showError, showSuccess } = useToast()
const { showError, showSuccess } = useToast();
if (confirm(`Êtes-vous sûr de vouloir supprimer le type "${type.name}" ? Cette action est irréversible.`)) {
if (
confirm(
`Êtes-vous sûr de vouloir supprimer le type "${type.name}" ? Cette action est irréversible.`
)
) {
try {
const result = await deleteMachineType(type.id)
const result = await deleteMachineType(type.id);
if (result.success) {
showSuccess(`Type "${type.name}" supprimé avec succès`)
showSuccess(`Type "${type.name}" supprimé avec succès`);
} else {
showError(`Erreur lors de la suppression: ${result.error}`)
showError(`Erreur lors de la suppression: ${result.error}`);
}
} catch (error) {
showError(`Erreur lors de la suppression: ${error.message}`)
showError(`Erreur lors de la suppression: ${error.message}`);
}
}
}
};
// Load machine types on mount
onMounted(async () => {
await loadMachineTypes()
})
await loadMachineTypes();
});
</script>

View File

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

View File

@@ -203,18 +203,7 @@
Constructeur :
{{ findComponentById(entry.composantId)?.constructeur?.name || findComponentById(entry.composantId)?.constructeurName || "—" }}
</div>
<div>
Machines liées :
{{ formatAssignmentList(getComponentMachineAssignments(findComponentById(entry.composantId))) || 'Aucune' }}
</div>
<div
v-if="formatAssignmentList(getComponentMachineAssignments(findComponentById(entry.composantId)))"
class="text-warning mt-1"
>
Ce composant est déjà lié à
{{ formatAssignmentList(getComponentMachineAssignments(findComponentById(entry.composantId))) }}.
La création ajoutera un nouveau lien.
</div>
</div>
</div>
</div>
@@ -321,20 +310,7 @@
Constructeur :
{{ findPieceById(entry.pieceId)?.constructeur?.name || findPieceById(entry.pieceId)?.constructeurName || "—" }}
</div>
<div>
Machines liées :
{{ formatAssignmentList(getPieceMachineAssignments(findPieceById(entry.pieceId))) || 'Aucune' }}
</div>
<div>
Composants liés :
{{ formatAssignmentList(getPieceComponentAssignments(findPieceById(entry.pieceId))) || 'Aucun' }}
</div>
<div
v-if="formatAssignmentList(getPieceMachineAssignments(findPieceById(entry.pieceId))) || formatAssignmentList(getPieceComponentAssignments(findPieceById(entry.pieceId)))"
class="text-warning mt-1"
>
Cette pièce dispose déjà de liaisons existantes. La création ajoutera un nouveau lien.
</div>
</div>
</div>
</div>
@@ -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'