All checks were successful
Build Docker Image / build (push) Successful in 5m8s
- Remove all Storybook files and configuration - Add type declarations for @vueuse/core, @formkit/core, vue3-apexcharts - Fix TypeScript configuration (typeRoots, include paths) - Fix Sentry config - move settings to plugin - Fix nullable prop assignments with ?? operator - Fix type narrowing issues with explicit type assertions - Fix Card component linkable computed properties - Update codegen with operationResultSuffix - Fix GraphQL operation type definitions
321 lines
10 KiB
Vue
321 lines
10 KiB
Vue
<template>
|
|
<MapPanel>
|
|
<template #header>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<div class="flex items-center gap-2">
|
|
<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
|
|
class="btn btn-ghost btn-xs btn-circle text-base-content/60 hover:text-base-content"
|
|
@click="emit('close')"
|
|
>
|
|
<Icon name="lucide:x" size="16" />
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Loading state -->
|
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
|
<span class="loading loading-spinner loading-md" />
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div v-else-if="entity" class="flex flex-col gap-4">
|
|
<!-- Entity details card -->
|
|
<div class="mb-2">
|
|
<HubCard v-if="entityType === 'hub'" :hub="entity" />
|
|
<SupplierCard v-else-if="entityType === 'supplier'" :supplier="entity" />
|
|
<OfferCard v-else-if="entityType === 'offer'" :offer="entity" compact />
|
|
</div>
|
|
|
|
<!-- Tabs for related objects -->
|
|
<div role="tablist" class="tabs tabs-boxed bg-white/10">
|
|
<a
|
|
v-for="tab in availableTabs"
|
|
:key="tab.id"
|
|
role="tab"
|
|
class="tab"
|
|
:class="{ 'tab-active': currentTab === tab.id }"
|
|
@click="currentTab = tab.id"
|
|
>
|
|
{{ tab.label }}
|
|
<span v-if="tab.count" class="ml-1 opacity-70">({{ tab.count }})</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Tab content -->
|
|
<div class="flex flex-col gap-2 min-h-[200px]">
|
|
<!-- 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.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>
|
|
|
|
<!-- Hubs tab -->
|
|
<div v-if="currentTab === 'hubs'">
|
|
<div v-if="relatedHubs.length === 0" class="text-center py-8 text-white/60">
|
|
<Icon name="lucide:warehouse" size="32" class="mb-2 opacity-50" />
|
|
<p>{{ $t('catalog.info.noHubs') }}</p>
|
|
</div>
|
|
<div v-else class="flex flex-col gap-2">
|
|
<HubCard
|
|
v-for="hub in relatedHubs"
|
|
:key="hub.uuid"
|
|
:hub="hub"
|
|
selectable
|
|
@select="onHubSelect(hub)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Suppliers tab -->
|
|
<div v-if="currentTab === 'suppliers'">
|
|
<div v-if="relatedSuppliers.length === 0" class="text-center py-8 text-white/60">
|
|
<Icon name="lucide:factory" size="32" class="mb-2 opacity-50" />
|
|
<p>{{ $t('catalog.info.noSuppliers') }}</p>
|
|
</div>
|
|
<div v-else class="flex flex-col gap-2">
|
|
<SupplierCard
|
|
v-for="supplier in relatedSuppliers"
|
|
:key="supplier.uuid"
|
|
:supplier="supplier"
|
|
selectable
|
|
@select="onSupplierSelect(supplier)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Offers tab (two-step for Hub/Supplier) -->
|
|
<div v-if="currentTab === 'offers'">
|
|
<!-- 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>
|
|
</div>
|
|
|
|
<!-- Add to filter button -->
|
|
<button class="btn btn-primary btn-sm mt-2" @click="emit('add-to-filter')">
|
|
<Icon name="lucide:filter-plus" size="16" />
|
|
{{ $t('catalog.info.addToFilter') }}
|
|
</button>
|
|
</div>
|
|
</MapPanel>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { InfoEntityType } from '~/composables/useCatalogSearch'
|
|
|
|
const props = defineProps<{
|
|
entityType: InfoEntityType
|
|
entityId: string
|
|
entity: any
|
|
relatedProducts?: any[]
|
|
relatedHubs?: any[]
|
|
relatedSuppliers?: any[]
|
|
relatedOffers?: any[]
|
|
selectedProduct?: string | null
|
|
loading?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'close': []
|
|
'add-to-filter': []
|
|
'open-info': [type: InfoEntityType, uuid: string]
|
|
'select-product': [uuid: string | null]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const { entityColors } = useCatalogSearch()
|
|
|
|
// Safe accessors for optional arrays
|
|
const relatedProducts = computed(() => props.relatedProducts ?? [])
|
|
const relatedHubs = computed(() => props.relatedHubs ?? [])
|
|
const relatedSuppliers = computed(() => props.relatedSuppliers ?? [])
|
|
const relatedOffers = computed(() => props.relatedOffers ?? [])
|
|
|
|
// Current active tab
|
|
const currentTab = ref<string>('offers')
|
|
|
|
// Entity name
|
|
const entityName = computed(() => {
|
|
return props.entity?.name || props.entityId.slice(0, 8) + '...'
|
|
})
|
|
|
|
// Badge color and label
|
|
const badgeColor = computed(() => {
|
|
if (props.entityType === 'hub') return entityColors.hub
|
|
if (props.entityType === 'supplier') return entityColors.supplier
|
|
if (props.entityType === 'offer') return entityColors.offer
|
|
return '#666'
|
|
})
|
|
|
|
const badgeLabel = computed(() => {
|
|
if (props.entityType === 'hub') return t('catalog.entities.hub')
|
|
if (props.entityType === 'supplier') return t('catalog.entities.supplier')
|
|
if (props.entityType === 'offer') return t('catalog.entities.offer')
|
|
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: 'offers',
|
|
label: t('catalog.tabs.offers'),
|
|
count: props.selectedProduct
|
|
? props.relatedOffers?.length || 0
|
|
: props.relatedProducts?.length || 0
|
|
})
|
|
tabs.push({
|
|
id: 'suppliers',
|
|
label: t('catalog.tabs.suppliers'),
|
|
count: props.relatedSuppliers?.length || 0
|
|
})
|
|
} else if (props.entityType === 'supplier') {
|
|
tabs.push({
|
|
id: 'offers',
|
|
label: t('catalog.tabs.offers'),
|
|
count: props.selectedProduct
|
|
? props.relatedOffers?.length || 0
|
|
: props.relatedProducts?.length || 0
|
|
})
|
|
tabs.push({
|
|
id: 'hubs',
|
|
label: t('catalog.tabs.hubs'),
|
|
count: props.relatedHubs?.length || 0
|
|
})
|
|
} else if (props.entityType === 'offer') {
|
|
if (props.relatedProducts && props.relatedProducts.length > 0) {
|
|
tabs.push({
|
|
id: 'products',
|
|
label: t('catalog.tabs.product')
|
|
})
|
|
}
|
|
tabs.push({
|
|
id: 'hubs',
|
|
label: t('catalog.tabs.hubs'),
|
|
count: props.relatedHubs?.length || 0
|
|
})
|
|
if (props.relatedSuppliers && props.relatedSuppliers.length > 0) {
|
|
tabs.push({
|
|
id: 'suppliers',
|
|
label: t('catalog.tabs.supplier')
|
|
})
|
|
}
|
|
}
|
|
|
|
return tabs
|
|
})
|
|
|
|
// Set default active tab when entity type changes
|
|
watch(
|
|
() => props.entityType,
|
|
() => {
|
|
const firstTab = availableTabs.value[0]
|
|
if (firstTab) {
|
|
currentTab.value = firstTab.id
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
// Handlers for selecting related items
|
|
const onProductSelect = (product: any) => {
|
|
if (product.uuid) {
|
|
emit('select-product', product.uuid)
|
|
}
|
|
}
|
|
|
|
const onHubSelect = (hub: any) => {
|
|
if (hub.uuid) {
|
|
emit('open-info', 'hub', hub.uuid)
|
|
}
|
|
}
|
|
|
|
const onSupplierSelect = (supplier: any) => {
|
|
if (supplier.uuid) {
|
|
emit('open-info', 'supplier', supplier.uuid)
|
|
}
|
|
}
|
|
|
|
const onOfferSelect = (offer: any) => {
|
|
if (offer.uuid) {
|
|
emit('open-info', 'offer', offer.uuid)
|
|
}
|
|
}
|
|
</script>
|