Show component selections and support multi product requirements

This commit is contained in:
Matthieu
2026-01-25 11:40:29 +01:00
parent 55739fe50f
commit b27662d2bc
4 changed files with 438 additions and 55 deletions

View File

@@ -146,12 +146,26 @@
<span>{{ description }}</span>
</li>
</ul>
<ProductSelect
v-model="editionForm.productId"
:disabled="saving"
:type-product-id="primaryProductRequirement?.typeProductId || null"
helper-text="Un produit valide est requis pour cette pièce."
/>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div
v-for="entry in productRequirementEntries"
:key="entry.key"
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 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,
constructeurIds: [] as string[],
prix: '' as string,
productId: null as string | null,
})
const productSelections = ref<(string | null)[]>([])
const customFieldInputs = ref<CustomFieldInput[]>([])
const documentIcon = (doc: any) =>
@@ -592,14 +606,18 @@ const selectedType = computed(() => {
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(() =>
getStructureProducts(resolvedStructure.value),
)
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
const primaryProductRequirement = computed(() => structureProducts.value[0] ?? null)
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
if (!requirement) {
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(() =>
customFieldInputs.value.every((field) => {
if (!field.required) {
@@ -645,7 +707,7 @@ const canSubmit = computed(() =>
piece.value &&
editionForm.name &&
requiredCustomFieldsFilled.value &&
(!requiresProductSelection.value || editionForm.productId) &&
productSelectionsFilled.value &&
!saving.value,
),
)
@@ -730,11 +792,26 @@ watch(
currentPiece.constructeur ? [currentPiece.constructeur] : [],
)
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
editionForm.productId = currentPiece.product?.id || currentPiece.productId || null
if (editionForm.constructeurIds.length) {
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)
initialized = true
@@ -755,6 +832,7 @@ watch(resolvedStructure, (currentStructure) => {
if (!piece.value) {
return
}
ensureProductSelections(structureProducts.value.length)
refreshCustomFieldInputs(currentStructure, piece.value.customFieldValues)
})
@@ -763,7 +841,7 @@ const submitEdition = async () => {
return
}
if (requiresProductSelection.value && !editionForm.productId) {
if (!productSelectionsFilled.value) {
toast.showError('Sélectionnez un produit conforme au squelette.')
return
}
@@ -784,11 +862,13 @@ const submitEdition = async () => {
const reference = editionForm.reference.trim()
payload.reference = reference ? reference : null
const selectedProductId =
typeof editionForm.productId === 'string'
? editionForm.productId.trim()
: ''
payload.productId = selectedProductId || null
const normalizedProductIds = productRequirementEntries.value
.map((entry) => productSelections.value[entry.index])
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
.map((value) => value.trim())
payload.productIds = normalizedProductIds
payload.productId = normalizedProductIds[0] || null
if (rawPrice) {
const parsed = Number(rawPrice)
@@ -981,12 +1061,6 @@ const formatDefaultValue = (type: string, defaultValue: any): string => {
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) => ({
customFieldName: field.name,
customFieldType: field.type,

View File

@@ -118,12 +118,26 @@
<span>{{ description }}</span>
</li>
</ul>
<ProductSelect
v-model="creationForm.productId"
:disabled="submitting || !selectedType"
:type-product-id="primaryProductRequirement?.typeProductId || null"
helper-text="Un produit est requis pour cette pièce."
/>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div
v-for="entry in productRequirementEntries"
:key="entry.key"
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 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,
constructeurIds: [] as string[],
prix: '' as string,
productId: null as string | null,
})
const productSelections = ref<(string | null)[]>([])
const lastSuggestedName = ref('')
const customFieldInputs = ref<CustomFieldInput[]>([])
@@ -364,14 +378,18 @@ const selectedType = computed(() => {
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(() =>
getStructureProducts(selectedType.value?.structure ?? null),
)
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
const primaryProductRequirement = computed(() => structureProducts.value[0] ?? null)
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
if (!requirement) {
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) => {
if (!type) {
clearCreationForm()
@@ -411,7 +462,7 @@ watch(selectedType, (type) => {
}
lastSuggestedName.value = creationForm.name
customFieldInputs.value = normalizeCustomFieldInputs(type.structure)
creationForm.productId = null
productSelections.value = Array.from({ length: structureProducts.value.length }, () => null)
})
const requiredCustomFieldsFilled = computed(() =>
@@ -431,7 +482,7 @@ const canSubmit = computed(() =>
selectedType.value &&
creationForm.name &&
requiredCustomFieldsFilled.value &&
(!requiresProductSelection.value || creationForm.productId) &&
productSelectionsFilled.value &&
!submitting.value,
),
)
@@ -449,18 +500,12 @@ const toFieldString = (value: unknown): string => {
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 = () => {
creationForm.name = ''
creationForm.reference = ''
creationForm.constructeurIds = []
creationForm.prix = ''
creationForm.productId = null
productSelections.value = []
lastSuggestedName.value = ''
}
@@ -470,7 +515,7 @@ const submitCreation = async () => {
return
}
if (requiresProductSelection.value && !creationForm.productId) {
if (!productSelectionsFilled.value) {
toast.showError('Sélectionnez un produit conforme au squelette.')
return
}
@@ -487,12 +532,13 @@ const submitCreation = async () => {
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
const selectedProductId =
typeof creationForm.productId === 'string'
? creationForm.productId.trim()
: ''
if (selectedProductId) {
payload.productId = selectedProductId
const normalizedProductIds = productRequirementEntries.value
.map((entry) => productSelections.value[entry.index])
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
.map((value) => value.trim())
if (normalizedProductIds.length) {
payload.productIds = normalizedProductIds
payload.productId = normalizedProductIds[0]
}
const rawPrice = typeof creationForm.prix === 'string'