feat(promo-offers): охват сегментов и прогресс доставки на странице отправки

Сегмент выбирался вслепую — в списке не было числа пользователей, а после
отправки экран показывал только количество созданных офферов: сколько
человек реально получило предложение и сколько заблокировало бота, узнать
было негде.

В селекте сегментов теперь стоит охват каждого (GET /promo-offers/segments),
под ним — сколько уйдёт по выбранному. После отправки экран результата
показывает прогресс доставки с карточками Всего/Отправлено/Заблокировали/
Ошибки и опрашивает запись рассылки раз в 3 секунды, пока она не завершится.

Прогресс-блок и бейдж статуса вынесены из AdminBroadcastDetail в
BroadcastDeliveryStats и переиспользуются обеими страницами, а условие
«доставка ещё идёт» — в утилиту broadcastStatus с тестами.
This commit is contained in:
kewldan
2026-07-28 22:24:58 +03:00
parent 51d0f0431a
commit c6d8c0afe0
10 changed files with 298 additions and 128 deletions

View File

@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { broadcastPollInterval, isBroadcastInFlight } from './broadcastStatus';
describe('broadcastStatus', () => {
it('treats queued, in-progress and cancelling deliveries as running', () => {
expect(isBroadcastInFlight('queued')).toBe(true);
expect(isBroadcastInFlight('in_progress')).toBe(true);
expect(isBroadcastInFlight('cancelling')).toBe(true);
});
it('treats finished deliveries as done', () => {
expect(isBroadcastInFlight('completed')).toBe(false);
expect(isBroadcastInFlight('partial')).toBe(false);
expect(isBroadcastInFlight('failed')).toBe(false);
expect(isBroadcastInFlight('cancelled')).toBe(false);
});
it('handles a missing status without polling', () => {
expect(isBroadcastInFlight(undefined)).toBe(false);
expect(broadcastPollInterval(undefined)).toBe(false);
});
it('polls only while the delivery is running', () => {
expect(broadcastPollInterval('in_progress')).toBe(3000);
expect(broadcastPollInterval('completed')).toBe(false);
});
});

View File

@@ -0,0 +1,13 @@
/** Статусы, при которых доставка ещё идёт и запись рассылки нужно перезапрашивать. */
export const BROADCAST_IN_FLIGHT_STATUSES = ['queued', 'in_progress', 'cancelling'] as const;
/** Доставка ещё не завершена: показываем прогресс и продолжаем поллинг. */
export function isBroadcastInFlight(status: string | undefined): boolean {
if (!status) return false;
return (BROADCAST_IN_FLIGHT_STATUSES as readonly string[]).includes(status);
}
/** Интервал поллинга для react-query: пока идёт доставка — 3 секунды, иначе не опрашиваем. */
export function broadcastPollInterval(status: string | undefined): number | false {
return isBroadcastInFlight(status) ? 3000 : false;
}