diff --git a/app/shared/model/componentStructure.ts b/app/shared/model/componentStructure.ts new file mode 100644 index 0000000..d4192f4 --- /dev/null +++ b/app/shared/model/componentStructure.ts @@ -0,0 +1,794 @@ +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(' • ') +} diff --git a/app/shared/model/definitionOverrides.ts b/app/shared/model/definitionOverrides.ts new file mode 100644 index 0000000..b309f52 --- /dev/null +++ b/app/shared/model/definitionOverrides.ts @@ -0,0 +1,49 @@ +import { uniqueConstructeurIds } from '../constructeurUtils' + +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 +} diff --git a/app/shared/model/pieceProductStructure.ts b/app/shared/model/pieceProductStructure.ts new file mode 100644 index 0000000..a7dff65 --- /dev/null +++ b/app/shared/model/pieceProductStructure.ts @@ -0,0 +1,177 @@ +import { + createEmptyPieceModelStructure, + createEmptyProductModelStructure, + type PieceModelCustomField, + type PieceModelProduct, + type PieceModelStructure, + type PieceModelStructureEditorField, + type PieceModelStructureForEditor, + type ProductModelStructure, +} from '../types/inventory' +import { isPlainObject, sanitizeProducts, hydrateProducts } from './componentStructure' + +export const defaultPieceStructure = (): PieceModelStructure => ({ + ...createEmptyPieceModelStructure(), +}) + +export const defaultProductStructure = (): ProductModelStructure => ({ + ...createEmptyProductModelStructure(), +}) + +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 : [], + products: Array.isArray((input as any).products) ? (input as any).products : [], + } + + for (const [key, value] of Object.entries(input as Record)) { + if (key === 'customFields' || key === 'products') { + 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() + } +} + +export const cloneProductStructure = (input: any): ProductModelStructure => { + return clonePieceStructure(input) +} + +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: string) => option.trim()) + .filter((option: string) => 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) +} + +const sanitizePieceProducts = (products: any[]): PieceModelProduct[] => { + return sanitizeProducts(products) as PieceModelProduct[] +} + +export const normalizePieceStructureForSave = (input: any): PieceModelStructure => { + const source = clonePieceStructure(input) + const restEntries = Object.entries(source).filter( + ([key]) => key !== 'customFields' && key !== 'products', + ) + return { + ...Object.fromEntries(restEntries), + products: sanitizePieceProducts(source.products || []), + 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' && key !== 'products'), + ), + products: hydrateProducts(source.products || []) as PieceModelProduct[], + 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 + const products = Array.isArray((structure as any).products) + ? (structure as any).products.length + : 0 + + if (!customFields && !products) { + return 'Aucun produit ni champ personnalisé' + } + + const segments: string[] = [] + if (products) { + segments.push(`${products} produit(s)`) + } + if (customFields) { + segments.push(`${customFields} champ(s) personnalisé(s)`) + } + + return segments.join(' · ') +} + +export const normalizeProductStructureForSave = (input: any): ProductModelStructure => + normalizePieceStructureForSave(input) + +export const hydrateProductStructureForEditor = (input: any) => + hydratePieceStructureForEditor(input) + +export const formatProductStructurePreview = (structure: any) => + formatPieceStructurePreview(structure) diff --git a/app/shared/modelUtils.ts b/app/shared/modelUtils.ts index 0da7b3f..02cf7dd 100644 --- a/app/shared/modelUtils.ts +++ b/app/shared/modelUtils.ts @@ -1,1017 +1,36 @@ -import { - createEmptyComponentModelStructure, - type ComponentModelCustomFieldType, - type ComponentModelCustomField, - type ComponentModelPiece, - type ComponentModelProduct, - type ComponentModelStructure, - type ComponentModelStructureNode, - type PieceModelCustomField, - type PieceModelProduct, - type PieceModelStructure, - type PieceModelStructureEditorField, - type PieceModelStructureForEditor, - type ProductModelStructure, - createEmptyProductModelStructure, - 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 - 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() - } -} - -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: 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) -} - -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 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) -} - -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 -} - -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 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 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(' • ') -} - -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(), -}) - -export const defaultProductStructure = (): ProductModelStructure => ({ - ...createEmptyProductModelStructure(), -}) - -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 : [], - products: Array.isArray((input as any).products) ? (input as any).products : [], - } - - for (const [key, value] of Object.entries(input as Record)) { - if (key === 'customFields' || key === 'products') { - 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() - } -} - -export const cloneProductStructure = (input: any): ProductModelStructure => { - return clonePieceStructure(input) -} - -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: string) => option.trim()) - .filter((option: string) => 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) -} - -const sanitizePieceProducts = (products: any[]): PieceModelProduct[] => { - return sanitizeProducts(products) as PieceModelProduct[] -} - -export const normalizePieceStructureForSave = (input: any): PieceModelStructure => { - const source = clonePieceStructure(input) - const restEntries = Object.entries(source).filter( - ([key]) => key !== 'customFields' && key !== 'products', - ) - return { - ...Object.fromEntries(restEntries), - products: sanitizePieceProducts(source.products || []), - 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' && key !== 'products'), - ), - products: hydrateProducts(source.products || []) as PieceModelProduct[], - 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 - const products = Array.isArray((structure as any).products) - ? (structure as any).products.length - : 0 - - if (!customFields && !products) { - return 'Aucun produit ni champ personnalisé' - } - - const segments: string[] = [] - if (products) { - segments.push(`${products} produit(s)`) - } - if (customFields) { - segments.push(`${customFields} champ(s) personnalisé(s)`) - } - - return segments.join(' · ') -} - -export const normalizeProductStructureForSave = (input: any): ProductModelStructure => - normalizePieceStructureForSave(input) - -export const hydrateProductStructureForEditor = (input: any) => - hydratePieceStructureForEditor(input) - -export const formatProductStructurePreview = (structure: any) => - formatPieceStructurePreview(structure) +export { + isPlainObject, + defaultStructure, + cloneStructure, + toStringArray, + extractFieldValueObject, + sanitizeCustomFields, + sanitizePieces, + sanitizeProducts, + hydrateProducts, + normalizeStructureForEditor, + normalizeStructureForSave, + hydrateStructureForEditor, + extractStructureFromComponent, + computeStructureStats, + formatStructurePreview, + type ModelStructurePreview, +} from './model/componentStructure' + +export { + defaultPieceStructure, + defaultProductStructure, + clonePieceStructure, + cloneProductStructure, + normalizePieceStructureForSave, + hydratePieceStructureForEditor, + formatPieceStructurePreview, + normalizeProductStructureForSave, + hydrateProductStructureForEditor, + formatProductStructurePreview, +} from './model/pieceProductStructure' + +export { + sanitizeDefinitionOverrides, + type DefinitionOverridePayload, +} from './model/definitionOverrides'