Files
Inventory_frontend/app/components/StructureNodeEditor.vue
Matthieu ce3f081a0a refactor(category) : remove quantity field from category structure editor
Quantity is now managed per-component on the component edit page,
not at the category level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 13:52:49 +01:00

417 lines
16 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div :class="containerClass">
<div class="border border-base-200 rounded-lg bg-base-100 shadow-sm">
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-base-200 px-4 py-3">
<div class="flex-1 min-w-[220px] space-y-2">
<label class="label">
<span class="label-text text-xs font-semibold">
{{ isRoot ? 'Composant racine de la catégorie' : 'Famille de composant' }}
</span>
</label>
<template v-if="isRoot">
<p class="text-[11px] text-base-content/50">
Le composant racine correspond à la catégorie que vous éditez. Sélectionnez uniquement les familles pour les sous-composants.
</p>
</template>
<template v-else-if="!lockType">
<select
v-model="node.typeComposantId"
class="select select-bordered select-sm w-full"
:disabled="isLocked"
@change="handleComponentTypeSelect(node)"
>
<option value="">
Sélectionner une famille de composant
</option>
<option
v-for="type in componentTypes"
:key="type.id"
:value="type.id"
>
{{ formatComponentTypeOption(type) }}
</option>
</select>
<p class="text-[11px] text-base-content/50">
{{ node.typeComposantId ? `Sélection : ${getComponentTypeLabel(node.typeComposantId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
<div v-if="!isRoot" class="form-control mt-2">
<label class="label py-1">
<span class="label-text text-[11px]">Alias (optionnel)</span>
</label>
<input
v-model="node.alias"
type="text"
class="input input-bordered input-xs"
placeholder="Alias du sous-composant"
:disabled="isLocked"
/>
</div>
</template>
<template v-else>
<div class="input input-bordered input-sm bg-base-200 flex items-center">
{{ lockedTypeDisplay }}
</div>
</template>
</div>
<button
v-if="!isRoot && !isLocked"
type="button"
class="btn btn-error btn-xs btn-square"
@click="emit('remove')"
>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
<div v-else-if="!isRoot && isLocked" class="tooltip tooltip-left" data-tip="Ce sous-composant ne peut pas être supprimé">
<button type="button" class="btn btn-ghost btn-xs btn-square opacity-30 cursor-not-allowed" disabled>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
<div class="px-4 py-4 space-y-5">
<section v-if="isRoot" class="space-y-3">
<h4 :class="headingClass">
{{ isRoot ? 'Champs personnalisés du composant' : 'Champs personnalisés' }}
</h4>
<p v-if="!(node.customFields?.length)" class="text-xs text-base-content/50">
Aucun champ n'a encore été défini.
</p>
<div v-else class="space-y-2">
<div
v-for="(field, index) in node.customFields"
:key="`field-${index}`"
class="border border-base-200 rounded-md p-3 space-y-2 transition-colors"
:class="customFieldReorderClass(index)"
draggable="true"
@dragstart="onCustomFieldDragStart(index, $event)"
@dragenter="onCustomFieldDragEnter(index)"
@dragover.prevent
@drop="onCustomFieldDrop(index)"
@dragend="onCustomFieldDragEnd"
>
<div class="flex items-start justify-between gap-2">
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing"
title="Réorganiser"
draggable="true"
@dragstart.stop="onCustomFieldDragStart(index, $event)"
>
<IconLucideGripVertical class="w-4 h-4" aria-hidden="true" />
</button>
</div>
<div class="flex-1 space-y-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<input
v-model="field.name"
type="text"
class="input input-bordered input-xs"
placeholder="Nom du champ"
/>
<select v-model="field.type" class="select select-bordered select-xs">
<option value="text">Texte</option>
<option value="number">Nombre</option>
<option value="select">Liste</option>
<option value="boolean">Oui/Non</option>
<option value="date">Date</option>
</select>
</div>
<div class="flex items-center gap-2 text-xs">
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" />
Obligatoire
</div>
<textarea
v-if="field.type === 'select'"
v-model="field.optionsText"
class="textarea textarea-bordered textarea-xs h-20"
placeholder="Option 1&#10;Option 2"
></textarea>
</div>
<button
type="button"
class="btn btn-error btn-xs btn-square"
@click="removeCustomField(index)"
>
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-outline btn-xs" @click="addCustomField">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section>
<section v-if="isRoot" class="space-y-3">
<h4 :class="headingClass">
{{ isRoot ? 'Produits inclus par défaut' : 'Produits' }}
</h4>
<p v-if="!(node.products?.length)" class="text-xs text-base-content/50">
Aucun produit défini.
</p>
<div v-else class="space-y-2">
<div
v-for="(product, index) in node.products"
:key="`product-${index}`"
class="relative border border-base-200 rounded-md p-3 pl-10 space-y-3 transition-colors"
:class="productReorderClass(index)"
@dragenter="onProductDragEnter(index)"
@dragover="onProductDragOver"
@drop="onProductDrop(index)"
>
<button
type="button"
class="absolute left-2 top-3 btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing"
draggable="true"
title="Réorganiser"
@dragstart="onProductDragStart(index, $event)"
@dragend="onProductDragEnd"
>
<IconLucideGripVertical class="w-4 h-4" aria-hidden="true" />
</button>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-3">
<div class="form-control">
<label class="label py-1"><span class="label-text text-xs">Famille de produit</span></label>
<select
v-model="product.typeProductId"
class="select select-bordered select-xs"
@change="handleProductTypeSelect(product)"
>
<option value="">
Sélectionner une famille
</option>
<option
v-for="type in productTypes"
:key="type.id"
:value="type.id"
>
{{ formatProductTypeOption(type) }}
</option>
</select>
</div>
</div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removeProduct(index)">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-outline btn-xs" @click="addProduct">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section>
<section v-if="isRoot" class="space-y-3">
<h4 :class="headingClass">
{{ isRoot ? 'Pièces incluses par défaut' : 'Pièces' }}
</h4>
<p v-if="!(node.pieces?.length)" class="text-xs text-base-content/50">
Aucune pièce définie.
</p>
<div v-else class="space-y-2">
<div
v-for="(piece, index) in node.pieces"
:key="`piece-${index}`"
class="relative border border-base-200 rounded-md p-3 pl-10 space-y-3 transition-colors"
:class="pieceReorderClass(index)"
@dragenter="onPieceDragEnter(index)"
@dragover="onPieceDragOver"
@drop="onPieceDrop(index)"
>
<button
type="button"
class="absolute left-2 top-3 btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing"
draggable="true"
title="Réorganiser"
@dragstart="onPieceDragStart(index, $event)"
@dragend="onPieceDragEnd"
>
<IconLucideGripVertical class="w-4 h-4" aria-hidden="true" />
</button>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-3">
<div class="form-control">
<label class="label"><span class="label-text">Famille de pièce</span></label>
<div>
<select
v-model="piece.typePieceId"
class="select select-bordered select-xs"
@change="handlePieceTypeSelect(piece)"
>
<option value="">
Sélectionner une famille
</option>
<option
v-for="type in pieceTypes"
:key="type.id"
:value="type.id"
>
{{ formatPieceTypeOption(type) }}
</option>
</select>
</div>
<p class="mt-1 text-[11px] text-base-content/50">
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
<!-- Quantity is set per-component on the component edit page -->
</div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-outline btn-xs" @click="addPiece">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section>
<section v-if="canManageSubcomponents || hasSubcomponents" class="space-y-3">
<h4 :class="headingClass">Sous-composants</h4>
<p v-if="!isRoot && canManageSubcomponents" class="text-[11px] text-base-content/50">
Sélectionnez uniquement la famille de ce sous-composant ; il sera configuré via son propre modèle.
</p>
<p v-if="!hasSubcomponents" class="text-xs text-base-content/50">
Aucun sous-composant défini.
</p>
<div v-else class="space-y-3">
<div
v-for="(subComponent, index) in node.subcomponents"
:key="`sub-${index}`"
class="relative pl-8 transition-shadow rounded-lg"
:class="subcomponentReorderClass(index)"
@dragenter="onSubcomponentDragEnter(index)"
@dragover="onSubcomponentDragOver"
@drop="onSubcomponentDrop(index)"
>
<button
type="button"
class="absolute left-0 top-4 btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing"
draggable="true"
title="Réorganiser"
@dragstart="onSubcomponentDragStart(index, $event)"
@dragend="onSubcomponentDragEnd"
>
<IconLucideGripVertical class="w-4 h-4" aria-hidden="true" />
</button>
<StructureNodeEditor
:node="subComponent"
:depth="depth + 1"
:component-types="componentTypes"
:piece-types="pieceTypes"
:product-types="productTypes"
:allow-subcomponents="childAllowSubcomponents"
:max-subcomponent-depth="maxSubcomponentDepth"
@remove="removeSubComponent(index)"
/>
</div>
</div>
<button
v-if="canManageSubcomponents"
type="button"
class="btn btn-outline btn-xs"
@click="addSubComponent"
>
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideTrash from '~icons/lucide/trash'
import IconLucideGripVertical from '~icons/lucide/grip-vertical'
import type { EditableStructureNode, ModelTypeOption } from '~/composables/useStructureNodeLogic'
defineOptions({ name: 'StructureNodeEditor' })
const props = withDefaults(defineProps<{
node: EditableStructureNode
depth?: number
componentTypes?: ModelTypeOption[]
pieceTypes?: ModelTypeOption[]
productTypes?: ModelTypeOption[]
isRoot?: boolean
lockType?: boolean
lockedTypeLabel?: string
allowSubcomponents?: boolean
maxSubcomponentDepth?: number
isLocked?: boolean
}>(), {
depth: 0,
componentTypes: () => [],
pieceTypes: () => [],
productTypes: () => [],
isRoot: false,
lockType: false,
lockedTypeLabel: '',
allowSubcomponents: true,
maxSubcomponentDepth: Infinity,
isLocked: false,
})
const emit = defineEmits(['remove'])
const {
isLocked,
componentTypes,
pieceTypes,
productTypes,
canManageSubcomponents,
childAllowSubcomponents,
hasSubcomponents,
containerClass,
headingClass,
lockedTypeDisplay,
getComponentTypeLabel,
getPieceTypeLabel,
formatComponentTypeOption,
formatPieceTypeOption,
formatProductTypeOption,
handleComponentTypeSelect,
handlePieceTypeSelect,
handleProductTypeSelect,
addCustomField,
removeCustomField,
addPiece,
removePiece,
addProduct,
removeProduct,
addSubComponent,
removeSubComponent,
onCustomFieldDragStart,
onCustomFieldDragEnter,
onCustomFieldDrop,
onCustomFieldDragEnd,
customFieldReorderClass,
onPieceDragStart,
onPieceDragEnter,
onPieceDragOver,
onPieceDrop,
onPieceDragEnd,
pieceReorderClass,
onProductDragStart,
onProductDragEnter,
onProductDragOver,
onProductDrop,
onProductDragEnd,
productReorderClass,
onSubcomponentDragStart,
onSubcomponentDragEnter,
onSubcomponentDragOver,
onSubcomponentDrop,
onSubcomponentDragEnd,
subcomponentReorderClass,
} = useStructureNodeLogic(props)
</script>