261 lines
8.7 KiB
Vue
261 lines
8.7 KiB
Vue
<template>
|
|
<div class="border border-base-200 rounded-lg bg-base-100" :class="depthPadding">
|
|
<div class="flex items-start justify-between gap-3 border-b border-base-200 px-4 py-3">
|
|
<div class="flex flex-col gap-1 flex-1">
|
|
<div class="flex items-center gap-2">
|
|
<button type="button" class="btn btn-ghost btn-xs" @click="toggle">
|
|
<IconLucideChevronRight
|
|
class="w-4 h-4 transition-transform"
|
|
:class="{ 'rotate-90': expanded }"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
<input
|
|
v-model="node.name"
|
|
type="text"
|
|
class="input input-sm input-bordered w-full"
|
|
placeholder="Nom du sous-composant"
|
|
/>
|
|
</div>
|
|
<span v-if="!expanded && node.description" class="text-xs text-gray-500 truncate">
|
|
{{ node.description }}
|
|
</span>
|
|
</div>
|
|
<button type="button" class="btn btn-error btn-xs btn-square" @click="emit('remove')">
|
|
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="expanded" class="space-y-5 px-4 py-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Description</span></label>
|
|
<textarea
|
|
v-model="node.description"
|
|
class="textarea textarea-bordered textarea-sm"
|
|
rows="2"
|
|
placeholder="Notes optionnelles"
|
|
></textarea>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Quantité</span></label>
|
|
<input
|
|
v-model.number="node.quantity"
|
|
type="number"
|
|
min="0"
|
|
step="1"
|
|
class="input input-bordered input-sm"
|
|
placeholder="Quantité (optionnel)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="space-y-2">
|
|
<h4 class="text-sm font-semibold">Champs personnalisés</h4>
|
|
<p v-if="!(node.customFields?.length)" class="text-xs text-gray-500">
|
|
Aucun champ défini.
|
|
</p>
|
|
<div v-else class="space-y-2">
|
|
<div
|
|
v-for="(field, fieldIndex) in node.customFields"
|
|
:key="`sub-${depth}-${fieldIndex}`"
|
|
class="border border-base-200 rounded-md p-3 space-y-2"
|
|
>
|
|
<div class="flex items-start justify-between gap-2">
|
|
<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="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
<label class="flex items-center gap-2 text-xs">
|
|
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" />
|
|
Obligatoire
|
|
</label>
|
|
<input
|
|
v-model="field.defaultValue"
|
|
type="text"
|
|
class="input input-bordered input-xs"
|
|
placeholder="Valeur par défaut"
|
|
/>
|
|
</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(fieldIndex)">
|
|
<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 un champ
|
|
</button>
|
|
</section>
|
|
|
|
<section class="space-y-2">
|
|
<h4 class="text-sm font-semibold">Pièces associées</h4>
|
|
<p v-if="!(node.pieces?.length)" class="text-xs text-gray-500">Aucune pièce définie.</p>
|
|
<div v-else class="space-y-2">
|
|
<div
|
|
v-for="(piece, pieceIndex) in node.pieces"
|
|
:key="`piece-${depth}-${pieceIndex}`"
|
|
class="border border-base-200 rounded-md p-3"
|
|
>
|
|
<div class="flex items-start justify-between gap-2">
|
|
<div class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-2">
|
|
<input
|
|
v-model="piece.name"
|
|
type="text"
|
|
class="input input-bordered input-xs"
|
|
placeholder="Nom de la pièce"
|
|
/>
|
|
<input
|
|
v-model="piece.reference"
|
|
type="text"
|
|
class="input input-bordered input-xs"
|
|
placeholder="Référence"
|
|
/>
|
|
<input
|
|
v-model.number="piece.quantity"
|
|
type="number"
|
|
min="0"
|
|
step="1"
|
|
class="input input-bordered input-xs"
|
|
placeholder="Quantité"
|
|
/>
|
|
</div>
|
|
<button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(pieceIndex)">
|
|
<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 une pièce
|
|
</button>
|
|
</section>
|
|
|
|
<section class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<h4 class="text-sm font-semibold">Sous-composants</h4>
|
|
<button type="button" class="btn btn-outline btn-xs" @click="addSubComponent">
|
|
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
|
|
Ajouter
|
|
</button>
|
|
</div>
|
|
<p v-if="!(node.subComponents?.length)" class="text-xs text-gray-500">Aucun sous-composant défini.</p>
|
|
<div v-else class="space-y-3">
|
|
<StructureSubComponentEditor
|
|
v-for="(sub, index) in node.subComponents"
|
|
:key="`sub-${depth}-${index}`"
|
|
:node="sub"
|
|
:depth="depth + 1"
|
|
@remove="removeSubComponent(index)"
|
|
/>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
|
import IconLucidePlus from '~icons/lucide/plus'
|
|
import IconLucideTrash from '~icons/lucide/trash'
|
|
|
|
defineOptions({ name: 'StructureSubComponentEditor' })
|
|
|
|
const props = defineProps({
|
|
node: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
depth: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['remove'])
|
|
|
|
const expanded = ref(true)
|
|
const depthPadding = computed(() => (props.depth > 0 ? 'ml-4' : ''))
|
|
|
|
const toggle = () => {
|
|
expanded.value = !expanded.value
|
|
}
|
|
|
|
const ensureArray = (key: 'customFields' | 'pieces' | 'subComponents') => {
|
|
if (!Array.isArray(props.node[key])) {
|
|
props.node[key] = []
|
|
}
|
|
}
|
|
|
|
const addCustomField = () => {
|
|
ensureArray('customFields')
|
|
props.node.customFields.push({
|
|
name: '',
|
|
type: 'text',
|
|
required: false,
|
|
defaultValue: '',
|
|
optionsText: '',
|
|
options: [],
|
|
})
|
|
}
|
|
|
|
const removeCustomField = (index: number) => {
|
|
if (!Array.isArray(props.node.customFields)) return
|
|
props.node.customFields.splice(index, 1)
|
|
}
|
|
|
|
const addPiece = () => {
|
|
ensureArray('pieces')
|
|
props.node.pieces.push({
|
|
name: '',
|
|
reference: '',
|
|
quantity: undefined,
|
|
})
|
|
}
|
|
|
|
const removePiece = (index: number) => {
|
|
if (!Array.isArray(props.node.pieces)) return
|
|
props.node.pieces.splice(index, 1)
|
|
}
|
|
|
|
const addSubComponent = () => {
|
|
ensureArray('subComponents')
|
|
props.node.subComponents.push({
|
|
name: '',
|
|
description: '',
|
|
quantity: undefined,
|
|
customFields: [],
|
|
pieces: [],
|
|
subComponents: [],
|
|
})
|
|
}
|
|
|
|
const removeSubComponent = (index: number) => {
|
|
if (!Array.isArray(props.node.subComponents)) return
|
|
props.node.subComponents.splice(index, 1)
|
|
}
|
|
</script>
|