244 lines
7.8 KiB
Vue
244 lines
7.8 KiB
Vue
<template>
|
|
<Section variant="plain">
|
|
<Stack gap="8">
|
|
<template v-if="hasOrderError">
|
|
<div class="text-sm text-error">
|
|
{{ orderError }}
|
|
</div>
|
|
<Button @click="loadOrder">{{ t('ordersDetail.errors.retry') }}</Button>
|
|
</template>
|
|
|
|
<div v-else-if="isLoadingOrder" class="text-sm text-base-content/60">
|
|
{{ t('ordersDetail.states.loading') }}
|
|
</div>
|
|
|
|
<template v-else>
|
|
<Card padding="lg" class="border border-base-300">
|
|
<RouteSummaryHeader :title="orderTitle" :meta="orderMeta" />
|
|
</Card>
|
|
|
|
<Card v-if="orderRoutesForMap.length" padding="lg" class="border border-base-300">
|
|
<Stack gap="4">
|
|
<RouteStagesList
|
|
:stages="orderStageItems"
|
|
:empty-text="t('ordersDetail.sections.stages.empty')"
|
|
/>
|
|
|
|
<div class="divider my-0"></div>
|
|
|
|
<RequestRoutesMap :routes="orderRoutesForMap" :height="260" />
|
|
</Stack>
|
|
</Card>
|
|
|
|
<div class="space-y-3">
|
|
<Heading :level="3" weight="semibold">{{ t('ordersDetail.sections.timeline.title') }}</Heading>
|
|
<GanttTimeline
|
|
v-if="order?.stages"
|
|
:stages="order.stages"
|
|
:showLoading="showLoading"
|
|
:showUnloading="showUnloading"
|
|
/>
|
|
<Text v-else tone="muted">{{ t('ordersDetail.sections.timeline.empty') }}</Text>
|
|
</div>
|
|
</template>
|
|
</Stack>
|
|
</Section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { GetOrderDocument } from '~/composables/graphql/team/orders-generated'
|
|
import type { RouteStageItem } from '~/components/RouteStagesList.vue'
|
|
|
|
definePageMeta({
|
|
middleware: ['auth-oidc']
|
|
})
|
|
|
|
const route = useRoute()
|
|
const { t } = useI18n()
|
|
|
|
const order = ref<any>(null)
|
|
const isLoadingOrder = ref(true)
|
|
const hasOrderError = ref(false)
|
|
const orderError = ref('')
|
|
const showLoading = ref(true)
|
|
const showUnloading = ref(true)
|
|
|
|
const orderTitle = computed(() => {
|
|
const source = order.value?.sourceLocationName || t('ordersDetail.labels.source_unknown')
|
|
const destination = order.value?.destinationLocationName || t('ordersDetail.labels.destination_unknown')
|
|
return `${source} → ${destination}`
|
|
})
|
|
|
|
const orderMeta = computed(() => {
|
|
const meta: string[] = []
|
|
const orderName = order.value?.name || (route.params.id as string)
|
|
if (orderName) meta.push(`#${orderName}`)
|
|
|
|
const line = order.value?.orderLines?.[0]
|
|
if (line?.quantity) {
|
|
meta.push(`${line.quantity} ${line.unit || t('ordersDetail.labels.unit_tons')}`)
|
|
}
|
|
if (line?.productName) {
|
|
meta.push(line.productName)
|
|
}
|
|
if (order.value?.totalAmount) {
|
|
meta.push(formatPrice(order.value.totalAmount, order.value?.currency))
|
|
}
|
|
|
|
const durationDays = getOrderDuration()
|
|
if (durationDays) {
|
|
meta.push(`${durationDays} ${t('ordersDetail.labels.delivery_days')}`)
|
|
}
|
|
|
|
return meta
|
|
})
|
|
|
|
const orderRoutesForMap = computed(() => {
|
|
const stages = (order.value?.stages || [])
|
|
.filter(Boolean)
|
|
.map((stage: any) => {
|
|
if (stage.stageType === 'transport') {
|
|
if (!stage.sourceLatitude || !stage.sourceLongitude || !stage.destinationLatitude || !stage.destinationLongitude) return null
|
|
return {
|
|
fromLat: stage.sourceLatitude,
|
|
fromLon: stage.sourceLongitude,
|
|
fromName: stage.sourceLocationName,
|
|
toLat: stage.destinationLatitude,
|
|
toLon: stage.destinationLongitude,
|
|
toName: stage.destinationLocationName,
|
|
transportType: stage.transportType
|
|
}
|
|
}
|
|
return null
|
|
})
|
|
.filter(Boolean)
|
|
|
|
if (!stages.length) return []
|
|
return [{ stages }]
|
|
})
|
|
|
|
const orderStageItems = computed<RouteStageItem[]>(() => {
|
|
return (order.value?.stages || []).map((stage: any) => {
|
|
const isTransport = stage.stageType === 'transport'
|
|
const from = isTransport ? stage.sourceLocationName : stage.locationName
|
|
const to = isTransport ? stage.destinationLocationName : stage.locationName
|
|
|
|
const meta: string[] = []
|
|
const dateRange = getStageDateRange(stage)
|
|
if (dateRange) {
|
|
meta.push(dateRange)
|
|
}
|
|
|
|
const companies = getCompaniesSummary(stage)
|
|
companies.forEach((company: any) => {
|
|
meta.push(
|
|
`${company.name} · ${company.totalWeight || 0}${t('ordersDetail.labels.weight_unit')} · ${company.tripsCount || 0} ${t('ordersDetail.labels.trips')}`
|
|
)
|
|
})
|
|
|
|
return {
|
|
key: stage.uuid,
|
|
from,
|
|
to,
|
|
label: stage.name,
|
|
meta
|
|
}
|
|
})
|
|
})
|
|
|
|
const loadOrder = async () => {
|
|
try {
|
|
isLoadingOrder.value = true
|
|
hasOrderError.value = false
|
|
const orderUuid = route.params.id as string
|
|
const { data, error: orderErrorResp } = await useServerQuery('order-detail', GetOrderDocument, { orderUuid }, 'team', 'orders')
|
|
if (orderErrorResp.value) throw orderErrorResp.value
|
|
order.value = data.value?.getOrder
|
|
} catch (err: any) {
|
|
hasOrderError.value = true
|
|
orderError.value = err.message || t('ordersDetail.errors.load_failed')
|
|
} finally {
|
|
isLoadingOrder.value = false
|
|
}
|
|
}
|
|
|
|
const formatPrice = (price: number, currency?: string | null) => {
|
|
if (!price) return t('ordersDetail.labels.price_zero')
|
|
return new Intl.NumberFormat('ru-RU', {
|
|
style: 'currency',
|
|
currency: currency || 'RUB',
|
|
minimumFractionDigits: 0
|
|
}).format(price)
|
|
}
|
|
|
|
const getCompaniesSummary = (stage: any) => {
|
|
const companies = []
|
|
if (stage.stageType === 'service' && stage.selectedCompany) {
|
|
companies.push({
|
|
name: stage.selectedCompany.name,
|
|
totalWeight: 0,
|
|
tripsCount: 0,
|
|
company: stage.selectedCompany
|
|
})
|
|
return companies
|
|
}
|
|
|
|
if (stage.stageType === 'transport' && stage.trips?.length) {
|
|
const companiesMap = new Map()
|
|
stage.trips.forEach((trip: any) => {
|
|
const companyName = trip.company?.name || t('ordersDetail.labels.company_unknown')
|
|
const weight = trip.plannedWeight || 0
|
|
if (companiesMap.has(companyName)) {
|
|
const existing = companiesMap.get(companyName)
|
|
existing.totalWeight += weight
|
|
existing.tripsCount += 1
|
|
} else {
|
|
companiesMap.set(companyName, {
|
|
name: companyName,
|
|
totalWeight: weight,
|
|
tripsCount: 1,
|
|
company: trip.company
|
|
})
|
|
}
|
|
})
|
|
return Array.from(companiesMap.values())
|
|
}
|
|
return []
|
|
}
|
|
|
|
const getOrderDuration = () => {
|
|
if (!order.value?.stages?.length) return 0
|
|
let minDate: Date | null = null
|
|
let maxDate: Date | null = null
|
|
order.value.stages.forEach((stage: any) => {
|
|
stage.trips?.forEach((trip: any) => {
|
|
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate)
|
|
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate)
|
|
if (!minDate || startDate < minDate) minDate = startDate
|
|
if (!maxDate || endDate > maxDate) maxDate = endDate
|
|
})
|
|
})
|
|
if (!minDate || !maxDate) return 0
|
|
const diffTime = Math.abs(maxDate.getTime() - minDate.getTime())
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|
}
|
|
|
|
const getStageDateRange = (stage: any) => {
|
|
if (!stage.trips?.length) return t('ordersDetail.labels.dates_undefined')
|
|
let minDate: Date | null = null
|
|
let maxDate: Date | null = null
|
|
stage.trips.forEach((trip: any) => {
|
|
const startDate = new Date(trip.plannedLoadingDate || trip.actualLoadingDate)
|
|
const endDate = new Date(trip.plannedUnloadingDate || trip.actualUnloadingDate)
|
|
if (!minDate || startDate < minDate) minDate = startDate
|
|
if (!maxDate || endDate > maxDate) maxDate = endDate
|
|
})
|
|
if (!minDate || !maxDate) return t('ordersDetail.labels.dates_undefined')
|
|
const formatDate = (date: Date) => date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })
|
|
if (minDate.toDateString() === maxDate.toDateString()) return formatDate(minDate)
|
|
return `${formatDate(minDate)} - ${formatDate(maxDate)}`
|
|
}
|
|
|
|
await loadOrder()
|
|
</script>
|