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 { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||
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 {
|
||||
getStructureProducts,
|
||||
buildProductRequirementDescriptions,
|
||||
buildProductRequirementEntries,
|
||||
resizeProductSelections,
|
||||
areProductSelectionsFilled,
|
||||
applyProductSelection,
|
||||
collectNormalizedProductIds,
|
||||
} from '~/shared/utils/pieceProductSelectionUtils'
|
||||
import { getModelType } from '~/services/modelTypes'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
@@ -429,72 +438,36 @@ 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 structureProducts = computed(() =>
|
||||
getStructureProducts(resolvedStructure.value),
|
||||
)
|
||||
|
||||
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(() =>
|
||||
structureProducts.value.map((requirement, index) =>
|
||||
describeProductRequirement(requirement, index),
|
||||
),
|
||||
buildProductRequirementDescriptions(structureProducts.value),
|
||||
)
|
||||
|
||||
const ensureProductSelections = (count: number) => {
|
||||
const next = Array.from({ length: count }, (_, index) => productSelections.value[index] ?? null)
|
||||
productSelections.value = next
|
||||
productSelections.value = resizeProductSelections(productSelections.value, count)
|
||||
}
|
||||
|
||||
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,
|
||||
})),
|
||||
buildProductRequirementEntries(structureProducts.value, 'piece-product-requirement'),
|
||||
)
|
||||
|
||||
const productSelectionsFilled = computed(() =>
|
||||
!requiresProductSelection.value ||
|
||||
productRequirementEntries.value.every((entry) => {
|
||||
const value = productSelections.value[entry.index]
|
||||
return typeof value === 'string' && value.trim().length > 0
|
||||
}),
|
||||
areProductSelectionsFilled(
|
||||
requiresProductSelection.value,
|
||||
productRequirementEntries.value,
|
||||
productSelections.value,
|
||||
),
|
||||
)
|
||||
|
||||
const setProductSelection = (index: number, value: string | null) => {
|
||||
const normalized = typeof value === 'string' ? value : null
|
||||
const next = [...productSelections.value]
|
||||
next[index] = normalized
|
||||
productSelections.value = next
|
||||
productSelections.value = applyProductSelection(productSelections.value, index, value)
|
||||
}
|
||||
|
||||
watch(structureProducts, (products) => {
|
||||
@@ -676,10 +649,10 @@ const submitEdition = async () => {
|
||||
const reference = editionForm.reference.trim()
|
||||
payload.reference = reference ? reference : 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())
|
||||
const normalizedProductIds = collectNormalizedProductIds(
|
||||
productRequirementEntries.value,
|
||||
productSelections.value,
|
||||
)
|
||||
|
||||
payload.productIds = normalizedProductIds
|
||||
payload.productId = normalizedProductIds[0] || null
|
||||
|
||||
@@ -224,8 +224,17 @@ import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||
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 {
|
||||
getStructureProducts,
|
||||
buildProductRequirementDescriptions,
|
||||
buildProductRequirementEntries,
|
||||
resizeProductSelections,
|
||||
areProductSelectionsFilled,
|
||||
applyProductSelection,
|
||||
collectNormalizedProductIds,
|
||||
} from '~/shared/utils/pieceProductSelectionUtils'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
normalizeCustomFieldInputs,
|
||||
@@ -303,70 +312,34 @@ 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 structureProducts = computed(() =>
|
||||
getStructureProducts(selectedType.value?.structure ?? null),
|
||||
)
|
||||
|
||||
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(() =>
|
||||
structureProducts.value.map((requirement, index) =>
|
||||
describeProductRequirement(requirement, index),
|
||||
),
|
||||
buildProductRequirementDescriptions(structureProducts.value),
|
||||
)
|
||||
|
||||
const ensureProductSelections = (count: number) => {
|
||||
const next = Array.from({ length: count }, (_, index) => productSelections.value[index] ?? null)
|
||||
productSelections.value = next
|
||||
productSelections.value = resizeProductSelections(productSelections.value, count)
|
||||
}
|
||||
|
||||
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,
|
||||
})),
|
||||
buildProductRequirementEntries(structureProducts.value, 'piece-create-product-requirement'),
|
||||
)
|
||||
|
||||
const productSelectionsFilled = computed(() =>
|
||||
!requiresProductSelection.value ||
|
||||
productRequirementEntries.value.every((entry) => {
|
||||
const value = productSelections.value[entry.index]
|
||||
return typeof value === 'string' && value.trim().length > 0
|
||||
}),
|
||||
areProductSelectionsFilled(
|
||||
requiresProductSelection.value,
|
||||
productRequirementEntries.value,
|
||||
productSelections.value,
|
||||
),
|
||||
)
|
||||
|
||||
const setProductSelection = (index: number, value: string | null) => {
|
||||
const normalized = typeof value === 'string' ? value : null
|
||||
const next = [...productSelections.value]
|
||||
next[index] = normalized
|
||||
productSelections.value = next
|
||||
productSelections.value = applyProductSelection(productSelections.value, index, value)
|
||||
}
|
||||
|
||||
watch(structureProducts, (products) => {
|
||||
@@ -440,10 +413,10 @@ const submitCreation = async () => {
|
||||
|
||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
||||
|
||||
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())
|
||||
const normalizedProductIds = collectNormalizedProductIds(
|
||||
productRequirementEntries.value,
|
||||
productSelections.value,
|
||||
)
|
||||
if (normalizedProductIds.length) {
|
||||
payload.productIds = normalizedProductIds
|
||||
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