Enrich offer card origin, price, and duration
All checks were successful
Build Docker Image / build (push) Successful in 4m40s

This commit is contained in:
Ruslan Bakiev
2026-02-06 17:37:58 +07:00
parent e4f81dba7c
commit 675f46a75e
5 changed files with 83 additions and 11 deletions

View File

@@ -28,6 +28,7 @@
: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)"
:total-time-seconds="option.routes?.[0]?.totalTimeSeconds ?? null"
:kyc-profile-uuid="getKycProfileUuid(option.sourceUuid)" :kyc-profile-uuid="getKycProfileUuid(option.sourceUuid)"
@select="navigateToOffer(option.sourceUuid)" @select="navigateToOffer(option.sourceUuid)"
/> />
@@ -209,7 +210,9 @@ const getRouteStages = (option: ProductRouteOption) => {
.filter((stage): stage is NonNullable<RouteStageType> => stage !== null) .filter((stage): stage is NonNullable<RouteStageType> => stage !== null)
.map((stage) => ({ .map((stage) => ({
transportType: stage.transportType, transportType: stage.transportType,
distanceKm: stage.distanceKm distanceKm: stage.distanceKm,
travelTimeSeconds: stage.travelTimeSeconds,
fromName: stage.fromName
})) }))
} }

View File

@@ -160,13 +160,14 @@
v-for="(offer, index) in offersWithPrice" v-for="(offer, index) in offersWithPrice"
:key="offer.uuid ?? index" :key="offer.uuid ?? index"
:supplier-name="offer.supplierName" :supplier-name="offer.supplierName"
:location-name="offer.locationName || offer.locationCountry || offer.locationName" :location-name="offer.locationName || offer.locationCountry || offer.country || 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" :quantity="offer.quantity"
:currency="offer.currency" :currency="offer.currency"
:unit="offer.unit" :unit="offer.unit"
:stages="getOfferStages(offer)" :stages="getOfferStages(offer)"
:total-time-seconds="offer.routes?.[0]?.totalTimeSeconds ?? null"
@select="onOfferSelect(offer)" @select="onOfferSelect(offer)"
/> />
</div> </div>
@@ -465,7 +466,9 @@ const getOfferStages = (offer: InfoOfferItem) => {
.filter((stage): stage is NonNullable<RouteStageType> => stage !== null) .filter((stage): stage is NonNullable<RouteStageType> => stage !== null)
.map(stage => ({ .map(stage => ({
transportType: stage.transportType, transportType: stage.transportType,
distanceKm: stage.distanceKm distanceKm: stage.distanceKm,
travelTimeSeconds: stage.travelTimeSeconds,
fromName: stage.fromName
})) }))
} }
</script> </script>

View File

@@ -26,9 +26,14 @@
</div> </div>
</div> </div>
<Text v-if="priceDisplay" weight="semibold" class="text-primary text-lg text-right"> <div class="text-right">
{{ priceDisplay }} <Text v-if="priceDisplay" weight="semibold" class="text-primary text-lg">
</Text> {{ priceDisplay }}
</Text>
<Text v-if="durationDisplay" size="xs" class="text-base-content/60">
{{ t('catalogOfferCard.labels.duration_label') }} {{ durationDisplay }}
</Text>
</div>
</div> </div>
<!-- Supplier info --> <!-- Supplier info -->
@@ -45,7 +50,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { RouteStage } from './RouteStepper.vue' interface RouteStage {
transportType?: string | null
distanceKm?: number | null
travelTimeSeconds?: number | null
fromName?: string | null
}
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
locationName?: string locationName?: string
@@ -56,8 +66,7 @@ const props = withDefaults(defineProps<{
currency?: string | null currency?: string | null
unit?: string | null unit?: string | null
stages?: RouteStage[] stages?: RouteStage[]
startName?: string totalTimeSeconds?: number | null
endName?: string
kycProfileUuid?: string | null kycProfileUuid?: string | null
}>(), { }>(), {
stages: () => [] stages: () => []
@@ -74,14 +83,17 @@ const supplierDisplay = computed(() => {
}) })
const originDisplay = computed(() => { const originDisplay = computed(() => {
return props.locationName || t('catalogOfferCard.labels.origin_unknown') const fromStage = props.stages?.find(stage => stage?.fromName)?.fromName
return props.locationName || fromStage || t('catalogOfferCard.labels.origin_unknown')
}) })
const priceDisplay = computed(() => { const priceDisplay = computed(() => {
if (props.pricePerUnit == null) 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 = Number(props.pricePerUnit).toLocaleString() const basePrice = Number(props.pricePerUnit)
const totalPrice = basePrice + (logisticsCost.value ?? 0)
const formattedPrice = totalPrice.toLocaleString()
return `${currSymbol}${formattedPrice}/${unitName}` return `${currSymbol}${formattedPrice}/${unitName}`
}) })
@@ -128,6 +140,21 @@ const formatDistance = (km?: number | null) => {
return t('catalogOfferCard.labels.distance_km', { km: formatted }) return t('catalogOfferCard.labels.distance_km', { km: formatted })
} }
const formatDuration = (seconds?: number | null) => {
if (!seconds) return null
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours >= 24) {
const days = Math.floor(hours / 24)
const remainingHours = hours % 24
return `${days}${t('catalogOfferCard.labels.time_days_short')} ${remainingHours}${t('catalogOfferCard.labels.time_hours_short')}`
}
if (hours > 0) {
return `${hours}${t('catalogOfferCard.labels.time_hours_short')} ${minutes}${t('catalogOfferCard.labels.time_minutes_short')}`
}
return `${minutes}${t('catalogOfferCard.labels.time_minutes_short')}`
}
const getTransportIcon = (type?: string | null) => { const getTransportIcon = (type?: string | null) => {
switch (type) { switch (type) {
case 'rail': case 'rail':
@@ -141,6 +168,37 @@ const getTransportIcon = (type?: string | null) => {
} }
} }
const getTransportRate = (type?: string | null) => {
switch (type) {
case 'rail':
return 0.18
case 'sea':
return 0.12
case 'road':
case 'auto':
default:
return 0.35
}
}
const logisticsCost = computed(() => {
if (!props.stages?.length) return null
return props.stages.reduce((sum, stage) => {
const km = stage?.distanceKm
if (km == null) return sum
return sum + km * getTransportRate(stage?.transportType)
}, 0)
})
const totalTimeSeconds = computed(() => {
if (props.totalTimeSeconds != null) return props.totalTimeSeconds
if (!props.stages?.length) return null
const sum = props.stages.reduce((acc, stage) => acc + (stage?.travelTimeSeconds || 0), 0)
return sum > 0 ? sum : null
})
const durationDisplay = computed(() => formatDuration(totalTimeSeconds.value))
const routeRows = computed(() => const routeRows = computed(() =>
(props.stages || []) (props.stages || [])
.filter(stage => stage?.distanceKm != null) .filter(stage => stage?.distanceKm != null)

View File

@@ -5,6 +5,10 @@
"default_unit": "t", "default_unit": "t",
"unit_kg": "kg", "unit_kg": "kg",
"distance_km": "{km} km", "distance_km": "{km} km",
"duration_label": "ETA",
"time_days_short": "d",
"time_hours_short": "h",
"time_minutes_short": "m",
"country_unknown": "Not specified", "country_unknown": "Not specified",
"supplier_unknown": "Supplier", "supplier_unknown": "Supplier",
"origin_label": "From", "origin_label": "From",

View File

@@ -5,6 +5,10 @@
"default_unit": "т", "default_unit": "т",
"unit_kg": "кг", "unit_kg": "кг",
"distance_km": "{km} км", "distance_km": "{km} км",
"duration_label": "Срок",
"time_days_short": "д",
"time_hours_short": "ч",
"time_minutes_short": "м",
"country_unknown": "Не указана", "country_unknown": "Не указана",
"supplier_unknown": "Поставщик", "supplier_unknown": "Поставщик",
"origin_label": "Откуда", "origin_label": "Откуда",