Redesign offer result card layout
All checks were successful
Build Docker Image / build (push) Successful in 4m53s

This commit is contained in:
Ruslan Bakiev
2026-02-06 17:07:38 +07:00
parent b971391fd7
commit e4f81dba7c
9 changed files with 106 additions and 24 deletions

View File

@@ -24,6 +24,7 @@
:location-name="getOfferData(option.sourceUuid)?.locationName" :location-name="getOfferData(option.sourceUuid)?.locationName"
:product-name="productName" :product-name="productName"
:price-per-unit="parseFloat(getOfferData(option.sourceUuid)?.pricePerUnit || '0') || null" :price-per-unit="parseFloat(getOfferData(option.sourceUuid)?.pricePerUnit || '0') || null"
:quantity="getOfferData(option.sourceUuid)?.quantity"
:currency="getOfferData(option.sourceUuid)?.currency" :currency="getOfferData(option.sourceUuid)?.currency"
:unit="getOfferData(option.sourceUuid)?.unit" :unit="getOfferData(option.sourceUuid)?.unit"
:stages="getRouteStages(option)" :stages="getRouteStages(option)"

View File

@@ -88,6 +88,7 @@
:location-name="offer.locationName || offer.locationCountry" :location-name="offer.locationName || offer.locationCountry"
:product-name="offer.productName" :product-name="offer.productName"
:price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null" :price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null"
:quantity="offer.quantity"
:currency="offer.currency" :currency="offer.currency"
:unit="offer.unit" :unit="offer.unit"
:stages="[]" :stages="[]"

View File

@@ -44,6 +44,7 @@
:location-name="offer.locationName" :location-name="offer.locationName"
:product-name="offer.productName || offer.title || undefined" :product-name="offer.productName || offer.title || undefined"
:price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null" :price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null"
:quantity="offer.quantity"
:currency="offer.currency" :currency="offer.currency"
:unit="offer.unit" :unit="offer.unit"
:stages="[]" :stages="[]"
@@ -91,6 +92,7 @@ interface Offer {
status?: string | null status?: string | null
latitude?: number | null latitude?: number | null
longitude?: number | null longitude?: number | null
quantity?: number | string | null
pricePerUnit?: number | string | null pricePerUnit?: number | string | null
currency?: string | null currency?: string | null
unit?: string | null unit?: string | null

View File

@@ -20,6 +20,7 @@
:location-name="offer.locationName" :location-name="offer.locationName"
:product-name="offer.title || undefined" :product-name="offer.title || undefined"
:price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null" :price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null"
:quantity="offer.quantity"
:currency="offer.currency" :currency="offer.currency"
:unit="offer.unit" :unit="offer.unit"
:stages="[]" :stages="[]"
@@ -56,6 +57,7 @@ interface Offer {
status?: string | null status?: string | null
validUntil?: string | null validUntil?: string | null
lines?: (OfferLine | null)[] | null lines?: (OfferLine | null)[] | null
quantity?: number | string | null
pricePerUnit?: number | string | null pricePerUnit?: number | string | null
currency?: string | null currency?: string | null
unit?: string | null unit?: string | null

View File

@@ -142,9 +142,13 @@
<span v-if="loadingOffers" class="loading loading-spinner loading-xs" /> <span v-if="loadingOffers" class="loading loading-spinner loading-xs" />
<span v-else-if="offersWithPrice.length > 0" class="text-white/50">({{ offersWithPrice.length }})</span> <span v-else-if="offersWithPrice.length > 0" class="text-white/50">({{ offersWithPrice.length }})</span>
</h3> </h3>
<button class="btn btn-ghost btn-xs text-white/60" @click="emit('select-product', null)"> <button
<Icon name="lucide:x" size="14" /> class="flex items-center gap-2 px-2 py-1 rounded-full border border-white/15 bg-white/10 text-xs text-white/80 hover:bg-white/20 transition-colors"
{{ $t('common.cancel') }} @click="emit('select-product', null)"
>
<Icon name="lucide:package" size="12" />
<span class="max-w-32 truncate">{{ selectedProductName }}</span>
<Icon name="lucide:x" size="12" />
</button> </button>
</div> </div>
@@ -159,6 +163,7 @@
:location-name="offer.locationName || offer.locationCountry || offer.locationName" :location-name="offer.locationName || offer.locationCountry || offer.locationName"
:product-name="offer.productName" :product-name="offer.productName"
:price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null" :price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null"
:quantity="offer.quantity"
:currency="offer.currency" :currency="offer.currency"
:unit="offer.unit" :unit="offer.unit"
:stages="getOfferStages(offer)" :stages="getOfferStages(offer)"
@@ -341,6 +346,12 @@ const offersWithPrice = computed(() =>
relatedOffers.value.filter(o => o?.pricePerUnit != null) relatedOffers.value.filter(o => o?.pricePerUnit != null)
) )
const selectedProductName = computed(() => {
if (!props.selectedProduct) return ''
const match = relatedProducts.value.find(p => p.uuid === props.selectedProduct)
return match?.name || props.selectedProduct.slice(0, 8) + '...'
})
// Entity name // Entity name
const entityName = computed(() => { const entityName = computed(() => {
return props.entity?.name || props.entity?.productName || props.entityId.slice(0, 8) + '...' return props.entity?.name || props.entity?.productName || props.entityId.slice(0, 8) + '...'

View File

@@ -1,15 +1,32 @@
<template> <template>
<Card padding="md" interactive @click="$emit('select')"> <Card padding="md" interactive @click="$emit('select')">
<!-- Header: Location + Price --> <!-- Header: Supplier + Price -->
<div class="flex items-start justify-between mb-3"> <div class="flex items-start justify-between gap-4">
<div> <div class="flex flex-col gap-1">
<Text weight="semibold">{{ supplierDisplay }}</Text> <div class="flex items-center gap-2">
<Text tone="muted" size="sm"> <div class="w-6 h-6 rounded-full flex items-center justify-center" style="background-color: #3b82f6">
{{ t('catalogOfferCard.labels.origin_label') }}: {{ originDisplay }} <Icon name="lucide:factory" size="14" class="text-white" />
</Text> </div>
<Text v-if="productName" tone="muted" size="sm">{{ productName }}</Text> <Text weight="semibold">{{ supplierDisplay }}</Text>
</div>
<div class="flex items-center gap-2 text-sm text-base-content/70">
<Icon name="lucide:map-pin" size="14" class="text-base-content/60" />
<span>{{ originDisplay }}</span>
</div>
<div v-if="productName" class="flex items-center gap-2 text-sm text-base-content/70">
<Icon name="lucide:package" size="14" class="text-base-content/60" />
<span>{{ productName }}</span>
</div>
<div v-if="quantityDisplay" class="flex items-center gap-2 text-sm text-base-content/70">
<Icon name="lucide:scale" size="14" class="text-base-content/60" />
<span>{{ quantityDisplay }}</span>
</div>
</div> </div>
<Text v-if="priceDisplay" weight="semibold" class="text-primary text-lg">
<Text v-if="priceDisplay" weight="semibold" class="text-primary text-lg text-right">
{{ priceDisplay }} {{ priceDisplay }}
</Text> </Text>
</div> </div>
@@ -17,13 +34,13 @@
<!-- Supplier info --> <!-- Supplier info -->
<SupplierInfoBlock v-if="kycProfileUuid" :kyc-profile-uuid="kycProfileUuid" class="mb-3" /> <SupplierInfoBlock v-if="kycProfileUuid" :kyc-profile-uuid="kycProfileUuid" class="mb-3" />
<!-- Route stepper --> <!-- Route lines -->
<RouteStepper <div v-if="routeRows.length" class="mt-3 pt-2 border-t border-base-200/60">
v-if="stages.length > 0" <div v-for="(row, index) in routeRows" :key="index" class="flex items-center gap-2 text-sm text-base-content/70">
:stages="stages" <Icon :name="row.icon" size="14" class="text-base-content/60" />
:start-name="startName" <span>{{ row.distanceLabel }}</span>
:end-name="endName" </div>
/> </div>
</Card> </Card>
</template> </template>
@@ -35,6 +52,7 @@ const props = withDefaults(defineProps<{
supplierName?: string supplierName?: string
productName?: string productName?: string
pricePerUnit?: number | null pricePerUnit?: number | null
quantity?: number | string | null
currency?: string | null currency?: string | null
unit?: string | null unit?: string | null
stages?: RouteStage[] stages?: RouteStage[]
@@ -60,13 +78,25 @@ const originDisplay = computed(() => {
}) })
const priceDisplay = computed(() => { const priceDisplay = computed(() => {
if (!props.pricePerUnit) return null if (props.pricePerUnit == null) return null
const currSymbol = getCurrencySymbol(props.currency) const currSymbol = getCurrencySymbol(props.currency)
const unitName = getUnitName(props.unit) const unitName = getUnitName(props.unit)
const formattedPrice = props.pricePerUnit.toLocaleString() const formattedPrice = Number(props.pricePerUnit).toLocaleString()
return `${currSymbol}${formattedPrice}/${unitName}` return `${currSymbol}${formattedPrice}/${unitName}`
}) })
const quantityDisplay = computed(() => {
if (props.quantity == null || props.quantity === '') return null
const quantityValue = Number(props.quantity)
if (Number.isNaN(quantityValue)) return null
const formattedQuantity = quantityValue.toLocaleString()
const unitName = getUnitName(props.unit)
return t('catalogOfferCard.labels.quantity_with_unit', {
quantity: formattedQuantity,
unit: unitName
})
})
const getCurrencySymbol = (currency?: string | null) => { const getCurrencySymbol = (currency?: string | null) => {
switch (currency?.toUpperCase()) { switch (currency?.toUpperCase()) {
case 'USD': return '$' case 'USD': return '$'
@@ -80,14 +110,44 @@ const getCurrencySymbol = (currency?: string | null) => {
const getUnitName = (unit?: string | null) => { const getUnitName = (unit?: string | null) => {
switch (unit?.toLowerCase()) { switch (unit?.toLowerCase()) {
case 'т': case 'т':
case 't':
case 'ton': case 'ton':
case 'tonne': case 'tonne':
return 'тонна' return t('catalogOfferCard.labels.default_unit')
case 'кг': case 'кг':
case 'kg': case 'kg':
return 'кг' return t('catalogOfferCard.labels.unit_kg')
default: default:
return 'тонна' return t('catalogOfferCard.labels.default_unit')
} }
} }
const formatDistance = (km?: number | null) => {
if (km == null) return null
const formatted = Math.round(km).toLocaleString()
return t('catalogOfferCard.labels.distance_km', { km: formatted })
}
const getTransportIcon = (type?: string | null) => {
switch (type) {
case 'rail':
return 'lucide:train-front'
case 'sea':
return 'lucide:ship'
case 'road':
case 'auto':
default:
return 'lucide:truck'
}
}
const routeRows = computed(() =>
(props.stages || [])
.filter(stage => stage?.distanceKm != null)
.map(stage => ({
icon: getTransportIcon(stage?.transportType),
distanceLabel: formatDistance(stage?.distanceKm)
}))
.filter(row => !!row.distanceLabel)
)
</script> </script>

View File

@@ -31,6 +31,7 @@
:location-name="offer.locationName || offer.locationCountry" :location-name="offer.locationName || offer.locationCountry"
:product-name="offer.productName" :product-name="offer.productName"
:price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null" :price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null"
:quantity="offer.quantity"
:currency="offer.currency" :currency="offer.currency"
:unit="offer.unit" :unit="offer.unit"
:stages="[]" :stages="[]"

View File

@@ -3,6 +3,8 @@
"labels": { "labels": {
"quantity_with_unit": "{quantity} {unit}", "quantity_with_unit": "{quantity} {unit}",
"default_unit": "t", "default_unit": "t",
"unit_kg": "kg",
"distance_km": "{km} km",
"country_unknown": "Not specified", "country_unknown": "Not specified",
"supplier_unknown": "Supplier", "supplier_unknown": "Supplier",
"origin_label": "From", "origin_label": "From",

View File

@@ -3,6 +3,8 @@
"labels": { "labels": {
"quantity_with_unit": "{quantity} {unit}", "quantity_with_unit": "{quantity} {unit}",
"default_unit": "т", "default_unit": "т",
"unit_kg": "кг",
"distance_km": "{km} км",
"country_unknown": "Не указана", "country_unknown": "Не указана",
"supplier_unknown": "Поставщик", "supplier_unknown": "Поставщик",
"origin_label": "Откуда", "origin_label": "Откуда",