586 lines
20 KiB
Vue
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>
|