Files
webapp/app/components/catalog/QuotePanel.vue
Ruslan Bakiev 25f946b293
All checks were successful
Build Docker Image / build (push) Successful in 5m46s
Fix geo GraphQL schema mismatch: camelCase → snake_case
All geo .graphql operations and consuming code updated to match
server schema which uses snake_case field/argument names.
Removed non-existent QuoteCalculations query, using NearestOffers instead.
2026-03-09 21:45:57 +07:00

149 lines
4.2 KiB
Vue

<template>
<div class="flex flex-col h-full">
<!-- Header -->
<div class="flex-shrink-0 p-4 border-b border-white/10">
<div class="flex items-center justify-between">
<h3 class="font-semibold text-base text-white">{{ $t('catalog.headers.offers') }}</h3>
<span class="badge badge-neutral">{{ totalOffers }}</span>
</div>
</div>
<!-- Content (scrollable) -->
<div class="flex-1 overflow-y-auto p-4">
<div v-if="loading" class="flex items-center justify-center py-8">
<span class="loading loading-spinner loading-md text-white" />
</div>
<div v-else-if="offersWithPrice.length === 0" class="text-center py-8 text-white/60">
<Icon name="lucide:search-x" size="32" class="mb-2" />
<p>{{ $t('catalog.empty.noOffers') }}</p>
</div>
<div v-else class="flex flex-col gap-3">
<div
v-for="group in offerGroups"
:key="group.id"
class="flex flex-col gap-0"
>
<div
v-if="group.offers.length > 1"
class="rounded-2xl overflow-hidden border border-base-200/60 divide-y divide-base-200/60"
>
<div
v-for="offer in group.offers"
:key="offer.uuid"
class="cursor-pointer"
@click="emit('select-offer', offer)"
>
<OfferResultCard
grouped
:supplier-name="offer.supplier_name"
:location-name="offer.country || ''"
:product-name="offer.product_name"
:price-per-unit="offer.price_per_unit ? Number(offer.price_per_unit) : null"
:quantity="offer.quantity"
:currency="offer.currency"
:unit="offer.unit"
:stages="getOfferStages(offer)"
:total-time-seconds="offer.routes?.[0]?.total_time_seconds ?? null"
/>
</div>
</div>
<div
v-else
v-for="offer in group.offers"
:key="offer.uuid"
class="cursor-pointer"
@click="emit('select-offer', offer)"
>
<OfferResultCard
:supplier-name="offer.supplier_name"
:location-name="offer.country || ''"
:product-name="offer.product_name"
:price-per-unit="offer.price_per_unit ? Number(offer.price_per_unit) : null"
:quantity="offer.quantity"
:currency="offer.currency"
:unit="offer.unit"
:stages="getOfferStages(offer)"
:total-time-seconds="offer.routes?.[0]?.total_time_seconds ?? null"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Offer {
uuid: string
product_name?: string | null
product_uuid?: string | null
supplier_name?: string | null
supplier_uuid?: string | null
quantity?: number | string | null
unit?: string | null
price_per_unit?: number | string | null
currency?: string | null
country?: string | null
country_code?: string | null
routes?: Array<{
total_time_seconds?: number | null
stages?: Array<{
transport_type?: string | null
distance_km?: number | null
travel_time_seconds?: number | null
from_name?: string | null
} | null> | null
} | null> | null
}
interface OfferGroup {
id: string
offers: Offer[]
}
const emit = defineEmits<{
'select-offer': [offer: Offer]
}>()
const props = defineProps<{
loading: boolean
offers: Offer[]
calculations?: OfferGroup[]
}>()
const offersWithPrice = computed(() =>
(props.offers || []).filter(o => o?.price_per_unit != null)
)
const totalOffers = computed(() => {
if (props.calculations?.length) {
return props.calculations.reduce((sum, calc) => sum + (calc.offers?.length || 0), 0)
}
return props.offers.length
})
const offerGroups = computed<OfferGroup[]>(() => {
if (props.calculations?.length) return props.calculations
return offersWithPrice.value.map(offer => ({
id: offer.uuid,
offers: [offer]
}))
})
const getOfferStages = (offer: Offer) => {
const route = offer.routes?.[0]
if (!route?.stages) return []
return route.stages
.filter((stage): stage is NonNullable<typeof stage> => stage !== null)
.map((stage) => ({
transportType: stage.transport_type,
distanceKm: stage.distance_km,
travelTimeSeconds: stage.travel_time_seconds,
fromName: stage.from_name
}))
}
</script>