Files
Inventory_frontend/app/composables/useMachineDetailData.ts
Matthieu a7415964a7 feat(machine) : single save button + link versioning display
- Replace auto-save-on-blur with single "Enregistrer" button
- Add Cancel button that resets local state
- Expose saveFieldDefinitions via defineExpose on MachineInfoCard
- Remove standalone save button from MachineCustomFieldDefEditor
- Add saveAllMachineCustomFields batch method
- Add submitEdition/cancelEdition/saving/canSubmit to orchestrator
- Show diff summary badges in version list entries
- Show link changes in restore modal description

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:51:34 +01:00

509 lines
16 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,
} from '~/shared/constructeurUtils'
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)
// 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 = 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 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,
transformCustomFields,
transformComponentCustomFields,
syncMachineCustomFields,
setMachineCustomFieldValue,
updateMachineCustomField,
updatePieceCustomField,
saveAllMachineCustomFields,
} = 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,
} = 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) || ''
machineConstructeurIds.value = uniqueConstructeurIds(
machine.value.constructeurIds,
machine.value.constructeurs,
machine.value.constructeur,
)
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,
machineDocumentsLoaded,
machineComponentLinks,
machinePieceLinks,
machineProductLinks,
applyMachineLinks,
refreshMachineDocuments,
transformComponentCustomFields,
transformCustomFields,
loadProductDocuments,
upsertCustomFieldValue,
updateMachineApi,
updateComposantApi: updateComposantApi,
updatePieceApi,
apiPatch,
toast,
})
// 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. Reload machine data to get fresh state
await loadMachineData()
// 4. 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()
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,
sites,
// UI state
machineDocumentFiles, machineDocumentsUploading, machineDocumentsLoaded,
machineCustomFields, previewDocument, previewVisible,
isEditMode, debug,
componentsCollapsed, collapseToggleToken, piecesCollapsed, pieceCollapseToggleToken,
// Computed
componentTypeOptions, pieceTypeOptions, componentTypeLabelMap, pieceTypeLabelMap,
productInventory, productById, flattenedComponents, machinePieces,
machineDirectProducts, machineDocumentsList, visibleMachineCustomFields,
// Methods
findProductById, resolveProductReference, getProductDisplay,
initMachineFields, getMachineFieldId,
syncMachineCustomFields, setMachineCustomFieldValue,
updateMachineCustomField, updatePieceCustomField,
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,
// External
constructeurs, loadProducts, updateMachineStructure, toast,
downloadDocument: downloadDocumentHelper,
}
}