Files
Inventory_frontend/app/composables/useStructureNodeLogic.ts

463 lines
14 KiB
TypeScript

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<string, ModelTypeOption>()
componentTypes.value.forEach((type) => {
if (type && typeof type.id === 'string') {
map.set(type.id, type)
}
})
return map
})
const componentTypeCodeMap = computed(() => {
const map = new Map<string, ModelTypeOption>()
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<string, ModelTypeOption>()
pieceTypes.value.forEach((type) => {
if (type && typeof type.id === 'string') {
map.set(type.id, type)
}
})
return map
})
const productTypeMap = computed(() => {
const map = new Map<string, ModelTypeOption>()
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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,
}
}