Show component selections and support multi product requirements
This commit is contained in:
@@ -56,6 +56,15 @@ export function usePieces () {
|
|||||||
piece.productId = productId
|
piece.productId = productId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const productIds = Array.isArray(piece.productIds) ? piece.productIds.filter(Boolean) : []
|
||||||
|
if (productIds.length === 0 && piece.productId) {
|
||||||
|
piece.productIds = [piece.productId]
|
||||||
|
} else if (productIds.length > 0) {
|
||||||
|
piece.productIds = productIds.map((id) => String(id))
|
||||||
|
if (!piece.productId) {
|
||||||
|
piece.productId = piece.productIds[0] || null
|
||||||
|
}
|
||||||
|
}
|
||||||
const ids = uniqueConstructeurIds(
|
const ids = uniqueConstructeurIds(
|
||||||
piece.constructeurIds,
|
piece.constructeurIds,
|
||||||
piece.constructeurs,
|
piece.constructeurs,
|
||||||
|
|||||||
@@ -176,6 +176,18 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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">
|
<div v-if="getStructureSubcomponents(selectedTypeStructure).length" class="space-y-2">
|
||||||
<h3 class="font-semibold text-sm text-base-content">Sous-composants</h3>
|
<h3 class="font-semibold text-sm text-base-content">Sous-composants</h3>
|
||||||
<ul class="list-disc list-inside space-y-1">
|
<ul class="list-disc list-inside space-y-1">
|
||||||
@@ -189,7 +201,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<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"
|
class="text-xs text-gray-500"
|
||||||
>
|
>
|
||||||
Ce squelette ne définit pas encore de pièces, sous-composants ou valeurs par défaut.
|
Ce squelette ne définit pas encore de pièces, sous-composants ou valeurs par défaut.
|
||||||
@@ -198,6 +210,50 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</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">
|
<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">
|
<header class="space-y-1">
|
||||||
<h2 class="font-semibold text-base-content">Champs personnalisés</h2>
|
<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 { useComponentTypes } from '~/composables/useComponentTypes'
|
||||||
import { useComposants } from '~/composables/useComposants'
|
import { useComposants } from '~/composables/useComposants'
|
||||||
import { usePieceTypes } from '~/composables/usePieceTypes'
|
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 { useCustomFields } from '~/composables/useCustomFields'
|
||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
@@ -436,7 +495,10 @@ const router = useRouter()
|
|||||||
const { get } = useApi()
|
const { get } = useApi()
|
||||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
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 { ensureConstructeurs } = useConstructeurs()
|
||||||
const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields()
|
const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -512,6 +574,36 @@ const pieceTypeLabelMap = computed(() => ({
|
|||||||
),
|
),
|
||||||
...fetchedPieceTypeMap.value,
|
...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) => {
|
const documentThumbnailClass = (document: any) => {
|
||||||
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||||
return 'h-24 w-20'
|
return 'h-24 w-20'
|
||||||
@@ -1018,6 +1110,10 @@ const getStructurePieces = (structure: ComponentModelStructure | null) => {
|
|||||||
return Array.isArray(structure?.pieces) ? structure.pieces : []
|
return Array.isArray(structure?.pieces) ? structure.pieces : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStructureProducts = (structure: ComponentModelStructure | null) => {
|
||||||
|
return Array.isArray(structure?.products) ? structure.products : []
|
||||||
|
}
|
||||||
|
|
||||||
const getStructureSubcomponents = (structure: ComponentModelStructure | null) => {
|
const getStructureSubcomponents = (structure: ComponentModelStructure | null) => {
|
||||||
if (Array.isArray(structure?.subcomponents)) {
|
if (Array.isArray(structure?.subcomponents)) {
|
||||||
return structure.subcomponents
|
return structure.subcomponents
|
||||||
@@ -1026,6 +1122,9 @@ const getStructureSubcomponents = (structure: ComponentModelStructure | null) =>
|
|||||||
return Array.isArray(legacy) ? legacy : []
|
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 resolvePieceLabel = (piece: Record<string, any>) => {
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
if (piece.role) {
|
if (piece.role) {
|
||||||
@@ -1069,16 +1168,65 @@ const fetchPieceTypeNames = async (ids: string[]) => {
|
|||||||
fetchedPieceTypeMap.value = next
|
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(
|
watch(
|
||||||
selectedTypeStructure,
|
selectedTypeStructure,
|
||||||
(structure) => {
|
(structure) => {
|
||||||
const ids = getStructurePieces(structure)
|
const pieceIds = getStructurePieces(structure)
|
||||||
.map((piece: any) => piece?.typePieceId)
|
.map((piece: any) => piece?.typePieceId)
|
||||||
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
||||||
if (!ids.length) {
|
if (pieceIds.length) {
|
||||||
return
|
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 },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
@@ -1109,6 +1257,104 @@ const resolveSubcomponentLabel = (node: Record<string, any>) => {
|
|||||||
return parts.length ? parts.join(' • ') : 'Sous-composant'
|
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) => ({
|
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
||||||
customFieldName: field.name,
|
customFieldName: field.name,
|
||||||
customFieldType: field.type,
|
customFieldType: field.type,
|
||||||
@@ -1208,7 +1454,15 @@ const saveCustomFieldValues = async (updatedComponent: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
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
|
loading.value = false
|
||||||
if (component.value?.id) {
|
if (component.value?.id) {
|
||||||
await refreshDocuments()
|
await refreshDocuments()
|
||||||
|
|||||||
@@ -146,12 +146,26 @@
|
|||||||
<span>{{ description }}</span>
|
<span>{{ description }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ProductSelect
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
v-model="editionForm.productId"
|
<div
|
||||||
:disabled="saving"
|
v-for="entry in productRequirementEntries"
|
||||||
:type-product-id="primaryProductRequirement?.typeProductId || null"
|
:key="entry.key"
|
||||||
helper-text="Un produit valide est requis pour cette pièce."
|
class="form-control"
|
||||||
/>
|
>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs font-medium">
|
||||||
|
{{ entry.label }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<ProductSelect
|
||||||
|
:model-value="productSelections[entry.index] || null"
|
||||||
|
:disabled="saving"
|
||||||
|
:type-product-id="entry.typeProductId"
|
||||||
|
helper-text="Un produit valide est requis pour cette pièce."
|
||||||
|
@update:model-value="(value) => setProductSelection(entry.index, value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedType || resolvedStructure" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
<div v-if="selectedType || resolvedStructure" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||||
@@ -448,8 +462,8 @@ const editionForm = reactive({
|
|||||||
reference: '' as string,
|
reference: '' as string,
|
||||||
constructeurIds: [] as string[],
|
constructeurIds: [] as string[],
|
||||||
prix: '' as string,
|
prix: '' as string,
|
||||||
productId: null as string | null,
|
|
||||||
})
|
})
|
||||||
|
const productSelections = ref<(string | null)[]>([])
|
||||||
|
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
const documentIcon = (doc: any) =>
|
const documentIcon = (doc: any) =>
|
||||||
@@ -592,14 +606,18 @@ const selectedType = computed(() => {
|
|||||||
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
|
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getStructureProducts = (structure: PieceModelStructure | null) =>
|
||||||
|
Array.isArray(structure?.products) ? structure.products : []
|
||||||
|
|
||||||
|
const getStructureCustomFields = (structure: PieceModelStructure | null) =>
|
||||||
|
Array.isArray(structure?.customFields) ? structure.customFields : []
|
||||||
|
|
||||||
const structureProducts = computed(() =>
|
const structureProducts = computed(() =>
|
||||||
getStructureProducts(resolvedStructure.value),
|
getStructureProducts(resolvedStructure.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
|
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
|
||||||
|
|
||||||
const primaryProductRequirement = computed(() => structureProducts.value[0] ?? null)
|
|
||||||
|
|
||||||
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
||||||
if (!requirement) {
|
if (!requirement) {
|
||||||
return `Produit ${index + 1}`
|
return `Produit ${index + 1}`
|
||||||
@@ -628,6 +646,50 @@ const productRequirementDescriptions = computed(() =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ensureProductSelections = (count: number) => {
|
||||||
|
const next = Array.from({ length: count }, (_, index) => productSelections.value[index] ?? null)
|
||||||
|
productSelections.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
let pendingProductIds: string[] = []
|
||||||
|
|
||||||
|
const productRequirementEntries = computed(() =>
|
||||||
|
structureProducts.value.map((requirement, index) => ({
|
||||||
|
index,
|
||||||
|
key: `piece-product-requirement-${index}-${requirement?.typeProductId || 'any'}`,
|
||||||
|
label: describeProductRequirement(requirement, index),
|
||||||
|
typeProductId: requirement?.typeProductId ? String(requirement.typeProductId) : null,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const productSelectionsFilled = computed(() =>
|
||||||
|
!requiresProductSelection.value ||
|
||||||
|
productRequirementEntries.value.every((entry) => {
|
||||||
|
const value = productSelections.value[entry.index]
|
||||||
|
return typeof value === 'string' && value.trim().length > 0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const setProductSelection = (index: number, value: string | null) => {
|
||||||
|
const normalized = typeof value === 'string' ? value : null
|
||||||
|
const next = [...productSelections.value]
|
||||||
|
next[index] = normalized
|
||||||
|
productSelections.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(structureProducts, (products) => {
|
||||||
|
ensureProductSelections(products.length)
|
||||||
|
if (!pendingProductIds.length || products.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const next = Array.from(
|
||||||
|
{ length: products.length },
|
||||||
|
(_, index) => pendingProductIds[index] ?? null,
|
||||||
|
)
|
||||||
|
productSelections.value = next
|
||||||
|
pendingProductIds = []
|
||||||
|
})
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
customFieldInputs.value.every((field) => {
|
customFieldInputs.value.every((field) => {
|
||||||
if (!field.required) {
|
if (!field.required) {
|
||||||
@@ -645,7 +707,7 @@ const canSubmit = computed(() =>
|
|||||||
piece.value &&
|
piece.value &&
|
||||||
editionForm.name &&
|
editionForm.name &&
|
||||||
requiredCustomFieldsFilled.value &&
|
requiredCustomFieldsFilled.value &&
|
||||||
(!requiresProductSelection.value || editionForm.productId) &&
|
productSelectionsFilled.value &&
|
||||||
!saving.value,
|
!saving.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -730,11 +792,26 @@ watch(
|
|||||||
currentPiece.constructeur ? [currentPiece.constructeur] : [],
|
currentPiece.constructeur ? [currentPiece.constructeur] : [],
|
||||||
)
|
)
|
||||||
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
|
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
|
||||||
editionForm.productId = currentPiece.product?.id || currentPiece.productId || null
|
|
||||||
if (editionForm.constructeurIds.length) {
|
if (editionForm.constructeurIds.length) {
|
||||||
void ensureConstructeurs(editionForm.constructeurIds)
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingProductIds = Array.isArray(currentPiece.productIds) && currentPiece.productIds.length
|
||||||
|
? currentPiece.productIds.map((id: unknown) => String(id))
|
||||||
|
: currentPiece.product?.id || currentPiece.productId
|
||||||
|
? [String(currentPiece.product?.id || currentPiece.productId)]
|
||||||
|
: []
|
||||||
|
pendingProductIds = existingProductIds
|
||||||
|
ensureProductSelections(structureProducts.value.length)
|
||||||
|
if (existingProductIds.length && structureProducts.value.length) {
|
||||||
|
const next = Array.from(
|
||||||
|
{ length: structureProducts.value.length },
|
||||||
|
(_, index) => existingProductIds[index] ?? null,
|
||||||
|
)
|
||||||
|
productSelections.value = next
|
||||||
|
pendingProductIds = []
|
||||||
|
}
|
||||||
|
|
||||||
refreshCustomFieldInputs(currentType?.structure ?? null, currentPiece.customFieldValues)
|
refreshCustomFieldInputs(currentType?.structure ?? null, currentPiece.customFieldValues)
|
||||||
|
|
||||||
initialized = true
|
initialized = true
|
||||||
@@ -755,6 +832,7 @@ watch(resolvedStructure, (currentStructure) => {
|
|||||||
if (!piece.value) {
|
if (!piece.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ensureProductSelections(structureProducts.value.length)
|
||||||
refreshCustomFieldInputs(currentStructure, piece.value.customFieldValues)
|
refreshCustomFieldInputs(currentStructure, piece.value.customFieldValues)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -763,7 +841,7 @@ const submitEdition = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiresProductSelection.value && !editionForm.productId) {
|
if (!productSelectionsFilled.value) {
|
||||||
toast.showError('Sélectionnez un produit conforme au squelette.')
|
toast.showError('Sélectionnez un produit conforme au squelette.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -784,11 +862,13 @@ const submitEdition = async () => {
|
|||||||
const reference = editionForm.reference.trim()
|
const reference = editionForm.reference.trim()
|
||||||
payload.reference = reference ? reference : null
|
payload.reference = reference ? reference : null
|
||||||
|
|
||||||
const selectedProductId =
|
const normalizedProductIds = productRequirementEntries.value
|
||||||
typeof editionForm.productId === 'string'
|
.map((entry) => productSelections.value[entry.index])
|
||||||
? editionForm.productId.trim()
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
||||||
: ''
|
.map((value) => value.trim())
|
||||||
payload.productId = selectedProductId || null
|
|
||||||
|
payload.productIds = normalizedProductIds
|
||||||
|
payload.productId = normalizedProductIds[0] || null
|
||||||
|
|
||||||
if (rawPrice) {
|
if (rawPrice) {
|
||||||
const parsed = Number(rawPrice)
|
const parsed = Number(rawPrice)
|
||||||
@@ -981,12 +1061,6 @@ const formatDefaultValue = (type: string, defaultValue: any): string => {
|
|||||||
return String(defaultValue)
|
return String(defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStructureProducts = (structure: PieceModelStructure | null) =>
|
|
||||||
Array.isArray(structure?.products) ? structure.products : []
|
|
||||||
|
|
||||||
const getStructureCustomFields = (structure: PieceModelStructure | null) =>
|
|
||||||
Array.isArray(structure?.customFields) ? structure.customFields : []
|
|
||||||
|
|
||||||
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
||||||
customFieldName: field.name,
|
customFieldName: field.name,
|
||||||
customFieldType: field.type,
|
customFieldType: field.type,
|
||||||
|
|||||||
@@ -118,12 +118,26 @@
|
|||||||
<span>{{ description }}</span>
|
<span>{{ description }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ProductSelect
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
v-model="creationForm.productId"
|
<div
|
||||||
:disabled="submitting || !selectedType"
|
v-for="entry in productRequirementEntries"
|
||||||
:type-product-id="primaryProductRequirement?.typeProductId || null"
|
:key="entry.key"
|
||||||
helper-text="Un produit est requis pour cette pièce."
|
class="form-control"
|
||||||
/>
|
>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs font-medium">
|
||||||
|
{{ entry.label }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<ProductSelect
|
||||||
|
:model-value="productSelections[entry.index] || null"
|
||||||
|
:disabled="submitting || !selectedType"
|
||||||
|
:type-product-id="entry.typeProductId"
|
||||||
|
helper-text="Un produit est requis pour cette pièce."
|
||||||
|
@update:model-value="(value) => setProductSelection(entry.index, value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedType" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
<div v-if="selectedType" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||||
@@ -317,8 +331,8 @@ const creationForm = reactive({
|
|||||||
reference: '' as string,
|
reference: '' as string,
|
||||||
constructeurIds: [] as string[],
|
constructeurIds: [] as string[],
|
||||||
prix: '' as string,
|
prix: '' as string,
|
||||||
productId: null as string | null,
|
|
||||||
})
|
})
|
||||||
|
const productSelections = ref<(string | null)[]>([])
|
||||||
|
|
||||||
const lastSuggestedName = ref('')
|
const lastSuggestedName = ref('')
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
@@ -364,14 +378,18 @@ const selectedType = computed(() => {
|
|||||||
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
|
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getStructureCustomFields = (structure: PieceModelStructure | null) =>
|
||||||
|
Array.isArray(structure?.customFields) ? structure.customFields : []
|
||||||
|
|
||||||
|
const getStructureProducts = (structure: PieceModelStructure | null) =>
|
||||||
|
Array.isArray(structure?.products) ? structure.products : []
|
||||||
|
|
||||||
const structureProducts = computed(() =>
|
const structureProducts = computed(() =>
|
||||||
getStructureProducts(selectedType.value?.structure ?? null),
|
getStructureProducts(selectedType.value?.structure ?? null),
|
||||||
)
|
)
|
||||||
|
|
||||||
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
|
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
|
||||||
|
|
||||||
const primaryProductRequirement = computed(() => structureProducts.value[0] ?? null)
|
|
||||||
|
|
||||||
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
||||||
if (!requirement) {
|
if (!requirement) {
|
||||||
return `Produit ${index + 1}`
|
return `Produit ${index + 1}`
|
||||||
@@ -400,6 +418,39 @@ const productRequirementDescriptions = computed(() =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ensureProductSelections = (count: number) => {
|
||||||
|
const next = Array.from({ length: count }, (_, index) => productSelections.value[index] ?? null)
|
||||||
|
productSelections.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
const productRequirementEntries = computed(() =>
|
||||||
|
structureProducts.value.map((requirement, index) => ({
|
||||||
|
index,
|
||||||
|
key: `piece-create-product-requirement-${index}-${requirement?.typeProductId || 'any'}`,
|
||||||
|
label: describeProductRequirement(requirement, index),
|
||||||
|
typeProductId: requirement?.typeProductId ? String(requirement.typeProductId) : null,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const productSelectionsFilled = computed(() =>
|
||||||
|
!requiresProductSelection.value ||
|
||||||
|
productRequirementEntries.value.every((entry) => {
|
||||||
|
const value = productSelections.value[entry.index]
|
||||||
|
return typeof value === 'string' && value.trim().length > 0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const setProductSelection = (index: number, value: string | null) => {
|
||||||
|
const normalized = typeof value === 'string' ? value : null
|
||||||
|
const next = [...productSelections.value]
|
||||||
|
next[index] = normalized
|
||||||
|
productSelections.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(structureProducts, (products) => {
|
||||||
|
ensureProductSelections(products.length)
|
||||||
|
})
|
||||||
|
|
||||||
watch(selectedType, (type) => {
|
watch(selectedType, (type) => {
|
||||||
if (!type) {
|
if (!type) {
|
||||||
clearCreationForm()
|
clearCreationForm()
|
||||||
@@ -411,7 +462,7 @@ watch(selectedType, (type) => {
|
|||||||
}
|
}
|
||||||
lastSuggestedName.value = creationForm.name
|
lastSuggestedName.value = creationForm.name
|
||||||
customFieldInputs.value = normalizeCustomFieldInputs(type.structure)
|
customFieldInputs.value = normalizeCustomFieldInputs(type.structure)
|
||||||
creationForm.productId = null
|
productSelections.value = Array.from({ length: structureProducts.value.length }, () => null)
|
||||||
})
|
})
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
@@ -431,7 +482,7 @@ const canSubmit = computed(() =>
|
|||||||
selectedType.value &&
|
selectedType.value &&
|
||||||
creationForm.name &&
|
creationForm.name &&
|
||||||
requiredCustomFieldsFilled.value &&
|
requiredCustomFieldsFilled.value &&
|
||||||
(!requiresProductSelection.value || creationForm.productId) &&
|
productSelectionsFilled.value &&
|
||||||
!submitting.value,
|
!submitting.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -449,18 +500,12 @@ const toFieldString = (value: unknown): string => {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStructureCustomFields = (structure: PieceModelStructure | null) =>
|
|
||||||
Array.isArray(structure?.customFields) ? structure.customFields : []
|
|
||||||
|
|
||||||
const getStructureProducts = (structure: PieceModelStructure | null) =>
|
|
||||||
Array.isArray(structure?.products) ? structure.products : []
|
|
||||||
|
|
||||||
const clearCreationForm = () => {
|
const clearCreationForm = () => {
|
||||||
creationForm.name = ''
|
creationForm.name = ''
|
||||||
creationForm.reference = ''
|
creationForm.reference = ''
|
||||||
creationForm.constructeurIds = []
|
creationForm.constructeurIds = []
|
||||||
creationForm.prix = ''
|
creationForm.prix = ''
|
||||||
creationForm.productId = null
|
productSelections.value = []
|
||||||
lastSuggestedName.value = ''
|
lastSuggestedName.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +515,7 @@ const submitCreation = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiresProductSelection.value && !creationForm.productId) {
|
if (!productSelectionsFilled.value) {
|
||||||
toast.showError('Sélectionnez un produit conforme au squelette.')
|
toast.showError('Sélectionnez un produit conforme au squelette.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -487,12 +532,13 @@ const submitCreation = async () => {
|
|||||||
|
|
||||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
||||||
|
|
||||||
const selectedProductId =
|
const normalizedProductIds = productRequirementEntries.value
|
||||||
typeof creationForm.productId === 'string'
|
.map((entry) => productSelections.value[entry.index])
|
||||||
? creationForm.productId.trim()
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
||||||
: ''
|
.map((value) => value.trim())
|
||||||
if (selectedProductId) {
|
if (normalizedProductIds.length) {
|
||||||
payload.productId = selectedProductId
|
payload.productIds = normalizedProductIds
|
||||||
|
payload.productId = normalizedProductIds[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawPrice = typeof creationForm.prix === 'string'
|
const rawPrice = typeof creationForm.prix === 'string'
|
||||||
|
|||||||
Reference in New Issue
Block a user