feat: allow machine skeleton reconfiguration
This commit is contained in:
@@ -73,6 +73,30 @@ export function useMachines() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reconfigureSkeleton = async (machineId, payload) => {
|
||||||
|
if (!machineId) {
|
||||||
|
return { success: false, error: 'Identifiant de machine manquant' }
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await patch(`/machines/${machineId}/skeleton`, payload)
|
||||||
|
if (result.success) {
|
||||||
|
const index = machines.value.findIndex(machine => machine.id === machineId)
|
||||||
|
if (index !== -1) {
|
||||||
|
machines.value[index] = result.data?.machine || result.data
|
||||||
|
}
|
||||||
|
showSuccess('Structure de la machine mise à jour avec succès')
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la reconfiguration du squelette de la machine:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deleteMachine = async (id) => {
|
const deleteMachine = async (id) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -113,6 +137,7 @@ export function useMachines() {
|
|||||||
createMachine,
|
createMachine,
|
||||||
createMachineFromType,
|
createMachineFromType,
|
||||||
updateMachine: updateMachineData,
|
updateMachine: updateMachineData,
|
||||||
|
reconfigureSkeleton,
|
||||||
deleteMachine,
|
deleteMachine,
|
||||||
getMachineById,
|
getMachineById,
|
||||||
getMachinesBySite,
|
getMachinesBySite,
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<h1 class="text-3xl font-bold">Détails de la machine</h1>
|
<h1 class="text-3xl font-bold">Détails de la machine</h1>
|
||||||
<div class="flex items-center gap-2 print:hidden" data-print-hide>
|
<div class="flex items-center gap-2 print:hidden" data-print-hide>
|
||||||
<button
|
<button
|
||||||
@click="toggleEditMode"
|
@click="toggleEditMode"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
:class="{ 'btn-outline': isEditMode }"
|
:class="{ 'btn-outline': isEditMode }"
|
||||||
>
|
>
|
||||||
@@ -34,6 +34,15 @@
|
|||||||
/>
|
/>
|
||||||
{{ isEditMode ? 'Voir détails' : 'Modifier' }}
|
{{ isEditMode ? 'Voir détails' : 'Modifier' }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="machineHasSkeletonRequirements"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline"
|
||||||
|
:disabled="skeletonEditor.open"
|
||||||
|
@click="openSkeletonEditor"
|
||||||
|
>
|
||||||
|
Modifier les éléments du squelette
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!isEditMode"
|
v-if="!isEditMode"
|
||||||
@click="openPrintModal"
|
@click="openPrintModal"
|
||||||
@@ -68,6 +77,323 @@
|
|||||||
</div>
|
</div>
|
||||||
</PageHero>
|
</PageHero>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="skeletonEditor.open"
|
||||||
|
class="card bg-base-100 shadow-lg border border-primary/20"
|
||||||
|
>
|
||||||
|
<div class="card-body space-y-6">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="card-title">Modifier les éléments du squelette</h2>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Sélectionnez les composants et pièces à associer à cette machine selon les exigences du type.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
:disabled="skeletonEditor.submitting"
|
||||||
|
@click="closeSkeletonEditor"
|
||||||
|
>
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="skeletonEditor.loading" class="flex justify-center py-12">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="!machineHasSkeletonRequirements" class="text-sm text-gray-500">
|
||||||
|
Ce type de machine ne possède pas de squelette structuré.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="componentRequirements.length" class="space-y-4">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700">Sélection des composants</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="requirement in componentRequirements"
|
||||||
|
:key="requirement.id"
|
||||||
|
class="border border-base-200 rounded-lg p-4 space-y-3"
|
||||||
|
:id="`component-reconfigure-${requirement.id}`"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium text-sm">
|
||||||
|
{{ requirement.label || requirement.typeComposant?.name || 'Famille de composants' }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
Type : {{ requirement.typeComposant?.name || 'Non défini' }} · Min : {{ requirement.minCount ?? (requirement.required ? 1 : 0) }}
|
||||||
|
· Max : {{ requirement.maxCount ?? '∞' }} ·
|
||||||
|
{{ requirement.allowNewModels ? 'Nouveaux modèles autorisés' : 'Modèles existants uniquement' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline"
|
||||||
|
@click="addComponentSelectionEntry(requirement)"
|
||||||
|
:disabled="requirement.maxCount !== null && getComponentRequirementEntries(requirement.id).length >= requirement.maxCount"
|
||||||
|
>
|
||||||
|
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="getComponentRequirementEntries(requirement.id).length === 0" class="text-xs text-gray-500">
|
||||||
|
Aucun composant sélectionné pour ce groupe.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(entry, entryIndex) in getComponentRequirementEntries(requirement.id)"
|
||||||
|
:key="`${requirement.id}-${entryIndex}`"
|
||||||
|
class="bg-base-200/60 rounded-md p-3 space-y-3"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||||
|
<label class="inline-flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="radio radio-xs"
|
||||||
|
:checked="entry.mode === 'model'"
|
||||||
|
@change="setComponentSelectionMode(requirement.id, entryIndex, 'model')"
|
||||||
|
/>
|
||||||
|
Modèle existant
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="radio radio-xs"
|
||||||
|
:checked="entry.mode === 'manual'"
|
||||||
|
@change="setComponentSelectionMode(requirement.id, entryIndex, 'manual')"
|
||||||
|
:disabled="!requirement.allowNewModels"
|
||||||
|
/>
|
||||||
|
Définir manuellement
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="entry.mode === 'model'" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs">Modèle de composant</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-sm"
|
||||||
|
:value="entry.componentModelId || ''"
|
||||||
|
@change="updateComponentSelectionEntry(requirement.id, entryIndex, { componentModelId: $event.target.value || '' })"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un modèle</option>
|
||||||
|
<option
|
||||||
|
v-for="model in getComponentModelsForType(requirement.typeComposantId)"
|
||||||
|
:key="model.id"
|
||||||
|
:value="model.id"
|
||||||
|
>
|
||||||
|
{{ model.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p v-if="loadingComponentModels" class="text-[10px] text-gray-500 mt-1">Chargement des modèles...</p>
|
||||||
|
<p
|
||||||
|
v-else-if="getComponentModelsForType(requirement.typeComposantId).length === 0"
|
||||||
|
class="text-[10px] text-gray-500 mt-1"
|
||||||
|
>
|
||||||
|
Aucun modèle disponible pour ce type.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs">Nom du composant</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm"
|
||||||
|
:value="entry.name"
|
||||||
|
placeholder="Nom du composant"
|
||||||
|
@input="updateComponentSelectionEntry(requirement.id, entryIndex, { name: $event.target.value })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs">Référence (optionnel)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm"
|
||||||
|
placeholder="(Non géré pour l'instant)"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-square btn-xs btn-error"
|
||||||
|
@click="removeComponentSelectionEntry(requirement.id, entryIndex)"
|
||||||
|
>
|
||||||
|
<IconLucideX class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="pieceRequirements.length" class="space-y-4">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700">Sélection des pièces principales</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="requirement in pieceRequirements"
|
||||||
|
:key="requirement.id"
|
||||||
|
class="border border-base-200 rounded-lg p-4 space-y-3"
|
||||||
|
:id="`piece-reconfigure-${requirement.id}`"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium text-sm">
|
||||||
|
{{ requirement.label || requirement.typePiece?.name || 'Groupe de pièces' }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
Type : {{ requirement.typePiece?.name || 'Non défini' }} · Min : {{ requirement.minCount ?? (requirement.required ? 1 : 0) }}
|
||||||
|
· Max : {{ requirement.maxCount ?? '∞' }} ·
|
||||||
|
{{ requirement.allowNewModels ? 'Nouveaux modèles autorisés' : 'Modèles existants uniquement' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline"
|
||||||
|
@click="addPieceSelectionEntry(requirement)"
|
||||||
|
:disabled="requirement.maxCount !== null && getPieceRequirementEntries(requirement.id).length >= requirement.maxCount"
|
||||||
|
>
|
||||||
|
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="getPieceRequirementEntries(requirement.id).length === 0" class="text-xs text-gray-500">
|
||||||
|
Aucune pièce sélectionnée pour ce groupe.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(entry, entryIndex) in getPieceRequirementEntries(requirement.id)"
|
||||||
|
:key="`${requirement.id}-piece-${entryIndex}`"
|
||||||
|
class="bg-base-200/60 rounded-md p-3 space-y-3"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||||
|
<label class="inline-flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="radio radio-xs"
|
||||||
|
:checked="entry.mode === 'model'"
|
||||||
|
@change="setPieceSelectionMode(requirement.id, entryIndex, 'model')"
|
||||||
|
/>
|
||||||
|
Modèle existant
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="radio radio-xs"
|
||||||
|
:checked="entry.mode === 'manual'"
|
||||||
|
@change="setPieceSelectionMode(requirement.id, entryIndex, 'manual')"
|
||||||
|
:disabled="!requirement.allowNewModels"
|
||||||
|
/>
|
||||||
|
Définir manuellement
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="entry.mode === 'model'" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs">Modèle de pièce</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-sm"
|
||||||
|
:value="entry.pieceModelId || ''"
|
||||||
|
@change="updatePieceSelectionEntry(requirement.id, entryIndex, { pieceModelId: $event.target.value || '' })"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un modèle</option>
|
||||||
|
<option
|
||||||
|
v-for="model in getPieceModelsForType(requirement.typePieceId)"
|
||||||
|
:key="model.id"
|
||||||
|
:value="model.id"
|
||||||
|
>
|
||||||
|
{{ model.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p v-if="loadingPieceModels" class="text-[10px] text-gray-500 mt-1">Chargement des modèles...</p>
|
||||||
|
<p
|
||||||
|
v-else-if="getPieceModelsForType(requirement.typePieceId).length === 0"
|
||||||
|
class="text-[10px] text-gray-500 mt-1"
|
||||||
|
>
|
||||||
|
Aucun modèle disponible pour ce type.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs">Nom de la pièce</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm"
|
||||||
|
:value="entry.name"
|
||||||
|
placeholder="Nom de la pièce"
|
||||||
|
@input="updatePieceSelectionEntry(requirement.id, entryIndex, { name: $event.target.value })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs">Référence (optionnel)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm"
|
||||||
|
placeholder="(Non géré pour l'instant)"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-square btn-xs btn-error"
|
||||||
|
@click="removePieceSelectionEntry(requirement.id, entryIndex)"
|
||||||
|
>
|
||||||
|
<IconLucideX class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap justify-end gap-2 pt-2 border-t border-base-200">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost"
|
||||||
|
:disabled="skeletonEditor.submitting"
|
||||||
|
@click="closeSkeletonEditor"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:class="{ loading: skeletonEditor.submitting }"
|
||||||
|
@click="saveSkeletonConfiguration"
|
||||||
|
>
|
||||||
|
Sauvegarder la configuration
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Machine Info Card -->
|
<!-- Machine Info Card -->
|
||||||
<div class="card bg-base-100 shadow-lg">
|
<div class="card bg-base-100 shadow-lg">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -572,6 +898,8 @@ import IconLucideSquarePen from '~icons/lucide/square-pen'
|
|||||||
import IconLucideEye from '~icons/lucide/eye'
|
import IconLucideEye from '~icons/lucide/eye'
|
||||||
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
||||||
import IconLucidePrinter from '~icons/lucide/printer'
|
import IconLucidePrinter from '~icons/lucide/printer'
|
||||||
|
import IconLucidePlus from '~icons/lucide/plus'
|
||||||
|
import IconLucideX from '~icons/lucide/x'
|
||||||
import ModelStructureViewer from '~/components/ModelStructureViewer.vue'
|
import ModelStructureViewer from '~/components/ModelStructureViewer.vue'
|
||||||
import { useComponentModels } from '~/composables/useComponentModels'
|
import { useComponentModels } from '~/composables/useComponentModels'
|
||||||
import { usePieceModels } from '~/composables/usePieceModels'
|
import { usePieceModels } from '~/composables/usePieceModels'
|
||||||
@@ -590,7 +918,7 @@ if (!machineId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
const { updateMachine: updateMachineApi } = useMachines()
|
const { updateMachine: updateMachineApi, reconfigureSkeleton: reconfigureMachineSkeleton } = useMachines()
|
||||||
const {
|
const {
|
||||||
getComposantsByMachine,
|
getComposantsByMachine,
|
||||||
updateComposant: updateComposantApi
|
updateComposant: updateComposantApi
|
||||||
@@ -612,10 +940,12 @@ const {
|
|||||||
loadComponentModels,
|
loadComponentModels,
|
||||||
getComponentModelsForType,
|
getComponentModelsForType,
|
||||||
createComponentModel,
|
createComponentModel,
|
||||||
|
loadingComponentModels,
|
||||||
} = useComponentModels()
|
} = useComponentModels()
|
||||||
const {
|
const {
|
||||||
loadPieceModels,
|
loadPieceModels,
|
||||||
getPieceModelsForType,
|
getPieceModelsForType,
|
||||||
|
loadingPieceModels,
|
||||||
} = usePieceModels()
|
} = usePieceModels()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
@@ -675,6 +1005,392 @@ const saveComponentStructureSummary = computed(() =>
|
|||||||
formatStructurePreview(saveComponentAsModelModal.structure)
|
formatStructurePreview(saveComponentAsModelModal.structure)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const skeletonEditor = reactive({
|
||||||
|
open: false,
|
||||||
|
loading: false,
|
||||||
|
submitting: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const componentRequirementSelections = reactive({})
|
||||||
|
const pieceRequirementSelections = reactive({})
|
||||||
|
|
||||||
|
const machineType = computed(() => machine.value?.typeMachine || null)
|
||||||
|
const componentRequirements = computed(() => machineType.value?.componentRequirements || [])
|
||||||
|
const pieceRequirements = computed(() => machineType.value?.pieceRequirements || [])
|
||||||
|
const machineHasSkeletonRequirements = computed(() =>
|
||||||
|
componentRequirements.value.length > 0 || pieceRequirements.value.length > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const getComponentRequirementEntries = (requirementId) => {
|
||||||
|
return componentRequirementSelections[requirementId] || []
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPieceRequirementEntries = (requirementId) => {
|
||||||
|
return pieceRequirementSelections[requirementId] || []
|
||||||
|
}
|
||||||
|
|
||||||
|
const createComponentSelectionEntry = () => ({
|
||||||
|
mode: 'model',
|
||||||
|
componentModelId: '',
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const createPieceSelectionEntry = () => ({
|
||||||
|
mode: 'model',
|
||||||
|
pieceModelId: '',
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetSkeletonRequirementSelections = () => {
|
||||||
|
Object.keys(componentRequirementSelections).forEach((key) => {
|
||||||
|
delete componentRequirementSelections[key]
|
||||||
|
})
|
||||||
|
Object.keys(pieceRequirementSelections).forEach((key) => {
|
||||||
|
delete pieceRequirementSelections[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addComponentSelectionEntry = (requirement) => {
|
||||||
|
const entries = getComponentRequirementEntries(requirement.id)
|
||||||
|
const max = requirement.maxCount ?? null
|
||||||
|
if (max !== null && entries.length >= max) {
|
||||||
|
toast.showError(
|
||||||
|
`Vous ne pouvez pas ajouter plus de ${max} composant(s) pour ${requirement.label || requirement.typeComposant?.name || 'ce groupe'}`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
componentRequirementSelections[requirement.id] = [...entries, createComponentSelectionEntry()]
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeComponentSelectionEntry = (requirementId, index) => {
|
||||||
|
const entries = getComponentRequirementEntries(requirementId)
|
||||||
|
componentRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setComponentSelectionMode = (requirementId, index, mode) => {
|
||||||
|
const entries = getComponentRequirementEntries(requirementId)
|
||||||
|
componentRequirementSelections[requirementId] = entries.map((entry, i) => {
|
||||||
|
if (i !== index) return entry
|
||||||
|
if (mode === 'model') {
|
||||||
|
return { ...entry, mode: 'model', componentModelId: entry.componentModelId || '', name: '' }
|
||||||
|
}
|
||||||
|
return { ...entry, mode: 'manual', componentModelId: '', name: entry.name || '' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateComponentSelectionEntry = (requirementId, index, patch) => {
|
||||||
|
const entries = getComponentRequirementEntries(requirementId)
|
||||||
|
componentRequirementSelections[requirementId] = entries.map((entry, i) =>
|
||||||
|
i === index ? { ...entry, ...patch } : entry
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPieceSelectionEntry = (requirement) => {
|
||||||
|
const entries = getPieceRequirementEntries(requirement.id)
|
||||||
|
const max = requirement.maxCount ?? null
|
||||||
|
if (max !== null && entries.length >= max) {
|
||||||
|
toast.showError(
|
||||||
|
`Vous ne pouvez pas ajouter plus de ${max} pièce(s) pour ${requirement.label || requirement.typePiece?.name || 'ce groupe'}`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pieceRequirementSelections[requirement.id] = [...entries, createPieceSelectionEntry()]
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePieceSelectionEntry = (requirementId, index) => {
|
||||||
|
const entries = getPieceRequirementEntries(requirementId)
|
||||||
|
pieceRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPieceSelectionMode = (requirementId, index, mode) => {
|
||||||
|
const entries = getPieceRequirementEntries(requirementId)
|
||||||
|
pieceRequirementSelections[requirementId] = entries.map((entry, i) => {
|
||||||
|
if (i !== index) return entry
|
||||||
|
if (mode === 'model') {
|
||||||
|
return { ...entry, mode: 'model', pieceModelId: entry.pieceModelId || '', name: '' }
|
||||||
|
}
|
||||||
|
return { ...entry, mode: 'manual', pieceModelId: '', name: entry.name || '' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePieceSelectionEntry = (requirementId, index, patch) => {
|
||||||
|
const entries = getPieceRequirementEntries(requirementId)
|
||||||
|
pieceRequirementSelections[requirementId] = entries.map((entry, i) =>
|
||||||
|
i === index ? { ...entry, ...patch } : entry
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectPiecesForSkeleton = () => {
|
||||||
|
const aggregated = []
|
||||||
|
machinePieces.value.forEach((piece) => {
|
||||||
|
aggregated.push(piece)
|
||||||
|
})
|
||||||
|
flattenedComponents.value.forEach((component) => {
|
||||||
|
;(component.pieces || []).forEach((piece) => {
|
||||||
|
aggregated.push(piece)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return aggregated
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeSkeletonRequirementSelections = async () => {
|
||||||
|
skeletonEditor.loading = true
|
||||||
|
try {
|
||||||
|
resetSkeletonRequirementSelections()
|
||||||
|
const type = machineType.value
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentTypeIds = new Set()
|
||||||
|
;(type.componentRequirements || []).forEach((requirement) => {
|
||||||
|
if (requirement.typeComposantId) {
|
||||||
|
componentTypeIds.add(requirement.typeComposantId)
|
||||||
|
}
|
||||||
|
const existingComponents = flattenedComponents.value.filter(
|
||||||
|
(component) => component.typeMachineComponentRequirementId === requirement.id
|
||||||
|
)
|
||||||
|
const entries = existingComponents.map((component) => {
|
||||||
|
const modelId = component.composantModelId || component.composantModel?.id || null
|
||||||
|
if (modelId) {
|
||||||
|
return { mode: 'model', componentModelId: modelId, name: '' }
|
||||||
|
}
|
||||||
|
return { mode: 'manual', componentModelId: '', name: component.name || '' }
|
||||||
|
})
|
||||||
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
||||||
|
while (entries.length < min) {
|
||||||
|
entries.push(createComponentSelectionEntry())
|
||||||
|
}
|
||||||
|
componentRequirementSelections[requirement.id] = entries.length ? entries : []
|
||||||
|
})
|
||||||
|
|
||||||
|
const pieceTypeIds = new Set()
|
||||||
|
const allPieces = collectPiecesForSkeleton()
|
||||||
|
;(type.pieceRequirements || []).forEach((requirement) => {
|
||||||
|
if (requirement.typePieceId) {
|
||||||
|
pieceTypeIds.add(requirement.typePieceId)
|
||||||
|
}
|
||||||
|
const existingPieces = allPieces.filter(
|
||||||
|
(piece) => piece.typeMachinePieceRequirementId === requirement.id
|
||||||
|
)
|
||||||
|
const entries = existingPieces.map((piece) => {
|
||||||
|
const modelId = piece.pieceModelId || piece.pieceModel?.id || null
|
||||||
|
if (modelId) {
|
||||||
|
return { mode: 'model', pieceModelId: modelId, name: '' }
|
||||||
|
}
|
||||||
|
return { mode: 'manual', pieceModelId: '', name: piece.name || '' }
|
||||||
|
})
|
||||||
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
||||||
|
while (entries.length < min) {
|
||||||
|
entries.push(createPieceSelectionEntry())
|
||||||
|
}
|
||||||
|
pieceRequirementSelections[requirement.id] = entries.length ? entries : []
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
...Array.from(componentTypeIds).filter(Boolean).map((id) => loadComponentModels(id)),
|
||||||
|
...Array.from(pieceTypeIds).filter(Boolean).map((id) => loadPieceModels(id)),
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
skeletonEditor.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSkeletonEditor = async () => {
|
||||||
|
if (skeletonEditor.open) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
skeletonEditor.open = true
|
||||||
|
await initializeSkeletonRequirementSelections()
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSkeletonEditor = () => {
|
||||||
|
if (skeletonEditor.submitting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
skeletonEditor.open = false
|
||||||
|
skeletonEditor.loading = false
|
||||||
|
skeletonEditor.submitting = false
|
||||||
|
resetSkeletonRequirementSelections()
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateSkeletonSelections = (type) => {
|
||||||
|
const errors = []
|
||||||
|
const componentSelectionsPayload = []
|
||||||
|
const pieceSelectionsPayload = []
|
||||||
|
|
||||||
|
for (const requirement of type.componentRequirements || []) {
|
||||||
|
const entries = getComponentRequirementEntries(requirement.id)
|
||||||
|
const usableEntries = entries.filter((entry) => {
|
||||||
|
if (entry.mode === 'model') {
|
||||||
|
return !!entry.componentModelId
|
||||||
|
}
|
||||||
|
return !!entry.name && entry.name.trim().length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
||||||
|
const max = requirement.maxCount ?? null
|
||||||
|
|
||||||
|
if (usableEntries.length < min) {
|
||||||
|
errors.push(
|
||||||
|
`Le groupe "${requirement.label || requirement.typeComposant?.name || 'Composants'}" nécessite au moins ${min} élément(s).`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== null && usableEntries.length > max) {
|
||||||
|
errors.push(
|
||||||
|
`Le groupe "${requirement.label || requirement.typeComposant?.name || 'Composants'}" ne peut dépasser ${max} élément(s).`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requirement.allowNewModels && usableEntries.some((entry) => entry.mode === 'manual')) {
|
||||||
|
errors.push(
|
||||||
|
`Le groupe "${requirement.label || requirement.typeComposant?.name || 'Composants'}" n'autorise que les modèles existants.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
usableEntries.forEach((entry) => {
|
||||||
|
if (entry.mode === 'model') {
|
||||||
|
componentSelectionsPayload.push({
|
||||||
|
requirementId: requirement.id,
|
||||||
|
componentModelId: entry.componentModelId,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
componentSelectionsPayload.push({
|
||||||
|
requirementId: requirement.id,
|
||||||
|
definition: {
|
||||||
|
name: entry.name.trim(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const requirement of type.pieceRequirements || []) {
|
||||||
|
const entries = getPieceRequirementEntries(requirement.id)
|
||||||
|
const usableEntries = entries.filter((entry) => {
|
||||||
|
if (entry.mode === 'model') {
|
||||||
|
return !!entry.pieceModelId
|
||||||
|
}
|
||||||
|
return !!entry.name && entry.name.trim().length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
||||||
|
const max = requirement.maxCount ?? null
|
||||||
|
|
||||||
|
if (usableEntries.length < min) {
|
||||||
|
errors.push(
|
||||||
|
`Le groupe "${requirement.label || requirement.typePiece?.name || 'Pièces'}" nécessite au moins ${min} élément(s).`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== null && usableEntries.length > max) {
|
||||||
|
errors.push(
|
||||||
|
`Le groupe "${requirement.label || requirement.typePiece?.name || 'Pièces'}" ne peut dépasser ${max} élément(s).`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requirement.allowNewModels && usableEntries.some((entry) => entry.mode === 'manual')) {
|
||||||
|
errors.push(
|
||||||
|
`Le groupe "${requirement.label || requirement.typePiece?.name || 'Pièces'}" n'autorise que les modèles existants.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
usableEntries.forEach((entry) => {
|
||||||
|
if (entry.mode === 'model') {
|
||||||
|
pieceSelectionsPayload.push({
|
||||||
|
requirementId: requirement.id,
|
||||||
|
pieceModelId: entry.pieceModelId,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
pieceSelectionsPayload.push({
|
||||||
|
requirementId: requirement.id,
|
||||||
|
definition: {
|
||||||
|
name: entry.name.trim(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return { valid: false, error: errors[0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
componentSelections: componentSelectionsPayload,
|
||||||
|
pieceSelections: pieceSelectionsPayload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applySkeletonReconfigurationResult = async (data) => {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
const updatedMachine = data.machine || data
|
||||||
|
if (updatedMachine) {
|
||||||
|
machine.value = {
|
||||||
|
...machine.value,
|
||||||
|
...updatedMachine,
|
||||||
|
documents: updatedMachine.documents || machine.value?.documents || [],
|
||||||
|
}
|
||||||
|
initMachineFields()
|
||||||
|
machineDocumentsLoaded.value = !!(machine.value.documents?.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newComponents = data.components ?? updatedMachine?.components ?? null
|
||||||
|
if (Array.isArray(newComponents)) {
|
||||||
|
components.value = transformComponentCustomFields(newComponents)
|
||||||
|
collapseAllComponents()
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPieces = data.pieces ?? updatedMachine?.pieces ?? null
|
||||||
|
if (Array.isArray(newPieces)) {
|
||||||
|
pieces.value = transformCustomFields(newPieces)
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureModelsForExistingEntities()
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSkeletonConfiguration = async () => {
|
||||||
|
if (!machine.value?.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = machineType.value
|
||||||
|
let payload = { componentSelections: [], pieceSelections: [] }
|
||||||
|
|
||||||
|
if (type && machineHasSkeletonRequirements.value) {
|
||||||
|
const validation = validateSkeletonSelections(type)
|
||||||
|
if (!validation.valid) {
|
||||||
|
toast.showError(validation.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
componentSelections: validation.componentSelections,
|
||||||
|
pieceSelections: validation.pieceSelections,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skeletonEditor.submitting = true
|
||||||
|
try {
|
||||||
|
const result = await reconfigureMachineSkeleton(machine.value.id, payload)
|
||||||
|
if (result.success) {
|
||||||
|
await applySkeletonReconfigurationResult(result.data)
|
||||||
|
skeletonEditor.open = false
|
||||||
|
resetSkeletonRequirementSelections()
|
||||||
|
} else if (result.error) {
|
||||||
|
toast.showError(result.error)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la reconfiguration du squelette de la machine:', error)
|
||||||
|
toast.showError('Erreur lors de la mise à jour des éléments du squelette')
|
||||||
|
} finally {
|
||||||
|
skeletonEditor.submitting = false
|
||||||
|
skeletonEditor.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleMachineConstructeurChange = async (value) => {
|
const handleMachineConstructeurChange = async (value) => {
|
||||||
machineConstructeurId.value = value
|
machineConstructeurId.value = value
|
||||||
await updateMachineInfo()
|
await updateMachineInfo()
|
||||||
|
|||||||
Reference in New Issue
Block a user