set up new view for skeleton hiearchi

This commit is contained in:
Matthieu
2025-09-22 08:34:05 +02:00
parent e33e91ee26
commit 936a9d74ca
30 changed files with 4530 additions and 2288 deletions

View File

@@ -300,6 +300,99 @@
</div>
</div>
<!-- Requirement Summary -->
<div
v-if="componentRequirementGroups.length || pieceRequirementGroups.length"
class="card bg-base-100 shadow-lg"
>
<div class="card-body space-y-6">
<div>
<h2 class="card-title">Structure sélectionnée</h2>
<p class="text-sm text-gray-500">
Synthèse des familles définies dans le type et des modèles utilisés pour cette machine.
</p>
</div>
<div v-if="componentRequirementGroups.length" class="space-y-4">
<h3 class="text-sm font-semibold text-gray-700">Composants</h3>
<div
v-for="group in componentRequirementGroups"
:key="group.requirement.id"
class="rounded-lg border border-base-200 p-4"
>
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
<div>
<h4 class="font-medium text-sm">
{{ group.requirement.label || group.requirement.typeComposant?.name || 'Famille de composants' }}
</h4>
<p class="text-xs text-gray-500">
Type : {{ group.requirement.typeComposant?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
</p>
</div>
<span class="badge badge-outline badge-sm">{{ group.components.length }} composant(s)</span>
</div>
<div v-if="group.components.length" class="space-y-2">
<div
v-for="component in group.components"
:key="component.id"
class="flex flex-wrap items-center gap-2 text-sm"
>
<span class="font-medium">{{ component.name }}</span>
<span v-if="component.composantModel" class="badge badge-sm badge-primary badge-outline">
Modèle : {{ component.composantModel.name }}
</span>
<span v-else class="badge badge-sm badge-outline">Défini manuellement</span>
<span v-if="component.parentComposantId" class="text-xs text-gray-500">
(Sous-composant)
</span>
</div>
</div>
<p v-else class="text-xs text-gray-500">Aucun composant rattaché à ce groupe.</p>
</div>
</div>
<div v-if="pieceRequirementGroups.length" class="space-y-4">
<h3 class="text-sm font-semibold text-gray-700">Pièces principales</h3>
<div
v-for="group in pieceRequirementGroups"
:key="group.requirement.id"
class="rounded-lg border border-base-200 p-4"
>
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
<div>
<h4 class="font-medium text-sm">
{{ group.requirement.label || group.requirement.typePiece?.name || 'Groupe de pièces' }}
</h4>
<p class="text-xs text-gray-500">
Type : {{ group.requirement.typePiece?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
</p>
</div>
<span class="badge badge-outline badge-sm">{{ group.pieces.length }} pièce(s)</span>
</div>
<div v-if="group.pieces.length" class="space-y-2">
<div
v-for="piece in group.pieces"
:key="piece.id"
class="flex flex-wrap items-center gap-2 text-sm"
>
<span class="font-medium">{{ piece.name }}</span>
<span v-if="piece.pieceModel" class="badge badge-sm badge-primary badge-outline">
Modèle : {{ piece.pieceModel.name }}
</span>
<span v-else class="badge badge-sm badge-outline">Définie manuellement</span>
<span v-if="piece.parentComponentName" class="text-xs text-gray-500">
(Rattachée à {{ piece.parentComponentName }})
</span>
</div>
</div>
<p v-else class="text-xs text-gray-500">Aucune pièce rattachée à ce groupe.</p>
</div>
</div>
</div>
</div>
<!-- Components Section -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
@@ -327,8 +420,14 @@
:is-edit-mode="isEditMode"
:collapse-all="componentsCollapsed"
:toggle-token="collapseToggleToken"
:component-model-options-provider="getComponentModelOptions"
:piece-model-options-provider="getPieceModelOptions"
@update="updateComponent"
@edit-piece="updatePieceFromComponent"
@assign-model="assignComponentModel"
@assign-piece-model="assignPieceModel"
@custom-field-update="updatePieceCustomField"
@create-model-from-component="openSaveComponentModelModal"
/>
</div>
</div>
@@ -346,9 +445,11 @@
:key="piece.id"
:piece="piece"
:is-edit-mode="isEditMode"
:piece-model-options="getPieceModelOptions(piece)"
@update="updatePieceInfo"
@edit="editPiece"
@custom-field-update="updatePieceCustomField"
@assign-model="assignPieceModel"
/>
</div>
</div>
@@ -378,7 +479,73 @@
@select-all="setAllPrintSelection(true)"
@deselect-all="setAllPrintSelection(false)"
/>
</template>
<div v-if="saveComponentAsModelModal.open" class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg">
Enregistrer « {{ saveComponentAsModelModal.component?.name || 'Composant' }} » comme modèle
</h3>
<p class="text-xs text-gray-500 mb-4">
Le modèle sera associé au type {{ saveComponentAsModelModal.typeLabel || 'de composant' }} et
pourra être réutilisé lors de la configuration d'autres machines.
</p>
<form class="space-y-4" @submit.prevent="submitSaveComponentModelModal">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label"><span class="label-text">Nom du modèle</span></label>
<input
v-model="saveComponentAsModelModal.form.name"
type="text"
class="input input-bordered input-sm"
required
/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Type de composant</span></label>
<select
v-model="saveComponentAsModelModal.form.typeComposantId"
class="select select-bordered select-sm"
required
disabled
>
<option :value="saveComponentAsModelModal.form.typeComposantId">
{{ saveComponentAsModelModal.typeLabel || 'Type de composant' }}
</option>
</select>
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="saveComponentAsModelModal.form.description"
class="textarea textarea-bordered textarea-sm"
rows="3"
placeholder="Notes optionnelles"
></textarea>
</div>
<div class="bg-base-200/60 border border-base-200 rounded-lg p-3 space-y-3">
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-500">
<span class="badge badge-outline badge-sm">{{ saveComponentStructureSummary }}</span>
</div>
<ModelStructureViewer :structure="saveComponentAsModelModal.structure" />
</div>
<div class="modal-action">
<button type="button" class="btn btn-outline" @click="closeSaveComponentModelModal">
Annuler
</button>
<button type="submit" class="btn btn-primary" :class="{ loading: saveComponentAsModelModal.submitting }">
Sauvegarder et assigner
</button>
</div>
</form>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'
@@ -405,6 +572,14 @@ import IconLucideSquarePen from '~icons/lucide/square-pen'
import IconLucideEye from '~icons/lucide/eye'
import IconLucideChevronRight from '~icons/lucide/chevron-right'
import IconLucidePrinter from '~icons/lucide/printer'
import ModelStructureViewer from '~/components/ModelStructureViewer.vue'
import { useComponentModels } from '~/composables/useComponentModels'
import { usePieceModels } from '~/composables/usePieceModels'
import {
defaultStructure,
extractStructureFromComponent,
formatStructurePreview,
} from '~/shared/modelUtils'
const route = useRoute()
const machineId = route.params.id
@@ -433,6 +608,16 @@ const {
loadDocumentsByComponent,
loadDocumentsByPiece
} = useDocuments()
const {
loadComponentModels,
getComponentModelsForType,
createComponentModel,
} = useComponentModels()
const {
loadPieceModels,
getPieceModelsForType,
} = usePieceModels()
const toast = useToast()
// Data
const loading = ref(true)
@@ -473,11 +658,62 @@ const printSelection = reactive({
pieces: {},
})
const saveComponentAsModelModal = reactive({
open: false,
submitting: false,
component: null,
typeLabel: '',
structure: defaultStructure(),
form: {
name: '',
description: '',
typeComposantId: '',
},
})
const saveComponentStructureSummary = computed(() =>
formatStructurePreview(saveComponentAsModelModal.structure)
)
const handleMachineConstructeurChange = async (value) => {
machineConstructeurId.value = value
await updateMachineInfo()
}
const openSaveComponentModelModal = (component) => {
if (!component) return
const requirement = component.typeMachineComponentRequirement || null
const typeComposantId = requirement?.typeComposantId
|| component.typeComposantId
|| component.typeComposant?.id
|| ''
saveComponentAsModelModal.open = true
saveComponentAsModelModal.submitting = false
saveComponentAsModelModal.component = component
saveComponentAsModelModal.typeLabel = requirement?.typeComposant?.name
|| component.typeComposant?.name
|| 'Composant'
saveComponentAsModelModal.form = {
name: component.name || '',
description: component.description || '',
typeComposantId,
}
saveComponentAsModelModal.structure = extractStructureFromComponent(component)
}
const closeSaveComponentModelModal = () => {
saveComponentAsModelModal.open = false
saveComponentAsModelModal.component = null
saveComponentAsModelModal.typeLabel = ''
saveComponentAsModelModal.structure = defaultStructure()
saveComponentAsModelModal.form = {
name: '',
description: '',
typeComposantId: '',
}
}
// Mode d'édition
const isEditMode = ref(false)
const debug = ref(false) // Ajout de debug pour afficher les infos de debug
@@ -521,16 +757,178 @@ const machinePieces = computed(() => {
const machineDocumentsList = computed(() => machine.value?.documents || [])
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
const allComponents = computed(() => {
return components.value
})
const subComponents = (parentId) => {
return components.value.filter(comp => comp.parentComposantId === parentId)
const flattenComponents = (list = []) => {
const result = []
const traverse = (items) => {
items.forEach((item) => {
result.push(item)
if (item.subComponents && item.subComponents.length) {
traverse(item.subComponents)
}
})
}
traverse(list)
return result
}
const componentPieces = (composantId) => {
return pieces.value.filter(piece => piece.composantId === composantId)
const flattenedComponents = computed(() => flattenComponents(components.value))
const preloadModelsForTypeMachine = async (typeMachine) => {
if (!typeMachine) return
const componentTypeIds = new Set(
(typeMachine.componentRequirements || [])
.map((requirement) => requirement.typeComposantId)
.filter(Boolean),
)
const pieceTypeIds = new Set(
(typeMachine.pieceRequirements || [])
.map((requirement) => requirement.typePieceId)
.filter(Boolean),
)
await Promise.all([
...Array.from(componentTypeIds).map((id) => loadComponentModels(id)),
...Array.from(pieceTypeIds).map((id) => loadPieceModels(id)),
])
}
const ensureModelsForExistingEntities = async () => {
const componentTypeIds = new Set()
const gatherComponentTypes = (items = []) => {
items.forEach((item) => {
const typeId = item.typeMachineComponentRequirement?.typeComposantId
|| item.typeComposantId
|| item.typeComposant?.id
if (typeId) {
componentTypeIds.add(typeId)
}
if (item.subComponents?.length) {
gatherComponentTypes(item.subComponents)
}
})
}
gatherComponentTypes(components.value)
const pieceTypeIds = new Set()
pieces.value.forEach((piece) => {
const typeId = piece.typeMachinePieceRequirement?.typePieceId
|| piece.typePieceId
|| piece.typePiece?.id
if (typeId) {
pieceTypeIds.add(typeId)
}
})
await Promise.all([
...Array.from(componentTypeIds).map((id) => loadComponentModels(id)),
...Array.from(pieceTypeIds).map((id) => loadPieceModels(id)),
])
}
const componentRequirementGroups = computed(() => {
const requirements = machine.value?.typeMachine?.componentRequirements || []
if (!requirements.length) return []
const groups = requirements.map((requirement) => ({
requirement,
components: [],
}))
const map = new Map(groups.map((group) => [group.requirement.id, group]))
flattenedComponents.value.forEach((component) => {
const reqId = component.typeMachineComponentRequirementId
if (reqId && map.has(reqId)) {
map.get(reqId).components.push(component)
}
})
return groups
})
const pieceRequirementGroups = computed(() => {
const requirements = machine.value?.typeMachine?.pieceRequirements || []
if (!requirements.length) return []
const groups = requirements.map((requirement) => ({
requirement,
pieces: [],
}))
const map = new Map(groups.map((group) => [group.requirement.id, group]))
const collectPieces = () => {
const collected = []
// Pièces rattachées à la machine directement
machinePieces.value.forEach((piece) => {
collected.push({ ...piece, parentComponentName: null })
})
// Pièces rattachées aux composants
flattenedComponents.value.forEach((component) => {
if (component.pieces && component.pieces.length) {
component.pieces.forEach((piece) => {
collected.push({ ...piece, parentComponentName: component.name })
})
}
})
return collected
}
collectPieces().forEach((piece) => {
const reqId = piece.typeMachinePieceRequirementId
if (reqId && map.has(reqId)) {
map.get(reqId).pieces.push(piece)
}
})
return groups
})
const getComponentModelOptions = (entity) => {
const requirement = entity?.typeMachineComponentRequirement
const typeId = requirement?.typeComposantId
|| entity?.typeComposantId
|| entity?.typeComposant?.id
if (!typeId) return []
return getComponentModelsForType(typeId)
}
const getPieceModelOptions = (entity) => {
const requirement = entity?.typeMachinePieceRequirement
const typeId = requirement?.typePieceId
|| entity?.typePieceId
|| entity?.typePiece?.id
if (!typeId) return []
return getPieceModelsForType(typeId)
}
const findComponentById = (items, id) => {
for (const item of items || []) {
if (item.id === id) return item
const found = findComponentById(item.subComponents, id)
if (found) return found
}
return null
}
const findPieceById = (pieceId) => {
const direct = pieces.value.find((piece) => piece.id === pieceId)
if (direct) return direct
const searchInComponents = (items) => {
for (const item of items || []) {
const match = (item.pieces || []).find((piece) => piece.id === pieceId)
if (match) return match
const nested = searchInComponents(item.subComponents)
if (nested) return nested
}
return null
}
return searchInComponents(components.value)
}
const refreshMachineDocuments = async () => {
@@ -756,7 +1154,12 @@ const transformComponentCustomFields = (componentsData) => {
})) || [];
// Transform pieces for the current component
const pieces = component.pieces ? transformCustomFields(component.pieces) : [];
const pieces = component.pieces
? transformCustomFields(component.pieces).map((piece) => ({
...piece,
parentComponentName: component.name,
}))
: [];
// Recursively transform sub-components (using 'sousComposants' from backend)
const subComponents = component.sousComposants ? transformComponentCustomFields(component.sousComposants) : [];
@@ -798,6 +1201,8 @@ const loadMachineData = async () => {
machine.value.documents = machine.value.documents || []
machineDocumentsLoaded.value = !!(machine.value.documents?.length)
console.log('Machine trouvée et assignée:', machine.value)
await preloadModelsForTypeMachine(machine.value.typeMachine)
} else {
console.error('Machine non trouvée:', machineId)
console.error('Erreur API:', machineResult.error)
@@ -833,6 +1238,7 @@ const loadMachineData = async () => {
}
})
console.log('=== FIN HIÉRARCHIE ===')
await ensureModelsForExistingEntities()
} else {
console.error('Erreur lors du chargement des composants:', componentsResult.error)
}
@@ -843,6 +1249,7 @@ const loadMachineData = async () => {
pieces.value = transformCustomFields(machine.value.pieces)
console.log('Pièces transformées:', pieces.value)
console.log('Pièces chargées:', pieces.value.length)
await ensureModelsForExistingEntities()
} else {
console.log('Aucune pièce trouvée dans la réponse de la machine')
}
@@ -887,10 +1294,12 @@ const updateComponent = async (updatedComponent) => {
reference: updatedComponent.reference,
constructeurId: updatedComponent.constructeurId || updatedComponent.constructeur?.id || null,
emplacement: updatedComponent.emplacement,
prix: prixValue && prixValue !== '' ? parseFloat(prixValue) : null
prix: prixValue && prixValue !== '' ? parseFloat(prixValue) : null,
composantModelId: updatedComponent.composantModelId || updatedComponent.composantModel?.id || null,
})
if (result.success) {
Object.assign(updatedComponent, result.data)
const transformed = transformComponentCustomFields([result.data])[0]
Object.assign(updatedComponent, transformed)
}
} catch (error) {
console.error('Erreur lors de la mise à jour du composant:', error)
@@ -904,10 +1313,12 @@ const updatePieceFromComponent = async (updatedPiece) => {
reference: updatedPiece.reference,
constructeurId: updatedPiece.constructeurId || updatedPiece.constructeur?.id || null,
emplacement: updatedPiece.emplacement,
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null,
pieceModelId: updatedPiece.pieceModelId || updatedPiece.pieceModel?.id || null,
})
if (result.success) {
Object.assign(updatedPiece, result.data)
const transformed = transformCustomFields([result.data])[0]
Object.assign(updatedPiece, transformed)
// Si la pièce a des champs personnalisés mis à jour, les traiter
if (updatedPiece.customFields) {
for (const field of updatedPiece.customFields) {
@@ -934,16 +1345,131 @@ const updatePieceInfo = async (updatedPiece) => {
reference: updatedPiece.reference,
constructeurId: updatedPiece.constructeurId || updatedPiece.constructeur?.id || null,
emplacement: updatedPiece.emplacement,
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null,
pieceModelId: updatedPiece.pieceModelId || updatedPiece.pieceModel?.id || null,
})
if (result.success) {
Object.assign(updatedPiece, result.data)
const transformed = transformCustomFields([result.data])[0]
Object.assign(updatedPiece, transformed)
}
} catch (error) {
console.error('Erreur lors de la mise à jour de la pièce:', error)
}
}
const assignComponentModel = async ({ componentId, composantModelId, previousModelId, previousModel }) => {
if (!componentId) return
try {
const result = await updateComposantApi(componentId, {
composantModelId: composantModelId || null,
})
if (result.success) {
const transformed = transformComponentCustomFields([result.data])[0]
const target = findComponentById(components.value, componentId)
if (target) {
Object.assign(target, transformed)
}
} else {
const target = findComponentById(components.value, componentId)
if (target) {
target.composantModelId = previousModelId || null
target.composantModel = previousModel || null
}
toast.showError(result.error || 'Impossible de mettre à jour le modèle du composant')
}
} catch (error) {
console.error('Erreur lors de l\'assignation du modèle de composant:', error)
const target = findComponentById(components.value, componentId)
if (target) {
target.composantModelId = previousModelId || null
target.composantModel = previousModel || null
}
toast.showError('Impossible de mettre à jour le modèle du composant')
}
}
const submitSaveComponentModelModal = async () => {
if (!saveComponentAsModelModal.form.typeComposantId) {
toast.showError('Type de composant manquant pour créer le modèle')
return
}
if (!saveComponentAsModelModal.form.name.trim()) {
toast.showError('Veuillez renseigner un nom de modèle')
return
}
saveComponentAsModelModal.submitting = true
try {
const payload = {
name: saveComponentAsModelModal.form.name.trim(),
description: saveComponentAsModelModal.form.description.trim() || undefined,
typeComposantId: saveComponentAsModelModal.form.typeComposantId,
structure: saveComponentAsModelModal.structure,
}
const result = await createComponentModel(payload)
if (!result.success) {
toast.showError(result.error || 'Impossible de créer le modèle de composant')
return
}
const newModel = result.data
if (newModel?.typeComposantId) {
await loadComponentModels(newModel.typeComposantId)
}
const sourceComponent = saveComponentAsModelModal.component
if (sourceComponent && newModel?.id) {
await assignComponentModel({
componentId: sourceComponent.id,
composantModelId: newModel.id,
previousModelId: sourceComponent.composantModelId,
previousModel: sourceComponent.composantModel,
})
}
closeSaveComponentModelModal()
toast.showSuccess('Composant enregistré comme modèle')
} catch (error) {
console.error('Erreur lors de la création du modèle de composant:', error)
toast.showError('Impossible de créer le modèle de composant')
} finally {
saveComponentAsModelModal.submitting = false
}
}
const assignPieceModel = async ({ pieceId, pieceModelId, previousModelId, previousModel }) => {
if (!pieceId) return
try {
const result = await updatePieceApi(pieceId, {
pieceModelId: pieceModelId || null,
})
if (result.success) {
const transformed = transformCustomFields([result.data])[0]
const target = findPieceById(pieceId)
if (target) {
Object.assign(target, transformed)
}
} else {
const target = findPieceById(pieceId)
if (target) {
target.pieceModelId = previousModelId || null
target.pieceModel = previousModel || null
}
toast.showError(result.error || 'Impossible de mettre à jour le modèle de pièce')
}
} catch (error) {
console.error('Erreur lors de l\'assignation du modèle de pièce:', error)
const target = findPieceById(pieceId)
if (target) {
target.pieceModelId = previousModelId || null
target.pieceModel = previousModel || null
}
toast.showError('Impossible de mettre à jour le modèle de pièce')
}
}
// Méthodes pour les champs personnalisés de la machine
const setMachineCustomFieldValue = (fieldValueId, value) => {
const fieldValue = machine.value?.customFieldValues?.find(fv => fv.id === fieldValueId)