454 lines
14 KiB
TypeScript
454 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
|
|
isLocked: boolean
|
|
}
|
|
|
|
export function useStructureNodeLogic(props: StructureNodeLogicDeps) {
|
|
// --- Computed props ---
|
|
const isLocked = computed(() => props.isLocked === 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,
|
|
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 {
|
|
// Computed state
|
|
isLocked,
|
|
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,
|
|
}
|
|
}
|