From 1f5f1509a95ecfe4e0a5f89beb67695dec9e6fbd Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sat, 24 Jan 2026 00:58:06 +0100 Subject: [PATCH] wip: machine create skeleton links --- app/pages/component/[id]/edit.vue | 52 +++++++++++- app/pages/component/create.vue | 48 ++++++++++- app/pages/machines/new.vue | 132 +++++++++++++++++++++++++----- 3 files changed, 209 insertions(+), 23 deletions(-) diff --git a/app/pages/component/[id]/edit.vue b/app/pages/component/[id]/edit.vue index b43c711..d0f38a5 100644 --- a/app/pages/component/[id]/edit.vue +++ b/app/pages/component/[id]/edit.vue @@ -400,6 +400,7 @@ import DocumentUpload from '~/components/DocumentUpload.vue' import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue' import { useComponentTypes } from '~/composables/useComponentTypes' import { useComposants } from '~/composables/useComposants' +import { usePieceTypes } from '~/composables/usePieceTypes' import { useCustomFields } from '~/composables/useCustomFields' import { useApi } from '~/composables/useApi' import { useToast } from '~/composables/useToast' @@ -434,6 +435,7 @@ const route = useRoute() const router = useRouter() const { get } = useApi() const { componentTypes, loadComponentTypes } = useComponentTypes() +const { pieceTypes, loadPieceTypes } = usePieceTypes() const { updateComposant } = useComposants() const { ensureConstructeurs } = useConstructeurs() const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields() @@ -500,6 +502,16 @@ const documentPreviewSrc = (document: any) => { } return document.path } + +const fetchedPieceTypeMap = ref>({}) +const pieceTypeLabelMap = computed(() => ({ + ...Object.fromEntries( + (pieceTypes.value || []) + .filter((type: any) => type?.id) + .map((type: any) => [type.id, type.name || type.code || '']), + ), + ...fetchedPieceTypeMap.value, +})) const documentThumbnailClass = (document: any) => { if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) { return 'h-24 w-20' @@ -1023,6 +1035,8 @@ const resolvePieceLabel = (piece: Record) => { parts.push(piece.typePiece.name) } else if (piece.typePieceLabel) { parts.push(piece.typePieceLabel) + } else if (piece.typePieceId && pieceTypeLabelMap.value[piece.typePieceId]) { + parts.push(pieceTypeLabelMap.value[piece.typePieceId]) } else if (piece.typePiece?.code) { parts.push(`Famille ${piece.typePiece.code}`) } else if (piece.familyCode) { @@ -1033,6 +1047,42 @@ const resolvePieceLabel = (piece: Record) => { return parts.length ? parts.join(' • ') : 'Pièce' } +const fetchPieceTypeNames = async (ids: string[]) => { + const missing = ids.filter((id) => id && !pieceTypeLabelMap.value[id]) + if (!missing.length) { + return + } + const results = await Promise.allSettled( + missing.map((id) => get(`/model_types/${id}`)), + ) + const next = { ...fetchedPieceTypeMap.value } + results.forEach((result, index) => { + if (result.status !== 'fulfilled') { + return + } + const data = result.value?.data + const name = data?.name || data?.code + if (name) { + next[missing[index]] = name + } + }) + fetchedPieceTypeMap.value = next +} + +watch( + selectedTypeStructure, + (structure) => { + const ids = getStructurePieces(structure) + .map((piece: any) => piece?.typePieceId) + .filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0) + if (!ids.length) { + return + } + fetchPieceTypeNames(Array.from(new Set(ids))).catch(() => {}) + }, + { immediate: true }, +) + const resolveSubcomponentLabel = (node: Record) => { const parts: string[] = [] if (node.alias) { @@ -1158,7 +1208,7 @@ const saveCustomFieldValues = async (updatedComponent: any) => { } onMounted(async () => { - await Promise.allSettled([loadComponentTypes(), fetchComponent()]) + await Promise.allSettled([loadComponentTypes(), loadPieceTypes(), fetchComponent()]) loading.value = false if (component.value?.id) { await refreshDocuments() diff --git a/app/pages/component/create.vue b/app/pages/component/create.vue index 4e3b860..1e024ef 100644 --- a/app/pages/component/create.vue +++ b/app/pages/component/create.vue @@ -355,6 +355,7 @@ import { usePieces } from '~/composables/usePieces' import { usePieceTypes } from '~/composables/usePieceTypes' import { useProducts } from '~/composables/useProducts' import { useProductTypes } from '~/composables/useProductTypes' +import { useApi } from '~/composables/useApi' import { useToast } from '~/composables/useToast' import { useCustomFields } from '~/composables/useCustomFields' import { useDocuments } from '~/composables/useDocuments' @@ -375,6 +376,7 @@ interface ComponentCatalogType extends ModelType { const route = useRoute() const router = useRouter() +const { get } = useApi() const { componentTypes, loadComponentTypes, loadingComponentTypes } = useComponentTypes() const { pieceTypes, loadPieceTypes } = usePieceTypes() @@ -418,13 +420,15 @@ const structureDataLoading = computed( () => piecesLoading.value || componentsLoading.value || productsLoading.value, ) -const pieceTypeLabelMap = computed(() => - Object.fromEntries( +const fetchedPieceTypeMap = ref>({}) +const pieceTypeLabelMap = computed(() => ({ + ...Object.fromEntries( (pieceTypes.value || []) .filter((type: any) => type?.id) .map((type: any) => [type.id, type.name || type.code || '']), ), -) + ...fetchedPieceTypeMap.value, +})) const productTypeLabelMap = computed(() => Object.fromEntries( (productTypes.value || []) @@ -804,6 +808,8 @@ const resolvePieceLabel = (piece: Record) => { parts.push(piece.typePiece.name) } else if (piece.typePieceLabel) { parts.push(piece.typePieceLabel) + } else if (piece.typePieceId && pieceTypeLabelMap.value[piece.typePieceId]) { + parts.push(pieceTypeLabelMap.value[piece.typePieceId]) } else if (piece.typePiece?.code) { parts.push(`Famille ${piece.typePiece.code}`) } else if (piece.familyCode) { @@ -814,6 +820,42 @@ const resolvePieceLabel = (piece: Record) => { return parts.length ? parts.join(' • ') : 'Pièce' } +const fetchPieceTypeNames = async (ids: string[]) => { + const missing = ids.filter((id) => id && !pieceTypeLabelMap.value[id]) + if (!missing.length) { + return + } + const results = await Promise.allSettled( + missing.map((id) => get(`/model_types/${id}`)), + ) + const next = { ...fetchedPieceTypeMap.value } + results.forEach((result, index) => { + if (result.status !== 'fulfilled') { + return + } + const data = result.value?.data + const name = data?.name || data?.code + if (name) { + next[missing[index]] = name + } + }) + fetchedPieceTypeMap.value = next +} + +watch( + selectedTypeStructure, + (structure) => { + const ids = getStructurePieces(structure) + .map((piece: any) => piece?.typePieceId) + .filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0) + if (!ids.length) { + return + } + fetchPieceTypeNames(Array.from(new Set(ids))).catch(() => {}) + }, + { immediate: true }, +) + const resolveProductLabel = (product: Record) => { const parts: string[] = [] if (product.role) { diff --git a/app/pages/machines/new.vue b/app/pages/machines/new.vue index c88be5f..7fbe50c 100644 --- a/app/pages/machines/new.vue +++ b/app/pages/machines/new.vue @@ -273,18 +273,19 @@

Aucune pièce disponible pour cette famille. @@ -743,6 +744,7 @@ import { useMachineTypesApi } from '~/composables/useMachineTypesApi' import { useComposants } from '~/composables/useComposants' import { usePieces } from '~/composables/usePieces' import { useProducts } from '~/composables/useProducts' +import { useApi } from '~/composables/useApi' import { useToast } from '~/composables/useToast' import { sanitizeDefinitionOverrides } from '~/shared/modelUtils' import SearchSelect from '~/components/common/SearchSelect.vue' @@ -754,12 +756,13 @@ import IconLucideAlertTriangle from '~icons/lucide/alert-triangle' import IconLucideCheckCircle2 from '~icons/lucide/check-circle-2' import IconLucideCircle from '~icons/lucide/circle' -const { createMachine, createMachineFromType } = useMachines() +const { createMachine, createMachineFromType, reconfigureSkeleton } = useMachines() const { sites, loadSites } = useSites() const { machineTypes, loadMachineTypes, loading: machineTypesLoading } = useMachineTypesApi() const { composants, loadComposants, loading: composantsLoading } = useComposants() const { pieces, loadPieces, loading: piecesLoading } = usePieces() const { products, loadProducts, loading: productsLoading } = useProducts() +const { get } = useApi() const toast = useToast() const submitting = ref(false) @@ -842,6 +845,85 @@ const productById = computed(() => { return map }) +const pieceOptionsByKey = ref({}) +const pieceLoadingByKey = ref({}) + +const extractCollection = (payload) => { + if (Array.isArray(payload)) { + return payload + } + if (Array.isArray(payload?.member)) { + return payload.member + } + if (Array.isArray(payload?.['hydra:member'])) { + return payload['hydra:member'] + } + if (Array.isArray(payload?.data)) { + return payload.data + } + return [] +} + +const getPieceKey = (requirement, entryIndex) => `${requirement?.id || 'req'}:${entryIndex}` + +const findPieceInCachedOptions = (id) => { + if (!id) { + return null + } + const buckets = Object.values(pieceOptionsByKey.value || {}) + for (const bucket of buckets) { + if (!Array.isArray(bucket)) { + continue + } + const found = bucket.find((piece) => piece?.id === id) + if (found) { + return found + } + } + return null +} + +const cachePieceIfMissing = (piece) => { + if (!piece?.id) { + return + } + if (pieceById.value.has(piece.id)) { + return + } + const current = Array.isArray(pieces.value) ? pieces.value : [] + pieces.value = [...current, piece] +} + +const fetchPieceOptions = async (requirement, entryIndex, term = '') => { + const key = getPieceKey(requirement, entryIndex) + if (pieceLoadingByKey.value[key]) { + return + } + + const requirementTypeId = requirement?.typePieceId || requirement?.typePiece?.id || null + const params = new URLSearchParams() + params.set('itemsPerPage', '50') + if (term && term.trim()) { + params.set('name', term.trim()) + } + if (requirementTypeId) { + params.set('typePiece', `/api/model_types/${requirementTypeId}`) + } + + pieceLoadingByKey.value = { ...pieceLoadingByKey.value, [key]: true } + try { + const result = await get(`/pieces?${params.toString()}`) + if (result.success) { + pieceOptionsByKey.value = { + ...pieceOptionsByKey.value, + [key]: extractCollection(result.data) + } + } + } finally { + pieceLoadingByKey.value = { ...pieceLoadingByKey.value, [key]: false } + } +} + const isPlainObject = value => value !== null && typeof value === 'object' && !Array.isArray(value) const toTrimmedString = (value) => { @@ -1077,7 +1159,12 @@ const getComponentOptions = (requirement, currentEntry) => { }) } -const getPieceOptions = (requirement, currentEntry) => { +const getPieceOptions = (requirement, currentEntry, entryIndex) => { + const key = getPieceKey(requirement, entryIndex) + const cached = pieceOptionsByKey.value[key] + if (cached) { + return cached + } const requirementTypeId = requirement?.typePieceId || requirement?.typePiece?.id || null const usedIds = new Set( selectedPieceIds.value.filter((id) => id && (!currentEntry || id !== currentEntry.pieceId)), @@ -1241,8 +1328,11 @@ const setPieceRequirementPiece = (requirement, index, pieceId) => { if (!entry) return entry.pieceId = pieceId || null if (pieceId) { - const piece = findPieceById(pieceId) + const piece = findPieceById(pieceId) || findPieceInCachedOptions(pieceId) entry.typePieceId = piece?.typePieceId || requirement?.typePieceId || null + if (piece) { + cachePieceIfMissing(piece) + } } else { entry.typePieceId = requirement?.typePieceId || null } @@ -1259,7 +1349,7 @@ const findPieceById = (id) => { if (!id) { return null } - return pieceById.value.get(id) || null + return pieceById.value.get(id) || findPieceInCachedOptions(id) || null } const findProductById = (id) => { @@ -1519,6 +1609,7 @@ const addPieceSelectionEntry = (requirement) => { ...entries, createPieceSelectionEntry(requirement), ] + fetchPieceOptions(requirement, entries.length).catch(() => {}) } const removePieceSelectionEntry = (requirementId, index) => { @@ -2096,6 +2187,9 @@ const initializeRequirementSelections = (type) => { const initialCount = Math.max(min, requirement.required ? 1 : 0) if (initialCount > 0) { pieceRequirementSelections[requirement.id] = Array.from({ length: initialCount }, () => createPieceSelectionEntry(requirement)) + pieceRequirementSelections[requirement.id].forEach((_, index) => { + fetchPieceOptions(requirement, index).catch(() => {}) + }) } else { pieceRequirementSelections[requirement.id] = [] } @@ -2158,22 +2252,22 @@ const finalizeMachineCreation = async () => { productLinks = validationResult.productLinks } - const payload = { - ...baseMachineData, - ...(hasRequirements - ? { - componentLinks, - pieceLinks, - productLinks - } - : {}) - } - const result = hasRequirements - ? await createMachine(payload) + ? await createMachine(baseMachineData) : await createMachineFromType(baseMachineData, type) if (result.success) { + if (hasRequirements && result.data?.id) { + const skeletonResult = await reconfigureSkeleton(result.data.id, { + componentLinks, + pieceLinks, + productLinks, + }) + if (!skeletonResult.success) { + toast.showError(skeletonResult.error || 'Impossible d\'enregistrer les pièces/composants') + return + } + } newMachine.name = '' newMachine.siteId = '' newMachine.typeMachineId = ''