All checks were successful
Build Docker Image / build (push) Successful in 4m3s
- Add strictScalars: true to codegen.ts with proper scalar mappings (Date, Decimal, JSONString, JSON, UUID, BigInt → string/Record) - Replace all ref<any[]> with proper GraphQL-derived types - Add type guards for null filtering in arrays - Fix bugs exposed by typing (locationLatitude vs latitude, etc.) - Add interfaces for external components (MapboxSearchBox) This enables end-to-end type safety from GraphQL schema to frontend.
180 lines
5.9 KiB
Vue
180 lines
5.9 KiB
Vue
<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.locationLatitude && offer.locationLongitude" 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.locationLongitude, offer.locationLatitude],
|
||
zoom: 8
|
||
}"
|
||
>
|
||
<MapboxDefaultMarker
|
||
:lnglat="[offer.locationLongitude, offer.locationLatitude]"
|
||
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, type GetOfferQueryResult, type GetSupplierProfileByTeamQueryResult } from '~/composables/graphql/public/exchange-generated'
|
||
|
||
type Offer = NonNullable<GetOfferQueryResult['getOffer']>
|
||
type SupplierProfile = NonNullable<GetSupplierProfileByTeamQueryResult['getSupplierProfileByTeam']>
|
||
|
||
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<Offer | null>(null)
|
||
const supplier = ref<SupplierProfile | null>(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>
|