chore: retire legacy model catalogs

This commit is contained in:
MatthieuTD
2025-10-02 16:29:50 +02:00
parent 386a1c9d1b
commit c5f2c568b6
19 changed files with 1234 additions and 3597 deletions

View File

@@ -1,398 +1,29 @@
<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="/component-catalog" class="tab tab-active">
Composants
<main class="container mx-auto px-6 py-12">
<section class="mx-auto max-w-2xl space-y-4 rounded-2xl border border-dashed border-base-300 bg-base-100 p-8 text-center shadow-sm">
<h1 class="text-3xl font-semibold text-gray-800">
Le catalogue de modèles a été retiré
</h1>
<p class="text-sm text-gray-500">
Les modèles sont désormais gérés directement depuis les catégories de composants.
Retrouvez tous vos squelettes et paramètres de référence dans la gestion des catégories.
</p>
<div class="flex flex-wrap items-center justify-center gap-3 pt-2">
<NuxtLink to="/component-category" class="btn btn-primary">
Ouvrir la gestion des catégories
</NuxtLink>
<NuxtLink to="/pieces-catalog" class="tab">
Pièces
<NuxtLink to="/piece-category" class="btn btn-outline">
Gérer les catégories de 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" />
</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"
/>
</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"
:root-type-id="form.data.typeComposantId"
:root-type-label="selectedComponentTypeLabel"
:lock-root-type="!!form.data.typeComposantId"
/>
<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>
</section>
</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,
defaultStructure,
cloneStructure,
normalizeStructureForSave,
} from '~/shared/modelUtils'
import { componentModelStructureValidator } from '~/shared/types/inventory'
import { formatFrenchDate } from '~/utils/date'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideLayers from '~icons/lucide/layers'
<script setup lang="ts">
import { onMounted } from 'vue'
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: defaultStructure(),
}
})
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: defaultStructure(),
}
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: cloneStructure(model.structure || defaultStructure()),
}
}
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 selectedComponentType = computed(() => {
if (!form.data.typeComposantId) {
return null
}
return componentTypes.value.find((type) => type.id === form.data.typeComposantId) || null
})
const selectedComponentTypeLabel = computed(() => {
const type = selectedComponentType.value
if (!type) {
return ''
}
return type.code ? `${type.name} (${type.code})` : type.name
})
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 {
const normalizedStructure = normalizeStructureForSave(form.data.structure)
const validationResult = componentModelStructureValidator.safeParse(normalizedStructure)
if (!validationResult.success) {
showError(`Structure invalide: ${validationResult.issues.join(', ')}`)
return
}
if (form.mode === 'create') {
const result = await createComponentModel({
name: form.data.name.trim(),
description: form.data.description.trim() || undefined,
typeComposantId: form.data.typeComposantId,
structure: normalizedStructure,
})
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: normalizedStructure,
})
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()
}
onMounted(() => {
navigateTo('/component-category', { replace: true })
})
</script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,27 @@
<template>
<div class="min-h-[60vh] flex flex-col items-center justify-center gap-6 text-center">
<div class="space-y-2">
<h1 class="text-3xl font-semibold">
Gestion des catalogues
</h1>
<div class="min-h-[60vh] flex flex-col items-center justify-center gap-6 text-center px-6">
<div class="space-y-2 max-w-xl">
<h1 class="text-3xl font-semibold">Catalogue retiré</h1>
<p class="text-sm text-gray-500">
Administrez les modèles de composants et de pièces utilisés lors de la configuration des machines.
La gestion des modèles s'effectue désormais directement depuis les catégories de composants et de pièces.
Retrouvez l'intégralité des squelettes et champs personnalisés dans ces écrans dédiés.
</p>
</div>
<div class="flex flex-wrap items-center justify-center gap-3">
<NuxtLink to="/component-catalog" class="btn btn-primary">
Catalogue de composant
<NuxtLink to="/component-category" class="btn btn-primary">
Gestion des catégories de composants
</NuxtLink>
<NuxtLink to="/pieces-catalog" class="btn btn-outline">
Catalogue de pièce
<NuxtLink to="/piece-category" class="btn btn-outline">
Gestion des catégories de pièces
</NuxtLink>
</div>
</div>
</template>
<script setup>
const route = useRoute()
if (route.fullPath === '/models') {
navigateTo('/component-catalog', { replace: true })
}
<script setup lang="ts">
import { onMounted } from 'vue'
onMounted(() => {
navigateTo('/component-category', { replace: true })
})
</script>

View File

@@ -1,363 +1,29 @@
<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="/component-catalog" class="tab">
Composants
<main class="container mx-auto px-6 py-12">
<section class="mx-auto max-w-2xl space-y-4 rounded-2xl border border-dashed border-base-300 bg-base-100 p-8 text-center shadow-sm">
<h1 class="text-3xl font-semibold text-gray-800">
Le catalogue de pièces a été archivé
</h1>
<p class="text-sm text-gray-500">
La configuration des pièces repose désormais sur les catégories enrichies de squelettes.
Utilisez la gestion des catégories pour définir les structures et champs personnalisés de référence.
</p>
<div class="flex flex-wrap items-center justify-center gap-3 pt-2">
<NuxtLink to="/piece-category" class="btn btn-primary">
Gérer les catégories de pièces
</NuxtLink>
<NuxtLink to="/pieces-catalog" class="tab tab-active">
Pièces
<NuxtLink to="/component-category" class="btn btn-outline">
Accéder aux catégories de composants
</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" />
</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"
/>
</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>
</section>
</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'
<script setup lang="ts">
import { onMounted } from 'vue'
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()
}
onMounted(() => {
navigateTo('/piece-category', { replace: true })
})
</script>