Files
dc-observatio/services/pcp-tgu-ui-service/src/features/tgu-slots/slotSelection.ts
T
Дмитрий Соловьев 354730fb6c feat(tgu-ui): запрет выбора конфликтных слотов + прохождение без подсветки
- конфликтные слоты нельзя добавить в выбор под бронь (клик по строке,
  Shift-диапазон, выбор полосы на карте, «выбрать все») — жёсткая блокировка;
  снятие выбора не блокируется (конфликт считается только для невыбранных)
- слоты одной полосы между собой не конфликтуют (groupOf) — иначе нельзя было
  бы набрать полосу поштучным кликом
- чекбокс «Конфликты» теперь и гасит, и блокирует выбор; выкл. — выбирать любые
- переход на прохождение (◀/▶) больше не подсвечивает никакой слот: активная
  подсветка строки — только по явному клику; добавлена прокрутка к началу прохода
- повторный Shift+клик по тому же концу снимает выделение (таблица и карта)
2026-06-17 14:07:03 +03:00

112 lines
5.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
}
/**
* Новое множество выбора после жеста «полоса от первого к последнему»: выбор полосы становится
* РОВНО диапазоном [fromKey..toKey] (в порядке времени). Члены полосы вне диапазона снимаются —
* так Shift+клик по слоту ближе к якорю «сжимает» выделение. Выбор за пределами полосы (другие
* полосы/КА) сохраняется: снимаем/добавляем только ключи из members. Жест ограничен одной полосой:
* вызывающий передаёт members именно её (один КА, один крен, подряд по времени); граница полосы —
* гарантия «нельзя выделить не полосу». Если fromKey или toKey не из members — выбор не меняется
* (страховка). Вход не мутируется.
*
* @param current Текущее множество выбранных ключей.
* @param members Ключи членов полосы в порядке времени.
* @param fromKey Ключ якоря (первый клик).
* @param toKey Ключ второго клика.
*/
export function selectStripRange(
current: ReadonlySet<string>,
members: readonly string[],
fromKey: string,
toKey: string,
): Set<string> {
const next = new Set(current)
const from = members.indexOf(fromKey)
const to = members.indexOf(toKey)
if (from < 0 || to < 0) return next
const lo = Math.min(from, to)
const hi = Math.max(from, to)
members.forEach((key, i) => {
if (i >= lo && i <= hi) next.add(key)
else next.delete(key)
})
return next
}
/**
* Снимает выбор с диапазона полосы [fromKey..toKey] (в порядке времени). Зеркало selectStripRange:
* убирает ровно члены диапазона, не трогая остальной выбор. Нужно для повторного Shift+клика по тому
* же концу — он снимает выделенный диапазон. Если fromKey/toKey не из members — выбор не меняется.
*
* @param current Текущее множество выбранных ключей.
* @param members Ключи членов полосы в порядке времени.
* @param fromKey Ключ якоря (первый клик).
* @param toKey Ключ второго клика.
*/
export function deselectStripRange(
current: ReadonlySet<string>,
members: readonly string[],
fromKey: string,
toKey: string,
): Set<string> {
const next = new Set(current)
const from = members.indexOf(fromKey)
const to = members.indexOf(toKey)
if (from < 0 || to < 0) return next
const lo = Math.min(from, to)
const hi = Math.max(from, to)
for (let i = lo; i <= hi; i++) next.delete(members[i])
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
}