From c831f65ef37f3d71bb86c46e8e78c47a0281b449 Mon Sep 17 00:00:00 2001 From: matthieu Date: Sun, 8 Mar 2026 14:53:37 +0100 Subject: [PATCH] refactor(frontend) : split useMachineDetailData into focused composables Co-Authored-By: Claude Opus 4.6 --- .../useMachineDetailCustomFields.ts | 396 +++++ app/composables/useMachineDetailData.ts | 1278 +++-------------- app/composables/useMachineDetailDocuments.ts | 146 ++ app/composables/useMachineDetailHierarchy.ts | 306 ++++ app/composables/useMachineDetailProducts.ts | 132 ++ app/composables/useMachineDetailUpdates.ts | 214 +++ 6 files changed, 1358 insertions(+), 1114 deletions(-) create mode 100644 app/composables/useMachineDetailCustomFields.ts create mode 100644 app/composables/useMachineDetailDocuments.ts create mode 100644 app/composables/useMachineDetailHierarchy.ts create mode 100644 app/composables/useMachineDetailProducts.ts create mode 100644 app/composables/useMachineDetailUpdates.ts diff --git a/app/composables/useMachineDetailCustomFields.ts b/app/composables/useMachineDetailCustomFields.ts new file mode 100644 index 0000000..62a36f7 --- /dev/null +++ b/app/composables/useMachineDetailCustomFields.ts @@ -0,0 +1,396 @@ +/** + * Machine detail — custom field management sub-composable. + * + * Handles custom field resolution, display filtering, sync and updates + * for machines, components and pieces. + */ + +import { ref, computed } from 'vue' +import { useCustomFields } from '~/composables/useCustomFields' +import { useToast } from '~/composables/useToast' +import { normalizeStructureForEditor } from '~/shared/modelUtils' +import { + shouldDisplayCustomField, + normalizeExistingCustomFieldDefinitions, + normalizeCustomFieldValueEntry, + mergeCustomFieldValuesWithDefinitions, + dedupeCustomFieldEntries, +} from '~/shared/utils/customFieldUtils' +import { + resolveConstructeurs, + uniqueConstructeurIds, +} from '~/shared/constructeurUtils' + +type AnyRecord = Record + +interface MachineDetailCustomFieldsDeps { + machine: Ref + isEditMode: Ref + constructeurs: Ref + resolveProductReference: (source: AnyRecord) => { product: unknown; productId: string | null } + getProductDisplay: (source: AnyRecord) => unknown +} + +export function useMachineDetailCustomFields(deps: MachineDetailCustomFieldsDeps) { + const { machine, isEditMode, constructeurs, resolveProductReference, getProductDisplay } = deps + const { + upsertCustomFieldValue, + updateCustomFieldValue: updateCustomFieldValueApi, + } = useCustomFields() + const toast = useToast() + + // --------------------------------------------------------------------------- + // State + // --------------------------------------------------------------------------- + + const machineCustomFields = ref([]) + + // --------------------------------------------------------------------------- + // Computed + // --------------------------------------------------------------------------- + + const visibleMachineCustomFields = computed(() => { + const fields = Array.isArray(machineCustomFields.value) ? machineCustomFields.value : [] + if (isEditMode.value) return fields + return fields.filter((field) => shouldDisplayCustomField(field)) + }) + + // --------------------------------------------------------------------------- + // Transform helpers + // --------------------------------------------------------------------------- + + const getStructureCustomFields = (structure: unknown): AnyRecord[] => { + if (!structure || typeof structure !== 'object') return [] + const normalized = normalizeStructureForEditor(structure as any) as any + return Array.isArray(normalized?.customFields) + ? (normalized.customFields as AnyRecord[]) + : [] + } + + const transformCustomFields = (piecesData: AnyRecord[]): AnyRecord[] => { + return (piecesData || []).map((piece) => { + const typePiece = (piece.typePiece as AnyRecord) || {} + + const normalizeStructureDefs = (structure: unknown) => + structure ? normalizeStructureForEditor(structure as AnyRecord) : null + + const normalizedStructureDefs = [ + normalizeStructureDefs((piece.definition as AnyRecord)?.structure), + normalizeStructureDefs(typePiece.structure), + ] + + const valueEntries = [ + ...(Array.isArray(piece.customFieldValues) ? piece.customFieldValues : []), + ...(Array.isArray(piece.customFields) + ? (piece.customFields as AnyRecord[]) + .map(normalizeCustomFieldValueEntry) + .filter((e) => e !== null) + : []), + ...(Array.isArray(typePiece.customFieldValues) + ? (typePiece.customFieldValues as AnyRecord[]) + .map(normalizeCustomFieldValueEntry) + .filter((e) => e !== null) + : []), + ] + + const customFields = dedupeCustomFieldEntries( + mergeCustomFieldValuesWithDefinitions( + valueEntries, + normalizeExistingCustomFieldDefinitions(piece.customFields), + normalizeExistingCustomFieldDefinitions((piece.definition as AnyRecord)?.customFields), + normalizeExistingCustomFieldDefinitions((piece.typePiece as AnyRecord)?.customFields), + normalizeExistingCustomFieldDefinitions(typePiece.customFields), + ...normalizedStructureDefs.map((def) => getStructureCustomFields(def)), + ), + ) + + const constructeurIds = uniqueConstructeurIds( + piece.constructeurs, + piece.constructeurIds, + piece.constructeurId, + piece.constructeur, + (piece.originalPiece as AnyRecord)?.constructeurs, + (piece.originalPiece as AnyRecord)?.constructeurIds, + (piece.originalPiece as AnyRecord)?.constructeurId, + (piece.originalPiece as AnyRecord)?.constructeur, + ) + + const { product: resolvedProduct, productId: resolvedProductId } = + resolveProductReference(piece) + + const constructeursList = resolveConstructeurs( + constructeurIds, + Array.isArray(piece.constructeurs) ? (piece.constructeurs as any[]) : [], + piece.constructeur ? [piece.constructeur as any] : [], + Array.isArray((piece.originalPiece as AnyRecord)?.constructeurs) + ? ((piece.originalPiece as AnyRecord).constructeurs as any[]) + : [], + (piece.originalPiece as AnyRecord)?.constructeur + ? [(piece.originalPiece as AnyRecord).constructeur as any] + : [], + constructeurs.value as any, + ) as any[] + + const normalizedPiece = { + ...piece, + product: resolvedProduct || piece.product || null, + productId: resolvedProductId || piece.productId || (piece.product as AnyRecord)?.id || null, + } + const productDisplay = getProductDisplay(normalizedPiece) + + return { + ...normalizedPiece, + customFields, + documents: piece.documents || [], + constructeurs: constructeursList, + constructeur: constructeursList[0] || piece.constructeur || null, + constructeurIds, + constructeurId: constructeurIds[0] || null, + typePieceId: + piece.typePieceId || + (piece.typePiece as AnyRecord)?.id || + null, + __productDisplay: productDisplay, + } + }) + } + + const transformComponentCustomFields = (componentsData: AnyRecord[]): AnyRecord[] => { + const normalizeStructureDefs = (structure: unknown) => + structure ? normalizeStructureForEditor(structure as AnyRecord) : null + + return (componentsData || []).map((component) => { + const type = (component.typeComposant as AnyRecord) || {} + + const normalizedStructureDefs = [ + normalizeStructureDefs((component.definition as AnyRecord)?.structure), + normalizeStructureDefs(type.structure), + ] + + const actualComponent = (component.originalComposant as AnyRecord) || component + + const valueEntries = [ + ...(Array.isArray(component.customFieldValues) ? component.customFieldValues : []), + ...(Array.isArray(component.customFields) + ? (component.customFields as AnyRecord[]) + .map(normalizeCustomFieldValueEntry) + .filter((e) => e !== null) + : []), + ...(Array.isArray(actualComponent?.customFields) + ? (actualComponent.customFields as AnyRecord[]) + .map(normalizeCustomFieldValueEntry) + .filter((e) => e !== null) + : []), + ] + + const customFields = dedupeCustomFieldEntries( + mergeCustomFieldValuesWithDefinitions( + valueEntries, + normalizeExistingCustomFieldDefinitions(component.customFields), + normalizeExistingCustomFieldDefinitions((component.definition as AnyRecord)?.customFields), + normalizeExistingCustomFieldDefinitions((component.typeComposant as AnyRecord)?.customFields), + normalizeExistingCustomFieldDefinitions(type.customFields), + normalizeExistingCustomFieldDefinitions(actualComponent?.customFields), + ...normalizedStructureDefs.map((def) => getStructureCustomFields(def)), + ), + ) + + const piecesTransformed = component.pieces + ? transformCustomFields(component.pieces as AnyRecord[]).map((p) => ({ + ...p, + parentComponentName: component.name, + })) + : [] + + const subComponents = component.sousComposants + ? transformComponentCustomFields(component.sousComposants as AnyRecord[]) + : [] + + const constructeurIds = uniqueConstructeurIds( + component.constructeurs, + component.constructeurIds, + component.constructeurId, + component.constructeur, + actualComponent?.constructeurs, + actualComponent?.constructeurIds, + actualComponent?.constructeurId, + actualComponent?.constructeur, + ) + + const constructeursList = resolveConstructeurs( + constructeurIds, + Array.isArray(component.constructeurs) ? (component.constructeurs as any[]) : [], + component.constructeur ? [component.constructeur as any] : [], + Array.isArray(actualComponent?.constructeurs) + ? (actualComponent.constructeurs as any[]) + : [], + actualComponent?.constructeur ? [actualComponent.constructeur as any] : [], + constructeurs.value as any, + ) as any[] + + const { product: resolvedProduct, productId: resolvedProductId } = + resolveProductReference(component) + const normalizedComponent = { + ...component, + product: resolvedProduct || component.product || null, + productId: + resolvedProductId || component.productId || (component.product as AnyRecord)?.id || null, + } + const productDisplay = getProductDisplay(normalizedComponent) + + return { + ...normalizedComponent, + customFields, + pieces: piecesTransformed, + subComponents, + documents: component.documents || [], + constructeurs: constructeursList, + constructeur: constructeursList[0] || component.constructeur || null, + constructeurIds, + constructeurId: constructeurIds[0] || null, + typeComposantId: + component.typeComposantId || + (component.typeComposant as AnyRecord)?.id || + null, + __productDisplay: productDisplay, + } + }) + } + + // --------------------------------------------------------------------------- + // Machine custom field methods + // --------------------------------------------------------------------------- + + const syncMachineCustomFields = () => { + if (!machine.value) { + machineCustomFields.value = [] + return + } + const valueEntries = [ + ...(Array.isArray(machine.value.customFieldValues) ? machine.value.customFieldValues : []), + ...(Array.isArray(machine.value.customFields) + ? (machine.value.customFields as AnyRecord[]) + .map(normalizeCustomFieldValueEntry) + .filter((e) => e !== null) + : []), + ] + const merged = dedupeCustomFieldEntries( + mergeCustomFieldValuesWithDefinitions( + valueEntries, + normalizeExistingCustomFieldDefinitions(machine.value.customFields), + ), + ).map((field: AnyRecord) => ({ ...field, readOnly: false })) + machineCustomFields.value = merged + } + + const setMachineCustomFieldValue = (field: AnyRecord, value: unknown) => { + if (!field) return + field.value = value + if (field.customFieldValueId && (machine.value as AnyRecord)?.customFieldValues) { + const stored = ((machine.value as AnyRecord).customFieldValues as AnyRecord[]).find( + (fv) => fv.id === field.customFieldValueId, + ) + if (stored) stored.value = value + } + } + + const updateMachineCustomField = async (field: AnyRecord) => { + if (!machine.value || !field) return + + const { id: customFieldId, customFieldValueId } = field + const fieldLabel = (field.name as string) || 'Champ personnalisé' + + try { + if (customFieldValueId) { + const result: any = await updateCustomFieldValueApi(customFieldValueId as string, { + value: field.value ?? '', + } as any) + if (result.success) { + toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`) + syncMachineCustomFields() + } else { + toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`) + } + return + } + if (!customFieldId) { + toast.showError( + 'Impossible de mettre à jour ce champ personnalisé (identifiant manquant).', + ) + return + } + const result: any = await upsertCustomFieldValue( + customFieldId as string, + 'machine', + machine.value.id as string, + field.value ?? '', + ) + if (result.success) { + const createdValue = result.data as AnyRecord + toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`) + if (createdValue?.id) { + if (!createdValue.customField) { + createdValue.customField = { + id: customFieldId, + name: field.name, + type: field.type, + required: field.required, + options: field.options, + } + } + field.customFieldValueId = createdValue.id + field.readOnly = false + const existingValues = Array.isArray(machine.value.customFieldValues) + ? (machine.value.customFieldValues as AnyRecord[]).filter( + (item) => item.id !== createdValue.id, + ) + : [] + machine.value.customFieldValues = [...existingValues, createdValue] + } + syncMachineCustomFields() + } else { + toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`) + } + } catch (error) { + console.error('Erreur lors de la mise à jour du champ personnalisé de la machine:', error) + toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`) + } + } + + const updatePieceCustomField = async (fieldUpdate: AnyRecord) => { + try { + const result: any = await upsertCustomFieldValue( + fieldUpdate.fieldId as string, + 'piece', + fieldUpdate.pieceId as string, + fieldUpdate.value, + ) + if (result.success) { + toast.showSuccess('Champ personnalisé mis à jour avec succès') + } else { + toast.showError('Erreur lors de la mise à jour du champ personnalisé') + } + } catch (error) { + toast.showError('Erreur lors de la mise à jour du champ personnalisé') + console.error('Erreur lors de la mise à jour du champ personnalisé:', error) + } + } + + return { + // State + machineCustomFields, + + // Computed + visibleMachineCustomFields, + + // Transform functions + transformCustomFields, + transformComponentCustomFields, + + // Methods + syncMachineCustomFields, + setMachineCustomFieldValue, + updateMachineCustomField, + updatePieceCustomField, + } +} diff --git a/app/composables/useMachineDetailData.ts b/app/composables/useMachineDetailData.ts index ebd4946..dc1ae6f 100644 --- a/app/composables/useMachineDetailData.ts +++ b/app/composables/useMachineDetailData.ts @@ -1,9 +1,8 @@ /** - * Machine detail page — core state & business logic. + * Machine detail page — core state & business logic (orchestrator). * * Extracted from pages/machine/[id].vue (F1.1). - * Manages reactive state, data loading, transforms, updates, - * document management and custom field logic. + * Composes sub-composables for documents, custom fields, hierarchy and products. */ import { ref, computed, watch } from 'vue' @@ -15,55 +14,24 @@ import { usePieceTypes } from '~/composables/usePieceTypes' import { useCustomFields } from '~/composables/useCustomFields' import { useApi } from '~/composables/useApi' import { useToast } from '~/composables/useToast' -import { useDocuments } from '~/composables/useDocuments' import { useConstructeurs } from '~/composables/useConstructeurs' -import { useProducts } from '~/composables/useProducts' import { useMachinePrint } from '~/composables/useMachinePrint' -import { getFileIcon } from '~/utils/fileIcons' -import { normalizeStructureForEditor } from '~/shared/modelUtils' import { resolveConstructeurs, uniqueConstructeurIds, formatConstructeurContact as formatConstructeurContactSummary, } from '~/shared/constructeurUtils' -import { - formatCustomFieldValue, - shouldDisplayCustomField, - normalizeExistingCustomFieldDefinitions, - normalizeCustomFieldValueEntry, - mergeCustomFieldValuesWithDefinitions, - dedupeCustomFieldEntries, - summarizeCustomFields, -} from '~/shared/utils/customFieldUtils' -import { - resolveIdentifier, - resolveProductReference as _resolveProductReference, - getProductDisplay as _getProductDisplay, - getProductSuppliersLabel, - getProductPriceLabel, - extractParentLinkIdentifiers, -} from '~/shared/utils/productDisplayUtils' -import { - buildMachineHierarchyFromLinks, - resolveLinkArray, -} from '~/composables/useMachineHierarchy' -import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview' -import { - formatSize, - shouldInlinePdf, - documentPreviewSrc, - documentThumbnailClass, - documentIcon, - downloadDocument as downloadDocumentHelper, -} from '~/shared/utils/documentDisplayUtils' +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, @@ -72,18 +40,8 @@ export function useMachineDetailData(machineId: string) { const { updatePiece: updatePieceApi } = usePieces() const { componentTypes, loadComponentTypes } = useComponentTypes() const { pieceTypes, loadPieceTypes } = usePieceTypes() - const { products, loadProducts } = useProducts() - const { - upsertCustomFieldValue, - updateCustomFieldValue: updateCustomFieldValueApi, - } = useCustomFields() - const { get, post: apiPost, delete: apiDel } = useApi() - const { - uploadDocuments, - deleteDocument, - loadDocumentsByMachine, - loadDocumentsByProduct, - } = useDocuments() + const { upsertCustomFieldValue } = useCustomFields() + const { get } = useApi() const toast = useToast() const { constructeurs, loadConstructeurs } = useConstructeurs() @@ -97,24 +55,13 @@ export function useMachineDetailData(machineId: string) { handlePrintConfirm: _handlePrintConfirm, } = useMachinePrint() - // --------------------------------------------------------------------------- // Core state - // --------------------------------------------------------------------------- - const loading = ref(true) const machine = ref(null) - const components = ref([]) - const pieces = ref([]) - const machineComponentLinks = ref([]) - const machinePieceLinks = ref([]) - const machineProductLinks = ref([]) const productDocumentsMap = ref>(new Map()) const printAreaRef = ref(null) - // --------------------------------------------------------------------------- // Machine fields - // --------------------------------------------------------------------------- - const machineName = ref('') const machineReference = ref('') const machineConstructeurIds = ref([]) @@ -156,17 +103,7 @@ export function useMachineDetailData(machineId: string) { () => machineConstructeursDisplay.value.length > 0, ) - // --------------------------------------------------------------------------- // UI state - // --------------------------------------------------------------------------- - - const machineDocumentFiles = ref([]) - const machineDocumentsUploading = ref(false) - const machineDocumentsLoaded = ref(false) - const machineCustomFields = ref([]) - const previewDocument = ref(null) - const previewVisible = ref(false) - const isEditMode = ref(false) const debug = ref(false) @@ -176,10 +113,93 @@ export function useMachineDetailData(machineId: string) { const piecesCollapsed = ref(true) const pieceCollapseToggleToken = ref(0) - // --------------------------------------------------------------------------- - // Product helpers - // --------------------------------------------------------------------------- + // 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, + transformCustomFields, + transformComponentCustomFields, + syncMachineCustomFields, + setMachineCustomFieldValue, + updateMachineCustomField, + updatePieceCustomField, + } = 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 || []) @@ -199,444 +219,7 @@ export function useMachineDetailData(machineId: string) { return map }) - const productInventory = computed(() => products.value || []) - - const productById = computed(() => { - const map = new Map() - ;(productInventory.value as AnyRecord[]).forEach((product: AnyRecord) => { - if (product?.id) map.set(product.id as string, product) - }) - return map - }) - - const findProductById = (productId: string | null | undefined): AnyRecord | null => { - if (!productId) return null - return productById.value.get(productId) || null - } - - const resolveProductReference = (source: AnyRecord) => - _resolveProductReference(source, findProductById as any) - const getProductDisplay = (source: AnyRecord) => - _getProductDisplay(source, findProductById as any) - - // --------------------------------------------------------------------------- - // Helpers - // --------------------------------------------------------------------------- - - const isPlainObject = (value: unknown): boolean => - Object.prototype.toString.call(value) === '[object Object]' - - const flattenComponents = (list: AnyRecord[] = []): AnyRecord[] => { - const result: AnyRecord[] = [] - const traverse = (items: AnyRecord[]) => { - items.forEach((item) => { - result.push(item) - if (Array.isArray(item.subComponents) && item.subComponents.length) { - traverse(item.subComponents as AnyRecord[]) - } - }) - } - traverse(list) - return result - } - - const findComponentById = (items: AnyRecord[] | undefined, id: string): AnyRecord | null => { - for (const item of items || []) { - if (item.id === id) return item - const found = findComponentById(item.subComponents as AnyRecord[] | undefined, id) - if (found) return found - } - return null - } - - const findPieceById = (pieceId: string): AnyRecord | null => { - const direct = pieces.value.find((p) => p.id === pieceId) - if (direct) return direct - - const searchInComponents = (items: AnyRecord[]): AnyRecord | null => { - for (const item of items || []) { - const match = ((item.pieces as AnyRecord[]) || []).find((p) => p.id === pieceId) - if (match) return match - const nested = searchInComponents((item.subComponents as AnyRecord[]) || []) - if (nested) return nested - } - return null - } - return searchInComponents(components.value) - } - - const collectConstructeurs = (...sources: unknown[]): AnyRecord[] => { - const ids = uniqueConstructeurIds(...sources) - if (!ids.length) return [] - const pools = sources - .flatMap((source) => { - if (Array.isArray(source)) return [source] - if (source && typeof source === 'object' && (source as AnyRecord).id) return [[source]] - return [] - }) - .filter(Boolean) as AnyRecord[][] - return resolveConstructeurs(ids, ...(pools as any[])) as any[] - } - - // --------------------------------------------------------------------------- - // Transform functions - // --------------------------------------------------------------------------- - - const getStructureCustomFields = (structure: unknown): AnyRecord[] => { - if (!structure || typeof structure !== 'object') return [] - const normalized = normalizeStructureForEditor(structure as any) as any - return Array.isArray(normalized?.customFields) - ? (normalized.customFields as AnyRecord[]) - : [] - } - - const transformCustomFields = (piecesData: AnyRecord[]): AnyRecord[] => { - return (piecesData || []).map((piece) => { - const typePiece = (piece.typePiece as AnyRecord) || {} - - const normalizeStructureDefs = (structure: unknown) => - structure ? normalizeStructureForEditor(structure as AnyRecord) : null - - const normalizedStructureDefs = [ - normalizeStructureDefs((piece.definition as AnyRecord)?.structure), - normalizeStructureDefs((piece.typePiece as AnyRecord)?.structure), - normalizeStructureDefs(typePiece.structure), - ] - - const valueEntries = [ - ...(Array.isArray(piece.customFieldValues) ? piece.customFieldValues : []), - ...(Array.isArray(piece.customFields) - ? (piece.customFields as AnyRecord[]) - .map(normalizeCustomFieldValueEntry) - .filter((e) => e !== null) - : []), - ...(Array.isArray(typePiece.customFieldValues) - ? (typePiece.customFieldValues as AnyRecord[]) - .map(normalizeCustomFieldValueEntry) - .filter((e) => e !== null) - : []), - ] - - const customFields = dedupeCustomFieldEntries( - mergeCustomFieldValuesWithDefinitions( - valueEntries, - normalizeExistingCustomFieldDefinitions(piece.customFields), - normalizeExistingCustomFieldDefinitions((piece.definition as AnyRecord)?.customFields), - normalizeExistingCustomFieldDefinitions((piece.typePiece as AnyRecord)?.customFields), - normalizeExistingCustomFieldDefinitions(typePiece.customFields), - ...normalizedStructureDefs.map((def) => getStructureCustomFields(def)), - ), - ) - - const constructeurIds = uniqueConstructeurIds( - piece.constructeurs, - piece.constructeurIds, - piece.constructeurId, - piece.constructeur, - (piece.originalPiece as AnyRecord)?.constructeurs, - (piece.originalPiece as AnyRecord)?.constructeurIds, - (piece.originalPiece as AnyRecord)?.constructeurId, - (piece.originalPiece as AnyRecord)?.constructeur, - ) - - const { product: resolvedProduct, productId: resolvedProductId } = - resolveProductReference(piece) - - const constructeursList = resolveConstructeurs( - constructeurIds, - Array.isArray(piece.constructeurs) ? (piece.constructeurs as any[]) : [], - piece.constructeur ? [piece.constructeur as any] : [], - Array.isArray((piece.originalPiece as AnyRecord)?.constructeurs) - ? ((piece.originalPiece as AnyRecord).constructeurs as any[]) - : [], - (piece.originalPiece as AnyRecord)?.constructeur - ? [(piece.originalPiece as AnyRecord).constructeur as any] - : [], - constructeurs.value as any, - ) as any[] - - const normalizedPiece = { - ...piece, - product: resolvedProduct || piece.product || null, - productId: resolvedProductId || piece.productId || (piece.product as AnyRecord)?.id || null, - } - const productDisplay = getProductDisplay(normalizedPiece) - - return { - ...normalizedPiece, - customFields, - documents: piece.documents || [], - constructeurs: constructeursList, - constructeur: constructeursList[0] || piece.constructeur || null, - constructeurIds, - constructeurId: constructeurIds[0] || null, - typePieceId: - piece.typePieceId || - (piece.typePiece as AnyRecord)?.id || - null, - __productDisplay: productDisplay, - } - }) - } - - const transformComponentCustomFields = (componentsData: AnyRecord[]): AnyRecord[] => { - const normalizeStructureDefs = (structure: unknown) => - structure ? normalizeStructureForEditor(structure as AnyRecord) : null - - return (componentsData || []).map((component) => { - const type = (component.typeComposant as AnyRecord) || {} - - const normalizedStructureDefs = [ - normalizeStructureDefs((component.definition as AnyRecord)?.structure), - normalizeStructureDefs((component.typeComposant as AnyRecord)?.structure), - normalizeStructureDefs(type.structure), - ] - - const actualComponent = (component.originalComposant as AnyRecord) || component - - const valueEntries = [ - ...(Array.isArray(component.customFieldValues) ? component.customFieldValues : []), - ...(Array.isArray(component.customFields) - ? (component.customFields as AnyRecord[]) - .map(normalizeCustomFieldValueEntry) - .filter((e) => e !== null) - : []), - ...(Array.isArray(actualComponent?.customFields) - ? (actualComponent.customFields as AnyRecord[]) - .map(normalizeCustomFieldValueEntry) - .filter((e) => e !== null) - : []), - ] - - const customFields = dedupeCustomFieldEntries( - mergeCustomFieldValuesWithDefinitions( - valueEntries, - normalizeExistingCustomFieldDefinitions(component.customFields), - normalizeExistingCustomFieldDefinitions((component.definition as AnyRecord)?.customFields), - normalizeExistingCustomFieldDefinitions((component.typeComposant as AnyRecord)?.customFields), - normalizeExistingCustomFieldDefinitions(type.customFields), - normalizeExistingCustomFieldDefinitions(actualComponent?.customFields), - ...normalizedStructureDefs.map((def) => getStructureCustomFields(def)), - ), - ) - - const piecesTransformed = component.pieces - ? transformCustomFields(component.pieces as AnyRecord[]).map((p) => ({ - ...p, - parentComponentName: component.name, - })) - : [] - - const subComponents = component.sousComposants - ? transformComponentCustomFields(component.sousComposants as AnyRecord[]) - : [] - - const constructeurIds = uniqueConstructeurIds( - component.constructeurs, - component.constructeurIds, - component.constructeurId, - component.constructeur, - actualComponent?.constructeurs, - actualComponent?.constructeurIds, - actualComponent?.constructeurId, - actualComponent?.constructeur, - ) - - const constructeursList = resolveConstructeurs( - constructeurIds, - Array.isArray(component.constructeurs) ? (component.constructeurs as any[]) : [], - component.constructeur ? [component.constructeur as any] : [], - Array.isArray(actualComponent?.constructeurs) - ? (actualComponent.constructeurs as any[]) - : [], - actualComponent?.constructeur ? [actualComponent.constructeur as any] : [], - constructeurs.value as any, - ) as any[] - - const { product: resolvedProduct, productId: resolvedProductId } = - resolveProductReference(component) - const normalizedComponent = { - ...component, - product: resolvedProduct || component.product || null, - productId: - resolvedProductId || component.productId || (component.product as AnyRecord)?.id || null, - } - const productDisplay = getProductDisplay(normalizedComponent) - - return { - ...normalizedComponent, - customFields, - pieces: piecesTransformed, - subComponents, - documents: component.documents || [], - constructeurs: constructeursList, - constructeur: constructeursList[0] || component.constructeur || null, - constructeurIds, - constructeurId: constructeurIds[0] || null, - typeComposantId: - component.typeComposantId || - (component.typeComposant as AnyRecord)?.id || - null, - __productDisplay: productDisplay, - } - }) - } - - // --------------------------------------------------------------------------- - // Hierarchy & links - // --------------------------------------------------------------------------- - - const applyMachineLinks = (source: AnyRecord): boolean => { - const container = (source?.machine as AnyRecord) ?? null - const componentLinksData = - resolveLinkArray(source, ['componentLinks', 'machineComponentLinks']) ?? - resolveLinkArray(container, ['componentLinks', 'machineComponentLinks']) - const pieceLinksData = - resolveLinkArray(source, ['pieceLinks', 'machinePieceLinks']) ?? - resolveLinkArray(container, ['pieceLinks', 'machinePieceLinks']) - const productLinksData = - resolveLinkArray(source, ['productLinks', 'machineProductLinks']) ?? - resolveLinkArray(container, ['productLinks', 'machineProductLinks']) - - if (componentLinksData === null && pieceLinksData === null && productLinksData === null) { - return false - } - - const normalizedComponentLinks = (componentLinksData ?? []) as AnyRecord[] - const normalizedPieceLinks = (pieceLinksData ?? []) as AnyRecord[] - const normalizedProductLinks = (productLinksData ?? []) as AnyRecord[] - - machineComponentLinks.value = normalizedComponentLinks - machinePieceLinks.value = normalizedPieceLinks - machineProductLinks.value = normalizedProductLinks - - const { components: hierarchy, machinePieces: machineLevelPieces } = - buildMachineHierarchyFromLinks( - normalizedComponentLinks, - normalizedPieceLinks, - findProductById as any, - constructeurs.value, - ) - - components.value = transformComponentCustomFields(hierarchy as AnyRecord[]) - pieces.value = transformCustomFields(machineLevelPieces as AnyRecord[]) - - return true - } - - // --------------------------------------------------------------------------- - // Computed values - // --------------------------------------------------------------------------- - - const flattenedComponents = computed(() => flattenComponents(components.value)) - - const machinePieces = computed(() => { - return pieces.value.filter((piece) => { - const parentLinkId = resolveIdentifier( - piece.parentComponentLinkId, - (piece.machinePieceLink as AnyRecord)?.parentComponentLinkId, - piece.parentLinkId, - ) - if (parentLinkId) return false - return !piece.composantId - }) - }) - - const machineDirectProducts = computed(() => { - return machineProductLinks.value.map((link) => { - const productObj = link.product as AnyRecord | string | null - let resolved: AnyRecord | null = null - let productId: string | null = null - - if (typeof productObj === 'string') { - productId = productObj.split('/').pop() || null - resolved = productId ? findProductById(productId) : null - } else if (productObj && typeof productObj === 'object') { - productId = (productObj as AnyRecord)?.id as string | null - // Prefer the embedded product from the structure endpoint — it has richer - // data (typeProduct as object, supplierPrice, constructeurs) than the - // global products cache which may store typeProduct as an IRI string. - const cached = productId ? findProductById(productId) : null - resolved = productObj as AnyRecord - if (cached) { - // Merge: use embedded as base, overlay any non-null cached fields - resolved = { ...resolved, ...Object.fromEntries( - Object.entries(cached as AnyRecord).filter(([, v]) => v != null && v !== ''), - ) } - // But always prefer the embedded typeProduct when it's an object - if (productObj.typeProduct && typeof productObj.typeProduct === 'object') { - resolved.typeProduct = productObj.typeProduct - } - } - } - - const constructeurIds = uniqueConstructeurIds( - resolved?.constructeurs, - resolved?.constructeurIds, - ) - const resolvedConstructeurs = resolveConstructeurs( - constructeurIds, - resolved?.constructeurs as any[] || [], - constructeurs.value, - ) - - return { - id: (resolved?.id as string) || productId || null, - linkId: (link.id as string) || (typeof link['@id'] === 'string' ? link['@id'].split('/').pop() : null) || null, - name: (resolved?.name as string) || 'Produit inconnu', - reference: (resolved?.reference as string) || null, - supplierLabel: resolvedConstructeurs.length - ? resolvedConstructeurs.map((c) => c.name).filter(Boolean).join(', ') || null - : getProductSuppliersLabel(resolved), - priceLabel: resolved ? getProductPriceLabel(resolved) : null, - groupLabel: ((resolved?.typeProduct as AnyRecord)?.name as string) || '', - documents: productId ? (productDocumentsMap.value.get(productId) || []) : [], - } - }) - }) - - const loadProductDocuments = async () => { - const productIds = machineProductLinks.value - .map((link) => { - const p = link.product as AnyRecord | string | null - if (typeof p === 'string') return p.split('/').pop() || null - return (p as AnyRecord)?.id as string | null - }) - .filter((id): id is string => !!id) - - const results = await Promise.allSettled( - productIds.map(async (id) => { - const result: any = await loadDocumentsByProduct(id, { updateStore: false }) - if (result.success && Array.isArray(result.data)) { - return { id, docs: result.data as AnyRecord[] } - } - return { id, docs: [] } - }), - ) - - const map = new Map() - results.forEach((r) => { - if (r.status === 'fulfilled' && r.value.docs.length) { - map.set(r.value.id, r.value.docs) - } - }) - productDocumentsMap.value = map - } - - const machineDocumentsList = computed( - () => ((machine.value as AnyRecord)?.documents as AnyRecord[]) || [], - ) - - const visibleMachineCustomFields = computed(() => { - const fields = Array.isArray(machineCustomFields.value) ? machineCustomFields.value : [] - if (isEditMode.value) return fields - return fields.filter((field) => shouldDisplayCustomField(field)) - }) - - // --------------------------------------------------------------------------- // Machine field methods - // --------------------------------------------------------------------------- - const initMachineFields = () => { if (machine.value) { machineName.value = (machine.value.name as string) || '' @@ -653,349 +236,43 @@ export function useMachineDetailData(machineId: string) { return machine.value ? `machine-${fieldName}-${machine.value.id}` : `machine-${fieldName}` } - // --------------------------------------------------------------------------- - // Custom field methods - // --------------------------------------------------------------------------- - - const syncMachineCustomFields = () => { - if (!machine.value) { - machineCustomFields.value = [] - return - } - const valueEntries = [ - ...(Array.isArray(machine.value.customFieldValues) ? machine.value.customFieldValues : []), - ...(Array.isArray(machine.value.customFields) - ? (machine.value.customFields as AnyRecord[]) - .map(normalizeCustomFieldValueEntry) - .filter((e) => e !== null) - : []), - ] - const merged = dedupeCustomFieldEntries( - mergeCustomFieldValuesWithDefinitions( - valueEntries, - normalizeExistingCustomFieldDefinitions(machine.value.customFields), - ), - ).map((field: AnyRecord) => ({ ...field, readOnly: false })) - machineCustomFields.value = merged + // Product documents wrapper + const loadProductDocuments = async () => { + const map = await _loadProductDocuments(machineProductLinks.value) + productDocumentsMap.value = map } - const setMachineCustomFieldValue = (field: AnyRecord, value: unknown) => { - if (!field) return - field.value = value - if (field.customFieldValueId && (machine.value as AnyRecord)?.customFieldValues) { - const stored = ((machine.value as AnyRecord).customFieldValues as AnyRecord[]).find( - (fv) => fv.id === field.customFieldValueId, - ) - if (stored) stored.value = value - } - } + // Update methods (delegated to useMachineDetailUpdates) + const { + updateMachineInfo, + updateComponent, + updatePieceFromComponent, + updatePieceInfo, + handleMachineConstructeurChange, + editComponent, + editPiece, + } = useMachineDetailUpdates({ + machine, + machineName, + machineReference, + machineConstructeurIds, + machineDocumentsLoaded, + machineComponentLinks, + machinePieceLinks, + machineProductLinks, + applyMachineLinks, + refreshMachineDocuments, + transformComponentCustomFields, + transformCustomFields, + loadProductDocuments, + upsertCustomFieldValue, + updateMachineApi, + updateComposantApi: updateComposantApi, + updatePieceApi, + toast, + }) - const updateMachineCustomField = async (field: AnyRecord) => { - if (!machine.value || !field) return - - const { id: customFieldId, customFieldValueId } = field - const fieldLabel = (field.name as string) || 'Champ personnalisé' - - try { - if (customFieldValueId) { - const result: any = await updateCustomFieldValueApi(customFieldValueId as string, { - value: field.value ?? '', - } as any) - if (result.success) { - toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`) - syncMachineCustomFields() - } else { - toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`) - } - return - } - if (!customFieldId) { - toast.showError( - 'Impossible de mettre à jour ce champ personnalisé (identifiant manquant).', - ) - return - } - const result: any = await upsertCustomFieldValue( - customFieldId as string, - 'machine', - machine.value.id as string, - field.value ?? '', - ) - if (result.success) { - const createdValue = result.data as AnyRecord - toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`) - if (createdValue?.id) { - if (!createdValue.customField) { - createdValue.customField = { - id: customFieldId, - name: field.name, - type: field.type, - required: field.required, - options: field.options, - } - } - field.customFieldValueId = createdValue.id - field.readOnly = false - const existingValues = Array.isArray(machine.value.customFieldValues) - ? (machine.value.customFieldValues as AnyRecord[]).filter( - (item) => item.id !== createdValue.id, - ) - : [] - machine.value.customFieldValues = [...existingValues, createdValue] - } - syncMachineCustomFields() - } else { - toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`) - } - } catch (error) { - console.error('Erreur lors de la mise à jour du champ personnalisé de la machine:', error) - toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`) - } - } - - const updatePieceCustomField = async (fieldUpdate: AnyRecord) => { - try { - const result: any = await upsertCustomFieldValue( - fieldUpdate.fieldId as string, - 'piece', - fieldUpdate.pieceId as string, - fieldUpdate.value, - ) - if (result.success) { - toast.showSuccess('Champ personnalisé mis à jour avec succès') - } else { - toast.showError('Erreur lors de la mise à jour du champ personnalisé') - } - } catch (error) { - toast.showError('Erreur lors de la mise à jour du champ personnalisé') - console.error('Erreur lors de la mise à jour du champ personnalisé:', error) - } - } - - // --------------------------------------------------------------------------- - // Document methods - // --------------------------------------------------------------------------- - - const refreshMachineDocuments = async () => { - if (!machine.value?.id) return - const result: any = await loadDocumentsByMachine(machine.value.id as string, { updateStore: false }) - if (result.success && machine.value) { - machine.value.documents = result.data || [] - machineDocumentsLoaded.value = true - } - } - - const handleMachineFilesAdded = async (files: File[]) => { - if (!files.length || !machine.value?.id) return - machineDocumentsUploading.value = true - try { - const result: any = await uploadDocuments( - { files, context: { machineId: machine.value.id } } as any, - { updateStore: false }, - ) - if (result.success && machine.value) { - const newDocs = (result.data as AnyRecord[]) || [] - machine.value.documents = [ - ...newDocs, - ...((machine.value.documents as AnyRecord[]) || []), - ] - machineDocumentFiles.value = [] - } - } finally { - machineDocumentsUploading.value = false - } - } - - const removeMachineDocument = async (documentId: string) => { - if (!documentId) return - const result: any = await deleteDocument(documentId, { updateStore: false }) - if (result.success && machine.value) { - machine.value.documents = ((machine.value.documents as AnyRecord[]) || []).filter( - (doc) => doc.id !== documentId, - ) - } - } - - const openPreview = (doc: AnyRecord) => { - if (!canPreviewDocument(doc)) return - previewDocument.value = doc - previewVisible.value = true - } - - const closePreview = () => { - previewVisible.value = false - previewDocument.value = null - } - - // --------------------------------------------------------------------------- - // Update methods - // --------------------------------------------------------------------------- - - const updateMachineInfo = async () => { - if (!machine.value) return - try { - const cIds = uniqueConstructeurIds(machineConstructeurIds.value) - machineConstructeurIds.value = cIds - - const result: any = await updateMachineApi(machine.value.id as string, { - name: machineName.value, - reference: machineReference.value, - constructeurIds: cIds, - } as any) - if (result.success) { - const machinePayload = - result.data?.machine && typeof result.data.machine === 'object' - ? result.data.machine - : result.data - if (machinePayload && typeof machinePayload === 'object') { - machine.value = { - ...machine.value, - ...machinePayload, - documents: machinePayload.documents || machine.value.documents || [], - customFieldValues: machinePayload.customFieldValues || machine.value.customFieldValues || [], - } - machineConstructeurIds.value = uniqueConstructeurIds( - machine.value!.constructeurIds, - machine.value!.constructeurs, - machine.value!.constructeur, - ) - const linksApplied = applyMachineLinks(result.data) - if (linksApplied && machine.value) { - machine.value.componentLinks = machineComponentLinks.value - machine.value.pieceLinks = machinePieceLinks.value - } - loadProductDocuments().catch(() => {}) - } - } - } catch (error) { - console.error('Erreur lors de la mise à jour de la machine:', error) - } - } - - const updateComponent = async (updatedComponent: AnyRecord) => { - try { - const cIds = uniqueConstructeurIds( - updatedComponent.constructeurIds, - updatedComponent.constructeurId, - updatedComponent.constructeur, - ) - const productId = updatedComponent.productId - ? String(updatedComponent.productId) - : null - const prix = - updatedComponent.prix !== null && - updatedComponent.prix !== undefined && - String(updatedComponent.prix).trim() !== '' - ? Number(updatedComponent.prix) - : null - - const result: any = await updateComposantApi(updatedComponent.id as string, { - name: updatedComponent.name, - reference: updatedComponent.reference, - constructeurIds: cIds, - prix: Number.isNaN(prix) ? null : prix, - productId, - } as any) - if (result.success) { - const transformed = transformComponentCustomFields([result.data])[0] - Object.assign(updatedComponent, transformed) - } - } catch (error) { - console.error('Erreur lors de la mise à jour du composant:', error) - } - } - - const updatePieceFromComponent = async (updatedPiece: AnyRecord) => { - try { - const cIds = uniqueConstructeurIds( - updatedPiece.constructeurIds, - updatedPiece.constructeurId, - updatedPiece.constructeur, - ) - const productId = updatedPiece.productId ? String(updatedPiece.productId) : null - const prix = - updatedPiece.prix !== null && - updatedPiece.prix !== undefined && - String(updatedPiece.prix).trim() !== '' - ? Number(updatedPiece.prix) - : null - - const result: any = await updatePieceApi(updatedPiece.id as string, { - name: updatedPiece.name, - reference: updatedPiece.reference, - constructeurIds: cIds, - prix: Number.isNaN(prix) ? null : prix, - productId, - } as any) - if (result.success) { - const transformed = transformCustomFields([result.data])[0] - Object.assign(updatedPiece, transformed) - if (updatedPiece.customFields) { - for (const field of updatedPiece.customFields as AnyRecord[]) { - if (field.value !== undefined) { - await upsertCustomFieldValue( - field.id as string, - 'piece', - updatedPiece.id as string, - field.value, - ) - } - } - } - } - } catch (error) { - console.error('Erreur lors de la mise à jour de la pièce:', error) - } - } - - const updatePieceInfo = async (updatedPiece: AnyRecord) => { - try { - const cIds = uniqueConstructeurIds( - updatedPiece.constructeurIds, - updatedPiece.constructeurId, - updatedPiece.constructeur, - ) - const productId = updatedPiece.productId ? String(updatedPiece.productId) : null - const prix = - updatedPiece.prix !== null && - updatedPiece.prix !== undefined && - String(updatedPiece.prix).trim() !== '' - ? Number(updatedPiece.prix) - : null - - const result: any = await updatePieceApi(updatedPiece.id as string, { - name: updatedPiece.name, - reference: updatedPiece.reference, - constructeurIds: cIds, - prix: Number.isNaN(prix) ? null : prix, - productId, - } as any) - if (result.success) { - const transformed = transformCustomFields([result.data])[0] - Object.assign(updatedPiece, transformed) - } - } catch (error) { - console.error('Erreur lors de la mise à jour de la pièce:', error) - } - } - - const handleMachineConstructeurChange = async (value: unknown) => { - machineConstructeurIds.value = uniqueConstructeurIds(value) - await updateMachineInfo() - } - - const editComponent = () => { - toast.showInfo('La modification des composants sera bientôt disponible') - } - - const editPiece = () => { - toast.showInfo('La modification des pièces sera bientôt disponible') - } - - // --------------------------------------------------------------------------- // UI methods - // --------------------------------------------------------------------------- - const toggleEditMode = () => { isEditMode.value = !isEditMode.value debug.value = !debug.value @@ -1019,10 +296,7 @@ export function useMachineDetailData(machineId: string) { pieceCollapseToggleToken.value += 1 } - // --------------------------------------------------------------------------- // Print wrappers - // --------------------------------------------------------------------------- - const ensurePrintSelectionEntries = () => _ensurePrintEntries(components.value, machinePieces.value) const setAllPrintSelection = (value: boolean) => @@ -1038,125 +312,7 @@ export function useMachineDetailData(machineId: string) { components.value as any, ) - // --------------------------------------------------------------------------- - // Structure link management - // --------------------------------------------------------------------------- - - const reloadMachineStructure = async () => { - const result: any = await get(`/machines/${machineId}/structure`) - if (result.success) { - const machinePayload = - result.data?.machine && typeof result.data.machine === 'object' - ? result.data.machine - : result.data - if (machinePayload && typeof machinePayload === 'object') { - machine.value = { - ...machine.value, - ...machinePayload, - documents: machinePayload.documents || (machine.value as AnyRecord)?.documents || [], - customFieldValues: machinePayload.customFieldValues || (machine.value as AnyRecord)?.customFieldValues || [], - } - const linksApplied = applyMachineLinks(result.data) - if (linksApplied && machine.value) { - machine.value.componentLinks = machineComponentLinks.value - machine.value.pieceLinks = machinePieceLinks.value - machine.value.productLinks = machineProductLinks.value - } - syncMachineCustomFields() - } - } - } - - const addComponentLink = async (composantId: string) => { - const result: any = await apiPost('/machine_component_links', { - machine: `/api/machines/${machineId}`, - composant: `/api/composants/${composantId}`, - }) - if (result.success) { - toast.showSuccess('Composant ajouté à la machine') - await reloadMachineStructure() - } else { - toast.showError('Erreur lors de l\'ajout du composant') - } - return result - } - - const removeComponentLink = async (linkId: string) => { - const result: any = await apiDel(`/machine_component_links/${linkId}`) - if (result.success) { - toast.showSuccess('Composant retiré de la machine') - await reloadMachineStructure() - } else { - toast.showError('Erreur lors de la suppression du composant') - } - return result - } - - const addPieceLink = async (pieceId: string, parentComponentLinkId?: string) => { - const payload: any = { - machine: `/api/machines/${machineId}`, - piece: `/api/pieces/${pieceId}`, - } - if (parentComponentLinkId) { - payload.parentLink = `/api/machine_component_links/${parentComponentLinkId}` - } - const result: any = await apiPost('/machine_piece_links', payload) - if (result.success) { - toast.showSuccess('Pièce ajoutée à la machine') - await reloadMachineStructure() - } else { - toast.showError('Erreur lors de l\'ajout de la pièce') - } - return result - } - - const removePieceLink = async (linkId: string) => { - const result: any = await apiDel(`/machine_piece_links/${linkId}`) - if (result.success) { - toast.showSuccess('Pièce retirée de la machine') - await reloadMachineStructure() - } else { - toast.showError('Erreur lors de la suppression de la pièce') - } - return result - } - - const addProductLink = async (productId: string, parentComponentLinkId?: string, parentPieceLinkId?: string) => { - const payload: any = { - machine: `/api/machines/${machineId}`, - product: `/api/products/${productId}`, - } - if (parentComponentLinkId) { - payload.parentComponentLink = `/api/machine_component_links/${parentComponentLinkId}` - } - if (parentPieceLinkId) { - payload.parentPieceLink = `/api/machine_piece_links/${parentPieceLinkId}` - } - const result: any = await apiPost('/machine_product_links', payload) - if (result.success) { - toast.showSuccess('Produit ajouté à la machine') - await reloadMachineStructure() - } else { - toast.showError('Erreur lors de l\'ajout du produit') - } - return result - } - - const removeProductLink = async (linkId: string) => { - const result: any = await apiDel(`/machine_product_links/${linkId}`) - if (result.success) { - toast.showSuccess('Produit retiré de la machine') - await reloadMachineStructure() - } else { - toast.showError('Erreur lors de la suppression du produit') - } - return result - } - - // --------------------------------------------------------------------------- // Data loading - // --------------------------------------------------------------------------- - const loadMachineData = async () => { loading.value = true try { @@ -1198,14 +354,14 @@ export function useMachineDetailData(machineId: string) { ? refreshMachineDocuments() : Promise.resolve() - if (!(productInventory.value as AnyRecord[]).length) { - try { - await loadProducts() - } catch (error) { - console.error('Erreur lors du chargement des produits:', error) - } - } + // 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) { @@ -1248,10 +404,7 @@ export function useMachineDetailData(machineId: string) { ]) } - // --------------------------------------------------------------------------- // Watchers - // --------------------------------------------------------------------------- - watch(() => (machine.value as AnyRecord)?.customFieldValues, () => syncMachineCustomFields(), { deep: true }) watch(() => (machine.value as AnyRecord)?.customFields, () => syncMachineCustomFields(), { deep: true }) watch( @@ -1260,151 +413,48 @@ export function useMachineDetailData(machineId: string) { { immediate: true }, ) - // --------------------------------------------------------------------------- - // Public API - // --------------------------------------------------------------------------- - return { // State - loading, - machine, - components, - pieces, - machineComponentLinks, - machinePieceLinks, - machineProductLinks, - printAreaRef, + loading, machine, components, pieces, printAreaRef, + machineComponentLinks, machinePieceLinks, machineProductLinks, // Machine fields - machineName, - machineReference, - machineConstructeurIds, - machineConstructeurId, - machineConstructeursDisplay, - machineConstructeurContact, - hasMachineConstructeur, + machineName, machineReference, machineConstructeurIds, machineConstructeurId, + machineConstructeursDisplay, machineConstructeurContact, hasMachineConstructeur, // UI state - machineDocumentFiles, - machineDocumentsUploading, - machineDocumentsLoaded, - machineCustomFields, - previewDocument, - previewVisible, - isEditMode, - debug, - componentsCollapsed, - collapseToggleToken, - piecesCollapsed, - pieceCollapseToggleToken, + machineDocumentFiles, machineDocumentsUploading, machineDocumentsLoaded, + machineCustomFields, previewDocument, previewVisible, + isEditMode, debug, + componentsCollapsed, collapseToggleToken, piecesCollapsed, pieceCollapseToggleToken, // Computed - componentTypeOptions, - pieceTypeOptions, - componentTypeLabelMap, - pieceTypeLabelMap, - productInventory, - productById, - flattenedComponents, - machinePieces, - machineDirectProducts, - machineDocumentsList, - visibleMachineCustomFields, + componentTypeOptions, pieceTypeOptions, componentTypeLabelMap, pieceTypeLabelMap, + productInventory, productById, flattenedComponents, machinePieces, + machineDirectProducts, machineDocumentsList, visibleMachineCustomFields, - // Product helpers - findProductById, - resolveProductReference, - getProductDisplay, - - // Helpers - isPlainObject, - flattenComponents, - findComponentById, - findPieceById, - collectConstructeurs, - - // Transform - transformCustomFields, - transformComponentCustomFields, - - // Hierarchy - applyMachineLinks, - - // Machine fields methods - initMachineFields, - getMachineFieldId, - - // Custom fields - syncMachineCustomFields, - setMachineCustomFieldValue, - updateMachineCustomField, - updatePieceCustomField, - - // Documents - refreshMachineDocuments, - handleMachineFilesAdded, - removeMachineDocument, - openPreview, - closePreview, - - // Updates - updateMachineInfo, - updateComponent, - updatePieceFromComponent, - updatePieceInfo, - handleMachineConstructeurChange, - editComponent, - editPiece, - - // UI methods - toggleEditMode, - toggleAllComponents, - collapseAllComponents, - toggleAllPieces, + // 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, // Print - printModalOpen, - printSelection, - ensurePrintSelectionEntries, - setAllPrintSelection, - openPrintModal, - closePrintModal, - handlePrintConfirm, + printModalOpen, printSelection, ensurePrintSelectionEntries, + setAllPrintSelection, openPrintModal, closePrintModal, handlePrintConfirm, - // Loading - loadMachineData, - loadInitialData, - - // Structure link management - addComponentLink, - removeComponentLink, - addPieceLink, - removePieceLink, - addProductLink, - removeProductLink, - reloadMachineStructure, + // Loading & structure + loadMachineData, loadInitialData, + addComponentLink, removeComponentLink, addPieceLink, removePieceLink, + addProductLink, removeProductLink, reloadMachineStructure, // External - constructeurs, - loadProducts, - updateMachineStructure, - toast, - - // Re-exports for template - formatCustomFieldValue, - summarizeCustomFields, - formatConstructeurContactSummary, - formatSize, - shouldInlinePdf, - documentPreviewSrc, - documentThumbnailClass, - documentIcon, + constructeurs, loadProducts, updateMachineStructure, toast, downloadDocument: downloadDocumentHelper, - canPreviewDocument, - isImageDocument, - isPdfDocument, - getFileIcon, - resolveIdentifier, - extractParentLinkIdentifiers, } } diff --git a/app/composables/useMachineDetailDocuments.ts b/app/composables/useMachineDetailDocuments.ts new file mode 100644 index 0000000..96fc29f --- /dev/null +++ b/app/composables/useMachineDetailDocuments.ts @@ -0,0 +1,146 @@ +/** + * Machine detail — document management sub-composable. + * + * Handles document loading, upload, delete and preview state. + */ + +import { ref, computed } from 'vue' +import { useDocuments } from '~/composables/useDocuments' +import { canPreviewDocument } from '~/utils/documentPreview' + +type AnyRecord = Record + +interface MachineDetailDocumentsDeps { + machine: Ref +} + +export function useMachineDetailDocuments(deps: MachineDetailDocumentsDeps) { + const { machine } = deps + const { + uploadDocuments, + deleteDocument, + loadDocumentsByMachine, + loadDocumentsByProduct, + } = useDocuments() + + // --------------------------------------------------------------------------- + // State + // --------------------------------------------------------------------------- + + const machineDocumentFiles = ref([]) + const machineDocumentsUploading = ref(false) + const machineDocumentsLoaded = ref(false) + const previewDocument = ref(null) + const previewVisible = ref(false) + + // --------------------------------------------------------------------------- + // Computed + // --------------------------------------------------------------------------- + + const machineDocumentsList = computed( + () => ((machine.value as AnyRecord)?.documents as AnyRecord[]) || [], + ) + + // --------------------------------------------------------------------------- + // Methods + // --------------------------------------------------------------------------- + + const refreshMachineDocuments = async () => { + if (!machine.value?.id) return + const result: any = await loadDocumentsByMachine(machine.value.id as string, { updateStore: false }) + if (result.success && machine.value) { + machine.value.documents = result.data || [] + machineDocumentsLoaded.value = true + } + } + + const handleMachineFilesAdded = async (files: File[]) => { + if (!files.length || !machine.value?.id) return + machineDocumentsUploading.value = true + try { + const result: any = await uploadDocuments( + { files, context: { machineId: machine.value.id } } as any, + { updateStore: false }, + ) + if (result.success && machine.value) { + const newDocs = (result.data as AnyRecord[]) || [] + machine.value.documents = [ + ...newDocs, + ...((machine.value.documents as AnyRecord[]) || []), + ] + machineDocumentFiles.value = [] + } + } finally { + machineDocumentsUploading.value = false + } + } + + const removeMachineDocument = async (documentId: string) => { + if (!documentId) return + const result: any = await deleteDocument(documentId, { updateStore: false }) + if (result.success && machine.value) { + machine.value.documents = ((machine.value.documents as AnyRecord[]) || []).filter( + (doc) => doc.id !== documentId, + ) + } + } + + const openPreview = (doc: AnyRecord) => { + if (!canPreviewDocument(doc)) return + previewDocument.value = doc + previewVisible.value = true + } + + const closePreview = () => { + previewVisible.value = false + previewDocument.value = null + } + + const loadProductDocuments = async (machineProductLinks: AnyRecord[]) => { + const productIds = machineProductLinks + .map((link) => { + const p = link.product as AnyRecord | string | null + if (typeof p === 'string') return p.split('/').pop() || null + return (p as AnyRecord)?.id as string | null + }) + .filter((id): id is string => !!id) + + const results = await Promise.allSettled( + productIds.map(async (id) => { + const result: any = await loadDocumentsByProduct(id, { updateStore: false }) + if (result.success && Array.isArray(result.data)) { + return { id, docs: result.data as AnyRecord[] } + } + return { id, docs: [] } + }), + ) + + const map = new Map() + results.forEach((r) => { + if (r.status === 'fulfilled' && r.value.docs.length) { + map.set(r.value.id, r.value.docs) + } + }) + return map + } + + return { + // State + machineDocumentFiles, + machineDocumentsUploading, + machineDocumentsLoaded, + previewDocument, + previewVisible, + + // Computed + machineDocumentsList, + + // Methods + refreshMachineDocuments, + handleMachineFilesAdded, + removeMachineDocument, + openPreview, + closePreview, + loadProductDocuments, + } +} diff --git a/app/composables/useMachineDetailHierarchy.ts b/app/composables/useMachineDetailHierarchy.ts new file mode 100644 index 0000000..17cd8bd --- /dev/null +++ b/app/composables/useMachineDetailHierarchy.ts @@ -0,0 +1,306 @@ +/** + * Machine detail — hierarchy & link management sub-composable. + * + * Handles machine hierarchy building, component/piece tree resolution, + * flatten helpers, find-by-id utilities, and structure link CRUD. + */ + +import { ref, computed } from 'vue' +import { useApi } from '~/composables/useApi' +import { useToast } from '~/composables/useToast' +import { + resolveIdentifier, +} from '~/shared/utils/productDisplayUtils' +import { + buildMachineHierarchyFromLinks, + resolveLinkArray, +} from '~/composables/useMachineHierarchy' + +type AnyRecord = Record + +interface MachineDetailHierarchyDeps { + machineId: string + machine: Ref + constructeurs: Ref + findProductById: (id: string | null | undefined) => AnyRecord | null + transformComponentCustomFields: (data: AnyRecord[]) => AnyRecord[] + transformCustomFields: (data: AnyRecord[]) => AnyRecord[] + syncMachineCustomFields: () => void +} + +export function useMachineDetailHierarchy(deps: MachineDetailHierarchyDeps) { + const { + machineId, + machine, + constructeurs, + findProductById, + transformComponentCustomFields, + transformCustomFields, + syncMachineCustomFields, + } = deps + + const { get, post: apiPost, delete: apiDel } = useApi() + const toast = useToast() + + // --------------------------------------------------------------------------- + // State + // --------------------------------------------------------------------------- + + const components = ref([]) + const pieces = ref([]) + const machineComponentLinks = ref([]) + const machinePieceLinks = ref([]) + const machineProductLinks = ref([]) + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + const flattenComponents = (list: AnyRecord[] = []): AnyRecord[] => { + const result: AnyRecord[] = [] + const traverse = (items: AnyRecord[]) => { + items.forEach((item) => { + result.push(item) + if (Array.isArray(item.subComponents) && item.subComponents.length) { + traverse(item.subComponents as AnyRecord[]) + } + }) + } + traverse(list) + return result + } + + const findComponentById = (items: AnyRecord[] | undefined, id: string): AnyRecord | null => { + for (const item of items || []) { + if (item.id === id) return item + const found = findComponentById(item.subComponents as AnyRecord[] | undefined, id) + if (found) return found + } + return null + } + + const findPieceById = (pieceId: string): AnyRecord | null => { + const direct = pieces.value.find((p) => p.id === pieceId) + if (direct) return direct + + const searchInComponents = (items: AnyRecord[]): AnyRecord | null => { + for (const item of items || []) { + const match = ((item.pieces as AnyRecord[]) || []).find((p) => p.id === pieceId) + if (match) return match + const nested = searchInComponents((item.subComponents as AnyRecord[]) || []) + if (nested) return nested + } + return null + } + return searchInComponents(components.value) + } + + // --------------------------------------------------------------------------- + // Hierarchy & links + // --------------------------------------------------------------------------- + + const applyMachineLinks = (source: AnyRecord): boolean => { + const container = (source?.machine as AnyRecord) ?? null + const componentLinksData = + resolveLinkArray(source, ['componentLinks', 'machineComponentLinks']) ?? + resolveLinkArray(container, ['componentLinks', 'machineComponentLinks']) + const pieceLinksData = + resolveLinkArray(source, ['pieceLinks', 'machinePieceLinks']) ?? + resolveLinkArray(container, ['pieceLinks', 'machinePieceLinks']) + const productLinksData = + resolveLinkArray(source, ['productLinks', 'machineProductLinks']) ?? + resolveLinkArray(container, ['productLinks', 'machineProductLinks']) + + if (componentLinksData === null && pieceLinksData === null && productLinksData === null) { + return false + } + + const normalizedComponentLinks = (componentLinksData ?? []) as AnyRecord[] + const normalizedPieceLinks = (pieceLinksData ?? []) as AnyRecord[] + const normalizedProductLinks = (productLinksData ?? []) as AnyRecord[] + + machineComponentLinks.value = normalizedComponentLinks + machinePieceLinks.value = normalizedPieceLinks + machineProductLinks.value = normalizedProductLinks + + const { components: hierarchy, machinePieces: machineLevelPieces } = + buildMachineHierarchyFromLinks( + normalizedComponentLinks, + normalizedPieceLinks, + findProductById as any, + constructeurs.value as any, + ) + + components.value = transformComponentCustomFields(hierarchy as AnyRecord[]) + pieces.value = transformCustomFields(machineLevelPieces as AnyRecord[]) + + return true + } + + // --------------------------------------------------------------------------- + // Computed + // --------------------------------------------------------------------------- + + const flattenedComponents = computed(() => flattenComponents(components.value)) + + const machinePieces = computed(() => { + return pieces.value.filter((piece) => { + const parentLinkId = resolveIdentifier( + piece.parentComponentLinkId, + (piece.machinePieceLink as AnyRecord)?.parentComponentLinkId, + piece.parentLinkId, + ) + if (parentLinkId) return false + return !piece.composantId + }) + }) + + // --------------------------------------------------------------------------- + // Structure reload + // --------------------------------------------------------------------------- + + const reloadMachineStructure = async () => { + const result: any = await get(`/machines/${machineId}/structure`) + if (result.success) { + const machinePayload = + result.data?.machine && typeof result.data.machine === 'object' + ? result.data.machine + : result.data + if (machinePayload && typeof machinePayload === 'object') { + machine.value = { + ...machine.value, + ...machinePayload, + documents: machinePayload.documents || (machine.value as AnyRecord)?.documents || [], + customFieldValues: machinePayload.customFieldValues || (machine.value as AnyRecord)?.customFieldValues || [], + } + const linksApplied = applyMachineLinks(result.data) + if (linksApplied && machine.value) { + machine.value.componentLinks = machineComponentLinks.value + machine.value.pieceLinks = machinePieceLinks.value + machine.value.productLinks = machineProductLinks.value + } + syncMachineCustomFields() + } + } + } + + // --------------------------------------------------------------------------- + // Structure link CRUD + // --------------------------------------------------------------------------- + + const addComponentLink = async (composantId: string) => { + const result: any = await apiPost('/machine_component_links', { + machine: `/api/machines/${machineId}`, + composant: `/api/composants/${composantId}`, + }) + if (result.success) { + toast.showSuccess('Composant ajouté à la machine') + await reloadMachineStructure() + } else { + toast.showError('Erreur lors de l\'ajout du composant') + } + return result + } + + const removeComponentLink = async (linkId: string) => { + const result: any = await apiDel(`/machine_component_links/${linkId}`) + if (result.success) { + toast.showSuccess('Composant retiré de la machine') + await reloadMachineStructure() + } else { + toast.showError('Erreur lors de la suppression du composant') + } + return result + } + + const addPieceLink = async (pieceId: string, parentComponentLinkId?: string) => { + const payload: any = { + machine: `/api/machines/${machineId}`, + piece: `/api/pieces/${pieceId}`, + } + if (parentComponentLinkId) { + payload.parentLink = `/api/machine_component_links/${parentComponentLinkId}` + } + const result: any = await apiPost('/machine_piece_links', payload) + if (result.success) { + toast.showSuccess('Pièce ajoutée à la machine') + await reloadMachineStructure() + } else { + toast.showError('Erreur lors de l\'ajout de la pièce') + } + return result + } + + const removePieceLink = async (linkId: string) => { + const result: any = await apiDel(`/machine_piece_links/${linkId}`) + if (result.success) { + toast.showSuccess('Pièce retirée de la machine') + await reloadMachineStructure() + } else { + toast.showError('Erreur lors de la suppression de la pièce') + } + return result + } + + const addProductLink = async (productId: string, parentComponentLinkId?: string, parentPieceLinkId?: string) => { + const payload: any = { + machine: `/api/machines/${machineId}`, + product: `/api/products/${productId}`, + } + if (parentComponentLinkId) { + payload.parentComponentLink = `/api/machine_component_links/${parentComponentLinkId}` + } + if (parentPieceLinkId) { + payload.parentPieceLink = `/api/machine_piece_links/${parentPieceLinkId}` + } + const result: any = await apiPost('/machine_product_links', payload) + if (result.success) { + toast.showSuccess('Produit ajouté à la machine') + await reloadMachineStructure() + } else { + toast.showError('Erreur lors de l\'ajout du produit') + } + return result + } + + const removeProductLink = async (linkId: string) => { + const result: any = await apiDel(`/machine_product_links/${linkId}`) + if (result.success) { + toast.showSuccess('Produit retiré de la machine') + await reloadMachineStructure() + } else { + toast.showError('Erreur lors de la suppression du produit') + } + return result + } + + return { + // State + components, + pieces, + machineComponentLinks, + machinePieceLinks, + machineProductLinks, + + // Computed + flattenedComponents, + machinePieces, + + // Helpers + flattenComponents, + findComponentById, + findPieceById, + + // Hierarchy + applyMachineLinks, + + // Structure link management + reloadMachineStructure, + addComponentLink, + removeComponentLink, + addPieceLink, + removePieceLink, + addProductLink, + removeProductLink, + } +} diff --git a/app/composables/useMachineDetailProducts.ts b/app/composables/useMachineDetailProducts.ts new file mode 100644 index 0000000..41a9b40 --- /dev/null +++ b/app/composables/useMachineDetailProducts.ts @@ -0,0 +1,132 @@ +/** + * Machine detail — product display sub-composable. + * + * Handles product resolution, display helpers, supplier info, + * and machine-level direct product links. + */ + +import { computed } from 'vue' +import { useProducts } from '~/composables/useProducts' +import { + resolveProductReference as _resolveProductReference, + getProductDisplay as _getProductDisplay, + getProductSuppliersLabel, + getProductPriceLabel, +} from '~/shared/utils/productDisplayUtils' +import { + resolveConstructeurs, + uniqueConstructeurIds, +} from '~/shared/constructeurUtils' + +type AnyRecord = Record + +interface MachineDetailProductsDeps { + machineProductLinks: Ref + productDocumentsMap: Ref> + constructeurs: Ref +} + +export function useMachineDetailProducts(deps: MachineDetailProductsDeps) { + const { machineProductLinks, productDocumentsMap, constructeurs } = deps + const { products, loadProducts } = useProducts() + + // --------------------------------------------------------------------------- + // Computed + // --------------------------------------------------------------------------- + + const productInventory = computed(() => products.value || []) + + const productById = computed(() => { + const map = new Map() + ;(productInventory.value as AnyRecord[]).forEach((product: AnyRecord) => { + if (product?.id) map.set(product.id as string, product) + }) + return map + }) + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + const findProductById = (productId: string | null | undefined): AnyRecord | null => { + if (!productId) return null + return productById.value.get(productId) || null + } + + const resolveProductReference = (source: AnyRecord) => + _resolveProductReference(source, findProductById as any) + const getProductDisplay = (source: AnyRecord) => + _getProductDisplay(source, findProductById as any) + + // --------------------------------------------------------------------------- + // Machine direct products + // --------------------------------------------------------------------------- + + const machineDirectProducts = computed(() => { + return machineProductLinks.value.map((link) => { + const productObj = link.product as AnyRecord | string | null + let resolved: AnyRecord | null = null + let productId: string | null = null + + if (typeof productObj === 'string') { + productId = productObj.split('/').pop() || null + resolved = productId ? findProductById(productId) : null + } else if (productObj && typeof productObj === 'object') { + productId = (productObj as AnyRecord)?.id as string | null + // Prefer the embedded product from the structure endpoint — it has richer + // data (typeProduct as object, supplierPrice, constructeurs) than the + // global products cache which may store typeProduct as an IRI string. + const cached = productId ? findProductById(productId) : null + resolved = productObj as AnyRecord + if (cached) { + // Merge: use embedded as base, overlay any non-null cached fields + resolved = { ...resolved, ...Object.fromEntries( + Object.entries(cached as AnyRecord).filter(([, v]) => v != null && v !== ''), + ) } + // But always prefer the embedded typeProduct when it's an object + if (productObj.typeProduct && typeof productObj.typeProduct === 'object') { + resolved.typeProduct = productObj.typeProduct + } + } + } + + const cIds = uniqueConstructeurIds( + resolved?.constructeurs, + resolved?.constructeurIds, + ) + const resolvedConstructeurs = resolveConstructeurs( + cIds, + resolved?.constructeurs as any[] || [], + constructeurs.value as any, + ) + + return { + id: (resolved?.id as string) || productId || null, + linkId: (link.id as string) || (typeof link['@id'] === 'string' ? link['@id'].split('/').pop() : null) || null, + name: (resolved?.name as string) || 'Produit inconnu', + reference: (resolved?.reference as string) || null, + supplierLabel: resolvedConstructeurs.length + ? resolvedConstructeurs.map((c) => c.name).filter(Boolean).join(', ') || null + : getProductSuppliersLabel(resolved), + priceLabel: resolved ? getProductPriceLabel(resolved) : null, + groupLabel: ((resolved?.typeProduct as AnyRecord)?.name as string) || '', + documents: productId ? (productDocumentsMap.value.get(productId) || []) : [], + } + }) + }) + + return { + // Computed + productInventory, + productById, + machineDirectProducts, + + // Helpers + findProductById, + resolveProductReference, + getProductDisplay, + + // Loading + loadProducts, + } +} diff --git a/app/composables/useMachineDetailUpdates.ts b/app/composables/useMachineDetailUpdates.ts new file mode 100644 index 0000000..1c8a362 --- /dev/null +++ b/app/composables/useMachineDetailUpdates.ts @@ -0,0 +1,214 @@ +/** + * Machine detail page — update/mutation methods. + * + * Extracted from useMachineDetailData.ts to keep the orchestrator under 500 lines. + */ + +import type { Ref } from 'vue' +import { uniqueConstructeurIds } from '~/shared/constructeurUtils' + +type AnyRecord = Record + +export interface UseMachineDetailUpdatesDeps { + machine: Ref + machineName: Ref + machineReference: Ref + machineConstructeurIds: Ref + machineDocumentsLoaded: Ref + machineComponentLinks: Ref + machinePieceLinks: Ref + machineProductLinks: Ref + applyMachineLinks: (data: AnyRecord) => boolean + refreshMachineDocuments: () => Promise + transformComponentCustomFields: (items: AnyRecord[]) => AnyRecord[] + transformCustomFields: (items: AnyRecord[]) => AnyRecord[] + loadProductDocuments: () => Promise + upsertCustomFieldValue: ( + fieldId: string, + entityType: string, + entityId: string, + value: unknown, + ) => Promise + updateMachineApi: (id: string, data: any) => Promise + updateComposantApi: (id: string, data: any) => Promise + updatePieceApi: (id: string, data: any) => Promise + toast: { showInfo: (msg: string) => void } +} + +export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) { + const { + machine, + machineName, + machineReference, + machineConstructeurIds, + machineComponentLinks, + machinePieceLinks, + applyMachineLinks, + loadProductDocuments, + transformComponentCustomFields, + transformCustomFields, + upsertCustomFieldValue, + updateMachineApi, + updateComposantApi, + updatePieceApi, + toast, + } = deps + + const updateMachineInfo = async () => { + if (!machine.value) return + try { + const cIds = uniqueConstructeurIds(machineConstructeurIds.value) + machineConstructeurIds.value = cIds + + const result: any = await updateMachineApi(machine.value.id as string, { + name: machineName.value, + reference: machineReference.value, + constructeurIds: cIds, + } as any) + if (result.success) { + const machinePayload = + result.data?.machine && typeof result.data.machine === 'object' + ? result.data.machine + : result.data + if (machinePayload && typeof machinePayload === 'object') { + machine.value = { + ...machine.value, + ...machinePayload, + documents: machinePayload.documents || machine.value.documents || [], + customFieldValues: machinePayload.customFieldValues || machine.value.customFieldValues || [], + } + machineConstructeurIds.value = uniqueConstructeurIds( + machine.value!.constructeurIds, + machine.value!.constructeurs, + machine.value!.constructeur, + ) + const linksApplied = applyMachineLinks(result.data) + if (linksApplied && machine.value) { + machine.value.componentLinks = machineComponentLinks.value + machine.value.pieceLinks = machinePieceLinks.value + } + loadProductDocuments().catch(() => {}) + } + } + } catch (error) { + console.error('Erreur lors de la mise à jour de la machine:', error) + } + } + + const updateComponent = async (updatedComponent: AnyRecord) => { + try { + const cIds = uniqueConstructeurIds( + updatedComponent.constructeurIds, + updatedComponent.constructeurId, + updatedComponent.constructeur, + ) + const productId = updatedComponent.productId + ? String(updatedComponent.productId) + : null + const prix = + updatedComponent.prix !== null && + updatedComponent.prix !== undefined && + String(updatedComponent.prix).trim() !== '' + ? Number(updatedComponent.prix) + : null + + const result: any = await updateComposantApi(updatedComponent.id as string, { + name: updatedComponent.name, + reference: updatedComponent.reference, + constructeurIds: cIds, + prix: Number.isNaN(prix) ? null : prix, + productId, + } as any) + if (result.success) { + const transformed = transformComponentCustomFields([result.data])[0] + Object.assign(updatedComponent, transformed) + } + } catch (error) { + console.error('Erreur lors de la mise à jour du composant:', error) + } + } + + const _buildAndUpdatePiece = async (updatedPiece: AnyRecord) => { + const cIds = uniqueConstructeurIds( + updatedPiece.constructeurIds, + updatedPiece.constructeurId, + updatedPiece.constructeur, + ) + const productId = updatedPiece.productId ? String(updatedPiece.productId) : null + const prix = + updatedPiece.prix !== null && + updatedPiece.prix !== undefined && + String(updatedPiece.prix).trim() !== '' + ? Number(updatedPiece.prix) + : null + + const result: any = await updatePieceApi(updatedPiece.id as string, { + name: updatedPiece.name, + reference: updatedPiece.reference, + constructeurIds: cIds, + prix: Number.isNaN(prix) ? null : prix, + productId, + } as any) + if (result.success) { + const transformed = transformCustomFields([result.data])[0] + Object.assign(updatedPiece, transformed) + } + return result + } + + const updatePieceFromComponent = async (updatedPiece: AnyRecord) => { + try { + const result = await _buildAndUpdatePiece(updatedPiece) + if (result?.success && updatedPiece.customFields) { + const fieldsToSave = (updatedPiece.customFields as AnyRecord[]).filter( + (field) => field.value !== undefined, + ) + if (fieldsToSave.length) { + await Promise.allSettled( + fieldsToSave.map((field) => + upsertCustomFieldValue( + field.id as string, + 'piece', + updatedPiece.id as string, + field.value, + ), + ), + ) + } + } + } catch (error) { + console.error('Erreur lors de la mise à jour de la pièce:', error) + } + } + + const updatePieceInfo = async (updatedPiece: AnyRecord) => { + try { + await _buildAndUpdatePiece(updatedPiece) + } catch (error) { + console.error('Erreur lors de la mise à jour de la pièce:', error) + } + } + + const handleMachineConstructeurChange = async (value: unknown) => { + machineConstructeurIds.value = uniqueConstructeurIds(value) + await updateMachineInfo() + } + + const editComponent = () => { + toast.showInfo('La modification des composants sera bientôt disponible') + } + + const editPiece = () => { + toast.showInfo('La modification des pièces sera bientôt disponible') + } + + return { + updateMachineInfo, + updateComponent, + updatePieceFromComponent, + updatePieceInfo, + handleMachineConstructeurChange, + editComponent, + editPiece, + } +}