wip: machine create skeleton links

This commit is contained in:
Matthieu
2026-01-24 00:58:06 +01:00
parent a8cb4d1ac0
commit 1f5f1509a9
3 changed files with 209 additions and 23 deletions

View File

@@ -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<Record<string, string>>({})
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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()

View File

@@ -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<Record<string, string>>({})
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
const parts: string[] = []
if (product.role) {

View File

@@ -273,18 +273,19 @@
</label>
<SearchSelect
:model-value="entry.pieceId || ''"
:options="getPieceOptions(requirement, entry)"
:loading="piecesLoading"
:options="getPieceOptions(requirement, entry, entryIndex)"
:loading="piecesLoading || pieceLoadingByKey[getPieceKey(requirement, entryIndex)]"
size="sm"
placeholder="Rechercher une pièce…"
empty-text="Aucune pièce disponible"
:option-label="pieceOptionLabel"
:option-description="pieceOptionDescription"
@search="(term) => fetchPieceOptions(requirement, entryIndex, term)"
@update:modelValue="setPieceRequirementPiece(requirement, entryIndex, $event || '')"
/>
</div>
<p
v-if="getPieceOptions(requirement, entry).length === 0"
v-if="getPieceOptions(requirement, entry, entryIndex).length === 0"
class="text-xs text-error"
>
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 = ''