/** * Machine skeleton editor — selection state, validation & save logic. * * Extracted from pages/machine/[id].vue (F1.1). * Manages the reactive selection state for component / piece / product * skeleton requirements, validation, and reconfiguration API calls. */ import { ref, reactive, computed } from 'vue' import { sanitizeDefinitionOverrides } from '~/shared/modelUtils' import { resolveIdentifier, extractParentLinkIdentifiers, } from '~/shared/utils/productDisplayUtils' import { uniqueConstructeurIds, } from '~/shared/constructeurUtils' import { resolveLinkArray } from '~/composables/useMachineHierarchy' import type { Ref, ComputedRef } from 'vue' type AnyRecord = Record export interface MachineSkeletonEditorDeps { machine: Ref components: Ref pieces: Ref machineComponentLinks: Ref machinePieceLinks: Ref machineProductLinks: Ref machineType: ComputedRef machineHasSkeletonRequirements: ComputedRef componentRequirements: ComputedRef pieceRequirements: ComputedRef productRequirements: ComputedRef componentTypeLabelMap: ComputedRef> pieceTypeLabelMap: ComputedRef> productInventory: ComputedRef flattenedComponents: ComputedRef machinePieces: ComputedRef machineDocumentsLoaded: Ref findProductById: (id: string | null | undefined) => AnyRecord | null findComponentById: (items: AnyRecord[] | undefined, id: string) => AnyRecord | null findPieceById: (id: string) => AnyRecord | null transformCustomFields: (pieces: AnyRecord[]) => AnyRecord[] transformComponentCustomFields: (components: AnyRecord[]) => AnyRecord[] applyMachineLinks: (source: AnyRecord) => boolean collapseAllComponents: () => void initMachineFields: () => void collectPiecesForSkeleton: () => AnyRecord[] constructeurs: Ref loadProducts: () => Promise reconfigureMachineSkeleton: (id: string, payload: AnyRecord) => Promise toast: { showError: (msg: string) => void; showInfo: (msg: string) => void } } export function useMachineSkeletonEditor(deps: MachineSkeletonEditorDeps) { const { machine, components, pieces, machineComponentLinks, machinePieceLinks, machineProductLinks, machineType, machineHasSkeletonRequirements, productRequirements, componentTypeLabelMap, pieceTypeLabelMap, productInventory, flattenedComponents, machineDocumentsLoaded, findProductById, findComponentById, findPieceById, transformCustomFields, transformComponentCustomFields, applyMachineLinks, collapseAllComponents, initMachineFields, collectPiecesForSkeleton, loadProducts, reconfigureMachineSkeleton, toast, } = deps // --------------------------------------------------------------------------- // View state // --------------------------------------------------------------------------- const activeMachineView = ref<'details' | 'skeleton'>('details') const isDetailsView = computed(() => activeMachineView.value === 'details') const isSkeletonView = computed(() => activeMachineView.value === 'skeleton') // --------------------------------------------------------------------------- // Editor state // --------------------------------------------------------------------------- const skeletonEditor = reactive({ open: false, loading: false, submitting: false, }) const componentRequirementSelections = reactive>({}) const pieceRequirementSelections = reactive>({}) const productRequirementSelections = reactive>({}) // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const isPlainObject = (value: unknown): boolean => Object.prototype.toString.call(value) === '[object Object]' const getComponentRequirementEntries = (requirementId: string): AnyRecord[] => componentRequirementSelections[requirementId] || [] const getPieceRequirementEntries = (requirementId: string): AnyRecord[] => pieceRequirementSelections[requirementId] || [] const getProductRequirementEntries = (requirementId: string): AnyRecord[] => productRequirementSelections[requirementId] || [] // --------------------------------------------------------------------------- // Label resolvers // --------------------------------------------------------------------------- const resolveComponentRequirementTypeLabel = (requirement: AnyRecord, entry: AnyRecord): string => { const typeId = (entry?.typeComposantId || requirement?.typeComposantId || null) as string | null if (!typeId) return ((requirement?.typeComposant as AnyRecord)?.name as string) || 'Type non défini' return componentTypeLabelMap.value.get(typeId) || ((requirement?.typeComposant as AnyRecord)?.name as string) || 'Type non défini' } const resolvePieceRequirementTypeLabel = (requirement: AnyRecord, entry: AnyRecord): string => { const typeId = (entry?.typePieceId || requirement?.typePieceId || null) as string | null if (!typeId) return ((requirement?.typePiece as AnyRecord)?.name as string) || 'Type non défini' return pieceTypeLabelMap.value.get(typeId) || ((requirement?.typePiece as AnyRecord)?.name as string) || 'Type non défini' } const resolveProductRequirementTypeLabel = (requirement: AnyRecord, entry: AnyRecord): string => { const typeId = (entry?.typeProductId as string) || (requirement?.typeProductId as string) || ((requirement?.typeProduct as AnyRecord)?.id as string) || null if (typeId) { const typeMatch = productRequirements.value.find( (req: AnyRecord) => req.typeProductId === typeId || (req.typeProduct as AnyRecord)?.id === typeId, ) if (typeMatch && (typeMatch.typeProduct as AnyRecord)?.name) { return (typeMatch.typeProduct as AnyRecord).name as string } } return ((requirement?.typeProduct as AnyRecord)?.name as string) || 'Catégorie non définie' } const getProductOptionsForRequirement = (requirement: AnyRecord): AnyRecord[] => { const requirementTypeId = (requirement?.typeProductId as string) || ((requirement?.typeProduct as AnyRecord)?.id as string) || null return (productInventory.value as AnyRecord[]).filter((product) => { if (!product?.id) return false if (!requirementTypeId) return true const productTypeId = (product.typeProductId as string) || ((product.typeProduct as AnyRecord)?.id as string) || null return productTypeId === requirementTypeId }) } // --------------------------------------------------------------------------- // Selection entry factories // --------------------------------------------------------------------------- const createComponentSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => { const link = (source?.machineComponentLink as AnyRecord) || null const entry: AnyRecord = { linkId: resolveIdentifier(link?.id, source?.machineComponentLinkId, source?.linkId), composantId: resolveIdentifier(source?.composantId, source?.componentId, source?.id), parentLinkId: resolveIdentifier(link?.parentLinkId, link?.parentComponentLinkId, source?.parentComponentLinkId, source?.parentLinkId), parentRequirementId: resolveIdentifier(link?.parentRequirementId, source?.parentRequirementId, requirement?.parentRequirementId), parentMachineComponentRequirementId: resolveIdentifier(link?.parentMachineComponentRequirementId, source?.parentMachineComponentRequirementId, requirement?.parentMachineComponentRequirementId), parentMachinePieceRequirementId: resolveIdentifier(link?.parentMachinePieceRequirementId, source?.parentMachinePieceRequirementId, requirement?.parentMachinePieceRequirementId), parentComponentId: resolveIdentifier(link?.parentComponentId, source?.parentComponentId), parentPieceId: resolveIdentifier(link?.parentPieceId, source?.parentPieceId), typeComposantId: (source?.typeMachineComponentRequirement as AnyRecord)?.typeComposantId || source?.typeComposantId || (source?.typeComposant as AnyRecord)?.id || requirement?.typeComposantId || null, definition: { name: source?.name || source?.nom || (requirement?.typeComposant as AnyRecord)?.name || '', reference: source?.reference || '', constructeurIds: [] as string[], constructeurId: null as string | null, prix: source?.prix ?? source?.price ?? null, }, } const defConstructeurIds = uniqueConstructeurIds( (link?.overrides as AnyRecord)?.constructeurIds, (link?.overrides as AnyRecord)?.constructeurId, source?.constructeurIds, source?.constructeurId, source?.constructeur, ) ;(entry.definition as AnyRecord).constructeurIds = defConstructeurIds ;(entry.definition as AnyRecord).constructeurId = defConstructeurIds[0] || null if (link?.overrides && isPlainObject(link.overrides)) { entry.definition = { ...(entry.definition as AnyRecord), ...(link.overrides as AnyRecord) } } const finalConstructeurIds = uniqueConstructeurIds( (entry.definition as AnyRecord).constructeurIds, (entry.definition as AnyRecord).constructeurId, (entry.definition as AnyRecord).constructeur, ) ;(entry.definition as AnyRecord).constructeurIds = finalConstructeurIds ;(entry.definition as AnyRecord).constructeurId = finalConstructeurIds[0] || null return entry } const createPieceSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => { const link = (source?.machinePieceLink as AnyRecord) || null const entry: AnyRecord = { linkId: resolveIdentifier(link?.id, source?.machinePieceLinkId, source?.linkId), pieceId: resolveIdentifier(source?.pieceId, source?.id), parentLinkId: resolveIdentifier(link?.parentLinkId, source?.parentLinkId), parentComponentLinkId: resolveIdentifier(link?.parentComponentLinkId, source?.parentComponentLinkId, source?.machineComponentLinkId), parentRequirementId: resolveIdentifier(link?.parentRequirementId, source?.parentRequirementId, requirement?.parentRequirementId), parentMachineComponentRequirementId: resolveIdentifier(link?.parentMachineComponentRequirementId, source?.parentMachineComponentRequirementId, requirement?.parentMachineComponentRequirementId), parentMachinePieceRequirementId: resolveIdentifier(link?.parentMachinePieceRequirementId, source?.parentMachinePieceRequirementId, requirement?.parentMachinePieceRequirementId), parentComponentId: resolveIdentifier(link?.parentComponentId, source?.parentComponentId, source?.composantId), parentPieceId: resolveIdentifier(link?.parentPieceId, source?.parentPieceId), composantId: resolveIdentifier(source?.composantId, link?.composantId, link?.componentId), typePieceId: (source?.typeMachinePieceRequirement as AnyRecord)?.typePieceId || source?.typePieceId || (source?.typePiece as AnyRecord)?.id || requirement?.typePieceId || null, definition: { name: source?.name || source?.nom || (requirement?.typePiece as AnyRecord)?.name || '', reference: source?.reference || '', constructeurIds: [] as string[], constructeurId: null as string | null, prix: source?.prix ?? source?.price ?? null, }, } const defConstructeurIds = uniqueConstructeurIds( (link?.overrides as AnyRecord)?.constructeurIds, (link?.overrides as AnyRecord)?.constructeurId, source?.constructeurIds, source?.constructeurId, source?.constructeur, ) ;(entry.definition as AnyRecord).constructeurIds = defConstructeurIds ;(entry.definition as AnyRecord).constructeurId = defConstructeurIds[0] || null if (link?.overrides && isPlainObject(link.overrides)) { entry.definition = { ...(entry.definition as AnyRecord), ...(link.overrides as AnyRecord) } } const finalConstructeurIds = uniqueConstructeurIds( (entry.definition as AnyRecord).constructeurIds, (entry.definition as AnyRecord).constructeurId, (entry.definition as AnyRecord).constructeur, ) ;(entry.definition as AnyRecord).constructeurIds = finalConstructeurIds ;(entry.definition as AnyRecord).constructeurId = finalConstructeurIds[0] || null return entry } const createProductSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => { const link = (source?.machineProductLink as AnyRecord) || source || null return { linkId: resolveIdentifier(link?.id, source?.machineProductLinkId, source?.linkId), productId: resolveIdentifier(source?.productId, link?.productId), parentLinkId: resolveIdentifier(link?.parentLinkId, source?.parentLinkId), parentComponentLinkId: resolveIdentifier(link?.parentComponentLinkId, source?.parentComponentLinkId), parentPieceLinkId: resolveIdentifier(link?.parentPieceLinkId, source?.parentPieceLinkId), parentRequirementId: resolveIdentifier(link?.parentRequirementId, source?.parentRequirementId, requirement?.parentRequirementId), parentComponentRequirementId: resolveIdentifier(link?.parentComponentRequirementId, source?.parentComponentRequirementId, requirement?.parentComponentRequirementId), parentPieceRequirementId: resolveIdentifier(link?.parentPieceRequirementId, source?.parentPieceRequirementId, requirement?.parentPieceRequirementId), parentMachineComponentRequirementId: resolveIdentifier(link?.parentMachineComponentRequirementId, source?.parentMachineComponentRequirementId, requirement?.parentMachineComponentRequirementId), parentMachinePieceRequirementId: resolveIdentifier(link?.parentMachinePieceRequirementId, source?.parentMachinePieceRequirementId, requirement?.parentMachinePieceRequirementId), typeProductId: resolveIdentifier(link?.typeProductId, source?.typeProductId, requirement?.typeProductId, (requirement?.typeProduct as AnyRecord)?.id), } } // --------------------------------------------------------------------------- // Selection CRUD // --------------------------------------------------------------------------- const resetSkeletonRequirementSelections = () => { Object.keys(componentRequirementSelections).forEach((k) => delete componentRequirementSelections[k]) Object.keys(pieceRequirementSelections).forEach((k) => delete pieceRequirementSelections[k]) Object.keys(productRequirementSelections).forEach((k) => delete productRequirementSelections[k]) } const addComponentSelectionEntry = (requirement: AnyRecord) => { const entries = getComponentRequirementEntries(requirement.id as string) const max = (requirement.maxCount as number | null) ?? null if (max !== null && entries.length >= max) { toast.showError( `Vous ne pouvez pas ajouter plus de ${max} composant(s) pour ${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'ce groupe'}`, ) return } componentRequirementSelections[requirement.id as string] = [ ...entries, createComponentSelectionEntry(requirement), ] } const removeComponentSelectionEntry = (requirementId: string, index: number) => { const entries = getComponentRequirementEntries(requirementId) componentRequirementSelections[requirementId] = entries.filter((_, i) => i !== index) } const setComponentRequirementType = (requirementId: string, index: number, value: string | null) => { const entry = getComponentRequirementEntries(requirementId)[index] if (!entry) return entry.typeComposantId = value || null } const setComponentRequirementConstructeur = (requirementId: string, index: number, value: unknown) => { const entry = getComponentRequirementEntries(requirementId)[index] if (!entry) return const ids = uniqueConstructeurIds(value) ;(entry.definition as AnyRecord).constructeurIds = ids ;(entry.definition as AnyRecord).constructeurId = ids[0] || null } const addPieceSelectionEntry = (requirement: AnyRecord) => { const entries = getPieceRequirementEntries(requirement.id as string) const max = (requirement.maxCount as number | null) ?? 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 as AnyRecord)?.name || 'ce groupe'}`, ) return } pieceRequirementSelections[requirement.id as string] = [ ...entries, createPieceSelectionEntry(requirement), ] } const removePieceSelectionEntry = (requirementId: string, index: number) => { const entries = getPieceRequirementEntries(requirementId) pieceRequirementSelections[requirementId] = entries.filter((_, i) => i !== index) } const setPieceRequirementType = (requirementId: string, index: number, value: string | null) => { const entry = getPieceRequirementEntries(requirementId)[index] if (!entry) return entry.typePieceId = value || null } const setPieceRequirementConstructeur = (requirementId: string, index: number, value: unknown) => { const entry = getPieceRequirementEntries(requirementId)[index] if (!entry) return const ids = uniqueConstructeurIds(value) ;(entry.definition as AnyRecord).constructeurIds = ids ;(entry.definition as AnyRecord).constructeurId = ids[0] || null } const addProductSelectionEntry = (requirement: AnyRecord) => { const entries = getProductRequirementEntries(requirement.id as string) const max = (requirement.maxCount as number | null) ?? null if (max !== null && entries.length >= max) { toast.showError( `Vous ne pouvez pas ajouter plus de ${max} produit(s) pour ${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'ce groupe'}`, ) return } productRequirementSelections[requirement.id as string] = [ ...entries, createProductSelectionEntry(requirement), ] } const removeProductSelectionEntry = (requirementId: string, index: number) => { const entries = getProductRequirementEntries(requirementId) productRequirementSelections[requirementId] = entries.filter((_, i) => i !== index) } const setProductRequirementProduct = (requirementId: string, index: number, productId: string | null) => { const entry = getProductRequirementEntries(requirementId)[index] if (!entry) return const normalizedProductId = productId || null entry.productId = normalizedProductId if (normalizedProductId) { const product = findProductById(normalizedProductId) entry.typeProductId = (product?.typeProductId as string) || ((product?.typeProduct as AnyRecord)?.id as string) || (entry.typeProductId as string) || null } } const setProductRequirementType = (requirementId: string, index: number, value: string | null) => { const entry = getProductRequirementEntries(requirementId)[index] if (!entry) return entry.typeProductId = value || entry.typeProductId || null } // --------------------------------------------------------------------------- // Skeleton initialization // --------------------------------------------------------------------------- const initializeSkeletonRequirementSelections = async () => { skeletonEditor.loading = true try { resetSkeletonRequirementSelections() const type = machineType.value as AnyRecord if (!type) return try { await loadProducts() } catch (error) { console.error('Erreur lors du chargement des produits pour le squelette:', error) } ;((type.componentRequirements as AnyRecord[]) || []).forEach((requirement) => { const existing = flattenedComponents.value.filter( (c) => c.typeMachineComponentRequirementId === requirement.id, ) const entries = existing.map((c) => createComponentSelectionEntry(requirement, c)) const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0) while (entries.length < min) entries.push(createComponentSelectionEntry(requirement)) if (entries.length) componentRequirementSelections[requirement.id as string] = entries }) const allPieces = collectPiecesForSkeleton() ;((type.pieceRequirements as AnyRecord[]) || []).forEach((requirement) => { const existing = allPieces.filter( (p) => p.typeMachinePieceRequirementId === requirement.id, ) const entries = existing.map((p) => createPieceSelectionEntry(requirement, p)) const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0) while (entries.length < min) entries.push(createPieceSelectionEntry(requirement)) if (entries.length) pieceRequirementSelections[requirement.id as string] = entries }) const existingProductLinks = Array.isArray(machineProductLinks.value) ? machineProductLinks.value : Array.isArray(machine.value?.productLinks) ? (machine.value.productLinks as AnyRecord[]) : [] ;((type.productRequirements as AnyRecord[]) || []).forEach((requirement) => { const matches = existingProductLinks.filter((link) => { const reqId = resolveIdentifier(link?.typeMachineProductRequirementId, link?.requirementId) return reqId === requirement.id }) const entries = matches.map((link) => createProductSelectionEntry(requirement, link)) const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0) while (entries.length < min) entries.push(createProductSelectionEntry(requirement)) if (entries.length) productRequirementSelections[requirement.id as string] = entries }) } finally { skeletonEditor.loading = false } } // --------------------------------------------------------------------------- // Editor open/close // --------------------------------------------------------------------------- const openSkeletonEditor = async () => { if (skeletonEditor.open) return skeletonEditor.open = true await initializeSkeletonRequirementSelections() } const closeSkeletonEditor = () => { if (!skeletonEditor.open) return if (skeletonEditor.submitting) return skeletonEditor.open = false skeletonEditor.loading = false skeletonEditor.submitting = false resetSkeletonRequirementSelections() } const changeMachineView = async (view: 'details' | 'skeleton') => { if (view === activeMachineView.value) return if (view === 'skeleton') { if (!machineHasSkeletonRequirements.value) { toast.showInfo('Aucun squelette configuré pour cette machine.') return } activeMachineView.value = 'skeleton' if (!skeletonEditor.open) { try { await openSkeletonEditor() } catch (error) { console.error("Impossible d'ouvrir l'éditeur de squelette:", error) toast.showError('Impossible de charger les éléments du squelette.') activeMachineView.value = 'details' } } return } closeSkeletonEditor() activeMachineView.value = 'details' } // --------------------------------------------------------------------------- // Validation & save // --------------------------------------------------------------------------- const computeSkeletonProductUsage = (type: AnyRecord): Map => { const usage = new Map() const increment = (typeProductId: string | null) => { if (!typeProductId) return usage.set(typeProductId, (usage.get(typeProductId) ?? 0) + 1) } for (const requirement of (type.componentRequirements as AnyRecord[]) || []) { getComponentRequirementEntries(requirement.id as string).forEach((entry) => { if (!entry?.composantId) return const component = findComponentById(components.value, entry.composantId as string) const typeProductId = ((component?.product as AnyRecord)?.typeProductId as string) || (((component?.product as AnyRecord)?.typeProduct as AnyRecord)?.id as string) || null increment(typeProductId) }) } for (const requirement of (type.pieceRequirements as AnyRecord[]) || []) { getPieceRequirementEntries(requirement.id as string).forEach((entry) => { if (!entry?.pieceId) return const piece = findPieceById(entry.pieceId as string) const typeProductId = ((piece?.product as AnyRecord)?.typeProductId as string) || (((piece?.product as AnyRecord)?.typeProduct as AnyRecord)?.id as string) || null increment(typeProductId) }) } for (const requirement of (type.productRequirements as AnyRecord[]) || []) { getProductRequirementEntries(requirement.id as string).forEach((entry) => { if (!entry?.productId) return const product = findProductById(entry.productId as string) const typeProductId = ((product?.typeProductId as string) || ((product?.typeProduct as AnyRecord)?.id as string) || (entry?.typeProductId as string) || (requirement?.typeProductId as string) || ((requirement?.typeProduct as AnyRecord)?.id as string) || null) increment(typeProductId) }) } return usage } const validateSkeletonSelections = (type: AnyRecord) => { const errors: string[] = [] const componentLinksPayload: AnyRecord[] = [] const pieceLinksPayload: AnyRecord[] = [] const productLinksPayload: AnyRecord[] = [] for (const requirement of (type.componentRequirements as AnyRecord[]) || []) { const entries = getComponentRequirementEntries(requirement.id as string) const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0) const max = (requirement.maxCount as number | null) ?? null if (entries.length < min) { errors.push(`Le groupe "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}" nécessite au moins ${min} élément(s).`) } if (max !== null && entries.length > max) { errors.push(`Le groupe "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}" ne peut dépasser ${max} élément(s).`) } entries.forEach((entry) => { const resolvedTypeId = (entry.typeComposantId || requirement.typeComposantId || null) as string | null if (!resolvedTypeId) { errors.push(`Le groupe "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}" nécessite un type de composant.`) return } const payload: AnyRecord = { requirementId: requirement.id, typeComposantId: resolvedTypeId } if (entry.linkId) { payload.id = entry.linkId; payload.linkId = entry.linkId } if (entry.composantId) payload.composantId = entry.composantId const overrides = sanitizeDefinitionOverrides(entry.definition) if (overrides) payload.overrides = overrides Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry)) componentLinksPayload.push(payload) }) } for (const requirement of (type.pieceRequirements as AnyRecord[]) || []) { const entries = getPieceRequirementEntries(requirement.id as string) const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0) const max = (requirement.maxCount as number | null) ?? null if (entries.length < min) { errors.push(`Le groupe "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}" nécessite au moins ${min} élément(s).`) } if (max !== null && entries.length > max) { errors.push(`Le groupe "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}" ne peut dépasser ${max} élément(s).`) } entries.forEach((entry) => { const resolvedTypeId = (entry.typePieceId || requirement.typePieceId || null) as string | null if (!resolvedTypeId) { errors.push(`Le groupe "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}" nécessite un type de pièce.`) return } const payload: AnyRecord = { requirementId: requirement.id, typePieceId: resolvedTypeId } if (entry.linkId) { payload.id = entry.linkId; payload.linkId = entry.linkId } if (entry.pieceId) payload.pieceId = entry.pieceId if (entry.composantId) payload.composantId = entry.composantId const overrides = sanitizeDefinitionOverrides(entry.definition) if (overrides) payload.overrides = overrides Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry)) pieceLinksPayload.push(payload) }) } const productUsage = computeSkeletonProductUsage(type) for (const requirement of (type.productRequirements as AnyRecord[]) || []) { const entries = getProductRequirementEntries(requirement.id as string) const max = (requirement.maxCount as number | null) ?? null if (max !== null && entries.length > max) { errors.push(`Le groupe "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}" ne peut dépasser ${max} sélection(s) directe(s).`) } const typeProductId = (requirement.typeProductId as string) || ((requirement.typeProduct as AnyRecord)?.id as string) || null const count = typeProductId ? productUsage.get(typeProductId) ?? 0 : 0 const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0) if (count < min) { errors.push(`Le groupe "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}" nécessite au moins ${min} sélection(s).`) } if (max !== null && count > max) { errors.push(`Le groupe "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}" ne peut dépasser ${max} sélection(s).`) } entries.forEach((entry) => { if (!entry.productId) { errors.push(`Sélectionner un produit pour "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}".`) return } const product = findProductById(entry.productId as string) if (!product) { errors.push(`Le produit sélectionné est introuvable (ID: ${entry.productId}).`) return } const productTypeId = (product.typeProductId as string) || ((product.typeProduct as AnyRecord)?.id as string) || (entry.typeProductId as string) || null if (typeProductId && productTypeId && productTypeId !== typeProductId) { errors.push(`Le produit "${product.name || product.reference || product.id}" n'appartient pas à la catégorie attendue.`) return } const payload: AnyRecord = { requirementId: requirement.id, productId: entry.productId } if (entry.linkId) { payload.id = entry.linkId; payload.linkId = entry.linkId } if (entry.typeProductId) payload.typeProductId = entry.typeProductId Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry)) productLinksPayload.push(payload) }) } if (errors.length > 0) return { valid: false as const, error: errors[0] } return { valid: true as const, componentLinks: componentLinksPayload, pieceLinks: pieceLinksPayload, productLinks: productLinksPayload, } } // --------------------------------------------------------------------------- // Apply reconfiguration result // --------------------------------------------------------------------------- const applySkeletonReconfigurationResult = async (data: AnyRecord) => { if (!data) return const updatedMachine = (data.machine as AnyRecord) || data if (updatedMachine) { machine.value = { ...machine.value, ...updatedMachine, documents: (updatedMachine.documents as AnyRecord[]) || (machine.value?.documents as AnyRecord[]) || [], } initMachineFields() machineDocumentsLoaded.value = !!((machine.value!.documents as AnyRecord[])?.length) } const linksApplied = applyMachineLinks(data) || applyMachineLinks(updatedMachine) if (linksApplied) { if (machine.value) { machine.value.componentLinks = machineComponentLinks.value machine.value.pieceLinks = machinePieceLinks.value machine.value.productLinks = machineProductLinks.value } collapseAllComponents() return } const newComponents = (data.components ?? updatedMachine?.components ?? null) as AnyRecord[] | null if (Array.isArray(newComponents)) { components.value = transformComponentCustomFields(newComponents) collapseAllComponents() } const newPieces = (data.pieces ?? updatedMachine?.pieces ?? null) as AnyRecord[] | null if (Array.isArray(newPieces)) { pieces.value = transformCustomFields(newPieces) } const prodLinks = resolveLinkArray(data, ['productLinks', 'machineProductLinks']) ?? resolveLinkArray(updatedMachine, ['productLinks', 'machineProductLinks']) if (Array.isArray(prodLinks)) { machineProductLinks.value = prodLinks as AnyRecord[] if (machine.value) machine.value.productLinks = prodLinks } } // --------------------------------------------------------------------------- // Save // --------------------------------------------------------------------------- const saveSkeletonConfiguration = async () => { if (!machine.value?.id) return const type = machineType.value as AnyRecord let payload: AnyRecord = { componentLinks: [], pieceLinks: [], productLinks: [] } if (type && machineHasSkeletonRequirements.value) { const validation = validateSkeletonSelections(type) if (!validation.valid) { toast.showError((validation as AnyRecord).error as string) return } payload = { componentLinks: (validation as AnyRecord).componentLinks, pieceLinks: (validation as AnyRecord).pieceLinks, productLinks: (validation as AnyRecord).productLinks, } } skeletonEditor.submitting = true try { const result = await reconfigureMachineSkeleton(machine.value.id as string, payload) if ((result as AnyRecord).success) { await applySkeletonReconfigurationResult((result as AnyRecord).data as AnyRecord) await changeMachineView('details') } else if ((result as AnyRecord).error) { toast.showError((result as AnyRecord).error as string) } } 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 } } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- return { // View state activeMachineView, isDetailsView, isSkeletonView, // Editor state skeletonEditor, componentRequirementSelections, pieceRequirementSelections, productRequirementSelections, // Entry getters getComponentRequirementEntries, getPieceRequirementEntries, getProductRequirementEntries, // Label resolvers resolveComponentRequirementTypeLabel, resolvePieceRequirementTypeLabel, resolveProductRequirementTypeLabel, getProductOptionsForRequirement, // Selection CRUD addComponentSelectionEntry, removeComponentSelectionEntry, setComponentRequirementType, setComponentRequirementConstructeur, addPieceSelectionEntry, removePieceSelectionEntry, setPieceRequirementType, setPieceRequirementConstructeur, addProductSelectionEntry, removeProductSelectionEntry, setProductRequirementProduct, setProductRequirementType, // Editor lifecycle openSkeletonEditor, closeSkeletonEditor, changeMachineView, initializeSkeletonRequirementSelections, // Validation & save validateSkeletonSelections, saveSkeletonConfiguration, } }