Files
webapp/app/pages/catalog/suppliers/[id].vue
2026-01-07 09:10:35 +07:00

252 lines
8.4 KiB
Vue

<template>
<Stack gap="0">
<!-- Loading -->
<Section v-if="isLoading" variant="plain" paddingY="lg">
<Stack align="center" justify="center" gap="4">
<Spinner />
<Text tone="muted">{{ t('catalogSupplier.states.loading') }}</Text>
</Stack>
</Section>
<!-- Error / Not Found -->
<Section v-else-if="!supplier" variant="plain" paddingY="lg">
<Card padding="lg">
<Stack align="center" gap="4">
<IconCircle tone="primary">
<Icon name="lucide:building-2" size="24" />
</IconCircle>
<Heading :level="2">{{ t('catalogSupplier.not_found.title') }}</Heading>
<Text tone="muted">{{ t('catalogSupplier.not_found.subtitle') }}</Text>
<Button @click="navigateTo(localePath('/catalog'))">
{{ t('catalogSupplier.actions.back_to_catalog') }}
</Button>
</Stack>
</Card>
</Section>
<template v-else>
<!-- Map Hero -->
<MapHero
:title="supplier.name"
:location="supplierLocation"
:badges="supplierBadges"
:initial-zoom="3"
/>
<!-- Offers Section -->
<Section v-if="offers.length > 0" variant="plain" paddingY="md">
<Stack gap="4">
<Heading :level="2">{{ t('catalogSupplier.sections.offers.title') }}</Heading>
<Grid :cols="1" :md="2" :lg="3" :gap="4">
<OfferCard
v-for="offer in offers"
:key="offer.uuid"
:offer="offer"
/>
</Grid>
</Stack>
</Section>
<!-- Products Section -->
<Section v-if="uniqueProducts.length > 0" variant="plain" paddingY="md">
<Stack gap="4">
<Heading :level="2">{{ t('catalogSupplier.sections.products.title') }}</Heading>
<Stack direction="row" gap="2" wrap>
<NuxtLink
v-for="product in uniqueProducts"
:key="product.uuid"
:to="localePath(`/catalog/products/${product.uuid}`)"
>
<Pill variant="primary" class="hover:bg-primary hover:text-white transition-colors cursor-pointer">
{{ product.name }}
</Pill>
</NuxtLink>
</Stack>
</Stack>
</Section>
<!-- Locations Map Section -->
<Section v-if="uniqueLocations.length > 0" variant="plain" paddingY="md">
<Stack gap="4">
<Heading :level="2">{{ t('catalogSupplier.sections.locations.title') }}</Heading>
<div class="h-64 rounded-lg overflow-hidden border border-base-300 bg-base-100">
<ClientOnly>
<MapboxGlobe
:key="`supplier-locations-${supplierId}`"
:map-id="`supplier-locations-${supplierId}`"
:locations="mapLocations"
:height="256"
:initial-zoom="3"
/>
</ClientOnly>
</div>
<Stack direction="row" gap="2" wrap>
<NuxtLink
v-for="location in uniqueLocations"
:key="location.uuid"
:to="localePath(`/catalog/hubs/${location.uuid}`)"
>
<Pill variant="outline" class="hover:bg-base-200 transition-colors cursor-pointer">
<Icon name="lucide:map-pin" size="14" />
{{ location.name }}
</Pill>
</NuxtLink>
</Stack>
</Stack>
</Section>
</template>
</Stack>
</template>
<script setup lang="ts">
import {
GetSupplierProfileDocument,
GetSupplierOffersDocument,
GetSupplierProfilesDocument,
GetOffersDocument,
} from '~/composables/graphql/public/exchange-generated'
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.id as string)
// Supplier location for map - use supplier's own coordinates
const supplierLocation = computed(() => {
if (supplier.value?.latitude && supplier.value?.longitude) {
return {
uuid: supplier.value.uuid,
name: supplier.value.name,
latitude: supplier.value.latitude,
longitude: supplier.value.longitude,
country: supplier.value.country,
countryCode: supplier.value.countryCode
}
}
// Fallback to first offer location if supplier has no coordinates
const firstOffer = offers.value.find(o => o.locationLatitude && o.locationLongitude)
if (firstOffer) {
return {
uuid: firstOffer.locationUuid,
name: firstOffer.locationName,
latitude: firstOffer.locationLatitude,
longitude: firstOffer.locationLongitude,
country: firstOffer.locationCountry,
countryCode: firstOffer.locationCountryCode
}
}
return null
})
// Badges for MapHero
const supplierBadges = computed(() => {
const badges: Array<{ icon?: string; text: string }> = []
if (supplier.value?.country) {
badges.push({ icon: 'lucide:globe', text: supplier.value.country })
}
if (supplier.value?.isVerified) {
badges.push({ icon: 'lucide:check-circle', text: t('catalogSupplier.badges.verified') })
}
if (offers.value.length > 0) {
badges.push({ icon: 'lucide:package', text: t('catalogSupplier.badges.offers', { count: offers.value.length }) })
}
return badges
})
// Unique products from offers
const uniqueProducts = computed(() => {
const products = new Map<string, { uuid: string; name: string }>()
offers.value.forEach(offer => {
offer.lines?.forEach((line: any) => {
if (line?.productUuid && line?.productName) {
products.set(line.productUuid, { uuid: line.productUuid, name: line.productName })
}
})
})
return Array.from(products.values())
})
// Unique locations from offers
const uniqueLocations = computed(() => {
const locations = new Map<string, { uuid: string; name: string; latitude: number; longitude: number; country?: string | null; countryCode?: string | null }>()
offers.value.forEach(offer => {
if (offer.locationUuid && offer.locationName && offer.locationLatitude && offer.locationLongitude) {
locations.set(offer.locationUuid, {
uuid: offer.locationUuid,
name: offer.locationName,
latitude: offer.locationLatitude,
longitude: offer.locationLongitude,
country: offer.locationCountry,
countryCode: offer.locationCountryCode
})
}
})
return Array.from(locations.values())
})
// Locations for the map
const mapLocations = computed(() => {
return uniqueLocations.value.map(loc => ({
uuid: loc.uuid,
name: loc.name,
latitude: loc.latitude,
longitude: loc.longitude,
country: ''
}))
})
try {
const { data: supplierData } = await useServerQuery('catalog-supplier-detail', GetSupplierProfileDocument, { uuid: supplierId.value }, 'public', 'exchange')
supplier.value = supplierData.value?.getSupplierProfile || null
if (!supplier.value) {
const { data: suppliersList } = await useServerQuery('catalog-suppliers-fallback', GetSupplierProfilesDocument, {}, 'public', 'exchange')
supplier.value = (suppliersList.value?.getSupplierProfiles || []).find((s: any) => s?.teamUuid === supplierId.value || s?.uuid === supplierId.value) || null
}
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('catalog-supplier-offers', GetSupplierOffersDocument, { teamUuid: primaryId }, 'public', 'exchange')
offers.value = offersData.value?.getOffers || []
if (!offers.value.length) {
const { data: allOffersData } = await useServerQuery('catalog-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:', error)
} finally {
isLoading.value = false
}
// SEO
useHead(() => ({
title: supplier.value?.name
? t('catalogSupplier.meta.title_with_name', { name: supplier.value.name })
: t('catalogSupplier.meta.title'),
meta: [
{
name: 'description',
content: supplier.value?.description
|| (supplier.value?.name
? t('catalogSupplier.meta.description_with_name', { name: supplier.value.name })
: t('catalogSupplier.meta.description'))
}
]
}))
</script>