Extract ~1300 LOC of reusable logic into dedicated modules: - shared/utils/customFieldUtils.ts: field normalization, merge, dedup, display - shared/utils/productDisplayUtils.ts: product resolution and display helpers - composables/useMachineHierarchy.ts: hierarchy tree builder from links - composables/useMachinePrint.ts: print selection and execution logic These extractions prepare the ground for wiring [id].vue to import from these modules instead of inlining all logic. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
354 lines
13 KiB
TypeScript
354 lines
13 KiB
TypeScript
/**
|
||
* Product resolution and display utilities.
|
||
*
|
||
* Extracted from pages/machine/[id].vue – these functions resolve product
|
||
* references from deeply nested API payloads and build display objects.
|
||
*/
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Types
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export interface ProductDisplay {
|
||
name: string
|
||
reference: string | null
|
||
category: string | null
|
||
suppliers: string | null
|
||
price: string | null
|
||
}
|
||
|
||
type AnyRecord = Record<string, unknown>
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const isPlainObject = (value: unknown): value is AnyRecord =>
|
||
Object.prototype.toString.call(value) === '[object Object]'
|
||
|
||
export const resolveIdentifier = (...candidates: unknown[]): string | null => {
|
||
for (const candidate of candidates) {
|
||
if (candidate !== undefined && candidate !== null && candidate !== '') {
|
||
return candidate as string
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Supplier / price labels
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const getProductSuppliersLabel = (product: AnyRecord | null): string | null => {
|
||
if (!product) return null
|
||
const suppliers = Array.isArray(product.constructeurs)
|
||
? (product.constructeurs as AnyRecord[]).map((c) => c?.name as string).filter(Boolean)
|
||
: []
|
||
return suppliers.length > 0 ? suppliers.join(', ') : null
|
||
}
|
||
|
||
export const getProductPriceLabel = (product: AnyRecord | null): string | null => {
|
||
if (!product) return null
|
||
const priceValue =
|
||
(product.supplierPrice ?? product.prix ?? product.price ?? null) as string | number | null
|
||
if (priceValue === undefined || priceValue === null) return null
|
||
const numeric = Number(priceValue)
|
||
if (Number.isNaN(numeric)) return null
|
||
return `${numeric.toFixed(2)} €`
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// resolveProductReference
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const resolveProductReference = (
|
||
source: AnyRecord | null | undefined,
|
||
findProductById: (id: string) => AnyRecord | null,
|
||
): { product: AnyRecord | null; productId: string | null } => {
|
||
if (!source || typeof source !== 'object') {
|
||
return { product: null, productId: null }
|
||
}
|
||
|
||
const candidateKeys: (string | null)[] = [
|
||
null,
|
||
'productLink',
|
||
'machinePieceLink',
|
||
'machineComponentLink',
|
||
'machineProductLink',
|
||
'originalPiece',
|
||
'originalComposant',
|
||
'link',
|
||
'overrides',
|
||
'machineComponentLinkOverrides',
|
||
'requirement',
|
||
'selection',
|
||
'entry',
|
||
]
|
||
|
||
let product: AnyRecord | null = null
|
||
let productId: string | null = null
|
||
|
||
const inspect = (container: unknown) => {
|
||
if (!container || typeof container !== 'object') return
|
||
const c = container as AnyRecord
|
||
if (!product && c.product && typeof c.product === 'object') {
|
||
product = c.product as AnyRecord
|
||
}
|
||
if (!productId) {
|
||
const candidate =
|
||
(c.productId as string) ||
|
||
(c.product && typeof c.product === 'object'
|
||
? ((c.product as AnyRecord).id as string) || ((c.product as AnyRecord).productId as string)
|
||
: null) ||
|
||
null
|
||
if (candidate) productId = candidate
|
||
}
|
||
}
|
||
|
||
candidateKeys.forEach((key) => {
|
||
if (key === null) inspect(source)
|
||
else inspect((source as AnyRecord)[key])
|
||
})
|
||
|
||
if (!product && productId) {
|
||
product = findProductById(productId) || null
|
||
}
|
||
|
||
if (!product && !productId && source.productName) {
|
||
const suppliersLabel =
|
||
typeof source.constructeursLabel === 'string'
|
||
? source.constructeursLabel
|
||
: typeof source.productSuppliers === 'string'
|
||
? source.productSuppliers
|
||
: null
|
||
|
||
return {
|
||
product: {
|
||
name: source.productName,
|
||
reference: source.productReference || null,
|
||
typeProduct: source.productCategory ? { name: source.productCategory } : null,
|
||
constructeurs: suppliersLabel
|
||
? (suppliersLabel as string)
|
||
.split(',')
|
||
.map((name: string) => name.trim())
|
||
.filter((name: string) => name.length > 0)
|
||
.map((name: string) => ({ name }))
|
||
: undefined,
|
||
supplierPrice: source.productPrice ?? source.productPriceLabel ?? source.price ?? null,
|
||
} as AnyRecord,
|
||
productId: null,
|
||
}
|
||
}
|
||
|
||
if (productId && product && product.id && product.id !== productId) {
|
||
const resolved = findProductById(productId)
|
||
if (resolved) product = resolved
|
||
}
|
||
|
||
return { product: product || null, productId: productId || null }
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// getProductDisplay
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const getProductDisplay = (
|
||
source: AnyRecord | null | undefined,
|
||
findProductById: (id: string) => AnyRecord | null,
|
||
): ProductDisplay | null => {
|
||
if (!source || typeof source !== 'object') return null
|
||
|
||
const { product, productId } = resolveProductReference(source, findProductById)
|
||
|
||
if (product) {
|
||
return {
|
||
name: (product.name as string) || (product.reference as string) || 'Produit catalogue',
|
||
reference: (product.reference as string) || null,
|
||
category: (product.typeProduct as AnyRecord)?.name as string || null,
|
||
suppliers: getProductSuppliersLabel(product),
|
||
price: getProductPriceLabel(product),
|
||
}
|
||
}
|
||
|
||
let fallbackName =
|
||
(source.productName ||
|
||
source.productLabel ||
|
||
source.typeProductLabel ||
|
||
(source.typeProduct as AnyRecord)?.name ||
|
||
(productId ? `Produit ${productId}` : null)) as string | null
|
||
let fallbackReference = (source.productReference || source.reference || null) as string | null
|
||
let fallbackCategory =
|
||
(source.productCategory ||
|
||
source.typeProductLabel ||
|
||
(source.typeProduct as AnyRecord)?.name ||
|
||
null) as string | null
|
||
let fallbackSuppliers =
|
||
(source.productSuppliers ||
|
||
source.constructeursLabel ||
|
||
source.supplierLabel ||
|
||
null) as string | null
|
||
let fallbackPrice =
|
||
(source.productPriceLabel ||
|
||
source.productPrice ||
|
||
source.priceLabel ||
|
||
source.price ||
|
||
null) as string | number | null
|
||
|
||
const structuralCandidates = [
|
||
source.products,
|
||
source.productSkeleton,
|
||
(source.definition as AnyRecord)?.products,
|
||
(source.definition as AnyRecord)?.productSkeleton,
|
||
((source.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
||
((source.definition as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||
(source.structure as AnyRecord)?.products,
|
||
(source.structure as AnyRecord)?.productSkeleton,
|
||
(source.requirement as AnyRecord)?.products,
|
||
(source.requirement as AnyRecord)?.productSkeleton,
|
||
((source.requirement as AnyRecord)?.structure as AnyRecord)?.products,
|
||
((source.requirement as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||
((source.requirement as AnyRecord)?.componentSkeleton as AnyRecord)?.products,
|
||
(source.typeMachineComponentRequirement as AnyRecord)?.products,
|
||
(source.typeMachineComponentRequirement as AnyRecord)?.productSkeleton,
|
||
((source.typeMachineComponentRequirement as AnyRecord)?.structure as AnyRecord)?.products,
|
||
((source.typeMachineComponentRequirement as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||
((source.typeMachineComponentRequirement as AnyRecord)?.componentSkeleton as AnyRecord)?.products,
|
||
(source.typeComposant as AnyRecord)?.products,
|
||
(source.typeComposant as AnyRecord)?.productSkeleton,
|
||
((source.typeComposant as AnyRecord)?.structure as AnyRecord)?.products,
|
||
((source.typeComposant as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||
(source.originalComposant as AnyRecord)?.products,
|
||
(source.originalComposant as AnyRecord)?.productSkeleton,
|
||
((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.products,
|
||
((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.productSkeleton,
|
||
(((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
||
(((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||
(source.originalComponent as AnyRecord)?.products,
|
||
(source.originalComponent as AnyRecord)?.productSkeleton,
|
||
((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.products,
|
||
((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.productSkeleton,
|
||
(((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
||
(((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||
]
|
||
|
||
const structuralProducts = structuralCandidates
|
||
.flatMap((candidate) => {
|
||
if (Array.isArray(candidate)) return candidate
|
||
if (candidate && typeof candidate === 'object' && Array.isArray((candidate as AnyRecord).products)) {
|
||
return (candidate as AnyRecord).products as unknown[]
|
||
}
|
||
return []
|
||
})
|
||
.filter((entry) => entry && typeof entry === 'object')
|
||
|
||
const structuralProduct = structuralProducts.length ? (structuralProducts[0] as AnyRecord) : null
|
||
|
||
const structuralFamilyCode =
|
||
(structuralProduct && typeof structuralProduct.familyCode === 'string'
|
||
? structuralProduct.familyCode
|
||
: null) ||
|
||
(typeof source.familyCode === 'string' ? source.familyCode : null)
|
||
|
||
if (!fallbackName && structuralProduct) {
|
||
fallbackName =
|
||
(structuralProduct.typeProductLabel as string) ||
|
||
((structuralProduct.typeProduct as AnyRecord)?.name as string) ||
|
||
(structuralProduct.reference as string) ||
|
||
(structuralFamilyCode ? `Famille ${structuralFamilyCode}` : null) ||
|
||
null
|
||
}
|
||
|
||
if (!fallbackReference && structuralProduct?.reference) {
|
||
fallbackReference = structuralProduct.reference as string
|
||
}
|
||
|
||
if (!fallbackCategory) {
|
||
fallbackCategory =
|
||
(structuralProduct?.typeProductLabel as string) ||
|
||
((structuralProduct?.typeProduct as AnyRecord)?.name as string) ||
|
||
(structuralFamilyCode ? `Famille ${structuralFamilyCode}` : null) ||
|
||
null
|
||
}
|
||
|
||
if (!fallbackSuppliers && structuralProduct?.supplierLabel) {
|
||
fallbackSuppliers = structuralProduct.supplierLabel as string
|
||
}
|
||
|
||
if (!fallbackSuppliers && Array.isArray(structuralProduct?.constructeurs)) {
|
||
const supplierNames = (structuralProduct!.constructeurs as AnyRecord[])
|
||
.map((c) => c?.name as string)
|
||
.filter((name) => typeof name === 'string' && name.trim().length > 0)
|
||
if (supplierNames.length) fallbackSuppliers = supplierNames.join(', ')
|
||
}
|
||
|
||
if (!fallbackPrice && structuralProduct?.priceLabel) fallbackPrice = structuralProduct.priceLabel as string
|
||
if (!fallbackPrice && structuralProduct?.price) fallbackPrice = structuralProduct.price as string | number
|
||
|
||
if (fallbackName || fallbackReference || fallbackCategory || fallbackSuppliers || fallbackPrice) {
|
||
return {
|
||
name: fallbackName || 'Produit catalogue',
|
||
reference: fallbackReference,
|
||
category: fallbackCategory,
|
||
suppliers: fallbackSuppliers,
|
||
price:
|
||
typeof fallbackPrice === 'number'
|
||
? `${fallbackPrice.toFixed(2)} €`
|
||
: (fallbackPrice as string) || null,
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Parent link identifiers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const extractParentLinkIdentifiers = (source: AnyRecord | null | undefined): AnyRecord => {
|
||
if (!source || typeof source !== 'object') return {}
|
||
|
||
const identifiers: AnyRecord = {}
|
||
|
||
const idKeys = [
|
||
'parentRequirementId',
|
||
'parentComponentRequirementId',
|
||
'parentPieceRequirementId',
|
||
'parentMachineComponentRequirementId',
|
||
'parentMachinePieceRequirementId',
|
||
'parentLinkId',
|
||
'parentComponentLinkId',
|
||
'parentPieceLinkId',
|
||
'parentComponentId',
|
||
'parentPieceId',
|
||
]
|
||
|
||
idKeys.forEach((key) => {
|
||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||
const value = source[key]
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
identifiers[key] = value
|
||
}
|
||
}
|
||
})
|
||
|
||
const objectKeys = [
|
||
'parentRequirement',
|
||
'parentComponentRequirement',
|
||
'parentPieceRequirement',
|
||
'parentMachineComponentRequirement',
|
||
'parentMachinePieceRequirement',
|
||
]
|
||
|
||
objectKeys.forEach((key) => {
|
||
const value = source[key]
|
||
if (isPlainObject(value) && value.id !== undefined && value.id !== null && value.id !== '') {
|
||
const idKey = `${key}Id`
|
||
if (!Object.prototype.hasOwnProperty.call(identifiers, idKey)) {
|
||
identifiers[idKey] = value.id
|
||
}
|
||
}
|
||
})
|
||
|
||
return identifiers
|
||
}
|