import { createEmptyComponentModelStructure, type ComponentModelCustomFieldType, type ComponentModelCustomField, type ComponentModelPiece, type ComponentModelProduct, type ComponentModelStructure, type ComponentModelStructureNode, } from '../types/inventory' export const isPlainObject = (value: unknown): value is Record => { return value !== null && typeof value === 'object' && !Array.isArray(value) } export interface ModelStructurePreview { customFields: number pieces: number products: number subcomponents: number } export const defaultStructure = (): ComponentModelStructure => ({ ...createEmptyComponentModelStructure(), }) const ensureStructureShape = (input: any): ComponentModelStructure => { const base = createEmptyComponentModelStructure() if (!isPlainObject(input)) { return base } const clone: ComponentModelStructure = { ...base, customFields: Array.isArray((input as any).customFields) ? (input as any).customFields : [], pieces: Array.isArray((input as any).pieces) ? (input as any).pieces : [], products: Array.isArray((input as any).products) ? (input as any).products : [], subcomponents: Array.isArray((input as any).subcomponents) ? (input as any).subcomponents : Array.isArray((input as any).subComponents) ? (input as any).subComponents : [], typeComposantId: typeof (input as any).typeComposantId === 'string' ? (input as any).typeComposantId : undefined, typeComposantLabel: typeof (input as any).typeComposantLabel === 'string' ? (input as any).typeComposantLabel : undefined, modelId: typeof (input as any).modelId === 'string' ? (input as any).modelId : undefined, familyCode: typeof (input as any).familyCode === 'string' ? (input as any).familyCode : undefined, alias: typeof (input as any).alias === 'string' ? (input as any).alias : undefined, } return clone } export const cloneStructure = (input: any): ComponentModelStructure => { try { const cloned = JSON.parse(JSON.stringify(input ?? defaultStructure())) return ensureStructureShape(cloned) } catch (_error) { return defaultStructure() } } export const toStringArray = (input: unknown): string[] | undefined => { if (!Array.isArray(input)) { return undefined } const parsed = input .map((value) => { if (typeof value === 'string') { return value.trim() } if (value === null || value === undefined) { return '' } return String(value).trim() }) .filter((value) => value.length > 0) return parsed.length ? parsed : undefined } export const extractFieldValueObject = (field: any): Record => { if (isPlainObject(field?.value)) { return field.value as Record } return {} } export const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => { if (!Array.isArray(fields)) { return [] } return fields .map((field, index) => { const rawName = typeof field?.name === 'string' ? field.name : typeof field?.key === 'string' ? field.key : '' const name = rawName.trim() if (!name) { return null } const valueObject = extractFieldValueObject(field) const candidateType = typeof field?.type === 'string' && field.type ? field.type : typeof valueObject?.type === 'string' ? valueObject.type : '' const allowedTypes: ComponentModelCustomFieldType[] = ['text', 'number', 'select', 'boolean', 'date'] const type = allowedTypes.includes(candidateType as ComponentModelCustomFieldType) ? (candidateType as ComponentModelCustomFieldType) : 'text' const required = typeof valueObject?.required === 'boolean' ? valueObject.required : !!field?.required let options: string[] | undefined if (type === 'select') { options = toStringArray(valueObject?.options) || toStringArray((valueObject as any)?.choices) || toStringArray(field?.options) if (!options && typeof field?.optionsText === 'string') { const parsedFromText = field.optionsText .split(/\r?\n/) .map((option: string) => option.trim()) .filter((option: string) => option.length > 0) options = parsedFromText.length ? parsedFromText : undefined } } const result: ComponentModelCustomField = { name, type, required } if (options) { result.options = options } const defaultCandidate = field?.defaultValue ?? valueObject?.defaultValue ?? field?.value ?? field?.default ?? null const resolvedDefault = (() => { if (defaultCandidate === undefined || defaultCandidate === null) { return undefined } if (typeof defaultCandidate === 'object') { if (defaultCandidate === null) { return undefined } if ('defaultValue' in (defaultCandidate as Record)) { return (defaultCandidate as Record).defaultValue } if ('value' in (defaultCandidate as Record)) { return (defaultCandidate as Record).value } return undefined } return defaultCandidate })() if (resolvedDefault !== undefined && resolvedDefault !== null && resolvedDefault !== '') { result.defaultValue = String(resolvedDefault) } const id = typeof field?.id === 'string' ? field.id : undefined if (id) { result.id = id } const customFieldId = typeof field?.customFieldId === 'string' ? field.customFieldId : undefined if (customFieldId) { result.customFieldId = customFieldId } const orderIndex = typeof field?.orderIndex === 'number' ? field.orderIndex : index result.orderIndex = orderIndex return result }) .filter((field): field is ComponentModelCustomField => !!field) } export const sanitizePieces = (pieces: any[]): ComponentModelPiece[] => { if (!Array.isArray(pieces)) { return [] } return pieces .map((piece) => { const rawTypePieceId = typeof piece?.typePieceId === 'string' ? piece.typePieceId.trim() : typeof piece?.typePiece?.id === 'string' ? piece.typePiece.id.trim() : '' const typePieceId = rawTypePieceId.length > 0 ? rawTypePieceId : undefined const rawTypePieceLabel = typeof piece?.typePieceLabel === 'string' ? piece.typePieceLabel.trim() : typeof piece?.typePiece?.name === 'string' ? piece.typePiece.name.trim() : '' const typePieceLabel = rawTypePieceLabel.length > 0 ? rawTypePieceLabel : undefined const reference = typeof piece?.reference === 'string' && piece.reference.trim().length > 0 ? piece.reference.trim() : undefined const rawFamilyCode = typeof piece?.familyCode === 'string' ? piece.familyCode.trim() : typeof piece?.typePiece?.code === 'string' ? piece.typePiece.code.trim() : '' const familyCode = rawFamilyCode.length > 0 ? rawFamilyCode : undefined const rawRole = typeof piece?.role === 'string' ? piece.role.trim() : '' const role = rawRole.length > 0 ? rawRole : undefined if (!typePieceId && !typePieceLabel && !reference && !familyCode) { return null } const result: ComponentModelPiece = {} if (role) { result.role = role } if (familyCode) { result.familyCode = familyCode } if (reference !== undefined) { result.reference = reference } if (typePieceId) { result.typePieceId = typePieceId } if (typePieceLabel) { result.typePieceLabel = typePieceLabel } return result }) .filter((piece): piece is ComponentModelPiece => !!piece) } export const sanitizeProducts = (products: any[]): ComponentModelProduct[] => { if (!Array.isArray(products)) { return [] } return products .map((product) => { const rawTypeProductId = typeof product?.typeProductId === 'string' ? product.typeProductId.trim() : typeof product?.typeProduct?.id === 'string' ? product.typeProduct.id.trim() : '' const typeProductId = rawTypeProductId.length > 0 ? rawTypeProductId : undefined const rawTypeProductLabel = typeof product?.typeProductLabel === 'string' ? product.typeProductLabel.trim() : typeof product?.typeProduct?.name === 'string' ? product.typeProduct.name.trim() : '' const typeProductLabel = rawTypeProductLabel.length > 0 ? rawTypeProductLabel : undefined const reference = typeof product?.reference === 'string' && product.reference.trim().length > 0 ? product.reference.trim() : undefined const rawFamilyCode = typeof product?.familyCode === 'string' ? product.familyCode.trim() : typeof product?.typeProduct?.code === 'string' ? product.typeProduct.code.trim() : '' const familyCode = rawFamilyCode.length > 0 ? rawFamilyCode : undefined const rawRole = typeof product?.role === 'string' ? product.role.trim() : '' const role = rawRole.length > 0 ? rawRole : undefined if (!typeProductId && !typeProductLabel && !reference && !familyCode) { return null } const result: ComponentModelProduct = {} if (role) { result.role = role } if (familyCode) { result.familyCode = familyCode } if (reference !== undefined) { result.reference = reference } if (typeProductId) { result.typeProductId = typeProductId } if (typeProductLabel) { result.typeProductLabel = typeProductLabel } return result }) .filter((product): product is ComponentModelProduct => !!product) } const sanitizeSubcomponents = (components: any[]): ComponentModelStructureNode[] => { if (!Array.isArray(components)) { return [] } return components .map((component) => { const rawTypeComposantId = typeof component?.typeComposantId === 'string' ? component.typeComposantId.trim() : typeof component?.typeComposant?.id === 'string' ? component.typeComposant.id.trim() : '' const typeComposantId = rawTypeComposantId.length > 0 ? rawTypeComposantId : undefined const modelId = typeof component?.modelId === 'string' && component.modelId.trim().length > 0 ? component.modelId.trim() : undefined const familyCode = typeof component?.familyCode === 'string' && component.familyCode.trim().length > 0 ? component.familyCode.trim() : undefined const alias = typeof component?.alias === 'string' && component.alias.trim().length > 0 ? component.alias.trim() : undefined if (!typeComposantId && !modelId && !familyCode) { return null } const result: ComponentModelStructureNode = { subcomponents: sanitizeSubcomponents( Array.isArray(component?.subcomponents) ? component.subcomponents : component?.subComponents, ), } if (typeComposantId) { result.typeComposantId = typeComposantId } const typeComposantLabel = typeof component?.typeComposantLabel === 'string' ? component.typeComposantLabel.trim() : typeof component?.typeComposant?.name === 'string' ? component.typeComposant.name.trim() : '' if (typeComposantLabel) { result.typeComposantLabel = typeComposantLabel } if (modelId) { result.modelId = modelId } if (familyCode) { result.familyCode = familyCode } if (alias) { result.alias = alias } return result }) .filter((component): component is ComponentModelStructureNode => !!component) } const hydrateCustomFields = (fields: any[]): any[] => { if (!Array.isArray(fields)) { return [] } return fields.map((field, index) => { const valueObject = extractFieldValueObject(field) const name = typeof field?.name === 'string' ? field.name : typeof field?.key === 'string' ? field.key : '' const candidateType = typeof field?.type === 'string' && field.type ? field.type : typeof valueObject?.type === 'string' ? valueObject.type : '' const allowedTypes: ComponentModelCustomFieldType[] = ['text', 'number', 'select', 'boolean', 'date'] const type = allowedTypes.includes(candidateType as ComponentModelCustomFieldType) ? (candidateType as ComponentModelCustomFieldType) : 'text' const required = typeof field?.required === 'boolean' ? field.required : typeof valueObject?.required === 'boolean' ? valueObject.required : false const options = toStringArray(field?.options) || toStringArray(valueObject?.options) || toStringArray((valueObject as any)?.choices) || [] const optionsText = typeof field?.optionsText === 'string' ? field.optionsText : options.length ? options.join('\n') : '' const defaultCandidate = field?.defaultValue ?? valueObject?.defaultValue ?? field?.value ?? field?.default ?? null const resolvedDefault = (() => { if (defaultCandidate === undefined || defaultCandidate === null) { return undefined } if (typeof defaultCandidate === 'object') { if (defaultCandidate === null) { return undefined } if ('defaultValue' in (defaultCandidate as Record)) { return (defaultCandidate as Record).defaultValue } if ('value' in (defaultCandidate as Record)) { return (defaultCandidate as Record).value } return undefined } return defaultCandidate })() const defaultValue = resolvedDefault !== undefined && resolvedDefault !== null && resolvedDefault !== '' ? String(resolvedDefault) : '' const id = typeof field?.id === 'string' ? field.id : undefined const customFieldId = typeof field?.customFieldId === 'string' ? field.customFieldId : undefined const orderIndex = typeof field?.orderIndex === 'number' ? field.orderIndex : index return { name, type, required, options, optionsText, defaultValue, id, customFieldId, orderIndex, } }) } const hydratePieces = (pieces: any[]): ComponentModelPiece[] => { if (!Array.isArray(pieces)) { return [] } return pieces.map((piece) => ({ typePieceId: piece?.typePieceId ?? piece?.typePiece?.id ?? '', typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '', reference: piece?.reference ?? '', familyCode: piece?.familyCode ?? piece?.typePiece?.code ?? '', role: piece?.role ?? '', })) } export const hydrateProducts = (products: any[]): ComponentModelProduct[] => { if (!Array.isArray(products)) { return [] } return products.map((product) => ({ typeProductId: product?.typeProductId ?? product?.typeProduct?.id ?? '', typeProductLabel: product?.typeProductLabel ?? product?.typeProduct?.name ?? '', reference: product?.reference ?? '', familyCode: product?.familyCode ?? product?.typeProduct?.code ?? '', role: product?.role ?? '', })) } const hydrateSubcomponents = (components: any[]): ComponentModelStructureNode[] => { if (!Array.isArray(components)) { return [] } return components.map((component) => ({ typeComposantId: component?.typeComposantId ?? component?.typeComposant?.id ?? '', typeComposantLabel: component?.typeComposantLabel ?? component?.typeComposant?.name ?? '', modelId: component?.modelId ?? '', familyCode: component?.familyCode ?? component?.typeComposant?.code ?? '', alias: component?.alias ?? component?.name ?? '', subcomponents: hydrateSubcomponents( Array.isArray(component?.subcomponents) ? component.subcomponents : component?.subComponents, ), })) } export const normalizeStructureForEditor = (input: any): ComponentModelStructure => { const source = cloneStructure(input) const sanitizedCustomFields = sanitizeCustomFields(source.customFields) const customFields = sanitizedCustomFields.map((field) => { const options = Array.isArray(field.options) ? [...field.options] : [] const optionsText = options.length ? options.join('\n') : '' const defaultValue = field.defaultValue !== undefined && field.defaultValue !== null && field.defaultValue !== '' ? String(field.defaultValue) : null const copy: ComponentModelCustomField = { name: field.name, type: field.type, required: field.required, options, defaultValue, optionsText, id: field.id, customFieldId: field.customFieldId, } return copy }) const result: ComponentModelStructure = { customFields: customFields as ComponentModelCustomField[], pieces: sanitizePieces(source.pieces), products: sanitizeProducts(source.products), subcomponents: hydrateSubcomponents(source.subcomponents), } if (typeof source.typeComposantId === 'string' && source.typeComposantId.length > 0) { result.typeComposantId = source.typeComposantId } if (typeof source.typeComposantLabel === 'string' && source.typeComposantLabel.length > 0) { result.typeComposantLabel = source.typeComposantLabel } if (typeof source.modelId === 'string' && source.modelId.length > 0) { result.modelId = source.modelId } if (typeof source.familyCode === 'string' && source.familyCode.length > 0) { result.familyCode = source.familyCode } if (typeof source.alias === 'string' && source.alias.length > 0) { result.alias = source.alias } return result } export const normalizeStructureForSave = (input: any): any => { const source = cloneStructure(input) const sanitizedCustomFields = sanitizeCustomFields(source.customFields) const backendCustomFields = sanitizedCustomFields.map((field) => { const value: Record = { type: field.type, required: !!field.required, } if (field.options && field.options.length) { value.options = field.options } if (field.defaultValue !== undefined && field.defaultValue !== null && field.defaultValue !== '') { value.defaultValue = field.defaultValue } const payload: Record = { key: field.name, value, } if (field.id) { payload.id = field.id } if (field.customFieldId) { payload.customFieldId = field.customFieldId } return payload }) as any const backendPieces = sanitizePieces(source.pieces).map((piece) => { const payload: Record = {} if ((piece as any).familyCode) { payload.familyCode = (piece as any).familyCode } if (piece.typePieceId) { payload.typePieceId = piece.typePieceId } if (piece.typePieceLabel) { payload.typePieceLabel = piece.typePieceLabel } if (piece.reference) { payload.reference = piece.reference } return payload }) as any const backendProducts = sanitizeProducts(source.products).map((product) => { const payload: Record = {} if ((product as any).familyCode) { payload.familyCode = (product as any).familyCode } if (product.typeProductId) { payload.typeProductId = product.typeProductId } if (product.role) { payload.role = product.role } return payload }) as any const mapSubcomponentForSave = (subcomponent: ComponentModelStructureNode): any => { const payload: Record = {} if (subcomponent.typeComposantId) { payload.typeComposantId = subcomponent.typeComposantId } if (subcomponent.modelId) { payload.modelId = subcomponent.modelId } if (subcomponent.familyCode) { payload.familyCode = subcomponent.familyCode } if (subcomponent.alias) { payload.alias = subcomponent.alias } if (Array.isArray(subcomponent.subcomponents) && subcomponent.subcomponents.length) { payload.subcomponents = subcomponent.subcomponents.map(mapSubcomponentForSave) } return payload } const backendSubcomponents = sanitizeSubcomponents(source.subcomponents).map(mapSubcomponentForSave) as any const result: ComponentModelStructure = { customFields: backendCustomFields, pieces: backendPieces, products: backendProducts, subcomponents: backendSubcomponents, } if (typeof source.typeComposantId === 'string' && source.typeComposantId.length > 0) { (result as any).typeComposantId = source.typeComposantId } if (typeof source.typeComposantLabel === 'string' && source.typeComposantLabel.length > 0) { (result as any).typeComposantLabel = source.typeComposantLabel } if (typeof source.modelId === 'string' && source.modelId.length > 0) { (result as any).modelId = source.modelId } if (typeof source.familyCode === 'string' && source.familyCode.length > 0) { (result as any).familyCode = source.familyCode } if (typeof source.alias === 'string' && source.alias.length > 0) { (result as any).alias = source.alias } return result } export const hydrateStructureForEditor = (input: any): ComponentModelStructure => { const source = cloneStructure(input) return { customFields: hydrateCustomFields(source.customFields), pieces: hydratePieces(source.pieces), products: hydrateProducts(source.products), subcomponents: hydrateSubcomponents( Array.isArray(source.subcomponents) ? source.subcomponents : (source as any).subComponents, ), typeComposantId: source.typeComposantId ?? '', typeComposantLabel: source.typeComposantLabel ?? '', modelId: source.modelId ?? '', familyCode: source.familyCode ?? '', alias: source.alias ?? '', } } const mapComponentCustomFields = (fields: any[]) => { if (!Array.isArray(fields)) { return [] } return hydrateCustomFields(fields).map((field, index) => { const defaultValue = field?.defaultValue !== undefined && field?.defaultValue !== null && field?.defaultValue !== '' ? field.defaultValue : null return { name: typeof field?.name === 'string' ? field.name : '', type: field?.type ?? 'text', required: !!field?.required, options: Array.isArray(field?.options) ? field.options : [], optionsText: typeof field?.optionsText === 'string' ? field.optionsText : '', defaultValue, id: typeof (field as any)?.id === 'string' ? (field as any).id : undefined, customFieldId: typeof (field as any)?.customFieldId === 'string' ? (field as any).customFieldId : undefined, orderIndex: typeof field?.orderIndex === 'number' ? field.orderIndex : index, } }) } const mapComponentPieces = (pieces: any[]): ComponentModelPiece[] => { if (!Array.isArray(pieces)) { return [] } return pieces.map((piece) => ({ reference: piece?.reference ?? '', typePieceId: piece?.typePieceId ?? piece?.typePiece?.id ?? '', typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '', familyCode: piece?.familyCode ?? piece?.typePiece?.code ?? '', role: piece?.role ?? '', })) } const mapComponentProducts = (products: any[]): ComponentModelProduct[] => { if (!Array.isArray(products)) { return [] } return products.map((product) => ({ reference: product?.reference ?? '', typeProductId: product?.typeProductId ?? product?.typeProduct?.id ?? '', typeProductLabel: product?.typeProductLabel ?? product?.typeProduct?.name ?? '', familyCode: product?.familyCode ?? product?.typeProduct?.code ?? '', role: product?.role ?? '', })) } const mapSubcomponents = (components: any[]): ComponentModelStructureNode[] => { if (!Array.isArray(components)) { return [] } return components.map((component) => ({ typeComposantId: component?.typeComposantId ?? component?.typeComposant?.id ?? '', typeComposantLabel: component?.typeComposantLabel ?? component?.typeComposant?.name ?? '', modelId: component?.modelId ?? '', familyCode: component?.familyCode ?? component?.typeComposant?.code ?? '', alias: component?.alias ?? component?.name ?? '', subcomponents: mapSubcomponents( Array.isArray(component?.subcomponents) ? component.subcomponents : component?.subComponents, ), })) } export const extractStructureFromComponent = (component: any) => { if (!component) { return defaultStructure() } const raw = { customFields: mapComponentCustomFields(component.customFields), pieces: mapComponentPieces(component.pieces), products: mapComponentProducts(component.products), subcomponents: mapSubcomponents( Array.isArray(component?.subcomponents) ? component.subcomponents : component?.subComponents, ), typeComposantId: component?.typeComposantId ?? component?.typeComposant?.id ?? '', typeComposantLabel: component?.typeComposant?.name ?? component?.typeComposantLabel ?? '', modelId: component?.modelId ?? '', familyCode: component?.familyCode ?? component?.typeComposant?.code ?? '', alias: component?.alias ?? component?.name ?? '', } return normalizeStructureForEditor(raw) } export const computeStructureStats = (structure: any): ModelStructurePreview => { if (!structure || typeof structure !== 'object') { return { customFields: 0, pieces: 0, products: 0, subcomponents: 0 } } return { customFields: Array.isArray(structure.customFields) ? structure.customFields.length : 0, pieces: Array.isArray(structure.pieces) ? structure.pieces.length : 0, products: Array.isArray(structure.products) ? structure.products.length : 0, subcomponents: Array.isArray(structure.subcomponents) ? structure.subcomponents.length : Array.isArray(structure.subComponents) ? structure.subComponents.length : 0, } } export const formatStructurePreview = (structure: any) => { const stats = computeStructureStats(structure) if (!stats.customFields && !stats.pieces && !stats.products && !stats.subcomponents) { return 'Structure vide' } const segments: string[] = [] if (stats.customFields) segments.push(`${stats.customFields} champ(s)`) if (stats.pieces) segments.push(`${stats.pieces} pièce(s)`) if (stats.products) segments.push(`${stats.products} produit(s)`) if (stats.subcomponents) segments.push(`${stats.subcomponents} sous-composant(s)`) return segments.join(' • ') }