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
|
||||
const navigateToOffer = (offerUuid?: string | null) => {
|
||||
if (!offerUuid) return
|
||||
const offer = offersData.value.get(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}`))
|
||||
navigateTo(localePath(`/catalog/offers/${offerUuid}`))
|
||||
}
|
||||
|
||||
// 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