import { createEmptyComponentModelStructure, type ComponentModelCustomFieldType, type ComponentModelCustomField, type ComponentModelPiece, type ComponentModelStructure, type ComponentModelStructureNode, type PieceModelCustomField, type PieceModelStructure, type PieceModelStructureEditorField, type PieceModelStructureForEditor, createEmptyPieceModelStructure, } from './types/inventory' import { uniqueConstructeurIds } from './constructeurUtils' export const isPlainObject = (value: unknown): value is Record => { return value !== null && typeof value === 'object' && !Array.isArray(value) } export interface ModelStructurePreview { customFields: number pieces: 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 : [], 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() } } 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 } const extractFieldValueObject = (field: any): Record => { if (isPlainObject(field?.value)) { return field.value as Record } return {} } 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) => option.trim()) .filter((option) => 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) } 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) } 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) } 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), 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 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, 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 } 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 ?? '', })) } 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 hydrateStructureForEditor = (input: any): ComponentModelStructure => { const source = cloneStructure(input) return { customFields: hydrateCustomFields(source.customFields), pieces: hydratePieces(source.pieces), 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 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), 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, subcomponents: 0 } } return { customFields: Array.isArray(structure.customFields) ? structure.customFields.length : 0, pieces: Array.isArray(structure.pieces) ? structure.pieces.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.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.subcomponents) segments.push(`${stats.subcomponents} sous-composant(s)`) return segments.join(' • ') } export interface DefinitionOverridePayload { name?: string reference?: string constructeurIds?: string[] prix?: number } export const sanitizeDefinitionOverrides = (definition: any): DefinitionOverridePayload | null => { if (!definition || typeof definition !== 'object') { return null } const payload: DefinitionOverridePayload = {} if (typeof definition.name === 'string') { const name = definition.name.trim() if (name.length > 0) { payload.name = name } } if (typeof definition.reference === 'string') { const reference = definition.reference.trim() if (reference.length > 0) { payload.reference = reference } } const constructeurIds = uniqueConstructeurIds( definition.constructeurIds, definition.constructeurId, definition.constructeur, definition.constructeurs, ) if (constructeurIds.length) { payload.constructeurIds = constructeurIds } if (definition.prix !== undefined && definition.prix !== null && definition.prix !== '') { const parsed = Number(definition.prix) if (!Number.isNaN(parsed)) { payload.prix = parsed } } return Object.keys(payload).length ? payload : null } export const defaultPieceStructure = (): PieceModelStructure => ({ ...createEmptyPieceModelStructure(), }) const ensurePieceStructureShape = (input: any): PieceModelStructure => { const base = createEmptyPieceModelStructure() if (!isPlainObject(input)) { return base } const clone: PieceModelStructure = { ...base, customFields: Array.isArray((input as any).customFields) ? (input as any).customFields : [], } for (const [key, value] of Object.entries(input as Record)) { if (key === 'customFields') { continue } clone[key] = value } return clone } export const clonePieceStructure = (input: any): PieceModelStructure => { try { const cloned = JSON.parse(JSON.stringify(input ?? defaultPieceStructure())) return ensurePieceStructureShape(cloned) } catch (error) { return defaultPieceStructure() } } const sanitizePieceCustomFields = (fields: any[]): PieceModelCustomField[] => { if (!Array.isArray(fields)) { return [] } return fields .map((field, index) => { const name = typeof field?.name === 'string' ? field.name.trim() : '' if (!name) { return null } const type = typeof field?.type === 'string' && field.type ? field.type : 'text' const required = !!field?.required let options: string[] | undefined if (type === 'select') { const rawOptions = typeof field?.optionsText === 'string' ? field.optionsText : Array.isArray(field?.options) ? field.options.join('\n') : '' const parsed = rawOptions .split(/\r?\n/) .map((option) => option.trim()) .filter((option) => option.length > 0) options = parsed.length > 0 ? parsed : undefined } const result: PieceModelCustomField = { name, type, required } if (options) { result.options = options } const orderIndex = typeof field?.orderIndex === 'number' ? field.orderIndex : index result.orderIndex = orderIndex return result }) .filter((field): field is PieceModelCustomField => !!field) } export const normalizePieceStructureForSave = (input: any): PieceModelStructure => { const source = clonePieceStructure(input) return { ...Object.fromEntries( Object.entries(source).filter(([key]) => key !== 'customFields'), ), customFields: sanitizePieceCustomFields(source.customFields), } } const hydratePieceCustomFields = (fields: any[]): PieceModelStructureEditorField[] => { if (!Array.isArray(fields)) { return [] } return fields.map((field, index) => ({ name: field?.name ?? '', type: field?.type ?? 'text', required: !!field?.required, options: Array.isArray(field?.options) ? field.options : undefined, optionsText: typeof field?.optionsText === 'string' ? field.optionsText : Array.isArray(field?.options) ? field.options.join('\n') : '', orderIndex: typeof field?.orderIndex === 'number' ? field.orderIndex : index, })) } export const hydratePieceStructureForEditor = (input: any): PieceModelStructureForEditor => { const source = clonePieceStructure(input) const payload: PieceModelStructureForEditor = { ...Object.fromEntries( Object.entries(source).filter(([key]) => key !== 'customFields'), ), customFields: hydratePieceCustomFields(source.customFields), } return payload } export const formatPieceStructurePreview = (structure: any) => { if (!structure || typeof structure !== 'object') { return 'Aucun champ personnalisé' } const customFields = Array.isArray((structure as any).customFields) ? (structure as any).customFields.length : 0 if (!customFields) { return 'Aucun champ personnalisé' } return `${customFields} champ(s) personnalisé(s)` }