refactor(hub): replace sections with sources list and map
All checks were successful
Build Docker Image / build (push) Successful in 4m45s
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:
@@ -193,6 +193,16 @@ export type RouteType = {
|
|||||||
geometry?: Maybe<Scalars['JSONString']['output']>;
|
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<{
|
export type FindRoutesQueryVariables = Exact<{
|
||||||
fromUuid: Scalars['String']['input'];
|
fromUuid: Scalars['String']['input'];
|
||||||
toUuid: 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 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 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 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>;
|
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>;
|
||||||
|
|||||||
@@ -25,269 +25,209 @@
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Map Hero -->
|
<!-- Header with product filter -->
|
||||||
<MapHero
|
<Section variant="plain" paddingY="md">
|
||||||
:title="hub.name"
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
:location="mapLocation"
|
<div>
|
||||||
:badges="hubBadges"
|
<Heading :level="1">{{ hub.name }}</Heading>
|
||||||
/>
|
<Text tone="muted" size="sm">
|
||||||
|
{{ hub.country }}
|
||||||
<!-- Offers Section -->
|
<span v-if="hub.latitude && hub.longitude" class="ml-2">
|
||||||
<Section v-if="offers.length > 0" variant="plain" paddingY="md">
|
{{ hub.latitude.toFixed(2) }}°, {{ hub.longitude.toFixed(2) }}°
|
||||||
<Stack gap="4">
|
</span>
|
||||||
<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') }}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</div>
|
||||||
</Card>
|
<select
|
||||||
|
v-model="selectedProductUuid"
|
||||||
|
class="select select-bordered w-full sm:w-64"
|
||||||
|
>
|
||||||
|
<option value="">{{ t('catalogHub.sources.selectProduct') }}</option>
|
||||||
|
<option v-for="product in products" :key="product.uuid" :value="product.uuid">
|
||||||
|
{{ product.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<!-- Suppliers Section -->
|
<!-- Split: list + map -->
|
||||||
<Section v-if="uniqueSuppliers.length > 0" variant="plain" paddingY="md">
|
<Section variant="plain" paddingY="md">
|
||||||
<Stack gap="4">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
<Heading :level="2">{{ t('catalogHub.sections.suppliers.title') }}</Heading>
|
<!-- Sources list -->
|
||||||
<Grid :cols="1" :md="2" :lg="3" :gap="4">
|
<div class="order-2 lg:order-1">
|
||||||
<SupplierCard
|
<!-- Loading routes -->
|
||||||
v-for="supplier in uniqueSuppliers"
|
<div v-if="isLoadingRoutes" class="flex items-center justify-center h-64">
|
||||||
:key="supplier.teamUuid"
|
<Stack align="center" gap="2">
|
||||||
:supplier="supplier"
|
<Spinner />
|
||||||
/>
|
<Text tone="muted" size="sm">{{ t('catalogHub.sources.loading') }}</Text>
|
||||||
</Grid>
|
</Stack>
|
||||||
</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>
|
</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}`)"
|
|
||||||
>
|
|
||||||
<Pill variant="primary" class="hover:bg-primary hover:text-white transition-colors cursor-pointer">
|
|
||||||
{{ product.name }}
|
|
||||||
</Pill>
|
|
||||||
</NuxtLink>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<!-- Nearby Connections -->
|
|
||||||
<NearbyConnectionsSection
|
|
||||||
:auto-edges="autoEdges"
|
|
||||||
:rail-edges="railEdges"
|
|
||||||
:hub="currentHubForMap"
|
|
||||||
:rail-hub="railHubForMap"
|
|
||||||
:auto-route-geometries="autoRouteGeometries"
|
|
||||||
:rail-route-geometries="railRouteGeometries"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</Stack>
|
</Stack>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { GetLocationOffersDocument, GetSupplierProfilesDocument } from '~/composables/graphql/public/exchange-generated'
|
import { GetNodeConnectionsDocument } from '~/composables/graphql/public/geo-generated'
|
||||||
import { GetNodeConnectionsDocument, GetAutoRouteDocument, GetRailRouteDocument } from '~/composables/graphql/public/geo-generated'
|
import { GetAvailableProductsDocument } from '~/composables/graphql/public/exchange-generated'
|
||||||
import type { EdgeType } from '~/composables/graphql/public/geo-generated'
|
import { FindProductRoutesDocument, type ProductRouteOptionType, type RoutePathType } from '~/composables/graphql/public/geo-generated'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'topnav'
|
layout: 'topnav'
|
||||||
})
|
})
|
||||||
|
|
||||||
interface RouteGeometry {
|
|
||||||
toUuid: string
|
|
||||||
coordinates: [number, number][]
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const localePath = useLocalePath()
|
const localePath = useLocalePath()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { execute } = useGraphQL()
|
||||||
|
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
const isLoadingRoutes = ref(false)
|
||||||
const hub = ref<any>(null)
|
const hub = ref<any>(null)
|
||||||
const railHub = ref<any>(null)
|
const products = ref<Array<{ uuid: string; name: string }>>([])
|
||||||
const offers = ref<any[]>([])
|
const selectedProductUuid = ref('')
|
||||||
const allSuppliers = ref<any[]>([])
|
const sources = ref<ProductRouteOptionType[]>([])
|
||||||
const autoEdges = ref<EdgeType[]>([])
|
const selectedSourceUuid = ref<string | null>(null)
|
||||||
const railEdges = ref<EdgeType[]>([])
|
|
||||||
const autoRouteGeometries = ref<RouteGeometry[]>([])
|
|
||||||
const railRouteGeometries = ref<RouteGeometry[]>([])
|
|
||||||
|
|
||||||
const hubId = computed(() => route.params.id as string)
|
const hubId = computed(() => route.params.id as string)
|
||||||
|
|
||||||
// Map location
|
// Selected product name
|
||||||
const mapLocation = computed(() => ({
|
const selectedProductName = computed(() => {
|
||||||
uuid: hub.value?.uuid,
|
const product = products.value.find(p => p.uuid === selectedProductUuid.value)
|
||||||
name: hub.value?.name,
|
return product?.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
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Unique suppliers
|
// All routes for map (flatten sources -> routes)
|
||||||
const uniqueSuppliers = computed(() => {
|
const allRoutes = computed(() => {
|
||||||
const suppliers = new Map<string, { teamUuid: string; name: string; offersCount: number }>()
|
if (selectedSourceUuid.value) {
|
||||||
offers.value.forEach(offer => {
|
const source = sources.value.find(s => s.sourceUuid === selectedSourceUuid.value)
|
||||||
if (offer.teamUuid) {
|
return (source?.routes || []).filter((r): r is RoutePathType => r !== null)
|
||||||
const existing = suppliers.get(offer.teamUuid)
|
}
|
||||||
const supplierInfo = allSuppliers.value.find(s => s.teamUuid === offer.teamUuid)
|
return sources.value.flatMap(s => (s.routes || []).filter((r): r is RoutePathType => r !== null))
|
||||||
if (existing) {
|
|
||||||
existing.offersCount++
|
|
||||||
} else {
|
|
||||||
suppliers.set(offer.teamUuid, {
|
|
||||||
teamUuid: offer.teamUuid,
|
|
||||||
name: supplierInfo?.name || t('catalogHub.labels.default_supplier'),
|
|
||||||
offersCount: 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return Array.from(suppliers.values())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Unique products
|
const selectSource = (source: ProductRouteOptionType) => {
|
||||||
const uniqueProducts = computed(() => {
|
if (selectedSourceUuid.value === source.sourceUuid) {
|
||||||
const products = new Map<string, { uuid: string; name: string }>()
|
selectedSourceUuid.value = null
|
||||||
offers.value.forEach(offer => {
|
} else {
|
||||||
offer.lines?.forEach((line: any) => {
|
selectedSourceUuid.value = source.sourceUuid || null
|
||||||
if (line?.productUuid && line?.productName) {
|
}
|
||||||
products.set(line.productUuid, { uuid: line.productUuid, name: line.productName })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
|
|
||||||
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,
|
|
||||||
{
|
|
||||||
fromLat: hubLat,
|
|
||||||
fromLon: hubLon,
|
|
||||||
toLat: edge.toLatitude!,
|
|
||||||
toLon: edge.toLongitude!
|
|
||||||
},
|
|
||||||
'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][]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load ${transportType} route to ${edge.toName}:`, error)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const results = await Promise.all(routePromises)
|
|
||||||
return results.filter(Boolean) as RouteGeometry[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load routes when product changes
|
||||||
|
const loadRoutes = async () => {
|
||||||
|
if (!selectedProductUuid.value || !hubId.value) {
|
||||||
|
sources.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingRoutes.value = true
|
||||||
|
selectedSourceUuid.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await execute(
|
||||||
|
FindProductRoutesDocument,
|
||||||
|
{
|
||||||
|
productUuid: selectedProductUuid.value,
|
||||||
|
toUuid: hubId.value,
|
||||||
|
limitSources: 12,
|
||||||
|
limitRoutes: 1
|
||||||
|
},
|
||||||
|
'public',
|
||||||
|
'geo'
|
||||||
|
)
|
||||||
|
sources.value = (data?.findProductRoutes || []).filter((s): s is ProductRouteOptionType => s !== null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading routes:', error)
|
||||||
|
sources.value = []
|
||||||
|
} finally {
|
||||||
|
isLoadingRoutes.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
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-connections', GetNodeConnectionsDocument, { uuid: hubId.value }, 'public', 'geo'),
|
||||||
useServerQuery('hub-offers', GetLocationOffersDocument, { locationUuid: hubId.value }, 'public', 'exchange'),
|
useServerQuery('available-products', GetAvailableProductsDocument, {}, 'public', 'exchange')
|
||||||
useServerQuery('hub-suppliers', GetSupplierProfilesDocument, {}, 'public', 'exchange')
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const connectionsResult = connectionsData.value
|
hub.value = connectionsData.value?.nodeConnections?.hub || null
|
||||||
hub.value = connectionsResult?.nodeConnections?.hub || null
|
products.value = (productsData.value?.getAvailableProducts || [])
|
||||||
railHub.value = connectionsResult?.nodeConnections?.railNode || null
|
.filter((p): p is { uuid: string; name: string } => p !== null && !!p.uuid && !!p.name)
|
||||||
offers.value = offersData.value?.getOffers || []
|
.map(p => ({ uuid: p.uuid!, name: p.name! }))
|
||||||
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
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading hub:', error)
|
console.error('Error loading hub:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -305,8 +245,8 @@ useHead(() => ({
|
|||||||
content: t('catalogHub.meta.description', {
|
content: t('catalogHub.meta.description', {
|
||||||
name: hub.value?.name || '',
|
name: hub.value?.name || '',
|
||||||
country: hub.value?.country || '',
|
country: hub.value?.country || '',
|
||||||
offers: offers.value.length,
|
offers: sources.value.length,
|
||||||
suppliers: uniqueSuppliers.value.length
|
suppliers: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
31
graphql/operations/public/geo/FindProductRoutes.graphql
Normal file
31
graphql/operations/public/geo/FindProductRoutes.graphql
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,12 @@
|
|||||||
"byRoad": "By Road",
|
"byRoad": "By Road",
|
||||||
"byRail": "By Rail",
|
"byRail": "By Rail",
|
||||||
"empty": "No connections available"
|
"empty": "No connections available"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"title": "Available sources",
|
||||||
|
"selectProduct": "Select a product",
|
||||||
|
"empty": "No sources for this product",
|
||||||
|
"loading": "Loading routes..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,12 @@
|
|||||||
"byRoad": "По автодороге",
|
"byRoad": "По автодороге",
|
||||||
"byRail": "По железной дороге",
|
"byRail": "По железной дороге",
|
||||||
"empty": "Нет доступных маршрутов"
|
"empty": "Нет доступных маршрутов"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"title": "Откуда можно привезти",
|
||||||
|
"selectProduct": "Выберите товар",
|
||||||
|
"empty": "Нет источников для этого товара",
|
||||||
|
"loading": "Загрузка маршрутов..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user