All checks were successful
Build Docker Image / build (push) Successful in 4m47s
All catalog pages now use CatalogPage component with map on the right side: - /catalog/hubs/[id] - shows hub on map - /catalog/offers - shows empty map (products have no coords) - /catalog/offers/[productId] - shows hubs where product can be delivered - /catalog/suppliers/[supplierId] - shows supplier location - /catalog/suppliers/[supplierId]/[productId] - shows hubs for supplier's product - /catalog/suppliers/[supplierId]/[productId]/[hubId] - shows source and destination
210 lines
6.5 KiB
Vue
210 lines
6.5 KiB
Vue
<template>
|
|
<CatalogPage
|
|
:items="products"
|
|
:map-items="mapItems"
|
|
:loading="isLoading"
|
|
with-map
|
|
map-id="supplier-products-map"
|
|
point-color="#3b82f6"
|
|
>
|
|
<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">
|
|
<!-- Breadcrumbs -->
|
|
<SuppliersBreadcrumbs :supplier-id="supplierId" :supplier-name="supplier?.name" />
|
|
|
|
<!-- 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)
|
|
|
|
// 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 => {
|
|
offer.lines?.forEach((line: any) => {
|
|
if (line?.productUuid && line?.productName && !productsMap.has(line.productUuid)) {
|
|
productsMap.set(line.productUuid, {
|
|
uuid: line.productUuid,
|
|
name: line.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>
|