Show component selections and support multi product requirements
This commit is contained in:
@@ -176,6 +176,18 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="getStructureProducts(selectedTypeStructure).length" class="space-y-2">
|
||||
<h3 class="font-semibold text-sm text-base-content">Produits imposés</h3>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li
|
||||
v-for="(product, index) in getStructureProducts(selectedTypeStructure)"
|
||||
:key="product.role || product.typeProductId || product.familyCode || index"
|
||||
>
|
||||
{{ resolveProductLabel(product) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="getStructureSubcomponents(selectedTypeStructure).length" class="space-y-2">
|
||||
<h3 class="font-semibold text-sm text-base-content">Sous-composants</h3>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
@@ -189,7 +201,7 @@
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="!getStructureCustomFields(selectedTypeStructure).length && !getStructurePieces(selectedTypeStructure).length && !getStructureSubcomponents(selectedTypeStructure).length"
|
||||
v-if="!getStructureCustomFields(selectedTypeStructure).length && !getStructurePieces(selectedTypeStructure).length && !getStructureProducts(selectedTypeStructure).length && !getStructureSubcomponents(selectedTypeStructure).length"
|
||||
class="text-xs text-gray-500"
|
||||
>
|
||||
Ce squelette ne définit pas encore de pièces, sous-composants ou valeurs par défaut.
|
||||
@@ -198,6 +210,50 @@
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="structureSelections.hasAny"
|
||||
class="space-y-3 rounded-lg border border-base-200 bg-base-200/30 p-4"
|
||||
>
|
||||
<header class="space-y-1">
|
||||
<h2 class="font-semibold text-base-content">Sélections actuelles</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Voici les pièces, produits et sous-composants réellement choisis pour ce composant.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div v-if="structureSelections.pieces.length" class="space-y-2">
|
||||
<h3 class="font-semibold text-sm text-base-content">Pièces choisies</h3>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
<li v-for="entry in structureSelections.pieces" :key="`selected-piece-${entry.path}-${entry.id}`">
|
||||
<span class="font-medium">{{ entry.resolvedName }}</span>
|
||||
<span class="text-xs text-base-content/70"> — {{ entry.requirementLabel }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="structureSelections.products.length" class="space-y-2">
|
||||
<h3 class="font-semibold text-sm text-base-content">Produits choisis</h3>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
<li v-for="entry in structureSelections.products" :key="`selected-product-${entry.path}-${entry.id}`">
|
||||
<span class="font-medium">{{ entry.resolvedName }}</span>
|
||||
<span class="text-xs text-base-content/70"> — {{ entry.requirementLabel }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="structureSelections.components.length" class="space-y-2">
|
||||
<h3 class="font-semibold text-sm text-base-content">Sous-composants choisis</h3>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
<li v-for="entry in structureSelections.components" :key="`selected-component-${entry.path}-${entry.id}`">
|
||||
<span class="font-medium">{{ entry.resolvedName }}</span>
|
||||
<span class="text-xs text-base-content/70"> — {{ entry.requirementLabel }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
@@ -401,6 +457,9 @@ import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
import { useComponentTypes } from '~/composables/useComponentTypes'
|
||||
import { useComposants } from '~/composables/useComposants'
|
||||
import { usePieceTypes } from '~/composables/usePieceTypes'
|
||||
import { useProductTypes } from '~/composables/useProductTypes'
|
||||
import { usePieces } from '~/composables/usePieces'
|
||||
import { useProducts } from '~/composables/useProducts'
|
||||
import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
@@ -436,7 +495,10 @@ const router = useRouter()
|
||||
const { get } = useApi()
|
||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||
const { updateComposant } = useComposants()
|
||||
const { productTypes, loadProductTypes } = useProductTypes()
|
||||
const { updateComposant, loadComposants, composants: componentCatalogRef } = useComposants()
|
||||
const { pieces, loadPieces } = usePieces()
|
||||
const { products, loadProducts } = useProducts()
|
||||
const { ensureConstructeurs } = useConstructeurs()
|
||||
const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields()
|
||||
const toast = useToast()
|
||||
@@ -512,6 +574,36 @@ const pieceTypeLabelMap = computed(() => ({
|
||||
),
|
||||
...fetchedPieceTypeMap.value,
|
||||
}))
|
||||
const fetchedProductTypeMap = ref<Record<string, string>>({})
|
||||
const productTypeLabelMap = computed(() => ({
|
||||
...Object.fromEntries(
|
||||
(productTypes.value || [])
|
||||
.filter((type: any) => type?.id)
|
||||
.map((type: any) => [type.id, type.name || type.code || '']),
|
||||
),
|
||||
...fetchedProductTypeMap.value,
|
||||
}))
|
||||
const pieceCatalogMap = computed(() =>
|
||||
new Map(
|
||||
(pieces.value || [])
|
||||
.filter((item: any) => item?.id)
|
||||
.map((item: any) => [String(item.id), item]),
|
||||
),
|
||||
)
|
||||
const productCatalogMap = computed(() =>
|
||||
new Map(
|
||||
(products.value || [])
|
||||
.filter((item: any) => item?.id)
|
||||
.map((item: any) => [String(item.id), item]),
|
||||
),
|
||||
)
|
||||
const componentCatalogMap = computed(() =>
|
||||
new Map(
|
||||
(componentCatalogRef.value || [])
|
||||
.filter((item: any) => item?.id)
|
||||
.map((item: any) => [String(item.id), item]),
|
||||
),
|
||||
)
|
||||
const documentThumbnailClass = (document: any) => {
|
||||
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||
return 'h-24 w-20'
|
||||
@@ -1018,6 +1110,10 @@ const getStructurePieces = (structure: ComponentModelStructure | null) => {
|
||||
return Array.isArray(structure?.pieces) ? structure.pieces : []
|
||||
}
|
||||
|
||||
const getStructureProducts = (structure: ComponentModelStructure | null) => {
|
||||
return Array.isArray(structure?.products) ? structure.products : []
|
||||
}
|
||||
|
||||
const getStructureSubcomponents = (structure: ComponentModelStructure | null) => {
|
||||
if (Array.isArray(structure?.subcomponents)) {
|
||||
return structure.subcomponents
|
||||
@@ -1026,6 +1122,9 @@ const getStructureSubcomponents = (structure: ComponentModelStructure | null) =>
|
||||
return Array.isArray(legacy) ? legacy : []
|
||||
}
|
||||
|
||||
const isNonEmptyString = (value: unknown): value is string =>
|
||||
typeof value === 'string' && value.trim().length > 0
|
||||
|
||||
const resolvePieceLabel = (piece: Record<string, any>) => {
|
||||
const parts: string[] = []
|
||||
if (piece.role) {
|
||||
@@ -1069,16 +1168,65 @@ const fetchPieceTypeNames = async (ids: string[]) => {
|
||||
fetchedPieceTypeMap.value = next
|
||||
}
|
||||
|
||||
const resolveProductLabel = (product: Record<string, any>) => {
|
||||
const parts: string[] = []
|
||||
if (product.role) {
|
||||
parts.push(product.role)
|
||||
}
|
||||
if (product.typeProduct?.name) {
|
||||
parts.push(product.typeProduct.name)
|
||||
} else if (product.typeProductLabel) {
|
||||
parts.push(product.typeProductLabel)
|
||||
} else if (product.typeProductId && productTypeLabelMap.value[product.typeProductId]) {
|
||||
parts.push(productTypeLabelMap.value[product.typeProductId])
|
||||
} else if (product.typeProduct?.code) {
|
||||
parts.push(`Catégorie ${product.typeProduct.code}`)
|
||||
} else if (product.familyCode) {
|
||||
parts.push(`Catégorie ${product.familyCode}`)
|
||||
} else if (product.typeProductId) {
|
||||
parts.push(`#${product.typeProductId}`)
|
||||
}
|
||||
return parts.length ? parts.join(' • ') : 'Produit'
|
||||
}
|
||||
|
||||
const fetchProductTypeNames = async (ids: string[]) => {
|
||||
const missing = ids.filter((id) => id && !productTypeLabelMap.value[id])
|
||||
if (!missing.length) {
|
||||
return
|
||||
}
|
||||
const results = await Promise.allSettled(
|
||||
missing.map((id) => get(`/model_types/${id}`)),
|
||||
)
|
||||
const next = { ...fetchedProductTypeMap.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
|
||||
}
|
||||
})
|
||||
fetchedProductTypeMap.value = next
|
||||
}
|
||||
|
||||
watch(
|
||||
selectedTypeStructure,
|
||||
(structure) => {
|
||||
const ids = getStructurePieces(structure)
|
||||
const pieceIds = getStructurePieces(structure)
|
||||
.map((piece: any) => piece?.typePieceId)
|
||||
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
||||
if (!ids.length) {
|
||||
return
|
||||
if (pieceIds.length) {
|
||||
fetchPieceTypeNames(Array.from(new Set(pieceIds))).catch(() => {})
|
||||
}
|
||||
|
||||
const productIds = getStructureProducts(structure)
|
||||
.map((product: any) => product?.typeProductId)
|
||||
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
||||
if (productIds.length) {
|
||||
fetchProductTypeNames(Array.from(new Set(productIds))).catch(() => {})
|
||||
}
|
||||
fetchPieceTypeNames(Array.from(new Set(ids))).catch(() => {})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
@@ -1109,6 +1257,104 @@ const resolveSubcomponentLabel = (node: Record<string, any>) => {
|
||||
return parts.length ? parts.join(' • ') : 'Sous-composant'
|
||||
}
|
||||
|
||||
type SelectionEntry = {
|
||||
id: string
|
||||
path: string
|
||||
requirementLabel: string
|
||||
resolvedName: string
|
||||
}
|
||||
|
||||
const collectStructureSelections = (root: any): {
|
||||
pieces: SelectionEntry[]
|
||||
products: SelectionEntry[]
|
||||
components: SelectionEntry[]
|
||||
} => {
|
||||
const piecesSelected: SelectionEntry[] = []
|
||||
const productsSelected: SelectionEntry[] = []
|
||||
const componentsSelected: SelectionEntry[] = []
|
||||
|
||||
if (!root || typeof root !== 'object') {
|
||||
return { pieces: piecesSelected, products: productsSelected, components: componentsSelected }
|
||||
}
|
||||
|
||||
const visitNode = (node: any, fallbackPath = 'racine') => {
|
||||
if (!node || typeof node !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const nodePath = isNonEmptyString(node.path) ? node.path : fallbackPath
|
||||
|
||||
const nodePieces = Array.isArray(node.pieces) ? node.pieces : []
|
||||
nodePieces.forEach((entry: any, index: number) => {
|
||||
const selectedId = entry?.selectedPieceId
|
||||
if (!isNonEmptyString(selectedId)) {
|
||||
return
|
||||
}
|
||||
const definition = entry?.definition ?? entry
|
||||
const catalogPiece = pieceCatalogMap.value.get(selectedId)
|
||||
piecesSelected.push({
|
||||
id: selectedId,
|
||||
path: isNonEmptyString(entry?.path) ? entry.path : `${nodePath}:piece-${index + 1}`,
|
||||
requirementLabel: resolvePieceLabel(definition),
|
||||
resolvedName: catalogPiece?.name || selectedId,
|
||||
})
|
||||
})
|
||||
|
||||
const nodeProducts = Array.isArray(node.products) ? node.products : []
|
||||
nodeProducts.forEach((entry: any, index: number) => {
|
||||
const selectedId = entry?.selectedProductId
|
||||
if (!isNonEmptyString(selectedId)) {
|
||||
return
|
||||
}
|
||||
const definition = entry?.definition ?? entry
|
||||
const catalogProduct = productCatalogMap.value.get(selectedId)
|
||||
productsSelected.push({
|
||||
id: selectedId,
|
||||
path: isNonEmptyString(entry?.path) ? entry.path : `${nodePath}:product-${index + 1}`,
|
||||
requirementLabel: resolveProductLabel(definition),
|
||||
resolvedName: catalogProduct?.name || selectedId,
|
||||
})
|
||||
})
|
||||
|
||||
const nodeChildren = Array.isArray(node.subcomponents)
|
||||
? node.subcomponents
|
||||
: Array.isArray(node.subComponents)
|
||||
? node.subComponents
|
||||
: []
|
||||
|
||||
nodeChildren.forEach((child: any, index: number) => {
|
||||
const selectedId = child?.selectedComponentId
|
||||
if (isNonEmptyString(selectedId)) {
|
||||
const definition = child?.definition ?? child
|
||||
const catalogComponent = componentCatalogMap.value.get(selectedId)
|
||||
componentsSelected.push({
|
||||
id: selectedId,
|
||||
path: isNonEmptyString(child?.path) ? child.path : `${nodePath}:subcomponent-${index + 1}`,
|
||||
requirementLabel: resolveSubcomponentLabel(definition),
|
||||
resolvedName: catalogComponent?.name || selectedId,
|
||||
})
|
||||
}
|
||||
|
||||
visitNode(child, isNonEmptyString(child?.path) ? child.path : `${nodePath}:subcomponent-${index + 1}`)
|
||||
})
|
||||
}
|
||||
|
||||
visitNode(root, isNonEmptyString(root?.path) ? root.path : 'racine')
|
||||
|
||||
return { pieces: piecesSelected, products: productsSelected, components: componentsSelected }
|
||||
}
|
||||
|
||||
const structureSelections = computed(() => {
|
||||
const selections = collectStructureSelections(component.value?.structure)
|
||||
const total =
|
||||
selections.pieces.length + selections.products.length + selections.components.length
|
||||
return {
|
||||
...selections,
|
||||
total,
|
||||
hasAny: total > 0,
|
||||
}
|
||||
})
|
||||
|
||||
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
||||
customFieldName: field.name,
|
||||
customFieldType: field.type,
|
||||
@@ -1208,7 +1454,15 @@ const saveCustomFieldValues = async (updatedComponent: any) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.allSettled([loadComponentTypes(), loadPieceTypes(), fetchComponent()])
|
||||
await Promise.allSettled([
|
||||
loadComponentTypes(),
|
||||
loadPieceTypes(),
|
||||
loadProductTypes(),
|
||||
loadPieces({ itemsPerPage: 500 }),
|
||||
loadProducts({ itemsPerPage: 500, force: true }),
|
||||
loadComposants({ itemsPerPage: 500 }),
|
||||
fetchComponent(),
|
||||
])
|
||||
loading.value = false
|
||||
if (component.value?.id) {
|
||||
await refreshDocuments()
|
||||
|
||||
Reference in New Issue
Block a user