import type { LocationQuery } from 'vue-router' export type SelectMode = 'product' | 'supplier' | 'hub' | null export type MapViewMode = 'offers' | 'hubs' | 'suppliers' export type CatalogMode = 'explore' | 'quote' export type InfoEntityType = 'hub' | 'supplier' | 'offer' export type DisplayMode = | 'map-default' | 'grid-products' | 'grid-suppliers' | 'grid-hubs' | 'grid-hubs-for-product' | 'grid-products-from-supplier' | 'grid-products-in-hub' | 'grid-offers' export interface InfoId { type: InfoEntityType uuid: string } export interface SearchFilter { type: 'product' | 'supplier' | 'hub' | 'quantity' id: string label: string } export interface SearchState { selectMode: SelectMode product: { id: string; name: string } | null supplier: { id: string; name: string } | null hub: { id: string; name: string } | null quantity: string | null } // Color scheme for entity types export const entityColors = { product: '#f97316', // orange supplier: '#3b82f6', // blue hub: '#22c55e', // green offer: '#f97316' // orange (same as product context) } as const // Filter labels cache (to show names instead of UUIDs) const filterLabels = ref>>({ product: {}, supplier: {}, hub: {} }) export function useCatalogSearch() { const route = useRoute() const router = useRouter() const { t } = useI18n() // Parse current state from query params const selectMode = computed(() => { const select = route.query.select as string | undefined if (select === 'product' || select === 'supplier' || select === 'hub') { return select } return null }) // Parse info state from query param (format: "type:uuid") const infoId = computed(() => { const info = route.query.info as string | undefined if (!info) return null const [type, uuid] = info.split(':') if (type && uuid && ['hub', 'supplier', 'offer'].includes(type)) { return { type: type as InfoEntityType, uuid } } return null }) // Info panel tab (stored in URL for sharing) const infoTab = computed(() => route.query.infoTab as string | undefined) // Info panel selected product (stored in URL for sharing) const infoProduct = computed(() => route.query.infoProduct as string | undefined) const productId = computed(() => route.query.product as string | undefined) const supplierId = computed(() => route.query.supplier as string | undefined) const hubId = computed(() => route.query.hub as string | undefined) const quantity = computed(() => route.query.qty as string | undefined) // Get label for a filter (from cache or fallback to ID) const getLabel = (type: string, id: string | undefined): string | null => { if (!id) return null return filterLabels.value[type]?.[id] || id.slice(0, 8) + '...' } // Set label in cache (trigger reactivity by reassigning) const setLabel = (type: string, id: string, label: string) => { filterLabels.value = { ...filterLabels.value, [type]: { ...filterLabels.value[type], [id]: label } } } // Active tokens for display const activeTokens = computed(() => { const tokens: Array<{ type: string; id: string; label: string; icon: string }> = [] if (productId.value) { tokens.push({ type: 'product', id: productId.value, label: getLabel('product', productId.value) || t('catalog.filters.product'), icon: 'lucide:package' }) } if (supplierId.value) { tokens.push({ type: 'supplier', id: supplierId.value, label: getLabel('supplier', supplierId.value) || t('catalog.filters.supplier'), icon: 'lucide:factory' }) } if (hubId.value) { tokens.push({ type: 'hub', id: hubId.value, label: getLabel('hub', hubId.value) || t('catalog.filters.hub'), icon: 'lucide:map-pin' }) } if (quantity.value) { tokens.push({ type: 'quantity', id: quantity.value, label: `${quantity.value} т`, icon: 'lucide:scale' }) } return tokens }) // Available chips (filters not yet set) const availableChips = computed(() => { const chips: Array<{ type: string; label: string }> = [] if (!productId.value && selectMode.value !== 'product') { chips.push({ type: 'product', label: t('catalog.filters.product') }) } if (!supplierId.value && selectMode.value !== 'supplier') { chips.push({ type: 'supplier', label: t('catalog.filters.supplier') }) } if (!hubId.value && selectMode.value !== 'hub') { chips.push({ type: 'hub', label: t('catalog.filters.hub') }) } // Quantity only available after product is selected if (productId.value && !quantity.value) { chips.push({ type: 'quantity', label: t('catalog.filters.quantity') }) } return chips }) // Determine what content to show const displayMode = computed(() => { // Selection mode takes priority if (selectMode.value === 'product') return 'grid-products' if (selectMode.value === 'supplier') return 'grid-suppliers' if (selectMode.value === 'hub') return 'grid-hubs' // Results based on filters if (productId.value && hubId.value) return 'grid-offers' if (supplierId.value && productId.value) return 'grid-offers' if (productId.value) return 'grid-hubs-for-product' if (supplierId.value) return 'grid-products-from-supplier' if (hubId.value) return 'grid-products-in-hub' // Default: show map with all offers return 'map-default' }) // Check if we're on the main page (not /catalog) const localePath = useLocalePath() const isMainPage = computed(() => { const catalogPath = localePath('/catalog') return !route.path.startsWith(catalogPath) }) // Navigation helpers const updateQuery = (updates: Partial) => { const newQuery = { ...route.query } Object.entries(updates).forEach(([key, value]) => { if (value === null || value === undefined) { delete newQuery[key] } else { newQuery[key] = value as string } }) // If on main page and adding filters, navigate to /catalog if (isMainPage.value && Object.keys(newQuery).length > 0) { router.push({ path: localePath('/catalog'), query: newQuery }) } else { router.push({ query: newQuery }) } } const startSelect = (type: SelectMode) => { updateQuery({ select: type }) } const cancelSelect = () => { updateQuery({ select: null }) } const selectItem = (type: string, id: string, label: string) => { setLabel(type, id, label) updateQuery({ [type]: id, select: null, // Exit selection mode info: null // Exit info mode }) } const removeFilter = (type: string) => { updateQuery({ [type]: null }) } const editFilter = (type: string) => { updateQuery({ select: type as SelectMode }) } const setQuantity = (value: string) => { const qty = value ? value : null updateQuery({ qty }) } const openInfo = (type: InfoEntityType, uuid: string) => { updateQuery({ info: `${type}:${uuid}`, select: null, infoTab: null, infoProduct: null }) } const closeInfo = () => { updateQuery({ info: null, infoTab: null, infoProduct: null }) } const setInfoTab = (tab: string) => { updateQuery({ infoTab: tab }) } const setInfoProduct = (productUuid: string | null) => { updateQuery({ infoProduct: productUuid }) } const clearAll = () => { if (isMainPage.value) { router.push({ path: localePath('/catalog'), query: {} }) } else { router.push({ query: {} }) } } // Text search (for filtering within current grid) - shared via useState const searchQuery = useState('catalog-search-query', () => '') // Map view mode (stored in URL query param 'view') const mapViewMode = computed(() => { const view = route.query.view as string | undefined if (view === 'hubs' || view === 'suppliers' || view === 'offers') { return view } return 'offers' // default }) const setMapViewMode = (mode: MapViewMode) => { updateQuery({ view: mode === 'offers' ? null : mode }) } // Drawer state for list view const isDrawerOpen = ref(false) const drawerSelectedItem = ref<{ uuid: string; name: string } | null>(null) const openDrawer = () => { isDrawerOpen.value = true drawerSelectedItem.value = null // Set selectMode based on mapViewMode so SelectionPanel shows the right list const newSelectMode: SelectMode = mapViewMode.value === 'hubs' ? 'hub' : mapViewMode.value === 'suppliers' ? 'supplier' : 'product' startSelect(newSelectMode) } const closeDrawer = () => { isDrawerOpen.value = false drawerSelectedItem.value = null cancelSelect() // Also exit selection mode } const selectDrawerItem = (uuid: string, name: string) => { drawerSelectedItem.value = { uuid, name } } const applyDrawerFilter = () => { if (!drawerSelectedItem.value) { closeDrawer() return } // Determine filter type based on mapViewMode const type = mapViewMode.value === 'hubs' ? 'hub' : mapViewMode.value === 'suppliers' ? 'supplier' : 'product' // offers -> select product from offer const { uuid, name } = drawerSelectedItem.value selectItem(type, uuid, name) closeDrawer() } // Catalog mode: explore (map browsing) or quote (search for offers) const catalogMode = computed(() => { return route.query.mode === 'quote' ? 'quote' : 'explore' }) const setCatalogMode = (newMode: CatalogMode) => { updateQuery({ mode: newMode === 'explore' ? null : newMode }) } // Can search for offers (product + hub or product + supplier required) const canSearch = computed(() => { return !!(productId.value && (hubId.value || supplierId.value)) }) // Labels for Quote mode display const productLabel = computed(() => getLabel('product', productId.value)) const hubLabel = computed(() => getLabel('hub', hubId.value)) const supplierLabel = computed(() => getLabel('supplier', supplierId.value)) return { // State selectMode, infoId, infoTab, infoProduct, displayMode, catalogMode, productId, supplierId, hubId, quantity, searchQuery, mapViewMode, // Drawer state isDrawerOpen, drawerSelectedItem, // Colors entityColors, // Computed activeTokens, availableChips, canSearch, productLabel, hubLabel, supplierLabel, // Actions startSelect, cancelSelect, selectItem, removeFilter, editFilter, setQuantity, openInfo, closeInfo, setInfoTab, setInfoProduct, clearAll, setLabel, setMapViewMode, setCatalogMode, // Drawer actions openDrawer, closeDrawer, selectDrawerItem, applyDrawerFilter, // Labels cache filterLabels } }