Restore catalogs and type-aware machine selection

This commit is contained in:
MatthieuTD
2025-10-06 17:19:30 +02:00
parent 082b1ccc05
commit f9641dbd62
4 changed files with 683 additions and 26 deletions
+5 -1
View File
@@ -62,7 +62,11 @@ export function usePieces () {
const result = await post('/pieces', pieceData)
if (result.success) {
pieces.value.push(result.data)
showSuccess(`Pièce "${pieceData.name}" créée avec succès`)
const displayName = result.data?.name
|| pieceData?.definition?.name
|| pieceData?.name
|| 'Pièce'
showSuccess(`Pièce "${displayName}" créée avec succès`)
}
return result
} catch (error) {
+79
View File
@@ -563,6 +563,27 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text text-xs">Catégorie de composant</span>
</label>
<select
class="select select-bordered select-xs md:select-sm"
:value="entry.typeComposantId || requirement.typeComposantId || ''"
@change="setComponentRequirementType(requirement.id, entryIndex, ($event.target && $event.target.value) || '')"
>
<option value="">
{{ requirement.typeComposant?.name ? `Type du requirement (${requirement.typeComposant.name})` : 'Suivre le requirement' }}
</option>
<option
v-for="type in componentTypeOptions"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text text-xs">Nom du composant</span>
@@ -670,6 +691,27 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text text-xs">Catégorie de pièce</span>
</label>
<select
class="select select-bordered select-xs md:select-sm"
:value="entry.typePieceId || requirement.typePieceId || ''"
@change="setPieceRequirementType(requirement.id, entryIndex, ($event.target && $event.target.value) || '')"
>
<option value="">
{{ requirement.typePiece?.name ? `Type du requirement (${requirement.typePiece.name})` : 'Suivre le requirement' }}
</option>
<option
v-for="type in pieceTypeOptions"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text text-xs">Nom de la pièce</span>
@@ -778,6 +820,8 @@ import { useRoute } from 'vue-router'
import { useMachines } from '~/composables/useMachines'
import { useComposants } from '~/composables/useComposants'
import { usePieces } from '~/composables/usePieces'
import { useComponentTypes } from '~/composables/useComponentTypes'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { useCustomFields } from '~/composables/useCustomFields'
import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
@@ -822,6 +866,8 @@ const {
getPiecesByMachine,
updatePiece: updatePieceApi
} = usePieces()
const { componentTypes, loadComponentTypes } = useComponentTypes()
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const { upsertCustomFieldValue, updateCustomFieldValue: updateCustomFieldValueApi } = useCustomFields()
const {
@@ -889,8 +935,16 @@ const machineHasSkeletonRequirements = computed(() =>
componentRequirements.value.length > 0 || pieceRequirements.value.length > 0
)
const componentTypeOptions = computed(() => componentTypes.value || [])
const pieceTypeOptions = computed(() => pieceTypes.value || [])
const componentTypeLabelMap = computed(() => {
const map = new Map()
componentTypeOptions.value.forEach((type) => {
if (type?.id) {
map.set(type.id, type.name || '')
}
})
componentRequirements.value.forEach((requirement) => {
const type = requirement.typeComposant
if (type?.id) {
@@ -902,6 +956,11 @@ const componentTypeLabelMap = computed(() => {
const pieceTypeLabelMap = computed(() => {
const map = new Map()
pieceTypeOptions.value.forEach((type) => {
if (type?.id) {
map.set(type.id, type.name || '')
}
})
pieceRequirements.value.forEach((requirement) => {
const type = requirement.typePiece
if (type?.id) {
@@ -1006,6 +1065,13 @@ const removeComponentSelectionEntry = (requirementId, index) => {
componentRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
}
const setComponentRequirementType = (requirementId, index, value) => {
const entries = getComponentRequirementEntries(requirementId)
const entry = entries[index]
if (!entry) return
entry.typeComposantId = value || null
}
const setComponentRequirementConstructeur = (requirementId, index, value) => {
const entries = getComponentRequirementEntries(requirementId)
const entry = entries[index]
@@ -1030,6 +1096,13 @@ const removePieceSelectionEntry = (requirementId, index) => {
pieceRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
}
const setPieceRequirementType = (requirementId, index, value) => {
const entries = getPieceRequirementEntries(requirementId)
const entry = entries[index]
if (!entry) return
entry.typePieceId = value || null
}
const setPieceRequirementConstructeur = (requirementId, index, value) => {
const entries = getPieceRequirementEntries(requirementId)
const entry = entries[index]
@@ -2331,6 +2404,12 @@ onMounted(() => {
if (!constructeurs.value.length) {
loadConstructeurs()
}
if (!componentTypes.value.length) {
loadComponentTypes()
}
if (!pieceTypes.value.length) {
loadPieceTypes()
}
// Vérifier si on doit activer le mode édition depuis l'URL
const route = useRoute()
+102 -3
View File
@@ -159,6 +159,27 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text text-xs">Catégorie de composant</span>
</label>
<select
class="select select-bordered select-xs md:select-sm"
:value="entry.typeComposantId || requirement.typeComposantId || ''"
@change="setComponentRequirementType(requirement.id, entryIndex, ($event.target && $event.target.value) || '')"
>
<option value="">
{{ requirement.typeComposant?.name ? `Type du requirement (${requirement.typeComposant.name})` : 'Suivre le requirement' }}
</option>
<option
v-for="type in componentTypeOptions"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text text-xs">Nom du composant</span>
@@ -266,6 +287,27 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text text-xs">Catégorie de pièce</span>
</label>
<select
class="select select-bordered select-xs md:select-sm"
:value="entry.typePieceId || requirement.typePieceId || ''"
@change="setPieceRequirementType(requirement.id, entryIndex, ($event.target && $event.target.value) || '')"
>
<option value="">
{{ requirement.typePiece?.name ? `Type du requirement (${requirement.typePiece.name})` : 'Suivre le requirement' }}
</option>
<option
v-for="type in pieceTypeOptions"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text text-xs">Nom de la pièce</span>
@@ -559,6 +601,8 @@ import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
import { useMachines } from '~/composables/useMachines'
import { useSites } from '~/composables/useSites'
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
import { useComponentTypes } from '~/composables/useComponentTypes'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { useToast } from '~/composables/useToast'
import { sanitizeDefinitionOverrides } from '~/shared/modelUtils'
import IconLucidePlus from '~icons/lucide/plus'
@@ -571,6 +615,8 @@ import IconLucideCircle from '~icons/lucide/circle'
const { createMachine, createMachineFromType } = useMachines()
const { sites, loadSites } = useSites()
const { machineTypes, loadMachineTypes } = useMachineTypesApi()
const { componentTypes, loadComponentTypes } = useComponentTypes()
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const toast = useToast()
const submitting = ref(false)
@@ -592,6 +638,9 @@ const selectedMachineType = computed(() => {
return machineTypes.value.find(type => type.id === newMachine.typeMachineId) || null
})
const componentTypeOptions = computed(() => componentTypes.value || [])
const pieceTypeOptions = computed(() => pieceTypes.value || [])
const getStatusBadgeClass = (status) => {
if (status === 'ready') {
return 'badge-success'
@@ -602,12 +651,46 @@ const getStatusBadgeClass = (status) => {
return 'badge-error'
}
const componentTypeLabelMap = computed(() => {
const map = new Map()
componentTypeOptions.value.forEach((type) => {
if (type?.id) {
map.set(type.id, type.name || '')
}
})
const requirementTypes = selectedMachineType.value?.componentRequirements || []
requirementTypes.forEach((requirement) => {
const type = requirement?.typeComposant
if (type?.id && type?.name) {
map.set(type.id, type.name)
}
})
return map
})
const pieceTypeLabelMap = computed(() => {
const map = new Map()
pieceTypeOptions.value.forEach((type) => {
if (type?.id) {
map.set(type.id, type.name || '')
}
})
const requirementTypes = selectedMachineType.value?.pieceRequirements || []
requirementTypes.forEach((requirement) => {
const type = requirement?.typePiece
if (type?.id && type?.name) {
map.set(type.id, type.name)
}
})
return map
})
const resolveComponentRequirementTypeLabel = (requirement, entry) => {
const typeId = entry?.typeComposantId || requirement?.typeComposantId || null
if (!typeId) {
return requirement?.typeComposant?.name || 'Type non défini'
}
return requirement?.typeComposant?.name || 'Type non défini'
return componentTypeLabelMap.value.get(typeId) || requirement?.typeComposant?.name || 'Type non défini'
}
const resolvePieceRequirementTypeLabel = (requirement, entry) => {
@@ -615,7 +698,7 @@ const resolvePieceRequirementTypeLabel = (requirement, entry) => {
if (!typeId) {
return requirement?.typePiece?.name || 'Type non défini'
}
return requirement?.typePiece?.name || 'Type non défini'
return pieceTypeLabelMap.value.get(typeId) || requirement?.typePiece?.name || 'Type non défini'
}
const getComponentRequirementEntries = requirementId => componentRequirementSelections[requirementId] || []
@@ -680,6 +763,13 @@ const removeComponentSelectionEntry = (requirementId, index) => {
componentRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
}
const setComponentRequirementType = (requirementId, index, value) => {
const entries = getComponentRequirementEntries(requirementId)
const entry = entries[index]
if (!entry) return
entry.typeComposantId = value || null
}
const setComponentRequirementConstructeur = (requirementId, index, value) => {
const entries = getComponentRequirementEntries(requirementId)
const entry = entries[index]
@@ -705,6 +795,13 @@ const removePieceSelectionEntry = (requirementId, index) => {
pieceRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
}
const setPieceRequirementType = (requirementId, index, value) => {
const entries = getPieceRequirementEntries(requirementId)
const entry = entries[index]
if (!entry) return
entry.typePieceId = value || null
}
const setPieceRequirementConstructeur = (requirementId, index, value) => {
const entries = getPieceRequirementEntries(requirementId)
const entry = entries[index]
@@ -1153,7 +1250,9 @@ watch(
onMounted(async () => {
await Promise.all([
loadSites(),
loadMachineTypes()
loadMachineTypes(),
loadComponentTypes(),
loadPieceTypes()
])
})
</script>
+492 -17
View File
@@ -1,29 +1,504 @@
<template>
<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>
<main class="container mx-auto px-6 py-10 space-y-8">
<header class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<h1 class="text-3xl font-semibold text-base-content">Catalogue des pièces</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.
Consultez les catégories disponibles et instanciez des pièces à partir de leur squelette canonique.
</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="/component-category" class="btn btn-outline">
Accéder aux catégories de composants
</NuxtLink>
</div>
<NuxtLink to="/piece-category" class="btn btn-outline btn-sm md:btn-md self-start">
Gérer les catégories
</NuxtLink>
</header>
<section>
<div v-if="loadingTypes" class="flex justify-center py-16">
<span class="loading loading-spinner loading-lg" aria-hidden="true" />
</div>
<template v-else>
<p v-if="!pieceTypeList.length" class="text-sm text-gray-500">
Aucune catégorie de pièce n'est disponible pour le moment. Créez des catégories dans la gestion dédiée pour définir vos squelettes.
</p>
<div v-else class="grid gap-6 lg:grid-cols-2">
<article
v-for="type in pieceTypeList"
:key="type.id"
class="card bg-base-100 border border-base-200 shadow-sm"
>
<div class="card-body space-y-4">
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div class="space-y-1">
<h2 class="card-title text-xl">{{ type.name }}</h2>
<p v-if="type.description" class="text-sm text-gray-500">
{{ type.description }}
</p>
</div>
<button type="button" class="btn btn-primary btn-sm md:btn-md self-start" @click="openCreationModal(type)">
Instancier une pièce
</button>
</div>
<div class="flex flex-wrap gap-2 text-xs text-gray-500">
<span class="badge badge-outline">{{ formatPieceStructurePreview(type.structure) }}</span>
<span v-if="getCategoryCustomFields(type).length" class="badge badge-outline">
{{ getCategoryCustomFields(type).length }} champ(s) personnalisés
</span>
</div>
<div v-if="type.structure" class="space-y-3">
<details class="collapse collapse-arrow bg-base-200/60">
<summary class="collapse-title text-sm font-medium">
Détails du squelette
</summary>
<div class="collapse-content space-y-3 text-sm text-base-content/80">
<div v-if="getStructureCustomFields(type.structure).length" class="space-y-1">
<h3 class="font-semibold text-sm text-base-content">Valeurs par défaut</h3>
<ul class="list-disc list-inside space-y-1">
<li
v-for="field in getStructureCustomFields(type.structure)"
:key="field.name"
>
<span class="font-medium">{{ field.name }}</span>
<span v-if="field.value !== undefined && field.value !== null"> : {{ field.value }}</span>
</li>
</ul>
</div>
<p
v-if="!getStructureCustomFields(type.structure).length"
class="text-xs text-gray-500"
>
Ce squelette ne définit pas encore de valeurs par défaut spécifiques.
</p>
</div>
</details>
</div>
<div v-if="getCategoryCustomFields(type).length" class="space-y-2">
<h3 class="text-sm font-semibold text-base-content">Champs personnalisés de la catégorie</h3>
<ul class="space-y-1 text-sm text-base-content/80">
<li
v-for="field in getCategoryCustomFields(type)"
:key="field.id || field.name"
class="flex flex-wrap gap-2 items-center"
>
<span class="font-medium">{{ field.name }}</span>
<span class="badge badge-outline badge-xs">{{ field.type || 'text' }}</span>
<span v-if="field.required" class="badge badge-outline badge-xs badge-error">Obligatoire</span>
</li>
</ul>
</div>
<div v-if="getExistingPieces(type).length" class="space-y-2">
<h3 class="text-sm font-semibold text-base-content">Pièces existantes</h3>
<ul class="space-y-1 text-sm text-base-content/80">
<li
v-for="piece in getExistingPieces(type).slice(0, 5)"
:key="piece.id"
>
<span class="font-medium">{{ piece.name || 'Pièce sans nom' }}</span>
<span v-if="piece.machine?.name" class="text-xs text-gray-500">
· Machine : {{ piece.machine.name }}
</span>
</li>
</ul>
<p
v-if="getExistingPieces(type).length > 5"
class="text-xs text-gray-500"
>
+ {{ getExistingPieces(type).length - 5 }} pièce(s) supplémentaires.
</p>
</div>
</div>
</article>
</div>
</template>
</section>
</main>
<Teleport to="body">
<div v-if="creationModalOpen" class="modal modal-open">
<div class="modal-box max-w-3xl space-y-6">
<div class="space-y-1">
<h3 class="text-lg font-semibold">
Instancier une pièce
</h3>
<p class="text-sm text-gray-500">
Sélectionnez la machine et le requirement cible puis ajustez les informations d'override avant la création.
</p>
<p v-if="selectedType" class="badge badge-outline badge-sm">
Catégorie : {{ selectedType.name }}
</p>
</div>
<form class="space-y-4" @submit.prevent="submitCreation">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Machine cible</span>
</label>
<select
v-model="creationForm.machineId"
class="select select-bordered select-sm md:select-md"
:disabled="machinesLoading || submitting"
required
>
<option value="">Sélectionner une machine</option>
<option
v-for="machine in machines"
:key="machine.id"
:value="machine.id"
>
{{ machine.name }}
</option>
</select>
<p v-if="machinesLoading" class="text-xs text-gray-500 mt-1">
Chargement des machines...
</p>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Requirement</span>
</label>
<select
v-model="creationForm.requirementId"
class="select select-bordered select-sm md:select-md"
:disabled="requirementLoading || !requirementOptions.length || submitting"
required
>
<option value="">Sélectionner un requirement</option>
<option
v-for="requirement in requirementOptions"
:key="requirement.id"
:value="requirement.id"
>
{{ resolveRequirementLabel(requirement) }}
</option>
</select>
<p v-if="requirementLoading" class="text-xs text-gray-500 mt-1">
Chargement des requirements de la machine...
</p>
<p
v-else-if="creationForm.machineId && !requirementOptions.length"
class="text-xs text-error mt-1"
>
Cette machine n'a pas de requirement de pièce configuré.
</p>
</div>
</div>
<div
v-if="selectedRequirement"
class="rounded-lg border border-base-200 bg-base-200/60 p-4 text-xs text-base-content/80 space-y-1"
>
<div class="flex flex-wrap gap-2 items-center">
<span class="font-medium text-sm">Requirement sélectionné :</span>
<span class="badge badge-outline badge-sm">{{ resolveRequirementLabel(selectedRequirement) }}</span>
</div>
<p v-if="selectedRequirement.typePiece?.name" class="text-xs">
Type attendu : {{ selectedRequirement.typePiece.name }}
</p>
<p v-if="selectedRequirement.maxCount !== null && selectedRequirement.maxCount !== undefined" class="text-xs">
Capacité : {{ selectedRequirement.minCount ?? (selectedRequirement.required ? 1 : 0) }} -
{{ selectedRequirement.maxCount ?? '' }} élément(s)
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Nom de la pièce</span>
</label>
<input
v-model="creationForm.name"
type="text"
class="input input-bordered input-sm md:input-md"
:disabled="submitting"
placeholder="Nom affiché après instanciation"
>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Référence</span>
</label>
<input
v-model="creationForm.reference"
type="text"
class="input input-bordered input-sm md:input-md"
:disabled="submitting"
placeholder="Référence interne ou constructeur"
>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Constructeur</span>
</label>
<ConstructeurSelect
v-model="creationForm.constructeurId"
class="w-full"
:disabled="submitting"
placeholder="Rechercher un constructeur..."
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Prix</span>
</label>
<input
v-model="creationForm.prix"
type="number"
step="0.01"
min="0"
class="input input-bordered input-sm md:input-md"
:disabled="submitting"
placeholder="Valeur indicative (€)"
>
</div>
</div>
</form>
<div class="modal-action">
<button type="button" class="btn btn-ghost" :disabled="submitting" @click="closeCreationModal">
Annuler
</button>
<button
type="button"
class="btn btn-primary"
:disabled="!canSubmit"
@click="submitCreation"
>
<span v-if="submitting" class="loading loading-spinner loading-sm mr-2"></span>
Créer la pièce
</button>
</div>
</div>
<button class="modal-backdrop" aria-label="Fermer" @click="closeCreationModal"></button>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { useMachines } from '~/composables/useMachines'
import { usePieces } from '~/composables/usePieces'
import { useToast } from '~/composables/useToast'
import { useApi } from '~/composables/useApi'
import { formatPieceStructurePreview, sanitizeDefinitionOverrides } from '~/shared/modelUtils'
import type { PieceModelStructure } from '~/shared/types/inventory'
import type { ModelType } from '~/services/modelTypes'
onMounted(() => {
navigateTo('/piece-category', { replace: true })
interface PieceCatalogType extends ModelType {
structure: PieceModelStructure | null
customFields?: Array<Record<string, any>>
pieces?: Array<Record<string, any>>
}
const { pieceTypes, loadPieceTypes, loadingPieceTypes } = usePieceTypes()
const { machines, loadMachines, loading: machinesLoadingRef } = useMachines()
const { createPiece } = usePieces()
const toast = useToast()
const { apiCall } = useApi()
const creationModalOpen = ref(false)
const selectedType = ref<PieceCatalogType | null>(null)
const submitting = ref(false)
const requirementLoading = ref(false)
const creationForm = reactive({
machineId: '' as string,
requirementId: '' as string,
name: '' as string,
reference: '' as string,
constructeurId: null as string | null,
prix: '' as string,
})
const machineRequirementCache = reactive<Record<string, { requirements: any[] }>>({})
const lastSuggestedName = ref('')
let requirementRequestToken = 0
const loadingTypes = computed(() => loadingPieceTypes.value)
const pieceTypeList = computed<PieceCatalogType[]>(() => (pieceTypes.value || []) as PieceCatalogType[])
const machinesLoading = computed(() => machinesLoadingRef.value)
const requirementOptions = computed(() => {
const machineId = creationForm.machineId
if (!machineId) {
return []
}
const entry = machineRequirementCache[machineId]
if (!entry) {
return []
}
return Array.isArray(entry.requirements) ? entry.requirements : []
})
const selectedRequirement = computed(() => {
return requirementOptions.value.find((requirement: any) => requirement.id === creationForm.requirementId) || null
})
const canSubmit = computed(() => Boolean(creationForm.machineId && creationForm.requirementId && !submitting.value && !requirementLoading.value))
const getCategoryCustomFields = (type: PieceCatalogType) => Array.isArray(type?.customFields) ? type.customFields : []
const getStructureCustomFields = (structure: PieceModelStructure | null) => Array.isArray(structure?.customFields) ? structure.customFields : []
const getExistingPieces = (type: PieceCatalogType) => Array.isArray(type?.pieces) ? type.pieces : []
const resolveRequirementLabel = (requirement: any) => {
if (!requirement) {
return 'Requirement sans libellé'
}
const parts: string[] = []
if (requirement.label) {
parts.push(requirement.label)
}
if (requirement.typePiece?.name) {
parts.push(requirement.typePiece.name)
}
return parts.join(' • ') || 'Requirement'
}
const clearCreationForm = () => {
creationForm.machineId = ''
creationForm.requirementId = ''
creationForm.name = ''
creationForm.reference = ''
creationForm.constructeurId = null
creationForm.prix = ''
lastSuggestedName.value = ''
}
const resetCreationFormForType = () => {
clearCreationForm()
creationForm.name = selectedType.value?.name || ''
lastSuggestedName.value = creationForm.name
}
const openCreationModal = async (type: PieceCatalogType) => {
selectedType.value = type
resetCreationFormForType()
creationModalOpen.value = true
if (!machines.value?.length) {
await loadMachines()
}
}
const closeCreationModal = () => {
creationModalOpen.value = false
selectedType.value = null
clearCreationForm()
}
const ensureMachineRequirements = async (machineId: string) => {
if (!machineId || machineRequirementCache[machineId]) {
return
}
const requestId = ++requirementRequestToken
requirementLoading.value = true
try {
const result = await apiCall(`/machines/${machineId}`, { method: 'GET' })
if (result.success) {
const requirements = result.data?.typeMachine?.pieceRequirements || []
machineRequirementCache[machineId] = { requirements }
}
} finally {
if (requestId === requirementRequestToken) {
requirementLoading.value = false
}
}
}
watch(
() => creationForm.machineId,
async (machineId) => {
creationForm.requirementId = ''
if (!machineId) {
return
}
await ensureMachineRequirements(machineId)
},
)
watch(
() => creationForm.requirementId,
(requirementId) => {
if (!selectedType.value) {
return
}
const requirement = requirementId ? selectedRequirement.value : null
const suggestion =
requirement?.label || requirement?.typePiece?.name || selectedType.value?.name || ''
if (!creationForm.name || creationForm.name === lastSuggestedName.value) {
creationForm.name = suggestion
}
lastSuggestedName.value = suggestion
},
)
const submitCreation = async () => {
if (!creationForm.machineId || !creationForm.requirementId) {
toast.showError('Sélectionnez une machine et un requirement avant de continuer.')
return
}
if (!selectedType.value) {
toast.showError('Aucune catégorie sélectionnée.')
return
}
const payload: Record<string, any> = {
machineId: creationForm.machineId,
typeMachinePieceRequirementId: creationForm.requirementId,
}
const requirement = selectedRequirement.value
if (selectedType.value.id) {
const requirementTypeId = requirement?.typePieceId || null
if (!requirementTypeId || requirementTypeId !== selectedType.value.id) {
payload.typePieceId = selectedType.value.id
}
}
const overrides = sanitizeDefinitionOverrides({
name: creationForm.name,
reference: creationForm.reference,
constructeurId: creationForm.constructeurId,
prix: creationForm.prix,
})
if (overrides) {
payload.definition = overrides
}
submitting.value = true
try {
const result = await createPiece(payload)
if (result.success) {
await loadPieceTypes()
closeCreationModal()
} else if (result.error) {
toast.showError(result.error)
}
} catch (error: any) {
toast.showError(error?.message || 'Erreur lors de la création de la pièce')
} finally {
submitting.value = false
}
}
onMounted(async () => {
await Promise.allSettled([
loadPieceTypes(),
loadMachines(),
])
})
</script>