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:
2026-04-03 10:15:47 +02:00
parent 342ae37762
commit 1c3b566923
20 changed files with 452 additions and 77 deletions

View File

@@ -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,
}
}