Files
dc-observatio/services/pcp-tgu-ui-service/src/features/tgu-slots/slotSelection.ts
T
Дмитрий Соловьев c64222a90d 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 чисто.
2026-06-11 17:09:58 +03:00

53 lines
2.4 KiB
TypeScript

// Чистая логика выбора слотов и разбиения на чанки — выделена из TguSlotsTab,
// чтобы покрыть юнит-тестами без рендера компонента.
/**
* Новое множество выбора после клика по строке.
* Обычный клик — переключает один ключ. Shift+клик при заданном якоре — выбирает (или
* снимает) непрерывный диапазон от якоря до текущего индекса; направление задаётся новым
* состоянием текущей строки (если она была не выбрана — диапазон выбираем, иначе снимаем).
*
* @param current Текущее множество выбранных ключей.
* @param keyAt Доступ к ключу слота по индексу в видимом (отфильтрованном) списке.
* @param index Индекс строки, по которой кликнули.
* @param shiftKey Был ли зажат Shift.
* @param anchorIndex Индекс предыдущего клика (якорь) либо null.
*/
export function selectionAfterCheck(
current: ReadonlySet<string>,
keyAt: (index: number) => string,
index: number,
shiftKey: boolean,
anchorIndex: number | null,
): Set<string> {
const next = new Set(current)
const targetKey = keyAt(index)
if (shiftKey && anchorIndex != null) {
const lo = Math.min(anchorIndex, index)
const hi = Math.max(anchorIndex, index)
const select = !current.has(targetKey)
for (let i = lo; i <= hi; i++) {
const k = keyAt(i)
if (select) next.add(k)
else next.delete(k)
}
} else if (next.has(targetKey)) {
next.delete(targetKey)
} else {
next.add(targetKey)
}
return next
}
/**
* Разбивает массив на чанки заданного размера (для чанкованной брони крупных наборов).
* @param items Исходный массив.
* @param size Размер чанка; <= 0 — один чанк со всем массивом.
*/
export function chunk<T>(items: readonly T[], size: number): T[][] {
if (size <= 0) return items.length ? [items.slice()] : []
const out: T[][] = []
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))
return out
}