feat(catalog): add loading states for InfoPanel tabs and filter map by active tab
All checks were successful
Build Docker Image / build (push) Successful in 3m35s

- 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
This commit is contained in:
Ruslan Bakiev
2026-01-26 17:49:59 +07:00
parent f680740f52
commit 3f56a2f117
3 changed files with 156 additions and 93 deletions

View File

@@ -41,11 +41,17 @@
:key="tab.id" :key="tab.id"
role="tab" role="tab"
class="tab text-white/70" class="tab text-white/70"
:class="{ 'tab-active !text-white !bg-white/20': activeTab === tab.id }" :class="{
@click="onTabClick(tab.id)" '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.label }}
<span v-if="tab.count !== undefined" class="ml-1 opacity-70">({{ tab.count }})</span> <span v-if="tab.loading" class="ml-1">
<span class="loading loading-spinner loading-xs" />
</span>
<span v-else-if="tab.count !== undefined" class="ml-1 opacity-70">({{ tab.count }})</span>
</a> </a>
</div> </div>
@@ -185,6 +191,10 @@ const props = defineProps<{
selectedProduct?: string | null selectedProduct?: string | null
currentTab?: string currentTab?: string
loading?: boolean loading?: boolean
loadingProducts?: boolean
loadingHubs?: boolean
loadingSuppliers?: boolean
loadingOffers?: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -226,33 +236,45 @@ const entityIcon = computed(() => {
// Available tabs based on entity type and data // Available tabs based on entity type and data
const availableTabs = computed(() => { 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') { 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({ tabs.push({
id: 'offers', id: 'offers',
label: t('catalog.tabs.offers'), label: t('catalog.tabs.offers'),
count: props.selectedProduct count: offersLoading ? undefined : (offersCount || 0),
? props.relatedOffers?.length || 0 loading: offersLoading
: props.relatedProducts?.length || 0
}) })
tabs.push({ tabs.push({
id: 'suppliers', id: 'suppliers',
label: t('catalog.tabs.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') { } 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({ tabs.push({
id: 'offers', id: 'offers',
label: t('catalog.tabs.offers'), label: t('catalog.tabs.offers'),
count: props.selectedProduct count: offersLoading ? undefined : (offersCount || 0),
? props.relatedOffers?.length || 0 loading: offersLoading
: props.relatedProducts?.length || 0
}) })
tabs.push({ tabs.push({
id: 'hubs', id: 'hubs',
label: t('catalog.tabs.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') { } else if (props.entityType === 'offer') {
if (props.relatedProducts && props.relatedProducts.length > 0) { if (props.relatedProducts && props.relatedProducts.length > 0) {
@@ -264,12 +286,15 @@ const availableTabs = computed(() => {
tabs.push({ tabs.push({
id: 'hubs', id: 'hubs',
label: t('catalog.tabs.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) { if (props.relatedSuppliers && props.relatedSuppliers.length > 0) {
tabs.push({ tabs.push({
id: 'suppliers', id: 'suppliers',
label: t('catalog.tabs.supplier') label: t('catalog.tabs.supplier'),
count: props.loadingSuppliers ? undefined : (props.relatedSuppliers?.length || 0),
loading: props.loadingSuppliers
}) })
} }
} }

View File

@@ -287,7 +287,7 @@ export function useCatalogInfo() {
} }
// Load offers for supplier after product selection // Load offers for supplier after product selection
const loadOffersForSupplier = async (supplierUuid: string, productUuid: string) => { const loadOffersForSupplier = async (_supplierUuid: string, productUuid: string) => {
try { try {
const supplier = entity.value const supplier = entity.value
if (!supplier?.latitude || !supplier?.longitude) { if (!supplier?.latitude || !supplier?.longitude) {
@@ -295,6 +295,10 @@ export function useCatalogInfo() {
return return
} }
isLoadingOffers.value = true
isLoadingHubs.value = true
try {
// Find offers near supplier for this product // Find offers near supplier for this product
const offersData = await execute( const offersData = await execute(
NearestOffersDocument, NearestOffersDocument,
@@ -310,6 +314,7 @@ export function useCatalogInfo() {
) )
relatedOffers.value = offersData?.nearestOffers || [] relatedOffers.value = offersData?.nearestOffers || []
isLoadingOffers.value = false
// Load hubs near each offer and aggregate (limit to 12) // Load hubs near each offer and aggregate (limit to 12)
const allHubs = new Map<string, any>() const allHubs = new Map<string, any>()
@@ -341,6 +346,10 @@ export function useCatalogInfo() {
if (allHubs.size >= 12) break if (allHubs.size >= 12) break
} }
relatedHubs.value = Array.from(allHubs.values()).slice(0, 12) relatedHubs.value = Array.from(allHubs.values()).slice(0, 12)
} finally {
isLoadingOffers.value = false
isLoadingHubs.value = false
}
} catch (error) { } catch (error) {
console.error('Error loading offers for supplier:', error) console.error('Error loading offers for supplier:', error)
} }
@@ -396,6 +405,10 @@ export function useCatalogInfo() {
relatedOffers.value = [] relatedOffers.value = []
selectedProduct.value = null selectedProduct.value = null
activeTab.value = 'products' activeTab.value = 'products'
isLoadingProducts.value = false
isLoadingHubs.value = false
isLoadingSuppliers.value = false
isLoadingOffers.value = false
} }
return { return {
@@ -408,6 +421,10 @@ export function useCatalogInfo() {
selectedProduct, selectedProduct,
activeTab, activeTab,
isLoading, isLoading,
isLoadingProducts,
isLoadingHubs,
isLoadingSuppliers,
isLoadingOffers,
// Actions // Actions
loadInfo, loadInfo,

View File

@@ -46,6 +46,10 @@
:selected-product="infoProduct ?? null" :selected-product="infoProduct ?? null"
:current-tab="infoTab" :current-tab="infoTab"
:loading="infoLoading" :loading="infoLoading"
:loading-products="isLoadingProducts"
:loading-hubs="isLoadingHubs"
:loading-suppliers="isLoadingSuppliers"
:loading-offers="isLoadingOffers"
@close="onInfoClose" @close="onInfoClose"
@add-to-filter="onInfoAddToFilter" @add-to-filter="onInfoAddToFilter"
@open-info="onInfoOpenRelated" @open-info="onInfoOpenRelated"
@@ -134,6 +138,10 @@ const {
relatedOffers, relatedOffers,
selectedProduct, selectedProduct,
isLoading: infoLoading, isLoading: infoLoading,
isLoadingProducts,
isLoadingHubs,
isLoadingSuppliers,
isLoadingOffers,
loadInfo, loadInfo,
selectProduct: selectInfoProduct, selectProduct: selectInfoProduct,
clearInfo 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(() => { const relatedPoints = computed(() => {
if (!infoId.value) return [] if (!infoId.value) return []
@@ -265,7 +273,15 @@ const relatedPoints = computed(() => {
type: 'hub' | 'supplier' | 'offer' type: 'hub' | 'supplier' | 'offer'
}> = [] }> = []
// Add hubs const currentTab = infoTab.value
// 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 hubs (for supplier's hubs tab or offer's hubs tab)
if (currentTab === 'hubs') {
relatedHubs.value.forEach(hub => { relatedHubs.value.forEach(hub => {
if (hub.latitude && hub.longitude) { if (hub.latitude && hub.longitude) {
points.push({ points.push({
@@ -277,8 +293,10 @@ const relatedPoints = computed(() => {
}) })
} }
}) })
}
// Add suppliers // Add suppliers (for hub's suppliers tab or offer's suppliers tab)
if (currentTab === 'suppliers') {
relatedSuppliers.value.forEach(supplier => { relatedSuppliers.value.forEach(supplier => {
if (supplier.latitude && supplier.longitude) { if (supplier.latitude && supplier.longitude) {
points.push({ points.push({
@@ -290,8 +308,10 @@ const relatedPoints = computed(() => {
}) })
} }
}) })
}
// Add offers // Add offers (for hub's/supplier's offers tab when product is selected)
if (currentTab === 'offers' && infoProduct.value) {
relatedOffers.value.forEach(offer => { relatedOffers.value.forEach(offer => {
if (offer.latitude && offer.longitude) { if (offer.latitude && offer.longitude) {
points.push({ points.push({
@@ -303,6 +323,7 @@ const relatedPoints = computed(() => {
}) })
} }
}) })
}
return points return points
}) })