@@ -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()