Files
Inventory_frontend/app/pages/models.vue
2025-09-22 08:34:05 +02:00

586 lines
20 KiB
Vue

<template>
<main class="container mx-auto px-6 py-8 space-y-8">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-800">Catalogue de modèles</h1>
<p class="text-sm text-gray-500">
Administrez les modèles de composants et de pièces disponibles lors de la création des machines.
</p>
</div>
<div class="tabs tabs-boxed w-full md:w-auto">
<button
type="button"
class="tab"
:class="{ 'tab-active': activeTab === 'components' }"
@click="activeTab = 'components'"
>
Modèles de composants
</button>
<button
type="button"
class="tab"
:class="{ 'tab-active': activeTab === 'pieces' }"
@click="activeTab = 'pieces'"
>
Modèles de pièces
</button>
</div>
</div>
<!-- Component Models -->
<section v-if="activeTab === 'components'" class="space-y-4">
<div 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="selectedComponentType" 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>
<span class="text-xs text-gray-500">
{{ componentModelsList.length }} modèle(s)
</span>
</div>
<button type="button" class="btn btn-primary btn-sm w-full md:w-auto" @click="openComponentModal('create')">
<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-12">
<span class="loading loading-spinner loading-md"></span>
</div>
<div v-else-if="componentModelsList.length === 0" class="py-12 text-center text-sm text-gray-500">
Aucun modèle trouvé pour 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">Structure</th>
<th class="hidden lg:table-cell">Dernière modification</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="model in componentModelsList" :key="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 lg:table-cell">{{ formatStructurePreview(model.structure) }}</td>
<td class="hidden lg:table-cell">{{ formatDate(model.updatedAt || model.createdAt) }}</td>
<td class="text-right space-x-2">
<button type="button" class="btn btn-sm btn-outline" @click="openComponentModal('edit', model)">
Éditer
</button>
<button type="button" class="btn btn-sm btn-error" @click="confirmDeleteComponentModel(model)">
Supprimer
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Piece Models -->
<section v-else class="space-y-4">
<div 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="selectedPieceType" 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>
<span class="text-xs text-gray-500">
{{ pieceModelsList.length }} modèle(s)
</span>
</div>
<button type="button" class="btn btn-primary btn-sm w-full md:w-auto" @click="openPieceModal('create')">
<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-12">
<span class="loading loading-spinner loading-md"></span>
</div>
<div v-else-if="pieceModelsList.length === 0" class="py-12 text-center text-sm text-gray-500">
Aucun modèle trouvé pour 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">Dernière modification</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="model in pieceModelsList" :key="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">{{ formatDate(model.updatedAt || model.createdAt) }}</td>
<td class="text-right space-x-2">
<button type="button" class="btn btn-sm btn-outline" @click="openPieceModal('edit', model)">
Éditer
</button>
<button type="button" class="btn btn-sm btn-error" @click="confirmDeletePieceModel(model)">
Supprimer
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Component Model Modal -->
<div v-if="componentModal.open" class="modal modal-open">
<div class="modal-box max-w-4xl">
<h3 class="font-bold text-lg mb-1">
{{ componentModal.mode === 'create' ? 'Nouveau modèle de composant' : 'Modifier le modèle de composant' }}
</h3>
<p class="text-xs text-gray-500 mb-4">
Définissez le modèle de composant ainsi que sa structure par défaut (sous-composants, pièces et champs personnalisés).
</p>
<form class="space-y-5" @submit.prevent="submitComponentModal">
<div class="form-control">
<label class="label"><span class="label-text">Nom</span></label>
<input v-model="componentModal.form.name" type="text" class="input input-bordered input-sm" required />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="componentModal.form.description"
class="textarea textarea-bordered textarea-sm"
rows="3"
placeholder="Notes sur ce modèle"
></textarea>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Type de composant</span></label>
<select v-model="componentModal.form.typeComposantId" class="select select-bordered select-sm" required>
<option value="" disabled>Choisir 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="componentModal.form.structure" />
<div class="rounded-lg border border-base-200 bg-base-200/60 p-3">
<ModelStructureViewer :structure="componentModal.form.structure" />
</div>
<div class="modal-action">
<button type="button" class="btn btn-outline" @click="closeComponentModal">Annuler</button>
<button type="submit" class="btn btn-primary" :class="{ loading: componentModal.submitting }">
{{ componentModal.mode === 'create' ? 'Créer' : 'Enregistrer' }}
</button>
</div>
</form>
</div>
</div>
<!-- Piece Model Modal -->
<div v-if="pieceModal.open" class="modal modal-open">
<div class="modal-box max-w-md">
<h3 class="font-bold text-lg mb-1">
{{ pieceModal.mode === 'create' ? 'Nouveau modèle de pièce' : 'Modifier le modèle de pièce' }}
</h3>
<form class="space-y-4" @submit.prevent="submitPieceModal">
<div class="form-control">
<label class="label"><span class="label-text">Nom</span></label>
<input v-model="pieceModal.form.name" type="text" class="input input-bordered input-sm" required />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="pieceModal.form.description"
class="textarea textarea-bordered textarea-sm"
rows="3"
placeholder="Notes sur ce modèle"
></textarea>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Type de pièce</span></label>
<select v-model="pieceModal.form.typePieceId" class="select select-bordered select-sm" required>
<option value="" disabled>Choisir un type</option>
<option
v-for="type in pieceTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="modal-action">
<button type="button" class="btn btn-outline" @click="closePieceModal">Annuler</button>
<button type="submit" class="btn btn-primary" :class="{ loading: pieceModal.submitting }">
{{ pieceModal.mode === 'create' ? 'Créer' : 'Enregistrer' }}
</button>
</div>
</form>
</div>
</div>
</main>
</template>
<script setup>
import { ref, computed, reactive, watch, onMounted } from 'vue'
import { useComponentModels } from '~/composables/useComponentModels'
import { usePieceModels } from '~/composables/usePieceModels'
import { useComponentTypes } from '~/composables/useComponentTypes'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { useToast } from '~/composables/useToast'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideLayers from '~icons/lucide/layers'
import IconLucidePackage from '~icons/lucide/package'
import ComponentModelStructureEditor from '~/components/ComponentModelStructureEditor.vue'
import ModelStructureViewer from '~/components/ModelStructureViewer.vue'
import {
defaultStructure,
cloneStructure,
formatStructurePreview,
normalizeStructureForSave,
} from '~/shared/modelUtils'
const activeTab = ref('components')
const selectedComponentType = ref('all')
const selectedPieceType = ref('all')
const { componentTypes, loadComponentTypes } = useComponentTypes()
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const {
componentModels,
loadComponentModels,
createComponentModel,
updateComponentModel,
deleteComponentModel,
loadingComponentModels,
getComponentModelsForType,
} = useComponentModels()
const {
pieceModels,
loadPieceModels,
createPieceModel,
updatePieceModel,
deletePieceModel,
loadingPieceModels,
getPieceModelsForType,
} = usePieceModels()
const toast = useToast()
const componentModal = reactive({
open: false,
mode: 'create',
submitting: false,
previousTypeId: null,
form: {
id: null,
name: '',
description: '',
typeComposantId: '',
structure: defaultStructure(),
},
})
const pieceModal = reactive({
open: false,
mode: 'create',
submitting: false,
previousTypeId: null,
form: {
id: null,
name: '',
description: '',
typePieceId: '',
},
})
const componentModelsList = computed(() => {
if (selectedComponentType.value === 'all') {
return componentModels.value
}
return getComponentModelsForType(selectedComponentType.value) || []
})
const pieceModelsList = computed(() => {
if (selectedPieceType.value === 'all') {
return pieceModels.value
}
return getPieceModelsForType(selectedPieceType.value) || []
})
const formatDate = (value) => {
if (!value) return '—'
const date = new Date(value)
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
const ensureTypeSelected = (typeId, list) => {
if (typeId && list.some((type) => type.id === typeId)) {
return typeId
}
return list[0]?.id || ''
}
const openComponentModal = (mode, model) => {
componentModal.mode = mode
componentModal.open = true
componentModal.submitting = false
componentModal.previousTypeId = model?.typeComposantId || null
if (mode === 'edit' && model) {
componentModal.form = {
id: model.id,
name: model.name,
description: model.description || '',
typeComposantId: model.typeComposantId || model.typeComposant?.id || '',
structure: cloneStructure(model.structure || defaultStructure()),
}
} else {
componentModal.form = {
id: null,
name: '',
description: '',
typeComposantId: ensureTypeSelected(selectedComponentType.value !== 'all' ? selectedComponentType.value : '', componentTypes.value),
structure: defaultStructure(),
}
}
}
const closeComponentModal = () => {
componentModal.open = false
}
const submitComponentModal = async () => {
if (!componentModal.form.typeComposantId) {
toast.showError('Veuillez sélectionner un type de composant')
return
}
componentModal.submitting = true
try {
if (componentModal.mode === 'create') {
await createComponentModel({
name: componentModal.form.name.trim(),
description: componentModal.form.description.trim() || undefined,
typeComposantId: componentModal.form.typeComposantId,
structure: normalizeStructureForSave(componentModal.form.structure),
})
} else {
await updateComponentModel(componentModal.form.id, {
name: componentModal.form.name.trim(),
description: componentModal.form.description.trim() || undefined,
typeComposantId: componentModal.form.typeComposantId,
structure: normalizeStructureForSave(componentModal.form.structure),
})
}
await refreshComponentModels(componentModal.form.typeComposantId)
if (selectedComponentType.value === 'all') {
await refreshComponentModels()
}
if (
componentModal.mode === 'edit' &&
componentModal.previousTypeId &&
componentModal.previousTypeId !== componentModal.form.typeComposantId
) {
await refreshComponentModels(componentModal.previousTypeId)
}
closeComponentModal()
} catch (error) {
toast.showError('Impossible d\'enregistrer le modèle de composant')
} finally {
componentModal.submitting = false
}
}
const confirmDeleteComponentModel = async (model) => {
if (!confirm(`Supprimer le modèle "${model.name}" ?`)) return
try {
const result = await deleteComponentModel(model.id)
if (result.success) {
await refreshComponentModels(model.typeComposantId)
if (selectedComponentType.value === 'all') {
await refreshComponentModels()
}
}
} catch (error) {
toast.showError('Impossible de supprimer ce modèle')
}
}
const openPieceModal = (mode, model) => {
pieceModal.mode = mode
pieceModal.open = true
pieceModal.submitting = false
pieceModal.previousTypeId = model?.typePieceId || null
if (mode === 'edit' && model) {
pieceModal.form = {
id: model.id,
name: model.name,
description: model.description || '',
typePieceId: model.typePieceId || model.typePiece?.id || '',
}
} else {
pieceModal.form = {
id: null,
name: '',
description: '',
typePieceId: ensureTypeSelected(selectedPieceType.value !== 'all' ? selectedPieceType.value : '', pieceTypes.value),
}
}
}
const closePieceModal = () => {
pieceModal.open = false
}
const submitPieceModal = async () => {
if (!pieceModal.form.typePieceId) {
toast.showError('Veuillez sélectionner un type de pièce')
return
}
pieceModal.submitting = true
try {
if (pieceModal.mode === 'create') {
await createPieceModel({
name: pieceModal.form.name.trim(),
description: pieceModal.form.description.trim() || undefined,
typePieceId: pieceModal.form.typePieceId,
structure: {},
})
} else {
await updatePieceModel(pieceModal.form.id, {
name: pieceModal.form.name.trim(),
description: pieceModal.form.description.trim() || undefined,
typePieceId: pieceModal.form.typePieceId,
})
}
await refreshPieceModels(pieceModal.form.typePieceId)
if (selectedPieceType.value === 'all') {
await refreshPieceModels()
}
if (
pieceModal.mode === 'edit' &&
pieceModal.previousTypeId &&
pieceModal.previousTypeId !== pieceModal.form.typePieceId
) {
await refreshPieceModels(pieceModal.previousTypeId)
}
closePieceModal()
} catch (error) {
toast.showError('Impossible d\'enregistrer le modèle de pièce')
} finally {
pieceModal.submitting = false
}
}
const confirmDeletePieceModel = async (model) => {
if (!confirm(`Supprimer le modèle "${model.name}" ?`)) return
try {
const result = await deletePieceModel(model.id)
if (result.success) {
await refreshPieceModels(model.typePieceId)
if (selectedPieceType.value === 'all') {
await refreshPieceModels()
}
}
} catch (error) {
toast.showError('Impossible de supprimer ce modèle')
}
}
const refreshComponentModels = async (typeId) => {
if (typeId) {
await loadComponentModels(typeId)
} else {
await loadComponentModels()
}
}
const refreshPieceModels = async (typeId) => {
if (typeId) {
await loadPieceModels(typeId)
} else {
await loadPieceModels()
}
}
watch(selectedComponentType, async (value) => {
await refreshComponentModels(value === 'all' ? undefined : value)
}, { immediate: true })
watch(selectedPieceType, async (value) => {
await refreshPieceModels(value === 'all' ? undefined : value)
}, { immediate: true })
onMounted(async () => {
await Promise.all([
loadComponentTypes(),
loadPieceTypes(),
])
})
</script>