From 57a08bb8c9854079f6b8477b2f245f43991bcd75 Mon Sep 17 00:00:00 2001 From: MatthieuTD <39524319+MatthieuTD@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:52:44 +0200 Subject: [PATCH] feat: allow machine skeleton reconfiguration --- app/composables/useMachines.js | 25 ++ app/pages/machine/[id].vue | 722 ++++++++++++++++++++++++++++++++- 2 files changed, 744 insertions(+), 3 deletions(-) diff --git a/app/composables/useMachines.js b/app/composables/useMachines.js index eb50e76..5b39196 100644 --- a/app/composables/useMachines.js +++ b/app/composables/useMachines.js @@ -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) => { loading.value = true try { @@ -113,6 +137,7 @@ export function useMachines() { createMachine, createMachineFromType, updateMachine: updateMachineData, + reconfigureSkeleton, deleteMachine, getMachineById, getMachinesBySite, diff --git a/app/pages/machine/[id].vue b/app/pages/machine/[id].vue index 9be742e..c34f8da 100644 --- a/app/pages/machine/[id].vue +++ b/app/pages/machine/[id].vue @@ -17,8 +17,8 @@

Détails de la machine

- + +
+ +
+ +
+ + +
+ +
@@ -572,6 +898,8 @@ 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 IconLucidePlus from '~icons/lucide/plus' +import IconLucideX from '~icons/lucide/x' import ModelStructureViewer from '~/components/ModelStructureViewer.vue' import { useComponentModels } from '~/composables/useComponentModels' import { usePieceModels } from '~/composables/usePieceModels' @@ -590,7 +918,7 @@ if (!machineId) { } // Composables -const { updateMachine: updateMachineApi } = useMachines() +const { updateMachine: updateMachineApi, reconfigureSkeleton: reconfigureMachineSkeleton } = useMachines() const { getComposantsByMachine, updateComposant: updateComposantApi @@ -612,10 +940,12 @@ const { loadComponentModels, getComponentModelsForType, createComponentModel, + loadingComponentModels, } = useComponentModels() const { loadPieceModels, getPieceModelsForType, + loadingPieceModels, } = usePieceModels() const toast = useToast() @@ -675,6 +1005,392 @@ const saveComponentStructureSummary = computed(() => 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) => { machineConstructeurId.value = value await updateMachineInfo()