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

@@ -14,7 +14,7 @@
/>
<!-- Piece Header (collapsible, same pattern as ComponentItem) -->
<div class="flex items-start justify-between p-4 rounded-lg" :class="piece._emptySlot ? 'bg-error/10 border border-error' : 'bg-base-200'">
<div class="flex items-start justify-between p-4 rounded-lg" :class="piece._emptySlot || piece.pendingEntity ? 'bg-error/10 border border-error' : 'bg-base-200'">
<div class="flex items-start gap-3 flex-1 min-w-0">
<button
type="button"
@@ -28,9 +28,18 @@
<span class="sr-only">{{ isCollapsed ? 'Déplier' : 'Replier' }} la pièce</span>
</button>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold" :class="{ 'text-error': piece._emptySlot }">
<h3 class="text-lg font-semibold" :class="{ 'text-error': piece._emptySlot || piece.pendingEntity }">
{{ pieceData.name }}
<span v-if="piece._emptySlot" class="text-sm font-semibold text-error ml-1"> manquant</span>
<button
v-if="piece.pendingEntity"
type="button"
class="badge badge-error badge-sm cursor-pointer hover:badge-outline transition-colors ml-1"
title="Cliquer pour associer un item"
@click.stop="$emit('fill-entity', piece.linkId, piece.modelTypeId)"
>
À remplir
</button>
<span
v-if="displayQuantity > 1"
class="text-sm font-normal text-base-content/60 ml-1"
@@ -77,7 +86,7 @@
</button>
</div>
<div v-show="!isCollapsed" class="space-y-4">
<div v-show="!isCollapsed && !piece.pendingEntity" class="space-y-4">
<div class="p-4 bg-base-100 border border-base-200 rounded-lg">
<div class="space-y-2 text-sm">
<div v-if="isEditMode" class="form-control">
@@ -308,7 +317,7 @@ const props = defineProps({
toggleToken: { type: Number, default: 0 },
})
const emit = defineEmits(['update', 'edit', 'custom-field-update', 'delete'])
const emit = defineEmits(['update', 'edit', 'custom-field-update', 'delete', 'fill-entity'])
// --- Local reactive data for editing ---
const pieceData = reactive({