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 { useCustomFields } from '~/composables/useCustomFields' 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 { useComponentHistory } from '~/composables/useComponentHistory' import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils' import { uniqueConstructeurIds } 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 { type CustomFieldInput, buildCustomFieldInputs, requiredCustomFieldsFilled as _requiredCustomFieldsFilled, saveCustomFieldValues as _saveCustomFieldValues, } from '~/shared/utils/customFieldFormUtils' 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 { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields() 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 customFieldInputs = ref([]) 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 refreshCustomFieldInputs = ( structureOverride?: ComponentModelStructure | null, valuesOverride?: any[] | null, ) => { const structure = structureOverride ?? selectedTypeStructure.value ?? null const values = valuesOverride ?? component.value?.customFieldValues ?? null customFieldInputs.value = buildCustomFieldInputs(structure, values) } const requiredCustomFieldsFilled = computed(() => _requiredCustomFieldsFilled(customFieldInputs.value), ) 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 : [] const customValues = Array.isArray(result.data?.customFieldValues) ? result.data.customFieldValues : [] refreshCustomFieldInputs(undefined, customValues) 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 selection entries (for selectors) --- const pieceSlotEntries = computed(() => { const structure = component.value?.structure if (!structure?.pieces) return [] return (structure.pieces as any[]).map((slot: any, i: number) => ({ slotId: slot.slotId, typePieceId: slot.typePieceId, selectedPieceId: slot.selectedPieceId ?? null, quantity: slot.quantity ?? 1, position: slot.position ?? i, label: pieceTypeLabelMap.value[slot.typePieceId] || `Pièce #${i + 1}`, })) }) const productSlotEntries = computed(() => { const structure = component.value?.structure if (!structure?.products) return [] return (structure.products as any[]).map((slot: any, i: number) => ({ slotId: slot.slotId, typeProductId: slot.typeProductId, selectedProductId: slot.selectedProductId ?? null, familyCode: slot.familyCode, position: slot.position ?? i, label: productTypeLabelMap.value[slot.typeProductId] || `Produit #${i + 1}`, })) }) const subcomponentSlotEntries = computed(() => { const structure = component.value?.structure if (!structure?.subcomponents) return [] return (structure.subcomponents as any[]).map((slot: any, i: number) => ({ slotId: slot.slotId, typeComposantId: slot.typeComposantId, selectedComponentId: slot.selectedComponentId ?? null, alias: slot.alias, familyCode: slot.familyCode, position: slot.position ?? i, label: slot.alias || `Sous-composant #${i + 1}`, })) }) const savePieceSlotSelection = async (slotId: string, selectedPieceId: string | null) => { const result = await patch(`/composant-piece-slots/${slotId}`, { selectedPieceId }) if (result.success) { const structure = component.value?.structure if (structure?.pieces) { const slot = (structure.pieces as any[]).find((s: any) => s.slotId === slotId) if (slot) slot.selectedPieceId = selectedPieceId } toast.showSuccess('Pièce mise à jour') } } const saveProductSlotSelection = async (slotId: string, selectedProductId: string | null) => { const result = await patch(`/composant-product-slots/${slotId}`, { selectedProductId }) if (result.success) { const structure = component.value?.structure if (structure?.products) { const slot = (structure.products as any[]).find((s: any) => s.slotId === slotId) if (slot) slot.selectedProductId = selectedProductId } toast.showSuccess('Produit mis à jour') } } const saveSubcomponentSlotSelection = async (slotId: string, selectedComposantId: string | null) => { const result = await patch(`/composant-subcomponent-slots/${slotId}`, { selectedComposantId }) if (result.success) { const structure = component.value?.structure if (structure?.subcomponents) { const slot = (structure.subcomponents as any[]).find((s: any) => s.slotId === slotId) if (slot) slot.selectedComponentId = selectedComposantId } toast.showSuccess('Sous-composant mis à jour') } } const saveSlotQuantity = async (slotId: string, quantity: number) => { if (!slotId || quantity < 1) return const result = await patch(`/composant-piece-slots/${slotId}`, { quantity: Math.max(1, quantity) }) if (result.success) { const structure = component.value?.structure if (structure?.pieces) { const slot = (structure.pieces as any[]).find((s: any) => s.slotId === slotId) if (slot) slot.quantity = quantity } toast.showSuccess('Quantité mise à jour') } } 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 payload.constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds) 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 await _saveCustomFieldValues( 'composant', updatedComponent.id, [ updatedComponent?.typeComposant?.structure?.customFields, ], { customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast }, ) 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 || '' editionForm.constructeurIds = uniqueConstructeurIds( currentComponent, Array.isArray(currentComponent.constructeurs) ? currentComponent.constructeurs : [], currentComponent.constructeur ? [currentComponent.constructeur] : [], ) editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : '' if (editionForm.constructeurIds.length) { void ensureConstructeurs(editionForm.constructeurIds) } initialized.value = true } refreshCustomFieldInputs(selectedTypeStructure.value ?? currentStructure, currentComponent.customFieldValues) }, { 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, customFieldInputs, historyFieldLabels, // Computed canEdit, canSubmit, componentTypeList, selectedType, selectedTypeStructure, structureSelections, pieceSlotEntries, productSlotEntries, subcomponentSlotEntries, // History history, historyLoading, historyError, // Methods openPreview, closePreview, removeDocument, handleFilesAdded, refreshDocuments, submitEdition, saveSlotQuantity, savePieceSlotSelection, saveProductSlotSelection, saveSubcomponentSlotSelection, resolvePieceLabel, resolveProductLabel, resolveSubcomponentLabel, formatStructurePreview, } }