Files
Inventory/frontend/app/composables/useMachineDetailData.ts
r-dev 0049638e3c fix(custom-fields) : context fields display, batch save, hierarchy propagation and UniqueEntity fix
- ComponentItem/PieceItem: DaisyUI divider, emit context-field-update for batch save
- CustomFieldDisplay: support editable/emit-blur/title/show-header props
- MachineComponentsCard/MachinePiecesCard: propagate custom-field-update events
- useMachineDetailCustomFields: pendingContextFieldUpdates + saveAllContextCustomFields
- useMachineDetailData: wire context field save into submitEdition
- useMachineDetailUpdates: only PATCH changed machine fields
- useMachineHierarchy: propagate contextCustomFields/Values from link to nodes
- componentStructure: include machineContextOnly in normalizeStructureForEditor
- Machine entity: convert empty reference to null, ignoreNull on UniqueEntity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:46:39 +02:00

536 lines
18 KiB
TypeScript

/**
* 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<string, unknown>
export function useMachineDetailData(machineId: string) {
// External composables
const {
updateMachine: updateMachineApi,
updateStructure: updateMachineStructure,
} = useMachines()
const { updateComposant: updateComposantApi } = useComposants()
const { updatePiece: updatePieceApi } = usePieces()
const { componentTypes, loadComponentTypes } = useComponentTypes()
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const { 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<AnyRecord | null>(null)
const productDocumentsMap = ref<Map<string, AnyRecord[]>>(new Map())
const printAreaRef = ref<HTMLElement | null>(null)
const saving = ref(false)
// Constructeur links
const { fetchLinks, syncLinks } = useConstructeurLinks()
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
// Machine fields
const machineName = ref('')
const machineReference = ref('')
const machineSiteId = ref('')
const machineConstructeurIds = ref<string[]>([])
const machineConstructeurId = computed({
get: () => machineConstructeurIds.value[0] || null,
set: (value: string | null) => {
machineConstructeurIds.value = value ? [value] : []
},
})
const machineConstructeursDisplay = computed(() => {
const ids = 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 debug = ref(false)
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<AnyRecord[]>([])
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 || [])
const componentTypeLabelMap = computed(() => {
const map = new Map<string, string>()
componentTypeOptions.value.forEach((type) => {
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
return map
})
const pieceTypeLabelMap = computed(() => {
const map = new Map<string, string>()
pieceTypeOptions.value.forEach((type) => {
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
return map
})
// 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
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
}
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 (machine.value) {
machine.value.componentLinks = machineComponentLinks.value
machine.value.pieceLinks = machinePieceLinks.value
machine.value.productLinks = machineProductLinks.value
}
if (!linksApplied) {
components.value = transformComponentCustomFields(machinePayload.components || [])
pieces.value = transformCustomFields(machinePayload.pieces || [])
machineProductLinks.value = Array.isArray(machinePayload.productLinks)
? machinePayload.productLinks
: []
}
if (machine.value) {
machine.value.productLinks = machineProductLinks.value
}
collapseAllComponents()
// Load product documents in background
loadProductDocuments().catch(() => {})
// Wait for documents if still loading
await documentPromise
} catch (error) {
console.error('Erreur lors du chargement des données:', error)
} finally {
loading.value = false
}
}
const loadInitialData = (): Promise<unknown[]> => {
return Promise.all([
loadConstructeurs(),
loadComponentTypes(),
loadPieceTypes(),
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, debug,
componentsCollapsed, collapseToggleToken, piecesCollapsed, pieceCollapseToggleToken,
// Computed
componentTypeOptions, pieceTypeOptions, componentTypeLabelMap, pieceTypeLabelMap,
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,
}
}