refactor: adopt canonical component model structure schema
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
import {
|
||||
createEmptyComponentModelStructure,
|
||||
type ComponentModelCustomField,
|
||||
type ComponentModelPiece,
|
||||
type ComponentModelStructure,
|
||||
type ComponentModelStructureNode,
|
||||
} from './types/inventory'
|
||||
|
||||
export const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
@@ -5,24 +13,50 @@ export const isPlainObject = (value: unknown): value is Record<string, unknown>
|
||||
export interface ModelStructurePreview {
|
||||
customFields: number
|
||||
pieces: number
|
||||
subComponents: number
|
||||
subcomponents: number
|
||||
}
|
||||
|
||||
export const defaultStructure = () => ({
|
||||
customFields: [],
|
||||
pieces: [],
|
||||
subComponents: [],
|
||||
export const defaultStructure = (): ComponentModelStructure => ({
|
||||
...createEmptyComponentModelStructure(),
|
||||
})
|
||||
|
||||
export const cloneStructure = (input: any) => {
|
||||
const ensureStructureShape = (input: any): ComponentModelStructure => {
|
||||
const base = createEmptyComponentModelStructure()
|
||||
if (!isPlainObject(input)) {
|
||||
return base
|
||||
}
|
||||
|
||||
const clone: ComponentModelStructure = {
|
||||
...base,
|
||||
customFields: Array.isArray((input as any).customFields) ? (input as any).customFields : [],
|
||||
pieces: Array.isArray((input as any).pieces) ? (input as any).pieces : [],
|
||||
subcomponents: Array.isArray((input as any).subcomponents)
|
||||
? (input as any).subcomponents
|
||||
: Array.isArray((input as any).subComponents)
|
||||
? (input as any).subComponents
|
||||
: [],
|
||||
typeComposantId: typeof (input as any).typeComposantId === 'string' ? (input as any).typeComposantId : undefined,
|
||||
typeComposantLabel: typeof (input as any).typeComposantLabel === 'string'
|
||||
? (input as any).typeComposantLabel
|
||||
: undefined,
|
||||
modelId: typeof (input as any).modelId === 'string' ? (input as any).modelId : undefined,
|
||||
familyCode: typeof (input as any).familyCode === 'string' ? (input as any).familyCode : undefined,
|
||||
alias: typeof (input as any).alias === 'string' ? (input as any).alias : undefined,
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
export const cloneStructure = (input: any): ComponentModelStructure => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(input ?? defaultStructure()))
|
||||
const cloned = JSON.parse(JSON.stringify(input ?? defaultStructure()))
|
||||
return ensureStructureShape(cloned)
|
||||
} catch (error) {
|
||||
return defaultStructure()
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeCustomFields = (fields: any[]): any[] => {
|
||||
const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => {
|
||||
if (!Array.isArray(fields)) {
|
||||
return []
|
||||
}
|
||||
@@ -51,16 +85,16 @@ const sanitizeCustomFields = (fields: any[]): any[] => {
|
||||
options = parsed.length > 0 ? parsed : undefined
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = { name, type, required }
|
||||
const result: ComponentModelCustomField = { name, type, required }
|
||||
if (options) {
|
||||
result.options = options
|
||||
}
|
||||
return result
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter((field): field is ComponentModelCustomField => !!field)
|
||||
}
|
||||
|
||||
const sanitizePieces = (pieces: any[]): any[] => {
|
||||
const sanitizePieces = (pieces: any[]): ComponentModelPiece[] => {
|
||||
if (!Array.isArray(pieces)) {
|
||||
return []
|
||||
}
|
||||
@@ -81,26 +115,18 @@ const sanitizePieces = (pieces: any[]): any[] => {
|
||||
: ''
|
||||
const typePieceLabel = rawTypePieceLabel.length > 0 ? rawTypePieceLabel : undefined
|
||||
|
||||
const rawName = typeof piece?.name === 'string' ? piece.name.trim() : ''
|
||||
const name = rawName || typePieceLabel || ''
|
||||
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
|
||||
if (!typePieceId && !typePieceLabel && !reference) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = { name }
|
||||
const result: ComponentModelPiece = {}
|
||||
if (reference !== undefined) {
|
||||
result.reference = reference
|
||||
}
|
||||
if (normalizedQuantity !== undefined) {
|
||||
result.quantity = normalizedQuantity
|
||||
}
|
||||
if (typePieceId) {
|
||||
result.typePieceId = typePieceId
|
||||
}
|
||||
@@ -109,10 +135,10 @@ const sanitizePieces = (pieces: any[]): any[] => {
|
||||
}
|
||||
return result
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter((piece): piece is ComponentModelPiece => !!piece)
|
||||
}
|
||||
|
||||
const sanitizeSubComponents = (components: any[]): any[] => {
|
||||
const sanitizeSubcomponents = (components: any[]): ComponentModelStructureNode[] => {
|
||||
if (!Array.isArray(components)) {
|
||||
return []
|
||||
}
|
||||
@@ -126,63 +152,68 @@ const sanitizeSubComponents = (components: any[]): any[] => {
|
||||
: ''
|
||||
const typeComposantId = rawTypeComposantId.length > 0 ? rawTypeComposantId : undefined
|
||||
|
||||
const rawTypeComposantLabel = typeof component?.typeComposantLabel === 'string'
|
||||
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()
|
||||
: ''
|
||||
const typeComposantLabel = rawTypeComposantLabel.length > 0 ? rawTypeComposantLabel : undefined
|
||||
|
||||
const rawName = typeof component?.name === 'string' ? component.name.trim() : ''
|
||||
const name = rawName || typeComposantLabel || ''
|
||||
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 result: Record<string, unknown> = {
|
||||
name,
|
||||
}
|
||||
|
||||
if (description !== undefined) {
|
||||
result.description = description
|
||||
}
|
||||
if (normalizedQuantity !== undefined) {
|
||||
result.quantity = normalizedQuantity
|
||||
}
|
||||
if (typeComposantId) {
|
||||
result.typeComposantId = typeComposantId
|
||||
}
|
||||
if (typeComposantLabel) {
|
||||
result.typeComposantLabel = typeComposantLabel
|
||||
}
|
||||
|
||||
const nestedSubComponents = sanitizeSubComponents(component?.subComponents)
|
||||
if (nestedSubComponents.length > 0) {
|
||||
result.subComponents = nestedSubComponents
|
||||
} else {
|
||||
result.subComponents = []
|
||||
if (modelId) {
|
||||
result.modelId = modelId
|
||||
}
|
||||
if (familyCode) {
|
||||
result.familyCode = familyCode
|
||||
}
|
||||
if (alias) {
|
||||
result.alias = alias
|
||||
}
|
||||
|
||||
// Sub components only carry structural info (they will be resolved via their own models)
|
||||
return result
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter((component): component is ComponentModelStructureNode => !!component)
|
||||
}
|
||||
|
||||
export const normalizeStructureForSave = (input: any) => {
|
||||
export const normalizeStructureForSave = (input: any): ComponentModelStructure => {
|
||||
const source = cloneStructure(input)
|
||||
|
||||
return {
|
||||
customFields: sanitizeCustomFields(source.customFields),
|
||||
pieces: sanitizePieces(source.pieces),
|
||||
subComponents: sanitizeSubComponents(source.subComponents),
|
||||
subcomponents: sanitizeSubcomponents(source.subcomponents),
|
||||
typeComposantId: source.typeComposantId,
|
||||
typeComposantLabel: source.typeComposantLabel,
|
||||
modelId: source.modelId,
|
||||
familyCode: source.familyCode,
|
||||
alias: source.alias,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,43 +231,50 @@ const hydrateCustomFields = (fields: any[]): any[] => {
|
||||
}))
|
||||
}
|
||||
|
||||
const hydratePieces = (pieces: any[]): any[] => {
|
||||
const hydratePieces = (pieces: any[]): ComponentModelPiece[] => {
|
||||
if (!Array.isArray(pieces)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return pieces.map((piece) => ({
|
||||
name: piece?.name ?? piece?.typePiece?.name ?? piece?.typePieceLabel ?? '',
|
||||
reference: piece?.reference ?? '',
|
||||
quantity: piece?.quantity ?? piece?.quantite ?? undefined,
|
||||
typePieceId: piece?.typePieceId ?? piece?.typePiece?.id ?? '',
|
||||
typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '',
|
||||
reference: piece?.reference ?? '',
|
||||
}))
|
||||
}
|
||||
|
||||
const hydrateSubComponents = (components: any[]): any[] => {
|
||||
const hydrateSubcomponents = (components: any[]): ComponentModelStructureNode[] => {
|
||||
if (!Array.isArray(components)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return components.map((component) => ({
|
||||
name: component?.name ?? component?.typeComposant?.name ?? component?.typeComposantLabel ?? '',
|
||||
description: component?.description ?? '',
|
||||
quantity: component?.quantity ?? component?.quantite ?? undefined,
|
||||
typeComposantId: component?.typeComposantId ?? component?.typeComposant?.id ?? '',
|
||||
typeComposantLabel: component?.typeComposantLabel ?? component?.typeComposant?.name ?? '',
|
||||
customFields: [],
|
||||
pieces: [],
|
||||
subComponents: hydrateSubComponents(component?.subComponents),
|
||||
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) => {
|
||||
export const hydrateStructureForEditor = (input: any): ComponentModelStructure => {
|
||||
const source = cloneStructure(input)
|
||||
return {
|
||||
customFields: hydrateCustomFields(source.customFields),
|
||||
pieces: hydratePieces(source.pieces),
|
||||
subComponents: hydrateSubComponents(source.subComponents),
|
||||
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 ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,32 +301,32 @@ const mapComponentCustomFields = (fields: any[]) => {
|
||||
}))
|
||||
}
|
||||
|
||||
const mapComponentPieces = (pieces: any[]) => {
|
||||
const mapComponentPieces = (pieces: any[]): ComponentModelPiece[] => {
|
||||
if (!Array.isArray(pieces)) {
|
||||
return []
|
||||
}
|
||||
return pieces.map((piece) => ({
|
||||
name: piece?.name ?? piece?.typePiece?.name ?? '',
|
||||
reference: piece?.reference ?? '',
|
||||
quantity: piece?.quantity ?? piece?.quantite ?? undefined,
|
||||
typePieceId: piece?.typePieceId ?? piece?.typePiece?.id ?? '',
|
||||
typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '',
|
||||
}))
|
||||
}
|
||||
|
||||
const mapSubComponents = (components: any[]): any[] => {
|
||||
const mapSubcomponents = (components: any[]): ComponentModelStructureNode[] => {
|
||||
if (!Array.isArray(components)) {
|
||||
return []
|
||||
}
|
||||
return components.map((component) => ({
|
||||
name: component?.name ?? component?.typeComposant?.name ?? '',
|
||||
description: component?.description ?? '',
|
||||
quantity: component?.quantity ?? component?.quantite ?? undefined,
|
||||
typeComposantId: component?.typeComposantId ?? component?.typeComposant?.id ?? '',
|
||||
typeComposantLabel: component?.typeComposantLabel ?? component?.typeComposant?.name ?? '',
|
||||
customFields: [],
|
||||
pieces: [],
|
||||
subComponents: mapSubComponents(component?.subComponents),
|
||||
modelId: component?.modelId ?? '',
|
||||
familyCode: component?.familyCode ?? component?.typeComposant?.code ?? '',
|
||||
alias: component?.alias ?? component?.name ?? '',
|
||||
subcomponents: mapSubcomponents(
|
||||
Array.isArray(component?.subcomponents)
|
||||
? component.subcomponents
|
||||
: component?.subComponents,
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -300,7 +338,16 @@ export const extractStructureFromComponent = (component: any) => {
|
||||
const raw = {
|
||||
customFields: mapComponentCustomFields(component.customFields),
|
||||
pieces: mapComponentPieces(component.pieces),
|
||||
subComponents: mapSubComponents(component.subComponents),
|
||||
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 normalizeStructureForSave(raw)
|
||||
@@ -308,25 +355,29 @@ export const extractStructureFromComponent = (component: any) => {
|
||||
|
||||
export const computeStructureStats = (structure: any): ModelStructurePreview => {
|
||||
if (!structure || typeof structure !== 'object') {
|
||||
return { customFields: 0, pieces: 0, subComponents: 0 }
|
||||
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,
|
||||
subcomponents: Array.isArray(structure.subcomponents)
|
||||
? structure.subcomponents.length
|
||||
: Array.isArray(structure.subComponents)
|
||||
? structure.subComponents.length
|
||||
: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const formatStructurePreview = (structure: any) => {
|
||||
const stats = computeStructureStats(structure)
|
||||
if (!stats.customFields && !stats.pieces && !stats.subComponents) {
|
||||
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)`)
|
||||
if (stats.subcomponents) segments.push(`${stats.subcomponents} sous-composant(s)`)
|
||||
return segments.join(' • ')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user