Files
web-frontend/app/composables/useOrderStatusPresentation.ts
2026-04-06 20:58:08 +07:00

320 lines
10 KiB
TypeScript
Raw Permalink 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.

type OrderStatusCode =
| 'NEW'
| 'MANAGER_PROCESSING'
| 'WAITING_DOUBLE_CONFIRM'
| 'CLIENT_REJECTED'
| 'MANAGER_REJECTED'
| 'MANAGER_BLOCKED'
| 'CONFIRMED'
| 'IN_PROGRESS'
| 'COMPLETED';
export type OrderStatusTone = 'warning' | 'info' | 'success' | 'danger';
type TimelineStage = {
code: string;
label: string;
note: string;
dateLabel: string;
state: 'done' | 'current' | 'upcoming';
};
type StatusBadgePresentation = {
label: string;
tone: OrderStatusTone;
};
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',
});
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);
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),
};
}
function buildManagerStages(status: string, createdAt: string | Date): StatusPresentation {
const dates = buildDates(createdAt);
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: 'Нужно уточнение по заказу перед публикацией предложения.',
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: 'Заказ принят в обработку.',
dateLabel: formatDay(dates.created),
state: 'done',
},
{
code: status,
label: 'Отклонен',
note: 'Дальнейшее исполнение не планируется.',
dateLabel: formatDay(dates.approval),
state: 'current',
},
],
};
}
if (status === 'MANAGER_BLOCKED') {
return {
title: 'Заказ ждёт уточнения',
summary: 'Менеджер уточняет параметры заказа перед расчётом.',
stages: [
{
code: 'NEW',
label: 'Заявка',
note: 'Заказ принят в обработку.',
dateLabel: formatDay(dates.created),
state: 'done',
},
{
code: status,
label: 'Пауза',
note: 'После уточнения покажем плановые даты по исполнению.',
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' : 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',
},
{
code: 'IN_PROGRESS',
label: 'Отгрузка',
note: 'Плановая дата передачи в логистику.',
dateLabel: formatDay(dates.shipment),
state: currentIndex > 4 ? 'done' : currentIndex === 4 ? 'current' : 'upcoming',
},
{
code: 'COMPLETED',
label: 'Доставка',
note: 'Плановая дата получения заказа.',
dateLabel: formatDay(dates.delivered),
state: currentIndex === 5 ? 'current' : 'upcoming',
},
];
if (status === 'NEW') {
return {
title: 'Заказ создан',
summary: 'Менеджер рассчитывает стоимость и готовит план по исполнению.',
stages,
};
}
if (status === 'MANAGER_PROCESSING') {
return {
title: 'Готовим предложение',
summary: 'Собираем итоговые условия и скоро покажем плановые даты.',
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: `Финальный ориентир по заказу: доставка ${formatDay(dates.delivered)}.`,
stages,
};
}
export function getOrderStatusBadgePresentation(status: string): StatusBadgePresentation {
return STATUS_BADGE_MAP[status] ?? {
label: status,
tone: 'info',
};
}
export function getOrderStatusPresentation(
status: string,
createdAt: string | Date,
audience: 'client' | 'manager' = 'client',
): StatusPresentation {
return audience === 'manager'
? buildManagerStages(status, createdAt)
: buildClientStages(status, createdAt);
}