/** * Machine detail page — core state & business logic (orchestrator). * * Extracted from pages/machine/[id].vue (F1.1). * Composes sub-composables for documents, custom fields, hierarchy and products. */ 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 { useConstructeurs } from '~/composables/useConstructeurs' import { useSites } from '~/composables/useSites' import { useMachinePrint } from '~/composables/useMachinePrint' import { resolveConstructeurs, uniqueConstructeurIds, formatConstructeurContact as formatConstructeurContactSummary, parseConstructeurLinksFromApi, constructeurIdsFromLinks, } from '~/shared/constructeurUtils' import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils' import { useConstructeurLinks } from '~/composables/useConstructeurLinks' import { useMachineDetailDocuments } from '~/composables/useMachineDetailDocuments' import { useMachineDetailCustomFields } from '~/composables/useMachineDetailCustomFields' import { useMachineDetailHierarchy } from '~/composables/useMachineDetailHierarchy' import { useMachineDetailProducts } from '~/composables/useMachineDetailProducts' import { useMachineDetailUpdates } from '~/composables/useMachineDetailUpdates' import { 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 { upsertCustomFieldValue } = useCustomFields() const { get, patch: apiPatch } = useApi() const toast = useToast() const { constructeurs, loadConstructeurs } = useConstructeurs() const { sites, loadSites } = useSites() const { printModalOpen, printSelection, ensurePrintSelectionEntries: _ensurePrintEntries, setAllPrintSelection: _setAllPrint, openPrintModal: _openPrintModal, closePrintModal, handlePrintConfirm: _handlePrintConfirm, } = useMachinePrint() // Core state const loading = ref(true) const machine = ref(null) const productDocumentsMap = ref>(new Map()) const printAreaRef = ref(null) const saving = ref(false) // Constructeur links const { fetchLinks, syncLinks } = useConstructeurLinks() const constructeurLinks = ref([]) const originalConstructeurLinks = ref([]) // Machine fields const machineName = ref('') const machineReference = ref('') const machineSiteId = 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 = machineConstructeurIds.value if (!ids.length) return [] as any[] // Extract nested constructeur objects from link entries as candidate pool const linkConstructeurs = constructeurLinks.value .filter(l => l.constructeur && l.constructeur.id) .map(l => l.constructeur!) as any[] return resolveConstructeurs( ids, linkConstructeurs, 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 isEditMode = ref(false) const canSubmit = computed(() => { if (!machine.value) return false if (saving.value) return false if (!machineName.value.trim()) return false return true }) const componentsCollapsed = ref(true) const collapseToggleToken = ref(0) const piecesCollapsed = ref(true) const pieceCollapseToggleToken = ref(0) // Sub-composables: Products (init first — hierarchy needs findProductById) // Products needs machineProductLinks from hierarchy, but hierarchy needs // findProductById from products. We break the cycle by passing a lazy // computed that reads from the hierarchy ref once it exists. const _machineProductLinksProxy = ref([]) const { productInventory, productById, machineDirectProducts, findProductById, resolveProductReference, getProductDisplay, loadProducts, } = useMachineDetailProducts({ machineProductLinks: _machineProductLinksProxy, productDocumentsMap, constructeurs, }) // Sub-composables: Custom fields const { machineCustomFields, visibleMachineCustomFields, pendingContextFieldUpdates, transformCustomFields, transformComponentCustomFields, syncMachineCustomFields, setMachineCustomFieldValue, updateMachineCustomField, updatePieceCustomField, handleCustomFieldUpdate, queueContextFieldUpdate, clearPendingContextFieldUpdates, saveAllMachineCustomFields, saveAllContextCustomFields, } = useMachineDetailCustomFields({ machine, isEditMode, constructeurs, resolveProductReference, getProductDisplay, }) // Sub-composables: Hierarchy (includes structure link CRUD) const hierarchy = useMachineDetailHierarchy({ machineId, machine, constructeurs, findProductById, transformComponentCustomFields, transformCustomFields, syncMachineCustomFields, }) const { components, pieces, machineComponentLinks, machinePieceLinks, machineProductLinks, flattenedComponents, machinePieces, applyMachineLinks, reloadMachineStructure, addComponentLink, removeComponentLink, addPieceLink, removePieceLink, addProductLink, removeProductLink, addComponentLinkCategoryOnly, addPieceLinkCategoryOnly, addProductLinkCategoryOnly, fillEntityLink, } = hierarchy // Keep the product links proxy in sync with the hierarchy's machineProductLinks watch(machineProductLinks, (val) => { _machineProductLinksProxy.value = val }, { immediate: true }) // Sub-composables: Documents const { machineDocumentFiles, machineDocumentsUploading, machineDocumentsLoaded, previewDocument, previewVisible, machineDocumentsList, refreshMachineDocuments, handleMachineFilesAdded, removeMachineDocument, openPreview, closePreview, loadProductDocuments: _loadProductDocuments, } = useMachineDetailDocuments({ machine }) // Type helpers const componentTypeOptions = computed(() => componentTypes.value || []) const pieceTypeOptions = computed(() => pieceTypes.value || []) // Machine field methods const initMachineFields = () => { if (machine.value) { machineName.value = (machine.value.name as string) || '' machineReference.value = (machine.value.reference as string) || '' // Parse constructeur links from structure response const rawLinks = Array.isArray(machine.value.constructeurs) ? machine.value.constructeurs as any[] : [] const parsed = parseConstructeurLinksFromApi(rawLinks) constructeurLinks.value = parsed originalConstructeurLinks.value = parsed.map(l => ({ ...l })) machineConstructeurIds.value = constructeurIdsFromLinks(parsed) machineSiteId.value = (machine.value.siteId as string) || (machine.value.site as AnyRecord)?.id as string || '' } } const getMachineFieldId = (fieldName: string): string => { return machine.value ? `machine-${fieldName}-${machine.value.id}` : `machine-${fieldName}` } // Product documents wrapper const loadProductDocuments = async () => { const map = await _loadProductDocuments(machineProductLinks.value) productDocumentsMap.value = map } // Update methods (delegated to useMachineDetailUpdates) const { updateMachineInfo, updateComponent, updatePieceFromComponent, updatePieceInfo, handleMachineConstructeurChange, editComponent, editPiece, } = useMachineDetailUpdates({ machine, machineName, machineReference, machineSiteId, machineConstructeurIds, constructeurLinks, originalConstructeurLinks, machineDocumentsLoaded, machineComponentLinks, machinePieceLinks, machineProductLinks, applyMachineLinks, refreshMachineDocuments, transformComponentCustomFields, transformCustomFields, loadProductDocuments, upsertCustomFieldValue, updateMachineApi, updateComposantApi: updateComposantApi, updatePieceApi, apiPatch, toast, syncLinks, }) // UI methods const toggleEditMode = () => { isEditMode.value = !isEditMode.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 } const submitEdition = async () => { if (!machine.value || saving.value) return saving.value = true try { // 1. Save machine info (name, reference, site, constructeurs) await updateMachineInfo() // 2. Save all custom field values await saveAllMachineCustomFields() // 3. Save contextual custom field values queued from piece/component inputs await saveAllContextCustomFields() // 4. Reload machine data to get fresh state await loadMachineData() // 5. Exit edit mode isEditMode.value = false toast.showSuccess('Machine mise à jour avec succès') } catch (error) { console.error('Erreur lors de la sauvegarde:', error) toast.showError('Erreur lors de la sauvegarde de la machine') } finally { saving.value = false } } const cancelEdition = () => { initMachineFields() syncMachineCustomFields() clearPendingContextFieldUpdates() constructeurLinks.value = originalConstructeurLinks.value.map(l => ({ ...l })) machineConstructeurIds.value = constructeurIdsFromLinks(constructeurLinks.value) isEditMode.value = false } // 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, ) // 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() // Load products in parallel — don't block hierarchy rendering const productsPromise = !(productInventory.value as AnyRecord[]).length ? loadProducts().catch((error: unknown) => { console.error('Erreur lors du chargement des produits:', error) }) : Promise.resolve() await productsPromise const linksApplied = applyMachineLinks(machineResult.data) 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.componentLinks = machineComponentLinks.value machine.value.pieceLinks = machinePieceLinks.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(), loadSites(), ]) } // 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 }, ) return { // State loading, machine, components, pieces, printAreaRef, machineComponentLinks, machinePieceLinks, machineProductLinks, // Machine fields machineName, machineReference, machineSiteId, machineConstructeurIds, machineConstructeurId, machineConstructeursDisplay, machineConstructeurContact, hasMachineConstructeur, constructeurLinks, originalConstructeurLinks, sites, // UI state machineDocumentFiles, machineDocumentsUploading, machineDocumentsLoaded, machineCustomFields, pendingContextFieldUpdates, previewDocument, previewVisible, isEditMode, componentsCollapsed, collapseToggleToken, piecesCollapsed, pieceCollapseToggleToken, // Computed componentTypeOptions, pieceTypeOptions, productInventory, productById, flattenedComponents, machinePieces, machineDirectProducts, machineDocumentsList, visibleMachineCustomFields, // Methods findProductById, resolveProductReference, getProductDisplay, initMachineFields, getMachineFieldId, syncMachineCustomFields, setMachineCustomFieldValue, updateMachineCustomField, updatePieceCustomField, handleCustomFieldUpdate, queueContextFieldUpdate, clearPendingContextFieldUpdates, saveAllContextCustomFields, refreshMachineDocuments, handleMachineFilesAdded, removeMachineDocument, openPreview, closePreview, updateMachineInfo, updateComponent, updatePieceFromComponent, updatePieceInfo, handleMachineConstructeurChange, editComponent, editPiece, toggleEditMode, toggleAllComponents, collapseAllComponents, toggleAllPieces, saving, canSubmit, submitEdition, cancelEdition, // Print printModalOpen, printSelection, ensurePrintSelectionEntries, setAllPrintSelection, openPrintModal, closePrintModal, handlePrintConfirm, // Loading & structure loadMachineData, loadInitialData, addComponentLink, removeComponentLink, addPieceLink, removePieceLink, addProductLink, removeProductLink, reloadMachineStructure, addComponentLinkCategoryOnly, addPieceLinkCategoryOnly, addProductLinkCategoryOnly, fillEntityLink, // External constructeurs, loadProducts, updateMachineStructure, toast, downloadDocument: downloadDocumentHelper, } }