Files
webapp/app/pages/catalog/index.vue
Ruslan Bakiev 7c566aeafc
All checks were successful
Build Docker Image / build (push) Successful in 4m3s
Fix SelectionPanel styling + add product filtering by supplier/hub
- SelectionPanel header: dark glass style instead of white
- useCatalogProducts: filter by supplierId or hubId using dedicated queries
- catalog/index: connect filters from query params to composable
2026-01-24 11:13:22 +07:00

335 lines
10 KiB
Vue

<template>
<CatalogPage
ref="catalogPageRef"
:loading="isLoading"
:use-server-clustering="true"
:cluster-node-type="clusterNodeType"
map-id="unified-catalog-map"
:point-color="mapPointColor"
:items="currentSelectionItems"
:hovered-id="hoveredItemId"
:show-panel="showPanel"
:filter-by-bounds="filterByBounds"
@select="onMapSelect"
@bounds-change="onBoundsChange"
@update:filter-by-bounds="filterByBounds = $event"
>
<!-- Panel slot - shows selection list OR quote results -->
<template #panel>
<!-- Selection mode: show list for picking product/hub/supplier -->
<SelectionPanel
v-if="selectMode"
:select-mode="selectMode"
:products="filteredProducts"
:hubs="filteredHubs"
:suppliers="filteredSuppliers"
:loading="selectionLoading"
:loading-more="selectionLoadingMore"
:has-more="selectionHasMore && !filterByBounds"
@select="onSelectItem"
@close="cancelSelect"
@load-more="onLoadMore"
@hover="onHoverItem"
/>
<!-- Quote results: show offers after search -->
<QuotePanel
v-else-if="showQuoteResults"
:loading="offersLoading"
:offers="offers"
@select-offer="onSelectOffer"
/>
</template>
</CatalogPage>
</template>
<script setup lang="ts">
import { GetOffersDocument } from '~/composables/graphql/public/exchange-generated'
import type { MapBounds } from '~/components/catalog/CatalogMap.vue'
definePageMeta({
layout: 'topnav'
})
const { t } = useI18n()
const { execute } = useGraphQL()
const router = useRouter()
const localePath = useLocalePath()
// Ref to CatalogPage for accessing bounds
const catalogPageRef = ref<{ currentBounds: Ref<MapBounds | null> } | null>(null)
// Filter by map bounds state
const filterByBounds = ref(false)
const currentMapBounds = ref<MapBounds | null>(null)
// Hovered item for map highlight
const hoveredItemId = ref<string | null>(null)
const onHoverItem = (uuid: string | null) => {
hoveredItemId.value = uuid
}
// Current selection items for hover highlighting on map
const currentSelectionItems = computed(() => {
if (selectMode.value === 'product') return filteredProducts.value
if (selectMode.value === 'hub') return filteredHubs.value
if (selectMode.value === 'supplier') return filteredSuppliers.value
return []
})
// Handle bounds change from map
const onBoundsChange = (bounds: MapBounds) => {
currentMapBounds.value = bounds
}
// Check if item is within map bounds
const isInBounds = (item: any, bounds: MapBounds | null): boolean => {
if (!bounds) return true
if (!item.latitude || !item.longitude) return false
const lat = Number(item.latitude)
const lng = Number(item.longitude)
return lat >= bounds.south && lat <= bounds.north
&& lng >= bounds.west && lng <= bounds.east
}
const {
catalogMode,
selectMode,
productId,
supplierId,
hubId,
canSearch,
mapViewMode,
setMapViewMode,
entityColors,
selectItem,
cancelSelect,
setLabel
} = useCatalogSearch()
// Composables for data (initialize immediately when selectMode changes)
const {
items: products,
isLoading: productsLoading,
isLoadingMore: productsLoadingMore,
canLoadMore: productsCanLoadMore,
loadMore: loadMoreProducts,
init: initProducts,
setSupplierFilter,
setHubFilter,
clearFilters: clearProductFilters
} = useCatalogProducts()
const { items: hubs, isLoading: hubsLoading, isLoadingMore: hubsLoadingMore, canLoadMore: hubsCanLoadMore, loadMore: loadMoreHubs, init: initHubs } = useCatalogHubs()
const { items: suppliers, isLoading: suppliersLoading, isLoadingMore: suppliersLoadingMore, canLoadMore: suppliersCanLoadMore, loadMore: loadMoreSuppliers, init: initSuppliers } = useCatalogSuppliers()
// Filtered items by map bounds
const filteredProducts = computed(() => {
if (!filterByBounds.value || !currentMapBounds.value) return products.value
return products.value.filter((p: any) => isInBounds(p, currentMapBounds.value))
})
const filteredHubs = computed(() => {
if (!filterByBounds.value || !currentMapBounds.value) return hubs.value
return hubs.value.filter((h: any) => isInBounds(h, currentMapBounds.value))
})
const filteredSuppliers = computed(() => {
if (!filterByBounds.value || !currentMapBounds.value) return suppliers.value
return suppliers.value.filter((s: any) => isInBounds(s, currentMapBounds.value))
})
// Selection loading state
const selectionLoading = computed(() => {
if (selectMode.value === 'product') return productsLoading.value
if (selectMode.value === 'hub') return hubsLoading.value
if (selectMode.value === 'supplier') return suppliersLoading.value
return false
})
// Selection loading more state
const selectionLoadingMore = computed(() => {
if (selectMode.value === 'product') return productsLoadingMore.value
if (selectMode.value === 'hub') return hubsLoadingMore.value
if (selectMode.value === 'supplier') return suppliersLoadingMore.value
return false
})
// Selection has more state
const selectionHasMore = computed(() => {
if (selectMode.value === 'product') return productsCanLoadMore.value
if (selectMode.value === 'hub') return hubsCanLoadMore.value
if (selectMode.value === 'supplier') return suppliersCanLoadMore.value
return false
})
// Load more handler
const onLoadMore = () => {
if (selectMode.value === 'product') loadMoreProducts()
if (selectMode.value === 'hub') loadMoreHubs()
if (selectMode.value === 'supplier') loadMoreSuppliers()
}
// Initialize data and sync map view when selectMode changes
watch(selectMode, async (mode) => {
if (mode === 'product') {
await initProducts()
setMapViewMode('offers')
}
if (mode === 'hub') {
await initHubs()
setMapViewMode('hubs')
}
if (mode === 'supplier') {
await initSuppliers()
setMapViewMode('suppliers')
}
}, { immediate: true })
// Apply filters to products based on selected supplier/hub
watch([supplierId, hubId], ([newSupplierId, newHubId]) => {
if (newSupplierId) {
setSupplierFilter(newSupplierId)
} else if (newHubId) {
setHubFilter(newHubId)
} else {
clearProductFilters()
}
}, { immediate: true })
// Offers data for quote results
const offers = ref<any[]>([])
const offersLoading = ref(false)
const showQuoteResults = ref(false)
// Watch for search trigger from topnav
const searchTrigger = useState<number>('catalog-search-trigger', () => 0)
watch(searchTrigger, () => {
if (canSearch.value) {
onSearch()
}
})
// Loading state
const isLoading = computed(() => offersLoading.value || selectionLoading.value)
// Show panel when selecting OR when showing quote results
const showPanel = computed(() => {
return selectMode.value !== null || showQuoteResults.value
})
// Cluster node type based on map view mode
const clusterNodeType = computed(() => {
if (mapViewMode.value === 'offers') return 'offer'
if (mapViewMode.value === 'hubs') return 'logistics'
if (mapViewMode.value === 'suppliers') return 'supplier'
return 'offer'
})
// Map point color based on map view mode
const mapPointColor = computed(() => {
if (mapViewMode.value === 'offers') return entityColors.offer
if (mapViewMode.value === 'hubs') return entityColors.hub
if (mapViewMode.value === 'suppliers') return entityColors.supplier
return entityColors.offer
})
// Handle map item selection
const onMapSelect = (item: any) => {
// If in selection mode, use map click to fill the selector
if (selectMode.value && item.uuid) {
const itemName = item.name || item.uuid.slice(0, 8) + '...'
// For hubs selection - click on hub fills hub selector
if (selectMode.value === 'hub' && mapViewMode.value === 'hubs') {
selectItem('hub', item.uuid, itemName)
showQuoteResults.value = false
offers.value = []
return
}
// For supplier selection - click on supplier fills supplier selector
if (selectMode.value === 'supplier' && mapViewMode.value === 'suppliers') {
selectItem('supplier', item.uuid, itemName)
showQuoteResults.value = false
offers.value = []
return
}
// For product selection viewing offers - try to find product in loaded list
if (selectMode.value === 'product' && mapViewMode.value === 'offers') {
// The offer has product info - we'll look it up from the products list if loaded
// or just use the offer's product_uuid if available
const product = products.value.find((p: any) => p.uuid === item.productUuid)
if (product) {
selectItem('product', product.uuid, product.name || itemName)
} else if (item.productUuid) {
selectItem('product', item.productUuid, item.productName || itemName)
}
showQuoteResults.value = false
offers.value = []
return
}
}
// Default behavior for quote mode etc
if (catalogMode.value === 'quote' && item.uuid) {
console.log('Selected from map:', item)
}
}
// Handle selection from SelectionPanel
const onSelectItem = (type: string, item: any) => {
if (item.uuid && item.name) {
selectItem(type, item.uuid, item.name)
showQuoteResults.value = false
offers.value = []
}
}
// Search for offers
const onSearch = async () => {
if (!canSearch.value) return
offersLoading.value = true
showQuoteResults.value = true
try {
const vars: any = {}
if (productId.value) vars.productUuid = productId.value
if (supplierId.value) vars.teamUuid = supplierId.value
if (hubId.value) vars.locationUuid = hubId.value
const data = await execute(GetOffersDocument, vars, 'public', 'exchange')
offers.value = data?.getOffers || []
// Update labels from response
if (offers.value.length > 0) {
const first = offers.value[0]
if (productId.value && first.productName) {
setLabel('product', productId.value, first.productName)
}
if (hubId.value && first.locationName) {
setLabel('hub', hubId.value, first.locationName)
}
if (supplierId.value && first.teamName) {
setLabel('supplier', supplierId.value, first.teamName)
}
}
} finally {
offersLoading.value = false
}
}
// Select offer - navigate to detail page
const onSelectOffer = (offer: any) => {
if (offer.uuid && offer.productUuid) {
router.push(localePath(`/catalog/offers/${offer.productUuid}?offer=${offer.uuid}`))
}
}
// SEO
useHead(() => ({
title: t('catalog.hero.title')
}))
</script>