refactor(hub): replace sections with sources list and map
All checks were successful
Build Docker Image / build (push) Successful in 4m45s

- Remove Offers, Suppliers, Products, NearbyConnections sections
- Add product filter dropdown
- Show sources list with routes from findProductRoutes API
- Display routes on map using RequestRoutesMap component
- Add i18n keys for sources section (en/ru)
This commit is contained in:
Ruslan Bakiev
2026-01-09 01:08:56 +07:00
parent 01e8eb2cfa
commit cd2c8afff1
5 changed files with 226 additions and 232 deletions

View File

@@ -193,6 +193,16 @@ export type RouteType = {
geometry?: Maybe<Scalars['JSONString']['output']>;
};
export type FindProductRoutesQueryVariables = Exact<{
productUuid: Scalars['String']['input'];
toUuid: Scalars['String']['input'];
limitSources?: InputMaybe<Scalars['Int']['input']>;
limitRoutes?: InputMaybe<Scalars['Int']['input']>;
}>;
export type FindProductRoutesQuery = { __typename?: 'Query', findProductRoutes?: Array<{ __typename?: 'ProductRouteOptionType', sourceUuid?: string | null, sourceName?: string | null, sourceLat?: number | null, sourceLon?: number | null, distanceKm?: number | null, routes?: Array<{ __typename?: 'RoutePathType', totalDistanceKm?: number | null, totalTimeSeconds?: number | null, stages?: Array<{ __typename?: 'RouteStageType', fromUuid?: string | null, fromName?: string | null, fromLat?: number | null, fromLon?: number | null, toUuid?: string | null, toName?: string | null, toLat?: number | null, toLon?: number | null, distanceKm?: number | null, travelTimeSeconds?: number | null, transportType?: string | null } | null> | null } | null> | null } | null> | null };
export type FindRoutesQueryVariables = Exact<{
fromUuid: Scalars['String']['input'];
toUuid: Scalars['String']['input'];
@@ -254,6 +264,7 @@ export type GetRailRouteQueryVariables = Exact<{
export type GetRailRouteQuery = { __typename?: 'Query', railRoute?: { __typename?: 'RouteType', distanceKm?: number | null, geometry?: any | null } | null };
export const FindProductRoutesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindProductRoutes"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"productUuid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"toUuid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limitSources"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limitRoutes"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findProductRoutes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"productUuid"},"value":{"kind":"Variable","name":{"kind":"Name","value":"productUuid"}}},{"kind":"Argument","name":{"kind":"Name","value":"toUuid"},"value":{"kind":"Variable","name":{"kind":"Name","value":"toUuid"}}},{"kind":"Argument","name":{"kind":"Name","value":"limitSources"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limitSources"}}},{"kind":"Argument","name":{"kind":"Name","value":"limitRoutes"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limitRoutes"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sourceUuid"}},{"kind":"Field","name":{"kind":"Name","value":"sourceName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceLat"}},{"kind":"Field","name":{"kind":"Name","value":"sourceLon"}},{"kind":"Field","name":{"kind":"Name","value":"distanceKm"}},{"kind":"Field","name":{"kind":"Name","value":"routes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalDistanceKm"}},{"kind":"Field","name":{"kind":"Name","value":"totalTimeSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"stages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fromUuid"}},{"kind":"Field","name":{"kind":"Name","value":"fromName"}},{"kind":"Field","name":{"kind":"Name","value":"fromLat"}},{"kind":"Field","name":{"kind":"Name","value":"fromLon"}},{"kind":"Field","name":{"kind":"Name","value":"toUuid"}},{"kind":"Field","name":{"kind":"Name","value":"toName"}},{"kind":"Field","name":{"kind":"Name","value":"toLat"}},{"kind":"Field","name":{"kind":"Name","value":"toLon"}},{"kind":"Field","name":{"kind":"Name","value":"distanceKm"}},{"kind":"Field","name":{"kind":"Name","value":"travelTimeSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"transportType"}}]}}]}}]}}]}}]} as unknown as DocumentNode<FindProductRoutesQuery, FindProductRoutesQueryVariables>;
export const FindRoutesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindRoutes"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fromUuid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"toUuid"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findRoutes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"fromUuid"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fromUuid"}}},{"kind":"Argument","name":{"kind":"Name","value":"toUuid"},"value":{"kind":"Variable","name":{"kind":"Name","value":"toUuid"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalDistanceKm"}},{"kind":"Field","name":{"kind":"Name","value":"totalTimeSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"stages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fromUuid"}},{"kind":"Field","name":{"kind":"Name","value":"fromName"}},{"kind":"Field","name":{"kind":"Name","value":"fromLat"}},{"kind":"Field","name":{"kind":"Name","value":"fromLon"}},{"kind":"Field","name":{"kind":"Name","value":"toUuid"}},{"kind":"Field","name":{"kind":"Name","value":"toName"}},{"kind":"Field","name":{"kind":"Name","value":"toLat"}},{"kind":"Field","name":{"kind":"Name","value":"toLon"}},{"kind":"Field","name":{"kind":"Name","value":"distanceKm"}},{"kind":"Field","name":{"kind":"Name","value":"travelTimeSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"transportType"}}]}}]}}]}}]} as unknown as DocumentNode<FindRoutesQuery, FindRoutesQueryVariables>;
export const GetAutoRouteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAutoRoute"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fromLat"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fromLon"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"toLat"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"toLon"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"autoRoute"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"fromLat"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fromLat"}}},{"kind":"Argument","name":{"kind":"Name","value":"fromLon"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fromLon"}}},{"kind":"Argument","name":{"kind":"Name","value":"toLat"},"value":{"kind":"Variable","name":{"kind":"Name","value":"toLat"}}},{"kind":"Argument","name":{"kind":"Name","value":"toLon"},"value":{"kind":"Variable","name":{"kind":"Name","value":"toLon"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"distanceKm"}},{"kind":"Field","name":{"kind":"Name","value":"geometry"}}]}}]}}]} as unknown as DocumentNode<GetAutoRouteQuery, GetAutoRouteQueryVariables>;
export const GetHubCountriesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetHubCountries"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hubCountries"}}]}}]} as unknown as DocumentNode<GetHubCountriesQuery, GetHubCountriesQueryVariables>;

View File

@@ -25,269 +25,209 @@
</Section>
<template v-else>
<!-- Map Hero -->
<MapHero
:title="hub.name"
:location="mapLocation"
:badges="hubBadges"
/>
<!-- Offers Section -->
<Section v-if="offers.length > 0" variant="plain" paddingY="md">
<Stack gap="4">
<Heading :level="2">{{ t('catalogHub.sections.offers.title') }}</Heading>
<Grid :cols="1" :md="2" :lg="3" :gap="4">
<OfferCard
v-for="offer in offers"
:key="offer.uuid"
:offer="offer"
/>
</Grid>
</Stack>
</Section>
<!-- Empty offers state -->
<Section v-else variant="plain" paddingY="md">
<Card padding="lg">
<Stack align="center" gap="4">
<IconCircle tone="primary">
<Icon name="lucide:package-x" size="24" />
</IconCircle>
<Heading :level="3">{{ t('catalogHub.empty.offers.title') }}</Heading>
<Text tone="muted" align="center">
{{ t('catalogHub.empty.offers.subtitle') }}
<!-- Header with product filter -->
<Section variant="plain" paddingY="md">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<Heading :level="1">{{ hub.name }}</Heading>
<Text tone="muted" size="sm">
{{ hub.country }}
<span v-if="hub.latitude && hub.longitude" class="ml-2">
{{ hub.latitude.toFixed(2) }}°, {{ hub.longitude.toFixed(2) }}°
</span>
</Text>
</Stack>
</Card>
</Section>
<!-- Suppliers Section -->
<Section v-if="uniqueSuppliers.length > 0" variant="plain" paddingY="md">
<Stack gap="4">
<Heading :level="2">{{ t('catalogHub.sections.suppliers.title') }}</Heading>
<Grid :cols="1" :md="2" :lg="3" :gap="4">
<SupplierCard
v-for="supplier in uniqueSuppliers"
:key="supplier.teamUuid"
:supplier="supplier"
/>
</Grid>
</Stack>
</Section>
<!-- Products Section -->
<Section v-if="uniqueProducts.length > 0" variant="plain" paddingY="md">
<Stack gap="4">
<Heading :level="2">{{ t('catalogHub.sections.products.title') }}</Heading>
<Stack direction="row" gap="2" wrap>
<NuxtLink
v-for="product in uniqueProducts"
:key="product.uuid"
:to="localePath(`/catalog/products/${product.uuid}`)"
</div>
<select
v-model="selectedProductUuid"
class="select select-bordered w-full sm:w-64"
>
<Pill variant="primary" class="hover:bg-primary hover:text-white transition-colors cursor-pointer">
<option value="">{{ t('catalogHub.sources.selectProduct') }}</option>
<option v-for="product in products" :key="product.uuid" :value="product.uuid">
{{ product.name }}
</Pill>
</NuxtLink>
</Stack>
</Stack>
</option>
</select>
</div>
</Section>
<!-- Nearby Connections -->
<NearbyConnectionsSection
:auto-edges="autoEdges"
:rail-edges="railEdges"
:hub="currentHubForMap"
:rail-hub="railHubForMap"
:auto-route-geometries="autoRouteGeometries"
:rail-route-geometries="railRouteGeometries"
/>
<!-- Split: list + map -->
<Section variant="plain" paddingY="md">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Sources list -->
<div class="order-2 lg:order-1">
<!-- Loading routes -->
<div v-if="isLoadingRoutes" class="flex items-center justify-center h-64">
<Stack align="center" gap="2">
<Spinner />
<Text tone="muted" size="sm">{{ t('catalogHub.sources.loading') }}</Text>
</Stack>
</div>
<!-- No product selected -->
<div v-else-if="!selectedProductUuid" class="flex items-center justify-center h-64">
<Text tone="muted">{{ t('catalogHub.sources.selectProduct') }}</Text>
</div>
<!-- Sources list -->
<div v-else-if="sources.length > 0" class="space-y-3 max-h-[600px] overflow-y-auto pr-2">
<Card
v-for="source in sources"
:key="source.sourceUuid"
padding="sm"
interactive
class="cursor-pointer"
@click="selectSource(source)"
:class="{ 'ring-2 ring-primary': selectedSourceUuid === source.sourceUuid }"
>
<div class="flex items-center justify-between">
<div>
<Text weight="semibold">{{ source.sourceName }}</Text>
<Text tone="muted" size="sm">
{{ selectedProductName }}
</Text>
</div>
<div class="text-right">
<Text weight="semibold" class="text-primary">
{{ formatDistance(source.distanceKm) }} км
</Text>
<Text tone="muted" size="sm">
{{ formatDuration(source.routes?.[0]?.totalTimeSeconds) }}
</Text>
</div>
</div>
</Card>
</div>
<!-- Empty state -->
<div v-else class="flex items-center justify-center h-64">
<Stack align="center" gap="2">
<Icon name="lucide:package-x" size="32" class="text-base-content/40" />
<Text tone="muted">{{ t('catalogHub.sources.empty') }}</Text>
</Stack>
</div>
</div>
<!-- Map -->
<div class="order-1 lg:order-2">
<RequestRoutesMap :routes="allRoutes" :height="600" />
</div>
</div>
</Section>
</template>
</Stack>
</template>
<script setup lang="ts">
import { GetLocationOffersDocument, GetSupplierProfilesDocument } from '~/composables/graphql/public/exchange-generated'
import { GetNodeConnectionsDocument, GetAutoRouteDocument, GetRailRouteDocument } from '~/composables/graphql/public/geo-generated'
import type { EdgeType } from '~/composables/graphql/public/geo-generated'
import { GetNodeConnectionsDocument } from '~/composables/graphql/public/geo-generated'
import { GetAvailableProductsDocument } from '~/composables/graphql/public/exchange-generated'
import { FindProductRoutesDocument, type ProductRouteOptionType, type RoutePathType } from '~/composables/graphql/public/geo-generated'
definePageMeta({
layout: 'topnav'
})
interface RouteGeometry {
toUuid: string
coordinates: [number, number][]
}
const route = useRoute()
const localePath = useLocalePath()
const { t } = useI18n()
const { execute } = useGraphQL()
const isLoading = ref(true)
const isLoadingRoutes = ref(false)
const hub = ref<any>(null)
const railHub = ref<any>(null)
const offers = ref<any[]>([])
const allSuppliers = ref<any[]>([])
const autoEdges = ref<EdgeType[]>([])
const railEdges = ref<EdgeType[]>([])
const autoRouteGeometries = ref<RouteGeometry[]>([])
const railRouteGeometries = ref<RouteGeometry[]>([])
const products = ref<Array<{ uuid: string; name: string }>>([])
const selectedProductUuid = ref('')
const sources = ref<ProductRouteOptionType[]>([])
const selectedSourceUuid = ref<string | null>(null)
const hubId = computed(() => route.params.id as string)
// Map location
const mapLocation = computed(() => ({
uuid: hub.value?.uuid,
name: hub.value?.name,
latitude: hub.value?.latitude,
longitude: hub.value?.longitude,
country: hub.value?.country,
countryCode: hub.value?.countryCode
}))
// Badges for MapHero
const hubBadges = computed(() => {
const badges: Array<{ icon?: string; text: string }> = []
if (hub.value?.country) {
badges.push({ icon: 'lucide:globe', text: hub.value.country })
}
if (offers.value.length > 0) {
badges.push({ icon: 'lucide:package', text: t('catalogHub.badges.offers', { count: offers.value.length }) })
}
if (hub.value?.latitude && hub.value?.longitude) {
badges.push({ icon: 'lucide:map-pin', text: `${hub.value.latitude.toFixed(2)}°, ${hub.value.longitude.toFixed(2)}°` })
}
return badges
// Selected product name
const selectedProductName = computed(() => {
const product = products.value.find(p => p.uuid === selectedProductUuid.value)
return product?.name || ''
})
// Unique suppliers
const uniqueSuppliers = computed(() => {
const suppliers = new Map<string, { teamUuid: string; name: string; offersCount: number }>()
offers.value.forEach(offer => {
if (offer.teamUuid) {
const existing = suppliers.get(offer.teamUuid)
const supplierInfo = allSuppliers.value.find(s => s.teamUuid === offer.teamUuid)
if (existing) {
existing.offersCount++
// All routes for map (flatten sources -> routes)
const allRoutes = computed(() => {
if (selectedSourceUuid.value) {
const source = sources.value.find(s => s.sourceUuid === selectedSourceUuid.value)
return (source?.routes || []).filter((r): r is RoutePathType => r !== null)
}
return sources.value.flatMap(s => (s.routes || []).filter((r): r is RoutePathType => r !== null))
})
const selectSource = (source: ProductRouteOptionType) => {
if (selectedSourceUuid.value === source.sourceUuid) {
selectedSourceUuid.value = null
} else {
suppliers.set(offer.teamUuid, {
teamUuid: offer.teamUuid,
name: supplierInfo?.name || t('catalogHub.labels.default_supplier'),
offersCount: 1
})
selectedSourceUuid.value = source.sourceUuid || null
}
}
})
return Array.from(suppliers.values())
})
// Unique products
const uniqueProducts = computed(() => {
const products = new Map<string, { uuid: string; name: string }>()
offers.value.forEach(offer => {
offer.lines?.forEach((line: any) => {
if (line?.productUuid && line?.productName) {
products.set(line.productUuid, { uuid: line.productUuid, name: line.productName })
// Load routes when product changes
const loadRoutes = async () => {
if (!selectedProductUuid.value || !hubId.value) {
sources.value = []
return
}
})
})
return Array.from(products.values())
})
// Current hub for NearbyConnectionsSection
const currentHubForMap = computed(() => ({
uuid: hub.value?.uuid || '',
name: hub.value?.name || '',
latitude: hub.value?.latitude || 0,
longitude: hub.value?.longitude || 0
}))
isLoadingRoutes.value = true
selectedSourceUuid.value = null
const railHubForMap = computed(() => ({
uuid: railHub.value?.uuid || hub.value?.uuid || '',
name: railHub.value?.name || hub.value?.name || '',
latitude: railHub.value?.latitude || hub.value?.latitude || 0,
longitude: railHub.value?.longitude || hub.value?.longitude || 0
}))
// Load route geometries for edges
const loadRouteGeometries = async (
edges: EdgeType[],
hubLat: number,
hubLon: number,
transportType: 'auto' | 'rail'
): Promise<RouteGeometry[]> => {
const RouteDocument = transportType === 'auto' ? GetAutoRouteDocument : GetRailRouteDocument
const routeField = transportType === 'auto' ? 'autoRoute' : 'railRoute'
const filteredEdges = edges
.filter(e => e?.transportType === transportType && e?.toLatitude && e?.toLongitude)
.sort((a, b) => (a.distanceKm || 0) - (b.distanceKm || 0))
.slice(0, 12)
const routePromises = filteredEdges.map(async (edge) => {
try {
const { data: routeDataResponse } = await useServerQuery(
`hub-route-${transportType}-${edge.toUuid}`,
RouteDocument,
const data = await execute(
FindProductRoutesDocument,
{
fromLat: hubLat,
fromLon: hubLon,
toLat: edge.toLatitude!,
toLon: edge.toLongitude!
productUuid: selectedProductUuid.value,
toUuid: hubId.value,
limitSources: 12,
limitRoutes: 1
},
'public',
'geo'
)
const routeData = routeDataResponse.value?.[routeField]
if (routeData?.geometry) {
const geometryArray = typeof routeData.geometry === 'string'
? JSON.parse(routeData.geometry)
: routeData.geometry
if (Array.isArray(geometryArray) && geometryArray.length > 0) {
return {
toUuid: edge.toUuid!,
coordinates: geometryArray as [number, number][]
}
}
}
sources.value = (data?.findProductRoutes || []).filter((s): s is ProductRouteOptionType => s !== null)
} catch (error) {
console.error(`Failed to load ${transportType} route to ${edge.toName}:`, error)
console.error('Error loading routes:', error)
sources.value = []
} finally {
isLoadingRoutes.value = false
}
return null
})
const results = await Promise.all(routePromises)
return results.filter(Boolean) as RouteGeometry[]
}
watch(selectedProductUuid, loadRoutes)
// Formatting helpers
const formatDistance = (km: number | null | undefined) => {
if (!km) return '0'
return Math.round(km).toLocaleString()
}
const formatDuration = (seconds: number | null | undefined) => {
if (!seconds) return '-'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 24) {
const days = Math.floor(hours / 24)
const remainingHours = hours % 24
return `${days}д ${remainingHours}ч`
}
if (hours > 0) {
return `${hours}ч ${minutes}м`
}
return `${minutes}м`
}
// Initial load
try {
const [{ data: connectionsData }, { data: offersData }, { data: suppliersData }] = await Promise.all([
const [{ data: connectionsData }, { data: productsData }] = await Promise.all([
useServerQuery('hub-connections', GetNodeConnectionsDocument, { uuid: hubId.value }, 'public', 'geo'),
useServerQuery('hub-offers', GetLocationOffersDocument, { locationUuid: hubId.value }, 'public', 'exchange'),
useServerQuery('hub-suppliers', GetSupplierProfilesDocument, {}, 'public', 'exchange')
useServerQuery('available-products', GetAvailableProductsDocument, {}, 'public', 'exchange')
])
const connectionsResult = connectionsData.value
hub.value = connectionsResult?.nodeConnections?.hub || null
railHub.value = connectionsResult?.nodeConnections?.railNode || null
offers.value = offersData.value?.getOffers || []
allSuppliers.value = suppliersData.value?.getSupplierProfiles || []
autoEdges.value = (connectionsResult?.nodeConnections?.autoEdges || []).filter((e): e is EdgeType => e !== null)
railEdges.value = (connectionsResult?.nodeConnections?.railEdges || []).filter((e): e is EdgeType => e !== null)
if (hub.value?.latitude && hub.value?.longitude) {
const railOrigin = railHub.value || hub.value
const [autoGeometries, railGeometries] = await Promise.all([
loadRouteGeometries(autoEdges.value, hub.value.latitude, hub.value.longitude, 'auto'),
loadRouteGeometries(railEdges.value, railOrigin.latitude, railOrigin.longitude, 'rail')
])
autoRouteGeometries.value = autoGeometries
railRouteGeometries.value = railGeometries
}
hub.value = connectionsData.value?.nodeConnections?.hub || null
products.value = (productsData.value?.getAvailableProducts || [])
.filter((p): p is { uuid: string; name: string } => p !== null && !!p.uuid && !!p.name)
.map(p => ({ uuid: p.uuid!, name: p.name! }))
} catch (error) {
console.error('Error loading hub:', error)
} finally {
@@ -305,8 +245,8 @@ useHead(() => ({
content: t('catalogHub.meta.description', {
name: hub.value?.name || '',
country: hub.value?.country || '',
offers: offers.value.length,
suppliers: uniqueSuppliers.value.length
offers: sources.value.length,
suppliers: 0
})
}
]

View File

@@ -0,0 +1,31 @@
query FindProductRoutes($productUuid: String!, $toUuid: String!, $limitSources: Int, $limitRoutes: Int) {
findProductRoutes(
productUuid: $productUuid
toUuid: $toUuid
limitSources: $limitSources
limitRoutes: $limitRoutes
) {
sourceUuid
sourceName
sourceLat
sourceLon
distanceKm
routes {
totalDistanceKm
totalTimeSeconds
stages {
fromUuid
fromName
fromLat
fromLon
toUuid
toName
toLat
toLon
distanceKm
travelTimeSeconds
transportType
}
}
}
}

View File

@@ -42,6 +42,12 @@
"byRoad": "By Road",
"byRail": "By Rail",
"empty": "No connections available"
},
"sources": {
"title": "Available sources",
"selectProduct": "Select a product",
"empty": "No sources for this product",
"loading": "Loading routes..."
}
}
}

View File

@@ -42,6 +42,12 @@
"byRoad": "По автодороге",
"byRail": "По железной дороге",
"empty": "Нет доступных маршрутов"
},
"sources": {
"title": "Откуда можно привезти",
"selectProduct": "Выберите товар",
"empty": "Нет источников для этого товара",
"loading": "Загрузка маршрутов..."
}
}
}