import { computed, onMounted, reactive, ref, watch } from 'vue' import { useRouter } from '#imports' import { useComponentTypes } from '~/composables/useComponentTypes' import { useComposants } from '~/composables/useComposants' import { usePieceTypes } from '~/composables/usePieceTypes' import { useProductTypes } from '~/composables/useProductTypes' import { usePieces } from '~/composables/usePieces' import { useProducts } from '~/composables/useProducts' import { useApi } from '~/composables/useApi' import { useToast } from '~/composables/useToast' import { extractRelationId } from '~/shared/apiRelations' import { useDocuments } from '~/composables/useDocuments' import { useConstructeurs } from '~/composables/useConstructeurs' import { useConstructeurLinks } from '~/composables/useConstructeurLinks' import { useComponentHistory } from '~/composables/useComponentHistory' import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils' import { uniqueConstructeurIds, constructeurIdsFromLinks } from '~/shared/constructeurUtils' import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils' import { getStructurePieces, getStructureProducts, resolvePieceLabel as _resolvePieceLabel, resolveProductLabel as _resolveProductLabel, resolveSubcomponentLabel, fetchModelTypeNames, buildTypeLabelMap, } from '~/shared/utils/structureDisplayUtils' import type { ComponentModelStructure } from '~/shared/types/inventory' import type { ModelType } from '~/services/modelTypes' import { canPreviewDocument } from '~/utils/documentPreview' import { useCustomFieldInputs, type CustomFieldInput } from '~/composables/useCustomFieldInputs' import { collectStructureSelections } from '~/shared/utils/structureSelectionUtils' interface ComponentCatalogType extends ModelType { structure: ComponentModelStructure | null customFields?: Array> } const historyFieldLabels: Record = { name: 'Nom', reference: 'Référence', prix: 'Prix', structure: 'Structure', typeComposant: 'Catégorie', product: 'Produit lié', constructeurIds: 'Fournisseurs', } export function useComponentEdit(componentId: string) { const { canEdit } = usePermissions() const router = useRouter() const { get, patch } = useApi() const { componentTypes, loadComponentTypes } = useComponentTypes() const { pieceTypes, loadPieceTypes } = usePieceTypes() const { productTypes, loadProductTypes } = useProductTypes() const { updateComposant, composants: componentCatalogRef } = useComposants() const { pieces } = usePieces() const { products } = useProducts() const { ensureConstructeurs } = useConstructeurs() const { fetchLinks, syncLinks } = useConstructeurLinks() const toast = useToast() const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments() const { history, loading: historyLoading, error: historyError, loadHistory, } = useComponentHistory() const component = ref(null) const loading = ref(true) const saving = ref(false) const selectedFiles = ref([]) const uploadingDocuments = ref(false) const loadingDocuments = ref(false) const componentDocuments = ref([]) const previewDocument = ref(null) const previewVisible = ref(false) const selectedTypeId = ref('') const editionForm = reactive({ name: '' as string, description: '' as string, reference: '' as string, constructeurIds: [] as string[], prix: '' as string, }) const constructeurLinks = ref([]) const originalConstructeurLinks = ref([]) const constructeurIdsFromForm = computed(() => constructeurIdsFromLinks(constructeurLinks.value)) const fetchedPieceTypeMap = ref>({}) const pieceTypeLabelMap = computed(() => buildTypeLabelMap(pieceTypes.value, fetchedPieceTypeMap.value), ) const fetchedProductTypeMap = ref>({}) const productTypeLabelMap = computed(() => buildTypeLabelMap(productTypes.value, fetchedProductTypeMap.value), ) const pieceCatalogMap = computed(() => new Map( (pieces.value || []) .filter((item: any) => item?.id) .map((item: any) => [String(item.id), item]), ), ) const productCatalogMap = computed(() => new Map( (products.value || []) .filter((item: any) => item?.id) .map((item: any) => [String(item.id), item]), ), ) const componentCatalogMap = computed(() => new Map( (componentCatalogRef.value || []) .filter((item: any) => item?.id) .map((item: any) => [String(item.id), item]), ), ) const openPreview = (doc: any) => { if (!doc || !canPreviewDocument(doc)) { return } previewDocument.value = doc previewVisible.value = true } const closePreview = () => { previewVisible.value = false previewDocument.value = null } const removeDocument = async (documentId: string | number | null | undefined) => { if (!documentId) { return } const result = await deleteDocument(documentId, { updateStore: false }) if (result.success) { componentDocuments.value = componentDocuments.value.filter((doc) => doc.id !== documentId) } } const refreshDocuments = async () => { if (!component.value?.id) { componentDocuments.value = [] return } loadingDocuments.value = true try { const result = await loadDocumentsByComponent(component.value.id, { updateStore: false }) if (result.success) { componentDocuments.value = Array.isArray(result.data) ? result.data : result.data ? [result.data] : [] } } finally { loadingDocuments.value = false } } const handleFilesAdded = async (files: File[]) => { if (!files?.length || !component.value?.id) { return } uploadingDocuments.value = true try { const result = await uploadDocuments( { files, context: { composantId: component.value.id }, }, { updateStore: false }, ) if (result.success) { selectedFiles.value = [] await refreshDocuments() } } finally { uploadingDocuments.value = false } } const componentTypeList = computed(() => (componentTypes.value || []) .filter((item: any) => item?.category === 'COMPONENT') as ComponentCatalogType[], ) const selectedType = computed(() => { if (!selectedTypeId.value) { return null } return componentTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null }) const selectedTypeStructure = computed(() => { const structure = selectedType.value?.structure ?? null return structure ? normalizeStructureForEditor(structure) : null }) const { fields: customFieldInputs, requiredFilled: requiredCustomFieldsFilled, saveAll: saveAllCustomFields, refresh: refreshCustomFieldInputs, } = useCustomFieldInputs({ definitions: computed(() => selectedTypeStructure.value?.customFields ?? []), values: computed(() => component.value?.customFieldValues ?? []), entityType: 'composant', entityId: computed(() => component.value?.id ?? null), onValueCreated: (newValue) => { if (component.value && Array.isArray(component.value.customFieldValues)) { component.value.customFieldValues.push(newValue) } }, }) const canSubmit = computed(() => Boolean( canEdit.value && component.value && editionForm.name && requiredCustomFieldsFilled.value && !saving.value, )) const fetchComponent = async () => { if (!componentId || typeof componentId !== 'string') { component.value = null componentDocuments.value = [] return } const result = await get(`/composants/${componentId}`) if (result.success) { component.value = result.data componentDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : [] // The watcher on useCustomFieldInputs will auto-refresh when component.value changes loadHistory(result.data.id).catch(() => {}) } else { component.value = null componentDocuments.value = [] } } const resolvePieceLabel = (piece: Record) => _resolvePieceLabel(piece, pieceTypeLabelMap.value) const resolveProductLabel = (product: Record) => _resolveProductLabel(product, productTypeLabelMap.value) const structureSelections = computed(() => { const selections = collectStructureSelections( component.value?.structure, { pieceCatalogMap: pieceCatalogMap.value, productCatalogMap: productCatalogMap.value, componentCatalogMap: componentCatalogMap.value, }, { resolvePieceLabel, resolveProductLabel, resolveSubcomponentLabel }, ) const total = selections.pieces.length + selections.products.length + selections.components.length return { ...selections, total, hasAny: total > 0, } }) // --- Slot local edits (saved on submit, not auto-saved) --- const slotEdits = reactive<{ pieces: Record products: Record subcomponents: Record }>({ pieces: {}, products: {}, subcomponents: {} }) const pieceSlotEntries = computed(() => { const structure = component.value?.structure if (!structure?.pieces) return [] return (structure.pieces as any[]).map((slot: any, i: number) => { const edits = slotEdits.pieces[slot.slotId] const selectedPieceId = edits && 'selectedPieceId' in edits ? edits.selectedPieceId : (slot.selectedPieceId ?? null) return { slotId: slot.slotId, typePieceId: slot.typePieceId, selectedPieceId, selectedPieceName: slot.selectedPieceName ?? null, quantity: edits && 'quantity' in edits ? edits.quantity! : (slot.quantity ?? 1), position: slot.position ?? i, label: pieceTypeLabelMap.value[slot.typePieceId] || `Pièce #${i + 1}`, isEmpty: !selectedPieceId, } }) }) const productSlotEntries = computed(() => { const structure = component.value?.structure if (!structure?.products) return [] return (structure.products as any[]).map((slot: any, i: number) => { const edits = slotEdits.products[slot.slotId] const selectedProductId = edits && 'selectedProductId' in edits ? edits.selectedProductId : (slot.selectedProductId ?? null) return { slotId: slot.slotId, typeProductId: slot.typeProductId, selectedProductId, selectedProductName: slot.selectedProductName ?? null, familyCode: slot.familyCode, position: slot.position ?? i, label: productTypeLabelMap.value[slot.typeProductId] || `Produit #${i + 1}`, isEmpty: !selectedProductId, } }) }) const subcomponentSlotEntries = computed(() => { const structure = component.value?.structure if (!structure?.subcomponents) return [] return (structure.subcomponents as any[]).map((slot: any, i: number) => { const edits = slotEdits.subcomponents[slot.slotId] const selectedComponentId = edits && 'selectedComposantId' in edits ? edits.selectedComposantId : (slot.selectedComponentId ?? null) return { slotId: slot.slotId, typeComposantId: slot.typeComposantId, selectedComponentId, selectedComponentName: slot.selectedComponentName ?? null, alias: slot.alias, familyCode: slot.familyCode, position: slot.position ?? i, label: slot.alias || `Sous-composant #${i + 1}`, isEmpty: !selectedComponentId, } }) }) const setPieceSlotSelection = (slotId: string, selectedPieceId: string | null) => { slotEdits.pieces[slotId] = { ...slotEdits.pieces[slotId], selectedPieceId } } const setProductSlotSelection = (slotId: string, selectedProductId: string | null) => { slotEdits.products[slotId] = { ...slotEdits.products[slotId], selectedProductId } } const setSubcomponentSlotSelection = (slotId: string, selectedComposantId: string | null) => { slotEdits.subcomponents[slotId] = { ...slotEdits.subcomponents[slotId], selectedComposantId } } const setSlotQuantity = (slotId: string, quantity: number) => { if (!slotId || quantity < 1) return slotEdits.pieces[slotId] = { ...slotEdits.pieces[slotId], quantity: Math.max(1, quantity) } } const submitEdition = async () => { if (!component.value) { return } const rawPrice = typeof editionForm.prix === 'string' ? editionForm.prix.trim() : editionForm.prix === null || editionForm.prix === undefined ? '' : String(editionForm.prix).trim() const payload: Record = { name: editionForm.name.trim(), description: editionForm.description.trim() || null, } const reference = editionForm.reference.trim() payload.reference = reference || null if (rawPrice) { const parsed = Number(rawPrice) if (!Number.isNaN(parsed)) { payload.prix = String(parsed) } } else { payload.prix = null } saving.value = true try { const result = await updateComposant(component.value.id, payload) if (result.success && result.data) { const updatedComponent = result.data as Record const failedFields = await saveAllCustomFields() if (failedFields.length) { toast.showError(`Erreur sur les champs : ${failedFields.join(', ')}`) } // Save slot edits const slotPromises: Promise[] = [] for (const [slotId, edits] of Object.entries(slotEdits.pieces)) { if (Object.keys(edits).length) { slotPromises.push(patch(`/composant-piece-slots/${slotId}`, { ...'selectedPieceId' in edits ? { selectedPieceId: edits.selectedPieceId } : {}, ...'quantity' in edits ? { quantity: edits.quantity } : {}, })) } } for (const [slotId, edits] of Object.entries(slotEdits.products)) { if ('selectedProductId' in edits) { slotPromises.push(patch(`/composant-product-slots/${slotId}`, { selectedProductId: edits.selectedProductId })) } } for (const [slotId, edits] of Object.entries(slotEdits.subcomponents)) { if ('selectedComposantId' in edits) { slotPromises.push(patch(`/composant-subcomponent-slots/${slotId}`, { selectedComposantId: edits.selectedComposantId })) } } await Promise.all(slotPromises) // Apply slot edits to local structure so UI reflects saved values const structure = component.value?.structure if (structure) { for (const [slotId, edits] of Object.entries(slotEdits.pieces)) { const slot = (structure.pieces as any[])?.find((s: any) => s.slotId === slotId) if (slot) { if ('selectedPieceId' in edits) slot.selectedPieceId = edits.selectedPieceId if ('quantity' in edits) slot.quantity = edits.quantity } } for (const [slotId, edits] of Object.entries(slotEdits.products)) { const slot = (structure.products as any[])?.find((s: any) => s.slotId === slotId) if (slot && 'selectedProductId' in edits) slot.selectedProductId = edits.selectedProductId } for (const [slotId, edits] of Object.entries(slotEdits.subcomponents)) { const slot = (structure.subcomponents as any[])?.find((s: any) => s.slotId === slotId) if (slot && 'selectedComposantId' in edits) slot.selectedComponentId = edits.selectedComposantId } } // Reset local slot edits slotEdits.pieces = {} slotEdits.products = {} slotEdits.subcomponents = {} await syncLinks('composant', component.value.id, originalConstructeurLinks.value, constructeurLinks.value) originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l })) toast.showSuccess('Composant mis à jour avec succès.') } } catch (error: any) { toast.showError(error?.message || 'Erreur lors de la mise à jour du composant') } finally { saving.value = false } } // --- Watchers --- const initialized = ref(false) watch( [component, selectedTypeStructure], ([currentComponent, currentStructure]) => { if (!currentComponent) { return } if (!initialized.value) { const resolvedTypeId = currentComponent.typeComposantId || extractRelationId(currentComponent.typeComposant) || '' if (resolvedTypeId && !currentComponent.typeComposantId) { currentComponent.typeComposantId = resolvedTypeId } selectedTypeId.value = resolvedTypeId editionForm.name = currentComponent.name || '' editionForm.description = currentComponent.description || '' editionForm.reference = currentComponent.reference || '' // Load constructeur links fetchLinks('composant', componentId).then((links) => { constructeurLinks.value = links originalConstructeurLinks.value = links.map(l => ({ ...l })) editionForm.constructeurIds = constructeurIdsFromLinks(links) if (editionForm.constructeurIds.length) { void ensureConstructeurs(editionForm.constructeurIds) } }) editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : '' initialized.value = true } // useCustomFieldInputs auto-refreshes via its watcher on definitions + values }, { immediate: true }, ) watch( selectedTypeStructure, (structure) => { const pieceIds = getStructurePieces(structure) .map((piece: any) => piece?.typePieceId) .filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0) if (pieceIds.length) { fetchModelTypeNames(Array.from(new Set(pieceIds)), pieceTypeLabelMap.value, get) .then((additions) => { if (Object.keys(additions).length) { fetchedPieceTypeMap.value = { ...fetchedPieceTypeMap.value, ...additions } } }) .catch(() => {}) } const productIds = getStructureProducts(structure) .map((product: any) => product?.typeProductId) .filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0) if (productIds.length) { fetchModelTypeNames(Array.from(new Set(productIds)), productTypeLabelMap.value, get) .then((additions) => { if (Object.keys(additions).length) { fetchedProductTypeMap.value = { ...fetchedProductTypeMap.value, ...additions } } }) .catch(() => {}) } }, { immediate: true }, ) // --- Lifecycle --- onMounted(async () => { await Promise.allSettled([ loadComponentTypes(), loadPieceTypes(), loadProductTypes(), fetchComponent(), ]) loading.value = false }) return { // State component, loading, saving, selectedFiles, uploadingDocuments, loadingDocuments, componentDocuments, previewDocument, previewVisible, selectedTypeId, editionForm, constructeurLinks, originalConstructeurLinks, constructeurIdsFromForm, customFieldInputs, historyFieldLabels, // Computed canEdit, canSubmit, componentTypeList, selectedType, selectedTypeStructure, structureSelections, pieceSlotEntries, productSlotEntries, subcomponentSlotEntries, // History history, historyLoading, historyError, // Methods openPreview, closePreview, removeDocument, handleFilesAdded, refreshDocuments, submitEdition, fetchComponent, setSlotQuantity, setPieceSlotSelection, setProductSlotSelection, setSubcomponentSlotSelection, resolvePieceLabel, resolveProductLabel, resolveSubcomponentLabel, formatStructurePreview, } }