refactor(frontend) : extract shared piece product selection utils
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -287,8 +287,17 @@ import { extractRelationId } from '~/shared/apiRelations'
|
|||||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
import type { PieceModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
|
import {
|
||||||
|
getStructureProducts,
|
||||||
|
buildProductRequirementDescriptions,
|
||||||
|
buildProductRequirementEntries,
|
||||||
|
resizeProductSelections,
|
||||||
|
areProductSelectionsFilled,
|
||||||
|
applyProductSelection,
|
||||||
|
collectNormalizedProductIds,
|
||||||
|
} from '~/shared/utils/pieceProductSelectionUtils'
|
||||||
import { getModelType } from '~/services/modelTypes'
|
import { getModelType } from '~/services/modelTypes'
|
||||||
import {
|
import {
|
||||||
type CustomFieldInput,
|
type CustomFieldInput,
|
||||||
@@ -429,72 +438,36 @@ 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 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 describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
|
||||||
if (!requirement) {
|
|
||||||
return `Produit ${index + 1}`
|
|
||||||
}
|
|
||||||
const parts: string[] = []
|
|
||||||
if (requirement.role) {
|
|
||||||
parts.push(requirement.role)
|
|
||||||
}
|
|
||||||
if (requirement.typeProductLabel) {
|
|
||||||
parts.push(requirement.typeProductLabel)
|
|
||||||
} else if (requirement.typeProductId) {
|
|
||||||
parts.push(`Catégorie #${requirement.typeProductId}`)
|
|
||||||
}
|
|
||||||
if (requirement.familyCode) {
|
|
||||||
parts.push(`Famille ${requirement.familyCode}`)
|
|
||||||
}
|
|
||||||
if (parts.length === 0) {
|
|
||||||
parts.push(`Produit ${index + 1}`)
|
|
||||||
}
|
|
||||||
return parts.join(' • ')
|
|
||||||
}
|
|
||||||
|
|
||||||
const productRequirementDescriptions = computed(() =>
|
const productRequirementDescriptions = computed(() =>
|
||||||
structureProducts.value.map((requirement, index) =>
|
buildProductRequirementDescriptions(structureProducts.value),
|
||||||
describeProductRequirement(requirement, index),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const ensureProductSelections = (count: number) => {
|
const ensureProductSelections = (count: number) => {
|
||||||
const next = Array.from({ length: count }, (_, index) => productSelections.value[index] ?? null)
|
productSelections.value = resizeProductSelections(productSelections.value, count)
|
||||||
productSelections.value = next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let pendingProductIds: string[] = []
|
let pendingProductIds: string[] = []
|
||||||
|
|
||||||
const productRequirementEntries = computed(() =>
|
const productRequirementEntries = computed(() =>
|
||||||
structureProducts.value.map((requirement, index) => ({
|
buildProductRequirementEntries(structureProducts.value, 'piece-product-requirement'),
|
||||||
index,
|
|
||||||
key: `piece-product-requirement-${index}-${requirement?.typeProductId || 'any'}`,
|
|
||||||
label: describeProductRequirement(requirement, index),
|
|
||||||
typeProductId: requirement?.typeProductId ? String(requirement.typeProductId) : null,
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const productSelectionsFilled = computed(() =>
|
const productSelectionsFilled = computed(() =>
|
||||||
!requiresProductSelection.value ||
|
areProductSelectionsFilled(
|
||||||
productRequirementEntries.value.every((entry) => {
|
requiresProductSelection.value,
|
||||||
const value = productSelections.value[entry.index]
|
productRequirementEntries.value,
|
||||||
return typeof value === 'string' && value.trim().length > 0
|
productSelections.value,
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const setProductSelection = (index: number, value: string | null) => {
|
const setProductSelection = (index: number, value: string | null) => {
|
||||||
const normalized = typeof value === 'string' ? value : null
|
productSelections.value = applyProductSelection(productSelections.value, index, value)
|
||||||
const next = [...productSelections.value]
|
|
||||||
next[index] = normalized
|
|
||||||
productSelections.value = next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(structureProducts, (products) => {
|
watch(structureProducts, (products) => {
|
||||||
@@ -676,10 +649,10 @@ const submitEdition = async () => {
|
|||||||
const reference = editionForm.reference.trim()
|
const reference = editionForm.reference.trim()
|
||||||
payload.reference = reference ? reference : null
|
payload.reference = reference ? reference : null
|
||||||
|
|
||||||
const normalizedProductIds = productRequirementEntries.value
|
const normalizedProductIds = collectNormalizedProductIds(
|
||||||
.map((entry) => productSelections.value[entry.index])
|
productRequirementEntries.value,
|
||||||
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
productSelections.value,
|
||||||
.map((value) => value.trim())
|
)
|
||||||
|
|
||||||
payload.productIds = normalizedProductIds
|
payload.productIds = normalizedProductIds
|
||||||
payload.productId = normalizedProductIds[0] || null
|
payload.productId = normalizedProductIds[0] || null
|
||||||
|
|||||||
@@ -224,8 +224,17 @@ import { useCustomFields } from '~/composables/useCustomFields'
|
|||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
import type { PieceModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
|
import {
|
||||||
|
getStructureProducts,
|
||||||
|
buildProductRequirementDescriptions,
|
||||||
|
buildProductRequirementEntries,
|
||||||
|
resizeProductSelections,
|
||||||
|
areProductSelectionsFilled,
|
||||||
|
applyProductSelection,
|
||||||
|
collectNormalizedProductIds,
|
||||||
|
} from '~/shared/utils/pieceProductSelectionUtils'
|
||||||
import {
|
import {
|
||||||
type CustomFieldInput,
|
type CustomFieldInput,
|
||||||
normalizeCustomFieldInputs,
|
normalizeCustomFieldInputs,
|
||||||
@@ -303,70 +312,34 @@ 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 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 describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
|
||||||
if (!requirement) {
|
|
||||||
return `Produit ${index + 1}`
|
|
||||||
}
|
|
||||||
const parts: string[] = []
|
|
||||||
if (requirement.role) {
|
|
||||||
parts.push(requirement.role)
|
|
||||||
}
|
|
||||||
if (requirement.typeProductLabel) {
|
|
||||||
parts.push(requirement.typeProductLabel)
|
|
||||||
} else if (requirement.typeProductId) {
|
|
||||||
parts.push(`Catégorie #${requirement.typeProductId}`)
|
|
||||||
}
|
|
||||||
if (requirement.familyCode) {
|
|
||||||
parts.push(`Famille ${requirement.familyCode}`)
|
|
||||||
}
|
|
||||||
if (parts.length === 0) {
|
|
||||||
parts.push(`Produit ${index + 1}`)
|
|
||||||
}
|
|
||||||
return parts.join(' • ')
|
|
||||||
}
|
|
||||||
|
|
||||||
const productRequirementDescriptions = computed(() =>
|
const productRequirementDescriptions = computed(() =>
|
||||||
structureProducts.value.map((requirement, index) =>
|
buildProductRequirementDescriptions(structureProducts.value),
|
||||||
describeProductRequirement(requirement, index),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const ensureProductSelections = (count: number) => {
|
const ensureProductSelections = (count: number) => {
|
||||||
const next = Array.from({ length: count }, (_, index) => productSelections.value[index] ?? null)
|
productSelections.value = resizeProductSelections(productSelections.value, count)
|
||||||
productSelections.value = next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const productRequirementEntries = computed(() =>
|
const productRequirementEntries = computed(() =>
|
||||||
structureProducts.value.map((requirement, index) => ({
|
buildProductRequirementEntries(structureProducts.value, 'piece-create-product-requirement'),
|
||||||
index,
|
|
||||||
key: `piece-create-product-requirement-${index}-${requirement?.typeProductId || 'any'}`,
|
|
||||||
label: describeProductRequirement(requirement, index),
|
|
||||||
typeProductId: requirement?.typeProductId ? String(requirement.typeProductId) : null,
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const productSelectionsFilled = computed(() =>
|
const productSelectionsFilled = computed(() =>
|
||||||
!requiresProductSelection.value ||
|
areProductSelectionsFilled(
|
||||||
productRequirementEntries.value.every((entry) => {
|
requiresProductSelection.value,
|
||||||
const value = productSelections.value[entry.index]
|
productRequirementEntries.value,
|
||||||
return typeof value === 'string' && value.trim().length > 0
|
productSelections.value,
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const setProductSelection = (index: number, value: string | null) => {
|
const setProductSelection = (index: number, value: string | null) => {
|
||||||
const normalized = typeof value === 'string' ? value : null
|
productSelections.value = applyProductSelection(productSelections.value, index, value)
|
||||||
const next = [...productSelections.value]
|
|
||||||
next[index] = normalized
|
|
||||||
productSelections.value = next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(structureProducts, (products) => {
|
watch(structureProducts, (products) => {
|
||||||
@@ -440,10 +413,10 @@ const submitCreation = async () => {
|
|||||||
|
|
||||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
||||||
|
|
||||||
const normalizedProductIds = productRequirementEntries.value
|
const normalizedProductIds = collectNormalizedProductIds(
|
||||||
.map((entry) => productSelections.value[entry.index])
|
productRequirementEntries.value,
|
||||||
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
productSelections.value,
|
||||||
.map((value) => value.trim())
|
)
|
||||||
if (normalizedProductIds.length) {
|
if (normalizedProductIds.length) {
|
||||||
payload.productIds = normalizedProductIds
|
payload.productIds = normalizedProductIds
|
||||||
payload.productId = normalizedProductIds[0]
|
payload.productId = normalizedProductIds[0]
|
||||||
|
|||||||
104
app/shared/utils/pieceProductSelectionUtils.ts
Normal file
104
app/shared/utils/pieceProductSelectionUtils.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the products array from a piece model structure, defaulting to [].
|
||||||
|
*/
|
||||||
|
export const getStructureProducts = (structure: PieceModelStructure | null): PieceModelProduct[] =>
|
||||||
|
Array.isArray(structure?.products) ? structure.products : []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a human-readable label for a single product requirement.
|
||||||
|
*/
|
||||||
|
export const describeProductRequirement = (requirement: PieceModelProduct, index: number): string => {
|
||||||
|
if (!requirement) {
|
||||||
|
return `Produit ${index + 1}`
|
||||||
|
}
|
||||||
|
const parts: string[] = []
|
||||||
|
if (requirement.role) {
|
||||||
|
parts.push(requirement.role)
|
||||||
|
}
|
||||||
|
if (requirement.typeProductLabel) {
|
||||||
|
parts.push(requirement.typeProductLabel)
|
||||||
|
} else if (requirement.typeProductId) {
|
||||||
|
parts.push(`Catégorie #${requirement.typeProductId}`)
|
||||||
|
}
|
||||||
|
if (requirement.familyCode) {
|
||||||
|
parts.push(`Famille ${requirement.familyCode}`)
|
||||||
|
}
|
||||||
|
if (parts.length === 0) {
|
||||||
|
parts.push(`Produit ${index + 1}`)
|
||||||
|
}
|
||||||
|
return parts.join(' • ')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build description strings for every product requirement in a structure.
|
||||||
|
*/
|
||||||
|
export const buildProductRequirementDescriptions = (
|
||||||
|
products: PieceModelProduct[],
|
||||||
|
): string[] =>
|
||||||
|
products.map((requirement, index) => describeProductRequirement(requirement, index))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the entry objects used to render product selection inputs.
|
||||||
|
*/
|
||||||
|
export const buildProductRequirementEntries = (
|
||||||
|
products: PieceModelProduct[],
|
||||||
|
keyPrefix: string,
|
||||||
|
) =>
|
||||||
|
products.map((requirement, index) => ({
|
||||||
|
index,
|
||||||
|
key: `${keyPrefix}-${index}-${requirement?.typeProductId || 'any'}`,
|
||||||
|
label: describeProductRequirement(requirement, index),
|
||||||
|
typeProductId: requirement?.typeProductId ? String(requirement.typeProductId) : null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize the selections array to match the expected count, preserving existing values.
|
||||||
|
*/
|
||||||
|
export const resizeProductSelections = (
|
||||||
|
current: (string | null)[],
|
||||||
|
count: number,
|
||||||
|
): (string | null)[] =>
|
||||||
|
Array.from({ length: count }, (_, index) => current[index] ?? null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true when all required product slots have a non-empty string value,
|
||||||
|
* or when no product selection is required.
|
||||||
|
*/
|
||||||
|
export const areProductSelectionsFilled = (
|
||||||
|
requiresSelection: boolean,
|
||||||
|
entries: { index: number }[],
|
||||||
|
selections: (string | null)[],
|
||||||
|
): boolean =>
|
||||||
|
!requiresSelection ||
|
||||||
|
entries.every((entry) => {
|
||||||
|
const value = selections[entry.index]
|
||||||
|
return typeof value === 'string' && value.trim().length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a single product selection by index, returning a new array.
|
||||||
|
*/
|
||||||
|
export const applyProductSelection = (
|
||||||
|
current: (string | null)[],
|
||||||
|
index: number,
|
||||||
|
value: string | null,
|
||||||
|
): (string | null)[] => {
|
||||||
|
const normalized = typeof value === 'string' ? value : null
|
||||||
|
const next = [...current]
|
||||||
|
next[index] = normalized
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract normalized product IDs from the current selections based on requirement entries.
|
||||||
|
*/
|
||||||
|
export const collectNormalizedProductIds = (
|
||||||
|
entries: { index: number }[],
|
||||||
|
selections: (string | null)[],
|
||||||
|
): string[] =>
|
||||||
|
entries
|
||||||
|
.map((entry) => selections[entry.index])
|
||||||
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
||||||
|
.map((value) => value.trim())
|
||||||
Reference in New Issue
Block a user