import { computed, watch } from 'vue' import type { ComponentModelPiece, ComponentModelProduct, ComponentModelStructureNode } from '~/shared/types/inventory' import { useStructureNodeCrud } from '~/composables/useStructureNodeCrud' export type ModelTypeOption = { id: string name: string code?: string | null } export type EditableStructureNode = ComponentModelStructureNode & { customFields?: any[] pieces?: ComponentModelPiece[] products?: ComponentModelProduct[] } export interface StructureNodeLogicDeps { node: EditableStructureNode depth: number componentTypes: ModelTypeOption[] pieceTypes: ModelTypeOption[] productTypes: ModelTypeOption[] isRoot: boolean lockType: boolean lockedTypeLabel: string allowSubcomponents: boolean maxSubcomponentDepth: number restrictedMode: boolean isLocked: boolean } export function useStructureNodeLogic(props: StructureNodeLogicDeps) { // --- Computed props --- const isLocked = computed(() => props.isLocked === true) const restrictedMode = computed(() => props.restrictedMode === true) const componentTypes = computed(() => props.componentTypes ?? []) const pieceTypes = computed(() => props.pieceTypes ?? []) const productTypes = computed(() => props.productTypes ?? []) const allowSubcomponents = computed(() => props.allowSubcomponents !== false) const maxSubcomponentDepth = computed(() => typeof props.maxSubcomponentDepth === 'number' ? props.maxSubcomponentDepth : Infinity, ) const currentDepth = computed(() => Math.max(0, props.depth ?? 0)) const canManageSubcomponents = computed( () => allowSubcomponents.value && currentDepth.value < maxSubcomponentDepth.value, ) const childAllowSubcomponents = computed( () => allowSubcomponents.value && currentDepth.value + 1 < maxSubcomponentDepth.value, ) const hasSubcomponents = computed( () => Array.isArray(props.node?.subcomponents) && props.node.subcomponents.length > 0, ) const depthClasses = ['', 'ml-4', 'ml-8', 'ml-12', 'ml-16', 'ml-20'] const containerClass = computed(() => { const level = currentDepth.value const index = Math.min(level, depthClasses.length - 1) return level === 0 ? 'space-y-4' : `${depthClasses[index]} space-y-4` }) const headingClass = computed(() => (props.isRoot ? 'text-sm font-semibold' : 'text-xs font-semibold')) // --- Type maps --- const formatModelTypeOption = (type: ModelTypeOption | undefined | null) => type?.name ?? '' const componentTypeMap = computed(() => { const map = new Map() componentTypes.value.forEach((type) => { if (type && typeof type.id === 'string') { map.set(type.id, type) } }) return map }) const componentTypeCodeMap = computed(() => { const map = new Map() componentTypes.value.forEach((type) => { const code = typeof type?.code === 'string' ? type.code.trim() : '' if (code) { map.set(code, type) } }) return map }) const pieceTypeMap = computed(() => { const map = new Map() pieceTypes.value.forEach((type) => { if (type && typeof type.id === 'string') { map.set(type.id, type) } }) return map }) const productTypeMap = computed(() => { const map = new Map() productTypes.value.forEach((type) => { if (type && typeof type.id === 'string') { map.set(type.id, type) } }) return map }) // --- Label getters --- const getComponentTypeLabel = (id?: string) => { if (!id) return '' return formatModelTypeOption(componentTypeMap.value.get(id)) } const getPieceTypeLabel = (id?: string) => { if (!id) return '' return formatModelTypeOption(pieceTypeMap.value.get(id)) } const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type) const formatPieceTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type) const formatProductTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type) const lockedTypeDisplay = computed(() => { if (props.lockedTypeLabel) { return props.lockedTypeLabel } return getComponentTypeLabel(props.node?.typeComposantId) || 'Famille non définie' }) // --- Sync functions --- const syncComponentType = (component: EditableStructureNode) => { if (!component) { return } if (props.isRoot) { component.typeComposantId = '' component.typeComposantLabel = '' component.familyCode = '' if (component.alias) { component.alias = '' } return } const id = typeof component.typeComposantId === 'string' ? component.typeComposantId : '' if (!id) { const code = typeof component.familyCode === 'string' && component.familyCode ? component.familyCode : '' if (code) { const codeMatch = componentTypeCodeMap.value.get(code) if (codeMatch?.id) { component.typeComposantId = codeMatch.id component.typeComposantLabel = formatModelTypeOption(codeMatch) component.familyCode = codeMatch.code ?? component.familyCode if (!component.alias || component.alias === '' || component.alias === lockedTypeDisplay.value) { component.alias = codeMatch.name || component.typeComposantLabel } return } } component.typeComposantLabel = '' component.familyCode = '' return } const option = componentTypeMap.value.get(id) if (!option) { component.typeComposantLabel = '' component.familyCode = '' return } component.typeComposantLabel = formatModelTypeOption(option) component.familyCode = option.code ?? component.familyCode if (!component.alias || component.alias === '' || component.alias === lockedTypeDisplay.value) { component.alias = option.name || component.typeComposantLabel } } const updatePieceTypeLabel = (piece: ComponentModelPiece & Record) => { if (!piece) return if (piece.typePieceId) { const option = pieceTypeMap.value.get(piece.typePieceId) if (option) { piece.typePieceLabel = formatPieceTypeOption(option) return } } if (piece.typePieceLabel) { const normalized = piece.typePieceLabel.trim().toLowerCase() if (normalized) { const match = pieceTypes.value.find((type) => { const formatted = formatPieceTypeOption(type).toLowerCase() const name = (type?.name ?? '').toLowerCase() const code = (type?.code ?? '').toLowerCase() return formatted === normalized || name === normalized || (!!code && code === normalized) }) if (match) { piece.typePieceId = match.id piece.typePieceLabel = formatPieceTypeOption(match) return } } } } const updateProductTypeLabel = (product: ComponentModelProduct & Record) => { if (!product) return if (product.typeProductId) { const option = productTypeMap.value.get(product.typeProductId) if (option) { product.typeProductLabel = formatProductTypeOption(option) product.familyCode = option.code ?? product.familyCode ?? '' return } } if (product.typeProductLabel) { const normalized = product.typeProductLabel.trim().toLowerCase() if (normalized) { const match = productTypes.value.find((type) => { const formatted = formatProductTypeOption(type).toLowerCase() const name = (type?.name ?? '').toLowerCase() const code = (type?.code ?? '').toLowerCase() return formatted === normalized || name === normalized || (!!code && code === normalized) }) if (match) { product.typeProductId = match.id product.typeProductLabel = formatProductTypeOption(match) product.familyCode = match.code ?? product.familyCode ?? '' return } } } } const syncPieceLabels = (pieces?: any[]) => { if (!Array.isArray(pieces)) { return } pieces.forEach((piece) => { updatePieceTypeLabel(piece) }) } const syncProductLabels = (products?: any[]) => { if (!Array.isArray(products)) { return } products.forEach((product) => { updateProductTypeLabel(product) }) } // --- Handler functions --- const handleComponentTypeSelect = (component: any) => { syncComponentType(component) } const handlePieceTypeSelect = (piece: ComponentModelPiece & Record) => { if (!piece) { return } const id = typeof piece.typePieceId === 'string' ? piece.typePieceId : '' if (!id) { piece.typePieceLabel = '' return } const option = pieceTypeMap.value.get(id) if (!option) { piece.typePieceId = '' piece.typePieceLabel = '' return } piece.typePieceLabel = formatPieceTypeOption(option) } const handleProductTypeSelect = (product: ComponentModelProduct & Record) => { if (!product) { return } const id = typeof product.typeProductId === 'string' ? product.typeProductId : '' if (!id) { product.typeProductLabel = '' return } const option = productTypeMap.value.get(id) if (!option) { product.typeProductId = '' product.typeProductLabel = '' return } product.typeProductLabel = formatProductTypeOption(option) product.familyCode = option.code ?? product.familyCode ?? '' } // --- CRUD & Lock (delegated to useStructureNodeCrud) --- const crud = useStructureNodeCrud({ node: props.node, restrictedMode: props.restrictedMode, canManageSubcomponents: () => canManageSubcomponents.value, }) // --- Watchers --- watch( canManageSubcomponents, (allowed) => { if (!allowed && Array.isArray(props.node.subcomponents) && props.node.subcomponents.length) { props.node.subcomponents.splice(0, props.node.subcomponents.length) } }, { immediate: true }, ) watch(componentTypes, () => { syncComponentType(props.node) }, { deep: true, immediate: true }) watch( () => props.node.typeComposantId, () => { syncComponentType(props.node) }, ) watch(pieceTypes, () => { syncPieceLabels(props.node?.pieces) }, { deep: true, immediate: true }) watch( () => props.node.pieces, (value) => { syncPieceLabels(value) }, { deep: true }, ) watch(productTypes, () => { syncProductLabels(props.node?.products) }, { deep: true, immediate: true }) watch( () => props.node.products, (value) => { syncProductLabels(value) }, { deep: true }, ) watch( () => props.node.customFields, (value) => { if (!Array.isArray(value)) { return } value.sort((a: any, b: any) => { const left = typeof a?.orderIndex === 'number' ? a.orderIndex : 0 const right = typeof b?.orderIndex === 'number' ? b.orderIndex : 0 return left - right }) crud.reindexCustomFields() }, { deep: true }, ) watch( () => [props.lockedTypeLabel, props.lockType], () => { if (props.lockType && props.isRoot) { const label = props.lockedTypeLabel || lockedTypeDisplay.value props.node.typeComposantLabel = label if (label && (!props.node.alias || props.node.alias === lockedTypeDisplay.value)) { props.node.alias = label } if (props.node.typeComposantId) { const option = componentTypeMap.value.get(props.node.typeComposantId) props.node.familyCode = option?.code ?? props.node.familyCode } } }, { immediate: true }, ) return { // Lock checks isCustomFieldLocked: crud.isCustomFieldLocked, isPieceLocked: crud.isPieceLocked, isProductLocked: crud.isProductLocked, isSubcomponentLocked: crud.isSubcomponentLocked, // Computed state isLocked, restrictedMode, componentTypes, pieceTypes, productTypes, canManageSubcomponents, childAllowSubcomponents, hasSubcomponents, containerClass, headingClass, lockedTypeDisplay, // Label getters & formatters getComponentTypeLabel, getPieceTypeLabel, formatComponentTypeOption, formatPieceTypeOption, formatProductTypeOption, // Handlers handleComponentTypeSelect, handlePieceTypeSelect, handleProductTypeSelect, // CRUD addCustomField: crud.addCustomField, removeCustomField: crud.removeCustomField, addPiece: crud.addPiece, removePiece: crud.removePiece, addProduct: crud.addProduct, removeProduct: crud.removeProduct, addSubComponent: crud.addSubComponent, removeSubComponent: crud.removeSubComponent, // Drag reorder — custom fields onCustomFieldDragStart: crud.onCustomFieldDragStart, onCustomFieldDragEnter: crud.onCustomFieldDragEnter, onCustomFieldDrop: crud.onCustomFieldDrop, onCustomFieldDragEnd: crud.onCustomFieldDragEnd, customFieldReorderClass: crud.customFieldReorderClass, // Drag reorder — pieces onPieceDragStart: crud.onPieceDragStart, onPieceDragEnter: crud.onPieceDragEnter, onPieceDragOver: crud.onPieceDragOver, onPieceDrop: crud.onPieceDrop, onPieceDragEnd: crud.onPieceDragEnd, pieceReorderClass: crud.pieceReorderClass, // Drag reorder — products onProductDragStart: crud.onProductDragStart, onProductDragEnter: crud.onProductDragEnter, onProductDragOver: crud.onProductDragOver, onProductDrop: crud.onProductDrop, onProductDragEnd: crud.onProductDragEnd, productReorderClass: crud.productReorderClass, // Drag reorder — subcomponents onSubcomponentDragStart: crud.onSubcomponentDragStart, onSubcomponentDragEnter: crud.onSubcomponentDragEnter, onSubcomponentDragOver: crud.onSubcomponentDragOver, onSubcomponentDrop: crud.onSubcomponentDrop, onSubcomponentDragEnd: crud.onSubcomponentDragEnd, subcomponentReorderClass: crud.subcomponentReorderClass, } }