Implement unified catalog search with token-based filtering
All checks were successful
Build Docker Image / build (push) Successful in 3m23s
All checks were successful
Build Docker Image / build (push) Successful in 3m23s
- Add useCatalogSearch composable for managing unified search state - Add UnifiedSearchBar component with token chips for filters - Add CatalogHero component for empty/landing state - Create grid components for each display mode: - CatalogGridProducts, CatalogGridSuppliers, CatalogGridHubs - CatalogGridHubsForProduct, CatalogGridProductsFromSupplier - CatalogGridProductsInHub, CatalogGridOffers - Add unified catalog page at /catalog with query params - Remove SubNavigation from catalog section (kept for other sections) - Update all links to use new unified catalog paths - Delete old nested catalog pages (offers/suppliers/hubs flows) - Add i18n translations for catalog section
This commit is contained in:
233
app/composables/useCatalogSearch.ts
Normal file
233
app/composables/useCatalogSearch.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
|
||||
export type SelectMode = 'product' | 'supplier' | 'hub' | null
|
||||
export type DisplayMode =
|
||||
| 'hero'
|
||||
| 'grid-products'
|
||||
| 'grid-suppliers'
|
||||
| 'grid-hubs'
|
||||
| 'grid-hubs-for-product'
|
||||
| 'grid-products-from-supplier'
|
||||
| 'grid-products-in-hub'
|
||||
| 'grid-offers'
|
||||
|
||||
export interface SearchFilter {
|
||||
type: 'product' | 'supplier' | 'hub' | 'location' | '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
|
||||
location: { id: string; name: string } | null
|
||||
quantity: string | null
|
||||
}
|
||||
|
||||
// Filter labels cache (to show names instead of UUIDs)
|
||||
const filterLabels = ref<Record<string, Record<string, string>>>({
|
||||
product: {},
|
||||
supplier: {},
|
||||
hub: {},
|
||||
location: {}
|
||||
})
|
||||
|
||||
export function useCatalogSearch() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Parse current state from query params
|
||||
const selectMode = computed<SelectMode>(() => {
|
||||
const select = route.query.select as string | undefined
|
||||
if (select === 'product' || select === 'supplier' || select === 'hub') {
|
||||
return select
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
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 locationId = computed(() => route.query.location 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
|
||||
const setLabel = (type: string, id: string, label: string) => {
|
||||
if (!filterLabels.value[type]) {
|
||||
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 (locationId.value) {
|
||||
tokens.push({
|
||||
type: 'location',
|
||||
id: locationId.value,
|
||||
label: getLabel('location', locationId.value) || t('catalog.filters.location'),
|
||||
icon: 'lucide:navigation'
|
||||
})
|
||||
}
|
||||
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') })
|
||||
}
|
||||
if (!locationId.value) {
|
||||
chips.push({ type: 'location', label: t('catalog.filters.location') })
|
||||
}
|
||||
if (!quantity.value) {
|
||||
chips.push({ type: 'quantity', label: t('catalog.filters.quantity') })
|
||||
}
|
||||
|
||||
return chips
|
||||
})
|
||||
|
||||
// Determine what content to show
|
||||
const displayMode = computed<DisplayMode>(() => {
|
||||
// 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'
|
||||
|
||||
// Empty state
|
||||
return 'hero'
|
||||
})
|
||||
|
||||
// Navigation helpers
|
||||
const updateQuery = (updates: Partial<LocationQuery>) => {
|
||||
const newQuery = { ...route.query }
|
||||
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
if (value === null || value === undefined) {
|
||||
delete newQuery[key]
|
||||
} else {
|
||||
newQuery[key] = value as string
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
const removeFilter = (type: string) => {
|
||||
updateQuery({ [type]: null })
|
||||
}
|
||||
|
||||
const editFilter = (type: string) => {
|
||||
updateQuery({ select: type as SelectMode })
|
||||
}
|
||||
|
||||
const clearAll = () => {
|
||||
router.push({ query: {} })
|
||||
}
|
||||
|
||||
// Text search (for filtering within current grid)
|
||||
const searchQuery = ref('')
|
||||
|
||||
return {
|
||||
// State
|
||||
selectMode,
|
||||
displayMode,
|
||||
productId,
|
||||
supplierId,
|
||||
hubId,
|
||||
locationId,
|
||||
quantity,
|
||||
searchQuery,
|
||||
|
||||
// Computed
|
||||
activeTokens,
|
||||
availableChips,
|
||||
|
||||
// Actions
|
||||
startSelect,
|
||||
cancelSelect,
|
||||
selectItem,
|
||||
removeFilter,
|
||||
editFilter,
|
||||
clearAll,
|
||||
setLabel,
|
||||
|
||||
// Labels cache
|
||||
filterLabels
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user