feat(machines) : allow category-only links on machine structure
Enable adding a component, piece, or product to a machine by selecting only the category (ModelType) without a specific entity. The link displays a red "À remplir" badge; clicking it reopens the modal pre-filled with the category so the user can associate an item later. Backend: entity FKs made nullable on the 3 link tables, modelType FK added, controller/audit/version/MCP normalization adapted for null entities. Frontend: modal accepts category-only confirm, page handles fill mode, hierarchy builder creates pending nodes, display components show clickable badge with event propagation through the full hierarchy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,10 @@ export function useMachineDetailData(machineId: string) {
|
||||
removePieceLink,
|
||||
addProductLink,
|
||||
removeProductLink,
|
||||
addComponentLinkCategoryOnly,
|
||||
addPieceLinkCategoryOnly,
|
||||
addProductLinkCategoryOnly,
|
||||
fillEntityLink,
|
||||
} = hierarchy
|
||||
|
||||
// Keep the product links proxy in sync with the hierarchy's machineProductLinks
|
||||
@@ -511,6 +515,8 @@ export function useMachineDetailData(machineId: string) {
|
||||
loadMachineData, loadInitialData,
|
||||
addComponentLink, removeComponentLink, addPieceLink, removePieceLink,
|
||||
addProductLink, removeProductLink, reloadMachineStructure,
|
||||
addComponentLinkCategoryOnly, addPieceLinkCategoryOnly,
|
||||
addProductLinkCategoryOnly, fillEntityLink,
|
||||
|
||||
// External
|
||||
constructeurs, loadProducts, updateMachineStructure, toast,
|
||||
|
||||
@@ -39,7 +39,7 @@ export function useMachineDetailHierarchy(deps: MachineDetailHierarchyDeps) {
|
||||
syncMachineCustomFields,
|
||||
} = deps
|
||||
|
||||
const { get, post: apiPost, delete: apiDel } = useApi()
|
||||
const { get, post: apiPost, delete: apiDel, patch: apiPatch } = useApi()
|
||||
const toast = useToast()
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -263,6 +263,69 @@ export function useMachineDetailHierarchy(deps: MachineDetailHierarchyDeps) {
|
||||
return result
|
||||
}
|
||||
|
||||
const addComponentLinkCategoryOnly = async (modelTypeId: string) => {
|
||||
const result: any = await apiPost('/machine_component_links', {
|
||||
machine: `/api/machines/${machineId}`,
|
||||
modelType: `/api/model_types/${modelTypeId}`,
|
||||
})
|
||||
if (result.success) {
|
||||
toast.showSuccess('Catégorie ajoutée — item à remplir')
|
||||
await reloadMachineStructure()
|
||||
} else {
|
||||
toast.showError('Erreur lors de l\'ajout')
|
||||
}
|
||||
}
|
||||
|
||||
const addPieceLinkCategoryOnly = async (modelTypeId: string) => {
|
||||
const result: any = await apiPost('/machine_piece_links', {
|
||||
machine: `/api/machines/${machineId}`,
|
||||
modelType: `/api/model_types/${modelTypeId}`,
|
||||
})
|
||||
if (result.success) {
|
||||
toast.showSuccess('Catégorie ajoutée — item à remplir')
|
||||
await reloadMachineStructure()
|
||||
} else {
|
||||
toast.showError('Erreur lors de l\'ajout')
|
||||
}
|
||||
}
|
||||
|
||||
const addProductLinkCategoryOnly = async (modelTypeId: string) => {
|
||||
const result: any = await apiPost('/machine_product_links', {
|
||||
machine: `/api/machines/${machineId}`,
|
||||
modelType: `/api/model_types/${modelTypeId}`,
|
||||
})
|
||||
if (result.success) {
|
||||
toast.showSuccess('Catégorie ajoutée — item à remplir')
|
||||
await reloadMachineStructure()
|
||||
} else {
|
||||
toast.showError('Erreur lors de l\'ajout')
|
||||
}
|
||||
}
|
||||
|
||||
const fillEntityLink = async (linkId: string, entityId: string, entityKind: string) => {
|
||||
let endpoint = ''
|
||||
let payload: Record<string, string> = {}
|
||||
|
||||
if (entityKind === 'component') {
|
||||
endpoint = `/machine_component_links/${linkId}`
|
||||
payload = { composant: `/api/composants/${entityId}` }
|
||||
} else if (entityKind === 'piece') {
|
||||
endpoint = `/machine_piece_links/${linkId}`
|
||||
payload = { piece: `/api/pieces/${entityId}` }
|
||||
} else {
|
||||
endpoint = `/machine_product_links/${linkId}`
|
||||
payload = { product: `/api/products/${entityId}` }
|
||||
}
|
||||
|
||||
const result: any = await apiPatch(endpoint, payload)
|
||||
if (result.success) {
|
||||
toast.showSuccess('Item associé avec succès')
|
||||
await reloadMachineStructure()
|
||||
} else {
|
||||
toast.showError('Erreur lors de l\'association')
|
||||
}
|
||||
}
|
||||
|
||||
const removeProductLink = async (linkId: string) => {
|
||||
const result: any = await apiDel(`/machine_product_links/${linkId}`)
|
||||
if (result.success) {
|
||||
@@ -301,6 +364,10 @@ export function useMachineDetailHierarchy(deps: MachineDetailHierarchyDeps) {
|
||||
addPieceLink,
|
||||
removePieceLink,
|
||||
addProductLink,
|
||||
addComponentLinkCategoryOnly,
|
||||
addPieceLinkCategoryOnly,
|
||||
addProductLinkCategoryOnly,
|
||||
fillEntityLink,
|
||||
removeProductLink,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export function useMachineDetailProducts(deps: MachineDetailProductsDeps) {
|
||||
return {
|
||||
id: (resolved?.id as string) || productId || null,
|
||||
linkId: (link.id as string) || (typeof link['@id'] === 'string' ? link['@id'].split('/').pop() : null) || null,
|
||||
name: (resolved?.name as string) || 'Produit inconnu',
|
||||
name: (resolved?.name as string) || (link.modelType as AnyRecord)?.name as string || 'Produit inconnu',
|
||||
reference: (resolved?.reference as string) || null,
|
||||
supplierLabel: resolvedConstructeurs.length
|
||||
? resolvedConstructeurs.map((c) => c.name).filter(Boolean).join(', ') || null
|
||||
@@ -111,6 +111,9 @@ export function useMachineDetailProducts(deps: MachineDetailProductsDeps) {
|
||||
priceLabel: resolved ? getProductPriceLabel(resolved) : null,
|
||||
groupLabel: ((resolved?.typeProduct as AnyRecord)?.name as string) || '',
|
||||
documents: productId ? (productDocumentsMap.value.get(productId) || []) : [],
|
||||
pendingEntity: (link.pendingEntity as boolean) || false,
|
||||
modelTypeId: (link.modelTypeId as string) || null,
|
||||
modelType: (link.modelType as string) || null,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -150,6 +150,30 @@ export const buildMachineHierarchyFromLinks = (
|
||||
const createPieceNode = (link: AnyRecord, parentComponentName: string | null = null): AnyRecord | null => {
|
||||
if (!link || typeof link !== 'object') return null
|
||||
|
||||
// Category-only link (no entity yet)
|
||||
if (link.pendingEntity || (!link.piece && !link.pieceId)) {
|
||||
const machinePieceLinkId = normalizePieceLinkId(link)
|
||||
const mt = (link.modelType || null) as AnyRecord | null
|
||||
return {
|
||||
id: machinePieceLinkId || `pending-${link.id}`,
|
||||
linkId: machinePieceLinkId,
|
||||
name: mt?.name || 'Catégorie sans item',
|
||||
reference: null,
|
||||
prix: null,
|
||||
pendingEntity: true,
|
||||
modelTypeId: link.modelTypeId || mt?.id || null,
|
||||
modelType: mt,
|
||||
pieceId: null,
|
||||
constructeurs: [],
|
||||
documents: [],
|
||||
customFields: [],
|
||||
parentComponentLinkId: link.parentComponentLinkId || link.parentLinkId || null,
|
||||
parentComponentName,
|
||||
machinePieceLinkId,
|
||||
quantity: 1,
|
||||
}
|
||||
}
|
||||
|
||||
const appliedPiece = (link.piece && typeof link.piece === 'object' ? link.piece : {}) as AnyRecord
|
||||
const originalPiece = (link.originalPiece && typeof link.originalPiece === 'object' ? link.originalPiece : null) as AnyRecord | null
|
||||
|
||||
@@ -205,6 +229,35 @@ export const buildMachineHierarchyFromLinks = (
|
||||
const createComponentNode = (link: AnyRecord): AnyRecord | null => {
|
||||
if (!link || typeof link !== 'object') return null
|
||||
|
||||
// Category-only link (no entity yet)
|
||||
if (link.pendingEntity || (!link.composant && !link.composantId)) {
|
||||
const machineComponentLinkId = normalizeComponentLinkId(link)
|
||||
const mt = (link.modelType || null) as AnyRecord | null
|
||||
return {
|
||||
id: machineComponentLinkId || `pending-${link.id}`,
|
||||
linkId: machineComponentLinkId,
|
||||
name: mt?.name || 'Catégorie sans item',
|
||||
reference: null,
|
||||
prix: null,
|
||||
pendingEntity: true,
|
||||
modelTypeId: link.modelTypeId || mt?.id || null,
|
||||
modelType: mt,
|
||||
composantId: null,
|
||||
composant: null,
|
||||
constructeurs: [],
|
||||
documents: [],
|
||||
customFields: [],
|
||||
customFieldValues: [],
|
||||
subComponents: [],
|
||||
pieces: [],
|
||||
overrides: null,
|
||||
parentComponentLinkId: link.parentComponentLinkId || link.parentLinkId || null,
|
||||
machineComponentLinkId,
|
||||
childLinks: [],
|
||||
pieceLinks: [],
|
||||
}
|
||||
}
|
||||
|
||||
const appliedComponent = (link.composant && typeof link.composant === 'object' ? link.composant : {}) as AnyRecord
|
||||
const originalComponent = (link.originalComposant && typeof link.originalComposant === 'object' ? link.originalComposant : null) as AnyRecord | null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user