/** * 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 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(null) const components = ref([]) const pieces = ref([]) const machineComponentLinks = ref([]) const machinePieceLinks = ref([]) const machineProductLinks = ref([]) const productDocumentsMap = ref>(new Map()) const printAreaRef = ref(null) // --------------------------------------------------------------------------- // Machine fields // --------------------------------------------------------------------------- const machineName = ref('') const machineReference = ref('') const machineConstructeurIds = ref([]) 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([]) const machineDocumentsUploading = ref(false) const machineDocumentsLoaded = ref(false) const machineCustomFields = ref([]) const previewDocument = ref(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() 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() 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() ;(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() 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 => { 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, } }