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 @@
/>
<!-- Component Header -->
<div class="flex items-center gap-3 p-3 bg-base-200 rounded-lg cursor-pointer" @click="toggleCollapse">
<div class="flex items-center gap-3 p-3 rounded-lg cursor-pointer" :class="component.pendingEntity ? 'bg-error/10 border border-error' : 'bg-base-200'" @click="toggleCollapse">
<IconLucideChevronRight
class="w-4 h-4 shrink-0 transition-transform text-base-content/50"
:class="{ 'rotate-90': !isCollapsed }"
@@ -22,9 +22,18 @@
/>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<h3 class="text-sm font-semibold text-base-content truncate">
<h3 class="text-sm font-semibold truncate" :class="component.pendingEntity ? 'text-error' : 'text-base-content'">
{{ component.name }}
</h3>
<button
v-if="component.pendingEntity"
type="button"
class="badge badge-error badge-sm cursor-pointer hover:badge-outline transition-colors"
title="Cliquer pour associer un item"
@click.stop="$emit('fill-entity', component.linkId, component.modelTypeId)"
>
À remplir
</button>
<span v-if="component.reference" class="badge badge-outline badge-xs">{{ component.reference }}</span>
<span v-if="component.prix" class="badge badge-primary badge-xs">{{ component.prix }}</span>
</div>
@@ -54,7 +63,7 @@
</div>
<!-- Expanded content -->
<div v-show="!isCollapsed" class="mt-3 space-y-4 pl-7">
<div v-show="!isCollapsed && !component.pendingEntity" class="mt-3 space-y-4 pl-7">
<!-- Info fields -->
<div v-if="isEditMode" class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control">
@@ -241,6 +250,7 @@
@update="updatePiece"
@edit="editPiece"
@custom-field-update="updatePieceCustomField"
@fill-entity="(linkId, typeId) => $emit('fill-entity', linkId, typeId)"
/>
</div>
</div>
@@ -276,6 +286,7 @@
@update="$emit('update', $event)"
@edit-piece="$emit('edit-piece', $event)"
@custom-field-update="$emit('custom-field-update', $event)"
@fill-entity="(linkId, typeId) => $emit('fill-entity', linkId, typeId)"
/>
</div>
</div>
@@ -317,7 +328,7 @@ const props = defineProps({
toggleToken: { type: Number, default: 0 },
})
const emit = defineEmits(['update', 'edit-piece', 'custom-field-update', 'delete'])
const emit = defineEmits(['update', 'edit-piece', 'custom-field-update', 'delete', 'fill-entity'])
// --- Shared composables ---
const {