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
@@ -925,11 +925,44 @@
.tgu-slots-list {
min-width: 0;
min-height: 0;
overflow: auto;
display: flex;
flex-direction: column;
border-right: 1px solid var(--line);
background: var(--bg-panel);
}
/* Прокручиваемая область таблицы слотов — scrollElement виртуализатора. */
.tgu-slots-scroll {
min-height: 0;
flex: 1;
overflow: auto;
}
/* Панель выбора над таблицей: счётчик охвата + действия (все сутки / снять). */
.tgu-slots-selbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.4rem 0.55rem;
border-bottom: 1px solid var(--line);
background: var(--bg-panel);
font-size: 0.78rem;
}
.tgu-slots-selbar__count {
color: var(--text);
}
.tgu-slots-selbar__sub {
color: var(--text-dim);
}
.tgu-slots-selbar__actions {
display: flex;
gap: 0.35rem;
}
.tgu-slots-empty {
padding: 1rem;
color: var(--text-dim);
@@ -1363,6 +1396,53 @@
backdrop-filter: blur(6px);
}
/* Кнопка отмены внутри оверлея расчёта/автоподбора — на отдельной строке под текстом. */
.tgu-map-overlay button {
display: block;
margin-top: 0.5rem;
}
/* Подсказка о перегрузе карты слотами (карту не блокируем). */
.tgu-slots-map-hint {
position: absolute;
z-index: 5;
left: 50%;
bottom: 0.85rem;
transform: translateX(-50%);
max-width: 90%;
padding: 0.4rem 0.7rem;
border: 1px solid rgba(240, 173, 46, 0.42);
border-radius: 7px;
background: rgba(17, 19, 21, 0.88);
color: #ffe7b5;
font-size: 0.78rem;
backdrop-filter: blur(6px);
}
/* Баннер подтверждения крупной брони. */
.tgu-slots-confirm {
position: absolute;
z-index: 6;
top: 0.85rem;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.8rem;
border: 1px solid var(--accent);
border-radius: 8px;
background: rgba(17, 19, 21, 0.94);
color: var(--text);
font-size: 0.82rem;
backdrop-filter: blur(6px);
}
.tgu-slots-confirm__actions {
display: flex;
gap: 0.4rem;
}
.tgu-map-errors {
display: grid;
gap: 0.4rem;