frontend: refactor model type management and catalog routes
This commit is contained in:
@@ -1,339 +0,0 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8 space-y-8">
|
||||
<header class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold text-gray-800">Catalogue de composant</h1>
|
||||
<p class="text-sm text-gray-500">Gérez les modèles disponibles pour chaque famille de composant.</p>
|
||||
</div>
|
||||
<div class="tabs tabs-boxed">
|
||||
<NuxtLink to="/models/components" class="tab tab-active">Composants</NuxtLink>
|
||||
<NuxtLink to="/models/pieces" class="tab">Pièces</NuxtLink>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 gap-8 xl:grid-cols-[2fr,1fr]">
|
||||
<section class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body space-y-4">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:gap-4">
|
||||
<label class="form-control w-full md:w-64">
|
||||
<span class="label-text text-sm">Type de composant</span>
|
||||
<select v-model="selectedType" class="select select-bordered select-sm">
|
||||
<option value="all">Tous les types</option>
|
||||
<option
|
||||
v-for="type in componentTypes"
|
||||
:key="type.id"
|
||||
:value="type.id"
|
||||
>
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text text-sm">Filtrer</span>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
placeholder="Rechercher un modèle..."
|
||||
class="input input-bordered input-sm"
|
||||
/>
|
||||
</label>
|
||||
<span class="text-xs text-gray-500">{{ filteredModels.length }} modèle(s)</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm w-full md:w-auto" @click="startCreate">
|
||||
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
|
||||
Nouveau modèle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingComponentModels" class="flex justify-center py-16">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredModels.length === 0" class="py-16 text-center text-sm text-gray-500">
|
||||
Aucun modèle ne correspond à ce filtre.
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr class="text-sm text-gray-500">
|
||||
<th>Nom</th>
|
||||
<th class="hidden md:table-cell">Description</th>
|
||||
<th>Type</th>
|
||||
<th class="hidden xl:table-cell">Structure</th>
|
||||
<th class="hidden lg:table-cell">Modifié</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="model in filteredModels"
|
||||
:key="model.id"
|
||||
:class="{
|
||||
'bg-base-200/60': form.mode === 'edit' && form.data.id === model.id
|
||||
}"
|
||||
>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconLucideLayers class="w-4 h-4 text-primary" aria-hidden="true" />
|
||||
<span class="font-medium">{{ model.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">{{ model.description || '—' }}</td>
|
||||
<td>{{ model.typeComposant?.name || 'Non défini' }}</td>
|
||||
<td class="hidden xl:table-cell text-xs text-gray-500">{{ formatStructurePreview(model.structure) }}</td>
|
||||
<td class="hidden lg:table-cell text-xs text-gray-500">{{ formatFrenchDate(model.updatedAt || model.createdAt) }}</td>
|
||||
<td class="text-right space-x-2">
|
||||
<button type="button" class="btn btn-sm btn-outline" @click="startEdit(model)">
|
||||
Éditer
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-error" @click="confirmDelete(model)">
|
||||
Supprimer
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-lg">
|
||||
{{ form.mode === 'edit' ? 'Modifier le modèle' : 'Nouveau modèle' }}
|
||||
</h2>
|
||||
<button
|
||||
v-if="form.mode === 'edit'"
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs"
|
||||
@click="startCreate"
|
||||
>
|
||||
Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Nom</span></label>
|
||||
<input
|
||||
v-model="form.data.name"
|
||||
type="text"
|
||||
class="input input-bordered input-sm"
|
||||
placeholder="Nom du modèle"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Description</span></label>
|
||||
<textarea
|
||||
v-model="form.data.description"
|
||||
class="textarea textarea-bordered textarea-sm"
|
||||
rows="3"
|
||||
placeholder="Notes optionnelles"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Type de composant</span></label>
|
||||
<select
|
||||
v-model="form.data.typeComposantId"
|
||||
class="select select-bordered select-sm"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>Sélectionner un type</option>
|
||||
<option
|
||||
v-for="type in componentTypes"
|
||||
:key="type.id"
|
||||
:value="type.id"
|
||||
>
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="divider my-0">Structure</div>
|
||||
<ComponentModelStructureEditor v-model="form.data.structure" />
|
||||
<div class="rounded-lg border border-base-200 bg-base-200/60 p-3 text-xs text-gray-500">
|
||||
Aperçu : {{ formatStructurePreview(form.data.structure) }}
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end">
|
||||
<button type="submit" class="btn btn-primary" :class="{ loading: form.submitting }">
|
||||
{{ form.mode === 'edit' ? 'Enregistrer' : 'Créer' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, onMounted, watch } from 'vue'
|
||||
import { useComponentModels } from '~/composables/useComponentModels'
|
||||
import { useComponentTypes } from '~/composables/useComponentTypes'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import ComponentModelStructureEditor from '~/components/ComponentModelStructureEditor.vue'
|
||||
import { formatStructurePreview } from '~/shared/modelUtils'
|
||||
import { formatFrenchDate } from '~/utils/date'
|
||||
import IconLucidePlus from '~icons/lucide/plus'
|
||||
import IconLucideLayers from '~icons/lucide/layers'
|
||||
|
||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||
const {
|
||||
componentModels,
|
||||
loadComponentModels,
|
||||
createComponentModel,
|
||||
updateComponentModel,
|
||||
deleteComponentModel,
|
||||
loadingComponentModels,
|
||||
getComponentModelsForType,
|
||||
} = useComponentModels()
|
||||
const { showError, showSuccess } = useToast()
|
||||
|
||||
const selectedType = ref('all')
|
||||
const searchQuery = ref('')
|
||||
|
||||
const form = reactive({
|
||||
mode: 'create',
|
||||
submitting: false,
|
||||
data: {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
typeComposantId: '',
|
||||
structure: {},
|
||||
},
|
||||
})
|
||||
|
||||
const ensureTypeSelected = () => {
|
||||
if (form.data.typeComposantId && componentTypes.value.some((type) => type.id === form.data.typeComposantId)) {
|
||||
return
|
||||
}
|
||||
if (selectedType.value !== 'all' && componentTypes.value.some((type) => type.id === selectedType.value)) {
|
||||
form.data.typeComposantId = selectedType.value
|
||||
return
|
||||
}
|
||||
form.data.typeComposantId = componentTypes.value[0]?.id || ''
|
||||
}
|
||||
|
||||
const startCreate = () => {
|
||||
form.mode = 'create'
|
||||
form.data = {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
typeComposantId: selectedType.value !== 'all' ? selectedType.value : '',
|
||||
structure: {},
|
||||
}
|
||||
ensureTypeSelected()
|
||||
}
|
||||
|
||||
const startEdit = (model) => {
|
||||
form.mode = 'edit'
|
||||
form.data = {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
description: model.description || '',
|
||||
typeComposantId: model.typeComposantId || model.typeComposant?.id || '',
|
||||
structure: model.structure || {},
|
||||
}
|
||||
}
|
||||
|
||||
const filteredModels = computed(() => {
|
||||
const list = selectedType.value === 'all'
|
||||
? componentModels.value
|
||||
: getComponentModelsForType(selectedType.value) || []
|
||||
|
||||
if (!searchQuery.value.trim()) {
|
||||
return list
|
||||
}
|
||||
const term = searchQuery.value.toLowerCase()
|
||||
return list.filter((model) => {
|
||||
return (
|
||||
model.name?.toLowerCase().includes(term)
|
||||
|| model.description?.toLowerCase().includes(term)
|
||||
|| model.typeComposant?.name?.toLowerCase().includes(term)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const refreshModels = async () => {
|
||||
if (selectedType.value === 'all') {
|
||||
await loadComponentModels()
|
||||
} else {
|
||||
await loadComponentModels(selectedType.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.data.typeComposantId) {
|
||||
showError('Veuillez sélectionner un type de composant')
|
||||
return
|
||||
}
|
||||
|
||||
form.submitting = true
|
||||
try {
|
||||
if (form.mode === 'create') {
|
||||
const result = await createComponentModel({
|
||||
name: form.data.name.trim(),
|
||||
description: form.data.description.trim() || undefined,
|
||||
typeComposantId: form.data.typeComposantId,
|
||||
structure: form.data.structure || {},
|
||||
})
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Impossible de créer le modèle')
|
||||
return
|
||||
}
|
||||
showSuccess(`Modèle "${result.data.name}" créé`)
|
||||
} else if (form.data.id) {
|
||||
const result = await updateComponentModel(form.data.id, {
|
||||
name: form.data.name.trim(),
|
||||
description: form.data.description.trim() || undefined,
|
||||
typeComposantId: form.data.typeComposantId,
|
||||
structure: form.data.structure || {},
|
||||
})
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Impossible de mettre à jour le modèle')
|
||||
return
|
||||
}
|
||||
showSuccess(`Modèle "${result.data.name}" mis à jour`)
|
||||
}
|
||||
|
||||
await refreshModels()
|
||||
startCreate()
|
||||
} finally {
|
||||
form.submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = async (model) => {
|
||||
const ok = confirm(`Supprimer le modèle "${model.name}" ?`)
|
||||
if (!ok) return
|
||||
const result = await deleteComponentModel(model.id)
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Impossible de supprimer ce modèle')
|
||||
return
|
||||
}
|
||||
if (form.mode === 'edit' && form.data.id === model.id) {
|
||||
startCreate()
|
||||
}
|
||||
showSuccess('Modèle supprimé')
|
||||
await refreshModels()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadComponentTypes(), loadComponentModels()])
|
||||
ensureTypeSelected()
|
||||
})
|
||||
|
||||
watch(selectedType, async () => {
|
||||
await refreshModels()
|
||||
if (form.mode === 'create') {
|
||||
ensureTypeSelected()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -7,10 +7,10 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-center gap-3">
|
||||
<NuxtLink to="/models/components" class="btn btn-primary">
|
||||
<NuxtLink to="/component-catalog" class="btn btn-primary">
|
||||
Catalogue de composant
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/models/pieces" class="btn btn-outline">
|
||||
<NuxtLink to="/pieces-catalog" class="btn btn-outline">
|
||||
Catalogue de pièce
|
||||
</NuxtLink>
|
||||
</div>
|
||||
@@ -20,6 +20,6 @@
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
if (route.fullPath === '/models') {
|
||||
navigateTo('/models/components', { replace: true })
|
||||
navigateTo('/component-catalog', { replace: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,341 +0,0 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8 space-y-8">
|
||||
<header class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold text-gray-800">Catalogue de pièce</h1>
|
||||
<p class="text-sm text-gray-500">Gérez les modèles disponibles pour chaque groupe de pièces.</p>
|
||||
</div>
|
||||
<div class="tabs tabs-boxed">
|
||||
<NuxtLink to="/models/components" class="tab">Composants</NuxtLink>
|
||||
<NuxtLink to="/models/pieces" class="tab tab-active">Pièces</NuxtLink>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 gap-8 xl:grid-cols-[2fr,1fr]">
|
||||
<section class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body space-y-4">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:gap-4">
|
||||
<label class="form-control w-full md:w-64">
|
||||
<span class="label-text text-sm">Type de pièce</span>
|
||||
<select v-model="selectedType" class="select select-bordered select-sm">
|
||||
<option value="all">Tous les types</option>
|
||||
<option v-for="type in pieceTypes" :key="type.id" :value="type.id">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text text-sm">Filtrer</span>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
placeholder="Rechercher un modèle..."
|
||||
class="input input-bordered input-sm"
|
||||
/>
|
||||
</label>
|
||||
<span class="text-xs text-gray-500">{{ filteredModels.length }} modèle(s)</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm w-full md:w-auto" @click="startCreate">
|
||||
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
|
||||
Nouveau modèle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingPieceModels" class="flex justify-center py-16">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredModels.length === 0" class="py-16 text-center text-sm text-gray-500">
|
||||
Aucun modèle ne correspond à ce filtre.
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr class="text-sm text-gray-500">
|
||||
<th>Nom</th>
|
||||
<th class="hidden md:table-cell">Description</th>
|
||||
<th>Type</th>
|
||||
<th class="hidden lg:table-cell">Modifié</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="model in filteredModels"
|
||||
:key="model.id"
|
||||
:class="{
|
||||
'bg-base-200/60': form.mode === 'edit' && form.data.id === model.id
|
||||
}"
|
||||
>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconLucidePackage class="w-4 h-4 text-secondary" aria-hidden="true" />
|
||||
<span class="font-medium">{{ model.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">{{ model.description || '—' }}</td>
|
||||
<td>{{ model.typePiece?.name || 'Non défini' }}</td>
|
||||
<td class="hidden lg:table-cell text-xs text-gray-500">{{ formatFrenchDate(model.updatedAt || model.createdAt) }}</td>
|
||||
<td class="text-right space-x-2">
|
||||
<button type="button" class="btn btn-sm btn-outline" @click="startEdit(model)">
|
||||
Éditer
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-error" @click="confirmDelete(model)">
|
||||
Supprimer
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-lg">
|
||||
{{ form.mode === 'edit' ? 'Modifier le modèle' : 'Nouveau modèle' }}
|
||||
</h2>
|
||||
<button
|
||||
v-if="form.mode === 'edit'"
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs"
|
||||
@click="startCreate"
|
||||
>
|
||||
Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Nom</span></label>
|
||||
<input
|
||||
v-model="form.data.name"
|
||||
type="text"
|
||||
class="input input-bordered input-sm"
|
||||
placeholder="Nom du modèle"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Description</span></label>
|
||||
<textarea
|
||||
v-model="form.data.description"
|
||||
class="textarea textarea-bordered textarea-sm"
|
||||
rows="3"
|
||||
placeholder="Notes optionnelles"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Type de pièce</span></label>
|
||||
<select
|
||||
v-model="form.data.typePieceId"
|
||||
class="select select-bordered select-sm"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>Sélectionner un type</option>
|
||||
<option v-for="type in pieceTypes" :key="type.id" :value="type.id">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="divider my-0">Structure</div>
|
||||
<PieceModelStructureEditor v-model="form.data.structure" />
|
||||
|
||||
<div class="rounded-lg border border-base-200 bg-base-200/60 p-3 text-xs text-gray-500">
|
||||
Aperçu : {{ formatStructurePreview(form.data.structure) }}
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end">
|
||||
<button type="submit" class="btn btn-primary" :class="{ loading: form.submitting }">
|
||||
{{ form.mode === 'edit' ? 'Enregistrer' : 'Créer' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, onMounted, watch } from 'vue'
|
||||
import PieceModelStructureEditor from '~/components/PieceModelStructureEditor.vue'
|
||||
import { usePieceModels } from '~/composables/usePieceModels'
|
||||
import { usePieceTypes } from '~/composables/usePieceTypes'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { formatStructurePreview } from '~/shared/modelUtils'
|
||||
import { formatFrenchDate } from '~/utils/date'
|
||||
import IconLucidePlus from '~icons/lucide/plus'
|
||||
import IconLucidePackage from '~icons/lucide/package'
|
||||
|
||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||
const {
|
||||
pieceModels,
|
||||
loadPieceModels,
|
||||
createPieceModel,
|
||||
updatePieceModel,
|
||||
deletePieceModel,
|
||||
loadingPieceModels,
|
||||
getPieceModelsForType,
|
||||
} = usePieceModels()
|
||||
const { showError, showSuccess } = useToast()
|
||||
|
||||
const selectedType = ref('all')
|
||||
const searchQuery = ref('')
|
||||
|
||||
const defaultStructure = () => ({ customFields: [] })
|
||||
|
||||
const cloneStructure = (value) => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value ?? defaultStructure()))
|
||||
} catch (error) {
|
||||
return defaultStructure()
|
||||
}
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
mode: 'create',
|
||||
submitting: false,
|
||||
data: {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
typePieceId: '',
|
||||
structure: defaultStructure(),
|
||||
},
|
||||
})
|
||||
|
||||
const ensureTypeSelected = () => {
|
||||
if (form.data.typePieceId && pieceTypes.value.some((type) => type.id === form.data.typePieceId)) {
|
||||
return
|
||||
}
|
||||
if (selectedType.value !== 'all' && pieceTypes.value.some((type) => type.id === selectedType.value)) {
|
||||
form.data.typePieceId = selectedType.value
|
||||
return
|
||||
}
|
||||
form.data.typePieceId = pieceTypes.value[0]?.id || ''
|
||||
}
|
||||
|
||||
const startCreate = () => {
|
||||
form.mode = 'create'
|
||||
form.data = {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
typePieceId: selectedType.value !== 'all' ? selectedType.value : '',
|
||||
structure: defaultStructure(),
|
||||
}
|
||||
ensureTypeSelected()
|
||||
}
|
||||
|
||||
const startEdit = (model) => {
|
||||
form.mode = 'edit'
|
||||
form.data = {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
description: model.description || '',
|
||||
typePieceId: model.typePieceId || model.typePiece?.id || '',
|
||||
structure: cloneStructure(model.structure || defaultStructure()),
|
||||
}
|
||||
}
|
||||
|
||||
const filteredModels = computed(() => {
|
||||
const list = selectedType.value === 'all'
|
||||
? pieceModels.value
|
||||
: getPieceModelsForType(selectedType.value) || []
|
||||
|
||||
if (!searchQuery.value.trim()) {
|
||||
return list
|
||||
}
|
||||
const term = searchQuery.value.toLowerCase()
|
||||
return list.filter((model) => {
|
||||
return (
|
||||
model.name?.toLowerCase().includes(term)
|
||||
|| model.description?.toLowerCase().includes(term)
|
||||
|| model.typePiece?.name?.toLowerCase().includes(term)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const refreshModels = async () => {
|
||||
if (selectedType.value === 'all') {
|
||||
await loadPieceModels()
|
||||
} else {
|
||||
await loadPieceModels(selectedType.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.data.typePieceId) {
|
||||
showError('Veuillez sélectionner un type de pièce')
|
||||
return
|
||||
}
|
||||
|
||||
form.submitting = true
|
||||
try {
|
||||
const structure = cloneStructure(form.data.structure)
|
||||
if (form.mode === 'create') {
|
||||
const result = await createPieceModel({
|
||||
name: form.data.name.trim(),
|
||||
description: form.data.description.trim() || undefined,
|
||||
typePieceId: form.data.typePieceId,
|
||||
structure,
|
||||
})
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Impossible de créer le modèle')
|
||||
return
|
||||
}
|
||||
showSuccess(`Modèle "${result.data.name}" créé`)
|
||||
} else if (form.data.id) {
|
||||
const result = await updatePieceModel(form.data.id, {
|
||||
name: form.data.name.trim(),
|
||||
description: form.data.description.trim() || undefined,
|
||||
typePieceId: form.data.typePieceId,
|
||||
structure,
|
||||
})
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Impossible de mettre à jour le modèle')
|
||||
return
|
||||
}
|
||||
showSuccess(`Modèle "${result.data.name}" mis à jour`)
|
||||
}
|
||||
|
||||
await refreshModels()
|
||||
startCreate()
|
||||
} finally {
|
||||
form.submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = async (model) => {
|
||||
const ok = confirm(`Supprimer le modèle "${model.name}" ?`)
|
||||
if (!ok) return
|
||||
const result = await deletePieceModel(model.id)
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Impossible de supprimer ce modèle')
|
||||
return
|
||||
}
|
||||
if (form.mode === 'edit' && form.data.id === model.id) {
|
||||
startCreate()
|
||||
}
|
||||
showSuccess('Modèle supprimé')
|
||||
await refreshModels()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadPieceTypes(), loadPieceModels()])
|
||||
ensureTypeSelected()
|
||||
})
|
||||
|
||||
watch(selectedType, async () => {
|
||||
await refreshModels()
|
||||
if (form.mode === 'create') {
|
||||
ensureTypeSelected()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user