UI improvements: filters, map layout, search bar
Some checks failed
Build Docker Image / build (push) Failing after 1m29s

- Add hubCountries query and country filter for hubs page
- Add getAvailableProducts query for offers (only products with active offers)
- Add sourceLatitude/sourceLongitude to orders GraphQL
- Fix ListMapLayout with position fixed for proper map height
- GlobalSearchBar: make fields wider, remove unit selector
- Remove status/isVerified filters from suppliers/offers (backend handles this)
This commit is contained in:
Ruslan Bakiev
2026-01-08 10:42:59 +07:00
parent 0c88cf383c
commit d6865d2129
15 changed files with 87 additions and 78 deletions

View File

@@ -1,23 +1,25 @@
<template>
<div class="flex flex-col flex-1 min-h-0">
<!-- Desktop: side-by-side layout -->
<div class="hidden lg:flex flex-1 gap-4 min-h-0 overflow-hidden">
<div class="hidden lg:flex flex-1 gap-4 min-h-0">
<!-- Left side: List (scrollable) -->
<div class="w-2/5 overflow-y-auto pr-2">
<slot name="list" />
</div>
<!-- Right side: Map (sticky, full height from SubNav to bottom) -->
<div class="w-3/5 h-full rounded-lg overflow-hidden sticky top-0 self-start">
<ClientOnly>
<CatalogMap
ref="mapRef"
:map-id="mapId"
:items="itemsWithCoords"
:point-color="pointColor"
@select-item="onMapSelectItem"
/>
</ClientOnly>
<!-- Right side: Map (fixed position, full height) -->
<div class="w-3/5 relative">
<div class="fixed top-28 right-6 w-[calc(60%-3rem)] h-[calc(100vh-8rem)] rounded-lg overflow-hidden">
<ClientOnly>
<CatalogMap
ref="mapRef"
:map-id="mapId"
:items="itemsWithCoords"
:point-color="pointColor"
@select-item="onMapSelectItem"
/>
</ClientOnly>
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@
>
<!-- Product field (clickable, navigates to /goods) -->
<div
class="flex flex-col px-4 py-2 min-w-32 pl-6 rounded-l-full hover:bg-base-200/50 border-r border-base-300 cursor-pointer"
class="flex flex-col px-4 py-2 min-w-48 pl-6 rounded-l-full hover:bg-base-200/50 border-r border-base-300 cursor-pointer"
@click="goToProductSelection"
>
<label class="text-xs font-semibold text-base-content/60 mb-0.5">
@@ -19,29 +19,23 @@
</div>
<!-- Quantity field (editable) -->
<div class="flex flex-col px-4 py-2 min-w-32 hover:bg-base-200/50 border-r border-base-300">
<div class="flex flex-col px-4 py-2 min-w-48 hover:bg-base-200/50 border-r border-base-300">
<label class="text-xs font-semibold text-base-content/60 mb-0.5">
{{ $t('search.quantity') }}
</label>
<div class="flex items-center gap-1">
<input
v-model="quantity"
type="number"
min="1"
:placeholder="$t('search.quantity_placeholder')"
class="w-16 bg-transparent outline-none text-sm"
@change="syncQuantityToStore"
/>
<select v-model="unit" class="bg-transparent outline-none text-sm text-base-content/70" @change="syncQuantityToStore">
<option value="t">{{ $t('units.t') }}</option>
<option value="kg">{{ $t('units.kg') }}</option>
</select>
</div>
<input
v-model="quantity"
type="number"
min="1"
:placeholder="$t('search.quantity_placeholder')"
class="w-full bg-transparent outline-none text-sm"
@change="syncQuantityToStore"
/>
</div>
<!-- Destination field (clickable, navigates to /select-location) -->
<div
class="flex flex-col px-4 py-2 min-w-32 hover:bg-base-200/50 cursor-pointer"
class="flex flex-col px-4 py-2 min-w-48 hover:bg-base-200/50 cursor-pointer"
@click="goToLocationSelection"
>
<label class="text-xs font-semibold text-base-content/60 mb-0.5">
@@ -67,7 +61,7 @@
<script setup lang="ts">
const emit = defineEmits<{
search: [params: { productUuid?: string; quantity?: number; unit?: string; locationUuid?: string }]
search: [params: { productUuid?: string; quantity?: number; locationUuid?: string }]
}>()
const router = useRouter()
@@ -84,13 +78,11 @@ const locationUuid = computed(() => searchStore.searchForm.locationUuid || '')
const quantity = ref<number | undefined>(
searchStore.searchForm.quantity ? Number(searchStore.searchForm.quantity) : undefined
)
const unit = ref(searchStore.searchForm.unit || 't')
const syncQuantityToStore = () => {
if (quantity.value) {
searchStore.setQuantity(String(quantity.value))
}
searchStore.setUnit(unit.value)
}
// Navigation to selection pages
@@ -135,7 +127,6 @@ const handleSearch = () => {
emit('search', {
productUuid: productUuid.value,
quantity: quantity.value,
unit: unit.value,
locationUuid: locationUuid.value
})
}
@@ -146,10 +137,4 @@ watch(() => searchStore.searchForm.quantity, (val) => {
quantity.value = Number(val)
}
}, { immediate: true })
watch(() => searchStore.searchForm.unit, (val) => {
if (val) {
unit.value = val
}
}, { immediate: true })
</script>

View File

@@ -1,4 +1,4 @@
import { GetNodesDocument } from '~/composables/graphql/public/geo-generated'
import { GetNodesDocument, GetHubCountriesDocument } from '~/composables/graphql/public/geo-generated'
const PAGE_SIZE = 24
@@ -6,6 +6,8 @@ const PAGE_SIZE = 24
const items = ref<any[]>([])
const total = ref(0)
const selectedFilter = ref('all')
const selectedCountry = ref('all')
const countries = ref<string[]>([])
const isLoading = ref(false)
const isLoadingMore = ref(false)
const isInitialized = ref(false)
@@ -22,6 +24,11 @@ export function useCatalogHubs() {
{ id: 'air', label: t('catalogHubsSection.filters.air') }
])
const countryFilters = computed(() => [
{ id: 'all', label: t('catalogHubsSection.filters.all_countries') },
...countries.value.map(c => ({ id: c, label: c }))
])
const itemsWithCoords = computed(() =>
items.value.filter(h => h.latitude && h.longitude)
)
@@ -44,9 +51,10 @@ export function useCatalogHubs() {
if (replace) isLoading.value = true
try {
const transportType = selectedFilter.value === 'all' ? null : selectedFilter.value
const country = selectedCountry.value === 'all' ? null : selectedCountry.value
const data = await execute(
GetNodesDocument,
{ limit: PAGE_SIZE, offset, transportType },
{ limit: PAGE_SIZE, offset, transportType, country },
'public',
'geo'
)
@@ -59,6 +67,15 @@ export function useCatalogHubs() {
}
}
const loadCountries = async () => {
try {
const data = await execute(GetHubCountriesDocument, {}, 'public', 'geo')
countries.value = data?.hubCountries || []
} catch (e) {
console.error('Failed to load hub countries', e)
}
}
const loadMore = async () => {
if (isLoadingMore.value) return
isLoadingMore.value = true
@@ -70,7 +87,7 @@ export function useCatalogHubs() {
}
// При смене фильтра - перезагрузка
watch(selectedFilter, () => {
watch([selectedFilter, selectedCountry], () => {
if (isInitialized.value) {
fetchPage(0, true)
}
@@ -79,7 +96,10 @@ export function useCatalogHubs() {
// Initialize data if not already loaded
const init = async () => {
if (!isInitialized.value && items.value.length === 0) {
await fetchPage(0, true)
await Promise.all([
fetchPage(0, true),
loadCountries()
])
}
}
@@ -87,7 +107,9 @@ export function useCatalogHubs() {
items,
total,
selectedFilter,
selectedCountry,
filters,
countryFilters,
isLoading,
isLoadingMore,
itemsWithCoords,

View File

@@ -35,7 +35,6 @@ export function useCatalogOffers() {
{
limit: PAGE_SIZE,
offset,
status: 'active',
productUuid: selectedProductUuid.value
},
'public',

View File

@@ -1,4 +1,4 @@
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
import { GetAvailableProductsDocument } from '~/composables/graphql/public/exchange-generated'
// Shared state
const items = ref<any[]>([])
@@ -13,12 +13,12 @@ export function useCatalogProducts() {
isLoading.value = true
try {
const data = await execute(
GetProductsDocument,
GetAvailableProductsDocument,
{},
'public',
'exchange'
)
items.value = data?.getProducts || []
items.value = data?.getAvailableProducts || []
isInitialized.value = true
} finally {
isLoading.value = false

View File

@@ -5,20 +5,13 @@ const PAGE_SIZE = 24
// Shared state across list and map views
const items = ref<any[]>([])
const total = ref(0)
const selectedFilter = ref('all')
const isLoading = ref(false)
const isLoadingMore = ref(false)
const isInitialized = ref(false)
export function useCatalogSuppliers() {
const { t } = useI18n()
const { execute } = useGraphQL()
const filters = computed(() => [
{ id: 'all', label: t('catalogSuppliersSection.filters.all') },
{ id: 'verified', label: t('catalogSuppliersSection.filters.verified') }
])
const itemsWithCoords = computed(() =>
items.value.filter(s => s.latitude && s.longitude)
)
@@ -28,10 +21,9 @@ export function useCatalogSuppliers() {
const fetchPage = async (offset: number, replace = false) => {
if (replace) isLoading.value = true
try {
const isVerified = selectedFilter.value === 'verified' ? true : null
const data = await execute(
GetSupplierProfilesDocument,
{ limit: PAGE_SIZE, offset, isVerified },
{ limit: PAGE_SIZE, offset },
'public',
'exchange'
)
@@ -54,13 +46,6 @@ export function useCatalogSuppliers() {
}
}
// При смене фильтра - перезагрузка
watch(selectedFilter, () => {
if (isInitialized.value) {
fetchPage(0, true)
}
})
// Initialize data if not already loaded
const init = async () => {
if (!isInitialized.value && items.value.length === 0) {
@@ -71,8 +56,6 @@ export function useCatalogSuppliers() {
return {
items,
total,
selectedFilter,
filters,
isLoading,
isLoadingMore,
itemsWithCoords,

View File

@@ -8,7 +8,10 @@
@select="onSelectHub"
>
<template #filters>
<CatalogFilterSelect :filters="filters" v-model="selectedFilter" />
<div class="flex gap-2">
<CatalogFilterSelect :filters="filters" v-model="selectedFilter" />
<CatalogFilterSelect :filters="countryFilters" v-model="selectedCountry" />
</div>
</template>
<template #card="{ item }">
@@ -45,7 +48,9 @@ const {
items,
total,
selectedFilter,
selectedCountry,
filters,
countryFilters,
isLoading,
isLoadingMore,
itemsByCountry,

View File

@@ -0,0 +1,9 @@
query GetAvailableProducts {
getAvailableProducts {
uuid
name
categoryId
categoryName
terminusSchemaId
}
}

View File

@@ -1,8 +1,7 @@
query GetOffers($status: String, $productUuid: String, $locationUuid: String, $categoryName: String, $teamUuid: String, $limit: Int, $offset: Int) {
getOffers(status: $status, productUuid: $productUuid, locationUuid: $locationUuid, categoryName: $categoryName, teamUuid: $teamUuid, limit: $limit, offset: $offset) {
query GetOffers($productUuid: String, $locationUuid: String, $categoryName: String, $teamUuid: String, $limit: Int, $offset: Int) {
getOffers(productUuid: $productUuid, locationUuid: $locationUuid, categoryName: $categoryName, teamUuid: $teamUuid, limit: $limit, offset: $offset) {
uuid
teamUuid
status
# Location
locationUuid
locationName
@@ -25,5 +24,5 @@ query GetOffers($status: String, $productUuid: String, $locationUuid: String, $c
createdAt
updatedAt
}
getOffersCount(status: $status, productUuid: $productUuid, locationUuid: $locationUuid, categoryName: $categoryName, teamUuid: $teamUuid)
getOffersCount(productUuid: $productUuid, locationUuid: $locationUuid, categoryName: $categoryName, teamUuid: $teamUuid)
}

View File

@@ -1,5 +1,5 @@
query GetSupplierProfiles($country: String, $isVerified: Boolean, $limit: Int, $offset: Int) {
getSupplierProfiles(country: $country, isVerified: $isVerified, limit: $limit, offset: $offset) {
query GetSupplierProfiles($country: String, $limit: Int, $offset: Int) {
getSupplierProfiles(country: $country, limit: $limit, offset: $offset) {
uuid
teamUuid
name
@@ -7,11 +7,9 @@ query GetSupplierProfiles($country: String, $isVerified: Boolean, $limit: Int, $
country
countryCode
logoUrl
isVerified
isActive
offersCount
latitude
longitude
}
getSupplierProfilesCount(country: $country, isVerified: $isVerified)
getSupplierProfilesCount(country: $country)
}

View File

@@ -0,0 +1,3 @@
query GetHubCountries {
hubCountries
}

View File

@@ -1,5 +1,5 @@
query GetNodes($limit: Int, $offset: Int, $transportType: String) {
nodes(limit: $limit, offset: $offset, transportType: $transportType) {
query GetNodes($limit: Int, $offset: Int, $transportType: String, $country: String) {
nodes(limit: $limit, offset: $offset, transportType: $transportType, country: $country) {
uuid
name
latitude
@@ -9,5 +9,5 @@ query GetNodes($limit: Int, $offset: Int, $transportType: String) {
syncedAt
transportTypes
}
nodesCount(transportType: $transportType)
nodesCount(transportType: $transportType, country: $country)
}

View File

@@ -6,6 +6,8 @@ query GetTeamOrders {
totalAmount
currency
sourceLocationName
sourceLatitude
sourceLongitude
destinationLocationName
createdAt
orderLines {

View File

@@ -11,6 +11,7 @@
},
"filters": {
"all": "All",
"all_countries": "All countries",
"auto": "Auto",
"rail": "Rail",
"sea": "Sea",

View File

@@ -11,6 +11,7 @@
},
"filters": {
"all": "Все",
"all_countries": "Все страны",
"auto": "Авто",
"rail": "Ж/д",
"sea": "Море",