feat: add product catalogue and product-aware UI
- introduce product catalogue pages, management view entries and shared product composables\n- wire product selection into component/piece flows and machine skeleton requirements\n- display linked product metadata and documents across machine, component and piece views\n- generalize model type tooling to handle PRODUCT category
This commit is contained in:
@@ -3,12 +3,16 @@ import {
|
||||
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'
|
||||
@@ -20,6 +24,7 @@ export const isPlainObject = (value: unknown): value is Record<string, unknown>
|
||||
export interface ModelStructurePreview {
|
||||
customFields: number
|
||||
pieces: number
|
||||
products: number
|
||||
subcomponents: number
|
||||
}
|
||||
|
||||
@@ -37,6 +42,7 @@ const ensureStructureShape = (input: any): 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)
|
||||
@@ -240,6 +246,66 @@ const sanitizePieces = (pieces: any[]): ComponentModelPiece[] => {
|
||||
.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 []
|
||||
@@ -331,6 +397,7 @@ export const normalizeStructureForEditor = (input: any): ComponentModelStructure
|
||||
const result: ComponentModelStructure = {
|
||||
customFields: customFields as ComponentModelCustomField[],
|
||||
pieces: sanitizePieces(source.pieces),
|
||||
products: sanitizeProducts(source.products),
|
||||
subcomponents: hydrateSubcomponents(source.subcomponents),
|
||||
}
|
||||
|
||||
@@ -398,6 +465,20 @@ export const normalizeStructureForSave = (input: any): any => {
|
||||
return payload
|
||||
}) as any
|
||||
|
||||
const backendProducts = sanitizeProducts(source.products).map((product) => {
|
||||
const payload: Record<string, any> = {}
|
||||
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<string, any> = {}
|
||||
if (subcomponent.typeComposantId) {
|
||||
@@ -423,6 +504,7 @@ export const normalizeStructureForSave = (input: any): any => {
|
||||
const result: ComponentModelStructure = {
|
||||
customFields: backendCustomFields,
|
||||
pieces: backendPieces,
|
||||
products: backendProducts,
|
||||
subcomponents: backendSubcomponents,
|
||||
}
|
||||
|
||||
@@ -545,6 +627,20 @@ const hydratePieces = (pieces: any[]): ComponentModelPiece[] => {
|
||||
}))
|
||||
}
|
||||
|
||||
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 []
|
||||
@@ -569,6 +665,7 @@ export const hydrateStructureForEditor = (input: any): ComponentModelStructure =
|
||||
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,
|
||||
),
|
||||
@@ -619,6 +716,19 @@ const mapComponentPieces = (pieces: any[]): ComponentModelPiece[] => {
|
||||
}))
|
||||
}
|
||||
|
||||
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 []
|
||||
@@ -645,6 +755,7 @@ export const extractStructureFromComponent = (component: any) => {
|
||||
const raw = {
|
||||
customFields: mapComponentCustomFields(component.customFields),
|
||||
pieces: mapComponentPieces(component.pieces),
|
||||
products: mapComponentProducts(component.products),
|
||||
subcomponents: mapSubcomponents(
|
||||
Array.isArray(component?.subcomponents)
|
||||
? component.subcomponents
|
||||
@@ -662,12 +773,13 @@ 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, 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)
|
||||
@@ -678,13 +790,14 @@ export const computeStructureStats = (structure: any): ModelStructurePreview =>
|
||||
|
||||
export const formatStructurePreview = (structure: any) => {
|
||||
const stats = computeStructureStats(structure)
|
||||
if (!stats.customFields && !stats.pieces && !stats.subcomponents) {
|
||||
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(' • ')
|
||||
}
|
||||
@@ -741,6 +854,10 @@ export const defaultPieceStructure = (): PieceModelStructure => ({
|
||||
...createEmptyPieceModelStructure(),
|
||||
})
|
||||
|
||||
export const defaultProductStructure = (): ProductModelStructure => ({
|
||||
...createEmptyProductModelStructure(),
|
||||
})
|
||||
|
||||
const ensurePieceStructureShape = (input: any): PieceModelStructure => {
|
||||
const base = createEmptyPieceModelStructure()
|
||||
if (!isPlainObject(input)) {
|
||||
@@ -750,10 +867,11 @@ const ensurePieceStructureShape = (input: any): PieceModelStructure => {
|
||||
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<string, unknown>)) {
|
||||
if (key === 'customFields') {
|
||||
if (key === 'customFields' || key === 'products') {
|
||||
continue
|
||||
}
|
||||
clone[key] = value
|
||||
@@ -771,6 +889,10 @@ export const clonePieceStructure = (input: any): PieceModelStructure => {
|
||||
}
|
||||
}
|
||||
|
||||
export const cloneProductStructure = (input: any): ProductModelStructure => {
|
||||
return clonePieceStructure(input)
|
||||
}
|
||||
|
||||
const sanitizePieceCustomFields = (fields: any[]): PieceModelCustomField[] => {
|
||||
if (!Array.isArray(fields)) {
|
||||
return []
|
||||
@@ -811,12 +933,18 @@ const sanitizePieceCustomFields = (fields: any[]): PieceModelCustomField[] => {
|
||||
.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(
|
||||
Object.entries(source).filter(([key]) => key !== 'customFields'),
|
||||
),
|
||||
...Object.fromEntries(restEntries),
|
||||
products: sanitizePieceProducts(source.products),
|
||||
customFields: sanitizePieceCustomFields(source.customFields),
|
||||
}
|
||||
}
|
||||
@@ -844,8 +972,9 @@ export const hydratePieceStructureForEditor = (input: any): PieceModelStructureF
|
||||
const source = clonePieceStructure(input)
|
||||
const payload: PieceModelStructureForEditor = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(source).filter(([key]) => key !== 'customFields'),
|
||||
Object.entries(source).filter(([key]) => key !== 'customFields' && key !== 'products'),
|
||||
),
|
||||
products: hydrateProducts(source.products) as PieceModelProduct[],
|
||||
customFields: hydratePieceCustomFields(source.customFields),
|
||||
}
|
||||
return payload
|
||||
@@ -859,10 +988,30 @@ export const formatPieceStructurePreview = (structure: any) => {
|
||||
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) {
|
||||
return 'Aucun champ personnalisé'
|
||||
if (!customFields && !products) {
|
||||
return 'Aucun produit ni champ personnalisé'
|
||||
}
|
||||
|
||||
return `${customFields} champ(s) personnalisé(s)`
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user