/** * Builds a component/piece hierarchy tree from flat machine link arrays. * * Extracted from pages/machine/[id].vue to keep the page orchestrator lean. */ import { resolveIdentifier, getProductDisplay } from '~/shared/utils/productDisplayUtils' import { resolveConstructeurs, uniqueConstructeurIds, type ConstructeurSummary } from '~/shared/constructeurUtils' type AnyRecord = Record // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const collectConstructeurs = ( allConstructeurs: AnyRecord[], ...sources: unknown[] ): AnyRecord[] => { const ids = uniqueConstructeurIds(...sources) if (!ids.length) return [] const pools = sources .flatMap((source) => { if (Array.isArray(source)) return [source] if (source && typeof source === 'object' && (source as AnyRecord).id) return [[source]] return [] }) .filter(Boolean) as AnyRecord[][] // ConstructeurSummary and AnyRecord are structurally compatible at runtime const allPools = [...pools, allConstructeurs] as unknown as Array return resolveConstructeurs(ids, ...allPools) as unknown as AnyRecord[] } // --------------------------------------------------------------------------- // Link array resolution // --------------------------------------------------------------------------- export const resolveLinkArray = (source: unknown, keys: string[]): unknown[] | null => { if (!source || typeof source !== 'object') return null for (const key of keys) { const value = (source as AnyRecord)[key] if (Array.isArray(value)) return value } return null } // --------------------------------------------------------------------------- // Merge trees (for incremental updates) // --------------------------------------------------------------------------- export function mergePieceLists(existing: AnyRecord[] = [], updates: AnyRecord[] = []): AnyRecord[] { if (!existing.length) { return updates.map((piece) => ({ ...piece, constructeurs: piece.constructeurs || [] })) } if (!updates.length) { return existing.map((piece) => ({ ...piece, constructeurs: piece.constructeurs || [] })) } const updateMap = new Map( updates.map((piece) => [piece.id, { ...piece, constructeurs: piece.constructeurs || [] }]), ) const merged = existing.map((piece) => { const update = updateMap.get(piece.id) if (!update) return piece return { ...piece, ...update, customFields: update.customFields ?? piece.customFields } }) updates.forEach((update) => { if (!existing.some((piece) => piece.id === update.id)) merged.push(update) }) return merged } export function mergeComponentTrees(existing: AnyRecord[] = [], updates: AnyRecord[] = []): AnyRecord[] { if (!existing.length) { return updates.map((component) => ({ ...component, constructeurs: component.constructeurs || [], pieces: ((component.pieces || []) as AnyRecord[]).map((piece) => ({ ...piece, constructeurs: piece.constructeurs || [], })), subComponents: mergeComponentTrees([], (component.subComponents || []) as AnyRecord[]), })) } if (!updates.length) return existing const updateMap = new Map( updates.map((component) => [ component.id, { ...component, constructeurs: component.constructeurs || [], pieces: ((component.pieces || []) as AnyRecord[]).map((piece) => ({ ...piece, constructeurs: piece.constructeurs || [], })), subComponents: mergeComponentTrees([], (component.subComponents || []) as AnyRecord[]), }, ]), ) const merged: AnyRecord[] = existing.map((component) => { const update = updateMap.get(component.id) if (!update) { return { ...component, constructeurs: component.constructeurs || [], pieces: mergePieceLists((component.pieces || []) as AnyRecord[], []), subComponents: mergeComponentTrees((component.subComponents || []) as AnyRecord[], []), } } return { ...component, ...update, customFields: update.customFields ?? component.customFields, pieces: mergePieceLists((component.pieces || []) as AnyRecord[], (update.pieces || []) as AnyRecord[]), subComponents: mergeComponentTrees( (component.subComponents || []) as AnyRecord[], (update.subComponents || []) as AnyRecord[], ), } }) updates.forEach((update) => { if (!existing.some((component) => component.id === update.id)) merged.push(update) }) return merged } // --------------------------------------------------------------------------- // Build hierarchy from links // --------------------------------------------------------------------------- export const buildMachineHierarchyFromLinks = ( componentLinks: AnyRecord[] = [], pieceLinks: AnyRecord[] = [], findProductById: (id: string) => AnyRecord | null, allConstructeurs: AnyRecord[] = [], ): { components: AnyRecord[]; machinePieces: AnyRecord[] } => { const normalizeComponentLinkId = (link: AnyRecord) => resolveIdentifier(link?.id, link?.linkId, link?.machineComponentLinkId) const normalizePieceLinkId = (link: AnyRecord) => resolveIdentifier(link?.id, link?.linkId, link?.machinePieceLinkId) const createPieceNode = (link: AnyRecord, parentComponentName: string | null = null): AnyRecord | null => { if (!link || typeof link !== 'object') return null const appliedPiece = (link.piece && typeof link.piece === 'object' ? link.piece : {}) as AnyRecord const originalPiece = (link.originalPiece && typeof link.originalPiece === 'object' ? link.originalPiece : null) as AnyRecord | null const machinePieceLinkId = normalizePieceLinkId(link) const pieceId = resolveIdentifier(appliedPiece.id, appliedPiece.pieceId, link.pieceId) const overrides = (link.overrides || null) as AnyRecord | null const basePiece: AnyRecord = { ...appliedPiece, id: appliedPiece.id || pieceId || machinePieceLinkId || `piece-${machinePieceLinkId}`, pieceId, name: overrides?.name || appliedPiece.name || (appliedPiece.definition as AnyRecord)?.name || (appliedPiece.definition as AnyRecord)?.role || originalPiece?.name || 'Pièce', reference: overrides?.reference || appliedPiece.reference || (appliedPiece.definition as AnyRecord)?.reference || originalPiece?.reference || null, prix: overrides?.prix ?? appliedPiece.prix ?? originalPiece?.prix ?? null, constructeur: appliedPiece.constructeur || originalPiece?.constructeur || null, constructeurId: appliedPiece.constructeurId || (appliedPiece.constructeur as AnyRecord)?.id || originalPiece?.constructeurId || null, documents: Array.isArray(appliedPiece.documents) ? appliedPiece.documents : Array.isArray(originalPiece?.documents) ? originalPiece!.documents : [], typePiece: appliedPiece.typePiece || null, typePieceId: appliedPiece.typePieceId || (appliedPiece.typePiece as AnyRecord)?.id || null, overrides, originalPiece, machinePieceLink: link, machinePieceLinkId, linkId: machinePieceLinkId, parentComponentLinkId: resolveIdentifier(link.parentComponentLinkId, link.parentLinkId, link.parentMachineComponentLinkId, appliedPiece.parentComponentLinkId), parentComponentId: resolveIdentifier(appliedPiece.parentComponentId, link.parentComponentId), parentComponentName, parentLinkId: resolveIdentifier(link.parentLinkId, link.parentMachinePieceLinkId, appliedPiece.parentLinkId), parentPieceLinkId: resolveIdentifier(link.parentPieceLinkId, appliedPiece.parentPieceLinkId), parentPieceId: resolveIdentifier(appliedPiece.parentPieceId, link.parentPieceId), quantity: typeof link.quantity === 'number' ? link.quantity : 1, definition: appliedPiece.definition || originalPiece?.definition || {}, customFields: appliedPiece.customFields || [], } const resolvedProductId = resolveIdentifier(appliedPiece.productId, (appliedPiece.product as AnyRecord)?.id, link.productId, (link.product as AnyRecord)?.id, originalPiece?.productId, (originalPiece?.product as AnyRecord)?.id) const resolvedProduct = (appliedPiece.product || link.product || originalPiece?.product || (resolvedProductId ? findProductById(resolvedProductId) : null) || null) as AnyRecord | null const constructeurs = collectConstructeurs(allConstructeurs, appliedPiece.constructeurs, appliedPiece.constructeur, appliedPiece.constructeurIds, appliedPiece.constructeurId, originalPiece?.constructeurs, originalPiece?.constructeur, originalPiece?.constructeurIds, originalPiece?.constructeurId) return { ...basePiece, constructeurs, constructeur: constructeurs[0] || basePiece.constructeur || null, constructeurId: (constructeurs[0] as AnyRecord)?.id || basePiece.constructeurId || null, productId: resolvedProductId || appliedPiece.productId || null, product: resolvedProduct || appliedPiece.product || null, __productDisplay: getProductDisplay({ product: resolvedProduct || appliedPiece.product || null, productId: resolvedProductId || appliedPiece.productId || null } as AnyRecord, findProductById), } } const createComponentNode = (link: AnyRecord): AnyRecord | null => { if (!link || typeof link !== 'object') return null const appliedComponent = (link.composant && typeof link.composant === 'object' ? link.composant : {}) as AnyRecord const originalComponent = (link.originalComposant && typeof link.originalComposant === 'object' ? link.originalComposant : null) as AnyRecord | null const machineComponentLinkId = normalizeComponentLinkId(link) const composantId = resolveIdentifier(appliedComponent.id, appliedComponent.composantId, link.composantId) const compOverrides = (link.overrides || null) as AnyRecord | null const componentName = (compOverrides?.name || appliedComponent.name || (appliedComponent.definition as AnyRecord)?.alias || (appliedComponent.definition as AnyRecord)?.name || originalComponent?.name || 'Composant') as string const linkedPieces = Array.isArray(link.pieceLinks) ? (link.pieceLinks as AnyRecord[]).map((pl) => createPieceNode(pl, componentName)).filter(Boolean) as AnyRecord[] : [] // If no linked pieces exist, build read-only entries from the composant's structure const structurePieceDefs = (!linkedPieces.length && appliedComponent.structure && typeof appliedComponent.structure === 'object') ? (Array.isArray((appliedComponent.structure as AnyRecord).pieces) ? (appliedComponent.structure as AnyRecord).pieces as AnyRecord[] : []) : [] const structurePieces = structurePieceDefs.map((def, index) => { const definition = (def.definition && typeof def.definition === 'object' ? def.definition : def) as AnyRecord const resolved = (def.resolvedPiece && typeof def.resolvedPiece === 'object' ? def.resolvedPiece : null) as AnyRecord | null const quantity = typeof definition.quantity === 'number' ? definition.quantity : (typeof def.quantity === 'number' ? def.quantity : 1) return { ...(resolved || {}), id: resolved?.id || `structure-piece-${composantId}-${index}`, pieceId: resolved?.id || null, name: resolved?.name || definition.role || definition.name || def.role || def.name || `Pièce ${index + 1}`, reference: resolved?.reference || definition.reference || def.reference || null, prix: resolved?.prix ?? null, constructeurs: resolved?.constructeurs || [], documents: [], quantity, slotId: def.slotId || definition.slotId || null, typePieceId: resolved?.typePieceId || definition.typePieceId || def.typePieceId || null, typePiece: resolved?.typePiece || null, parentComponentLinkId: machineComponentLinkId, parentComponentName: componentName, _structurePiece: true, } }) as AnyRecord[] const pieces = linkedPieces.length ? linkedPieces : structurePieces const subComponents = Array.isArray(link.childLinks) ? (link.childLinks as AnyRecord[]).map(createComponentNode).filter(Boolean) as AnyRecord[] : [] const resolvedProductId = resolveIdentifier(appliedComponent.productId, (appliedComponent.product as AnyRecord)?.id, link.productId, (link.product as AnyRecord)?.id, originalComponent?.productId, (originalComponent?.product as AnyRecord)?.id) const resolvedProduct = (appliedComponent.product || link.product || originalComponent?.product || (resolvedProductId ? findProductById(resolvedProductId) : null) || null) as AnyRecord | null const baseComponent: AnyRecord = { ...appliedComponent, id: appliedComponent.id || composantId || machineComponentLinkId || `component-${machineComponentLinkId}`, composantId, name: componentName, reference: compOverrides?.reference || appliedComponent.reference || (appliedComponent.definition as AnyRecord)?.reference || originalComponent?.reference || null, prix: compOverrides?.prix ?? appliedComponent.prix ?? originalComponent?.prix ?? null, constructeur: appliedComponent.constructeur || originalComponent?.constructeur || null, constructeurId: appliedComponent.constructeurId || (appliedComponent.constructeur as AnyRecord)?.id || originalComponent?.constructeurId || null, documents: Array.isArray(appliedComponent.documents) ? appliedComponent.documents : Array.isArray(originalComponent?.documents) ? originalComponent!.documents : [], typeComposant: appliedComponent.typeComposant || null, typeComposantId: appliedComponent.typeComposantId || (appliedComponent.typeComposant as AnyRecord)?.id || null, overrides: compOverrides, machineComponentLinkOverrides: compOverrides, definitionOverrides: compOverrides, originalComposant: originalComponent, machineComponentLink: link, machineComponentLinkId, componentLinkId: machineComponentLinkId, parentComponentLinkId: resolveIdentifier(link.parentComponentLinkId, link.parentLinkId, link.parentMachineComponentLinkId, appliedComponent.parentComponentLinkId), parentComposantId: resolveIdentifier(appliedComponent.parentComposantId, link.parentComponentId), definition: appliedComponent.definition || originalComponent?.definition || {}, customFields: appliedComponent.customFields || [], pieces, subComponents, subcomponents: subComponents, sousComposants: subComponents, } const constructeurs = collectConstructeurs(allConstructeurs, appliedComponent.constructeurs, appliedComponent.constructeur, appliedComponent.constructeurIds, appliedComponent.constructeurId, originalComponent?.constructeurs, originalComponent?.constructeur, originalComponent?.constructeurIds, originalComponent?.constructeurId) return { ...baseComponent, constructeurs, constructeur: constructeurs[0] || baseComponent.constructeur || null, constructeurId: (constructeurs[0] as AnyRecord)?.id || baseComponent.constructeurId || null, productId: resolvedProductId || appliedComponent.productId || null, product: resolvedProduct || appliedComponent.product || null, __productDisplay: getProductDisplay({ product: resolvedProduct || appliedComponent.product || null, productId: resolvedProductId || appliedComponent.productId || null } as AnyRecord, findProductById), } } const rootComponents = (Array.isArray(componentLinks) ? componentLinks : []) .filter((link) => !resolveIdentifier(link?.parentComponentLinkId, link?.parentLinkId, link?.parentMachineComponentLinkId)) .map(createComponentNode) .filter(Boolean) as AnyRecord[] const machinePieces = (Array.isArray(pieceLinks) ? pieceLinks : []) .filter((link) => !resolveIdentifier(link?.parentComponentLinkId, link?.parentLinkId, link?.parentMachineComponentLinkId)) .map((link) => createPieceNode(link, null)) .filter(Boolean) as AnyRecord[] return { components: rootComponents, machinePieces } }