From e18ce984e7225ca6075a5b5b97bc92ddd91fad81 Mon Sep 17 00:00:00 2001 From: matthieu Date: Sun, 8 Mar 2026 17:18:55 +0100 Subject: [PATCH] refactor(frontend) : extract shared piece product selection utils Co-Authored-By: Claude Opus 4.6 --- app/pages/pieces/[id]/edit.vue | 73 ++++-------- app/pages/pieces/create.vue | 73 ++++-------- .../utils/pieceProductSelectionUtils.ts | 104 ++++++++++++++++++ 3 files changed, 150 insertions(+), 100 deletions(-) create mode 100644 app/shared/utils/pieceProductSelectionUtils.ts diff --git a/app/pages/pieces/[id]/edit.vue b/app/pages/pieces/[id]/edit.vue index 3d3605b..0431d30 100644 --- a/app/pages/pieces/[id]/edit.vue +++ b/app/pages/pieces/[id]/edit.vue @@ -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 diff --git a/app/pages/pieces/create.vue b/app/pages/pieces/create.vue index d6cb88d..715b545 100644 --- a/app/pages/pieces/create.vue +++ b/app/pages/pieces/create.vue @@ -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] diff --git a/app/shared/utils/pieceProductSelectionUtils.ts b/app/shared/utils/pieceProductSelectionUtils.ts new file mode 100644 index 0000000..781c231 --- /dev/null +++ b/app/shared/utils/pieceProductSelectionUtils.ts @@ -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())