Fix type safety in catalog composables + 3 InfoPanel bugs
All checks were successful
Build Docker Image / build (push) Successful in 3m42s

- Add proper codegen types to all catalog composables:
  - useCatalogHubs: HubItem, NearestHubItem
  - useCatalogSuppliers: SupplierItem, NearestSupplierItem
  - useCatalogProducts: ProductItem
  - useCatalogOffers: OfferItem
  - useCatalogInfo: InfoEntity, ProductItem, HubItem, OfferItem

- Fix InfoPanel bugs for offers:
  - Use locationLatitude/locationLongitude for offer coordinates
  - Enrich entity with supplierName after loading profile
  - Apply-to-filter now adds both product AND hub for offers

- Filter null values from GraphQL array responses
- Add type-safe coordinate helper (getEntityCoords)
- Fix urlBounds type inference in useCatalogSearch
This commit is contained in:
Ruslan Bakiev
2026-01-26 23:30:16 +07:00
parent 839ab4e830
commit 70c53da8eb
7 changed files with 190 additions and 84 deletions

View File

@@ -1,9 +1,14 @@
import type { HubsListQueryResult, NearestHubsQueryResult } from '~/composables/graphql/public/geo-generated'
import { HubsListDocument, GetHubCountriesDocument, NearestHubsDocument } from '~/composables/graphql/public/geo-generated' import { HubsListDocument, GetHubCountriesDocument, NearestHubsDocument } from '~/composables/graphql/public/geo-generated'
const PAGE_SIZE = 24 const PAGE_SIZE = 24
// Type from codegen
type HubItem = NonNullable<NonNullable<HubsListQueryResult['hubsList']>[number]>
type NearestHubItem = NonNullable<NonNullable<NearestHubsQueryResult['nearestHubs']>[number]>
// Shared state across list and map views // Shared state across list and map views
const items = ref<any[]>([]) const items = ref<Array<HubItem | NearestHubItem>>([])
const total = ref(0) const total = ref(0)
const selectedFilter = ref('all') const selectedFilter = ref('all')
const selectedCountry = ref('all') const selectedCountry = ref('all')
@@ -67,7 +72,7 @@ export function useCatalogHubs() {
'public', 'public',
'geo' 'geo'
) )
const next = data?.nearestHubs || [] const next = (data?.nearestHubs || []).filter((h): h is NearestHubItem => h !== null)
items.value = next items.value = next
total.value = next.length total.value = next.length
isInitialized.value = true isInitialized.value = true
@@ -95,7 +100,7 @@ export function useCatalogHubs() {
'public', 'public',
'geo' 'geo'
) )
const next = data?.hubsList || [] const next = (data?.hubsList || []).filter((h): h is HubItem => h !== null)
items.value = replace ? next : items.value.concat(next) items.value = replace ? next : items.value.concat(next)
// hubsList doesn't return total count, estimate from fetched items // hubsList doesn't return total count, estimate from fetched items
if (replace) { if (replace) {

View File

@@ -1,24 +1,82 @@
import type { InfoEntityType } from './useCatalogSearch' import type { InfoEntityType } from './useCatalogSearch'
import type {
GetNodeQueryResult,
NearestHubsQueryResult,
NearestOffersQueryResult
} from '~/composables/graphql/public/geo-generated'
import { import {
GetNodeDocument, GetNodeDocument,
NearestOffersDocument, NearestOffersDocument,
NearestHubsDocument NearestHubsDocument
} from '~/composables/graphql/public/geo-generated' } from '~/composables/graphql/public/geo-generated'
import type {
GetOfferQueryResult,
GetSupplierProfileQueryResult
} from '~/composables/graphql/public/exchange-generated'
import { import {
GetOfferDocument, GetOfferDocument,
GetSupplierProfileDocument GetSupplierProfileDocument
} from '~/composables/graphql/public/exchange-generated' } from '~/composables/graphql/public/exchange-generated'
// Types from codegen
type NodeEntity = NonNullable<GetNodeQueryResult['node']>
type OfferEntity = NonNullable<GetOfferQueryResult['getOffer']>
type SupplierProfile = NonNullable<GetSupplierProfileQueryResult['getSupplierProfile']>
type HubItem = NonNullable<NonNullable<NearestHubsQueryResult['nearestHubs']>[number]>
type OfferItem = NonNullable<NonNullable<NearestOffersQueryResult['nearestOffers']>[number]>
// Product type (aggregated from offers)
interface ProductItem {
uuid: string
name: string
offersCount?: number
}
// Extended entity type with optional supplierName
// Using intersection to allow both coordinate patterns (node vs offer)
interface InfoEntity {
uuid?: string | null
name?: string | null
// Node coordinates
latitude?: number | null
longitude?: number | null
// Offer coordinates (different field names)
locationLatitude?: number | null
locationLongitude?: number | null
locationUuid?: string
locationName?: string
// Offer fields
productUuid?: string
productName?: string
teamUuid?: string
// Enriched field
supplierName?: string
// Allow any other fields
[key: string]: unknown
}
// Helper to get coordinates from entity (handles both node and offer patterns)
function getEntityCoords(e: InfoEntity | null): { lat: number; lon: number } | null {
if (!e) return null
// Try offer coords first (locationLatitude/locationLongitude)
const lat = e.locationLatitude ?? e.latitude
const lon = e.locationLongitude ?? e.longitude
if (lat != null && lon != null) {
return { lat, lon }
}
return null
}
export function useCatalogInfo() { export function useCatalogInfo() {
const { execute } = useGraphQL() const { execute } = useGraphQL()
// State // State with proper types
const entity = ref<any>(null) const entity = ref<InfoEntity | null>(null)
const entityType = ref<InfoEntityType | null>(null) // Track entity type explicitly const entityType = ref<InfoEntityType | null>(null)
const relatedProducts = ref<any[]>([]) const relatedProducts = ref<ProductItem[]>([])
const relatedHubs = ref<any[]>([]) const relatedHubs = ref<HubItem[]>([])
const relatedSuppliers = ref<any[]>([]) const relatedSuppliers = ref<SupplierProfile[]>([])
const relatedOffers = ref<any[]>([]) const relatedOffers = ref<OfferItem[]>([])
const selectedProduct = ref<string | null>(null) const selectedProduct = ref<string | null>(null)
const activeTab = ref<string>('products') const activeTab = ref<string>('products')
const isLoading = ref(false) const isLoading = ref(false)
@@ -34,9 +92,10 @@ export function useCatalogInfo() {
try { try {
// Load hub node details // Load hub node details
const nodeData = await execute(GetNodeDocument, { uuid }, 'public', 'geo') const nodeData = await execute(GetNodeDocument, { uuid }, 'public', 'geo')
entity.value = nodeData?.node entity.value = nodeData?.node ?? null
if (!entity.value?.latitude || !entity.value?.longitude) { const coords = getEntityCoords(entity.value)
if (!coords) {
console.warn('Hub has no coordinates') console.warn('Hub has no coordinates')
return return
} }
@@ -52,33 +111,35 @@ export function useCatalogInfo() {
execute( execute(
NearestOffersDocument, NearestOffersDocument,
{ {
lat: entity.value.latitude, lat: coords.lat,
lon: entity.value.longitude, lon: coords.lon,
radius: 500 radius: 500
}, },
'public', 'public',
'geo' 'geo'
).then(offersData => { ).then(offersData => {
// Group offers by product // Group offers by product
const productsMap = new Map<string, any>() const productsMap = new Map<string, ProductItem>()
const suppliersMap = new Map<string, any>() const suppliersMap = new Map<string, { uuid: string; name: string; latitude?: number | null; longitude?: number | null }>()
offersData?.nearestOffers?.forEach((offer: any) => { offersData?.nearestOffers?.forEach(offer => {
if (!offer) return
// Products // Products
if (offer?.productUuid) { if (offer.productUuid && offer.productName) {
if (!productsMap.has(offer.productUuid)) { const existing = productsMap.get(offer.productUuid)
if (existing) {
existing.offersCount = (existing.offersCount || 0) + 1
} else {
productsMap.set(offer.productUuid, { productsMap.set(offer.productUuid, {
uuid: offer.productUuid, uuid: offer.productUuid,
name: offer.productName, name: offer.productName,
offersCount: 0 offersCount: 1
}) })
} }
productsMap.get(offer.productUuid)!.offersCount++
} }
// Suppliers (extract from offers) // Suppliers (extract from offers)
if (offer?.supplierUuid) { if (offer.supplierUuid && !suppliersMap.has(offer.supplierUuid)) {
if (!suppliersMap.has(offer.supplierUuid)) {
suppliersMap.set(offer.supplierUuid, { suppliersMap.set(offer.supplierUuid, {
uuid: offer.supplierUuid, uuid: offer.supplierUuid,
name: offer.supplierName || 'Supplier', name: offer.supplierName || 'Supplier',
@@ -86,7 +147,6 @@ export function useCatalogInfo() {
longitude: offer.longitude longitude: offer.longitude
}) })
} }
}
}) })
relatedProducts.value = Array.from(productsMap.values()) relatedProducts.value = Array.from(productsMap.values())
@@ -101,7 +161,7 @@ export function useCatalogInfo() {
.catch(() => suppliersMap.get(supplierId)) // Fallback to basic info .catch(() => suppliersMap.get(supplierId)) // Fallback to basic info
) )
).then(profiles => { ).then(profiles => {
relatedSuppliers.value = profiles.filter(Boolean) relatedSuppliers.value = profiles.filter((p): p is SupplierProfile => p != null)
isLoadingSuppliers.value = false isLoadingSuppliers.value = false
}) })
} else { } else {
@@ -123,7 +183,7 @@ export function useCatalogInfo() {
try { try {
// Load supplier node details (might be geo node) // Load supplier node details (might be geo node)
const nodeData = await execute(GetNodeDocument, { uuid }, 'public', 'geo') const nodeData = await execute(GetNodeDocument, { uuid }, 'public', 'geo')
entity.value = nodeData?.node entity.value = nodeData?.node ?? null
// Also try to get supplier profile from exchange API for additional details // Also try to get supplier profile from exchange API for additional details
try { try {
@@ -164,18 +224,19 @@ export function useCatalogInfo() {
'geo' 'geo'
).then(offersData => { ).then(offersData => {
// Group offers by product // Group offers by product
const productsMap = new Map<string, any>() const productsMap = new Map<string, ProductItem>()
offersData?.nearestOffers?.forEach((offer: any) => { offersData?.nearestOffers?.forEach(offer => {
if (offer?.productUuid) { if (!offer || !offer.productUuid || !offer.productName) return
if (!productsMap.has(offer.productUuid)) { const existing = productsMap.get(offer.productUuid)
if (existing) {
existing.offersCount = (existing.offersCount || 0) + 1
} else {
productsMap.set(offer.productUuid, { productsMap.set(offer.productUuid, {
uuid: offer.productUuid, uuid: offer.productUuid,
name: offer.productName, name: offer.productName,
offersCount: 0 offersCount: 1
}) })
} }
productsMap.get(offer.productUuid)!.offersCount++
}
}) })
relatedProducts.value = Array.from(productsMap.values()) relatedProducts.value = Array.from(productsMap.values())
}).finally(() => { }).finally(() => {
@@ -194,7 +255,7 @@ export function useCatalogInfo() {
'public', 'public',
'geo' 'geo'
).then(hubsData => { ).then(hubsData => {
relatedHubs.value = hubsData?.nearestHubs || [] relatedHubs.value = (hubsData?.nearestHubs || []).filter((h): h is HubItem => h !== null)
}).finally(() => { }).finally(() => {
isLoadingHubs.value = false isLoadingHubs.value = false
}) })
@@ -210,9 +271,10 @@ export function useCatalogInfo() {
try { try {
// Load offer details from exchange API // Load offer details from exchange API
const offerData = await execute(GetOfferDocument, { uuid }, 'public', 'exchange') const offerData = await execute(GetOfferDocument, { uuid }, 'public', 'exchange')
entity.value = offerData?.getOffer entity.value = offerData?.getOffer ?? null
if (!entity.value?.latitude || !entity.value?.longitude) { const coords = getEntityCoords(entity.value)
if (!coords) {
console.warn('Offer has no coordinates') console.warn('Offer has no coordinates')
return return
} }
@@ -235,15 +297,15 @@ export function useCatalogInfo() {
execute( execute(
NearestHubsDocument, NearestHubsDocument,
{ {
lat: entity.value.latitude, lat: coords.lat,
lon: entity.value.longitude, lon: coords.lon,
radius: 1000, radius: 1000,
limit: 12 limit: 12
}, },
'public', 'public',
'geo' 'geo'
).then(hubsData => { ).then(hubsData => {
relatedHubs.value = hubsData?.nearestHubs || [] relatedHubs.value = (hubsData?.nearestHubs || []).filter((h): h is HubItem => h !== null)
}).finally(() => { }).finally(() => {
isLoadingHubs.value = false isLoadingHubs.value = false
}) })
@@ -257,9 +319,12 @@ export function useCatalogInfo() {
'public', 'public',
'exchange' 'exchange'
).then(supplierData => { ).then(supplierData => {
relatedSuppliers.value = supplierData?.getSupplierProfile const supplier = supplierData?.getSupplierProfile
? [supplierData.getSupplierProfile] relatedSuppliers.value = supplier ? [supplier] : []
: [] // Enrich entity with supplier name for display
if (supplier?.name && entity.value) {
entity.value = { ...entity.value, supplierName: supplier.name }
}
}).catch(() => { }).catch(() => {
// Supplier might not exist // Supplier might not exist
}).finally(() => { }).finally(() => {
@@ -301,19 +366,19 @@ export function useCatalogInfo() {
) )
// Offers already include routes from backend // Offers already include routes from backend
relatedOffers.value = offersData?.nearestOffers || [] relatedOffers.value = (offersData?.nearestOffers || []).filter((o): o is OfferItem => o !== null)
isLoadingOffers.value = false isLoadingOffers.value = false
// Extract unique suppliers from offers (use supplierUuid from offers) // Extract unique suppliers from offers (use supplierUuid from offers)
const supplierUuids = new Set<string>() const supplierUuids = new Set<string>()
relatedOffers.value.forEach((offer: any) => { relatedOffers.value.forEach(offer => {
if (offer.supplierUuid) { if (offer.supplierUuid) {
supplierUuids.add(offer.supplierUuid) supplierUuids.add(offer.supplierUuid)
} }
}) })
// Load supplier profiles (limit to 12) // Load supplier profiles (limit to 12)
const suppliers: any[] = [] const suppliers: SupplierProfile[] = []
for (const uuid of Array.from(supplierUuids).slice(0, 12)) { for (const uuid of Array.from(supplierUuids).slice(0, 12)) {
try { try {
const supplierData = await execute( const supplierData = await execute(
@@ -366,11 +431,11 @@ export function useCatalogInfo() {
'geo' 'geo'
) )
relatedOffers.value = offersData?.nearestOffers || [] relatedOffers.value = (offersData?.nearestOffers || []).filter((o): o is OfferItem => o !== null)
isLoadingOffers.value = false 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, HubItem>()
for (const offer of relatedOffers.value.slice(0, 3)) { for (const offer of relatedOffers.value.slice(0, 3)) {
// Check first 3 offers // Check first 3 offers
if (!offer.latitude || !offer.longitude) continue if (!offer.latitude || !offer.longitude) continue
@@ -387,8 +452,8 @@ export function useCatalogInfo() {
'public', 'public',
'geo' 'geo'
) )
hubsData?.nearestHubs?.forEach((hub: any) => { hubsData?.nearestHubs?.forEach(hub => {
if (!allHubs.has(hub.uuid)) { if (hub && hub.uuid && !allHubs.has(hub.uuid)) {
allHubs.set(hub.uuid, hub) allHubs.set(hub.uuid, hub)
} }
}) })
@@ -415,10 +480,10 @@ export function useCatalogInfo() {
if (!entity.value) return if (!entity.value) return
// Use stored entity type instead of inferring from properties // Use stored entity type instead of inferring from properties
if (entityType.value === 'hub') { if (entityType.value === 'hub' && entity.value.uuid) {
await loadOffersForHub(entity.value.uuid, productUuid) await loadOffersForHub(entity.value.uuid, productUuid)
activeTab.value = 'offers' activeTab.value = 'offers'
} else if (entityType.value === 'supplier') { } else if (entityType.value === 'supplier' && entity.value.uuid) {
await loadOffersForSupplier(entity.value.uuid, productUuid) await loadOffersForSupplier(entity.value.uuid, productUuid)
activeTab.value = 'offers' activeTab.value = 'offers'
} }

View File

@@ -1,9 +1,13 @@
import type { GetOffersQueryResult } from '~/composables/graphql/public/exchange-generated'
import { GetOffersDocument } from '~/composables/graphql/public/exchange-generated' import { GetOffersDocument } from '~/composables/graphql/public/exchange-generated'
const PAGE_SIZE = 24 const PAGE_SIZE = 24
// Type from codegen
type OfferItem = NonNullable<NonNullable<GetOffersQueryResult['getOffers']>[number]>
// Shared state across list and map views // Shared state across list and map views
const items = ref<any[]>([]) const items = ref<OfferItem[]>([])
const total = ref(0) const total = ref(0)
const selectedProductUuid = ref<string | null>(null) const selectedProductUuid = ref<string | null>(null)
const isLoading = ref(false) const isLoading = ref(false)
@@ -18,7 +22,7 @@ export function useCatalogOffers() {
.filter(offer => offer.locationLatitude && offer.locationLongitude) .filter(offer => offer.locationLatitude && offer.locationLongitude)
.map(offer => ({ .map(offer => ({
uuid: offer.uuid, uuid: offer.uuid,
name: offer.productName || offer.title, name: offer.productName || offer.locationName,
latitude: offer.locationLatitude, latitude: offer.locationLatitude,
longitude: offer.locationLongitude, longitude: offer.locationLongitude,
country: offer.locationCountry country: offer.locationCountry
@@ -40,7 +44,7 @@ export function useCatalogOffers() {
'public', 'public',
'exchange' 'exchange'
) )
const next = data?.getOffers || [] const next = (data?.getOffers || []).filter((o): o is OfferItem => o !== null)
items.value = replace ? next : items.value.concat(next) items.value = replace ? next : items.value.concat(next)
total.value = data?.getOffersCount ?? total.value total.value = data?.getOffersCount ?? total.value
isInitialized.value = true isInitialized.value = true

View File

@@ -1,3 +1,4 @@
import type { ProductsListQueryResult } from '~/composables/graphql/public/geo-generated'
import { import {
ProductsListDocument, ProductsListDocument,
GetNodeDocument, GetNodeDocument,
@@ -7,8 +8,11 @@ import {
GetSupplierProfileDocument GetSupplierProfileDocument
} from '~/composables/graphql/public/exchange-generated' } from '~/composables/graphql/public/exchange-generated'
// Type from codegen
type ProductItem = NonNullable<NonNullable<ProductsListQueryResult['productsList']>[number]>
// Shared state // Shared state
const items = ref<any[]>([]) const items = ref<ProductItem[]>([])
const isLoading = ref(false) const isLoading = ref(false)
const isLoadingMore = ref(false) const isLoadingMore = ref(false)
const isInitialized = ref(false) const isInitialized = ref(false)
@@ -130,7 +134,7 @@ export function useCatalogProducts() {
'public', 'public',
'geo' 'geo'
) )
items.value = data?.productsList || [] items.value = (data?.productsList || []).filter((p): p is ProductItem => p !== null)
} }
isInitialized.value = true isInitialized.value = true

View File

@@ -85,12 +85,12 @@ export function useCatalogSearch() {
const quantity = computed(() => route.query.qty as string | undefined) const quantity = computed(() => route.query.qty as string | undefined)
// Map bounds from URL (format: west,south,east,north) // Map bounds from URL (format: west,south,east,north)
const urlBounds = computed(() => { const urlBounds = computed((): { west: number; south: number; east: number; north: number } | null => {
const b = route.query.bounds as string | undefined const b = route.query.bounds as string | undefined
if (!b) return null if (!b) return null
const parts = b.split(',').map(Number) const parts = b.split(',').map(Number)
if (parts.length !== 4 || parts.some(isNaN)) return null if (parts.length !== 4 || parts.some(isNaN)) return null
return { west: parts[0], south: parts[1], east: parts[2], north: parts[3] } return { west: parts[0]!, south: parts[1]!, east: parts[2]!, north: parts[3]! }
}) })
// Filter by bounds checkbox state from URL // Filter by bounds checkbox state from URL

View File

@@ -1,9 +1,14 @@
import type { SuppliersListQueryResult, NearestSuppliersQueryResult } from '~/composables/graphql/public/geo-generated'
import { SuppliersListDocument, NearestSuppliersDocument } from '~/composables/graphql/public/geo-generated' import { SuppliersListDocument, NearestSuppliersDocument } from '~/composables/graphql/public/geo-generated'
const PAGE_SIZE = 24 const PAGE_SIZE = 24
// Types from codegen
type SupplierItem = NonNullable<NonNullable<SuppliersListQueryResult['suppliersList']>[number]>
type NearestSupplierItem = NonNullable<NonNullable<NearestSuppliersQueryResult['nearestSuppliers']>[number]>
// Shared state across list and map views // Shared state across list and map views
const items = ref<any[]>([]) const items = ref<Array<SupplierItem | NearestSupplierItem>>([])
const total = ref(0) const total = ref(0)
const isLoading = ref(false) const isLoading = ref(false)
const isLoadingMore = ref(false) const isLoadingMore = ref(false)
@@ -15,7 +20,7 @@ export function useCatalogSuppliers() {
const { execute } = useGraphQL() const { execute } = useGraphQL()
const itemsWithCoords = computed(() => const itemsWithCoords = computed(() =>
items.value.filter(s => s.latitude && s.longitude) items.value.filter((s): s is NearestSupplierItem => 'latitude' in s && 'longitude' in s && s.latitude != null && s.longitude != null)
) )
const canLoadMore = computed(() => items.value.length < total.value) const canLoadMore = computed(() => items.value.length < total.value)
@@ -38,7 +43,7 @@ export function useCatalogSuppliers() {
'public', 'public',
'geo' 'geo'
) )
items.value = data?.nearestSuppliers || [] items.value = (data?.nearestSuppliers || []).filter((s): s is NearestSupplierItem => s !== null)
total.value = items.value.length total.value = items.value.length
isInitialized.value = true isInitialized.value = true
return return
@@ -60,7 +65,7 @@ export function useCatalogSuppliers() {
'public', 'public',
'geo' 'geo'
) )
const next = data?.suppliersList || [] const next = (data?.suppliersList || []).filter((s): s is SupplierItem => s !== null)
items.value = replace ? next : items.value.concat(next) items.value = replace ? next : items.value.concat(next)
// suppliersList doesn't return total count, estimate from fetched items // suppliersList doesn't return total count, estimate from fetched items

View File

@@ -91,11 +91,27 @@ const onHoverItem = (uuid: string | null) => {
hoveredItemId.value = uuid hoveredItemId.value = uuid
} }
// Type for map items - must have required string uuid and number coordinates
type MapItemWithCoords = { uuid: string; name: string; latitude: number; longitude: number; country?: string }
// Helper to convert items to map-compatible format (filter null values)
const toMapItems = <T extends { uuid?: string | null; name?: string | null; latitude?: number | null; longitude?: number | null }>(
items: T[]
): MapItemWithCoords[] =>
items.filter((item): item is T & { uuid: string; latitude: number; longitude: number } =>
item.uuid != null && item.latitude != null && item.longitude != null
).map(item => ({
uuid: item.uuid,
name: item.name || '',
latitude: item.latitude,
longitude: item.longitude
}))
// Current selection items for hover highlighting on map // Current selection items for hover highlighting on map
const currentSelectionItems = computed(() => { const currentSelectionItems = computed((): MapItemWithCoords[] => {
if (selectMode.value === 'product') return filteredProducts.value if (selectMode.value === 'product') return [] // Products don't have coordinates
if (selectMode.value === 'hub') return filteredHubs.value if (selectMode.value === 'hub') return toMapItems(filteredHubs.value)
if (selectMode.value === 'supplier') return filteredSuppliers.value if (selectMode.value === 'supplier') return toMapItems(filteredSuppliers.value)
return [] return []
}) })
@@ -280,10 +296,10 @@ const relatedPoints = computed(() => {
// Add all hubs // Add all hubs
relatedHubs.value.forEach(hub => { relatedHubs.value.forEach(hub => {
if (hub.latitude && hub.longitude) { if (hub.uuid && hub.latitude && hub.longitude) {
points.push({ points.push({
uuid: hub.uuid, uuid: hub.uuid,
name: hub.name, name: hub.name || '',
latitude: hub.latitude, latitude: hub.latitude,
longitude: hub.longitude, longitude: hub.longitude,
type: 'hub' type: 'hub'
@@ -293,10 +309,10 @@ const relatedPoints = computed(() => {
// Add all suppliers // Add all suppliers
relatedSuppliers.value.forEach(supplier => { relatedSuppliers.value.forEach(supplier => {
if (supplier.latitude && supplier.longitude) { if (supplier.uuid && supplier.latitude && supplier.longitude) {
points.push({ points.push({
uuid: supplier.uuid, uuid: supplier.uuid,
name: supplier.name, name: supplier.name || '',
latitude: supplier.latitude, latitude: supplier.latitude,
longitude: supplier.longitude, longitude: supplier.longitude,
type: 'supplier' type: 'supplier'
@@ -416,10 +432,17 @@ const onInfoAddToFilter = () => {
if (!infoId.value || !entity.value) return if (!infoId.value || !entity.value) return
const { type, uuid } = infoId.value const { type, uuid } = infoId.value
// For offers, add the product to filter (not the offer itself) // For offers, add the product AND hub to filter
if (type === 'offer' && entity.value.productUuid) { if (type === 'offer') {
if (entity.value.productUuid) {
const productName = entity.value.productName || entity.value.name || uuid.slice(0, 8) + '...' const productName = entity.value.productName || entity.value.name || uuid.slice(0, 8) + '...'
selectItem('product', entity.value.productUuid, productName) selectItem('product', entity.value.productUuid, productName)
}
// Also add hub (location) to filter if available
if (entity.value.locationUuid) {
const hubName = entity.value.locationName || entity.value.locationUuid.slice(0, 8) + '...'
selectItem('hub', entity.value.locationUuid, hubName)
}
} else { } else {
// For hubs and suppliers, add directly // For hubs and suppliers, add directly
const name = entity.value.name || uuid.slice(0, 8) + '...' const name = entity.value.name || uuid.slice(0, 8) + '...'