166 lines
6.8 KiB
Vue
166 lines
6.8 KiB
Vue
<script setup lang="ts">
|
||
import {
|
||
GetHubCountriesDocument,
|
||
HubsListDocument,
|
||
type GetHubCountriesQueryVariables,
|
||
type HubsListQueryResult,
|
||
type HubsListQueryVariables,
|
||
} from '~/composables/graphql/public/geo-generated'
|
||
|
||
definePageMeta({
|
||
layout: 'manager',
|
||
middleware: ['auth-oidc'],
|
||
})
|
||
|
||
type HubRecord = HubsListQueryResult['hubsList'][number]
|
||
|
||
const { execute } = useGraphQL()
|
||
const search = ref('')
|
||
const selectedCountry = ref('')
|
||
|
||
const [{ data: countriesData }, { data: hubsData, pending, error }] = await Promise.all([
|
||
useAsyncData('manager-tariff-countries', async () => {
|
||
const response = await execute(GetHubCountriesDocument, {} as GetHubCountriesQueryVariables, 'public', 'geo')
|
||
return response.hubCountries || []
|
||
}),
|
||
useAsyncData('manager-tariff-hubs', async () => {
|
||
const response = await execute(HubsListDocument, {
|
||
limit: 60,
|
||
offset: 0,
|
||
country: null,
|
||
transportType: null,
|
||
west: null,
|
||
south: null,
|
||
east: null,
|
||
north: null,
|
||
} as HubsListQueryVariables, 'public', 'geo')
|
||
return response.hubsList || []
|
||
}),
|
||
])
|
||
|
||
const countries = computed(() => (countriesData.value || []).filter(Boolean))
|
||
const hubs = computed(() => (Array.isArray(hubsData.value) ? hubsData.value.filter((item): item is HubRecord => item !== null) : []))
|
||
const filteredHubs = computed(() => {
|
||
const query = search.value.trim().toLowerCase()
|
||
|
||
return hubs.value.filter((hub) => {
|
||
if (selectedCountry.value && hub.country !== selectedCountry.value) {
|
||
return false
|
||
}
|
||
|
||
if (!query) return true
|
||
|
||
return [hub.name, hub.country, hub.countryCode, ...(hub.transportTypes || [])]
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
.toLowerCase()
|
||
.includes(query)
|
||
})
|
||
})
|
||
|
||
const uniqueTransportTypes = computed(() => {
|
||
return new Set(
|
||
hubs.value.flatMap(hub => (hub.transportTypes || []).filter((item): item is string => Boolean(item))),
|
||
).size
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<section class="grid gap-3 lg:grid-cols-3">
|
||
<article class="rounded-[28px] bg-white p-5">
|
||
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Tariff anchors</p>
|
||
<p class="mt-3 text-3xl font-black text-[#2f2418]">{{ hubs.length }}</p>
|
||
<p class="mt-1 text-sm text-[#6f6353]">Хабы из geo graph для будущего tariff workspace</p>
|
||
</article>
|
||
<article class="rounded-[28px] bg-white p-5">
|
||
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Countries</p>
|
||
<p class="mt-3 text-3xl font-black text-[#2f2418]">{{ countries.length }}</p>
|
||
<p class="mt-1 text-sm text-[#6f6353]">Страны, уже доступные для corridor pricing</p>
|
||
</article>
|
||
<article class="rounded-[28px] bg-white p-5">
|
||
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Transport types</p>
|
||
<p class="mt-3 text-3xl font-black text-[#2f2418]">{{ uniqueTransportTypes }}</p>
|
||
<p class="mt-1 text-sm text-[#6f6353]">Основа для будущих tariff rules без Odoo</p>
|
||
</article>
|
||
</section>
|
||
|
||
<section class="mt-6 rounded-[28px] bg-white p-6">
|
||
<p class="text-xs font-bold uppercase tracking-[0.16em] text-[#8c7b67]">Tariff topology</p>
|
||
<h2 class="mt-2 text-3xl font-black text-[#2f2418]">Graph-native corridor base</h2>
|
||
<p class="mt-4 max-w-[840px] text-sm leading-6 text-[#5f4b33]">
|
||
Здесь я не делаю вид, что тарифный backend уже существует. Но сам экран уже опирается на реальные geo microservices и показывает,
|
||
от каких hub nodes дальше строить tariff references, quotation routes и decision rules.
|
||
</p>
|
||
|
||
<div class="mt-6 flex flex-col gap-3 lg:flex-row">
|
||
<label class="block flex-1">
|
||
<span class="sr-only">Search hubs</span>
|
||
<input
|
||
v-model="search"
|
||
class="input h-12 w-full rounded-full border-0 bg-[#f6f1ea] px-5 shadow-none"
|
||
placeholder="Поиск по хабу, стране или типу транспорта"
|
||
>
|
||
</label>
|
||
|
||
<select v-model="selectedCountry" class="select h-12 rounded-full border-0 bg-[#f6f1ea] px-5 shadow-none">
|
||
<option value="">Все страны</option>
|
||
<option v-for="country in countries" :key="country" :value="country">
|
||
{{ country }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="pending && !hubs.length" class="mt-6 rounded-[28px] bg-white p-8 text-center">
|
||
<p class="text-sm opacity-70">Загружаем graph hubs…</p>
|
||
</section>
|
||
|
||
<section v-else-if="error" class="mt-6 rounded-[28px] bg-rose-50/92 p-6 text-rose-700">
|
||
<p class="text-sm font-medium">Не удалось загрузить hub topology</p>
|
||
<p class="mt-2 text-sm opacity-80">{{ error.message }}</p>
|
||
</section>
|
||
|
||
<section v-else class="mt-6 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||
<article
|
||
v-for="hub in filteredHubs"
|
||
:key="hub.uuid"
|
||
class="rounded-[28px] bg-white p-5 shadow-none transition-[transform,box-shadow] hover:-translate-y-0.5 hover:shadow-[0_18px_34px_rgba(62,47,26,0.12)]"
|
||
>
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div>
|
||
<p class="text-xs font-bold uppercase tracking-[0.12em] text-[#8c7b67]">{{ hub.countryCode || 'Hub' }}</p>
|
||
<h3 class="mt-1 text-xl font-black leading-tight text-[#2f2418]">{{ hub.name || 'Unnamed hub' }}</h3>
|
||
<p class="mt-2 text-sm text-[#6f6353]">{{ hub.country || 'Country pending' }}</p>
|
||
</div>
|
||
<div class="rounded-full bg-[#f6f1ea] px-3 py-1 text-xs font-bold uppercase tracking-[0.12em] text-[#8a7761]">
|
||
{{ (hub.transportTypes || []).length }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-4 flex flex-wrap gap-2">
|
||
<span
|
||
v-for="transportType in hub.transportTypes || []"
|
||
:key="transportType || 'transport'"
|
||
class="rounded-full bg-[#fbf8f4] px-3 py-1 text-xs font-semibold text-[#5f4b33]"
|
||
>
|
||
{{ transportType }}
|
||
</span>
|
||
</div>
|
||
|
||
<p class="mt-4 text-sm leading-6 text-[#5f4b33]">
|
||
Этот хаб можно использовать как anchor point для новой tariff reference модели внутри Optovia manager workspace.
|
||
</p>
|
||
</article>
|
||
|
||
<article
|
||
v-if="filteredHubs.length === 0"
|
||
class="rounded-[28px] bg-white p-8 text-center md:col-span-2 xl:col-span-3"
|
||
>
|
||
<p class="text-lg font-semibold text-[#2f2418]">Под фильтры ничего не попало</p>
|
||
<p class="mt-2 text-sm text-[#6f6353]">Сбрось поиск или выбери другую страну.</p>
|
||
</article>
|
||
</section>
|
||
</div>
|
||
</template>
|