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,24 +1,82 @@
|
||||
import type { InfoEntityType } from './useCatalogSearch'
|
||||
import type {
|
||||
GetNodeQueryResult,
|
||||
NearestHubsQueryResult,
|
||||
NearestOffersQueryResult
|
||||
} from '~/composables/graphql/public/geo-generated'
|
||||
import {
|
||||
GetNodeDocument,
|
||||
NearestOffersDocument,
|
||||
NearestHubsDocument
|
||||
} from '~/composables/graphql/public/geo-generated'
|
||||
import type {
|
||||
GetOfferQueryResult,
|
||||
GetSupplierProfileQueryResult
|
||||
} from '~/composables/graphql/public/exchange-generated'
|
||||
import {
|
||||
GetOfferDocument,
|
||||
GetSupplierProfileDocument
|
||||
} 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() {
|
||||
const { execute } = useGraphQL()
|
||||
|
||||
// State
|
||||
const entity = ref<any>(null)
|
||||
const entityType = ref<InfoEntityType | null>(null) // Track entity type explicitly
|
||||
const relatedProducts = ref<any[]>([])
|
||||
const relatedHubs = ref<any[]>([])
|
||||
const relatedSuppliers = ref<any[]>([])
|
||||
const relatedOffers = ref<any[]>([])
|
||||
// State with proper types
|
||||
const entity = ref<InfoEntity | null>(null)
|
||||
const entityType = ref<InfoEntityType | null>(null)
|
||||
const relatedProducts = ref<ProductItem[]>([])
|
||||
const relatedHubs = ref<HubItem[]>([])
|
||||
const relatedSuppliers = ref<SupplierProfile[]>([])
|
||||
const relatedOffers = ref<OfferItem[]>([])
|
||||
const selectedProduct = ref<string | null>(null)
|
||||
const activeTab = ref<string>('products')
|
||||
const isLoading = ref(false)
|
||||
@@ -34,9 +92,10 @@ export function useCatalogInfo() {
|
||||
try {
|
||||
// Load hub node details
|
||||
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')
|
||||
return
|
||||
}
|
||||
@@ -52,40 +111,41 @@ export function useCatalogInfo() {
|
||||
execute(
|
||||
NearestOffersDocument,
|
||||
{
|
||||
lat: entity.value.latitude,
|
||||
lon: entity.value.longitude,
|
||||
lat: coords.lat,
|
||||
lon: coords.lon,
|
||||
radius: 500
|
||||
},
|
||||
'public',
|
||||
'geo'
|
||||
).then(offersData => {
|
||||
// Group offers by product
|
||||
const productsMap = new Map<string, any>()
|
||||
const suppliersMap = new Map<string, any>()
|
||||
const productsMap = new Map<string, ProductItem>()
|
||||
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
|
||||
if (offer?.productUuid) {
|
||||
if (!productsMap.has(offer.productUuid)) {
|
||||
if (offer.productUuid && offer.productName) {
|
||||
const existing = productsMap.get(offer.productUuid)
|
||||
if (existing) {
|
||||
existing.offersCount = (existing.offersCount || 0) + 1
|
||||
} else {
|
||||
productsMap.set(offer.productUuid, {
|
||||
uuid: offer.productUuid,
|
||||
name: offer.productName,
|
||||
offersCount: 0
|
||||
offersCount: 1
|
||||
})
|
||||
}
|
||||
productsMap.get(offer.productUuid)!.offersCount++
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
if (offer.supplierUuid && !suppliersMap.has(offer.supplierUuid)) {
|
||||
suppliersMap.set(offer.supplierUuid, {
|
||||
uuid: offer.supplierUuid,
|
||||
name: offer.supplierName || 'Supplier',
|
||||
latitude: offer.latitude,
|
||||
longitude: offer.longitude
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -101,7 +161,7 @@ export function useCatalogInfo() {
|
||||
.catch(() => suppliersMap.get(supplierId)) // Fallback to basic info
|
||||
)
|
||||
).then(profiles => {
|
||||
relatedSuppliers.value = profiles.filter(Boolean)
|
||||
relatedSuppliers.value = profiles.filter((p): p is SupplierProfile => p != null)
|
||||
isLoadingSuppliers.value = false
|
||||
})
|
||||
} else {
|
||||
@@ -123,7 +183,7 @@ export function useCatalogInfo() {
|
||||
try {
|
||||
// Load supplier node details (might be geo node)
|
||||
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
|
||||
try {
|
||||
@@ -164,17 +224,18 @@ export function useCatalogInfo() {
|
||||
'geo'
|
||||
).then(offersData => {
|
||||
// Group offers by product
|
||||
const productsMap = new Map<string, any>()
|
||||
offersData?.nearestOffers?.forEach((offer: any) => {
|
||||
if (offer?.productUuid) {
|
||||
if (!productsMap.has(offer.productUuid)) {
|
||||
productsMap.set(offer.productUuid, {
|
||||
uuid: offer.productUuid,
|
||||
name: offer.productName,
|
||||
offersCount: 0
|
||||
})
|
||||
}
|
||||
productsMap.get(offer.productUuid)!.offersCount++
|
||||
const productsMap = new Map<string, ProductItem>()
|
||||
offersData?.nearestOffers?.forEach(offer => {
|
||||
if (!offer || !offer.productUuid || !offer.productName) return
|
||||
const existing = productsMap.get(offer.productUuid)
|
||||
if (existing) {
|
||||
existing.offersCount = (existing.offersCount || 0) + 1
|
||||
} else {
|
||||
productsMap.set(offer.productUuid, {
|
||||
uuid: offer.productUuid,
|
||||
name: offer.productName,
|
||||
offersCount: 1
|
||||
})
|
||||
}
|
||||
})
|
||||
relatedProducts.value = Array.from(productsMap.values())
|
||||
@@ -194,7 +255,7 @@ export function useCatalogInfo() {
|
||||
'public',
|
||||
'geo'
|
||||
).then(hubsData => {
|
||||
relatedHubs.value = hubsData?.nearestHubs || []
|
||||
relatedHubs.value = (hubsData?.nearestHubs || []).filter((h): h is HubItem => h !== null)
|
||||
}).finally(() => {
|
||||
isLoadingHubs.value = false
|
||||
})
|
||||
@@ -210,9 +271,10 @@ export function useCatalogInfo() {
|
||||
try {
|
||||
// Load offer details from exchange API
|
||||
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')
|
||||
return
|
||||
}
|
||||
@@ -235,15 +297,15 @@ export function useCatalogInfo() {
|
||||
execute(
|
||||
NearestHubsDocument,
|
||||
{
|
||||
lat: entity.value.latitude,
|
||||
lon: entity.value.longitude,
|
||||
lat: coords.lat,
|
||||
lon: coords.lon,
|
||||
radius: 1000,
|
||||
limit: 12
|
||||
},
|
||||
'public',
|
||||
'geo'
|
||||
).then(hubsData => {
|
||||
relatedHubs.value = hubsData?.nearestHubs || []
|
||||
relatedHubs.value = (hubsData?.nearestHubs || []).filter((h): h is HubItem => h !== null)
|
||||
}).finally(() => {
|
||||
isLoadingHubs.value = false
|
||||
})
|
||||
@@ -257,9 +319,12 @@ export function useCatalogInfo() {
|
||||
'public',
|
||||
'exchange'
|
||||
).then(supplierData => {
|
||||
relatedSuppliers.value = supplierData?.getSupplierProfile
|
||||
? [supplierData.getSupplierProfile]
|
||||
: []
|
||||
const supplier = 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(() => {
|
||||
// Supplier might not exist
|
||||
}).finally(() => {
|
||||
@@ -301,19 +366,19 @@ export function useCatalogInfo() {
|
||||
)
|
||||
|
||||
// Offers already include routes from backend
|
||||
relatedOffers.value = offersData?.nearestOffers || []
|
||||
relatedOffers.value = (offersData?.nearestOffers || []).filter((o): o is OfferItem => o !== null)
|
||||
isLoadingOffers.value = false
|
||||
|
||||
// Extract unique suppliers from offers (use supplierUuid from offers)
|
||||
const supplierUuids = new Set<string>()
|
||||
relatedOffers.value.forEach((offer: any) => {
|
||||
relatedOffers.value.forEach(offer => {
|
||||
if (offer.supplierUuid) {
|
||||
supplierUuids.add(offer.supplierUuid)
|
||||
}
|
||||
})
|
||||
|
||||
// Load supplier profiles (limit to 12)
|
||||
const suppliers: any[] = []
|
||||
const suppliers: SupplierProfile[] = []
|
||||
for (const uuid of Array.from(supplierUuids).slice(0, 12)) {
|
||||
try {
|
||||
const supplierData = await execute(
|
||||
@@ -366,11 +431,11 @@ export function useCatalogInfo() {
|
||||
'geo'
|
||||
)
|
||||
|
||||
relatedOffers.value = offersData?.nearestOffers || []
|
||||
relatedOffers.value = (offersData?.nearestOffers || []).filter((o): o is OfferItem => o !== null)
|
||||
isLoadingOffers.value = false
|
||||
|
||||
// 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)) {
|
||||
// Check first 3 offers
|
||||
if (!offer.latitude || !offer.longitude) continue
|
||||
@@ -387,8 +452,8 @@ export function useCatalogInfo() {
|
||||
'public',
|
||||
'geo'
|
||||
)
|
||||
hubsData?.nearestHubs?.forEach((hub: any) => {
|
||||
if (!allHubs.has(hub.uuid)) {
|
||||
hubsData?.nearestHubs?.forEach(hub => {
|
||||
if (hub && hub.uuid && !allHubs.has(hub.uuid)) {
|
||||
allHubs.set(hub.uuid, hub)
|
||||
}
|
||||
})
|
||||
@@ -415,10 +480,10 @@ export function useCatalogInfo() {
|
||||
if (!entity.value) return
|
||||
|
||||
// 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)
|
||||
activeTab.value = 'offers'
|
||||
} else if (entityType.value === 'supplier') {
|
||||
} else if (entityType.value === 'supplier' && entity.value.uuid) {
|
||||
await loadOffersForSupplier(entity.value.uuid, productUuid)
|
||||
activeTab.value = 'offers'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user