Files
webapp/app/pages/catalog/suppliers/[supplierId]/index.vue
Ruslan Bakiev 0a79b90d1c
All checks were successful
Build Docker Image / build (push) Successful in 4m0s
Replace breadcrumbs with key-value badges in search bar
- Add key property to FilterOption interface in CatalogSearchBar
- Display badges in "Key: Value" format (e.g., "Поставщик: Name")
- Remove SuppliersBreadcrumbs from supplier catalog pages
- Add navigationFilters computed with supplier/product/hub badges
- Add handleRemoveFilter to navigate back when badge is clicked
2026-01-19 11:09:58 +07:00

249 lines
7.5 KiB
Vue

<template>
<CatalogPage
:items="filteredProducts"
:map-items="mapItems"
:loading="isLoading"
:total-count="products.length"
with-map
map-id="supplier-products-map"
point-color="#3b82f6"
>
<template #searchBar="{ displayedCount, totalCount }">
<CatalogSearchBar
v-model:search-query="searchQuery"
:active-filters="navigationFilters"
:displayed-count="displayedCount"
:total-count="totalCount"
@remove-filter="handleRemoveFilter"
/>
</template>
<template #header>
<!-- Supplier Not Found -->
<Card v-if="!isLoading && !supplier" padding="lg">
<Stack align="center" gap="4">
<IconCircle tone="primary">
<Icon name="lucide:building-2" size="24" />
</IconCircle>
<Heading :level="2">{{ t('catalogSupplierProducts.not_found.title') }}</Heading>
<Text tone="muted">{{ t('catalogSupplierProducts.not_found.subtitle') }}</Text>
<Button @click="navigateTo(localePath('/catalog/suppliers'))">
{{ t('catalogSupplierProducts.actions.back_to_suppliers') }}
</Button>
</Stack>
</Card>
<!-- Content Header -->
<Stack v-else gap="4">
<!-- Header with supplier info -->
<div class="flex items-start gap-4">
<!-- Logo -->
<div v-if="supplier?.logo" class="w-16 h-16 shrink-0">
<img :src="supplier.logo" :alt="supplier.name || ''" class="w-full h-full object-contain rounded-lg">
</div>
<div v-else class="w-16 h-16 bg-primary/10 text-primary font-bold rounded-lg flex items-center justify-center text-2xl shrink-0">
{{ supplier?.name?.charAt(0) }}
</div>
<div>
<Heading :level="1">{{ supplier?.name }}</Heading>
<Text tone="muted" size="sm">{{ supplier?.country }}</Text>
<div class="flex gap-2 mt-1">
<span v-if="supplier?.isVerified" class="badge badge-success badge-sm">
{{ t('catalogSupplier.badges.verified') }}
</span>
</div>
</div>
</div>
<!-- Products Section Title -->
<Text weight="semibold" size="lg">{{ t('catalogSupplierProducts.header.products_title', { count: products.length }) }}</Text>
</Stack>
</template>
<template #card="{ item }">
<HubProductCard
:name="item.name"
:price-history="getMockPriceHistory(item.uuid)"
@select="goToProduct(item.uuid)"
/>
</template>
<template #empty>
<Stack align="center" gap="2">
<Icon name="lucide:package-x" size="32" class="text-base-content/40" />
<Text tone="muted">{{ t('catalogSupplierProducts.empty.no_products') }}</Text>
</Stack>
</template>
</CatalogPage>
</template>
<script setup lang="ts">
import {
GetSupplierProfileDocument,
GetSupplierOffersDocument,
GetSupplierProfilesDocument,
GetOffersDocument,
} from '~/composables/graphql/public/exchange-generated'
definePageMeta({
layout: 'topnav'
})
const route = useRoute()
const localePath = useLocalePath()
const { t } = useI18n()
const isLoading = ref(true)
const supplier = ref<any>(null)
const offers = ref<any[]>([])
const supplierId = computed(() => route.params.supplierId as string)
// Navigation filters for search bar badges
const navigationFilters = computed(() => {
const filters: Array<{ id: string; label: string; key: string }> = []
if (supplier.value?.name) {
filters.push({
id: 'supplier',
key: 'Поставщик',
label: supplier.value.name
})
}
return filters
})
// Handle removing navigation filter (navigate back)
const handleRemoveFilter = (filterId: string) => {
if (filterId === 'supplier') {
navigateTo(localePath('/catalog/suppliers'))
}
}
// Search
const searchQuery = ref('')
const filteredProducts = computed(() => {
if (!searchQuery.value.trim()) return products.value
const q = searchQuery.value.toLowerCase()
return products.value.filter(item =>
item.name?.toLowerCase().includes(q)
)
})
// Map items - show supplier location
const mapItems = computed(() => {
if (!supplier.value?.latitude || !supplier.value?.longitude) return []
return [{
uuid: supplier.value.uuid || supplier.value.teamUuid || supplierId.value,
name: supplier.value.name || '',
latitude: Number(supplier.value.latitude),
longitude: Number(supplier.value.longitude),
country: supplier.value.country
}]
})
// Extract unique products from offers
const products = computed(() => {
const productsMap = new Map<string, { uuid: string; name: string; locationUuid?: string }>()
offers.value.forEach(offer => {
if (offer.productUuid && offer.productName && !productsMap.has(offer.productUuid)) {
productsMap.set(offer.productUuid, {
uuid: offer.productUuid,
name: offer.productName,
locationUuid: offer.locationUuid
})
}
})
return Array.from(productsMap.values())
})
// Navigate to product detail
const goToProduct = (productId: string) => {
navigateTo(localePath(`/catalog/suppliers/${supplierId.value}/${productId}`))
}
// Mock price history generator (seeded by uuid for consistent results)
const getMockPriceHistory = (uuid: string): number[] => {
const seed = uuid.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
const basePrice = 100 + (seed % 200)
return Array.from({ length: 7 }, (_, i) => {
const variation = Math.sin(seed + i * 0.5) * 20 + Math.cos(seed * 0.3 + i) * 10
return Math.round(basePrice + variation)
})
}
// Load data
try {
// Try to get supplier by UUID first
const { data: supplierData } = await useServerQuery(
'supplier-profile',
GetSupplierProfileDocument,
{ uuid: supplierId.value },
'public',
'exchange'
)
supplier.value = supplierData.value?.getSupplierProfile || null
// Fallback to searching in all suppliers
if (!supplier.value) {
const { data: suppliersList } = await useServerQuery(
'suppliers-fallback',
GetSupplierProfilesDocument,
{},
'public',
'exchange'
)
supplier.value = (suppliersList.value?.getSupplierProfiles || [])
.find((s: any) => s?.teamUuid === supplierId.value || s?.uuid === supplierId.value) || null
}
// Get supplier's offers
if (supplier.value) {
const teamIds = [
supplier.value?.teamUuid,
supplier.value?.uuid,
supplierId.value
].filter(Boolean)
if (teamIds.length) {
const primaryId = teamIds[0] as string
const { data: offersData } = await useServerQuery(
'supplier-offers',
GetSupplierOffersDocument,
{ teamUuid: primaryId },
'public',
'exchange'
)
offers.value = offersData.value?.getOffers || []
// Fallback if no offers found
if (!offers.value.length) {
const { data: allOffersData } = await useServerQuery(
'supplier-offers-fallback',
GetOffersDocument,
{},
'public',
'exchange'
)
const ids = new Set(teamIds)
offers.value = (allOffersData.value?.getOffers || [])
.filter((o: any) => o?.teamUuid && ids.has(o.teamUuid))
}
}
}
} catch (error) {
console.error('Error loading supplier products:', error)
} finally {
isLoading.value = false
}
// SEO
useHead(() => ({
title: supplier.value?.name
? t('catalogSupplierProducts.meta.title_with_name', { name: supplier.value.name })
: t('catalogSupplierProducts.meta.title')
}))
</script>