212 lines
6.5 KiB
Vue
212 lines
6.5 KiB
Vue
<template>
|
|
<Card padding="md" interactive @click="$emit('select')">
|
|
<!-- Header: Supplier + Price -->
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="flex flex-col gap-1">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-6 h-6 rounded-full flex items-center justify-center" style="background-color: #3b82f6">
|
|
<Icon name="lucide:factory" size="14" class="text-white" />
|
|
</div>
|
|
<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 class="text-right">
|
|
<Text v-if="priceDisplay" weight="semibold" class="text-primary text-lg">
|
|
{{ priceDisplay }}
|
|
</Text>
|
|
<Text v-if="durationDisplay" size="xs" class="text-base-content/60">
|
|
{{ t('catalogOfferCard.labels.duration_label') }} {{ durationDisplay }}
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Supplier info -->
|
|
<SupplierInfoBlock v-if="kycProfileUuid" :kyc-profile-uuid="kycProfileUuid" class="mb-3" />
|
|
|
|
<!-- Route lines -->
|
|
<div v-if="routeRows.length" class="mt-3 pt-2 border-t border-base-200/60">
|
|
<div v-for="(row, index) in routeRows" :key="index" class="flex items-center gap-2 text-sm text-base-content/70">
|
|
<Icon :name="row.icon" size="14" class="text-base-content/60" />
|
|
<span>{{ row.distanceLabel }}</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
interface RouteStage {
|
|
transportType?: string | null
|
|
distanceKm?: number | null
|
|
travelTimeSeconds?: number | null
|
|
fromName?: string | null
|
|
}
|
|
|
|
const props = withDefaults(defineProps<{
|
|
locationName?: string
|
|
supplierName?: string
|
|
productName?: string
|
|
pricePerUnit?: number | null
|
|
quantity?: number | string | null
|
|
currency?: string | null
|
|
unit?: string | null
|
|
stages?: RouteStage[]
|
|
totalTimeSeconds?: number | null
|
|
kycProfileUuid?: string | null
|
|
}>(), {
|
|
stages: () => []
|
|
})
|
|
|
|
defineEmits<{
|
|
select: []
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const supplierDisplay = computed(() => {
|
|
return props.supplierName || t('catalogOfferCard.labels.supplier_unknown')
|
|
})
|
|
|
|
const originDisplay = computed(() => {
|
|
const fromStage = props.stages?.find(stage => stage?.fromName)?.fromName
|
|
return props.locationName || fromStage || t('catalogOfferCard.labels.origin_unknown')
|
|
})
|
|
|
|
const priceDisplay = computed(() => {
|
|
if (props.pricePerUnit == null) return null
|
|
const currSymbol = getCurrencySymbol(props.currency)
|
|
const unitName = getUnitName(props.unit)
|
|
const basePrice = Number(props.pricePerUnit)
|
|
const totalPrice = basePrice + (logisticsCost.value ?? 0)
|
|
const formattedPrice = totalPrice.toLocaleString()
|
|
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) => {
|
|
switch (currency?.toUpperCase()) {
|
|
case 'USD': return '$'
|
|
case 'EUR': return '€'
|
|
case 'RUB': return '₽'
|
|
case 'CNY': return '¥'
|
|
default: return '$'
|
|
}
|
|
}
|
|
|
|
const getUnitName = (unit?: string | null) => {
|
|
switch (unit?.toLowerCase()) {
|
|
case 'т':
|
|
case 't':
|
|
case 'ton':
|
|
case 'tonne':
|
|
return t('catalogOfferCard.labels.default_unit')
|
|
case 'кг':
|
|
case 'kg':
|
|
return t('catalogOfferCard.labels.unit_kg')
|
|
default:
|
|
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 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) => {
|
|
switch (type) {
|
|
case 'rail':
|
|
return 'lucide:train-front'
|
|
case 'sea':
|
|
return 'lucide:ship'
|
|
case 'road':
|
|
case 'auto':
|
|
default:
|
|
return 'lucide:truck'
|
|
}
|
|
}
|
|
|
|
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(() =>
|
|
(props.stages || [])
|
|
.filter(stage => stage?.distanceKm != null)
|
|
.map(stage => ({
|
|
icon: getTransportIcon(stage?.transportType),
|
|
distanceLabel: formatDistance(stage?.distanceKm)
|
|
}))
|
|
.filter(row => !!row.distanceLabel)
|
|
)
|
|
</script>
|