Files
Inventory_frontend/app/composables/useMachineDetailData.ts
Matthieu daaa1c4cb9 refactor(machine): decompose detail page into composables + 7 components (F1.1)
Extract 2 composables (useMachineDetailData, useMachineSkeletonEditor) and
7 UI components from machine/[id].vue, reducing it from 2989 to 219 LOC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 11:19:22 +01:00

1405 lines
49 KiB
TypeScript

/**
* Machine detail page — core state & business logic.
*
* Extracted from pages/machine/[id].vue (F1.1).
* Manages reactive state, data loading, transforms, updates,
* document management and custom field logic.
*/
import { ref, computed, watch } from 'vue'
import { useMachines } from '~/composables/useMachines'
import { useComposants } from '~/composables/useComposants'
import { usePieces } from '~/composables/usePieces'
import { useComponentTypes } from '~/composables/useComponentTypes'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { useCustomFields } from '~/composables/useCustomFields'
import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
import { useConstructeurs } from '~/composables/useConstructeurs'
import { useProducts } from '~/composables/useProducts'
import { useMachinePrint } from '~/composables/useMachinePrint'
import { getFileIcon } from '~/utils/fileIcons'
import { normalizeStructureForEditor } from '~/shared/modelUtils'
import {
resolveConstructeurs,
uniqueConstructeurIds,
formatConstructeurContact as formatConstructeurContactSummary,
} from '~/shared/constructeurUtils'
import {
formatCustomFieldValue,
shouldDisplayCustomField,
normalizeExistingCustomFieldDefinitions,
normalizeCustomFieldValueEntry,
mergeCustomFieldValuesWithDefinitions,
dedupeCustomFieldEntries,
summarizeCustomFields,
} from '~/shared/utils/customFieldUtils'
import {
resolveIdentifier,
resolveProductReference as _resolveProductReference,
getProductDisplay as _getProductDisplay,
extractParentLinkIdentifiers,
} from '~/shared/utils/productDisplayUtils'
import {
buildMachineHierarchyFromLinks,
resolveLinkArray,
} from '~/composables/useMachineHierarchy'
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
import {
formatSize,
shouldInlinePdf,
documentPreviewSrc,
documentThumbnailClass,
documentIcon,
downloadDocument as downloadDocumentHelper,
} from '~/shared/utils/documentDisplayUtils'
type AnyRecord = Record<string, unknown>
export function useMachineDetailData(machineId: string) {
// ---------------------------------------------------------------------------
// External composables
// ---------------------------------------------------------------------------
const {
updateMachine: updateMachineApi,
reconfigureSkeleton: reconfigureMachineSkeleton,
} = useMachines()
const { updateComposant: updateComposantApi } = useComposants()
const { updatePiece: updatePieceApi } = usePieces()
const { componentTypes, loadComponentTypes } = useComponentTypes()
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const { products, loadProducts } = useProducts()
const {
upsertCustomFieldValue,
updateCustomFieldValue: updateCustomFieldValueApi,
} = useCustomFields()
const { get } = useApi()
const {
uploadDocuments,
deleteDocument,
loadDocumentsByMachine,
} = useDocuments()
const toast = useToast()
const { constructeurs, loadConstructeurs } = useConstructeurs()
const {
printModalOpen,
printSelection,
ensurePrintSelectionEntries: _ensurePrintEntries,
setAllPrintSelection: _setAllPrint,
openPrintModal: _openPrintModal,
closePrintModal,
handlePrintConfirm: _handlePrintConfirm,
} = useMachinePrint()
// ---------------------------------------------------------------------------
// Core state
// ---------------------------------------------------------------------------
const loading = ref(true)
const machine = ref<AnyRecord | null>(null)
const components = ref<AnyRecord[]>([])
const pieces = ref<AnyRecord[]>([])
const machineComponentLinks = ref<AnyRecord[]>([])
const machinePieceLinks = ref<AnyRecord[]>([])
const machineProductLinks = ref<AnyRecord[]>([])
const printAreaRef = ref<HTMLElement | null>(null)
// ---------------------------------------------------------------------------
// Machine fields
// ---------------------------------------------------------------------------
const machineName = ref('')
const machineReference = ref('')
const machineConstructeurIds = ref<string[]>([])
const machineConstructeurId = computed({
get: () => machineConstructeurIds.value[0] || null,
set: (value: string | null) => {
machineConstructeurIds.value = value ? [value] : []
},
})
const machineConstructeursDisplay = computed(() => {
const ids = uniqueConstructeurIds(
machineConstructeurIds.value,
(machine.value as AnyRecord)?.constructeurIds,
(machine.value as AnyRecord)?.constructeurs,
(machine.value as AnyRecord)?.constructeur,
)
return resolveConstructeurs(
ids,
Array.isArray((machine.value as AnyRecord)?.constructeurs)
? ((machine.value as AnyRecord).constructeurs as any[])
: [],
(machine.value as AnyRecord)?.constructeur
? [(machine.value as AnyRecord).constructeur as any]
: [],
constructeurs.value as any,
) as any[]
})
const machineConstructeurContact = computed(() =>
machineConstructeursDisplay.value
.map((c: any) => formatConstructeurContactSummary(c))
.filter(Boolean)
.join(' • '),
)
const hasMachineConstructeur = computed(
() => machineConstructeursDisplay.value.length > 0,
)
// ---------------------------------------------------------------------------
// UI state
// ---------------------------------------------------------------------------
const machineDocumentFiles = ref<File[]>([])
const machineDocumentsUploading = ref(false)
const machineDocumentsLoaded = ref(false)
const machineCustomFields = ref<AnyRecord[]>([])
const previewDocument = ref<AnyRecord | null>(null)
const previewVisible = ref(false)
const isEditMode = ref(false)
const debug = ref(false)
const componentsCollapsed = ref(true)
const collapseToggleToken = ref(0)
// ---------------------------------------------------------------------------
// Product helpers
// ---------------------------------------------------------------------------
const machineType = computed(() => (machine.value as AnyRecord)?.typeMachine || null)
const componentTypeOptions = computed(() => componentTypes.value || [])
const pieceTypeOptions = computed(() => pieceTypes.value || [])
const componentRequirements = computed(
() => ((machineType.value as AnyRecord)?.componentRequirements as AnyRecord[]) || [],
)
const pieceRequirements = computed(
() => ((machineType.value as AnyRecord)?.pieceRequirements as AnyRecord[]) || [],
)
const productRequirements = computed(
() => ((machineType.value as AnyRecord)?.productRequirements as AnyRecord[]) || [],
)
const machineHasSkeletonRequirements = computed(() =>
componentRequirements.value.length > 0 ||
pieceRequirements.value.length > 0 ||
productRequirements.value.length > 0,
)
const componentTypeLabelMap = computed(() => {
const map = new Map<string, string>()
componentTypeOptions.value.forEach((type) => {
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
componentRequirements.value.forEach((req: AnyRecord) => {
const type = req.typeComposant as AnyRecord | undefined
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
return map
})
const pieceTypeLabelMap = computed(() => {
const map = new Map<string, string>()
pieceTypeOptions.value.forEach((type) => {
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
pieceRequirements.value.forEach((req: AnyRecord) => {
const type = req.typePiece as AnyRecord | undefined
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
return map
})
const productInventory = computed(() => products.value || [])
const productById = computed(() => {
const map = new Map<string, AnyRecord>()
;(productInventory.value as AnyRecord[]).forEach((product: AnyRecord) => {
if (product?.id) map.set(product.id as string, product)
})
return map
})
const findProductById = (productId: string | null | undefined): AnyRecord | null => {
if (!productId) return null
return productById.value.get(productId) || null
}
const resolveProductReference = (source: AnyRecord) =>
_resolveProductReference(source, findProductById as any)
const getProductDisplay = (source: AnyRecord) =>
_getProductDisplay(source, findProductById as any)
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const isPlainObject = (value: unknown): boolean =>
Object.prototype.toString.call(value) === '[object Object]'
const flattenComponents = (list: AnyRecord[] = []): AnyRecord[] => {
const result: AnyRecord[] = []
const traverse = (items: AnyRecord[]) => {
items.forEach((item) => {
result.push(item)
if (Array.isArray(item.subComponents) && item.subComponents.length) {
traverse(item.subComponents as AnyRecord[])
}
})
}
traverse(list)
return result
}
const findComponentById = (items: AnyRecord[] | undefined, id: string): AnyRecord | null => {
for (const item of items || []) {
if (item.id === id) return item
const found = findComponentById(item.subComponents as AnyRecord[] | undefined, id)
if (found) return found
}
return null
}
const findPieceById = (pieceId: string): AnyRecord | null => {
const direct = pieces.value.find((p) => p.id === pieceId)
if (direct) return direct
const searchInComponents = (items: AnyRecord[]): AnyRecord | null => {
for (const item of items || []) {
const match = ((item.pieces as AnyRecord[]) || []).find((p) => p.id === pieceId)
if (match) return match
const nested = searchInComponents((item.subComponents as AnyRecord[]) || [])
if (nested) return nested
}
return null
}
return searchInComponents(components.value)
}
const collectConstructeurs = (...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 as any[])) as any[]
}
// ---------------------------------------------------------------------------
// Transform functions
// ---------------------------------------------------------------------------
const getStructureCustomFields = (structure: unknown): AnyRecord[] => {
if (!structure || typeof structure !== 'object') return []
const normalized = normalizeStructureForEditor(structure as any) as any
return Array.isArray(normalized?.customFields)
? (normalized.customFields as AnyRecord[])
: []
}
const transformCustomFields = (piecesData: AnyRecord[]): AnyRecord[] => {
return (piecesData || []).map((piece) => {
const requirement = (piece.typeMachinePieceRequirement as AnyRecord) || {}
const typePiece = (requirement.typePiece as AnyRecord) || (piece.typePiece as AnyRecord) || {}
const normalizeStructureDefs = (structure: unknown) =>
structure ? normalizeStructureForEditor(structure as AnyRecord) : null
const normalizedStructureDefs = [
normalizeStructureDefs((piece.definition as AnyRecord)?.structure),
normalizeStructureDefs((piece.typePiece as AnyRecord)?.structure),
normalizeStructureDefs(typePiece.structure),
normalizeStructureDefs(typePiece.pieceSkeleton),
normalizeStructureDefs((piece.typePiece as AnyRecord)?.pieceSkeleton),
normalizeStructureDefs(requirement.structure),
normalizeStructureDefs(requirement.pieceSkeleton),
]
const valueEntries = [
...(Array.isArray(piece.customFieldValues) ? piece.customFieldValues : []),
...(Array.isArray(piece.customFields)
? (piece.customFields as AnyRecord[])
.map(normalizeCustomFieldValueEntry)
.filter((e) => e !== null)
: []),
...(Array.isArray(typePiece.customFieldValues)
? (typePiece.customFieldValues as AnyRecord[])
.map(normalizeCustomFieldValueEntry)
.filter((e) => e !== null)
: []),
]
const customFields = dedupeCustomFieldEntries(
mergeCustomFieldValuesWithDefinitions(
valueEntries,
normalizeExistingCustomFieldDefinitions(piece.customFields),
normalizeExistingCustomFieldDefinitions((piece.definition as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions((piece.typePiece as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions(typePiece.customFields),
normalizeExistingCustomFieldDefinitions((requirement.typePiece as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions(requirement.customFields),
normalizeExistingCustomFieldDefinitions((requirement.definition as AnyRecord)?.customFields),
...normalizedStructureDefs.map((def) => getStructureCustomFields(def)),
),
)
const constructeurIds = uniqueConstructeurIds(
piece.constructeurIds,
piece.constructeurId,
piece.constructeur,
(piece.originalPiece as AnyRecord)?.constructeurIds,
(piece.originalPiece as AnyRecord)?.constructeurId,
(piece.originalPiece as AnyRecord)?.constructeur,
)
const { product: resolvedProduct, productId: resolvedProductId } =
resolveProductReference(piece)
const constructeursList = resolveConstructeurs(
constructeurIds,
Array.isArray(piece.constructeurs) ? (piece.constructeurs as any[]) : [],
piece.constructeur ? [piece.constructeur as any] : [],
Array.isArray((piece.originalPiece as AnyRecord)?.constructeurs)
? ((piece.originalPiece as AnyRecord).constructeurs as any[])
: [],
(piece.originalPiece as AnyRecord)?.constructeur
? [(piece.originalPiece as AnyRecord).constructeur as any]
: [],
constructeurs.value as any,
) as any[]
const normalizedPiece = {
...piece,
product: resolvedProduct || piece.product || null,
productId: resolvedProductId || piece.productId || (piece.product as AnyRecord)?.id || null,
}
const productDisplay = getProductDisplay(normalizedPiece)
return {
...normalizedPiece,
customFields,
documents: piece.documents || [],
constructeurs: constructeursList,
constructeur: constructeursList[0] || piece.constructeur || null,
constructeurIds,
constructeurId: constructeurIds[0] || null,
typePieceId:
piece.typePieceId ||
(piece.typeMachinePieceRequirement as AnyRecord)?.typePieceId ||
(piece.typePiece as AnyRecord)?.id ||
null,
__productDisplay: productDisplay,
}
})
}
const transformComponentCustomFields = (componentsData: AnyRecord[]): AnyRecord[] => {
const normalizeStructureDefs = (structure: unknown) =>
structure ? normalizeStructureForEditor(structure as AnyRecord) : null
return (componentsData || []).map((component) => {
const requirement = (component.typeMachineComponentRequirement as AnyRecord) || {}
const type = (requirement.typeComposant as AnyRecord) || (component.typeComposant as AnyRecord) || {}
const normalizedStructureDefs = [
normalizeStructureDefs((component.definition as AnyRecord)?.structure),
normalizeStructureDefs((component.typeComposant as AnyRecord)?.structure),
normalizeStructureDefs(type.structure),
normalizeStructureDefs(type.componentSkeleton),
normalizeStructureDefs(requirement.structure),
normalizeStructureDefs(requirement.componentSkeleton),
]
const actualComponent = (component.originalComposant as AnyRecord) || component
const valueEntries = [
...(Array.isArray(component.customFieldValues) ? component.customFieldValues : []),
...(Array.isArray(component.customFields)
? (component.customFields as AnyRecord[])
.map(normalizeCustomFieldValueEntry)
.filter((e) => e !== null)
: []),
...(Array.isArray(actualComponent?.customFields)
? (actualComponent.customFields as AnyRecord[])
.map(normalizeCustomFieldValueEntry)
.filter((e) => e !== null)
: []),
]
const customFields = dedupeCustomFieldEntries(
mergeCustomFieldValuesWithDefinitions(
valueEntries,
normalizeExistingCustomFieldDefinitions(component.customFields),
normalizeExistingCustomFieldDefinitions((component.definition as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions((component.typeComposant as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions(type.customFields),
normalizeExistingCustomFieldDefinitions(actualComponent?.customFields),
normalizeExistingCustomFieldDefinitions((requirement.typeComposant as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions(requirement.customFields),
normalizeExistingCustomFieldDefinitions((requirement.definition as AnyRecord)?.customFields),
...normalizedStructureDefs.map((def) => getStructureCustomFields(def)),
),
)
const piecesTransformed = component.pieces
? transformCustomFields(component.pieces as AnyRecord[]).map((p) => ({
...p,
parentComponentName: component.name,
}))
: []
const subComponents = component.sousComposants
? transformComponentCustomFields(component.sousComposants as AnyRecord[])
: []
const constructeurIds = uniqueConstructeurIds(
component.constructeurIds,
component.constructeurId,
component.constructeur,
actualComponent?.constructeurIds,
actualComponent?.constructeurId,
actualComponent?.constructeur,
)
const constructeursList = resolveConstructeurs(
constructeurIds,
Array.isArray(component.constructeurs) ? (component.constructeurs as any[]) : [],
component.constructeur ? [component.constructeur as any] : [],
Array.isArray(actualComponent?.constructeurs)
? (actualComponent.constructeurs as any[])
: [],
actualComponent?.constructeur ? [actualComponent.constructeur as any] : [],
constructeurs.value as any,
) as any[]
const { product: resolvedProduct, productId: resolvedProductId } =
resolveProductReference(component)
const normalizedComponent = {
...component,
product: resolvedProduct || component.product || null,
productId:
resolvedProductId || component.productId || (component.product as AnyRecord)?.id || null,
}
const productDisplay = getProductDisplay(normalizedComponent)
return {
...normalizedComponent,
customFields,
pieces: piecesTransformed,
subComponents,
documents: component.documents || [],
constructeurs: constructeursList,
constructeur: constructeursList[0] || component.constructeur || null,
constructeurIds,
constructeurId: constructeurIds[0] || null,
typeComposantId:
component.typeComposantId ||
(component.typeMachineComponentRequirement as AnyRecord)?.typeComposantId ||
(component.typeComposant as AnyRecord)?.id ||
null,
__productDisplay: productDisplay,
}
})
}
// ---------------------------------------------------------------------------
// Hierarchy & links
// ---------------------------------------------------------------------------
const applyMachineLinks = (source: AnyRecord): boolean => {
const container = (source?.machine as AnyRecord) ?? null
const componentLinksData =
resolveLinkArray(source, ['componentLinks', 'machineComponentLinks']) ??
resolveLinkArray(container, ['componentLinks', 'machineComponentLinks'])
const pieceLinksData =
resolveLinkArray(source, ['pieceLinks', 'machinePieceLinks']) ??
resolveLinkArray(container, ['pieceLinks', 'machinePieceLinks'])
const productLinksData =
resolveLinkArray(source, ['productLinks', 'machineProductLinks']) ??
resolveLinkArray(container, ['productLinks', 'machineProductLinks'])
if (componentLinksData === null && pieceLinksData === null && productLinksData === null) {
return false
}
const normalizedComponentLinks = (componentLinksData ?? []) as AnyRecord[]
const normalizedPieceLinks = (pieceLinksData ?? []) as AnyRecord[]
const normalizedProductLinks = (productLinksData ?? []) as AnyRecord[]
machineComponentLinks.value = normalizedComponentLinks
machinePieceLinks.value = normalizedPieceLinks
machineProductLinks.value = normalizedProductLinks
const { components: hierarchy, machinePieces: machineLevelPieces } =
buildMachineHierarchyFromLinks(
normalizedComponentLinks,
normalizedPieceLinks,
findProductById as any,
constructeurs.value,
)
components.value = transformComponentCustomFields(hierarchy as AnyRecord[])
pieces.value = transformCustomFields(machineLevelPieces as AnyRecord[])
return true
}
// ---------------------------------------------------------------------------
// Computed values
// ---------------------------------------------------------------------------
const flattenedComponents = computed(() => flattenComponents(components.value))
const machinePieces = computed(() => {
return pieces.value.filter((piece) => {
const parentLinkId = resolveIdentifier(
piece.parentComponentLinkId,
(piece.machinePieceLink as AnyRecord)?.parentComponentLinkId,
piece.parentLinkId,
)
if (parentLinkId) return false
return !piece.composantId
})
})
const machineDocumentsList = computed(
() => ((machine.value as AnyRecord)?.documents as AnyRecord[]) || [],
)
const visibleMachineCustomFields = computed(() => {
const fields = Array.isArray(machineCustomFields.value) ? machineCustomFields.value : []
if (isEditMode.value) return fields
return fields.filter((field) => shouldDisplayCustomField(field))
})
const componentRequirementGroups = computed(() => {
const reqs = ((machine.value as AnyRecord)?.typeMachine as AnyRecord)?.componentRequirements as AnyRecord[] || []
if (!reqs.length) return []
const groups = reqs.map((requirement: AnyRecord) => ({
requirement,
components: [] as AnyRecord[],
}))
const map = new Map(groups.map((g) => [g.requirement.id, g]))
flattenedComponents.value.forEach((component) => {
const reqId = component.typeMachineComponentRequirementId as string
if (reqId && map.has(reqId)) {
map.get(reqId)!.components.push({
...component,
__productDisplay: getProductDisplay(component),
})
}
})
return groups
})
const pieceRequirementGroups = computed(() => {
const reqs = ((machine.value as AnyRecord)?.typeMachine as AnyRecord)?.pieceRequirements as AnyRecord[] || []
if (!reqs.length) return []
const groups = reqs.map((requirement: AnyRecord) => ({
requirement,
pieces: [] as AnyRecord[],
}))
const map = new Map(groups.map((g) => [g.requirement.id, g]))
const collectPieces = (): AnyRecord[] => {
const collected: AnyRecord[] = []
machinePieces.value.forEach((piece) => {
collected.push({
...piece,
constructeurs: piece.constructeurs || [],
parentComponentName: null,
__productDisplay: getProductDisplay(piece),
})
})
flattenedComponents.value.forEach((component) => {
if (Array.isArray(component.pieces) && (component.pieces as AnyRecord[]).length) {
;(component.pieces as AnyRecord[]).forEach((piece) => {
collected.push({
...piece,
constructeurs: piece.constructeurs || [],
parentComponentName: component.name,
__productDisplay: getProductDisplay(piece),
})
})
}
})
return collected
}
collectPieces().forEach((piece) => {
const reqId = piece.typeMachinePieceRequirementId as string
if (reqId && map.has(reqId)) {
map.get(reqId)!.pieces.push(piece)
}
})
return groups
})
const productRequirementGroups = computed(() => {
const reqs = ((machine.value as AnyRecord)?.typeMachine as AnyRecord)?.productRequirements as AnyRecord[] || []
if (!reqs.length) return []
const componentAggregates = flattenedComponents.value || []
const pieceAggregates = collectPiecesForSkeleton()
const links = Array.isArray(machineProductLinks.value) ? machineProductLinks.value : []
return reqs.map((requirement: AnyRecord) => {
const typeProductId =
(requirement.typeProductId as string) ||
(requirement.typeProduct as AnyRecord)?.id as string ||
null
const directProducts = links
.filter((link) => {
const requirementId = resolveIdentifier(
link?.typeMachineProductRequirementId,
link?.requirementId,
)
return requirementId === requirement.id
})
.map((link) => {
const productId = resolveIdentifier(link?.productId, (link?.product as AnyRecord)?.id)
const product =
productId ? findProductById(productId as string) : (link?.product as AnyRecord) ?? null
const supplierLabel = Array.isArray((product as AnyRecord)?.constructeurs)
? ((product as AnyRecord).constructeurs as AnyRecord[])
.map((c) => c?.name)
.filter(Boolean)
.join(', ')
: (link?.constructeursLabel as string) || null
const priceValue =
(product as AnyRecord)?.supplierPrice ?? link?.supplierPrice ?? null
let priceLabel: string | null = null
if (priceValue !== undefined && priceValue !== null) {
const numericPrice = Number(priceValue)
if (!Number.isNaN(numericPrice)) {
priceLabel = `${numericPrice.toFixed(2)}`
}
}
return {
id: productId || link?.id || null,
name: (product as AnyRecord)?.name || link?.productName || productId || 'Produit',
reference: (product as AnyRecord)?.reference || link?.reference || null,
supplierLabel,
priceLabel,
}
})
let componentCount = 0
componentAggregates.forEach((component) => {
const componentTypeProductId =
(component?.product as AnyRecord)?.typeProductId ||
((component?.product as AnyRecord)?.typeProduct as AnyRecord)?.id ||
null
if (typeProductId && componentTypeProductId === typeProductId) componentCount += 1
})
let pieceCount = 0
pieceAggregates.forEach((piece) => {
const pieceTypeProductId =
(piece?.product as AnyRecord)?.typeProductId ||
((piece?.product as AnyRecord)?.typeProduct as AnyRecord)?.id ||
null
if (typeProductId && pieceTypeProductId === typeProductId) pieceCount += 1
})
return {
requirement,
directProducts,
componentCount,
pieceCount,
totalCount: directProducts.length + componentCount + pieceCount,
}
})
})
const machineDirectProducts = computed(() => {
return productRequirementGroups.value.flatMap((group: AnyRecord) =>
((group.directProducts as AnyRecord[]) || []).map((product) => ({
...product,
groupLabel:
(group.requirement as AnyRecord).label ||
((group.requirement as AnyRecord).typeProduct as AnyRecord)?.name ||
'Produit requis',
})),
)
})
// ---------------------------------------------------------------------------
// Machine field methods
// ---------------------------------------------------------------------------
const initMachineFields = () => {
if (machine.value) {
machineName.value = (machine.value.name as string) || ''
machineReference.value = (machine.value.reference as string) || ''
machineConstructeurIds.value = uniqueConstructeurIds(
machine.value.constructeurIds,
machine.value.constructeurs,
machine.value.constructeur,
)
}
}
const getMachineFieldId = (fieldName: string): string => {
return machine.value ? `machine-${fieldName}-${machine.value.id}` : `machine-${fieldName}`
}
// ---------------------------------------------------------------------------
// Custom field methods
// ---------------------------------------------------------------------------
const syncMachineCustomFields = () => {
if (!machine.value) {
machineCustomFields.value = []
return
}
const valueEntries = [
...(Array.isArray(machine.value.customFieldValues) ? machine.value.customFieldValues : []),
...(Array.isArray(machine.value.customFields)
? (machine.value.customFields as AnyRecord[])
.map(normalizeCustomFieldValueEntry)
.filter((e) => e !== null)
: []),
]
const merged = dedupeCustomFieldEntries(
mergeCustomFieldValuesWithDefinitions(
valueEntries,
normalizeExistingCustomFieldDefinitions(machine.value.customFields),
normalizeExistingCustomFieldDefinitions(
(machine.value.typeMachine as AnyRecord)?.customFields,
),
),
).map((field: AnyRecord) => ({ ...field, readOnly: false }))
machineCustomFields.value = merged
}
const setMachineCustomFieldValue = (field: AnyRecord, value: unknown) => {
if (!field) return
field.value = value
if (field.customFieldValueId && (machine.value as AnyRecord)?.customFieldValues) {
const stored = ((machine.value as AnyRecord).customFieldValues as AnyRecord[]).find(
(fv) => fv.id === field.customFieldValueId,
)
if (stored) stored.value = value
}
}
const updateMachineCustomField = async (field: AnyRecord) => {
if (!machine.value || !field) return
const { id: customFieldId, customFieldValueId } = field
const fieldLabel = (field.name as string) || 'Champ personnalisé'
try {
if (customFieldValueId) {
const result: any = await updateCustomFieldValueApi(customFieldValueId as string, {
value: field.value ?? '',
} as any)
if (result.success) {
toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`)
syncMachineCustomFields()
} else {
toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`)
}
return
}
if (!customFieldId) {
toast.showError(
'Impossible de mettre à jour ce champ personnalisé (identifiant manquant).',
)
return
}
const result: any = await upsertCustomFieldValue(
customFieldId as string,
'machine',
machine.value.id as string,
field.value ?? '',
)
if (result.success) {
const createdValue = result.data as AnyRecord
toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`)
if (createdValue?.id) {
if (!createdValue.customField) {
createdValue.customField = {
id: customFieldId,
name: field.name,
type: field.type,
required: field.required,
options: field.options,
}
}
field.customFieldValueId = createdValue.id
field.readOnly = false
const existingValues = Array.isArray(machine.value.customFieldValues)
? (machine.value.customFieldValues as AnyRecord[]).filter(
(item) => item.id !== createdValue.id,
)
: []
machine.value.customFieldValues = [...existingValues, createdValue]
}
syncMachineCustomFields()
} else {
toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`)
}
} catch (error) {
console.error('Erreur lors de la mise à jour du champ personnalisé de la machine:', error)
toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`)
}
}
const updatePieceCustomField = async (fieldUpdate: AnyRecord) => {
try {
const result: any = await upsertCustomFieldValue(
fieldUpdate.fieldId as string,
'piece',
fieldUpdate.pieceId as string,
fieldUpdate.value,
)
if (result.success) {
toast.showSuccess('Champ personnalisé mis à jour avec succès')
} else {
toast.showError('Erreur lors de la mise à jour du champ personnalisé')
}
} catch (error) {
toast.showError('Erreur lors de la mise à jour du champ personnalisé')
console.error('Erreur lors de la mise à jour du champ personnalisé:', error)
}
}
// ---------------------------------------------------------------------------
// Document methods
// ---------------------------------------------------------------------------
const refreshMachineDocuments = async () => {
if (!machine.value?.id) return
const result: any = await loadDocumentsByMachine(machine.value.id as string, { updateStore: false })
if (result.success && machine.value) {
machine.value.documents = result.data || []
machineDocumentsLoaded.value = true
}
}
const handleMachineFilesAdded = async (files: File[]) => {
if (!files.length || !machine.value?.id) return
machineDocumentsUploading.value = true
try {
const result: any = await uploadDocuments(
{ files, context: { machineId: machine.value.id } } as any,
{ updateStore: false },
)
if (result.success && machine.value) {
const newDocs = (result.data as AnyRecord[]) || []
machine.value.documents = [
...newDocs,
...((machine.value.documents as AnyRecord[]) || []),
]
machineDocumentFiles.value = []
}
} finally {
machineDocumentsUploading.value = false
}
}
const removeMachineDocument = async (documentId: string) => {
if (!documentId) return
const result: any = await deleteDocument(documentId, { updateStore: false })
if (result.success && machine.value) {
machine.value.documents = ((machine.value.documents as AnyRecord[]) || []).filter(
(doc) => doc.id !== documentId,
)
}
}
const openPreview = (doc: AnyRecord) => {
if (!canPreviewDocument(doc)) return
previewDocument.value = doc
previewVisible.value = true
}
const closePreview = () => {
previewVisible.value = false
previewDocument.value = null
}
// ---------------------------------------------------------------------------
// Update methods
// ---------------------------------------------------------------------------
const updateMachineInfo = async () => {
if (!machine.value) return
try {
const cIds = uniqueConstructeurIds(machineConstructeurIds.value)
machineConstructeurIds.value = cIds
const result: any = await updateMachineApi(machine.value.id as string, {
name: machineName.value,
reference: machineReference.value,
constructeurIds: cIds,
} as any)
if (result.success) {
const machinePayload =
result.data?.machine && typeof result.data.machine === 'object'
? result.data.machine
: result.data
if (machinePayload && typeof machinePayload === 'object') {
machine.value = {
...machine.value,
...machinePayload,
documents: machinePayload.documents || machine.value.documents || [],
customFieldValues: machinePayload.customFieldValues || machine.value.customFieldValues || [],
}
machineConstructeurIds.value = uniqueConstructeurIds(
machine.value!.constructeurIds,
machine.value!.constructeurs,
machine.value!.constructeur,
)
const linksApplied = applyMachineLinks(result.data)
if (linksApplied && machine.value) {
machine.value.componentLinks = machineComponentLinks.value
machine.value.pieceLinks = machinePieceLinks.value
}
}
}
} catch (error) {
console.error('Erreur lors de la mise à jour de la machine:', error)
}
}
const updateComponent = async (updatedComponent: AnyRecord) => {
try {
const cIds = uniqueConstructeurIds(
updatedComponent.constructeurIds,
updatedComponent.constructeurId,
updatedComponent.constructeur,
)
const productId = updatedComponent.productId
? String(updatedComponent.productId)
: null
const prix =
updatedComponent.prix !== null &&
updatedComponent.prix !== undefined &&
String(updatedComponent.prix).trim() !== ''
? Number(updatedComponent.prix)
: null
const result: any = await updateComposantApi(updatedComponent.id as string, {
name: updatedComponent.name,
reference: updatedComponent.reference,
constructeurIds: cIds,
prix: Number.isNaN(prix) ? null : prix,
productId,
} as any)
if (result.success) {
const transformed = transformComponentCustomFields([result.data])[0]
Object.assign(updatedComponent, transformed)
}
} catch (error) {
console.error('Erreur lors de la mise à jour du composant:', error)
}
}
const updatePieceFromComponent = async (updatedPiece: AnyRecord) => {
try {
const cIds = uniqueConstructeurIds(
updatedPiece.constructeurIds,
updatedPiece.constructeurId,
updatedPiece.constructeur,
)
const productId = updatedPiece.productId ? String(updatedPiece.productId) : null
const prix =
updatedPiece.prix !== null &&
updatedPiece.prix !== undefined &&
String(updatedPiece.prix).trim() !== ''
? Number(updatedPiece.prix)
: null
const result: any = await updatePieceApi(updatedPiece.id as string, {
name: updatedPiece.name,
reference: updatedPiece.reference,
constructeurIds: cIds,
prix: Number.isNaN(prix) ? null : prix,
productId,
} as any)
if (result.success) {
const transformed = transformCustomFields([result.data])[0]
Object.assign(updatedPiece, transformed)
if (updatedPiece.customFields) {
for (const field of updatedPiece.customFields as AnyRecord[]) {
if (field.value !== undefined) {
await upsertCustomFieldValue(
field.id as string,
'piece',
updatedPiece.id as string,
field.value,
)
}
}
}
}
} catch (error) {
console.error('Erreur lors de la mise à jour de la pièce:', error)
}
}
const updatePieceInfo = async (updatedPiece: AnyRecord) => {
try {
const cIds = uniqueConstructeurIds(
updatedPiece.constructeurIds,
updatedPiece.constructeurId,
updatedPiece.constructeur,
)
const productId = updatedPiece.productId ? String(updatedPiece.productId) : null
const prix =
updatedPiece.prix !== null &&
updatedPiece.prix !== undefined &&
String(updatedPiece.prix).trim() !== ''
? Number(updatedPiece.prix)
: null
const result: any = await updatePieceApi(updatedPiece.id as string, {
name: updatedPiece.name,
reference: updatedPiece.reference,
constructeurIds: cIds,
prix: Number.isNaN(prix) ? null : prix,
productId,
} as any)
if (result.success) {
const transformed = transformCustomFields([result.data])[0]
Object.assign(updatedPiece, transformed)
}
} catch (error) {
console.error('Erreur lors de la mise à jour de la pièce:', error)
}
}
const handleMachineConstructeurChange = async (value: unknown) => {
machineConstructeurIds.value = uniqueConstructeurIds(value)
await updateMachineInfo()
}
const editComponent = () => {
toast.showInfo('La modification des composants sera bientôt disponible')
}
const editPiece = () => {
toast.showInfo('La modification des pièces sera bientôt disponible')
}
// ---------------------------------------------------------------------------
// UI methods
// ---------------------------------------------------------------------------
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value
debug.value = !debug.value
if (isEditMode.value && !machineDocumentsLoaded.value) {
refreshMachineDocuments()
}
}
const toggleAllComponents = () => {
componentsCollapsed.value = !componentsCollapsed.value
collapseToggleToken.value += 1
}
const collapseAllComponents = () => {
componentsCollapsed.value = true
collapseToggleToken.value += 1
}
// ---------------------------------------------------------------------------
// Print wrappers
// ---------------------------------------------------------------------------
const ensurePrintSelectionEntries = () =>
_ensurePrintEntries(components.value, machinePieces.value)
const setAllPrintSelection = (value: boolean) =>
_setAllPrint(value, components.value, machinePieces.value)
const openPrintModal = () =>
_openPrintModal(components.value, machinePieces.value)
const handlePrintConfirm = () =>
_handlePrintConfirm(
machine.value as any,
machineName.value,
machineReference.value,
machinePieces.value as any,
components.value as any,
)
// ---------------------------------------------------------------------------
// Piece aggregation (used by skeleton & product requirement groups)
// ---------------------------------------------------------------------------
const collectPiecesForSkeleton = (): AnyRecord[] => {
const aggregated: AnyRecord[] = []
machinePieces.value.forEach((piece) => aggregated.push(piece))
flattenedComponents.value.forEach((component) => {
;((component.pieces as AnyRecord[]) || []).forEach((piece) => aggregated.push(piece))
})
return aggregated
}
// ---------------------------------------------------------------------------
// Data loading
// ---------------------------------------------------------------------------
const loadMachineData = async () => {
loading.value = true
try {
const machineResult: any = await get(`/machines/${machineId}/skeleton`)
if (!machineResult.success) {
console.error('Machine non trouvée:', machineId, machineResult.error)
machine.value = null
components.value = []
pieces.value = []
return
}
const machinePayload =
machineResult.data?.machine && typeof machineResult.data.machine === 'object'
? machineResult.data.machine
: machineResult.data
if (!machinePayload || typeof machinePayload !== 'object') {
console.error('Réponse machine invalide pour', machineId)
machine.value = null
components.value = []
pieces.value = []
return
}
machine.value = {
...machinePayload,
documents: machinePayload.documents || [],
customFieldValues: machinePayload.customFieldValues || [],
}
machineDocumentsLoaded.value = !!((machine.value!.documents as AnyRecord[])?.length)
syncMachineCustomFields()
initMachineFields()
if (!(productInventory.value as AnyRecord[]).length) {
try {
await loadProducts()
} catch (error) {
console.error('Erreur lors du chargement des produits:', error)
}
}
const linksApplied = applyMachineLinks(machineResult.data)
if (machine.value) {
machine.value.componentLinks = machineComponentLinks.value
machine.value.pieceLinks = machinePieceLinks.value
machine.value.productLinks = machineProductLinks.value
}
if (!linksApplied) {
components.value = transformComponentCustomFields(machinePayload.components || [])
pieces.value = transformCustomFields(machinePayload.pieces || [])
machineProductLinks.value = Array.isArray(machinePayload.productLinks)
? machinePayload.productLinks
: []
}
if (machine.value) {
machine.value.productLinks = machineProductLinks.value
}
collapseAllComponents()
if (!machineDocumentsLoaded.value) {
await refreshMachineDocuments()
}
} catch (error) {
console.error('Erreur lors du chargement des données:', error)
} finally {
loading.value = false
}
}
const loadInitialData = () => {
if (!constructeurs.value.length) loadConstructeurs()
if (!componentTypes.value.length) loadComponentTypes()
if (!pieceTypes.value.length) loadPieceTypes()
}
// ---------------------------------------------------------------------------
// Watchers
// ---------------------------------------------------------------------------
watch(() => (machine.value as AnyRecord)?.customFieldValues, () => syncMachineCustomFields(), { deep: true })
watch(() => (machine.value as AnyRecord)?.customFields, () => syncMachineCustomFields(), { deep: true })
watch(() => ((machine.value as AnyRecord)?.typeMachine as AnyRecord)?.customFields, () => syncMachineCustomFields(), { deep: true })
watch(
() => [components.value.length, machinePieces.value.length],
() => ensurePrintSelectionEntries(),
{ immediate: true },
)
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
return {
// State
loading,
machine,
components,
pieces,
machineComponentLinks,
machinePieceLinks,
machineProductLinks,
printAreaRef,
// Machine fields
machineName,
machineReference,
machineConstructeurIds,
machineConstructeurId,
machineConstructeursDisplay,
machineConstructeurContact,
hasMachineConstructeur,
// UI state
machineDocumentFiles,
machineDocumentsUploading,
machineDocumentsLoaded,
machineCustomFields,
previewDocument,
previewVisible,
isEditMode,
debug,
componentsCollapsed,
collapseToggleToken,
// Computed
machineType,
componentRequirements,
pieceRequirements,
productRequirements,
machineHasSkeletonRequirements,
componentTypeOptions,
pieceTypeOptions,
componentTypeLabelMap,
pieceTypeLabelMap,
productInventory,
productById,
flattenedComponents,
machinePieces,
machineDocumentsList,
visibleMachineCustomFields,
componentRequirementGroups,
pieceRequirementGroups,
productRequirementGroups,
machineDirectProducts,
// Product helpers
findProductById,
resolveProductReference,
getProductDisplay,
// Helpers
isPlainObject,
flattenComponents,
findComponentById,
findPieceById,
collectConstructeurs,
collectPiecesForSkeleton,
// Transform
transformCustomFields,
transformComponentCustomFields,
// Hierarchy
applyMachineLinks,
// Machine fields methods
initMachineFields,
getMachineFieldId,
// Custom fields
syncMachineCustomFields,
setMachineCustomFieldValue,
updateMachineCustomField,
updatePieceCustomField,
// Documents
refreshMachineDocuments,
handleMachineFilesAdded,
removeMachineDocument,
openPreview,
closePreview,
// Updates
updateMachineInfo,
updateComponent,
updatePieceFromComponent,
updatePieceInfo,
handleMachineConstructeurChange,
editComponent,
editPiece,
// UI methods
toggleEditMode,
toggleAllComponents,
collapseAllComponents,
// Print
printModalOpen,
printSelection,
ensurePrintSelectionEntries,
setAllPrintSelection,
openPrintModal,
closePrintModal,
handlePrintConfirm,
// Loading
loadMachineData,
loadInitialData,
// External (needed by skeleton editor)
constructeurs,
loadProducts,
reconfigureMachineSkeleton,
toast,
// Re-exports for template
formatCustomFieldValue,
summarizeCustomFields,
formatConstructeurContactSummary,
formatSize,
shouldInlinePdf,
documentPreviewSrc,
documentThumbnailClass,
documentIcon,
downloadDocument: downloadDocumentHelper,
canPreviewDocument,
isImageDocument,
isPdfDocument,
getFileIcon,
resolveIdentifier,
extractParentLinkIdentifiers,
}
}