Files
webapp/app/pages/manager/tariffs/index.vue
2026-04-11 08:31:34 +07:00

166 lines
6.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>