Files
Inventory/app/composables/useMachineDetailData.ts
Matthieu 32d03b480d refactor(machines) : remove TypeMachine skeleton system, simplify machine creation
- Remove TypeEdit*, TypeInfoDisplay, MachineSkeletonSummary, MachineCreatePreview components
- Remove machine-skeleton pages and type pages
- Remove useMachineTypesApi, useMachineSkeletonEditor, useMachineCreateSelections composables
- Add AddEntityToMachineModal for direct entity linking
- Update machine detail/create pages for direct custom fields
- Fix SearchSelect, category display, and ipartial search filters

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:25:23 +01:00

1411 lines
48 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,
getProductSuppliersLabel,
getProductPriceLabel,
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,
updateStructure: updateMachineStructure,
} = 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, post: apiPost, delete: apiDel } = useApi()
const {
uploadDocuments,
deleteDocument,
loadDocumentsByMachine,
loadDocumentsByProduct,
} = 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 productDocumentsMap = ref<Map<string, AnyRecord[]>>(new Map())
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)
const piecesCollapsed = ref(true)
const pieceCollapseToggleToken = ref(0)
// ---------------------------------------------------------------------------
// Product helpers
// ---------------------------------------------------------------------------
const componentTypeOptions = computed(() => componentTypes.value || [])
const pieceTypeOptions = computed(() => pieceTypes.value || [])
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) || '')
})
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) || '')
})
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 typePiece = (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),
]
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),
...normalizedStructureDefs.map((def) => getStructureCustomFields(def)),
),
)
const constructeurIds = uniqueConstructeurIds(
piece.constructeurs,
piece.constructeurIds,
piece.constructeurId,
piece.constructeur,
(piece.originalPiece as AnyRecord)?.constructeurs,
(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.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 type = (component.typeComposant as AnyRecord) || {}
const normalizedStructureDefs = [
normalizeStructureDefs((component.definition as AnyRecord)?.structure),
normalizeStructureDefs((component.typeComposant as AnyRecord)?.structure),
normalizeStructureDefs(type.structure),
]
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),
...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.constructeurs,
component.constructeurIds,
component.constructeurId,
component.constructeur,
actualComponent?.constructeurs,
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.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 machineDirectProducts = computed(() => {
return machineProductLinks.value.map((link) => {
const productObj = link.product as AnyRecord | string | null
let resolved: AnyRecord | null = null
let productId: string | null = null
if (typeof productObj === 'string') {
productId = productObj.split('/').pop() || null
resolved = productId ? findProductById(productId) : null
} else if (productObj && typeof productObj === 'object') {
productId = (productObj as AnyRecord)?.id as string | null
// Prefer the embedded product from the structure endpoint — it has richer
// data (typeProduct as object, supplierPrice, constructeurs) than the
// global products cache which may store typeProduct as an IRI string.
const cached = productId ? findProductById(productId) : null
resolved = productObj as AnyRecord
if (cached) {
// Merge: use embedded as base, overlay any non-null cached fields
resolved = { ...resolved, ...Object.fromEntries(
Object.entries(cached as AnyRecord).filter(([, v]) => v != null && v !== ''),
) }
// But always prefer the embedded typeProduct when it's an object
if (productObj.typeProduct && typeof productObj.typeProduct === 'object') {
resolved.typeProduct = productObj.typeProduct
}
}
}
const constructeurIds = uniqueConstructeurIds(
resolved?.constructeurs,
resolved?.constructeurIds,
)
const resolvedConstructeurs = resolveConstructeurs(
constructeurIds,
resolved?.constructeurs as any[] || [],
constructeurs.value,
)
return {
id: (resolved?.id as string) || productId || null,
linkId: (link.id as string) || (typeof link['@id'] === 'string' ? link['@id'].split('/').pop() : null) || null,
name: (resolved?.name as string) || 'Produit inconnu',
reference: (resolved?.reference as string) || null,
supplierLabel: resolvedConstructeurs.length
? resolvedConstructeurs.map((c) => c.name).filter(Boolean).join(', ') || null
: getProductSuppliersLabel(resolved),
priceLabel: resolved ? getProductPriceLabel(resolved) : null,
groupLabel: ((resolved?.typeProduct as AnyRecord)?.name as string) || '',
documents: productId ? (productDocumentsMap.value.get(productId) || []) : [],
}
})
})
const loadProductDocuments = async () => {
const productIds = machineProductLinks.value
.map((link) => {
const p = link.product as AnyRecord | string | null
if (typeof p === 'string') return p.split('/').pop() || null
return (p as AnyRecord)?.id as string | null
})
.filter((id): id is string => !!id)
const results = await Promise.allSettled(
productIds.map(async (id) => {
const result: any = await loadDocumentsByProduct(id, { updateStore: false })
if (result.success && Array.isArray(result.data)) {
return { id, docs: result.data as AnyRecord[] }
}
return { id, docs: [] }
}),
)
const map = new Map<string, AnyRecord[]>()
results.forEach((r) => {
if (r.status === 'fulfilled' && r.value.docs.length) {
map.set(r.value.id, r.value.docs)
}
})
productDocumentsMap.value = map
}
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))
})
// ---------------------------------------------------------------------------
// 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),
),
).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
}
loadProductDocuments().catch(() => {})
}
}
} 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
}
const toggleAllPieces = () => {
piecesCollapsed.value = !piecesCollapsed.value
pieceCollapseToggleToken.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,
)
// ---------------------------------------------------------------------------
// Structure link management
// ---------------------------------------------------------------------------
const reloadMachineStructure = async () => {
const result: any = await get(`/machines/${machineId}/structure`)
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 as AnyRecord)?.documents || [],
customFieldValues: machinePayload.customFieldValues || (machine.value as AnyRecord)?.customFieldValues || [],
}
const linksApplied = applyMachineLinks(result.data)
if (linksApplied && machine.value) {
machine.value.componentLinks = machineComponentLinks.value
machine.value.pieceLinks = machinePieceLinks.value
machine.value.productLinks = machineProductLinks.value
}
syncMachineCustomFields()
}
}
}
const addComponentLink = async (composantId: string) => {
const result: any = await apiPost('/machine_component_links', {
machine: `/api/machines/${machineId}`,
composant: `/api/composants/${composantId}`,
})
if (result.success) {
toast.showSuccess('Composant ajouté à la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de l\'ajout du composant')
}
return result
}
const removeComponentLink = async (linkId: string) => {
const result: any = await apiDel(`/machine_component_links/${linkId}`)
if (result.success) {
toast.showSuccess('Composant retiré de la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de la suppression du composant')
}
return result
}
const addPieceLink = async (pieceId: string, parentComponentLinkId?: string) => {
const payload: any = {
machine: `/api/machines/${machineId}`,
piece: `/api/pieces/${pieceId}`,
}
if (parentComponentLinkId) {
payload.parentLink = `/api/machine_component_links/${parentComponentLinkId}`
}
const result: any = await apiPost('/machine_piece_links', payload)
if (result.success) {
toast.showSuccess('Pièce ajoutée à la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de l\'ajout de la pièce')
}
return result
}
const removePieceLink = async (linkId: string) => {
const result: any = await apiDel(`/machine_piece_links/${linkId}`)
if (result.success) {
toast.showSuccess('Pièce retirée de la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de la suppression de la pièce')
}
return result
}
const addProductLink = async (productId: string, parentComponentLinkId?: string, parentPieceLinkId?: string) => {
const payload: any = {
machine: `/api/machines/${machineId}`,
product: `/api/products/${productId}`,
}
if (parentComponentLinkId) {
payload.parentComponentLink = `/api/machine_component_links/${parentComponentLinkId}`
}
if (parentPieceLinkId) {
payload.parentPieceLink = `/api/machine_piece_links/${parentPieceLinkId}`
}
const result: any = await apiPost('/machine_product_links', payload)
if (result.success) {
toast.showSuccess('Produit ajouté à la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de l\'ajout du produit')
}
return result
}
const removeProductLink = async (linkId: string) => {
const result: any = await apiDel(`/machine_product_links/${linkId}`)
if (result.success) {
toast.showSuccess('Produit retiré de la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de la suppression du produit')
}
return result
}
// ---------------------------------------------------------------------------
// Data loading
// ---------------------------------------------------------------------------
const loadMachineData = async () => {
loading.value = true
try {
const machineResult: any = await get(`/machines/${machineId}/structure`)
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()
// Start document loading early (independent of products/links)
const documentPromise = !machineDocumentsLoaded.value
? refreshMachineDocuments()
: Promise.resolve()
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()
// Load product documents in background
loadProductDocuments().catch(() => {})
// Wait for documents if still loading
await documentPromise
} catch (error) {
console.error('Erreur lors du chargement des données:', error)
} finally {
loading.value = false
}
}
const loadInitialData = (): Promise<unknown[]> => {
return Promise.all([
loadConstructeurs(),
loadComponentTypes(),
loadPieceTypes(),
])
}
// ---------------------------------------------------------------------------
// Watchers
// ---------------------------------------------------------------------------
watch(() => (machine.value as AnyRecord)?.customFieldValues, () => syncMachineCustomFields(), { deep: true })
watch(() => (machine.value 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,
piecesCollapsed,
pieceCollapseToggleToken,
// Computed
componentTypeOptions,
pieceTypeOptions,
componentTypeLabelMap,
pieceTypeLabelMap,
productInventory,
productById,
flattenedComponents,
machinePieces,
machineDirectProducts,
machineDocumentsList,
visibleMachineCustomFields,
// Product helpers
findProductById,
resolveProductReference,
getProductDisplay,
// Helpers
isPlainObject,
flattenComponents,
findComponentById,
findPieceById,
collectConstructeurs,
// 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,
toggleAllPieces,
// Print
printModalOpen,
printSelection,
ensurePrintSelectionEntries,
setAllPrintSelection,
openPrintModal,
closePrintModal,
handlePrintConfirm,
// Loading
loadMachineData,
loadInitialData,
// Structure link management
addComponentLink,
removeComponentLink,
addPieceLink,
removePieceLink,
addProductLink,
removeProductLink,
reloadMachineStructure,
// External
constructeurs,
loadProducts,
updateMachineStructure,
toast,
// Re-exports for template
formatCustomFieldValue,
summarizeCustomFields,
formatConstructeurContactSummary,
formatSize,
shouldInlinePdf,
documentPreviewSrc,
documentThumbnailClass,
documentIcon,
downloadDocument: downloadDocumentHelper,
canPreviewDocument,
isImageDocument,
isPdfDocument,
getFileIcon,
resolveIdentifier,
extractParentLinkIdentifiers,
}
}