refactor(catalog): replace InfoPanel tabs with vertical sections
All checks were successful
Build Docker Image / build (push) Successful in 3m57s
All checks were successful
Build Docker Image / build (push) Successful in 3m57s
- Remove all tabs from InfoPanel, use stacked sections instead - Load suppliers (for hub) and hubs (for supplier) immediately - Show entity header as text, not card - Simplify relatedPoints to show all points on map - Add translations for new section titles
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header -->
|
||||
<!-- Header with close button -->
|
||||
<div class="flex-shrink-0 p-4 border-b border-white/10">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex items-center justify-center w-6 h-6 rounded-full"
|
||||
@@ -27,148 +27,103 @@
|
||||
|
||||
<!-- Content -->
|
||||
<div v-else-if="entity" class="flex flex-col gap-4">
|
||||
<!-- Entity details card -->
|
||||
<div>
|
||||
<HubCard v-if="entityType === 'hub'" :hub="entity" />
|
||||
<SupplierCard v-else-if="entityType === 'supplier'" :supplier="entity" />
|
||||
<OfferCard v-else-if="entityType === 'offer'" :offer="entity" compact />
|
||||
</div>
|
||||
<!-- Entity Info Header (text, not card) -->
|
||||
<div class="mb-2">
|
||||
<!-- Location for hub/supplier -->
|
||||
<p v-if="entityLocation" class="text-sm text-white/70 flex items-center gap-1">
|
||||
<Icon name="lucide:map-pin" size="14" />
|
||||
{{ entityLocation }}
|
||||
</p>
|
||||
|
||||
<!-- Tabs for related objects -->
|
||||
<div role="tablist" class="tabs tabs-boxed bg-white/10">
|
||||
<a
|
||||
v-for="tab in availableTabs"
|
||||
:key="tab.id"
|
||||
role="tab"
|
||||
class="tab text-white/70"
|
||||
:class="{
|
||||
'tab-active !text-white !bg-white/20': activeTab === tab.id,
|
||||
'pointer-events-none opacity-50': tab.loading
|
||||
}"
|
||||
@click="!tab.loading && onTabClick(tab.id)"
|
||||
<!-- Price for offer -->
|
||||
<p v-if="entityType === 'offer' && entity?.pricePerUnit" class="text-sm text-white/70 flex items-center gap-1">
|
||||
<Icon name="lucide:tag" size="14" />
|
||||
{{ formatPrice(entity.pricePerUnit) }} {{ entity.currency || 'RUB' }}/{{ entity.unit || 't' }}
|
||||
</p>
|
||||
|
||||
<!-- Supplier link for offer -->
|
||||
<button
|
||||
v-if="entityType === 'offer' && entity?.teamUuid"
|
||||
class="text-sm text-primary hover:underline flex items-center gap-1 mt-1"
|
||||
@click="emit('open-info', 'supplier', entity.teamUuid)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<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>
|
||||
<Icon name="lucide:factory" size="14" />
|
||||
{{ entity.teamName || $t('catalog.info.viewSupplier') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab content -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Products tab (only for Offer entity) -->
|
||||
<div v-if="activeTab === 'products' && entityType === 'offer'">
|
||||
<div v-if="relatedProducts.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:package" size="32" class="mb-2 opacity-50" />
|
||||
<p>{{ $t('catalog.empty.noProducts') }}</p>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<ProductCard
|
||||
v-for="product in relatedProducts"
|
||||
:key="product.uuid"
|
||||
:product="product"
|
||||
selectable
|
||||
compact
|
||||
@select="onProductSelect(product)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Products Section (for hub/supplier) -->
|
||||
<section v-if="entityType === 'hub' || entityType === 'supplier'">
|
||||
<h3 class="text-sm font-semibold text-white/80 mb-2 flex items-center gap-2">
|
||||
<Icon name="lucide:package" size="16" />
|
||||
{{ productsSectionTitle }}
|
||||
<span v-if="loadingProducts" class="loading loading-spinner loading-xs" />
|
||||
<span v-else-if="relatedProducts.length > 0" class="text-white/50">({{ relatedProducts.length }})</span>
|
||||
</h3>
|
||||
|
||||
<div v-if="!loadingProducts && relatedProducts.length === 0" class="text-white/50 text-sm py-2">
|
||||
{{ $t('catalog.empty.noProducts') }}
|
||||
</div>
|
||||
|
||||
<!-- Hubs tab -->
|
||||
<div v-if="activeTab === 'hubs'">
|
||||
<div v-if="relatedHubs.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:warehouse" size="32" class="mb-2 opacity-50" />
|
||||
<p>{{ $t('catalog.info.noHubs') }}</p>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<HubCard
|
||||
v-for="hub in relatedHubs"
|
||||
:key="hub.uuid"
|
||||
:hub="hub"
|
||||
selectable
|
||||
@select="onHubSelect(hub)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="!loadingProducts" class="flex flex-col gap-2">
|
||||
<ProductCard
|
||||
v-for="product in relatedProducts"
|
||||
:key="product.uuid"
|
||||
:product="product"
|
||||
compact
|
||||
selectable
|
||||
@select="onProductSelect(product)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Suppliers tab -->
|
||||
<div v-if="activeTab === 'suppliers'">
|
||||
<div v-if="relatedSuppliers.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:factory" size="32" class="mb-2 opacity-50" />
|
||||
<p>{{ $t('catalog.info.noSuppliers') }}</p>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<SupplierCard
|
||||
v-for="supplier in relatedSuppliers"
|
||||
:key="supplier.uuid"
|
||||
:supplier="supplier"
|
||||
selectable
|
||||
@select="onSupplierSelect(supplier)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Suppliers Section (for hub only) -->
|
||||
<section v-if="entityType === 'hub'">
|
||||
<h3 class="text-sm font-semibold text-white/80 mb-2 flex items-center gap-2">
|
||||
<Icon name="lucide:factory" size="16" />
|
||||
{{ $t('catalog.info.suppliersNearby') }}
|
||||
<span v-if="loadingSuppliers" class="loading loading-spinner loading-xs" />
|
||||
<span v-else-if="relatedSuppliers.length > 0" class="text-white/50">({{ relatedSuppliers.length }})</span>
|
||||
</h3>
|
||||
|
||||
<div v-if="!loadingSuppliers && relatedSuppliers.length === 0" class="text-white/50 text-sm py-2">
|
||||
{{ $t('catalog.info.noSuppliers') }}
|
||||
</div>
|
||||
|
||||
<!-- Offers tab (two-step for Hub/Supplier) -->
|
||||
<div v-if="activeTab === 'offers'">
|
||||
<!-- Step 1: Select product (for Hub/Supplier) -->
|
||||
<div v-if="!selectedProduct && (entityType === 'hub' || entityType === 'supplier')">
|
||||
<div v-if="relatedProducts.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:package" size="32" class="mb-2 opacity-50" />
|
||||
<p>{{ $t('catalog.empty.noProducts') }}</p>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<ProductCard
|
||||
v-for="product in relatedProducts"
|
||||
:key="product.uuid"
|
||||
:product="product"
|
||||
selectable
|
||||
compact
|
||||
@select="onProductSelect(product)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Show offers for selected product -->
|
||||
<div v-else-if="selectedProduct">
|
||||
<!-- Back button to products -->
|
||||
<button
|
||||
class="btn btn-sm btn-ghost mb-2 text-white/80 hover:text-white"
|
||||
@click="emit('select-product', null)"
|
||||
>
|
||||
<Icon name="lucide:arrow-left" size="16" />
|
||||
{{ $t('common.back') }}
|
||||
</button>
|
||||
|
||||
<div v-if="relatedOffers.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:shopping-bag" size="32" class="mb-2 opacity-50" />
|
||||
<p>{{ $t('catalog.empty.noOffers') }}</p>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<OfferResultCard
|
||||
v-for="offer in relatedOffers"
|
||||
:key="offer.uuid"
|
||||
:location-name="offer.locationName || offer.productName"
|
||||
:product-name="offer.productName"
|
||||
:price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null"
|
||||
:currency="offer.currency"
|
||||
:unit="offer.unit"
|
||||
:stages="getOfferStages(offer)"
|
||||
@select="onOfferSelect(offer)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- For Offer entity type - just show message -->
|
||||
<div v-else class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:info" size="32" class="mb-2 opacity-50" />
|
||||
<p>{{ $t('catalog.info.selectProductFirst') }}</p>
|
||||
</div>
|
||||
<div v-else-if="!loadingSuppliers" class="flex flex-col gap-2">
|
||||
<SupplierCard
|
||||
v-for="supplier in relatedSuppliers"
|
||||
:key="supplier.uuid"
|
||||
:supplier="supplier"
|
||||
selectable
|
||||
@select="onSupplierSelect(supplier)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Hubs Section (for supplier/offer) -->
|
||||
<section v-if="entityType === 'supplier' || entityType === 'offer'">
|
||||
<h3 class="text-sm font-semibold text-white/80 mb-2 flex items-center gap-2">
|
||||
<Icon name="lucide:warehouse" size="16" />
|
||||
{{ $t('catalog.info.nearestHubs') }}
|
||||
<span v-if="loadingHubs" class="loading loading-spinner loading-xs" />
|
||||
<span v-else-if="relatedHubs.length > 0" class="text-white/50">({{ relatedHubs.length }})</span>
|
||||
</h3>
|
||||
|
||||
<div v-if="!loadingHubs && relatedHubs.length === 0" class="text-white/50 text-sm py-2">
|
||||
{{ $t('catalog.info.noHubs') }}
|
||||
</div>
|
||||
<div v-else-if="!loadingHubs" class="flex flex-col gap-2">
|
||||
<HubCard
|
||||
v-for="hub in relatedHubs"
|
||||
:key="hub.uuid"
|
||||
:hub="hub"
|
||||
selectable
|
||||
@select="onHubSelect(hub)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Add to filter button -->
|
||||
<button class="btn btn-primary btn-sm" @click="emit('add-to-filter')">
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="emit('add-to-filter')">
|
||||
<Icon name="lucide:filter-plus" size="16" />
|
||||
{{ $t('catalog.info.addToFilter') }}
|
||||
</button>
|
||||
@@ -212,13 +167,26 @@ const { entityColors } = useCatalogSearch()
|
||||
const relatedProducts = computed(() => props.relatedProducts ?? [])
|
||||
const relatedHubs = computed(() => props.relatedHubs ?? [])
|
||||
const relatedSuppliers = computed(() => props.relatedSuppliers ?? [])
|
||||
const relatedOffers = computed(() => props.relatedOffers ?? [])
|
||||
|
||||
// Entity name
|
||||
const entityName = computed(() => {
|
||||
return props.entity?.name || props.entity?.productName || props.entityId.slice(0, 8) + '...'
|
||||
})
|
||||
|
||||
// Entity location (address, city, country)
|
||||
const entityLocation = computed(() => {
|
||||
if (!props.entity) return null
|
||||
const parts = [props.entity.address, props.entity.city, props.entity.country].filter(Boolean)
|
||||
return parts.length > 0 ? parts.join(', ') : null
|
||||
})
|
||||
|
||||
// Products section title based on entity type
|
||||
const productsSectionTitle = computed(() => {
|
||||
return props.entityType === 'hub'
|
||||
? t('catalog.info.productsHere')
|
||||
: t('catalog.info.productsFromSupplier')
|
||||
})
|
||||
|
||||
// Badge color
|
||||
const badgeColor = computed(() => {
|
||||
if (props.entityType === 'hub') return entityColors.hub
|
||||
@@ -234,92 +202,16 @@ const entityIcon = computed(() => {
|
||||
return 'lucide:info'
|
||||
})
|
||||
|
||||
// Available tabs based on entity type and data
|
||||
const availableTabs = computed(() => {
|
||||
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: offersLoading ? undefined : (offersCount || 0),
|
||||
loading: offersLoading
|
||||
})
|
||||
tabs.push({
|
||||
id: 'suppliers',
|
||||
label: t('catalog.tabs.suppliers'),
|
||||
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: offersLoading ? undefined : (offersCount || 0),
|
||||
loading: offersLoading
|
||||
})
|
||||
tabs.push({
|
||||
id: 'hubs',
|
||||
label: t('catalog.tabs.hubs'),
|
||||
count: props.loadingHubs ? undefined : (props.relatedHubs?.length || 0),
|
||||
loading: props.loadingHubs
|
||||
})
|
||||
} else if (props.entityType === 'offer') {
|
||||
if (props.relatedProducts && props.relatedProducts.length > 0) {
|
||||
tabs.push({
|
||||
id: 'products',
|
||||
label: t('catalog.tabs.product')
|
||||
})
|
||||
}
|
||||
tabs.push({
|
||||
id: 'hubs',
|
||||
label: t('catalog.tabs.hubs'),
|
||||
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'),
|
||||
count: props.loadingSuppliers ? undefined : (props.relatedSuppliers?.length || 0),
|
||||
loading: props.loadingSuppliers
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return tabs
|
||||
})
|
||||
|
||||
// Active tab - use prop or default to first available
|
||||
const activeTab = computed(() => {
|
||||
// If prop is set and is a valid tab, use it
|
||||
if (props.currentTab && availableTabs.value.some(t => t.id === props.currentTab)) {
|
||||
return props.currentTab
|
||||
}
|
||||
// Default to first tab
|
||||
return availableTabs.value[0]?.id || 'offers'
|
||||
})
|
||||
|
||||
// Handler for tab click
|
||||
const onTabClick = (tabId: string) => {
|
||||
emit('update:current-tab', tabId)
|
||||
// Format price
|
||||
const formatPrice = (price: number | string) => {
|
||||
const num = typeof price === 'string' ? parseFloat(price) : price
|
||||
return new Intl.NumberFormat('ru-RU').format(num)
|
||||
}
|
||||
|
||||
// Handlers for selecting related items
|
||||
const onProductSelect = (product: any) => {
|
||||
if (product.uuid) {
|
||||
// Navigate to offer info for this product
|
||||
emit('select-product', product.uuid)
|
||||
}
|
||||
}
|
||||
@@ -335,20 +227,4 @@ const onSupplierSelect = (supplier: any) => {
|
||||
emit('open-info', 'supplier', supplier.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
const onOfferSelect = (offer: any) => {
|
||||
if (offer.uuid) {
|
||||
emit('open-info', 'offer', offer.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract route stages for OfferResultCard
|
||||
const getOfferStages = (offer: any) => {
|
||||
const route = offer.routes?.[0]
|
||||
if (!route?.stages) return []
|
||||
return route.stages.filter(Boolean).map((stage: any) => ({
|
||||
transportType: stage?.transportType,
|
||||
distanceKm: stage?.distanceKm
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useCatalogInfo() {
|
||||
const isLoadingSuppliers = ref(false)
|
||||
const isLoadingOffers = ref(false)
|
||||
|
||||
// Load hub info: hub details + products + suppliers
|
||||
// Load hub info: hub details + products + suppliers (in parallel)
|
||||
const loadHubInfo = async (uuid: string) => {
|
||||
try {
|
||||
// Load hub node details
|
||||
@@ -44,23 +44,27 @@ export function useCatalogInfo() {
|
||||
// Set default active tab to offers (first step shows products)
|
||||
activeTab.value = 'offers'
|
||||
|
||||
// Load products (offers grouped by product)
|
||||
// Load products AND suppliers in parallel
|
||||
isLoadingProducts.value = true
|
||||
try {
|
||||
const offersData = await execute(
|
||||
NearestOffersDocument,
|
||||
{
|
||||
lat: entity.value.latitude,
|
||||
lon: entity.value.longitude,
|
||||
radius: 500
|
||||
},
|
||||
'public',
|
||||
'geo'
|
||||
)
|
||||
isLoadingSuppliers.value = true
|
||||
|
||||
// Load products (offers grouped by product) and extract suppliers
|
||||
execute(
|
||||
NearestOffersDocument,
|
||||
{
|
||||
lat: entity.value.latitude,
|
||||
lon: entity.value.longitude,
|
||||
radius: 500
|
||||
},
|
||||
'public',
|
||||
'geo'
|
||||
).then(offersData => {
|
||||
// Group offers by product
|
||||
const productsMap = new Map<string, any>()
|
||||
const suppliersMap = new Map<string, any>()
|
||||
|
||||
offersData?.nearestOffers?.forEach((offer: any) => {
|
||||
// Products
|
||||
if (offer?.productUuid) {
|
||||
if (!productsMap.has(offer.productUuid)) {
|
||||
productsMap.set(offer.productUuid, {
|
||||
@@ -71,19 +75,50 @@ export function useCatalogInfo() {
|
||||
}
|
||||
productsMap.get(offer.productUuid)!.offersCount++
|
||||
}
|
||||
})
|
||||
relatedProducts.value = Array.from(productsMap.values())
|
||||
} finally {
|
||||
isLoadingProducts.value = false
|
||||
}
|
||||
|
||||
// Note: Suppliers and Offers loaded after product selection via loadOffersForHub
|
||||
// Suppliers (extract from offers)
|
||||
if (offer?.supplierUuid) {
|
||||
if (!suppliersMap.has(offer.supplierUuid)) {
|
||||
suppliersMap.set(offer.supplierUuid, {
|
||||
uuid: offer.supplierUuid,
|
||||
name: offer.supplierName || 'Supplier',
|
||||
latitude: offer.latitude,
|
||||
longitude: offer.longitude
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
relatedProducts.value = Array.from(productsMap.values())
|
||||
|
||||
// Load supplier profiles for detailed info
|
||||
const supplierUuids = Array.from(suppliersMap.keys()).slice(0, 12)
|
||||
if (supplierUuids.length > 0) {
|
||||
Promise.all(
|
||||
supplierUuids.map(supplierId =>
|
||||
execute(GetSupplierProfileDocument, { uuid: supplierId }, 'public', 'exchange')
|
||||
.then(data => data?.getSupplierProfile)
|
||||
.catch(() => suppliersMap.get(supplierId)) // Fallback to basic info
|
||||
)
|
||||
).then(profiles => {
|
||||
relatedSuppliers.value = profiles.filter(Boolean)
|
||||
isLoadingSuppliers.value = false
|
||||
})
|
||||
} else {
|
||||
relatedSuppliers.value = []
|
||||
isLoadingSuppliers.value = false
|
||||
}
|
||||
}).finally(() => {
|
||||
isLoadingProducts.value = false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error loading hub info:', error)
|
||||
isLoadingProducts.value = false
|
||||
isLoadingSuppliers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Load supplier info: supplier details + products
|
||||
// Load supplier info: supplier details + products + hubs (in parallel)
|
||||
const loadSupplierInfo = async (uuid: string) => {
|
||||
try {
|
||||
// Load supplier node details (might be geo node)
|
||||
@@ -113,20 +148,21 @@ export function useCatalogInfo() {
|
||||
// Set default active tab to offers (first step shows products)
|
||||
activeTab.value = 'offers'
|
||||
|
||||
// Load products (offers grouped by product)
|
||||
// Load products AND hubs in parallel
|
||||
isLoadingProducts.value = true
|
||||
try {
|
||||
const offersData = await execute(
|
||||
NearestOffersDocument,
|
||||
{
|
||||
lat: entity.value.latitude,
|
||||
lon: entity.value.longitude,
|
||||
radius: 500
|
||||
},
|
||||
'public',
|
||||
'geo'
|
||||
)
|
||||
isLoadingHubs.value = true
|
||||
|
||||
// Load products (offers grouped by product)
|
||||
execute(
|
||||
NearestOffersDocument,
|
||||
{
|
||||
lat: entity.value.latitude,
|
||||
lon: entity.value.longitude,
|
||||
radius: 500
|
||||
},
|
||||
'public',
|
||||
'geo'
|
||||
).then(offersData => {
|
||||
// Group offers by product
|
||||
const productsMap = new Map<string, any>()
|
||||
offersData?.nearestOffers?.forEach((offer: any) => {
|
||||
@@ -142,13 +178,30 @@ export function useCatalogInfo() {
|
||||
}
|
||||
})
|
||||
relatedProducts.value = Array.from(productsMap.values())
|
||||
} finally {
|
||||
}).finally(() => {
|
||||
isLoadingProducts.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Note: Hubs will be loaded after product selection
|
||||
// Load hubs near supplier
|
||||
execute(
|
||||
NearestHubsDocument,
|
||||
{
|
||||
lat: entity.value.latitude,
|
||||
lon: entity.value.longitude,
|
||||
radius: 1000,
|
||||
limit: 12
|
||||
},
|
||||
'public',
|
||||
'geo'
|
||||
).then(hubsData => {
|
||||
relatedHubs.value = hubsData?.nearestHubs || []
|
||||
}).finally(() => {
|
||||
isLoadingHubs.value = false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error loading supplier info:', error)
|
||||
isLoadingProducts.value = false
|
||||
isLoadingHubs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
:related-suppliers="relatedSuppliers"
|
||||
:related-offers="relatedOffers"
|
||||
:selected-product="infoProduct ?? null"
|
||||
:current-tab="infoTab"
|
||||
:loading="infoLoading"
|
||||
:loading-products="isLoadingProducts"
|
||||
:loading-hubs="isLoadingHubs"
|
||||
@@ -54,7 +53,6 @@
|
||||
@add-to-filter="onInfoAddToFilter"
|
||||
@open-info="onInfoOpenRelated"
|
||||
@select-product="onInfoSelectProduct"
|
||||
@update:current-tab="setInfoTab"
|
||||
/>
|
||||
|
||||
<!-- Quote results: show offers after search -->
|
||||
@@ -111,7 +109,6 @@ const {
|
||||
catalogMode,
|
||||
selectMode,
|
||||
infoId,
|
||||
infoTab,
|
||||
infoProduct,
|
||||
productId,
|
||||
supplierId,
|
||||
@@ -124,7 +121,6 @@ const {
|
||||
cancelSelect,
|
||||
openInfo,
|
||||
closeInfo,
|
||||
setInfoTab,
|
||||
setInfoProduct,
|
||||
setLabel
|
||||
} = useCatalogSearch()
|
||||
@@ -261,7 +257,7 @@ watch(infoProduct, async (productUuid) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Related points for Info mode (shown on map) - filtered by active tab
|
||||
// Related points for Info mode (shown on map) - show all related entities
|
||||
const relatedPoints = computed(() => {
|
||||
if (!infoId.value) return []
|
||||
|
||||
@@ -273,57 +269,31 @@ const relatedPoints = computed(() => {
|
||||
type: 'hub' | 'supplier' | 'offer'
|
||||
}> = []
|
||||
|
||||
const currentTab = infoTab.value
|
||||
// Add all 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 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 => {
|
||||
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'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// Add all 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return points
|
||||
})
|
||||
|
||||
@@ -56,7 +56,14 @@
|
||||
},
|
||||
"info": {
|
||||
"selectProductFirst": "Select a product first",
|
||||
"addToFilter": "Add to filter"
|
||||
"addToFilter": "Add to filter",
|
||||
"productsHere": "Products available here",
|
||||
"productsFromSupplier": "Products from this supplier",
|
||||
"nearestHubs": "Nearest hubs",
|
||||
"suppliersNearby": "Suppliers nearby",
|
||||
"noHubs": "No hubs found",
|
||||
"noSuppliers": "No suppliers found",
|
||||
"viewSupplier": "View supplier"
|
||||
},
|
||||
"modes": {
|
||||
"explore": "Explore",
|
||||
|
||||
@@ -56,7 +56,14 @@
|
||||
},
|
||||
"info": {
|
||||
"selectProductFirst": "Сначала выберите товар",
|
||||
"addToFilter": "Добавить в фильтр"
|
||||
"addToFilter": "Добавить в фильтр",
|
||||
"productsHere": "Товары доступные здесь",
|
||||
"productsFromSupplier": "Товары от этого поставщика",
|
||||
"nearestHubs": "Ближайшие хабы",
|
||||
"suppliersNearby": "Поставщики рядом",
|
||||
"noHubs": "Хабы не найдены",
|
||||
"noSuppliers": "Поставщики не найдены",
|
||||
"viewSupplier": "Посмотреть поставщика"
|
||||
},
|
||||
"modes": {
|
||||
"explore": "Исследовать",
|
||||
|
||||
Reference in New Issue
Block a user