Add mock order status timeline
This commit is contained in:
85
app/components/orders/OrderStatusTimelineCard.vue
Normal file
85
app/components/orders/OrderStatusTimelineCard.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
|
||||||
|
import { getOrderStatusPresentation } from '~/composables/useOrderStatusPresentation';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
status: string;
|
||||||
|
createdAt: string | Date;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
|
const presentation = computed(() => getOrderStatusPresentation(props.status, props.createdAt));
|
||||||
|
|
||||||
|
function itemClass(state: 'done' | 'current' | 'upcoming') {
|
||||||
|
if (state === 'current') {
|
||||||
|
return 'border-[#139957] bg-[#eef9f2]';
|
||||||
|
}
|
||||||
|
if (state === 'done') {
|
||||||
|
return 'border-[#d8eadf] bg-white';
|
||||||
|
}
|
||||||
|
return 'border-[#e4ede8] bg-[#f7faf8]';
|
||||||
|
}
|
||||||
|
|
||||||
|
function markerClass(state: 'done' | 'current' | 'upcoming') {
|
||||||
|
if (state === 'current') {
|
||||||
|
return 'bg-[#139957] ring-4 ring-[#dff4e8]';
|
||||||
|
}
|
||||||
|
if (state === 'done') {
|
||||||
|
return 'bg-[#9dcfb0]';
|
||||||
|
}
|
||||||
|
return 'bg-[#d8e4dd]';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="surface-card rounded-3xl p-5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-start justify-between gap-4 text-left"
|
||||||
|
@click="isExpanded = !isExpanded"
|
||||||
|
>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-[#5c7b69]">Текущий статус</p>
|
||||||
|
<h2 class="text-2xl font-black leading-tight text-[#123824]">
|
||||||
|
{{ presentation.title }}
|
||||||
|
</h2>
|
||||||
|
<p class="max-w-2xl text-sm leading-6 text-[#355947]">
|
||||||
|
{{ presentation.summary }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 pt-1">
|
||||||
|
<OrderStatusBadge :status="status" />
|
||||||
|
<span
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full bg-[#eef5f0] text-[#123824] transition-transform"
|
||||||
|
:class="{ 'rotate-180': isExpanded }"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="none">
|
||||||
|
<path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="isExpanded" class="mt-5 space-y-3 border-t border-[#e1ebe5] pt-5">
|
||||||
|
<div
|
||||||
|
v-for="stage in presentation.stages"
|
||||||
|
:key="stage.code"
|
||||||
|
class="rounded-3xl border p-4"
|
||||||
|
:class="itemClass(stage.state)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span class="mt-1 h-3 w-3 shrink-0 rounded-full" :class="markerClass(stage.state)" />
|
||||||
|
<div class="min-w-0 flex-1 space-y-1">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<p class="text-sm font-semibold text-[#123824]">{{ stage.label }}</p>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.12em] text-[#5c7b69]">{{ stage.dateLabel }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm leading-6 text-[#355947]">{{ stage.note }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
213
app/composables/useOrderStatusPresentation.ts
Normal file
213
app/composables/useOrderStatusPresentation.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
type OrderStatusCode =
|
||||||
|
| 'NEW'
|
||||||
|
| 'MANAGER_PROCESSING'
|
||||||
|
| 'WAITING_DOUBLE_CONFIRM'
|
||||||
|
| 'CLIENT_REJECTED'
|
||||||
|
| 'MANAGER_REJECTED'
|
||||||
|
| 'MANAGER_BLOCKED'
|
||||||
|
| 'CONFIRMED'
|
||||||
|
| 'IN_PROGRESS'
|
||||||
|
| 'COMPLETED';
|
||||||
|
|
||||||
|
type TimelineStage = {
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
note: string;
|
||||||
|
dateLabel: string;
|
||||||
|
state: 'done' | 'current' | 'upcoming';
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatusPresentation = {
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
stages: TimelineStage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const STAGE_ORDER: OrderStatusCode[] = [
|
||||||
|
'NEW',
|
||||||
|
'MANAGER_PROCESSING',
|
||||||
|
'WAITING_DOUBLE_CONFIRM',
|
||||||
|
'CONFIRMED',
|
||||||
|
'IN_PROGRESS',
|
||||||
|
'COMPLETED',
|
||||||
|
];
|
||||||
|
|
||||||
|
const DAY_FORMATTER = new Intl.DateTimeFormat('ru-RU', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
});
|
||||||
|
|
||||||
|
function addDays(date: Date, days: number) {
|
||||||
|
const next = new Date(date);
|
||||||
|
next.setDate(next.getDate() + days);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDay(date: Date) {
|
||||||
|
return DAY_FORMATTER.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stageIndex(status: string) {
|
||||||
|
const index = STAGE_ORDER.indexOf(status as OrderStatusCode);
|
||||||
|
return index >= 0 ? index : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDates(createdAt: string | Date) {
|
||||||
|
const base = new Date(createdAt);
|
||||||
|
|
||||||
|
return {
|
||||||
|
created: base,
|
||||||
|
offer: addDays(base, 1),
|
||||||
|
approval: addDays(base, 2),
|
||||||
|
production: addDays(base, 4),
|
||||||
|
shipment: addDays(base, 6),
|
||||||
|
delivered: addDays(base, 8),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrderStatusPresentation(status: string, createdAt: string | Date): StatusPresentation {
|
||||||
|
const dates = buildDates(createdAt);
|
||||||
|
|
||||||
|
if (status === 'CLIENT_REJECTED') {
|
||||||
|
return {
|
||||||
|
title: 'Заказ остановлен клиентом',
|
||||||
|
summary: 'Согласование остановлено. Если нужно, заказ можно собрать заново с новыми условиями.',
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
code: 'NEW',
|
||||||
|
label: 'Заявка принята',
|
||||||
|
note: 'Заказ попал в обработку.',
|
||||||
|
dateLabel: formatDay(dates.created),
|
||||||
|
state: 'done',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CLIENT_REJECTED',
|
||||||
|
label: 'Клиент отказался от продолжения',
|
||||||
|
note: 'Текущий заказ закрыт без запуска в работу.',
|
||||||
|
dateLabel: formatDay(dates.approval),
|
||||||
|
state: 'current',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'MANAGER_REJECTED' || status === 'MANAGER_BLOCKED') {
|
||||||
|
return {
|
||||||
|
title: status === 'MANAGER_BLOCKED' ? 'Заказ ждёт уточнения' : 'Заказ остановлен менеджером',
|
||||||
|
summary: status === 'MANAGER_BLOCKED'
|
||||||
|
? 'Сейчас ждём уточнение параметров, после него заказ вернётся в работу.'
|
||||||
|
: 'Менеджер остановил обработку. При необходимости можно собрать новый заказ.',
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
code: 'NEW',
|
||||||
|
label: 'Заявка принята',
|
||||||
|
note: 'Заказ попал в обработку.',
|
||||||
|
dateLabel: formatDay(dates.created),
|
||||||
|
state: 'done',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: status,
|
||||||
|
label: status === 'MANAGER_BLOCKED' ? 'Нужно уточнение по заказу' : 'Обработка остановлена',
|
||||||
|
note: status === 'MANAGER_BLOCKED'
|
||||||
|
? 'Менеджер запросил уточнение перед продолжением.'
|
||||||
|
: 'Текущий заказ завершён без запуска в производство.',
|
||||||
|
dateLabel: formatDay(dates.approval),
|
||||||
|
state: 'current',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = stageIndex(status);
|
||||||
|
|
||||||
|
const stages: TimelineStage[] = [
|
||||||
|
{
|
||||||
|
code: 'NEW',
|
||||||
|
label: 'Заявка принята',
|
||||||
|
note: 'Получили состав заказа и начали обработку.',
|
||||||
|
dateLabel: formatDay(dates.created),
|
||||||
|
state: currentIndex > 0 ? 'done' : 'current',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'MANAGER_PROCESSING',
|
||||||
|
label: 'Готовим предложение',
|
||||||
|
note: 'Уточняем цену, сроки и условия доставки.',
|
||||||
|
dateLabel: formatDay(dates.offer),
|
||||||
|
state: currentIndex > 1 ? 'done' : currentIndex === 1 ? 'current' : 'upcoming',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'WAITING_DOUBLE_CONFIRM',
|
||||||
|
label: 'Ждём подтверждение',
|
||||||
|
note: 'Показываем согласованные условия и ждём финальное подтверждение.',
|
||||||
|
dateLabel: formatDay(dates.approval),
|
||||||
|
state: currentIndex > 2 ? 'done' : currentIndex === 2 ? 'current' : 'upcoming',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CONFIRMED',
|
||||||
|
label: 'Заказ подтверждён',
|
||||||
|
note: `Планируем передать в производство и ориентируемся на выпуск ${formatDay(dates.production)}.`,
|
||||||
|
dateLabel: formatDay(dates.production),
|
||||||
|
state: currentIndex > 3 ? 'done' : currentIndex === 3 ? 'current' : 'upcoming',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'IN_PROGRESS',
|
||||||
|
label: 'Заказ в работе',
|
||||||
|
note: `Сейчас идёт исполнение. Следующее ожидаемое обновление около ${formatDay(dates.shipment)}.`,
|
||||||
|
dateLabel: formatDay(dates.shipment),
|
||||||
|
state: currentIndex > 4 ? 'done' : currentIndex === 4 ? 'current' : 'upcoming',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'COMPLETED',
|
||||||
|
label: 'Заказ завершён',
|
||||||
|
note: 'Исполнение закрыто, заказ готов к выдаче или уже доставлен.',
|
||||||
|
dateLabel: formatDay(dates.delivered),
|
||||||
|
state: currentIndex === 5 ? 'current' : currentIndex > 5 ? 'done' : 'upcoming',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (status === 'NEW') {
|
||||||
|
return {
|
||||||
|
title: 'Собираем предложение по заказу',
|
||||||
|
summary: `Сейчас уточняем стоимость и комплектацию. Следующее обновление ориентировочно ${formatDay(dates.offer)}.`,
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'MANAGER_PROCESSING') {
|
||||||
|
return {
|
||||||
|
title: 'Готовим условия по заказу',
|
||||||
|
summary: `Менеджер собирает финальные условия. Ориентир по следующему апдейту ${formatDay(dates.approval)}.`,
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'WAITING_DOUBLE_CONFIRM') {
|
||||||
|
return {
|
||||||
|
title: 'Ожидаем подтверждение условий',
|
||||||
|
summary: 'Нужно подтвердить текущие условия, после этого сразу двинем заказ дальше.',
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'CONFIRMED') {
|
||||||
|
return {
|
||||||
|
title: 'Заказ подтверждён',
|
||||||
|
summary: `Ожидаем выпуск с производства ориентировочно ${formatDay(dates.production)}.`,
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'IN_PROGRESS') {
|
||||||
|
return {
|
||||||
|
title: 'Заказ в работе',
|
||||||
|
summary: `Сейчас заказ исполняется. Следующее ожидаемое обновление около ${formatDay(dates.shipment)}.`,
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'Заказ завершён',
|
||||||
|
summary: 'Работы по заказу завершены, история этапов сохранена ниже.',
|
||||||
|
stages,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||||
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
|
|
||||||
import {
|
import {
|
||||||
BlockOrderDocument,
|
BlockOrderDocument,
|
||||||
CompleteOrderDocument,
|
CompleteOrderDocument,
|
||||||
@@ -137,16 +136,10 @@ async function completeOrder() {
|
|||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-[1.1fr_0.9fr]">
|
<div class="grid gap-4 lg:grid-cols-[1.1fr_0.9fr]">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<OrdersOrderStatusTimelineCard
|
||||||
<h2 class="text-xl font-bold text-[#123824]">Статус заказа</h2>
|
:status="currentOrder.status"
|
||||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-3">
|
:created-at="currentOrder.createdAt"
|
||||||
<div class="space-y-1 text-sm text-[#355947]">
|
/>
|
||||||
<p>Создан: {{ new Date(currentOrder.createdAt).toLocaleString() }}</p>
|
|
||||||
<p>Клиент: {{ currentOrder.customerId }}</p>
|
|
||||||
</div>
|
|
||||||
<OrderStatusBadge :status="currentOrder.status" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<div class="surface-card rounded-3xl p-5">
|
||||||
<h2 class="text-xl font-bold text-[#123824]">Состав заказа</h2>
|
<h2 class="text-xl font-bold text-[#123824]">Состав заказа</h2>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useQuery } from '@vue/apollo-composable';
|
import { useQuery } from '@vue/apollo-composable';
|
||||||
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
|
|
||||||
import {
|
import {
|
||||||
MyOrdersDocument,
|
MyOrdersDocument,
|
||||||
type MyOrdersQuery,
|
type MyOrdersQuery,
|
||||||
@@ -36,15 +35,10 @@ const currentOrder = computed<OrderItem | null>(() =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<OrdersOrderStatusTimelineCard
|
||||||
<h2 class="text-xl font-bold text-[#123824]">Статус заказа</h2>
|
:status="currentOrder.status"
|
||||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-3">
|
:created-at="currentOrder.createdAt"
|
||||||
<div class="space-y-1 text-sm text-[#355947]">
|
/>
|
||||||
<p>Создан: {{ new Date(currentOrder.createdAt).toLocaleString() }}</p>
|
|
||||||
</div>
|
|
||||||
<OrderStatusBadge :status="currentOrder.status" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<div class="surface-card rounded-3xl p-5">
|
||||||
<h2 class="text-xl font-bold text-[#123824]">Состав заказа</h2>
|
<h2 class="text-xl font-bold text-[#123824]">Состав заказа</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user