/** * 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 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(null) const components = ref([]) const pieces = ref([]) const machineComponentLinks = ref([]) const machinePieceLinks = ref([]) const machineProductLinks = ref([]) 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) // --------------------------------------------------------------------------- // 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() 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() 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() ;(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() // 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() // 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(() => ((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, } }