Refine order detail status layout
This commit is contained in:
@@ -1,30 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getOrderStatusBadgePresentation,
|
||||
type OrderStatusTone,
|
||||
} from '~/composables/useOrderStatusPresentation';
|
||||
|
||||
const props = defineProps<{
|
||||
status: string;
|
||||
}>();
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (props.status === 'NEW' || props.status === 'MANAGER_PROCESSING') return 'Заявка';
|
||||
if (props.status === 'WAITING_DOUBLE_CONFIRM' || props.status === 'CONFIRMED') return 'Предложение';
|
||||
if (props.status === 'IN_PROGRESS') return 'В работе';
|
||||
if (props.status === 'COMPLETED') return 'Завершен';
|
||||
if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'Отклонен';
|
||||
if (props.status === 'MANAGER_BLOCKED') return 'Пауза';
|
||||
return props.status;
|
||||
});
|
||||
const badgePresentation = computed(() => getOrderStatusBadgePresentation(props.status));
|
||||
|
||||
const className = computed(() => {
|
||||
if (props.status === 'COMPLETED') return 'bg-[#139957]';
|
||||
if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'bg-[#d94b55]';
|
||||
if (props.status === 'MANAGER_BLOCKED') return 'bg-[#f1a43a]';
|
||||
if (props.status === 'NEW' || props.status === 'MANAGER_PROCESSING') return 'bg-[#f1a43a]';
|
||||
function dotClass(tone: OrderStatusTone) {
|
||||
if (tone === 'success') return 'bg-[#139957]';
|
||||
if (tone === 'danger') return 'bg-[#d94b55]';
|
||||
if (tone === 'warning') return 'bg-[#f1a43a]';
|
||||
return 'bg-[#2e8de4]';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="inline-flex items-center gap-2 text-sm font-semibold text-[#123824]">
|
||||
<span class="h-2.5 w-2.5 rounded-full" :class="className" />
|
||||
<span>{{ statusLabel }}</span>
|
||||
<span class="inline-flex items-center gap-2 text-sm font-semibold leading-tight text-[#123824]">
|
||||
<span class="h-2.5 w-2.5 rounded-full" :class="dotClass(badgePresentation.tone)" />
|
||||
<span>{{ badgePresentation.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
|
||||
import { getOrderStatusPresentation } from '~/composables/useOrderStatusPresentation';
|
||||
import {
|
||||
getOrderStatusBadgePresentation,
|
||||
getOrderStatusPresentation,
|
||||
type OrderStatusTone,
|
||||
} from '~/composables/useOrderStatusPresentation';
|
||||
|
||||
const props = defineProps<{
|
||||
status: string;
|
||||
@@ -11,10 +15,53 @@ const props = defineProps<{
|
||||
const isExpanded = ref(false);
|
||||
|
||||
const presentation = computed(() => getOrderStatusPresentation(props.status, props.createdAt, props.audience ?? 'client'));
|
||||
const currentBadge = computed(() => getOrderStatusBadgePresentation(props.status));
|
||||
|
||||
function currentToneClass(tone: OrderStatusTone) {
|
||||
if (tone === 'success') {
|
||||
return {
|
||||
marker: 'bg-[#139957] ring-4 ring-[#dff4e8]',
|
||||
panel: 'bg-[#eef8f2]',
|
||||
title: 'text-[#123824]',
|
||||
note: 'text-[#355947]',
|
||||
date: 'text-[#139957]',
|
||||
connector: 'bg-[#bfe0cb]',
|
||||
};
|
||||
}
|
||||
if (tone === 'danger') {
|
||||
return {
|
||||
marker: 'bg-[#d94b55] ring-4 ring-[#fbe5e7]',
|
||||
panel: 'bg-[#fff1f2]',
|
||||
title: 'text-[#7e2130]',
|
||||
note: 'text-[#9b4150]',
|
||||
date: 'text-[#d94b55]',
|
||||
connector: 'bg-[#f1c6cb]',
|
||||
};
|
||||
}
|
||||
if (tone === 'warning') {
|
||||
return {
|
||||
marker: 'bg-[#f1a43a] ring-4 ring-[#fff0d9]',
|
||||
panel: 'bg-[#fff7eb]',
|
||||
title: 'text-[#6c4303]',
|
||||
note: 'text-[#8f6420]',
|
||||
date: 'text-[#c67d11]',
|
||||
connector: 'bg-[#efd1a2]',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
marker: 'bg-[#2e8de4] ring-4 ring-[#e3effb]',
|
||||
panel: 'bg-[#eef5fc]',
|
||||
title: 'text-[#174b7e]',
|
||||
note: 'text-[#436b92]',
|
||||
date: 'text-[#2e8de4]',
|
||||
connector: 'bg-[#c7dbef]',
|
||||
};
|
||||
}
|
||||
|
||||
function markerClass(state: 'done' | 'current' | 'upcoming') {
|
||||
if (state === 'current') {
|
||||
return 'bg-[#139957] ring-4 ring-[#dff4e8]';
|
||||
return currentToneClass(currentBadge.value.tone).marker;
|
||||
}
|
||||
if (state === 'done') {
|
||||
return 'bg-[#9dcfb0]';
|
||||
@@ -24,7 +71,9 @@ function markerClass(state: 'done' | 'current' | 'upcoming') {
|
||||
|
||||
function connectorClass(state: 'done' | 'current' | 'upcoming') {
|
||||
if (state === 'done' || state === 'current') {
|
||||
return 'bg-[#cfe5d7]';
|
||||
return state === 'current'
|
||||
? currentToneClass(currentBadge.value.tone).connector
|
||||
: 'bg-[#cfe5d7]';
|
||||
}
|
||||
|
||||
return 'bg-[#e4ece7]';
|
||||
@@ -32,7 +81,7 @@ function connectorClass(state: 'done' | 'current' | 'upcoming') {
|
||||
|
||||
function titleClass(state: 'done' | 'current' | 'upcoming') {
|
||||
if (state === 'current') {
|
||||
return 'text-[#123824]';
|
||||
return currentToneClass(currentBadge.value.tone).title;
|
||||
}
|
||||
if (state === 'done') {
|
||||
return 'text-[#355947]';
|
||||
@@ -43,7 +92,7 @@ function titleClass(state: 'done' | 'current' | 'upcoming') {
|
||||
|
||||
function noteClass(state: 'done' | 'current' | 'upcoming') {
|
||||
if (state === 'current') {
|
||||
return 'text-[#355947]';
|
||||
return currentToneClass(currentBadge.value.tone).note;
|
||||
}
|
||||
if (state === 'done') {
|
||||
return 'text-[#557562]';
|
||||
@@ -51,6 +100,28 @@ function noteClass(state: 'done' | 'current' | 'upcoming') {
|
||||
|
||||
return 'text-[#7d9688]';
|
||||
}
|
||||
|
||||
function stagePanelClass(state: 'done' | 'current' | 'upcoming') {
|
||||
if (state === 'current') {
|
||||
return currentToneClass(currentBadge.value.tone).panel;
|
||||
}
|
||||
if (state === 'done') {
|
||||
return 'bg-[#f3f7f4]';
|
||||
}
|
||||
|
||||
return 'bg-[#f7faf8]';
|
||||
}
|
||||
|
||||
function dateClass(state: 'done' | 'current' | 'upcoming') {
|
||||
if (state === 'current') {
|
||||
return currentToneClass(currentBadge.value.tone).date;
|
||||
}
|
||||
if (state === 'done') {
|
||||
return 'text-[#5c7b69]';
|
||||
}
|
||||
|
||||
return 'text-[#86a091]';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -60,17 +131,19 @@ function noteClass(state: 'done' | 'current' | 'upcoming') {
|
||||
class="flex w-full items-start justify-between gap-4 text-left"
|
||||
@click="isExpanded = !isExpanded"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h2 class="text-2xl font-black leading-tight text-[#123824]">
|
||||
{{ presentation.title }}
|
||||
</h2>
|
||||
<OrderStatusBadge :status="status" />
|
||||
</div>
|
||||
<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-[#f2f5f3] text-[#123824] transition-transform"
|
||||
:class="{ 'rotate-180': isExpanded }"
|
||||
@@ -97,15 +170,16 @@ function noteClass(state: 'done' | 'current' | 'upcoming') {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1 pb-5">
|
||||
<div
|
||||
class="min-w-0 flex-1 pb-5"
|
||||
:class="index < presentation.stages.length - 1 ? 'border-b border-[#e1ebe4]' : ''"
|
||||
class="rounded-[22px] px-4 py-4 transition-colors"
|
||||
:class="stagePanelClass(stage.state)"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<p class="text-sm font-semibold" :class="titleClass(stage.state)">
|
||||
{{ stage.label }}
|
||||
</p>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.12em] text-[#5c7b69]">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.12em]" :class="dateClass(stage.state)">
|
||||
{{ stage.dateLabel }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -117,4 +191,5 @@ function noteClass(state: 'done' | 'current' | 'upcoming') {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,6 +9,8 @@ type OrderStatusCode =
|
||||
| 'IN_PROGRESS'
|
||||
| 'COMPLETED';
|
||||
|
||||
export type OrderStatusTone = 'warning' | 'info' | 'success' | 'danger';
|
||||
|
||||
type TimelineStage = {
|
||||
code: string;
|
||||
label: string;
|
||||
@@ -17,6 +19,11 @@ type TimelineStage = {
|
||||
state: 'done' | 'current' | 'upcoming';
|
||||
};
|
||||
|
||||
type StatusBadgePresentation = {
|
||||
label: string;
|
||||
tone: OrderStatusTone;
|
||||
};
|
||||
|
||||
type StatusPresentation = {
|
||||
title: string;
|
||||
summary: string;
|
||||
@@ -37,6 +44,18 @@ const DAY_FORMATTER = new Intl.DateTimeFormat('ru-RU', {
|
||||
month: 'long',
|
||||
});
|
||||
|
||||
const STATUS_BADGE_MAP: Record<string, StatusBadgePresentation> = {
|
||||
NEW: { label: 'Заявка', tone: 'warning' },
|
||||
MANAGER_PROCESSING: { label: 'Готовим предложение', tone: 'warning' },
|
||||
WAITING_DOUBLE_CONFIRM: { label: 'Предложение', tone: 'info' },
|
||||
CLIENT_REJECTED: { label: 'Отклонен', tone: 'danger' },
|
||||
MANAGER_REJECTED: { label: 'Отклонен', tone: 'danger' },
|
||||
MANAGER_BLOCKED: { label: 'Пауза', tone: 'warning' },
|
||||
CONFIRMED: { label: 'Производство', tone: 'info' },
|
||||
IN_PROGRESS: { label: 'Отгрузка', tone: 'success' },
|
||||
COMPLETED: { label: 'Доставка', tone: 'success' },
|
||||
};
|
||||
|
||||
function addDays(date: Date, days: number) {
|
||||
const next = new Date(date);
|
||||
next.setDate(next.getDate() + days);
|
||||
@@ -149,14 +168,14 @@ function buildClientStages(status: string, createdAt: string | Date): StatusPres
|
||||
stages: [
|
||||
{
|
||||
code: 'NEW',
|
||||
label: 'Заказ создан',
|
||||
label: 'Заявка',
|
||||
note: 'Заказ принят в обработку.',
|
||||
dateLabel: formatDay(dates.created),
|
||||
state: 'done',
|
||||
},
|
||||
{
|
||||
code: status,
|
||||
label: 'Заказ остановлен',
|
||||
label: 'Отклонен',
|
||||
note: 'Дальнейшее исполнение не планируется.',
|
||||
dateLabel: formatDay(dates.approval),
|
||||
state: 'current',
|
||||
@@ -172,14 +191,14 @@ function buildClientStages(status: string, createdAt: string | Date): StatusPres
|
||||
stages: [
|
||||
{
|
||||
code: 'NEW',
|
||||
label: 'Заказ создан',
|
||||
label: 'Заявка',
|
||||
note: 'Заказ принят в обработку.',
|
||||
dateLabel: formatDay(dates.created),
|
||||
state: 'done',
|
||||
},
|
||||
{
|
||||
code: status,
|
||||
label: 'Уточняем детали',
|
||||
label: 'Пауза',
|
||||
note: 'После уточнения покажем плановые даты по исполнению.',
|
||||
dateLabel: formatDay(dates.approval),
|
||||
state: 'current',
|
||||
@@ -193,31 +212,45 @@ function buildClientStages(status: string, createdAt: string | Date): StatusPres
|
||||
const stages: TimelineStage[] = [
|
||||
{
|
||||
code: 'NEW',
|
||||
label: 'Заказ создан',
|
||||
label: 'Заявка',
|
||||
note: 'Приняли заказ и начали обработку.',
|
||||
dateLabel: formatDay(dates.created),
|
||||
state: currentIndex > 0 ? 'done' : 'current',
|
||||
state: currentIndex > 0 ? 'done' : currentIndex === 0 ? 'current' : 'upcoming',
|
||||
},
|
||||
{
|
||||
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: 'Плановая дата запуска или выхода из производства.',
|
||||
dateLabel: formatDay(dates.production),
|
||||
state: currentIndex > 3 ? 'done' : currentIndex >= 3 ? 'current' : 'upcoming',
|
||||
state: currentIndex > 3 ? 'done' : currentIndex === 3 ? 'current' : 'upcoming',
|
||||
},
|
||||
{
|
||||
code: 'IN_PROGRESS',
|
||||
label: 'Отгрузка',
|
||||
note: 'Плановая дата передачи в логистику.',
|
||||
dateLabel: formatDay(dates.shipment),
|
||||
state: currentIndex > 4 ? 'done' : currentIndex >= 4 ? 'current' : 'upcoming',
|
||||
state: currentIndex > 4 ? 'done' : currentIndex === 4 ? 'current' : 'upcoming',
|
||||
},
|
||||
{
|
||||
code: 'COMPLETED',
|
||||
label: 'Доставка',
|
||||
note: 'Плановая дата получения заказа.',
|
||||
dateLabel: formatDay(dates.delivered),
|
||||
state: currentIndex >= 5 ? 'current' : 'upcoming',
|
||||
state: currentIndex === 5 ? 'current' : 'upcoming',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -247,7 +280,7 @@ function buildClientStages(status: string, createdAt: string | Date): StatusPres
|
||||
|
||||
if (status === 'CONFIRMED') {
|
||||
return {
|
||||
title: 'Планируем производство',
|
||||
title: 'Производство запланировано',
|
||||
summary: `Ориентируемся на производство ${formatDay(dates.production)}, затем отгрузку и доставку по плану.`,
|
||||
stages,
|
||||
};
|
||||
@@ -255,7 +288,7 @@ function buildClientStages(status: string, createdAt: string | Date): StatusPres
|
||||
|
||||
if (status === 'IN_PROGRESS') {
|
||||
return {
|
||||
title: 'Заказ в работе',
|
||||
title: 'Готовим отгрузку',
|
||||
summary: `Производство идёт. Следующая плановая дата: отгрузка около ${formatDay(dates.shipment)}.`,
|
||||
stages,
|
||||
};
|
||||
@@ -268,6 +301,13 @@ function buildClientStages(status: string, createdAt: string | Date): StatusPres
|
||||
};
|
||||
}
|
||||
|
||||
export function getOrderStatusBadgePresentation(status: string): StatusBadgePresentation {
|
||||
return STATUS_BADGE_MAP[status] ?? {
|
||||
label: status,
|
||||
tone: 'info',
|
||||
};
|
||||
}
|
||||
|
||||
export function getOrderStatusPresentation(
|
||||
status: string,
|
||||
createdAt: string | Date,
|
||||
|
||||
@@ -32,19 +32,23 @@ const currentOrderCode = computed(() => formatOrderCode(currentOrder.value?.code
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="surface-card rounded-3xl px-5 py-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<NuxtLink to="/orders" class="text-sm font-semibold text-[#0d854a]">
|
||||
← Назад к моим заказам
|
||||
<div class="flex flex-wrap items-center gap-4 px-1">
|
||||
<NuxtLink
|
||||
to="/orders"
|
||||
aria-label="Назад к моим заказам"
|
||||
class="flex h-11 w-11 items-center justify-center rounded-full bg-white/70 text-[#0d854a] transition hover:bg-white"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M11.5 4.5L6 10L11.5 15.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</NuxtLink>
|
||||
<span class="hidden h-4 w-px bg-[#d8e4dd] md:block" />
|
||||
<span class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">
|
||||
<span class="h-8 w-px bg-[#d8e4dd]" />
|
||||
<h1 class="flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-black tracking-[-0.03em] text-[#123824] md:text-3xl">
|
||||
<span class="text-sm font-bold uppercase tracking-[0.18em] text-[#6a8a76]">
|
||||
Заказ
|
||||
</span>
|
||||
<span class="text-lg font-black tracking-[-0.03em] text-[#123824]">
|
||||
{{ currentOrderCode }}
|
||||
</span>
|
||||
</div>
|
||||
<span>{{ currentOrderCode }}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user