Files
Inventory_frontend/app/pages/models/components.vue

348 lines
12 KiB
Vue

<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">{{ formatDate(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 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 formatDate = (value) => {
if (!value) return '—'
return new Date(value).toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
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>