refactor(frontend) : extract StructureNodeEditor logic into composable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -356,26 +356,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import IconLucidePlus from '~icons/lucide/plus'
|
import IconLucidePlus from '~icons/lucide/plus'
|
||||||
import IconLucideTrash from '~icons/lucide/trash'
|
import IconLucideTrash from '~icons/lucide/trash'
|
||||||
import IconLucideGripVertical from '~icons/lucide/grip-vertical'
|
import IconLucideGripVertical from '~icons/lucide/grip-vertical'
|
||||||
import type { ComponentModelPiece, ComponentModelProduct, ComponentModelStructureNode } from '~/shared/types/inventory'
|
import type { EditableStructureNode, ModelTypeOption } from '~/composables/useStructureNodeLogic'
|
||||||
|
|
||||||
defineOptions({ name: 'StructureNodeEditor' })
|
defineOptions({ name: 'StructureNodeEditor' })
|
||||||
|
|
||||||
type ModelTypeOption = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
code?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type EditableStructureNode = ComponentModelStructureNode & {
|
|
||||||
customFields?: any[]
|
|
||||||
pieces?: ComponentModelPiece[]
|
|
||||||
products?: ComponentModelProduct[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
node: EditableStructureNode
|
node: EditableStructureNode
|
||||||
depth?: number
|
depth?: number
|
||||||
@@ -405,522 +392,60 @@ const props = withDefaults(defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits(['remove'])
|
const emit = defineEmits(['remove'])
|
||||||
|
|
||||||
const initialCustomFieldIndices = ref<Set<number>>(new Set())
|
const {
|
||||||
const initialPieceIndices = ref<Set<number>>(new Set())
|
isCustomFieldLocked,
|
||||||
const initialProductIndices = ref<Set<number>>(new Set())
|
isPieceLocked,
|
||||||
const initialSubcomponentIndices = ref<Set<number>>(new Set())
|
isProductLocked,
|
||||||
|
isSubcomponentLocked,
|
||||||
const initializeLockedIndices = () => {
|
isLocked,
|
||||||
if (props.restrictedMode) {
|
restrictedMode,
|
||||||
const customFieldsLength = Array.isArray(props.node.customFields) ? props.node.customFields.length : 0
|
componentTypes,
|
||||||
const piecesLength = Array.isArray(props.node.pieces) ? props.node.pieces.length : 0
|
pieceTypes,
|
||||||
const productsLength = Array.isArray(props.node.products) ? props.node.products.length : 0
|
productTypes,
|
||||||
const subcomponentsLength = Array.isArray(props.node.subcomponents) ? props.node.subcomponents.length : 0
|
|
||||||
|
|
||||||
initialCustomFieldIndices.value = new Set(Array.from({ length: customFieldsLength }, (_, i) => i))
|
|
||||||
initialPieceIndices.value = new Set(Array.from({ length: piecesLength }, (_, i) => i))
|
|
||||||
initialProductIndices.value = new Set(Array.from({ length: productsLength }, (_, i) => i))
|
|
||||||
initialSubcomponentIndices.value = new Set(Array.from({ length: subcomponentsLength }, (_, i) => i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeLockedIndices()
|
|
||||||
|
|
||||||
const isCustomFieldLocked = (index: number): boolean => {
|
|
||||||
return props.restrictedMode === true && initialCustomFieldIndices.value.has(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPieceLocked = (index: number): boolean => {
|
|
||||||
return props.restrictedMode === true && initialPieceIndices.value.has(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isProductLocked = (index: number): boolean => {
|
|
||||||
return props.restrictedMode === true && initialProductIndices.value.has(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSubcomponentLocked = (index: number): boolean => {
|
|
||||||
return props.restrictedMode === true && initialSubcomponentIndices.value.has(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
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'))
|
|
||||||
const lockedTypeDisplay = computed(() => {
|
|
||||||
if (props.lockedTypeLabel) {
|
|
||||||
return props.lockedTypeLabel
|
|
||||||
}
|
|
||||||
return getComponentTypeLabel(props.node?.typeComposantId) || 'Famille non définie'
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
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 ensureArray = (key: 'customFields' | 'pieces' | 'products' | 'subcomponents') => {
|
|
||||||
if (!Array.isArray((props.node as any)[key])) {
|
|
||||||
if (key === 'subcomponents') {
|
|
||||||
props.node.subcomponents = []
|
|
||||||
} else if (key === 'products') {
|
|
||||||
props.node.products = []
|
|
||||||
} else {
|
|
||||||
(props.node as any)[key] = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const reindexCustomFields = () => {
|
|
||||||
if (!Array.isArray(props.node.customFields)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
props.node.customFields.forEach((field: any, index: number) => {
|
|
||||||
if (!field || typeof field !== 'object') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
field.orderIndex = index
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const customFieldDrag = useDragReorder(
|
|
||||||
() => props.node.customFields,
|
|
||||||
{ onReorder: reindexCustomFields },
|
|
||||||
)
|
|
||||||
const onCustomFieldDragStart = customFieldDrag.onDragStart
|
|
||||||
const onCustomFieldDragEnter = customFieldDrag.onDragEnter
|
|
||||||
const onCustomFieldDrop = customFieldDrag.onDrop
|
|
||||||
const onCustomFieldDragEnd = customFieldDrag.onDragEnd
|
|
||||||
const customFieldReorderClass = customFieldDrag.reorderClass
|
|
||||||
|
|
||||||
const addCustomField = () => {
|
|
||||||
ensureArray('customFields')
|
|
||||||
const fields = props.node.customFields!
|
|
||||||
const nextIndex = fields.length
|
|
||||||
fields.push({
|
|
||||||
name: '',
|
|
||||||
type: 'text',
|
|
||||||
required: false,
|
|
||||||
optionsText: '',
|
|
||||||
options: [],
|
|
||||||
orderIndex: nextIndex,
|
|
||||||
})
|
|
||||||
reindexCustomFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeCustomField = (index: number) => {
|
|
||||||
if (!Array.isArray(props.node.customFields)) return
|
|
||||||
props.node.customFields.splice(index, 1)
|
|
||||||
reindexCustomFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
const addPiece = () => {
|
|
||||||
ensureArray('pieces')
|
|
||||||
props.node.pieces!.push({
|
|
||||||
typePieceId: '',
|
|
||||||
typePieceLabel: '',
|
|
||||||
reference: '',
|
|
||||||
familyCode: '',
|
|
||||||
role: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removePiece = (index: number) => {
|
|
||||||
if (!Array.isArray(props.node.pieces)) return
|
|
||||||
props.node.pieces.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addProduct = () => {
|
|
||||||
ensureArray('products')
|
|
||||||
props.node.products!.push({
|
|
||||||
typeProductId: '',
|
|
||||||
typeProductLabel: '',
|
|
||||||
familyCode: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeProduct = (index: number) => {
|
|
||||||
if (!Array.isArray(props.node.products)) return
|
|
||||||
props.node.products.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addSubComponent = () => {
|
|
||||||
if (!canManageSubcomponents.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ensureArray('subcomponents')
|
|
||||||
props.node.subcomponents.push({
|
|
||||||
typeComposantId: '',
|
|
||||||
typeComposantLabel: '',
|
|
||||||
modelId: '',
|
|
||||||
familyCode: '',
|
|
||||||
alias: '',
|
|
||||||
subcomponents: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeSubComponent = (index: number) => {
|
|
||||||
if (!Array.isArray(props.node.subcomponents)) return
|
|
||||||
props.node.subcomponents.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pieceDrag = useDragReorder(() => props.node.pieces)
|
|
||||||
const onPieceDragStart = pieceDrag.onDragStart
|
|
||||||
const onPieceDragEnter = pieceDrag.onDragEnter
|
|
||||||
const onPieceDragOver = pieceDrag.onDragOver
|
|
||||||
const onPieceDrop = pieceDrag.onDrop
|
|
||||||
const onPieceDragEnd = pieceDrag.onDragEnd
|
|
||||||
const pieceReorderClass = pieceDrag.reorderClass
|
|
||||||
|
|
||||||
const productDrag = useDragReorder(() => props.node.products)
|
|
||||||
const onProductDragStart = productDrag.onDragStart
|
|
||||||
const onProductDragEnter = productDrag.onDragEnter
|
|
||||||
const onProductDragOver = productDrag.onDragOver
|
|
||||||
const onProductDrop = productDrag.onDrop
|
|
||||||
const onProductDragEnd = productDrag.onDragEnd
|
|
||||||
const productReorderClass = productDrag.reorderClass
|
|
||||||
|
|
||||||
const subcomponentDrag = useDragReorder(
|
|
||||||
() => props.node.subcomponents,
|
|
||||||
{ draggingClass: 'ring-2 ring-primary', dropTargetClass: 'ring-2 ring-primary/70' },
|
|
||||||
)
|
|
||||||
const onSubcomponentDragStart = subcomponentDrag.onDragStart
|
|
||||||
const onSubcomponentDragEnter = subcomponentDrag.onDragEnter
|
|
||||||
const onSubcomponentDragOver = subcomponentDrag.onDragOver
|
|
||||||
const onSubcomponentDrop = subcomponentDrag.onDrop
|
|
||||||
const onSubcomponentDragEnd = subcomponentDrag.onDragEnd
|
|
||||||
const subcomponentReorderClass = subcomponentDrag.reorderClass
|
|
||||||
|
|
||||||
watch(
|
|
||||||
canManageSubcomponents,
|
canManageSubcomponents,
|
||||||
(allowed) => {
|
childAllowSubcomponents,
|
||||||
if (!allowed && Array.isArray(props.node.subcomponents) && props.node.subcomponents.length) {
|
hasSubcomponents,
|
||||||
props.node.subcomponents.splice(0, props.node.subcomponents.length)
|
containerClass,
|
||||||
}
|
headingClass,
|
||||||
},
|
lockedTypeDisplay,
|
||||||
{ immediate: true }
|
getComponentTypeLabel,
|
||||||
)
|
getPieceTypeLabel,
|
||||||
|
formatComponentTypeOption,
|
||||||
watch(componentTypes, () => {
|
formatPieceTypeOption,
|
||||||
syncComponentType(props.node)
|
formatProductTypeOption,
|
||||||
}, { deep: true, immediate: true })
|
handleComponentTypeSelect,
|
||||||
|
handlePieceTypeSelect,
|
||||||
watch(
|
handleProductTypeSelect,
|
||||||
() => props.node.typeComposantId,
|
addCustomField,
|
||||||
() => {
|
removeCustomField,
|
||||||
syncComponentType(props.node)
|
addPiece,
|
||||||
},
|
removePiece,
|
||||||
)
|
addProduct,
|
||||||
|
removeProduct,
|
||||||
watch(pieceTypes, () => {
|
addSubComponent,
|
||||||
syncPieceLabels(props.node?.pieces)
|
removeSubComponent,
|
||||||
}, { deep: true, immediate: true })
|
onCustomFieldDragStart,
|
||||||
|
onCustomFieldDragEnter,
|
||||||
watch(
|
onCustomFieldDrop,
|
||||||
() => props.node.pieces,
|
onCustomFieldDragEnd,
|
||||||
(value) => {
|
customFieldReorderClass,
|
||||||
syncPieceLabels(value)
|
onPieceDragStart,
|
||||||
},
|
onPieceDragEnter,
|
||||||
{ deep: true }
|
onPieceDragOver,
|
||||||
)
|
onPieceDrop,
|
||||||
|
onPieceDragEnd,
|
||||||
watch(productTypes, () => {
|
pieceReorderClass,
|
||||||
syncProductLabels(props.node?.products)
|
onProductDragStart,
|
||||||
}, { deep: true, immediate: true })
|
onProductDragEnter,
|
||||||
|
onProductDragOver,
|
||||||
watch(
|
onProductDrop,
|
||||||
() => props.node.products,
|
onProductDragEnd,
|
||||||
(value) => {
|
productReorderClass,
|
||||||
syncProductLabels(value)
|
onSubcomponentDragStart,
|
||||||
},
|
onSubcomponentDragEnter,
|
||||||
{ deep: true }
|
onSubcomponentDragOver,
|
||||||
)
|
onSubcomponentDrop,
|
||||||
|
onSubcomponentDragEnd,
|
||||||
watch(
|
subcomponentReorderClass,
|
||||||
() => props.node.customFields,
|
} = useStructureNodeLogic(props)
|
||||||
(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
|
|
||||||
})
|
|
||||||
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 }
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
205
app/composables/useStructureNodeCrud.ts
Normal file
205
app/composables/useStructureNodeCrud.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import type { EditableStructureNode } from '~/composables/useStructureNodeLogic'
|
||||||
|
|
||||||
|
export interface StructureNodeCrudDeps {
|
||||||
|
node: EditableStructureNode
|
||||||
|
restrictedMode: boolean
|
||||||
|
canManageSubcomponents: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStructureNodeCrud(props: StructureNodeCrudDeps) {
|
||||||
|
// --- Lock state ---
|
||||||
|
const initialCustomFieldIndices = ref<Set<number>>(new Set())
|
||||||
|
const initialPieceIndices = ref<Set<number>>(new Set())
|
||||||
|
const initialProductIndices = ref<Set<number>>(new Set())
|
||||||
|
const initialSubcomponentIndices = ref<Set<number>>(new Set())
|
||||||
|
|
||||||
|
const initializeLockedIndices = () => {
|
||||||
|
if (props.restrictedMode) {
|
||||||
|
const customFieldsLength = Array.isArray(props.node.customFields) ? props.node.customFields.length : 0
|
||||||
|
const piecesLength = Array.isArray(props.node.pieces) ? props.node.pieces.length : 0
|
||||||
|
const productsLength = Array.isArray(props.node.products) ? props.node.products.length : 0
|
||||||
|
const subcomponentsLength = Array.isArray(props.node.subcomponents) ? props.node.subcomponents.length : 0
|
||||||
|
|
||||||
|
initialCustomFieldIndices.value = new Set(Array.from({ length: customFieldsLength }, (_, i) => i))
|
||||||
|
initialPieceIndices.value = new Set(Array.from({ length: piecesLength }, (_, i) => i))
|
||||||
|
initialProductIndices.value = new Set(Array.from({ length: productsLength }, (_, i) => i))
|
||||||
|
initialSubcomponentIndices.value = new Set(Array.from({ length: subcomponentsLength }, (_, i) => i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeLockedIndices()
|
||||||
|
|
||||||
|
const isCustomFieldLocked = (index: number): boolean => {
|
||||||
|
return props.restrictedMode === true && initialCustomFieldIndices.value.has(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPieceLocked = (index: number): boolean => {
|
||||||
|
return props.restrictedMode === true && initialPieceIndices.value.has(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isProductLocked = (index: number): boolean => {
|
||||||
|
return props.restrictedMode === true && initialProductIndices.value.has(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSubcomponentLocked = (index: number): boolean => {
|
||||||
|
return props.restrictedMode === true && initialSubcomponentIndices.value.has(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
const ensureArray = (key: 'customFields' | 'pieces' | 'products' | 'subcomponents') => {
|
||||||
|
if (!Array.isArray((props.node as any)[key])) {
|
||||||
|
if (key === 'subcomponents') {
|
||||||
|
props.node.subcomponents = []
|
||||||
|
} else if (key === 'products') {
|
||||||
|
props.node.products = []
|
||||||
|
} else {
|
||||||
|
(props.node as any)[key] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom field reindex ---
|
||||||
|
const reindexCustomFields = () => {
|
||||||
|
if (!Array.isArray(props.node.customFields)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
props.node.customFields.forEach((field: any, index: number) => {
|
||||||
|
if (!field || typeof field !== 'object') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
field.orderIndex = index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Drag reorder ---
|
||||||
|
const customFieldDrag = useDragReorder(
|
||||||
|
() => props.node.customFields,
|
||||||
|
{ onReorder: reindexCustomFields },
|
||||||
|
)
|
||||||
|
|
||||||
|
const pieceDrag = useDragReorder(() => props.node.pieces)
|
||||||
|
const productDrag = useDragReorder(() => props.node.products)
|
||||||
|
const subcomponentDrag = useDragReorder(
|
||||||
|
() => props.node.subcomponents,
|
||||||
|
{ draggingClass: 'ring-2 ring-primary', dropTargetClass: 'ring-2 ring-primary/70' },
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- CRUD functions ---
|
||||||
|
const addCustomField = () => {
|
||||||
|
ensureArray('customFields')
|
||||||
|
const fields = props.node.customFields!
|
||||||
|
const nextIndex = fields.length
|
||||||
|
fields.push({
|
||||||
|
name: '',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
optionsText: '',
|
||||||
|
options: [],
|
||||||
|
orderIndex: nextIndex,
|
||||||
|
})
|
||||||
|
reindexCustomFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCustomField = (index: number) => {
|
||||||
|
if (!Array.isArray(props.node.customFields)) return
|
||||||
|
props.node.customFields.splice(index, 1)
|
||||||
|
reindexCustomFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPiece = () => {
|
||||||
|
ensureArray('pieces')
|
||||||
|
props.node.pieces!.push({
|
||||||
|
typePieceId: '',
|
||||||
|
typePieceLabel: '',
|
||||||
|
reference: '',
|
||||||
|
familyCode: '',
|
||||||
|
role: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePiece = (index: number) => {
|
||||||
|
if (!Array.isArray(props.node.pieces)) return
|
||||||
|
props.node.pieces.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addProduct = () => {
|
||||||
|
ensureArray('products')
|
||||||
|
props.node.products!.push({
|
||||||
|
typeProductId: '',
|
||||||
|
typeProductLabel: '',
|
||||||
|
familyCode: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeProduct = (index: number) => {
|
||||||
|
if (!Array.isArray(props.node.products)) return
|
||||||
|
props.node.products.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSubComponent = () => {
|
||||||
|
if (!props.canManageSubcomponents()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ensureArray('subcomponents')
|
||||||
|
props.node.subcomponents.push({
|
||||||
|
typeComposantId: '',
|
||||||
|
typeComposantLabel: '',
|
||||||
|
modelId: '',
|
||||||
|
familyCode: '',
|
||||||
|
alias: '',
|
||||||
|
subcomponents: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSubComponent = (index: number) => {
|
||||||
|
if (!Array.isArray(props.node.subcomponents)) return
|
||||||
|
props.node.subcomponents.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Lock checks
|
||||||
|
isCustomFieldLocked,
|
||||||
|
isPieceLocked,
|
||||||
|
isProductLocked,
|
||||||
|
isSubcomponentLocked,
|
||||||
|
// Helpers exposed for watchers
|
||||||
|
reindexCustomFields,
|
||||||
|
// CRUD
|
||||||
|
addCustomField,
|
||||||
|
removeCustomField,
|
||||||
|
addPiece,
|
||||||
|
removePiece,
|
||||||
|
addProduct,
|
||||||
|
removeProduct,
|
||||||
|
addSubComponent,
|
||||||
|
removeSubComponent,
|
||||||
|
// Drag reorder — custom fields
|
||||||
|
onCustomFieldDragStart: customFieldDrag.onDragStart,
|
||||||
|
onCustomFieldDragEnter: customFieldDrag.onDragEnter,
|
||||||
|
onCustomFieldDrop: customFieldDrag.onDrop,
|
||||||
|
onCustomFieldDragEnd: customFieldDrag.onDragEnd,
|
||||||
|
customFieldReorderClass: customFieldDrag.reorderClass,
|
||||||
|
// Drag reorder — pieces
|
||||||
|
onPieceDragStart: pieceDrag.onDragStart,
|
||||||
|
onPieceDragEnter: pieceDrag.onDragEnter,
|
||||||
|
onPieceDragOver: pieceDrag.onDragOver,
|
||||||
|
onPieceDrop: pieceDrag.onDrop,
|
||||||
|
onPieceDragEnd: pieceDrag.onDragEnd,
|
||||||
|
pieceReorderClass: pieceDrag.reorderClass,
|
||||||
|
// Drag reorder — products
|
||||||
|
onProductDragStart: productDrag.onDragStart,
|
||||||
|
onProductDragEnter: productDrag.onDragEnter,
|
||||||
|
onProductDragOver: productDrag.onDragOver,
|
||||||
|
onProductDrop: productDrag.onDrop,
|
||||||
|
onProductDragEnd: productDrag.onDragEnd,
|
||||||
|
productReorderClass: productDrag.reorderClass,
|
||||||
|
// Drag reorder — subcomponents
|
||||||
|
onSubcomponentDragStart: subcomponentDrag.onDragStart,
|
||||||
|
onSubcomponentDragEnter: subcomponentDrag.onDragEnter,
|
||||||
|
onSubcomponentDragOver: subcomponentDrag.onDragOver,
|
||||||
|
onSubcomponentDrop: subcomponentDrag.onDrop,
|
||||||
|
onSubcomponentDragEnd: subcomponentDrag.onDragEnd,
|
||||||
|
subcomponentReorderClass: subcomponentDrag.reorderClass,
|
||||||
|
}
|
||||||
|
}
|
||||||
462
app/composables/useStructureNodeLogic.ts
Normal file
462
app/composables/useStructureNodeLogic.ts
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user