- 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>
536 lines
18 KiB
TypeScript
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,
|
|
}
|
|
}
|