From 3f56a2f117e08d6a8ac03c6b1ccfef3925e3ffd8 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Mon, 26 Jan 2026 17:49:59 +0700 Subject: [PATCH] feat(catalog): add loading states for InfoPanel tabs and filter map by active tab - Add separate loading states for products, hubs, suppliers, offers - Show spinner on tabs while loading, disable tab during load - Filter relatedPoints on map by current active tab --- app/components/catalog/InfoPanel.vue | 53 ++++++++++---- app/composables/useCatalogInfo.ts | 101 ++++++++++++++++----------- app/pages/catalog/index.vue | 95 +++++++++++++++---------- 3 files changed, 156 insertions(+), 93 deletions(-) diff --git a/app/components/catalog/InfoPanel.vue b/app/components/catalog/InfoPanel.vue index 86df751..34ecd69 100644 --- a/app/components/catalog/InfoPanel.vue +++ b/app/components/catalog/InfoPanel.vue @@ -41,11 +41,17 @@ :key="tab.id" role="tab" class="tab text-white/70" - :class="{ 'tab-active !text-white !bg-white/20': activeTab === tab.id }" - @click="onTabClick(tab.id)" + :class="{ + 'tab-active !text-white !bg-white/20': activeTab === tab.id, + 'pointer-events-none opacity-50': tab.loading + }" + @click="!tab.loading && onTabClick(tab.id)" > {{ tab.label }} - ({{ tab.count }}) + + + + ({{ tab.count }}) @@ -185,6 +191,10 @@ const props = defineProps<{ selectedProduct?: string | null currentTab?: string loading?: boolean + loadingProducts?: boolean + loadingHubs?: boolean + loadingSuppliers?: boolean + loadingOffers?: boolean }>() const emit = defineEmits<{ @@ -226,33 +236,45 @@ const entityIcon = computed(() => { // Available tabs based on entity type and data const availableTabs = computed(() => { - const tabs: Array<{ id: string; label: string; count?: number }> = [] + const tabs: Array<{ id: string; label: string; count?: number; loading?: boolean }> = [] if (props.entityType === 'hub') { + // For hub: offers tab shows products first, then offers after product selection + const offersLoading = props.selectedProduct ? props.loadingOffers : props.loadingProducts + const offersCount = props.selectedProduct + ? props.relatedOffers?.length + : props.relatedProducts?.length + tabs.push({ id: 'offers', label: t('catalog.tabs.offers'), - count: props.selectedProduct - ? props.relatedOffers?.length || 0 - : props.relatedProducts?.length || 0 + count: offersLoading ? undefined : (offersCount || 0), + loading: offersLoading }) tabs.push({ id: 'suppliers', label: t('catalog.tabs.suppliers'), - count: props.relatedSuppliers?.length || 0 + count: props.loadingSuppliers ? undefined : (props.relatedSuppliers?.length || 0), + loading: props.loadingSuppliers }) } else if (props.entityType === 'supplier') { + // For supplier: offers tab shows products first, then offers after product selection + const offersLoading = props.selectedProduct ? props.loadingOffers : props.loadingProducts + const offersCount = props.selectedProduct + ? props.relatedOffers?.length + : props.relatedProducts?.length + tabs.push({ id: 'offers', label: t('catalog.tabs.offers'), - count: props.selectedProduct - ? props.relatedOffers?.length || 0 - : props.relatedProducts?.length || 0 + count: offersLoading ? undefined : (offersCount || 0), + loading: offersLoading }) tabs.push({ id: 'hubs', label: t('catalog.tabs.hubs'), - count: props.relatedHubs?.length || 0 + count: props.loadingHubs ? undefined : (props.relatedHubs?.length || 0), + loading: props.loadingHubs }) } else if (props.entityType === 'offer') { if (props.relatedProducts && props.relatedProducts.length > 0) { @@ -264,12 +286,15 @@ const availableTabs = computed(() => { tabs.push({ id: 'hubs', label: t('catalog.tabs.hubs'), - count: props.relatedHubs?.length || 0 + count: props.loadingHubs ? undefined : (props.relatedHubs?.length || 0), + loading: props.loadingHubs }) if (props.relatedSuppliers && props.relatedSuppliers.length > 0) { tabs.push({ id: 'suppliers', - label: t('catalog.tabs.supplier') + label: t('catalog.tabs.supplier'), + count: props.loadingSuppliers ? undefined : (props.relatedSuppliers?.length || 0), + loading: props.loadingSuppliers }) } } diff --git a/app/composables/useCatalogInfo.ts b/app/composables/useCatalogInfo.ts index f33f126..cd293a5 100644 --- a/app/composables/useCatalogInfo.ts +++ b/app/composables/useCatalogInfo.ts @@ -287,7 +287,7 @@ export function useCatalogInfo() { } // Load offers for supplier after product selection - const loadOffersForSupplier = async (supplierUuid: string, productUuid: string) => { + const loadOffersForSupplier = async (_supplierUuid: string, productUuid: string) => { try { const supplier = entity.value if (!supplier?.latitude || !supplier?.longitude) { @@ -295,52 +295,61 @@ export function useCatalogInfo() { return } - // Find offers near supplier for this product - const offersData = await execute( - NearestOffersDocument, - { - lat: supplier.latitude, - lon: supplier.longitude, - productUuid, - radius: 500, - limit: 12 - }, - 'public', - 'geo' - ) + isLoadingOffers.value = true + isLoadingHubs.value = true - relatedOffers.value = offersData?.nearestOffers || [] + try { + // Find offers near supplier for this product + const offersData = await execute( + NearestOffersDocument, + { + lat: supplier.latitude, + lon: supplier.longitude, + productUuid, + radius: 500, + limit: 12 + }, + 'public', + 'geo' + ) - // Load hubs near each offer and aggregate (limit to 12) - const allHubs = new Map() - for (const offer of relatedOffers.value.slice(0, 3)) { - // Check first 3 offers - if (!offer.latitude || !offer.longitude) continue + relatedOffers.value = offersData?.nearestOffers || [] + isLoadingOffers.value = false - try { - const hubsData = await execute( - NearestHubsDocument, - { - lat: offer.latitude, - lon: offer.longitude, - radius: 1000, - limit: 5 - }, - 'public', - 'geo' - ) - hubsData?.nearestHubs?.forEach((hub: any) => { - if (!allHubs.has(hub.uuid)) { - allHubs.set(hub.uuid, hub) - } - }) - } catch (e) { - console.warn('Error loading hubs for offer:', offer.uuid, e) + // Load hubs near each offer and aggregate (limit to 12) + const allHubs = new Map() + for (const offer of relatedOffers.value.slice(0, 3)) { + // Check first 3 offers + if (!offer.latitude || !offer.longitude) continue + + try { + const hubsData = await execute( + NearestHubsDocument, + { + lat: offer.latitude, + lon: offer.longitude, + radius: 1000, + limit: 5 + }, + 'public', + 'geo' + ) + hubsData?.nearestHubs?.forEach((hub: any) => { + if (!allHubs.has(hub.uuid)) { + allHubs.set(hub.uuid, hub) + } + }) + } catch (e) { + console.warn('Error loading hubs for offer:', offer.uuid, e) + } + + if (allHubs.size >= 12) break } - - if (allHubs.size >= 12) break + relatedHubs.value = Array.from(allHubs.values()).slice(0, 12) + } finally { + isLoadingOffers.value = false + isLoadingHubs.value = false } - relatedHubs.value = Array.from(allHubs.values()).slice(0, 12) } catch (error) { console.error('Error loading offers for supplier:', error) } @@ -396,6 +405,10 @@ export function useCatalogInfo() { relatedOffers.value = [] selectedProduct.value = null activeTab.value = 'products' + isLoadingProducts.value = false + isLoadingHubs.value = false + isLoadingSuppliers.value = false + isLoadingOffers.value = false } return { @@ -408,6 +421,10 @@ export function useCatalogInfo() { selectedProduct, activeTab, isLoading, + isLoadingProducts, + isLoadingHubs, + isLoadingSuppliers, + isLoadingOffers, // Actions loadInfo, diff --git a/app/pages/catalog/index.vue b/app/pages/catalog/index.vue index 37c4a21..c044b80 100644 --- a/app/pages/catalog/index.vue +++ b/app/pages/catalog/index.vue @@ -46,6 +46,10 @@ :selected-product="infoProduct ?? null" :current-tab="infoTab" :loading="infoLoading" + :loading-products="isLoadingProducts" + :loading-hubs="isLoadingHubs" + :loading-suppliers="isLoadingSuppliers" + :loading-offers="isLoadingOffers" @close="onInfoClose" @add-to-filter="onInfoAddToFilter" @open-info="onInfoOpenRelated" @@ -134,6 +138,10 @@ const { relatedOffers, selectedProduct, isLoading: infoLoading, + isLoadingProducts, + isLoadingHubs, + isLoadingSuppliers, + isLoadingOffers, loadInfo, selectProduct: selectInfoProduct, clearInfo @@ -253,7 +261,7 @@ watch(infoProduct, async (productUuid) => { } }) -// Related points for Info mode (shown on map) +// Related points for Info mode (shown on map) - filtered by active tab const relatedPoints = computed(() => { if (!infoId.value) return [] @@ -265,44 +273,57 @@ const relatedPoints = computed(() => { type: 'hub' | 'supplier' | 'offer' }> = [] - // Add hubs - relatedHubs.value.forEach(hub => { - if (hub.latitude && hub.longitude) { - points.push({ - uuid: hub.uuid, - name: hub.name, - latitude: hub.latitude, - longitude: hub.longitude, - type: 'hub' - }) - } - }) + const currentTab = infoTab.value - // Add suppliers - relatedSuppliers.value.forEach(supplier => { - if (supplier.latitude && supplier.longitude) { - points.push({ - uuid: supplier.uuid, - name: supplier.name, - latitude: supplier.latitude, - longitude: supplier.longitude, - type: 'supplier' - }) - } - }) + // Show content based on active tab + // Hub entity: offers tab → offers, suppliers tab → suppliers + // Supplier entity: offers tab → offers, hubs tab → hubs + // Offer entity: hubs tab → hubs, suppliers tab → suppliers - // Add offers - relatedOffers.value.forEach(offer => { - if (offer.latitude && offer.longitude) { - points.push({ - uuid: offer.uuid, - name: offer.productName || offer.name, - latitude: offer.latitude, - longitude: offer.longitude, - type: 'offer' - }) - } - }) + // Add hubs (for supplier's hubs tab or offer's hubs tab) + if (currentTab === 'hubs') { + relatedHubs.value.forEach(hub => { + if (hub.latitude && hub.longitude) { + points.push({ + uuid: hub.uuid, + name: hub.name, + latitude: hub.latitude, + longitude: hub.longitude, + type: 'hub' + }) + } + }) + } + + // Add suppliers (for hub's suppliers tab or offer's suppliers tab) + if (currentTab === 'suppliers') { + relatedSuppliers.value.forEach(supplier => { + if (supplier.latitude && supplier.longitude) { + points.push({ + uuid: supplier.uuid, + name: supplier.name, + latitude: supplier.latitude, + longitude: supplier.longitude, + type: 'supplier' + }) + } + }) + } + + // Add offers (for hub's/supplier's offers tab when product is selected) + if (currentTab === 'offers' && infoProduct.value) { + relatedOffers.value.forEach(offer => { + if (offer.latitude && offer.longitude) { + points.push({ + uuid: offer.uuid, + name: offer.productName || offer.name, + latitude: offer.latitude, + longitude: offer.longitude, + type: 'offer' + }) + } + }) + } return points })