From 90175557224501142da8c13b57ba4e122f7bd1cb Mon Sep 17 00:00:00 2001
From: Ruslan Bakiev
Date: Sat, 4 Apr 2026 14:01:46 +0700
Subject: [PATCH] Simplify order statuses and manager actions
---
app/components/orders/OrderStatusBadge.vue | 15 +-
.../orders/OrderStatusTimelineCard.vue | 3 +-
app/composables/useOrderStatusPresentation.ts | 178 ++++++++++++------
app/pages/client-orders/[id].vue | 153 ++++++++-------
app/pages/client-orders/index.vue | 8 +-
app/pages/orders/[id].vue | 1 +
6 files changed, 216 insertions(+), 142 deletions(-)
diff --git a/app/components/orders/OrderStatusBadge.vue b/app/components/orders/OrderStatusBadge.vue
index 317d385..da12d48 100644
--- a/app/components/orders/OrderStatusBadge.vue
+++ b/app/components/orders/OrderStatusBadge.vue
@@ -4,15 +4,12 @@ const props = defineProps<{
}>();
const statusLabel = computed(() => {
- if (props.status === 'NEW') return 'Уточнение цены';
- if (props.status === 'MANAGER_PROCESSING') return 'В работе у менеджера';
- if (props.status === 'WAITING_DOUBLE_CONFIRM') return 'Ожидает подтверждения';
- if (props.status === 'CONFIRMED') return 'Подтвержден';
- if (props.status === 'IN_PROGRESS') return 'Выполняется';
+ 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') return 'Отклонен клиентом';
- if (props.status === 'MANAGER_REJECTED') return 'Отклонен менеджером';
- if (props.status === 'MANAGER_BLOCKED') return 'Заблокирован';
+ if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'Отклонен';
+ if (props.status === 'MANAGER_BLOCKED') return 'Пауза';
return props.status;
});
@@ -20,7 +17,7 @@ const className = computed(() => {
if (props.status === 'COMPLETED') return 'badge badge-success';
if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'badge badge-error';
if (props.status === 'MANAGER_BLOCKED') return 'badge badge-warning';
- if (props.status === 'NEW') return 'badge badge-warning';
+ if (props.status === 'NEW' || props.status === 'MANAGER_PROCESSING') return 'badge badge-warning';
return 'badge badge-info';
});
diff --git a/app/components/orders/OrderStatusTimelineCard.vue b/app/components/orders/OrderStatusTimelineCard.vue
index 36bc266..2244d9c 100644
--- a/app/components/orders/OrderStatusTimelineCard.vue
+++ b/app/components/orders/OrderStatusTimelineCard.vue
@@ -5,11 +5,12 @@ import { getOrderStatusPresentation } from '~/composables/useOrderStatusPresenta
const props = defineProps<{
status: string;
createdAt: string | Date;
+ audience?: 'client' | 'manager';
}>();
const isExpanded = ref(false);
-const presentation = computed(() => getOrderStatusPresentation(props.status, props.createdAt));
+const presentation = computed(() => getOrderStatusPresentation(props.status, props.createdAt, props.audience ?? 'client'));
function itemClass(state: 'done' | 'current' | 'upcoming') {
if (state === 'current') {
diff --git a/app/composables/useOrderStatusPresentation.ts b/app/composables/useOrderStatusPresentation.ts
index 45fd482..fcc81b6 100644
--- a/app/composables/useOrderStatusPresentation.ts
+++ b/app/composables/useOrderStatusPresentation.ts
@@ -65,25 +65,99 @@ function buildDates(createdAt: string | Date) {
};
}
-export function getOrderStatusPresentation(status: string, createdAt: string | Date): StatusPresentation {
+function buildManagerStages(status: string, createdAt: string | Date): StatusPresentation {
const dates = buildDates(createdAt);
- if (status === 'CLIENT_REJECTED') {
+ const isOfferStage = ['WAITING_DOUBLE_CONFIRM', 'CONFIRMED'].includes(status);
+ const isWorkStage = ['IN_PROGRESS', 'COMPLETED'].includes(status);
+ const isStopped = ['CLIENT_REJECTED', 'MANAGER_REJECTED', 'MANAGER_BLOCKED'].includes(status);
+
+ const stages: TimelineStage[] = [
+ {
+ code: 'NEW',
+ label: 'Заявка',
+ note: 'Заказ создан и ждёт расчёта.',
+ dateLabel: formatDay(dates.created),
+ state: isOfferStage || isWorkStage || isStopped ? 'done' : 'current',
+ },
+ {
+ code: 'WAITING_DOUBLE_CONFIRM',
+ label: 'Предложение',
+ note: 'Цена и условия опубликованы клиенту.',
+ dateLabel: formatDay(dates.offer),
+ state: isWorkStage ? 'done' : isOfferStage ? 'current' : 'upcoming',
+ },
+ {
+ code: 'IN_PROGRESS',
+ label: status === 'COMPLETED' ? 'Исполнение завершено' : 'В работе',
+ note: status === 'COMPLETED'
+ ? 'Заказ передан в работу и закрыт.'
+ : 'Заказ передан в производство или исполнение.',
+ dateLabel: formatDay(dates.production),
+ state: isWorkStage ? 'current' : 'upcoming',
+ },
+ ];
+
+ if (status === 'MANAGER_BLOCKED') {
return {
- title: 'Заказ остановлен клиентом',
- summary: 'Согласование остановлено. Если нужно, заказ можно собрать заново с новыми условиями.',
+ title: 'Заказ на паузе',
+ summary: 'Нужно уточнение по заказу перед публикацией предложения.',
+ stages,
+ };
+ }
+
+ if (status === 'CLIENT_REJECTED' || status === 'MANAGER_REJECTED') {
+ return {
+ title: 'Заказ остановлен',
+ summary: 'Текущий заказ завершён без запуска в работу.',
+ stages,
+ };
+ }
+
+ if (isWorkStage) {
+ return {
+ title: status === 'COMPLETED' ? 'Заказ завершён' : 'Заказ в работе',
+ summary: status === 'COMPLETED'
+ ? 'Исполнение завершено, история этапов сохранена.'
+ : 'Предложение согласовано, заказ уже в работе.',
+ stages,
+ };
+ }
+
+ if (isOfferStage) {
+ return {
+ title: 'Предложение отправлено',
+ summary: 'Клиент уже видит цену и условия. Следующий шаг для менеджера: пустить заказ в работу.',
+ stages,
+ };
+ }
+
+ return {
+ title: 'Ждём расчёт по заявке',
+ summary: 'Заполните цену по позициям и логистике, после этого предложение уйдёт клиенту автоматически.',
+ stages,
+ };
+}
+
+function buildClientStages(status: string, createdAt: string | Date): StatusPresentation {
+ const dates = buildDates(createdAt);
+
+ if (status === 'CLIENT_REJECTED' || status === 'MANAGER_REJECTED') {
+ return {
+ title: 'Заказ остановлен',
+ summary: 'Текущий заказ закрыт. При необходимости можно оформить новый заказ.',
stages: [
{
code: 'NEW',
- label: 'Заявка принята',
- note: 'Заказ попал в обработку.',
+ label: 'Заказ создан',
+ note: 'Заказ принят в обработку.',
dateLabel: formatDay(dates.created),
state: 'done',
},
{
- code: 'CLIENT_REJECTED',
- label: 'Клиент отказался от продолжения',
- note: 'Текущий заказ закрыт без запуска в работу.',
+ code: status,
+ label: 'Заказ остановлен',
+ note: 'Дальнейшее исполнение не планируется.',
dateLabel: formatDay(dates.approval),
state: 'current',
},
@@ -91,26 +165,22 @@ export function getOrderStatusPresentation(status: string, createdAt: string | D
};
}
- if (status === 'MANAGER_REJECTED' || status === 'MANAGER_BLOCKED') {
+ if (status === 'MANAGER_BLOCKED') {
return {
- title: status === 'MANAGER_BLOCKED' ? 'Заказ ждёт уточнения' : 'Заказ остановлен менеджером',
- summary: status === 'MANAGER_BLOCKED'
- ? 'Сейчас ждём уточнение параметров, после него заказ вернётся в работу.'
- : 'Менеджер остановил обработку. При необходимости можно собрать новый заказ.',
+ title: 'Заказ ждёт уточнения',
+ summary: 'Менеджер уточняет параметры заказа перед расчётом.',
stages: [
{
code: 'NEW',
- label: 'Заявка принята',
- note: 'Заказ попал в обработку.',
+ label: 'Заказ создан',
+ note: 'Заказ принят в обработку.',
dateLabel: formatDay(dates.created),
state: 'done',
},
{
code: status,
- label: status === 'MANAGER_BLOCKED' ? 'Нужно уточнение по заказу' : 'Обработка остановлена',
- note: status === 'MANAGER_BLOCKED'
- ? 'Менеджер запросил уточнение перед продолжением.'
- : 'Текущий заказ завершён без запуска в производство.',
+ label: 'Уточняем детали',
+ note: 'После уточнения покажем плановые даты по исполнению.',
dateLabel: formatDay(dates.approval),
state: 'current',
},
@@ -123,76 +193,62 @@ export function getOrderStatusPresentation(status: string, createdAt: string | D
const stages: TimelineStage[] = [
{
code: 'NEW',
- label: 'Заявка принята',
- note: 'Получили состав заказа и начали обработку.',
+ 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)}.`,
+ 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: `Сейчас идёт исполнение. Следующее ожидаемое обновление около ${formatDay(dates.shipment)}.`,
+ 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: 'Исполнение закрыто, заказ готов к выдаче или уже доставлен.',
+ label: 'Доставка',
+ note: 'Плановая дата получения заказа.',
dateLabel: formatDay(dates.delivered),
- state: currentIndex === 5 ? 'current' : currentIndex > 5 ? 'done' : 'upcoming',
+ state: currentIndex >= 5 ? 'current' : 'upcoming',
},
];
if (status === 'NEW') {
return {
- title: 'Собираем предложение по заказу',
- summary: `Сейчас уточняем стоимость и комплектацию. Следующее обновление ориентировочно ${formatDay(dates.offer)}.`,
+ title: 'Заказ создан',
+ summary: 'Менеджер рассчитывает стоимость и готовит план по исполнению.',
stages,
};
}
if (status === 'MANAGER_PROCESSING') {
return {
- title: 'Готовим условия по заказу',
- summary: `Менеджер собирает финальные условия. Ориентир по следующему апдейту ${formatDay(dates.approval)}.`,
+ title: 'Готовим предложение',
+ summary: 'Собираем итоговые условия и скоро покажем плановые даты.',
stages,
};
}
if (status === 'WAITING_DOUBLE_CONFIRM') {
return {
- title: 'Ожидаем подтверждение условий',
- summary: 'Нужно подтвердить текущие условия, после этого сразу двинем заказ дальше.',
+ title: 'Предложение готово',
+ summary: 'Стоимость и условия уже рассчитаны. Следующий этап после запуска: производство.',
stages,
};
}
if (status === 'CONFIRMED') {
return {
- title: 'Заказ подтверждён',
- summary: `Ожидаем выпуск с производства ориентировочно ${formatDay(dates.production)}.`,
+ title: 'Планируем производство',
+ summary: `Ориентируемся на производство ${formatDay(dates.production)}, затем отгрузку и доставку по плану.`,
stages,
};
}
@@ -200,14 +256,24 @@ export function getOrderStatusPresentation(status: string, createdAt: string | D
if (status === 'IN_PROGRESS') {
return {
title: 'Заказ в работе',
- summary: `Сейчас заказ исполняется. Следующее ожидаемое обновление около ${formatDay(dates.shipment)}.`,
+ summary: `Производство идёт. Следующая плановая дата: отгрузка около ${formatDay(dates.shipment)}.`,
stages,
};
}
return {
- title: 'Заказ завершён',
- summary: 'Работы по заказу завершены, история этапов сохранена ниже.',
+ title: 'Доставка по плану',
+ summary: `Финальный ориентир по заказу: доставка ${formatDay(dates.delivered)}.`,
stages,
};
}
+
+export function getOrderStatusPresentation(
+ status: string,
+ createdAt: string | Date,
+ audience: 'client' | 'manager' = 'client',
+): StatusPresentation {
+ return audience === 'manager'
+ ? buildManagerStages(status, createdAt)
+ : buildClientStages(status, createdAt);
+}
diff --git a/app/pages/client-orders/[id].vue b/app/pages/client-orders/[id].vue
index 2b4573b..40dcb8f 100644
--- a/app/pages/client-orders/[id].vue
+++ b/app/pages/client-orders/[id].vue
@@ -1,9 +1,6 @@
@@ -217,6 +230,7 @@ async function completeOrder() {
@@ -237,6 +251,7 @@ async function completeOrder() {
step="0.01"
placeholder="Например, 125.50"
class="input input-bordered w-full rounded-2xl bg-white"
+ :disabled="!canEditOffer"
>
@@ -258,6 +273,7 @@ async function completeOrder() {
type="text"
placeholder="Например, доставка до склада 2-3 дня"
class="input input-bordered w-full rounded-2xl bg-white"
+ :disabled="!canEditOffer"
>
@@ -272,6 +288,7 @@ async function completeOrder() {
step="0.01"
placeholder="Например, 3000"
class="input input-bordered w-full rounded-2xl bg-white"
+ :disabled="!canEditOffer"
>
@@ -282,29 +299,21 @@ async function completeOrder() {
{{
offerTotal == null
? 'Итог по заказу посчитается автоматически после заполнения всех цен.'
- : `Предварительный итог: ${formatPrice(offerTotal)}`
+ : autosavePending
+ ? `Сохраняем и отправляем клиенту: ${formatPrice(offerTotal)}`
+ : `Предложение отправится клиенту автоматически: ${formatPrice(offerTotal)}`
}}
-
-
+
-
Действия менеджера
-
-
-
-
-
-
+
+
Следующий шаг
+
Когда всё согласовано, менеджер просто переводит заказ в работу.
+
diff --git a/app/pages/client-orders/index.vue b/app/pages/client-orders/index.vue
index 9497c1c..3564fd2 100644
--- a/app/pages/client-orders/index.vue
+++ b/app/pages/client-orders/index.vue
@@ -119,12 +119,12 @@ const statusTabs = computed
getOrderGroup(order) === 'NEW').length,
},
{
id: 'PRICED',
- label: 'Оценены',
+ label: 'Предложения',
count: searchedOrders.value.filter((order) => getOrderGroup(order) === 'PRICED').length,
},
{
@@ -143,12 +143,12 @@ const kanbanColumns = computed(() => {
const columns = [
{
id: 'NEW' as const,
- title: 'Новые',
+ title: 'Заявки',
tone: 'from-[#f5fff8] to-[#eefaf2]',
},
{
id: 'PRICED' as const,
- title: 'Оценены',
+ title: 'Предложения',
tone: 'from-[#fff9ef] to-[#fff1d9]',
},
{
diff --git a/app/pages/orders/[id].vue b/app/pages/orders/[id].vue
index 2ef1615..91dde59 100644
--- a/app/pages/orders/[id].vue
+++ b/app/pages/orders/[id].vue
@@ -45,6 +45,7 @@ const currentOrder = computed(() =>