Add offer detail page /catalog/offers/[offerId]
All checks were successful
Build Docker Image / build (push) Successful in 4m28s
All checks were successful
Build Docker Image / build (push) Successful in 4m28s
- New page shows offer details, supplier info, and full KYC profile - Updated CalcResultContent to navigate to offer page on card click
This commit is contained in:
@@ -180,11 +180,7 @@ const getKycProfileUuid = (offerUuid?: string | null) => {
|
|||||||
// Navigate to offer detail page
|
// Navigate to offer detail page
|
||||||
const navigateToOffer = (offerUuid?: string | null) => {
|
const navigateToOffer = (offerUuid?: string | null) => {
|
||||||
if (!offerUuid) return
|
if (!offerUuid) return
|
||||||
const offer = offersData.value.get(offerUuid)
|
navigateTo(localePath(`/catalog/offers/${offerUuid}`))
|
||||||
if (!offer?.teamUuid || !productUuid.value || !destinationUuid.value) return
|
|
||||||
|
|
||||||
// Navigate to /catalog/suppliers/[supplierId]/[productId]/[hubId]
|
|
||||||
navigateTo(localePath(`/catalog/suppliers/${offer.teamUuid}/${productUuid.value}/${destinationUuid.value}`))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load offer details for prices
|
// Load offer details for prices
|
||||||
|
|||||||
176
app/pages/catalog/offers/[offerId].vue
Normal file
176
app/pages/catalog/offers/[offerId].vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<Section variant="plain">
|
||||||
|
<Stack gap="6">
|
||||||
|
<!-- Loading -->
|
||||||
|
<Card v-if="isLoading" padding="lg">
|
||||||
|
<Stack align="center" gap="3">
|
||||||
|
<Spinner />
|
||||||
|
<Text tone="muted">Загрузка оффера...</Text>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<Alert v-else-if="!offer" variant="error">
|
||||||
|
<Text>Оффер не найден</Text>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- Offer Header -->
|
||||||
|
<Card padding="lg">
|
||||||
|
<Stack gap="4">
|
||||||
|
<!-- Product & Price -->
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<Text weight="semibold" size="xl">{{ offer.productName || 'Товар' }}</Text>
|
||||||
|
<Text v-if="offer.locationName" tone="muted">{{ offer.locationName }}</Text>
|
||||||
|
</div>
|
||||||
|
<Text v-if="priceDisplay" weight="bold" class="text-primary text-2xl">
|
||||||
|
{{ priceDisplay }}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quantity -->
|
||||||
|
<div v-if="offer.quantity" class="flex items-center gap-2">
|
||||||
|
<Icon name="lucide:package" size="16" class="text-base-content/60" />
|
||||||
|
<Text tone="muted">Доступно: {{ offer.quantity }} {{ offer.unit || 'т' }}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location on map -->
|
||||||
|
<div v-if="offer.latitude && offer.longitude" class="h-48 rounded-lg overflow-hidden">
|
||||||
|
<ClientOnly>
|
||||||
|
<MapboxMap
|
||||||
|
map-id="offer-location-map"
|
||||||
|
class="w-full h-full"
|
||||||
|
:options="{
|
||||||
|
style: 'mapbox://styles/mapbox/streets-v12',
|
||||||
|
center: [offer.longitude, offer.latitude],
|
||||||
|
zoom: 8
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<MapboxDefaultMarker
|
||||||
|
:lnglat="[offer.longitude, offer.latitude]"
|
||||||
|
color="#10b981"
|
||||||
|
/>
|
||||||
|
</MapboxMap>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Supplier Info -->
|
||||||
|
<Card v-if="supplier" padding="lg">
|
||||||
|
<Stack gap="3">
|
||||||
|
<Text weight="semibold" size="lg">Поставщик</Text>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div v-if="supplier.logoUrl" class="w-12 h-12 rounded-full overflow-hidden bg-base-200">
|
||||||
|
<img :src="supplier.logoUrl" :alt="supplier.name" class="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="w-12 h-12 rounded-full bg-base-200 flex items-center justify-center">
|
||||||
|
<Icon name="lucide:building-2" size="24" class="text-base-content/40" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text weight="medium" size="lg">{{ supplier.name }}</Text>
|
||||||
|
<Text v-if="supplier.country" tone="muted">{{ supplier.country }}</Text>
|
||||||
|
</div>
|
||||||
|
<div v-if="supplier.isVerified" class="badge badge-success ml-auto">Верифицирован</div>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- KYC Profile (full company info) -->
|
||||||
|
<KycProfileCard v-if="supplier?.kycProfileUuid" :kyc-profile-uuid="supplier.kycProfileUuid" />
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<Card padding="lg">
|
||||||
|
<Stack gap="3">
|
||||||
|
<Button variant="primary" size="lg" class="w-full">
|
||||||
|
<Icon name="lucide:message-circle" size="18" />
|
||||||
|
Связаться с поставщиком
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="lg" class="w-full" @click="goBack">
|
||||||
|
<Icon name="lucide:arrow-left" size="18" />
|
||||||
|
Назад к результатам
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
</Stack>
|
||||||
|
</Section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { GetOfferDocument, GetSupplierProfileByTeamDocument } from '~/composables/graphql/public/exchange-generated'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'topnav'
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const { execute } = useGraphQL()
|
||||||
|
|
||||||
|
const offerId = computed(() => route.params.offerId as string)
|
||||||
|
|
||||||
|
const isLoading = ref(true)
|
||||||
|
const offer = ref<any>(null)
|
||||||
|
const supplier = ref<any>(null)
|
||||||
|
|
||||||
|
// Load offer data
|
||||||
|
const loadOffer = async () => {
|
||||||
|
if (!offerId.value) return
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await execute(GetOfferDocument, { uuid: offerId.value }, 'public', 'exchange')
|
||||||
|
offer.value = data?.getOffer || null
|
||||||
|
|
||||||
|
// Load supplier if we have teamUuid
|
||||||
|
if (offer.value?.teamUuid) {
|
||||||
|
const supplierData = await execute(
|
||||||
|
GetSupplierProfileByTeamDocument,
|
||||||
|
{ teamUuid: offer.value.teamUuid },
|
||||||
|
'public',
|
||||||
|
'exchange'
|
||||||
|
)
|
||||||
|
supplier.value = supplierData?.getSupplierProfileByTeam || null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading offer:', error)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadOffer()
|
||||||
|
|
||||||
|
// Price display
|
||||||
|
const priceDisplay = computed(() => {
|
||||||
|
if (!offer.value?.pricePerUnit) return null
|
||||||
|
const currSymbol = getCurrencySymbol(offer.value.currency)
|
||||||
|
const unitName = offer.value.unit || 'т'
|
||||||
|
return `${currSymbol}${offer.value.pricePerUnit.toLocaleString()}/${unitName}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const getCurrencySymbol = (currency?: string | null) => {
|
||||||
|
switch (currency?.toUpperCase()) {
|
||||||
|
case 'USD': return '$'
|
||||||
|
case 'EUR': return '€'
|
||||||
|
case 'RUB': return '₽'
|
||||||
|
case 'CNY': return '¥'
|
||||||
|
default: return '$'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEO
|
||||||
|
useHead(() => ({
|
||||||
|
title: offer.value?.productName
|
||||||
|
? `${offer.value.productName} - Оффер`
|
||||||
|
: 'Оффер'
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user