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 = { 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); }