refactor : merge Inventory_frontend submodule into frontend/ directory
Merges the full git history of Inventory_frontend into the monorepo under frontend/. Removes the submodule in favor of a unified repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
519
frontend/app/composables/useMachineDetailData.ts
Normal file
519
frontend/app/composables/useMachineDetailData.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
/**
|
||||
* 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,
|
||||
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) || ''
|
||||
// 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. 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()
|
||||
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, 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user