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>
|
<template>
|
||||||
<div class="flex flex-col h-full">
|
<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-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 gap-2">
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center w-6 h-6 rounded-full"
|
class="flex items-center justify-center w-6 h-6 rounded-full"
|
||||||
@@ -27,78 +27,68 @@
|
|||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div v-else-if="entity" class="flex flex-col gap-4">
|
<div v-else-if="entity" class="flex flex-col gap-4">
|
||||||
<!-- Entity details card -->
|
<!-- Entity Info Header (text, not card) -->
|
||||||
<div>
|
<div class="mb-2">
|
||||||
<HubCard v-if="entityType === 'hub'" :hub="entity" />
|
<!-- Location for hub/supplier -->
|
||||||
<SupplierCard v-else-if="entityType === 'supplier'" :supplier="entity" />
|
<p v-if="entityLocation" class="text-sm text-white/70 flex items-center gap-1">
|
||||||
<OfferCard v-else-if="entityType === 'offer'" :offer="entity" compact />
|
<Icon name="lucide:map-pin" size="14" />
|
||||||
</div>
|
{{ entityLocation }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Tabs for related objects -->
|
<!-- Price for offer -->
|
||||||
<div role="tablist" class="tabs tabs-boxed bg-white/10">
|
<p v-if="entityType === 'offer' && entity?.pricePerUnit" class="text-sm text-white/70 flex items-center gap-1">
|
||||||
<a
|
<Icon name="lucide:tag" size="14" />
|
||||||
v-for="tab in availableTabs"
|
{{ formatPrice(entity.pricePerUnit) }} {{ entity.currency || 'RUB' }}/{{ entity.unit || 't' }}
|
||||||
:key="tab.id"
|
</p>
|
||||||
role="tab"
|
|
||||||
class="tab text-white/70"
|
<!-- Supplier link for offer -->
|
||||||
:class="{
|
<button
|
||||||
'tab-active !text-white !bg-white/20': activeTab === tab.id,
|
v-if="entityType === 'offer' && entity?.teamUuid"
|
||||||
'pointer-events-none opacity-50': tab.loading
|
class="text-sm text-primary hover:underline flex items-center gap-1 mt-1"
|
||||||
}"
|
@click="emit('open-info', 'supplier', entity.teamUuid)"
|
||||||
@click="!tab.loading && onTabClick(tab.id)"
|
|
||||||
>
|
>
|
||||||
{{ tab.label }}
|
<Icon name="lucide:factory" size="14" />
|
||||||
<span v-if="tab.loading" class="ml-1">
|
{{ entity.teamName || $t('catalog.info.viewSupplier') }}
|
||||||
<span class="loading loading-spinner loading-xs" />
|
</button>
|
||||||
</span>
|
|
||||||
<span v-else-if="tab.count !== undefined" class="ml-1 opacity-70">({{ tab.count }})</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab content -->
|
<!-- Products Section (for hub/supplier) -->
|
||||||
<div class="flex flex-col gap-2">
|
<section v-if="entityType === 'hub' || entityType === 'supplier'">
|
||||||
<!-- Products tab (only for Offer entity) -->
|
<h3 class="text-sm font-semibold text-white/80 mb-2 flex items-center gap-2">
|
||||||
<div v-if="activeTab === 'products' && entityType === 'offer'">
|
<Icon name="lucide:package" size="16" />
|
||||||
<div v-if="relatedProducts.length === 0" class="text-center py-8 text-white/60">
|
{{ productsSectionTitle }}
|
||||||
<Icon name="lucide:package" size="32" class="mb-2 opacity-50" />
|
<span v-if="loadingProducts" class="loading loading-spinner loading-xs" />
|
||||||
<p>{{ $t('catalog.empty.noProducts') }}</p>
|
<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>
|
</div>
|
||||||
<div v-else class="flex flex-col gap-2">
|
<div v-else-if="!loadingProducts" class="flex flex-col gap-2">
|
||||||
<ProductCard
|
<ProductCard
|
||||||
v-for="product in relatedProducts"
|
v-for="product in relatedProducts"
|
||||||
:key="product.uuid"
|
:key="product.uuid"
|
||||||
:product="product"
|
:product="product"
|
||||||
selectable
|
|
||||||
compact
|
compact
|
||||||
|
selectable
|
||||||
@select="onProductSelect(product)"
|
@select="onProductSelect(product)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Hubs tab -->
|
<!-- Suppliers Section (for hub only) -->
|
||||||
<div v-if="activeTab === 'hubs'">
|
<section v-if="entityType === 'hub'">
|
||||||
<div v-if="relatedHubs.length === 0" class="text-center py-8 text-white/60">
|
<h3 class="text-sm font-semibold text-white/80 mb-2 flex items-center gap-2">
|
||||||
<Icon name="lucide:warehouse" size="32" class="mb-2 opacity-50" />
|
<Icon name="lucide:factory" size="16" />
|
||||||
<p>{{ $t('catalog.info.noHubs') }}</p>
|
{{ $t('catalog.info.suppliersNearby') }}
|
||||||
</div>
|
<span v-if="loadingSuppliers" class="loading loading-spinner loading-xs" />
|
||||||
<div v-else class="flex flex-col gap-2">
|
<span v-else-if="relatedSuppliers.length > 0" class="text-white/50">({{ relatedSuppliers.length }})</span>
|
||||||
<HubCard
|
</h3>
|
||||||
v-for="hub in relatedHubs"
|
|
||||||
:key="hub.uuid"
|
|
||||||
:hub="hub"
|
|
||||||
selectable
|
|
||||||
@select="onHubSelect(hub)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Suppliers tab -->
|
<div v-if="!loadingSuppliers && relatedSuppliers.length === 0" class="text-white/50 text-sm py-2">
|
||||||
<div v-if="activeTab === 'suppliers'">
|
{{ $t('catalog.info.noSuppliers') }}
|
||||||
<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>
|
||||||
<div v-else class="flex flex-col gap-2">
|
<div v-else-if="!loadingSuppliers" class="flex flex-col gap-2">
|
||||||
<SupplierCard
|
<SupplierCard
|
||||||
v-for="supplier in relatedSuppliers"
|
v-for="supplier in relatedSuppliers"
|
||||||
:key="supplier.uuid"
|
:key="supplier.uuid"
|
||||||
@@ -107,68 +97,33 @@
|
|||||||
@select="onSupplierSelect(supplier)"
|
@select="onSupplierSelect(supplier)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Offers tab (two-step for Hub/Supplier) -->
|
<!-- Hubs Section (for supplier/offer) -->
|
||||||
<div v-if="activeTab === 'offers'">
|
<section v-if="entityType === 'supplier' || entityType === 'offer'">
|
||||||
<!-- Step 1: Select product (for Hub/Supplier) -->
|
<h3 class="text-sm font-semibold text-white/80 mb-2 flex items-center gap-2">
|
||||||
<div v-if="!selectedProduct && (entityType === 'hub' || entityType === 'supplier')">
|
<Icon name="lucide:warehouse" size="16" />
|
||||||
<div v-if="relatedProducts.length === 0" class="text-center py-8 text-white/60">
|
{{ $t('catalog.info.nearestHubs') }}
|
||||||
<Icon name="lucide:package" size="32" class="mb-2 opacity-50" />
|
<span v-if="loadingHubs" class="loading loading-spinner loading-xs" />
|
||||||
<p>{{ $t('catalog.empty.noProducts') }}</p>
|
<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>
|
||||||
<div v-else class="flex flex-col gap-2">
|
<div v-else-if="!loadingHubs" class="flex flex-col gap-2">
|
||||||
<ProductCard
|
<HubCard
|
||||||
v-for="product in relatedProducts"
|
v-for="hub in relatedHubs"
|
||||||
:key="product.uuid"
|
:key="hub.uuid"
|
||||||
:product="product"
|
:hub="hub"
|
||||||
selectable
|
selectable
|
||||||
compact
|
@select="onHubSelect(hub)"
|
||||||
@select="onProductSelect(product)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add to filter button -->
|
<!-- 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" />
|
<Icon name="lucide:filter-plus" size="16" />
|
||||||
{{ $t('catalog.info.addToFilter') }}
|
{{ $t('catalog.info.addToFilter') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -212,13 +167,26 @@ const { entityColors } = useCatalogSearch()
|
|||||||
const relatedProducts = computed(() => props.relatedProducts ?? [])
|
const relatedProducts = computed(() => props.relatedProducts ?? [])
|
||||||
const relatedHubs = computed(() => props.relatedHubs ?? [])
|
const relatedHubs = computed(() => props.relatedHubs ?? [])
|
||||||
const relatedSuppliers = computed(() => props.relatedSuppliers ?? [])
|
const relatedSuppliers = computed(() => props.relatedSuppliers ?? [])
|
||||||
const relatedOffers = computed(() => props.relatedOffers ?? [])
|
|
||||||
|
|
||||||
// Entity name
|
// Entity name
|
||||||
const entityName = computed(() => {
|
const entityName = computed(() => {
|
||||||
return props.entity?.name || props.entity?.productName || props.entityId.slice(0, 8) + '...'
|
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
|
// Badge color
|
||||||
const badgeColor = computed(() => {
|
const badgeColor = computed(() => {
|
||||||
if (props.entityType === 'hub') return entityColors.hub
|
if (props.entityType === 'hub') return entityColors.hub
|
||||||
@@ -234,92 +202,16 @@ const entityIcon = computed(() => {
|
|||||||
return 'lucide:info'
|
return 'lucide:info'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Available tabs based on entity type and data
|
// Format price
|
||||||
const availableTabs = computed(() => {
|
const formatPrice = (price: number | string) => {
|
||||||
const tabs: Array<{ id: string; label: string; count?: number; loading?: boolean }> = []
|
const num = typeof price === 'string' ? parseFloat(price) : price
|
||||||
|
return new Intl.NumberFormat('ru-RU').format(num)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlers for selecting related items
|
// Handlers for selecting related items
|
||||||
const onProductSelect = (product: any) => {
|
const onProductSelect = (product: any) => {
|
||||||
if (product.uuid) {
|
if (product.uuid) {
|
||||||
|
// Navigate to offer info for this product
|
||||||
emit('select-product', product.uuid)
|
emit('select-product', product.uuid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,20 +227,4 @@ const onSupplierSelect = (supplier: any) => {
|
|||||||
emit('open-info', 'supplier', supplier.uuid)
|
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>
|
</script>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function useCatalogInfo() {
|
|||||||
const isLoadingSuppliers = ref(false)
|
const isLoadingSuppliers = ref(false)
|
||||||
const isLoadingOffers = 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) => {
|
const loadHubInfo = async (uuid: string) => {
|
||||||
try {
|
try {
|
||||||
// Load hub node details
|
// Load hub node details
|
||||||
@@ -44,10 +44,12 @@ export function useCatalogInfo() {
|
|||||||
// Set default active tab to offers (first step shows products)
|
// Set default active tab to offers (first step shows products)
|
||||||
activeTab.value = 'offers'
|
activeTab.value = 'offers'
|
||||||
|
|
||||||
// Load products (offers grouped by product)
|
// Load products AND suppliers in parallel
|
||||||
isLoadingProducts.value = true
|
isLoadingProducts.value = true
|
||||||
try {
|
isLoadingSuppliers.value = true
|
||||||
const offersData = await execute(
|
|
||||||
|
// Load products (offers grouped by product) and extract suppliers
|
||||||
|
execute(
|
||||||
NearestOffersDocument,
|
NearestOffersDocument,
|
||||||
{
|
{
|
||||||
lat: entity.value.latitude,
|
lat: entity.value.latitude,
|
||||||
@@ -56,11 +58,13 @@ export function useCatalogInfo() {
|
|||||||
},
|
},
|
||||||
'public',
|
'public',
|
||||||
'geo'
|
'geo'
|
||||||
)
|
).then(offersData => {
|
||||||
|
|
||||||
// Group offers by product
|
// Group offers by product
|
||||||
const productsMap = new Map<string, any>()
|
const productsMap = new Map<string, any>()
|
||||||
|
const suppliersMap = new Map<string, any>()
|
||||||
|
|
||||||
offersData?.nearestOffers?.forEach((offer: any) => {
|
offersData?.nearestOffers?.forEach((offer: any) => {
|
||||||
|
// Products
|
||||||
if (offer?.productUuid) {
|
if (offer?.productUuid) {
|
||||||
if (!productsMap.has(offer.productUuid)) {
|
if (!productsMap.has(offer.productUuid)) {
|
||||||
productsMap.set(offer.productUuid, {
|
productsMap.set(offer.productUuid, {
|
||||||
@@ -71,19 +75,50 @@ export function useCatalogInfo() {
|
|||||||
}
|
}
|
||||||
productsMap.get(offer.productUuid)!.offersCount++
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading hub info:', 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) => {
|
const loadSupplierInfo = async (uuid: string) => {
|
||||||
try {
|
try {
|
||||||
// Load supplier node details (might be geo node)
|
// Load supplier node details (might be geo node)
|
||||||
@@ -113,10 +148,12 @@ export function useCatalogInfo() {
|
|||||||
// Set default active tab to offers (first step shows products)
|
// Set default active tab to offers (first step shows products)
|
||||||
activeTab.value = 'offers'
|
activeTab.value = 'offers'
|
||||||
|
|
||||||
// Load products (offers grouped by product)
|
// Load products AND hubs in parallel
|
||||||
isLoadingProducts.value = true
|
isLoadingProducts.value = true
|
||||||
try {
|
isLoadingHubs.value = true
|
||||||
const offersData = await execute(
|
|
||||||
|
// Load products (offers grouped by product)
|
||||||
|
execute(
|
||||||
NearestOffersDocument,
|
NearestOffersDocument,
|
||||||
{
|
{
|
||||||
lat: entity.value.latitude,
|
lat: entity.value.latitude,
|
||||||
@@ -125,8 +162,7 @@ export function useCatalogInfo() {
|
|||||||
},
|
},
|
||||||
'public',
|
'public',
|
||||||
'geo'
|
'geo'
|
||||||
)
|
).then(offersData => {
|
||||||
|
|
||||||
// Group offers by product
|
// Group offers by product
|
||||||
const productsMap = new Map<string, any>()
|
const productsMap = new Map<string, any>()
|
||||||
offersData?.nearestOffers?.forEach((offer: any) => {
|
offersData?.nearestOffers?.forEach((offer: any) => {
|
||||||
@@ -142,13 +178,30 @@ export function useCatalogInfo() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
relatedProducts.value = Array.from(productsMap.values())
|
relatedProducts.value = Array.from(productsMap.values())
|
||||||
} finally {
|
}).finally(() => {
|
||||||
isLoadingProducts.value = false
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading supplier info:', error)
|
console.error('Error loading supplier info:', error)
|
||||||
|
isLoadingProducts.value = false
|
||||||
|
isLoadingHubs.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
:related-suppliers="relatedSuppliers"
|
:related-suppliers="relatedSuppliers"
|
||||||
:related-offers="relatedOffers"
|
:related-offers="relatedOffers"
|
||||||
:selected-product="infoProduct ?? null"
|
:selected-product="infoProduct ?? null"
|
||||||
:current-tab="infoTab"
|
|
||||||
:loading="infoLoading"
|
:loading="infoLoading"
|
||||||
:loading-products="isLoadingProducts"
|
:loading-products="isLoadingProducts"
|
||||||
:loading-hubs="isLoadingHubs"
|
:loading-hubs="isLoadingHubs"
|
||||||
@@ -54,7 +53,6 @@
|
|||||||
@add-to-filter="onInfoAddToFilter"
|
@add-to-filter="onInfoAddToFilter"
|
||||||
@open-info="onInfoOpenRelated"
|
@open-info="onInfoOpenRelated"
|
||||||
@select-product="onInfoSelectProduct"
|
@select-product="onInfoSelectProduct"
|
||||||
@update:current-tab="setInfoTab"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Quote results: show offers after search -->
|
<!-- Quote results: show offers after search -->
|
||||||
@@ -111,7 +109,6 @@ const {
|
|||||||
catalogMode,
|
catalogMode,
|
||||||
selectMode,
|
selectMode,
|
||||||
infoId,
|
infoId,
|
||||||
infoTab,
|
|
||||||
infoProduct,
|
infoProduct,
|
||||||
productId,
|
productId,
|
||||||
supplierId,
|
supplierId,
|
||||||
@@ -124,7 +121,6 @@ const {
|
|||||||
cancelSelect,
|
cancelSelect,
|
||||||
openInfo,
|
openInfo,
|
||||||
closeInfo,
|
closeInfo,
|
||||||
setInfoTab,
|
|
||||||
setInfoProduct,
|
setInfoProduct,
|
||||||
setLabel
|
setLabel
|
||||||
} = useCatalogSearch()
|
} = 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(() => {
|
const relatedPoints = computed(() => {
|
||||||
if (!infoId.value) return []
|
if (!infoId.value) return []
|
||||||
|
|
||||||
@@ -273,15 +269,7 @@ const relatedPoints = computed(() => {
|
|||||||
type: 'hub' | 'supplier' | 'offer'
|
type: 'hub' | 'supplier' | 'offer'
|
||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
const currentTab = infoTab.value
|
// Add all hubs
|
||||||
|
|
||||||
// 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({
|
||||||
@@ -293,10 +281,8 @@ const relatedPoints = computed(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Add suppliers (for hub's suppliers tab or offer's suppliers tab)
|
// Add all suppliers
|
||||||
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({
|
||||||
@@ -308,22 +294,6 @@ const relatedPoints = computed(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
return points
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -56,7 +56,14 @@
|
|||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"selectProductFirst": "Select a product first",
|
"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": {
|
"modes": {
|
||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
|
|||||||
@@ -56,7 +56,14 @@
|
|||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"selectProductFirst": "Сначала выберите товар",
|
"selectProductFirst": "Сначала выберите товар",
|
||||||
"addToFilter": "Добавить в фильтр"
|
"addToFilter": "Добавить в фильтр",
|
||||||
|
"productsHere": "Товары доступные здесь",
|
||||||
|
"productsFromSupplier": "Товары от этого поставщика",
|
||||||
|
"nearestHubs": "Ближайшие хабы",
|
||||||
|
"suppliersNearby": "Поставщики рядом",
|
||||||
|
"noHubs": "Хабы не найдены",
|
||||||
|
"noSuppliers": "Поставщики не найдены",
|
||||||
|
"viewSupplier": "Посмотреть поставщика"
|
||||||
},
|
},
|
||||||
"modes": {
|
"modes": {
|
||||||
"explore": "Исследовать",
|
"explore": "Исследовать",
|
||||||
|
|||||||
Reference in New Issue
Block a user