refacto(F1.1): extract utility modules from machine/[id].vue
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>
This commit is contained in:
292
app/composables/useMachineHierarchy.ts
Normal file
292
app/composables/useMachineHierarchy.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* 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, resolveProductReference, getProductDisplay } from '~/shared/utils/productDisplayUtils'
|
||||
import { resolveConstructeurs, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
|
||||
type AnyRecord = Record<string, unknown>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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[][]
|
||||
|
||||
return resolveConstructeurs(ids, ...pools, allConstructeurs)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 = 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 requirement = (link.typeMachinePieceRequirement || appliedPiece.typeMachinePieceRequirement || originalPiece?.typeMachinePieceRequirement || null) as AnyRecord | null
|
||||
|
||||
const machinePieceLinkId = normalizePieceLinkId(link)
|
||||
const pieceId = resolveIdentifier(appliedPiece.id, appliedPiece.pieceId, link.pieceId)
|
||||
|
||||
const basePiece: AnyRecord = {
|
||||
...appliedPiece,
|
||||
id: appliedPiece.id || pieceId || machinePieceLinkId || `piece-${machinePieceLinkId}`,
|
||||
pieceId,
|
||||
name: link.overrides?.name || appliedPiece.name || (appliedPiece.definition as AnyRecord)?.name || (appliedPiece.definition as AnyRecord)?.role || originalPiece?.name || 'Pièce',
|
||||
reference: link.overrides?.reference || appliedPiece.reference || (appliedPiece.definition as AnyRecord)?.reference || originalPiece?.reference || null,
|
||||
prix: link.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 || requirement?.typePiece || null,
|
||||
typePieceId: appliedPiece.typePieceId || (appliedPiece.typePiece as AnyRecord)?.id || requirement?.typePieceId || (requirement?.typePiece as AnyRecord)?.id || null,
|
||||
typeMachinePieceRequirement: requirement,
|
||||
typeMachinePieceRequirementId: requirement?.id || null,
|
||||
requirementId: requirement?.id || null,
|
||||
overrides: link.overrides || null,
|
||||
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),
|
||||
parentMachineComponentRequirementId: resolveIdentifier(appliedPiece.parentMachineComponentRequirementId, link.parentMachineComponentRequirementId),
|
||||
parentMachinePieceRequirementId: resolveIdentifier(appliedPiece.parentMachinePieceRequirementId, link.parentMachinePieceRequirementId),
|
||||
definition: appliedPiece.definition || originalPiece?.definition || {},
|
||||
customFields: appliedPiece.customFields || [],
|
||||
skeletonOnly: !pieceId,
|
||||
}
|
||||
|
||||
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 requirement = (link.typeMachineComponentRequirement || appliedComponent.typeMachineComponentRequirement || originalComponent?.typeMachineComponentRequirement || null) as AnyRecord | null
|
||||
|
||||
const machineComponentLinkId = normalizeComponentLinkId(link)
|
||||
const composantId = resolveIdentifier(appliedComponent.id, appliedComponent.composantId, link.composantId)
|
||||
|
||||
const componentName = (link.overrides?.name || appliedComponent.name || (appliedComponent.definition as AnyRecord)?.alias || (appliedComponent.definition as AnyRecord)?.name || originalComponent?.name || 'Composant') as string
|
||||
|
||||
const pieces = Array.isArray(link.pieceLinks)
|
||||
? (link.pieceLinks as AnyRecord[]).map((pl) => createPieceNode(pl, componentName)).filter(Boolean) as AnyRecord[]
|
||||
: []
|
||||
|
||||
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: link.overrides?.reference || appliedComponent.reference || (appliedComponent.definition as AnyRecord)?.reference || originalComponent?.reference || null,
|
||||
prix: link.overrides?.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 || requirement?.typeComposant || null,
|
||||
typeComposantId: appliedComponent.typeComposantId || (appliedComponent.typeComposant as AnyRecord)?.id || requirement?.typeComposantId || (requirement?.typeComposant as AnyRecord)?.id || null,
|
||||
typeMachineComponentRequirement: requirement,
|
||||
typeMachineComponentRequirementId: requirement?.id || null,
|
||||
requirementId: requirement?.id || null,
|
||||
overrides: link.overrides || null,
|
||||
machineComponentLinkOverrides: link.overrides || null,
|
||||
definitionOverrides: link.overrides || null,
|
||||
originalComposant: originalComponent,
|
||||
machineComponentLink: link,
|
||||
machineComponentLinkId,
|
||||
componentLinkId: machineComponentLinkId,
|
||||
parentComponentLinkId: resolveIdentifier(link.parentComponentLinkId, link.parentLinkId, link.parentMachineComponentLinkId, appliedComponent.parentComponentLinkId),
|
||||
parentComposantId: resolveIdentifier(appliedComponent.parentComposantId, link.parentComponentId),
|
||||
parentRequirementId: resolveIdentifier(appliedComponent.parentRequirementId, link.parentRequirementId),
|
||||
parentMachineComponentRequirementId: resolveIdentifier(appliedComponent.parentMachineComponentRequirementId, link.parentMachineComponentRequirementId),
|
||||
parentMachinePieceRequirementId: resolveIdentifier(appliedComponent.parentMachinePieceRequirementId, link.parentMachinePieceRequirementId),
|
||||
definition: appliedComponent.definition || originalComponent?.definition || {},
|
||||
customFields: appliedComponent.customFields || [],
|
||||
pieces,
|
||||
subComponents,
|
||||
subcomponents: subComponents,
|
||||
sousComposants: subComponents,
|
||||
skeletonOnly: !composantId,
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
163
app/composables/useMachinePrint.ts
Normal file
163
app/composables/useMachinePrint.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Machine print selection and execution logic.
|
||||
*
|
||||
* Extracted from pages/machine/[id].vue.
|
||||
*/
|
||||
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
import { buildMachinePrintContext, buildMachinePrintHtml } from '~/utils/printTemplates/machineReport'
|
||||
import { resolveIdentifier } from '~/shared/utils/productDisplayUtils'
|
||||
|
||||
type AnyRecord = Record<string, unknown>
|
||||
|
||||
export interface PrintSelection {
|
||||
machine: { info: boolean; customFields: boolean; documents: boolean }
|
||||
components: Record<string, boolean>
|
||||
pieces: Record<string, boolean>
|
||||
}
|
||||
|
||||
export function useMachinePrint() {
|
||||
const printModalOpen = ref(false)
|
||||
const printSelection = reactive<PrintSelection>({
|
||||
machine: { info: true, customFields: true, documents: true },
|
||||
components: {},
|
||||
pieces: {},
|
||||
})
|
||||
|
||||
const ensurePrintSelectionEntries = (
|
||||
components: AnyRecord[],
|
||||
machinePieces: AnyRecord[],
|
||||
) => {
|
||||
printSelection.machine.info ??= true
|
||||
printSelection.machine.customFields ??= true
|
||||
printSelection.machine.documents ??= true
|
||||
|
||||
const ensureComponent = (component: AnyRecord) => {
|
||||
if (component?.id !== undefined && printSelection.components[component.id as string] === undefined) {
|
||||
printSelection.components[component.id as string] = true
|
||||
}
|
||||
;((component.pieces || []) as AnyRecord[]).forEach((piece) => {
|
||||
if (piece?.id !== undefined && printSelection.pieces[piece.id as string] === undefined) {
|
||||
printSelection.pieces[piece.id as string] = true
|
||||
}
|
||||
})
|
||||
;((component.subComponents || []) as AnyRecord[]).forEach(ensureComponent)
|
||||
}
|
||||
|
||||
components.forEach(ensureComponent)
|
||||
machinePieces.forEach((piece) => {
|
||||
if (piece?.id !== undefined && printSelection.pieces[piece.id as string] === undefined) {
|
||||
printSelection.pieces[piece.id as string] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setAllPrintSelection = (
|
||||
value: boolean,
|
||||
components: AnyRecord[],
|
||||
machinePieces: AnyRecord[],
|
||||
) => {
|
||||
ensurePrintSelectionEntries(components, machinePieces)
|
||||
printSelection.machine.info = value
|
||||
printSelection.machine.customFields = value
|
||||
printSelection.machine.documents = value
|
||||
Object.keys(printSelection.components).forEach((key) => {
|
||||
printSelection.components[key] = value
|
||||
})
|
||||
Object.keys(printSelection.pieces).forEach((key) => {
|
||||
printSelection.pieces[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
const openPrintModal = (components: AnyRecord[], machinePieces: AnyRecord[]) => {
|
||||
ensurePrintSelectionEntries(components, machinePieces)
|
||||
printModalOpen.value = true
|
||||
}
|
||||
|
||||
const closePrintModal = () => {
|
||||
printModalOpen.value = false
|
||||
}
|
||||
|
||||
const printMachine = (
|
||||
machine: AnyRecord,
|
||||
machineName: string,
|
||||
machineReference: string,
|
||||
machinePieces: AnyRecord[],
|
||||
components: AnyRecord[],
|
||||
currentSelection: PrintSelection = printSelection,
|
||||
) => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const context = buildMachinePrintContext({
|
||||
machine,
|
||||
machineName,
|
||||
machineReference,
|
||||
machinePieces,
|
||||
components,
|
||||
selection: currentSelection,
|
||||
})
|
||||
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style'))
|
||||
.map((node) => node.outerHTML)
|
||||
.join('')
|
||||
|
||||
const htmlContent = buildMachinePrintHtml(context, styles)
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.style.position = 'fixed'
|
||||
iframe.style.right = '0'
|
||||
iframe.style.bottom = '0'
|
||||
iframe.style.width = '0'
|
||||
iframe.style.height = '0'
|
||||
iframe.style.border = '0'
|
||||
iframe.setAttribute('title', 'print-frame')
|
||||
document.body.appendChild(iframe)
|
||||
|
||||
const iframeWindow = iframe.contentWindow
|
||||
const iframeDocument = iframe.contentDocument || iframeWindow?.document
|
||||
if (!iframeDocument || !iframeWindow) {
|
||||
iframe.remove()
|
||||
return
|
||||
}
|
||||
|
||||
iframeDocument.open()
|
||||
iframeDocument.write(htmlContent)
|
||||
iframeDocument.close()
|
||||
|
||||
const triggerPrint = () => {
|
||||
iframeWindow.focus()
|
||||
iframeWindow.print()
|
||||
setTimeout(() => {
|
||||
iframe.remove()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (iframeDocument.readyState === 'complete') {
|
||||
setTimeout(triggerPrint, 50)
|
||||
} else {
|
||||
iframe.onload = () => setTimeout(triggerPrint, 50)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrintConfirm = async (
|
||||
machine: AnyRecord,
|
||||
machineName: string,
|
||||
machineReference: string,
|
||||
machinePieces: AnyRecord[],
|
||||
components: AnyRecord[],
|
||||
) => {
|
||||
closePrintModal()
|
||||
await nextTick()
|
||||
printMachine(machine, machineName, machineReference, machinePieces, components, printSelection)
|
||||
}
|
||||
|
||||
return {
|
||||
printModalOpen,
|
||||
printSelection,
|
||||
ensurePrintSelectionEntries,
|
||||
setAllPrintSelection,
|
||||
openPrintModal,
|
||||
closePrintModal,
|
||||
printMachine,
|
||||
handlePrintConfirm,
|
||||
}
|
||||
}
|
||||
426
app/shared/utils/customFieldUtils.ts
Normal file
426
app/shared/utils/customFieldUtils.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* Custom field normalization, merging and display utilities.
|
||||
*
|
||||
* Extracted from pages/machine/[id].vue to be reusable across
|
||||
* machine detail, component, piece and product views.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Primitive helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const coerceValueForType = (type: string, rawValue: unknown): string => {
|
||||
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
||||
return ''
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
const normalized = String(rawValue).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') return 'true'
|
||||
if (normalized === 'false' || normalized === '0') return 'false'
|
||||
return ''
|
||||
}
|
||||
return String(rawValue)
|
||||
}
|
||||
|
||||
export const formatCustomFieldValue = (field: Record<string, unknown> | null | undefined): string => {
|
||||
if (!field) return 'Non défini'
|
||||
|
||||
const value = (field.value ?? field.defaultValue ?? '') as string
|
||||
if (value === '' || value === null || value === undefined) return 'Non défini'
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
const normalized = String(value).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') return 'Oui'
|
||||
if (normalized === 'false' || normalized === '0') return 'Non'
|
||||
}
|
||||
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export const shouldDisplayCustomField = (field: Record<string, unknown> | null | undefined): boolean => {
|
||||
if (!field) return false
|
||||
if (field.readOnly) return true
|
||||
if (field.type === 'boolean') return field.value !== undefined && field.value !== null
|
||||
|
||||
const value = field.value
|
||||
if (value === null || value === undefined) return false
|
||||
if (typeof value === 'string') return value.trim().length > 0
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Definition extraction helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const extractDefinitionName = (definition: Record<string, unknown> = {}): string => {
|
||||
if (typeof definition?.name === 'string' && (definition.name as string).trim()) {
|
||||
return (definition.name as string).trim()
|
||||
}
|
||||
if (typeof definition?.key === 'string' && (definition.key as string).trim()) {
|
||||
return (definition.key as string).trim()
|
||||
}
|
||||
if (typeof definition?.label === 'string' && (definition.label as string).trim()) {
|
||||
return (definition.label as string).trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export const extractDefinitionType = (
|
||||
definition: Record<string, unknown> = {},
|
||||
fallback = 'text',
|
||||
): string => {
|
||||
const allowed = ['text', 'number', 'select', 'boolean', 'date']
|
||||
const rawType =
|
||||
typeof definition?.type === 'string'
|
||||
? definition.type
|
||||
: typeof (definition?.value as Record<string, unknown>)?.type === 'string'
|
||||
? (definition.value as Record<string, unknown>).type as string
|
||||
: typeof fallback === 'string'
|
||||
? fallback
|
||||
: 'text'
|
||||
const normalized = (rawType as string).toLowerCase()
|
||||
return allowed.includes(normalized) ? normalized : 'text'
|
||||
}
|
||||
|
||||
export const extractDefinitionRequired = (
|
||||
definition: Record<string, unknown> = {},
|
||||
fallback = false,
|
||||
): boolean => {
|
||||
if (typeof definition?.required === 'boolean') return definition.required
|
||||
const nested = (definition?.value as Record<string, unknown>)?.required
|
||||
if (typeof nested === 'boolean') return nested
|
||||
if (typeof nested === 'string') {
|
||||
const normalized = nested.toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') return true
|
||||
if (normalized === 'false' || normalized === '0') return false
|
||||
}
|
||||
return !!fallback
|
||||
}
|
||||
|
||||
const extractOptionList = (input: unknown): string[] | undefined => {
|
||||
if (!Array.isArray(input)) return undefined
|
||||
const mapped = input
|
||||
.map((option) => {
|
||||
if (option === null || option === undefined) return ''
|
||||
if (typeof option === 'string') return option.trim()
|
||||
if (typeof option === 'object') {
|
||||
const record = (option || {}) as Record<string, unknown>
|
||||
for (const key of ['value', 'label', 'name']) {
|
||||
const candidate = record[key]
|
||||
if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate.trim()
|
||||
}
|
||||
}
|
||||
const fallback = String(option).trim()
|
||||
return fallback === '[object Object]' ? '' : fallback
|
||||
})
|
||||
.filter((option) => option.length > 0)
|
||||
return mapped.length ? mapped : undefined
|
||||
}
|
||||
|
||||
export const extractDefinitionOptions = (definition: Record<string, unknown> = {}): string[] => {
|
||||
const sources = [
|
||||
definition?.options,
|
||||
(definition?.value as Record<string, unknown>)?.options,
|
||||
(definition?.value as Record<string, unknown>)?.choices,
|
||||
]
|
||||
for (const source of sources) {
|
||||
const list = extractOptionList(source)
|
||||
if (list) return list
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export const extractDefinitionDefaultValue = (definition: Record<string, unknown> = {}): unknown => {
|
||||
const candidates = [
|
||||
definition?.defaultValue,
|
||||
(definition?.value as Record<string, unknown>)?.defaultValue,
|
||||
(definition?.value as Record<string, unknown>)?.value,
|
||||
definition?.value,
|
||||
definition?.default,
|
||||
]
|
||||
for (const candidate of candidates) {
|
||||
if (candidate === undefined || candidate === null || candidate === '') continue
|
||||
if (typeof candidate === 'object') {
|
||||
if (candidate === null) continue
|
||||
const nestedDefault =
|
||||
(candidate as Record<string, unknown>).defaultValue !== undefined &&
|
||||
(candidate as Record<string, unknown>).defaultValue !== null
|
||||
? (candidate as Record<string, unknown>).defaultValue
|
||||
: (candidate as Record<string, unknown>).value
|
||||
if (nestedDefault !== undefined && nestedDefault !== null && nestedDefault !== '') {
|
||||
return nestedDefault
|
||||
}
|
||||
continue
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normalization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface NormalizedCustomFieldDefinition {
|
||||
id?: string
|
||||
customFieldId?: string
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
defaultValue?: unknown
|
||||
readOnly: boolean
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
export const normalizeCustomFieldDefinitionEntry = (
|
||||
definition: Record<string, unknown> = {},
|
||||
fallbackIndex = 0,
|
||||
): NormalizedCustomFieldDefinition | null => {
|
||||
const name = extractDefinitionName(definition)
|
||||
if (!name) return null
|
||||
const type = extractDefinitionType(definition)
|
||||
const required = extractDefinitionRequired(definition)
|
||||
const options = extractDefinitionOptions(definition)
|
||||
const defaultValue = extractDefinitionDefaultValue(definition)
|
||||
const id = typeof definition?.id === 'string' ? definition.id : undefined
|
||||
const customFieldId = typeof definition?.customFieldId === 'string' ? definition.customFieldId : id
|
||||
const orderIndex = typeof definition?.orderIndex === 'number' ? definition.orderIndex : fallbackIndex
|
||||
return { id, customFieldId, name, type, required, options, defaultValue, readOnly: !!definition?.readOnly, orderIndex }
|
||||
}
|
||||
|
||||
export const normalizeExistingCustomFieldDefinitions = (
|
||||
fields: unknown,
|
||||
): NormalizedCustomFieldDefinition[] => {
|
||||
if (!Array.isArray(fields)) return []
|
||||
return fields
|
||||
.map((field, index) => normalizeCustomFieldDefinitionEntry(field as Record<string, unknown>, index))
|
||||
.filter((d): d is NormalizedCustomFieldDefinition => d !== null)
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom field value normalization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const normalizeCustomFieldValueEntry = (entry: Record<string, unknown> = {}): Record<string, unknown> | null => {
|
||||
if (!entry || typeof entry !== 'object') return null
|
||||
const normalizedDefinition = normalizeCustomFieldDefinitionEntry(entry)
|
||||
if (!normalizedDefinition) return null
|
||||
|
||||
const value = coerceValueForType(
|
||||
normalizedDefinition.type,
|
||||
(entry?.value ?? entry?.defaultValue ?? normalizedDefinition.defaultValue ?? '') as string,
|
||||
)
|
||||
|
||||
return {
|
||||
id: (entry?.customFieldValueId ?? entry?.id ?? null) as string | null,
|
||||
customFieldId:
|
||||
(entry?.customFieldId ?? normalizedDefinition.customFieldId ?? normalizedDefinition.id ?? null) as string | null,
|
||||
customField: {
|
||||
id: normalizedDefinition.id ?? normalizedDefinition.customFieldId ?? null,
|
||||
name: normalizedDefinition.name,
|
||||
type: normalizedDefinition.type,
|
||||
required: normalizedDefinition.required,
|
||||
options: normalizedDefinition.options,
|
||||
defaultValue: normalizedDefinition.defaultValue ?? '',
|
||||
},
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Merge & dedup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const mergeCustomFieldValuesWithDefinitions = (
|
||||
valueEntries: Record<string, unknown>[] = [],
|
||||
...definitionSources: unknown[][]
|
||||
): Record<string, unknown>[] => {
|
||||
const normalizedValues = (Array.isArray(valueEntries) ? valueEntries : [])
|
||||
.map((entry) => {
|
||||
if (!entry || typeof entry !== 'object') return null
|
||||
const normalizedDefinition = normalizeCustomFieldDefinitionEntry(
|
||||
((entry as Record<string, unknown>).customField || entry) as Record<string, unknown>,
|
||||
)
|
||||
if (!normalizedDefinition) return null
|
||||
|
||||
const value = coerceValueForType(
|
||||
normalizedDefinition.type,
|
||||
((entry as Record<string, unknown>)?.value ??
|
||||
(entry as Record<string, unknown>)?.defaultValue ??
|
||||
normalizedDefinition.defaultValue ??
|
||||
'') as string,
|
||||
)
|
||||
|
||||
return {
|
||||
customFieldValueId: (entry as Record<string, unknown>)?.id ?? (entry as Record<string, unknown>)?.customFieldValueId ?? null,
|
||||
id: normalizedDefinition.id,
|
||||
customFieldId: normalizedDefinition.customFieldId ?? normalizedDefinition.id ?? null,
|
||||
name: normalizedDefinition.name,
|
||||
type: normalizedDefinition.type,
|
||||
required: normalizedDefinition.required,
|
||||
options: normalizedDefinition.options,
|
||||
optionsText: normalizedDefinition.options?.length ? normalizedDefinition.options.join('\n') : '',
|
||||
defaultValue: normalizedDefinition.defaultValue ?? '',
|
||||
value,
|
||||
readOnly: !!(entry as Record<string, unknown>)?.readOnly,
|
||||
}
|
||||
})
|
||||
.filter((entry): entry is Record<string, unknown> => entry !== null)
|
||||
|
||||
const result = [...normalizedValues]
|
||||
const keyFor = (item: Record<string, unknown>) => (item?.id as string) ?? `${item?.name ?? ''}::${item?.type ?? ''}`
|
||||
const existingMap = new Map<string, Record<string, unknown>>()
|
||||
|
||||
result.forEach((item) => {
|
||||
const key = keyFor(item)
|
||||
if (key) existingMap.set(key, item)
|
||||
const fallbackKey = item?.name ? `${item.name}::${item.type ?? ''}` : null
|
||||
if (fallbackKey) existingMap.set(fallbackKey, item)
|
||||
})
|
||||
|
||||
const definitions = definitionSources
|
||||
.flatMap((source) => (Array.isArray(source) ? source : []))
|
||||
.map((definition) => normalizeCustomFieldDefinitionEntry(definition as Record<string, unknown>))
|
||||
.filter((d): d is NormalizedCustomFieldDefinition => d !== null)
|
||||
|
||||
definitions.forEach((normalizedDefinition) => {
|
||||
const key = normalizedDefinition.id ?? `${normalizedDefinition.name}::${normalizedDefinition.type}`
|
||||
if (!key) return
|
||||
|
||||
if (normalizedDefinition.id) {
|
||||
const fallbackKey = `${normalizedDefinition.name}::${normalizedDefinition.type}`
|
||||
if (existingMap.has(fallbackKey)) {
|
||||
const existingFallback = existingMap.get(fallbackKey)
|
||||
if (existingFallback) {
|
||||
existingFallback.id = existingFallback.id || normalizedDefinition.id
|
||||
existingFallback.customFieldId = normalizedDefinition.id
|
||||
existingFallback.readOnly = (existingFallback.readOnly as boolean) && normalizedDefinition.readOnly
|
||||
existingMap.delete(fallbackKey)
|
||||
existingMap.set(normalizedDefinition.id, existingFallback)
|
||||
existingMap.set(fallbackKey, existingFallback)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existing =
|
||||
existingMap.get(key) ||
|
||||
(normalizedDefinition.name ? existingMap.get(`${normalizedDefinition.name}::${normalizedDefinition.type}`) : null)
|
||||
|
||||
if (existing) {
|
||||
existing.name = existing.name || normalizedDefinition.name
|
||||
existing.type = existing.type || normalizedDefinition.type
|
||||
existing.required = (existing.required as boolean) || normalizedDefinition.required
|
||||
if (!(existing.options as string[])?.length && normalizedDefinition.options?.length) {
|
||||
existing.options = normalizedDefinition.options
|
||||
}
|
||||
if (!existing.defaultValue && normalizedDefinition.defaultValue) {
|
||||
existing.defaultValue = String(normalizedDefinition.defaultValue)
|
||||
if (!existing.value) {
|
||||
existing.value = coerceValueForType(existing.type as string, normalizedDefinition.defaultValue)
|
||||
}
|
||||
}
|
||||
existing.customFieldId = existing.customFieldId || normalizedDefinition.id
|
||||
existing.readOnly = (existing.readOnly as boolean) && normalizedDefinition.readOnly
|
||||
if (!existing.optionsText && normalizedDefinition.options?.length) {
|
||||
existing.optionsText = normalizedDefinition.options.join('\n')
|
||||
}
|
||||
if (normalizedDefinition.id) existingMap.set(normalizedDefinition.id, existing)
|
||||
if (normalizedDefinition.name) {
|
||||
existingMap.set(`${normalizedDefinition.name}::${normalizedDefinition.type}`, existing)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const entry: Record<string, unknown> = {
|
||||
customFieldValueId: null,
|
||||
id: normalizedDefinition.id,
|
||||
customFieldId: normalizedDefinition.id,
|
||||
name: normalizedDefinition.name,
|
||||
type: normalizedDefinition.type,
|
||||
required: normalizedDefinition.required,
|
||||
options: normalizedDefinition.options,
|
||||
optionsText: normalizedDefinition.options?.length ? normalizedDefinition.options.join('\n') : '',
|
||||
defaultValue: normalizedDefinition.defaultValue ?? '',
|
||||
value: coerceValueForType(normalizedDefinition.type, (normalizedDefinition.defaultValue ?? '') as string),
|
||||
readOnly: false,
|
||||
}
|
||||
result.push(entry)
|
||||
existingMap.set(key, entry)
|
||||
const fallbackKey = entry.name ? `${entry.name}::${entry.type}` : null
|
||||
if (fallbackKey) existingMap.set(fallbackKey, entry)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const dedupeCustomFieldEntries = (fields: Record<string, unknown>[]): Record<string, unknown>[] => {
|
||||
if (!Array.isArray(fields) || fields.length <= 1) {
|
||||
return Array.isArray(fields) ? fields : []
|
||||
}
|
||||
|
||||
const seen = new Set<string>()
|
||||
const result: Record<string, unknown>[] = []
|
||||
|
||||
for (const field of fields) {
|
||||
if (!field) continue
|
||||
|
||||
field.type = field.type || 'text'
|
||||
|
||||
let normalizedName = typeof field.name === 'string' ? (field.name as string).trim() : ''
|
||||
|
||||
if (!normalizedName && (field.customField as Record<string, unknown>)?.name) {
|
||||
normalizedName = String((field.customField as Record<string, unknown>).name).trim()
|
||||
field.name = normalizedName
|
||||
} else if (typeof field.name === 'string') {
|
||||
field.name = normalizedName
|
||||
}
|
||||
|
||||
const key =
|
||||
(field.customFieldId as string) ||
|
||||
(field.id as string) ||
|
||||
(normalizedName ? `${normalizedName}::${field.type || 'text'}` : null)
|
||||
|
||||
if (!key && !normalizedName) continue
|
||||
if (key && seen.has(key)) continue
|
||||
if (!normalizedName) continue
|
||||
|
||||
if (key) seen.add(key)
|
||||
if (normalizedName) seen.add(`${normalizedName}::${field.type || 'text'}`)
|
||||
result.push(field)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Summarize for display
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const summarizeCustomFields = (
|
||||
fields: Record<string, unknown>[] = [],
|
||||
): { key: string; label: string; value: string }[] => {
|
||||
const seen = new Set<string>()
|
||||
return fields
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const left = typeof a?.orderIndex === 'number' ? a.orderIndex : 0
|
||||
const right = typeof b?.orderIndex === 'number' ? b.orderIndex : 0
|
||||
return (left as number) - (right as number)
|
||||
})
|
||||
.filter(shouldDisplayCustomField)
|
||||
.filter((field) => {
|
||||
const key = (field.customFieldId || field.id || field.name) as string
|
||||
if (!key) return true
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.map((field, index) => ({
|
||||
key: ((field.customFieldId || field.id || field.name) as string) || `custom-field-${index}`,
|
||||
label: (field.name as string) || 'Champ',
|
||||
value: formatCustomFieldValue(field),
|
||||
}))
|
||||
}
|
||||
353
app/shared/utils/productDisplayUtils.ts
Normal file
353
app/shared/utils/productDisplayUtils.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user