refactor : merge Inventory_frontend submodule into frontend/ directory
Merges the full git history of Inventory_frontend into the monorepo under frontend/. Removes the submodule in favor of a unified repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
416
frontend/app/components/StructureNodeEditor.vue
Normal file
416
frontend/app/components/StructureNodeEditor.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<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 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>
|
||||
Reference in New Issue
Block a user