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
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:
@@ -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 }}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, any>()
|
||||
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<string, any>()
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user