280 lines
8.7 KiB
TypeScript
280 lines
8.7 KiB
TypeScript
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),
|
||
};
|
||
}
|
||
|
||
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' : 'current',
|
||
},
|
||
{
|
||
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 getOrderStatusPresentation(
|
||
status: string,
|
||
createdAt: string | Date,
|
||
audience: 'client' | 'manager' = 'client',
|
||
): StatusPresentation {
|
||
return audience === 'manager'
|
||
? buildManagerStages(status, createdAt)
|
||
: buildClientStages(status, createdAt);
|
||
}
|