All checks were successful
Build Docker Image / build (push) Successful in 3m35s
- Move Explore/Quote mode toggle to top right corner - Add view toggle (Offers/Hubs/Suppliers) to top left in Explore mode - Panel shows only in Quote mode when showPanel prop is true - Simplified panel slot structure
278 lines
7.7 KiB
Vue
278 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 Quote form or selection list -->
|
|
<template #panel>
|
|
<QuotePanel
|
|
:product-id="productId"
|
|
:product-label="getFilterLabel('product', productId)"
|
|
:hub-id="hubId"
|
|
:hub-label="getFilterLabel('hub', hubId)"
|
|
:supplier-id="supplierId"
|
|
:supplier-label="getFilterLabel('supplier', supplierId)"
|
|
:quantity="quantity"
|
|
:can-search="canSearch"
|
|
:show-results="showQuoteResults"
|
|
:loading="offersLoading"
|
|
:offers="offers"
|
|
@edit-filter="onEditFilter"
|
|
@remove-filter="onRemoveFilter"
|
|
@update-quantity="onUpdateQuantity"
|
|
@search="onSearch"
|
|
@clear-all="onClearAll"
|
|
@select-offer="onSelectOffer"
|
|
/>
|
|
</template>
|
|
</CatalogPage>
|
|
|
|
<!-- Product selection modal -->
|
|
<dialog ref="productModalRef" class="modal modal-bottom sm:modal-middle">
|
|
<div class="modal-box max-w-2xl max-h-[80vh]">
|
|
<h3 class="font-bold text-lg mb-4">{{ t('catalog.quote.selectProduct') }}</h3>
|
|
<div class="overflow-y-auto max-h-[60vh]">
|
|
<div v-if="productsLoading" class="flex justify-center py-8">
|
|
<span class="loading loading-spinner loading-md" />
|
|
</div>
|
|
<div v-else class="flex flex-col gap-2">
|
|
<button
|
|
v-for="product in products"
|
|
:key="product.uuid"
|
|
class="btn btn-ghost justify-start"
|
|
@click="selectProduct(product)"
|
|
>
|
|
<Icon name="lucide:package" size="16" />
|
|
{{ product.name }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-action">
|
|
<form method="dialog">
|
|
<button class="btn">{{ t('common.cancel') }}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
|
|
<!-- Hub selection modal -->
|
|
<dialog ref="hubModalRef" class="modal modal-bottom sm:modal-middle">
|
|
<div class="modal-box max-w-2xl max-h-[80vh]">
|
|
<h3 class="font-bold text-lg mb-4">{{ t('catalog.quote.selectHub') }}</h3>
|
|
<div class="overflow-y-auto max-h-[60vh]">
|
|
<div v-if="hubsLoading" class="flex justify-center py-8">
|
|
<span class="loading loading-spinner loading-md" />
|
|
</div>
|
|
<div v-else class="flex flex-col gap-2">
|
|
<button
|
|
v-for="hub in hubs"
|
|
:key="hub.uuid"
|
|
class="btn btn-ghost justify-start"
|
|
@click="selectHub(hub)"
|
|
>
|
|
<Icon name="lucide:map-pin" size="16" />
|
|
{{ hub.name }}
|
|
<span v-if="hub.country" class="text-base-content/60 text-sm ml-auto">{{ hub.country }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-action">
|
|
<form method="dialog">
|
|
<button class="btn">{{ t('common.cancel') }}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
</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,
|
|
productId,
|
|
supplierId,
|
|
hubId,
|
|
quantity,
|
|
canSearch,
|
|
mapViewMode,
|
|
entityColors,
|
|
selectItem,
|
|
removeFilter,
|
|
clearAll,
|
|
setLabel,
|
|
filterLabels
|
|
} = useCatalogSearch()
|
|
|
|
// Composables for data
|
|
const { items: products, isLoading: productsLoading, init: initProducts } = useCatalogProducts()
|
|
const { items: hubs, isLoading: hubsLoading, init: initHubs } = useCatalogHubs()
|
|
|
|
// Modal refs
|
|
const productModalRef = ref<HTMLDialogElement | null>(null)
|
|
const hubModalRef = ref<HTMLDialogElement | null>(null)
|
|
|
|
// Offers data for quote results
|
|
const offers = ref<any[]>([])
|
|
const offersLoading = ref(false)
|
|
const showQuoteResults = ref(false)
|
|
|
|
// Loading state
|
|
const isLoading = computed(() => offersLoading.value)
|
|
|
|
// Show panel when in Quote mode
|
|
const showPanel = computed(() => catalogMode.value === 'quote')
|
|
|
|
// Get filter label from cache
|
|
const getFilterLabel = (type: string, id: string | undefined): string | undefined => {
|
|
if (!id) return undefined
|
|
return filterLabels.value[type]?.[id]
|
|
}
|
|
|
|
// 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) => {
|
|
// Navigate to offer detail page if in quote mode with results
|
|
if (catalogMode.value === 'quote' && item.uuid) {
|
|
// Could navigate to offer detail
|
|
console.log('Selected from map:', item)
|
|
}
|
|
}
|
|
|
|
// Edit filter - open modal
|
|
const onEditFilter = async (type: string) => {
|
|
if (type === 'product') {
|
|
await initProducts()
|
|
productModalRef.value?.showModal()
|
|
} else if (type === 'hub') {
|
|
await initHubs()
|
|
hubModalRef.value?.showModal()
|
|
}
|
|
}
|
|
|
|
// Remove filter
|
|
const onRemoveFilter = (type: string) => {
|
|
removeFilter(type)
|
|
showQuoteResults.value = false
|
|
offers.value = []
|
|
}
|
|
|
|
// Update quantity
|
|
const onUpdateQuantity = (value: string) => {
|
|
// Store in URL
|
|
const route = useRoute()
|
|
const newQuery = { ...route.query }
|
|
if (value) {
|
|
newQuery.qty = value
|
|
} else {
|
|
delete newQuery.qty
|
|
}
|
|
router.push({ query: newQuery })
|
|
}
|
|
|
|
// Select product from modal
|
|
const selectProduct = (product: any) => {
|
|
selectItem('product', product.uuid, product.name)
|
|
productModalRef.value?.close()
|
|
showQuoteResults.value = false
|
|
offers.value = []
|
|
}
|
|
|
|
// Select hub from modal
|
|
const selectHub = (hub: any) => {
|
|
selectItem('hub', hub.uuid, hub.name)
|
|
hubModalRef.value?.close()
|
|
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
|
|
}
|
|
}
|
|
|
|
// Clear all filters
|
|
const onClearAll = () => {
|
|
clearAll()
|
|
showQuoteResults.value = false
|
|
offers.value = []
|
|
}
|
|
|
|
// 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>
|