Fix type safety in catalog composables + 3 InfoPanel bugs
All checks were successful
Build Docker Image / build (push) Successful in 3m42s
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:
@@ -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) {
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) + '...'
|
||||||
|
|||||||
Reference in New Issue
Block a user