feat(tgu-ui): бронирование слотов — виртуализация, автоподбор покрытия, эргономичный мультивыбор

Вкладка «Слоты» масштабирована под ~20k слотов/мес без торможения:
- wktCache: контур слота парсится один раз (кэш по slotKey), не на каждый ререндер сцены
- виртуализация таблицы (@tanstack/react-virtual): в DOM только видимое окно строк
- автоподбор покрытия: проброс coverageStrategy (GREEDY/CONTINUOUS) в request-cover,
  результат — преднабор брони (solver не дублируется, используется бэкендный)
- мультивыбор: Shift-диапазон по отсортированному списку, «все сутки»/«видимые»,
  счётчик охвата выбора
- бронь крупных наборов чанками (по 200) + подтверждение выше 500 + прогресс
- отмена расчёта (AbortController), мягкая подсказка перегруза карты
- чистая логика выбора/чанкинга вынесена в slotSelection.ts + юнит-тесты

Тесты: 70/70 (17 новых). tsc -b и vite build чисто.
This commit is contained in:
Дмитрий Соловьев
2026-06-11 17:09:58 +03:00
parent 345529cde0
commit c64222a90d
8 changed files with 641 additions and 122 deletions
@@ -26,11 +26,17 @@ async function readErrorMessage(response: Response, fallback: string): Promise<s
/**
* Рассчитывает слоты покрытия заявки (slots-service: GET /api/slots/request-cover).
* @param params Заявка, интервал и фильтры (ветвь, признак покрытия, КА, угол Солнца).
* @param params Заявка, интервал и фильтры (ветвь, признак покрытия, КА, угол Солнца,
* стратегия автоформирования покрытия).
* @param signal Сигнал отмены (AbortController) — прерывает долгий расчёт.
*
* @returns Список слотов-кандидатов с контурами для карты.
* @returns Список слотов-кандидатов с контурами для карты. При заданной coverageStrategy —
* уже отобранный достаточный набор покрытия (преднабор для брони).
*/
export async function fetchRequestSlots(params: RequestSlotsParams): Promise<SlotDTO[]> {
export async function fetchRequestSlots(
params: RequestSlotsParams,
signal?: AbortSignal,
): Promise<SlotDTO[]> {
const query = new URLSearchParams()
query.set('requestId', params.requestId)
query.set('timeStart', params.timeStart)
@@ -38,10 +44,12 @@ export async function fetchRequestSlots(params: RequestSlotsParams): Promise<Slo
if (params.revSign) query.set('revSign', params.revSign)
if (params.cov != null) query.set('cov', String(params.cov))
if (params.sun != null) query.set('sun', String(params.sun))
if (params.coverageStrategy) query.set('coverageStrategy', params.coverageStrategy)
params.satellites?.forEach((id) => query.append('satellites', String(id)))
const response = await apiFetch(`${BASE}/request-cover?${query}`, {
headers: { Accept: 'application/json' },
signal,
})
if (!response.ok) {