All checks were successful
Build Docker Image / build (push) Successful in 3m37s
- Fix team selector alignment in navbar (use items-center, fixed height) - Fix SelectionPanel header padding to account for parent p-4 - Add map search input (white glass, positioned next to panel) - Add infinite scroll to SelectionPanel with IntersectionObserver - Products load all at once (no server-side pagination yet) - Hubs and Suppliers support pagination with loadMore
255 lines
7.7 KiB
Vue
255 lines
7.7 KiB
Vue
<template>
|
|
<CatalogPage
|
|
:loading="isLoading"
|
|
:use-server-clustering="true"
|
|
:cluster-node-type="clusterNodeType"
|
|
map-id="unified-catalog-map"
|
|
:point-color="mapPointColor"
|
|
:items="[]"
|
|
:show-panel="showPanel"
|
|
@select="onMapSelect"
|
|
>
|
|
<!-- 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="products"
|
|
:hubs="hubs"
|
|
:suppliers="suppliers"
|
|
:loading="selectionLoading"
|
|
:loading-more="selectionLoadingMore"
|
|
:has-more="selectionHasMore"
|
|
@select="onSelectItem"
|
|
@close="cancelSelect"
|
|
@load-more="onLoadMore"
|
|
/>
|
|
|
|
<!-- 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'
|
|
|
|
definePageMeta({
|
|
layout: 'topnav'
|
|
})
|
|
|
|
const { t } = useI18n()
|
|
const { execute } = useGraphQL()
|
|
const router = useRouter()
|
|
const localePath = useLocalePath()
|
|
|
|
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 } = 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()
|
|
|
|
// 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 })
|
|
|
|
// 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>
|