export const isPlainObject = (value: unknown): value is Record => { return value !== null && typeof value === 'object' && !Array.isArray(value) } export interface ModelStructurePreview { customFields: number pieces: number subComponents: number } export const defaultStructure = () => ({ customFields: [], pieces: [], subComponents: [], }) export const cloneStructure = (input: any) => { try { return JSON.parse(JSON.stringify(input ?? defaultStructure())) } catch (error) { return defaultStructure() } } const sanitizeCustomFields = (fields: any[]): any[] => { if (!Array.isArray(fields)) { return [] } return fields .map((field) => { 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 const defaultValue = typeof field?.defaultValue === 'string' && field.defaultValue.trim().length > 0 ? field.defaultValue.trim() : undefined let options: string[] | undefined if (type === 'select') { const rawOptions = typeof field?.optionsText === 'string' ? field.optionsText : Array.isArray(field?.options) ? field.options.join('\n') : '' const parsed = rawOptions .split(/\r?\n/) .map((option) => option.trim()) .filter((option) => option.length > 0) options = parsed.length > 0 ? parsed : undefined } const result: Record = { name, type, required } if (defaultValue !== undefined) { result.defaultValue = defaultValue } if (options) { result.options = options } return result }) .filter(Boolean) } const sanitizePieces = (pieces: any[]): any[] => { if (!Array.isArray(pieces)) { return [] } return pieces .map((piece) => { const name = typeof piece?.name === 'string' ? piece.name.trim() : '' if (!name) { return null } const reference = typeof piece?.reference === 'string' && piece.reference.trim().length > 0 ? piece.reference.trim() : undefined const quantity = Number(piece?.quantity) const normalizedQuantity = Number.isFinite(quantity) && quantity > 0 ? quantity : undefined const result: Record = { name } if (reference !== undefined) { result.reference = reference } if (normalizedQuantity !== undefined) { result.quantity = normalizedQuantity } return result }) .filter(Boolean) } const sanitizeSubComponents = (components: any[]): any[] => { if (!Array.isArray(components)) { return [] } return components .map((component) => { const name = typeof component?.name === 'string' ? component.name.trim() : '' if (!name) { return null } const description = typeof component?.description === 'string' && component.description.trim().length > 0 ? component.description.trim() : undefined const quantity = Number(component?.quantity) const normalizedQuantity = Number.isFinite(quantity) && quantity > 0 ? quantity : undefined const customFields = sanitizeCustomFields(component?.customFields) const pieces = sanitizePieces(component?.pieces) const subComponents = sanitizeSubComponents(component?.subComponents) const result: Record = { name, customFields, pieces, subComponents, } if (description !== undefined) { result.description = description } if (normalizedQuantity !== undefined) { result.quantity = normalizedQuantity } return result }) .filter(Boolean) } export const normalizeStructureForSave = (input: any) => { const source = cloneStructure(input) return { customFields: sanitizeCustomFields(source.customFields), pieces: sanitizePieces(source.pieces), subComponents: sanitizeSubComponents(source.subComponents), } } const hydrateCustomFields = (fields: any[]): any[] => { if (!Array.isArray(fields)) { return [] } return fields.map((field) => ({ name: field?.name ?? '', type: field?.type ?? 'text', required: !!field?.required, defaultValue: field?.defaultValue ?? '', options: Array.isArray(field?.options) ? field.options : [], optionsText: Array.isArray(field?.options) ? field.options.join('\n') : (field?.optionsText ?? ''), })) } const hydratePieces = (pieces: any[]): any[] => { if (!Array.isArray(pieces)) { return [] } return pieces.map((piece) => ({ name: piece?.name ?? '', reference: piece?.reference ?? '', quantity: piece?.quantity ?? undefined, })) } const hydrateSubComponents = (components: any[]): any[] => { if (!Array.isArray(components)) { return [] } return components.map((component) => ({ name: component?.name ?? '', description: component?.description ?? '', quantity: component?.quantity ?? undefined, customFields: hydrateCustomFields(component?.customFields), pieces: hydratePieces(component?.pieces), subComponents: hydrateSubComponents(component?.subComponents), })) } export const hydrateStructureForEditor = (input: any) => { const source = cloneStructure(input) return { customFields: hydrateCustomFields(source.customFields), pieces: hydratePieces(source.pieces), subComponents: hydrateSubComponents(source.subComponents), } } const toOptionsText = (field: any) => { if (typeof field?.optionsText === 'string') { return field.optionsText } if (Array.isArray(field?.options)) { return field.options.join('\n') } return '' } const mapComponentCustomFields = (fields: any[]) => { if (!Array.isArray(fields)) { return [] } return fields.map((field) => ({ name: field?.name ?? '', type: field?.type ?? 'text', required: !!field?.required, defaultValue: field?.defaultValue ?? '', options: Array.isArray(field?.options) ? field.options : [], optionsText: toOptionsText(field), })) } const mapComponentPieces = (pieces: any[]) => { if (!Array.isArray(pieces)) { return [] } return pieces.map((piece) => ({ name: piece?.name ?? '', reference: piece?.reference ?? '', quantity: piece?.quantity ?? piece?.quantite ?? undefined, })) } const mapSubComponents = (components: any[]): any[] => { if (!Array.isArray(components)) { return [] } return components.map((component) => ({ name: component?.name ?? '', description: component?.description ?? '', quantity: component?.quantity ?? component?.quantite ?? undefined, customFields: mapComponentCustomFields(component?.customFields), pieces: mapComponentPieces(component?.pieces), subComponents: mapSubComponents(component?.subComponents), })) } export const extractStructureFromComponent = (component: any) => { if (!component) { return defaultStructure() } const raw = { customFields: mapComponentCustomFields(component.customFields), pieces: mapComponentPieces(component.pieces), subComponents: mapSubComponents(component.subComponents), } return normalizeStructureForSave(raw) } export const computeStructureStats = (structure: any): ModelStructurePreview => { if (!structure || typeof structure !== 'object') { return { customFields: 0, pieces: 0, subComponents: 0 } } return { customFields: Array.isArray(structure.customFields) ? structure.customFields.length : 0, pieces: Array.isArray(structure.pieces) ? structure.pieces.length : 0, subComponents: Array.isArray(structure.subComponents) ? structure.subComponents.length : 0, } } export const formatStructurePreview = (structure: any) => { const stats = computeStructureStats(structure) if (!stats.customFields && !stats.pieces && !stats.subComponents) { return 'Structure vide' } const segments: string[] = [] if (stats.customFields) segments.push(`${stats.customFields} champ(s)`) if (stats.pieces) segments.push(`${stats.pieces} pièce(s)`) if (stats.subComponents) segments.push(`${stats.subComponents} sous-composant(s)`) return segments.join(' • ') }