557 lines
17 KiB
TypeScript
557 lines
17 KiB
TypeScript
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,
|
|
GetSupplierOffersDocument
|
|
} 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)
|
|
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<InfoEntity | null>(null)
|
|
const entityType = ref<InfoEntityType | null>(null)
|
|
const relatedProducts = ref<InfoProductItem[]>([])
|
|
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)
|
|
|
|
// 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,
|
|
hubUuid: uuid,
|
|
limit: 500
|
|
},
|
|
'public',
|
|
'geo'
|
|
).then(offersData => {
|
|
// Group offers by product
|
|
const productsMap = new Map<string, InfoProductItem>()
|
|
const suppliersMap = new Map<string, { uuid: string; name: string; latitude?: number | null; longitude?: number | null }>()
|
|
|
|
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 from supplier offers (no geo radius)
|
|
execute(
|
|
GetSupplierOffersDocument,
|
|
{ teamUuid: uuid },
|
|
'public',
|
|
'exchange'
|
|
).then(offersData => {
|
|
const productsMap = new Map<string, InfoProductItem>()
|
|
offersData?.getOffers?.forEach(offer => {
|
|
if (!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,
|
|
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,
|
|
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
|
|
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<string>()
|
|
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 {
|
|
let hubUuid: string | null = relatedHubs.value?.[0]?.uuid ?? null
|
|
if (!hubUuid && supplier.uuid) {
|
|
const hubsData = await execute(
|
|
NearestHubsDocument,
|
|
{
|
|
lat: supplier.latitude,
|
|
lon: supplier.longitude,
|
|
sourceUuid: supplier.uuid,
|
|
limit: 1
|
|
},
|
|
'public',
|
|
'geo'
|
|
)
|
|
const hub = (hubsData?.nearestHubs || []).find((h): h is HubItem => h !== null)
|
|
if (hub?.uuid) {
|
|
hubUuid = hub.uuid
|
|
if (!relatedHubs.value.length) {
|
|
relatedHubs.value = [hub]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find offers near supplier for this product
|
|
const offersData = await execute(
|
|
NearestOffersDocument,
|
|
{
|
|
lat: supplier.latitude,
|
|
lon: supplier.longitude,
|
|
productUuid,
|
|
...(hubUuid ? { hubUuid } : {}),
|
|
limit: 12
|
|
},
|
|
'public',
|
|
'geo'
|
|
)
|
|
|
|
relatedOffers.value = (offersData?.nearestOffers || []).filter((o): o is OfferItem => {
|
|
if (!o) return false
|
|
if (!supplier.uuid) return true
|
|
return o.supplierUuid === supplier.uuid
|
|
})
|
|
isLoadingOffers.value = false
|
|
} 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
|
|
}
|
|
}
|