From 81eb18100096c083ffe1d6492e0597b5c83c11b6 Mon Sep 17 00:00:00 2001 From: matthieu Date: Sun, 8 Mar 2026 14:43:15 +0100 Subject: [PATCH] refactor(frontend) : split componentStructure.ts into focused modules Co-Authored-By: Claude Opus 4.6 --- app/shared/model/componentStructure.ts | 533 +----------------- app/shared/model/componentStructureHydrate.ts | 210 +++++++ .../model/componentStructureSanitize.ts | 312 ++++++++++ 3 files changed, 548 insertions(+), 507 deletions(-) create mode 100644 app/shared/model/componentStructureHydrate.ts create mode 100644 app/shared/model/componentStructureSanitize.ts diff --git a/app/shared/model/componentStructure.ts b/app/shared/model/componentStructure.ts index d4192f4..be936bc 100644 --- a/app/shared/model/componentStructure.ts +++ b/app/shared/model/componentStructure.ts @@ -1,13 +1,36 @@ import { createEmptyComponentModelStructure, - type ComponentModelCustomFieldType, type ComponentModelCustomField, - type ComponentModelPiece, - type ComponentModelProduct, type ComponentModelStructure, type ComponentModelStructureNode, } from '../types/inventory' +// Import for internal use in this file +import { sanitizeCustomFields, sanitizePieces, sanitizeProducts, sanitizeSubcomponents } from './componentStructureSanitize' +import { hydrateCustomFields, hydratePieces, hydrateProducts, hydrateSubcomponents, mapComponentCustomFields, mapComponentPieces, mapComponentProducts, mapSubcomponents } from './componentStructureHydrate' + +// Re-export sanitize functions so existing imports continue to work +export { + toStringArray, + extractFieldValueObject, + sanitizeCustomFields, + sanitizePieces, + sanitizeProducts, + sanitizeSubcomponents, +} from './componentStructureSanitize' + +// Re-export hydrate functions so existing imports continue to work +export { + hydrateCustomFields, + hydratePieces, + hydrateProducts, + hydrateSubcomponents, + mapComponentCustomFields, + mapComponentPieces, + mapComponentProducts, + mapSubcomponents, +} from './componentStructureHydrate' + export const isPlainObject = (value: unknown): value is Record => { return value !== null && typeof value === 'object' && !Array.isArray(value) } @@ -60,440 +83,6 @@ export const cloneStructure = (input: any): ComponentModelStructure => { } } -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) @@ -668,76 +257,6 @@ export const hydrateStructureForEditor = (input: any): ComponentModelStructure = } } -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() diff --git a/app/shared/model/componentStructureHydrate.ts b/app/shared/model/componentStructureHydrate.ts new file mode 100644 index 0000000..df7ebad --- /dev/null +++ b/app/shared/model/componentStructureHydrate.ts @@ -0,0 +1,210 @@ +import type { + ComponentModelCustomFieldType, + ComponentModelPiece, + ComponentModelProduct, + ComponentModelStructureNode, +} from '../types/inventory' +import { extractFieldValueObject, toStringArray } from './componentStructureSanitize' + +export 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, + } + }) +} + +export 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 ?? '', + })) +} + +export 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 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, + } + }) +} + +export 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 ?? '', + })) +} + +export 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 ?? '', + })) +} + +export 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, + ), + })) +} diff --git a/app/shared/model/componentStructureSanitize.ts b/app/shared/model/componentStructureSanitize.ts new file mode 100644 index 0000000..ad656b1 --- /dev/null +++ b/app/shared/model/componentStructureSanitize.ts @@ -0,0 +1,312 @@ +import type { + ComponentModelCustomField, + ComponentModelCustomFieldType, + ComponentModelPiece, + ComponentModelProduct, + ComponentModelStructureNode, +} from '../types/inventory' +// Inline helper to avoid circular dependency with componentStructure.ts +const isPlainObject = (value: unknown): value is Record => { + return value !== null && typeof value === 'object' && !Array.isArray(value) +} + +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) +} + +export 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) +}