Fix Info panel - translations, two-step offers flow, icon, add to filter
All checks were successful
Build Docker Image / build (push) Successful in 3m36s

- Add i18n translations for entities, tabs, and info sections (EN/RU)
- Refactor offers tab to two-step flow (products → offers) for Hub/Supplier
- Replace entity badge with circular icon in header
- Fix "Add to filter" button with name fallback and proper cleanup
- Update selectItem() to clear info param when adding to filter
This commit is contained in:
Ruslan Bakiev
2026-01-25 16:44:00 +07:00
parent 908d63062c
commit 39c3d24b3a
5 changed files with 110 additions and 37 deletions

View File

@@ -3,9 +3,12 @@
<template #header>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="badge badge-sm" :style="{ backgroundColor: badgeColor }">
{{ badgeLabel }}
</span>
<div
class="flex items-center justify-center w-6 h-6 rounded-full"
:style="{ backgroundColor: badgeColor }"
>
<Icon :name="entityIcon" size="14" class="text-white" />
</div>
<h3 class="font-semibold text-base text-base-content">{{ entityName }}</h3>
</div>
<button
@@ -48,11 +51,11 @@
<!-- Tab content -->
<div class="flex flex-col gap-2 min-h-[200px]">
<!-- Products tab -->
<div v-if="currentTab === 'products'">
<!-- Products tab (only for Offer entity) -->
<div v-if="currentTab === 'products' && entityType === 'offer'">
<div v-if="relatedProducts.length === 0" class="text-center py-8 text-white/60">
<Icon name="lucide:package" size="32" class="mb-2 opacity-50" />
<p>{{ $t('catalog.info.noProducts') }}</p>
<p>{{ $t('catalog.empty.noProducts') }}</p>
</div>
<div v-else class="flex flex-col gap-2">
<ProductCard
@@ -100,26 +103,58 @@
</div>
</div>
<!-- Offers tab -->
<!-- Offers tab (two-step for Hub/Supplier) -->
<div v-if="currentTab === 'offers'">
<div v-if="!selectedProduct" class="text-center py-8 text-white/60">
<!-- Step 1: Select product (for Hub/Supplier) -->
<div v-if="!selectedProduct && (entityType === 'hub' || entityType === 'supplier')">
<div v-if="relatedProducts.length === 0" class="text-center py-8 text-white/60">
<Icon name="lucide:package" size="32" class="mb-2 opacity-50" />
<p>{{ $t('catalog.empty.noProducts') }}</p>
</div>
<div v-else class="flex flex-col gap-2">
<ProductCard
v-for="product in relatedProducts"
:key="product.uuid"
:product="product"
selectable
compact
@select="onProductSelect(product)"
/>
</div>
</div>
<!-- Step 2: Show offers for selected product -->
<div v-else-if="selectedProduct">
<!-- Back button to products -->
<button
class="btn btn-sm btn-ghost mb-2 text-white/80 hover:text-white"
@click="emit('select-product', null)"
>
<Icon name="lucide:arrow-left" size="16" />
{{ $t('common.back') }}
</button>
<div v-if="relatedOffers.length === 0" class="text-center py-8 text-white/60">
<Icon name="lucide:shopping-bag" size="32" class="mb-2 opacity-50" />
<p>{{ $t('catalog.empty.noOffers') }}</p>
</div>
<div v-else class="flex flex-col gap-2">
<OfferCard
v-for="offer in relatedOffers"
:key="offer.uuid"
:offer="offer"
selectable
compact
@select="onOfferSelect(offer)"
/>
</div>
</div>
<!-- For Offer entity type - just show message -->
<div v-else class="text-center py-8 text-white/60">
<Icon name="lucide:info" size="32" class="mb-2 opacity-50" />
<p>{{ $t('catalog.info.selectProductFirst') }}</p>
</div>
<div v-else-if="relatedOffers.length === 0" class="text-center py-8 text-white/60">
<Icon name="lucide:shopping-bag" size="32" class="mb-2 opacity-50" />
<p>{{ $t('catalog.info.noOffers') }}</p>
</div>
<div v-else class="flex flex-col gap-2">
<OfferCard
v-for="offer in relatedOffers"
:key="offer.uuid"
:offer="offer"
selectable
compact
@select="onOfferSelect(offer)"
/>
</div>
</div>
</div>
@@ -158,7 +193,7 @@ const { t } = useI18n()
const { entityColors } = useCatalogSearch()
// Current active tab
const currentTab = ref<string>('products')
const currentTab = ref<string>('offers')
// Entity name
const entityName = computed(() => {
@@ -180,20 +215,24 @@ const badgeLabel = computed(() => {
return ''
})
const entityIcon = computed(() => {
if (props.entityType === 'hub') return 'lucide:warehouse'
if (props.entityType === 'supplier') return 'lucide:factory'
if (props.entityType === 'offer') return 'lucide:shopping-bag'
return 'lucide:info'
})
// Available tabs based on entity type and data
const availableTabs = computed(() => {
const tabs: Array<{ id: string; label: string; count?: number }> = []
if (props.entityType === 'hub') {
tabs.push({
id: 'products',
label: t('catalog.tabs.products'),
count: props.relatedProducts?.length || 0
})
tabs.push({
id: 'offers',
label: t('catalog.tabs.offers'),
count: props.relatedOffers?.length || 0
count: props.selectedProduct
? props.relatedOffers?.length || 0
: props.relatedProducts?.length || 0
})
tabs.push({
id: 'suppliers',
@@ -201,15 +240,12 @@ const availableTabs = computed(() => {
count: props.relatedSuppliers?.length || 0
})
} else if (props.entityType === 'supplier') {
tabs.push({
id: 'products',
label: t('catalog.tabs.products'),
count: props.relatedProducts?.length || 0
})
tabs.push({
id: 'offers',
label: t('catalog.tabs.offers'),
count: props.relatedOffers?.length || 0
count: props.selectedProduct
? props.relatedOffers?.length || 0
: props.relatedProducts?.length || 0
})
tabs.push({
id: 'hubs',

View File

@@ -210,7 +210,8 @@ export function useCatalogSearch() {
setLabel(type, id, label)
updateQuery({
[type]: id,
select: null // Exit selection mode
select: null, // Exit selection mode
info: null // Exit info mode
})
}

View File

@@ -395,9 +395,11 @@ const onInfoClose = () => {
const onInfoAddToFilter = () => {
if (!infoId.value || !entity.value) return
const { type, uuid } = infoId.value
const name = entity.value.name
// Fallback for name - offer entities might use productName
const name = entity.value.name || entity.value.productName || uuid.slice(0, 8) + '...'
selectItem(type, uuid, name)
closeInfo()
clearInfo()
}
const onInfoOpenRelated = (type: 'hub' | 'supplier' | 'offer', uuid: string) => {

View File

@@ -1,5 +1,10 @@
{
"catalog": {
"entities": {
"hub": "Hub",
"supplier": "Supplier",
"offer": "Offer"
},
"filters": {
"product": "Product",
"supplier": "Supplier",
@@ -41,6 +46,18 @@
"offers": "Offers",
"map": "Map"
},
"tabs": {
"products": "Products",
"offers": "Offers",
"suppliers": "Suppliers",
"hubs": "Hubs",
"product": "Product",
"supplier": "Supplier"
},
"info": {
"selectProductFirst": "Select a product first",
"addToFilter": "Add to filter"
},
"modes": {
"explore": "Explore",
"quote": "Get Quote"

View File

@@ -1,5 +1,10 @@
{
"catalog": {
"entities": {
"hub": "Хаб",
"supplier": "Поставщик",
"offer": "Оффер"
},
"filters": {
"product": "Товар",
"supplier": "Поставщик",
@@ -41,6 +46,18 @@
"offers": "Офферы",
"map": "Карта"
},
"tabs": {
"products": "Товары",
"offers": "Офферы",
"suppliers": "Поставщики",
"hubs": "Хабы",
"product": "Товар",
"supplier": "Поставщик"
},
"info": {
"selectProductFirst": "Сначала выберите товар",
"addToFilter": "Добавить в фильтр"
},
"modes": {
"explore": "Исследовать",
"quote": "Найти офферы"