Files
webapp/app/pages/manager/orders/[id].vue
2026-04-11 08:31:34 +07:00

244 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import {
GetOrderDocument,
type GetOrderQueryResult,
type GetOrderQueryVariables,
} from '~/composables/graphql/team/orders-generated'
definePageMeta({
layout: 'manager',
middleware: ['auth-oidc'],
})
type OrderRecord = NonNullable<GetOrderQueryResult['getOrder']>
type OrderStage = NonNullable<NonNullable<OrderRecord['stages']>[number]>
type OrderLine = NonNullable<NonNullable<OrderRecord['orderLines']>[number]>
type StageTrip = NonNullable<NonNullable<OrderStage['trips']>[number]>
const route = useRoute()
const localePath = useLocalePath()
const { execute } = useGraphQL()
const { locale } = useI18n()
const orderId = computed(() => String(route.params.id || '').trim())
const {
data,
pending,
error,
} = await useAsyncData(
() => `manager-order-${orderId.value}`,
async () => {
const response = await execute(GetOrderDocument, {
orderUuid: orderId.value,
} as GetOrderQueryVariables, 'team', 'orders')
return response.getOrder
},
)
const order = computed(() => data.value || null)
const stages = computed(() => ((order.value?.stages || []).filter((stage): stage is OrderStage => stage !== null)))
const lines = computed(() => ((order.value?.orderLines || []).filter((line): line is OrderLine => line !== null)))
const trips = computed(() => stages.value.flatMap(stage => (stage.trips || []).filter((trip): trip is StageTrip => trip !== null)))
function formatMoney(value?: number | null, currency?: string | null) {
return new Intl.NumberFormat(locale.value === 'ru' ? 'ru-RU' : 'en-US', {
style: 'currency',
currency: currency || 'USD',
maximumFractionDigits: 0,
}).format(Number(value || 0))
}
function formatDate(value?: string | null) {
if (!value) return 'Дата уточняется'
return new Intl.DateTimeFormat(locale.value === 'ru' ? 'ru-RU' : 'en-US', {
day: '2-digit',
month: 'short',
year: 'numeric',
}).format(new Date(value))
}
function routeLabel() {
return `${order.value?.sourceLocationName || 'Откуда'} - ${order.value?.destinationLocationName || 'Куда'}`
}
function statusMeta(status?: string | null) {
const normalized = String(status || '').toLowerCase()
if (normalized === 'delivered' || normalized === 'completed') return { label: 'Завершён', className: 'bg-emerald-100 text-emerald-700' }
if (normalized === 'cancelled' || normalized === 'canceled') return { label: 'Отменён', className: 'bg-rose-100 text-rose-700' }
if (normalized === 'in_transit' || normalized === 'processing') return { label: 'В пути', className: 'bg-sky-100 text-sky-700' }
return { label: 'В работе', className: 'bg-amber-100 text-amber-700' }
}
function tripDateLabel(trip: StageTrip) {
return trip.actualLoadingDate || trip.realLoadingDate || trip.plannedLoadingDate || trip.plannedUnloadingDate
}
</script>
<template>
<div>
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<NuxtLink :to="localePath('/manager/orders')" class="inline-flex items-center gap-2 text-sm font-bold text-[#8a7761] transition hover:text-[#2f2418]">
<Icon name="lucide:arrow-left" size="16" />
<span>Назад к списку</span>
</NuxtLink>
<p class="mt-4 text-xs font-bold uppercase tracking-[0.16em] text-[#8c7b67]">Order detail</p>
<h2 class="mt-1 text-3xl font-black text-[#2f2418]">{{ routeLabel() }}</h2>
<p class="mt-2 text-sm text-[#6f6353]">Заказ {{ order?.name || order?.uuid || orderId }}</p>
</div>
<div v-if="order" class="flex flex-wrap items-center gap-3">
<span class="inline-flex rounded-full px-3 py-1 text-xs font-semibold" :class="statusMeta(order.status).className">
{{ statusMeta(order.status).label }}
</span>
<div class="rounded-full bg-white px-4 py-2 text-sm font-bold text-[#2f2418]">
{{ formatMoney(order.totalAmount, order.currency) }}
</div>
</div>
</div>
<section v-if="pending" class="mt-6 rounded-[28px] bg-white p-8 text-center">
<p class="text-sm opacity-70">Загружаем заказ</p>
</section>
<section v-else-if="error || !order" class="mt-6 rounded-[28px] bg-rose-50/92 p-6 text-rose-700">
<p class="text-sm font-medium">Не удалось открыть заказ</p>
<p v-if="error" class="mt-2 text-sm opacity-80">{{ error.message }}</p>
</section>
<template v-else>
<section class="mt-6 grid gap-3 lg:grid-cols-4">
<article class="rounded-[28px] bg-white p-5">
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Created</p>
<p class="mt-3 text-lg font-black text-[#2f2418]">{{ formatDate(order.createdAt) }}</p>
</article>
<article class="rounded-[28px] bg-white p-5">
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Stages</p>
<p class="mt-3 text-3xl font-black text-[#2f2418]">{{ stages.length }}</p>
</article>
<article class="rounded-[28px] bg-white p-5">
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Trips</p>
<p class="mt-3 text-3xl font-black text-[#2f2418]">{{ trips.length }}</p>
</article>
<article class="rounded-[28px] bg-white p-5">
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Cargo lines</p>
<p class="mt-3 text-3xl font-black text-[#2f2418]">{{ lines.length }}</p>
</article>
</section>
<section class="mt-6 grid gap-4 xl:grid-cols-[minmax(0,1.3fr)_minmax(0,0.9fr)]">
<article class="rounded-[28px] bg-white p-6">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Cargo manifest</p>
<h3 class="mt-2 text-2xl font-black text-[#2f2418]">Order lines</h3>
</div>
<div class="rounded-full bg-[#f6f1ea] px-3 py-1 text-xs font-bold uppercase tracking-[0.12em] text-[#8a7761]">
{{ lines.length }} items
</div>
</div>
<div class="mt-5 overflow-hidden rounded-[24px] border border-[#eadfce]">
<div class="grid grid-cols-[minmax(0,1.7fr)_120px_120px] gap-3 bg-[#fbf8f4] px-4 py-3 text-xs font-bold uppercase tracking-[0.12em] text-[#8c7b67]">
<span>Product</span>
<span>Qty</span>
<span>Subtotal</span>
</div>
<div v-if="lines.length">
<div
v-for="line in lines"
:key="line.uuid"
class="grid grid-cols-[minmax(0,1.7fr)_120px_120px] gap-3 border-t border-[#f1e7da] px-4 py-4 text-sm text-[#4f4130]"
>
<div>
<p class="font-bold text-[#2f2418]">{{ line.productName || 'Cargo item' }}</p>
<p class="mt-1 text-xs text-[#8a7761]">{{ line.uuid }}</p>
</div>
<span>{{ line.quantity || 0 }} {{ line.unit || '' }}</span>
<span>{{ formatMoney(line.subtotal, order.currency) }}</span>
</div>
</div>
<div v-else class="px-4 py-6 text-sm text-[#6f6353]">
Состав груза ещё не заполнен.
</div>
</div>
</article>
<article class="rounded-[28px] bg-white p-6">
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Manager note</p>
<h3 class="mt-2 text-2xl font-black text-[#2f2418]">Summary</h3>
<p class="mt-4 text-sm leading-6 text-[#5f4b33]">
{{ order.notes || 'Комментарий пока не добавлен. Этот экран уже перенесён в manager-паттерн logistics и теперь опирается на Optovia GraphQL.' }}
</p>
<div class="mt-6 space-y-3 rounded-[24px] bg-[#fbf8f4] p-4">
<div class="flex items-center justify-between gap-3 text-sm">
<span class="text-[#8a7761]">Source</span>
<span class="font-bold text-[#2f2418]">{{ order.sourceLocationName || 'Не указано' }}</span>
</div>
<div class="flex items-center justify-between gap-3 text-sm">
<span class="text-[#8a7761]">Destination</span>
<span class="font-bold text-[#2f2418]">{{ order.destinationLocationName || 'Не указано' }}</span>
</div>
<div class="flex items-center justify-between gap-3 text-sm">
<span class="text-[#8a7761]">Status</span>
<span class="font-bold text-[#2f2418]">{{ statusMeta(order.status).label }}</span>
</div>
</div>
</article>
</section>
<section class="mt-6 rounded-[28px] bg-white p-6">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Route flow</p>
<h3 class="mt-2 text-2xl font-black text-[#2f2418]">Stages and trips</h3>
</div>
<div class="rounded-full bg-[#f6f1ea] px-3 py-1 text-xs font-bold uppercase tracking-[0.12em] text-[#8a7761]">
{{ stages.length }} stages
</div>
</div>
<div class="mt-5 space-y-3">
<article
v-for="stage in stages"
:key="stage.uuid"
class="rounded-[24px] border border-[#eadfce] bg-[#fbf8f4] p-5"
>
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p class="text-xs font-bold uppercase tracking-[0.12em] text-[#8c7b67]">{{ stage.stageType || 'Stage' }}</p>
<h4 class="mt-1 text-xl font-black text-[#2f2418]">{{ stage.name || 'Этап маршрута' }}</h4>
<p class="mt-2 text-sm text-[#5f4b33]">
{{ stage.sourceLocationName || 'Источник' }} {{ stage.destinationLocationName || stage.locationName || 'Точка назначения' }}
</p>
</div>
<div class="flex flex-wrap gap-2">
<span v-if="stage.transportType" class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-[#5f4b33]">
{{ stage.transportType }}
</span>
<span v-if="stage.selectedCompany?.name" class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-[#5f4b33]">
{{ stage.selectedCompany.name }}
</span>
</div>
</div>
<div v-if="(stage.trips || []).length" class="mt-4 grid gap-3 md:grid-cols-2">
<div
v-for="trip in stage.trips"
:key="trip?.uuid"
class="rounded-[20px] bg-white px-4 py-4"
>
<p class="text-sm font-bold text-[#2f2418]">{{ trip?.name || 'Trip' }}</p>
<p class="mt-1 text-xs text-[#8a7761]">{{ trip?.company?.name || trip?.company?.country || 'Carrier pending' }}</p>
<p class="mt-3 text-sm text-[#5f4b33]">{{ formatDate(tripDateLabel(trip || {} as StageTrip)) }}</p>
</div>
</div>
</article>
</div>
</section>
</template>
</div>
</template>