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 type OfferEntity = NonNullable type SupplierProfile = NonNullable type HubItem = NonNullable[number]> type OfferItem = NonNullable[number]> // Product type (aggregated from offers) export interface InfoProductItem { uuid: string name: string offersCount?: number } // Re-export types for InfoPanel export type InfoHubItem = HubItem export type InfoSupplierItem = SupplierProfile export type InfoOfferItem = OfferItem // Extended entity type with all known fields (NO index signature!) export interface InfoEntity { uuid?: string | null name?: string | null // Node coordinates latitude?: number | null longitude?: number | null // Location fields address?: string | null city?: string | null country?: string | null // Offer coordinates (different field names) locationLatitude?: number | null locationLongitude?: number | null locationUuid?: string | null locationName?: string | null // Offer fields productUuid?: string | null productName?: string | null teamUuid?: string | null teamName?: string | null pricePerUnit?: number | string | null currency?: string | null unit?: string | null // Enriched field from supplier profile supplierName?: string | null // KYC profile reference kycProfileUuid?: string | null } // 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 with proper types const entity = ref(null) const entityType = ref(null) const relatedProducts = ref([]) const relatedHubs = ref([]) const relatedSuppliers = ref([]) const relatedOffers = ref([]) const selectedProduct = ref(null) const activeTab = ref('products') const isLoading = ref(false) // Separate loading states for each tab const isLoadingProducts = ref(false) const isLoadingHubs = ref(false) const isLoadingSuppliers = ref(false) const isLoadingOffers = ref(false) // Load hub info: hub details + products + suppliers (in parallel) const loadHubInfo = async (uuid: string) => { try { // Load hub node details const nodeData = await execute(GetNodeDocument, { uuid }, 'public', 'geo') entity.value = nodeData?.node ?? null const coords = getEntityCoords(entity.value) if (!coords) { console.warn('Hub has no coordinates') return } // Set default active tab to offers (first step shows products) activeTab.value = 'offers' // Load products AND suppliers in parallel isLoadingProducts.value = true isLoadingSuppliers.value = true // Load products (offers grouped by product) and extract suppliers execute( NearestOffersDocument, { lat: coords.lat, lon: coords.lon, radius: 500 }, 'public', 'geo' ).then(offersData => { // Group offers by product const productsMap = new Map() const suppliersMap = new Map() offersData?.nearestOffers?.forEach(offer => { if (!offer) return // Products 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: 1 }) } } // Suppliers (extract from offers) if (offer.supplierUuid && !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((p): p is SupplierProfile => p != null) isLoadingSuppliers.value = false }) } else { relatedSuppliers.value = [] isLoadingSuppliers.value = false } }).finally(() => { isLoadingProducts.value = false }) } catch (error) { console.error('Error loading hub info:', error) isLoadingProducts.value = false isLoadingSuppliers.value = false } } // Load supplier info: supplier details + products + hubs (in parallel) const loadSupplierInfo = async (uuid: string) => { try { // Load supplier node details (might be geo node) const nodeData = await execute(GetNodeDocument, { uuid }, 'public', 'geo') entity.value = nodeData?.node ?? null // Also try to get supplier profile from exchange API for additional details try { const profileData = await execute( GetSupplierProfileDocument, { uuid }, 'public', 'exchange' ) if (profileData?.getSupplierProfile) { entity.value = { ...entity.value, ...profileData.getSupplierProfile } } } catch (e) { // Supplier profile might not exist, ignore } if (!entity.value?.latitude || !entity.value?.longitude) { console.warn('Supplier has no coordinates') return } // Set default active tab to offers (first step shows products) activeTab.value = 'offers' // Load products AND hubs in parallel isLoadingProducts.value = true isLoadingHubs.value = true // Load products (offers grouped by product) execute( NearestOffersDocument, { lat: entity.value.latitude, lon: entity.value.longitude, radius: 500 }, 'public', 'geo' ).then(offersData => { // Group offers by product const productsMap = new Map() 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()) }).finally(() => { isLoadingProducts.value = false }) // Load hubs near supplier execute( NearestHubsDocument, { lat: entity.value.latitude, lon: entity.value.longitude, radius: 1000, sourceUuid: entity.value.uuid, limit: 12 }, 'public', 'geo' ).then(hubsData => { relatedHubs.value = (hubsData?.nearestHubs || []).filter((h): h is HubItem => h !== null) }).finally(() => { isLoadingHubs.value = false }) } catch (error) { console.error('Error loading supplier info:', error) isLoadingProducts.value = false isLoadingHubs.value = false } } // Load offer info: offer details + supplier + hubs const loadOfferInfo = async (uuid: string) => { try { // Load offer details from exchange API const offerData = await execute(GetOfferDocument, { uuid }, 'public', 'exchange') entity.value = offerData?.getOffer ?? null const coords = getEntityCoords(entity.value) if (!coords) { console.warn('Offer has no coordinates') return } // Set default active tab to hubs activeTab.value = 'hubs' // Set product as "related product" (single item) if (entity.value?.productUuid && entity.value?.productName) { relatedProducts.value = [ { uuid: entity.value.productUuid, name: entity.value.productName } ] } // Load hubs near offer coordinates isLoadingHubs.value = true execute( NearestHubsDocument, { lat: coords.lat, lon: coords.lon, radius: 1000, sourceUuid: entity.value?.uuid ?? null, limit: 12 }, 'public', 'geo' ).then(hubsData => { relatedHubs.value = (hubsData?.nearestHubs || []).filter((h): h is HubItem => h !== null) }).finally(() => { isLoadingHubs.value = false }) // If offer has supplier UUID, load supplier profile if (entity.value?.teamUuid) { isLoadingSuppliers.value = true execute( GetSupplierProfileDocument, { uuid: entity.value.teamUuid }, 'public', 'exchange' ).then(supplierData => { 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(() => { isLoadingSuppliers.value = false }) } } catch (error) { console.error('Error loading offer info:', error) } } // Load offers for hub after product selection const loadOffersForHub = async (hubUuid: string, productUuid: string) => { try { const hub = entity.value if (!hub?.latitude || !hub?.longitude) { console.warn('Hub has no coordinates') return } // Load offers isLoadingOffers.value = true isLoadingSuppliers.value = true try { // Find offers near hub for this product WITH routes calculated on backend const offersData = await execute( NearestOffersDocument, { lat: hub.latitude, lon: hub.longitude, productUuid, hubUuid, // Pass hubUuid to get routes calculated on backend radius: 500, limit: 12 }, 'public', 'geo' ) // Offers already include routes from backend 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() relatedOffers.value.forEach(offer => { if (offer.supplierUuid) { supplierUuids.add(offer.supplierUuid) } }) // Load supplier profiles (limit to 12) const suppliers: SupplierProfile[] = [] for (const uuid of Array.from(supplierUuids).slice(0, 12)) { try { const supplierData = await execute( GetSupplierProfileDocument, { uuid }, 'public', 'exchange' ) if (supplierData?.getSupplierProfile) { suppliers.push(supplierData.getSupplierProfile) } } catch (e) { // Supplier might not exist } } relatedSuppliers.value = suppliers } finally { isLoadingOffers.value = false isLoadingSuppliers.value = false } } catch (error) { console.error('Error loading offers for hub:', error) } } // Load offers for supplier after product selection const loadOffersForSupplier = async (_supplierUuid: string, productUuid: string) => { try { const supplier = entity.value if (!supplier?.latitude || !supplier?.longitude) { console.warn('Supplier has no coordinates') return } isLoadingOffers.value = true isLoadingHubs.value = true try { // Find offers near supplier for this product const offersData = await execute( NearestOffersDocument, { lat: supplier.latitude, lon: supplier.longitude, productUuid, radius: 500, limit: 12 }, 'public', 'geo' ) 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() for (const offer of relatedOffers.value.slice(0, 3)) { // Check first 3 offers if (!offer.latitude || !offer.longitude) continue try { const hubsData = await execute( NearestHubsDocument, { lat: offer.latitude, lon: offer.longitude, radius: 1000, limit: 5 }, 'public', 'geo' ) hubsData?.nearestHubs?.forEach(hub => { if (hub && hub.uuid && !allHubs.has(hub.uuid)) { allHubs.set(hub.uuid, hub) } }) } catch (e) { console.warn('Error loading hubs for offer:', offer.uuid, e) } if (allHubs.size >= 12) break } relatedHubs.value = Array.from(allHubs.values()).slice(0, 12) } finally { isLoadingOffers.value = false isLoadingHubs.value = false } } catch (error) { console.error('Error loading offers for supplier:', error) } } // Select product (triggers offers loading) const selectProduct = async (productUuid: string) => { selectedProduct.value = productUuid if (!entity.value) return // Use stored entity type instead of inferring from properties if (entityType.value === 'hub' && entity.value.uuid) { await loadOffersForHub(entity.value.uuid, productUuid) activeTab.value = 'offers' } else if (entityType.value === 'supplier' && entity.value.uuid) { await loadOffersForSupplier(entity.value.uuid, productUuid) activeTab.value = 'offers' } } // Set active tab const setActiveTab = (tab: string) => { activeTab.value = tab } // Main load method - dispatches to specific loaders const loadInfo = async (type: InfoEntityType, uuid: string) => { isLoading.value = true clearInfo() // Clear previous data entityType.value = type // Store entity type try { if (type === 'hub') { await loadHubInfo(uuid) } else if (type === 'supplier') { await loadSupplierInfo(uuid) } else if (type === 'offer') { await loadOfferInfo(uuid) } } finally { isLoading.value = false } } // Clear all info data const clearInfo = () => { entity.value = null entityType.value = null relatedProducts.value = [] relatedHubs.value = [] relatedSuppliers.value = [] relatedOffers.value = [] selectedProduct.value = null activeTab.value = 'products' isLoadingProducts.value = false isLoadingHubs.value = false isLoadingSuppliers.value = false isLoadingOffers.value = false } return { // State entity, relatedProducts, relatedHubs, relatedSuppliers, relatedOffers, selectedProduct, activeTab, isLoading, isLoadingProducts, isLoadingHubs, isLoadingSuppliers, isLoadingOffers, // Actions loadInfo, selectProduct, setActiveTab, clearInfo } }