@@ -3,13 +3,13 @@ import { useVirtualizer } from "@tanstack/react-virtual";
import { fetchRequestsForMap } from "../../api/requestApi" ;
import {
bookRequestSlots ,
cancelRequestBooking ,
cancelBookedSlots ,
fetchBookedSlotsByRequest ,
fetchRequestSlots
} from "../../api/slotsApi" ;
import type { RequestMapItem } from "../../model/requestTypes" ;
import type {
BookedSlotDTO ,
BookedSlotView ,
RevolutionSign ,
SlotBookingRequest ,
SlotCoverageStrategy ,
@@ -26,7 +26,9 @@ import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
import { formatLocalDateTimeSeconds , parseLocal , toLocalInputValue , toLocalIso } from "../../utils/localTime" ;
import { SlotDaySelector , type SlotDay } from "./SlotDaySelector" ;
import { SlotPickMenu , type SlotPick } from "./SlotPickMenu" ;
import { chunk , selectionAfterCheck } from "./slotSelection" ;
import { selectionAfterCheck } from "./slotSelection" ;
import { runChunked } from "./runChunked" ;
import { ChunkProgress } from "./ChunkProgress" ;
const DAY_MS = 24 * 60 * 60 * 1000 ;
@@ -80,13 +82,12 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
const [ fromValue , setFromValue ] = useState ( ( ) = > toLocalInputValue ( range . fromMs ) ) ;
const [ toValue , setToValue ] = useState ( ( ) = > toLocalInputValue ( range . toMs ) ) ;
const [ revSign , setRevSign ] = useState < "" | RevolutionSign > ( "" ) ;
const [ withCoverage , setWithCoverage ] = useState ( false ) ;
// Стратегия автоформирования покрытия (кнопка «Автоподбор»).
const [ strategy , setStrategy ] = useState < SlotCoverageStrategy > ( "GREEDY" ) ;
// Результаты: слоты-кандидаты и уже забронированные слоты заявки.
// Результаты: слоты-кандидаты и уже забронированные слоты заявки (с bookedSlotId для снятия по id) .
const [ slots , setSlots ] = useState < SlotDTO [ ] > ( [ ] ) ;
const [ bookedSlots , setBookedSlots ] = useState < SlotDTO [ ] > ( [ ] ) ;
const [ bookedSlots , setBookedSlots ] = useState < BookedSlotView [ ] > ( [ ] ) ;
const [ selectedKeys , setSelectedKeys ] = useState < ReadonlySet < string > > ( ( ) = > new Set ( ) ) ;
// Фильтр отображения по суткам: индексы (1-based) выбранного диапазона суток расчёта.
@@ -96,9 +97,15 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
const [ calculating , setCalculating ] = useState ( false ) ;
const [ autoFilling , setAutoFilling ] = useState ( false ) ;
const [ booking , setBooking ] = useState ( false ) ;
// Прогресс чанков анной брони «X/Y» и набор, ждущий подтверждения (крупная бронь) .
const [ bookProgress , setBookProgress ] = useState < string > ( ) ;
// Снятие выбр анных броней пачками (request-aware batch) — отдельный флаг «в полёте» .
const [ cancellingSel , setCancellingSel ] = useState ( false ) ;
// Прогресс крупной чанковой операции (бронь / снятие выбранных) — общий бар <ChunkProgress>.
const [ progress , setProgress ] = useState < { label : string ; done : number ; total : number } | null > ( null ) ;
const [ pendingBook , setPendingBook ] = useState < SlotBookingRequest [ ] | null > ( null ) ;
// «Снять всё» убирает ВСЕ брони заявки — необратимо, поэтому всегда через подтверждение.
// cancelling отделяет «снятие всего в полёте» от брони/снятия выбранных.
const [ pendingCancel , setPendingCancel ] = useState ( false ) ;
const [ cancelling , setCancelling ] = useState ( false ) ;
const [ error , setError ] = useState < string > ( ) ;
const [ notice , setNotice ] = useState < string > ( ) ;
@@ -141,8 +148,14 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
! Number . isFinite ( parsedRange . toMs ) ||
parsedRange . fromMs >= parsedRange . toMs ;
// Ключи забронированных слотов — для бейджа «бронь» в таблице и отдельного слоя на карте.
// Ключи забронированных слотов — для бейджа «бронь» в таблице и слоя на карте.
const bookedKeys = useMemo ( ( ) = > new Set ( bookedSlots . map ( slotKey ) ) , [ bookedSlots ] ) ;
// Бронь по ключу слота — чтобы из выбранной/кликнутой строки достать bookedSlotId для снятия.
const bookedByKey = useMemo ( ( ) = > {
const map = new Map < string , BookedSlotView > ( ) ;
for ( const slot of bookedSlots ) map . set ( slotKey ( slot ) , slot ) ;
return map ;
} , [ bookedSlots ] ) ;
// id полигонов слотов, отобранных галочками под бронь — постоянная подсветка на карте.
const markedSlotIds = useMemo (
@@ -158,10 +171,34 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
return map ;
} , [ slots , bookedSlots ] ) ;
// С утки, по которым раскладываются рассчитанные слоты (для фильтра отображения).
// С троки таблицы = кандидаты + брони, которых нет среди кандидатов (Finding 2-A). Так бронь
// выбираема и снимаема, даже если кандидаты ещё не рассчитаны или бронь в другом наборе.
const displaySlots = useMemo < SlotDTO [ ] > ( ( ) = > {
const slotKeySet = new Set ( slots . map ( slotKey ) ) ;
const orphanBooked = bookedSlots . filter ( ( slot ) = > ! slotKeySet . has ( slotKey ( slot ) ) ) ;
return orphanBooked . length === 0 ? slots : [ . . . slots , . . . orphanBooked ] ;
} , [ slots , bookedSlots ] ) ;
// bookedSlotId выбранных броней (для «Снять выбранные», чанкуется) и число выбранных кандидатов
// под бронь (для «Забронировать»). Бронь и кандидат различаем по bookedKeys.
const selectedBookedIds = useMemo ( ( ) = > {
const ids : number [ ] = [ ] ;
for ( const key of selectedKeys ) {
const booked = bookedByKey . get ( key ) ;
if ( booked ) ids . push ( booked . bookedSlotId ) ;
}
return ids ;
} , [ selectedKeys , bookedByKey ] ) ;
const selectedBookableCount = useMemo ( ( ) = > {
let n = 0 ;
for ( const key of selectedKeys ) if ( ! bookedKeys . has ( key ) ) n += 1 ;
return n ;
} , [ selectedKeys , bookedKeys ] ) ;
// Сутки, по которым раскладываются строки таблицы (для фильтра отображения).
const days = useMemo < SlotDay [ ] > ( ( ) = > {
const buckets = new Map < number , number > ( ) ;
for ( const slot of slots ) {
for ( const slot of displaySlots ) {
const ms = parseLocal ( slot . tn ) ;
if ( ! Number . isFinite ( ms ) ) continue ;
const start = dayStartMs ( ms ) ;
@@ -170,7 +207,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
return Array . from ( buckets . entries ( ) )
. sort ( ( [ a ] , [ b ] ) = > a - b )
. map ( ( [ dayMs , count ] , idx ) = > ( { index : idx + 1 , dayMs , count , label : dayLabel ( dayMs ) } ) ) ;
} , [ slots ] ) ;
} , [ displaySlots ] ) ;
// Новый расчёт меняет набор суток — раскрываем фильтр на весь диапазон.
useEffect ( ( ) = > {
@@ -181,16 +218,16 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
const effDayFrom = Math . max ( 1 , Math . min ( dayFrom , days . length || 1 ) ) ;
const effDayTo = Math . max ( effDayFrom , Math . min ( dayTo , days . length || 1 ) ) ;
// Слоты выбранного диапазона суток — то, что видно и в таблице, и на карте.
// Слоты выбранного диапазона суток — то, что видно и в таблице, и на карте (кандидаты + брони) .
const filteredSlots = useMemo ( ( ) = > {
if ( days . length === 0 ) return slots ;
if ( days . length === 0 ) return displaySlots ;
const fromMs = days [ effDayFrom - 1 ] . dayMs ;
const toMsExcl = days [ effDayTo - 1 ] . dayMs + DAY_MS ;
return slots . filter ( ( slot ) = > {
return displaySlots . filter ( ( slot ) = > {
const ms = parseLocal ( slot . tn ) ;
return Number . isFinite ( ms ) && ms >= fromMs && ms < toMsExcl ;
} ) ;
} , [ slots , days , effDayFrom , effDayTo ] ) ;
} , [ displaySlots , days , effDayFrom , effDayTo ] ) ;
// Виртуализация таблицы: в DOM держим только видимое окно строк (+overscan), а не все слоты —
// иначе на тысячах строк рендер встаёт. Скролл-контейнер — секция списка (listRef).
@@ -279,6 +316,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
setSelectedKeys ( new Set ( ) ) ;
setBookedSlots ( [ ] ) ;
setPendingBook ( null ) ;
setPendingCancel ( false ) ;
setNotice ( undefined ) ;
setError ( undefined ) ;
setActiveSlotId ( undefined ) ;
@@ -328,7 +366,9 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
timeStart : toLocalIso ( parsedRange . fromMs ) ,
timeStop : toLocalIso ( parsedRange . toMs ) ,
revSign : revSign || undefined ,
cov : withCoverage ,
// cov=true (режим покрытия) ровно когда задана стратегия — это путь «Подобрать
// покрытие». «Показать все слоты» идёт без стратегии и получает полный набор кандидатов.
cov : auto ,
coverageStrategy
} ,
ctrl . signal
@@ -370,29 +410,34 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
const autoCover = ( ) = > void runCover ( strategy ) ;
const cancelCalculation = ( ) = > abortRef . current ? . abort ( ) ;
// Бронирует набор чанками (бэк бронируе т с лоты по одному в одной транзакции — крупный набор
// бьём на части, чтобы не упереться в таймаут/долгую транзакцию) и показывает прогресс .
// Любая операция с бронью «в полёте» — общий гей т б локировки кнопок (бронь / снятие выбранных /
// снятие всего). Отделён от busy (расчёт/автоподбор) .
const opBusy = booking || cancelling || cancellingSel ;
// Бронирует набор чанками (бэк бронирует по одному в транзакции — крупный набор бьём на части
// через runChunked) и показывает прогресс. bookedSlotId берём из ответа POST.
const performBook = async ( toBook : SlotBookingRequest [ ] ) = > {
setBooking ( true ) ;
setError ( undefined ) ;
setNotice ( undefined ) ;
setBookProgress ( toBook . length > BOOK_CHUNK ? ` 0/ ${ toBook . length } ` : undefined ) ;
setProgress ( toBook . length > BOOK_CHUNK ? { label : "Бронирование" , done : 0 , total : toBook.length } : null ) ;
try {
const collected : BookedSlotDTO [ ] = [ ] ;
let done = 0 ;
for ( const part of chunk ( toBook , BOOK_CHUNK ) ) {
const booked = await bookRequestSlots ( selectedRequestId , part ) ;
collected . push ( . . . booked ) ;
done += part . length ;
if ( toBook . length > BOOK_CHUNK ) setBookProgress ( ` ${ done } / ${ toBook . length } ` ) ;
}
// Источник истины — ответ POST: BookedSlotDTO несёт тот же cycle, что мы отправили (= cycle
// рассчитанного слота). А вот GET booking/by-request отдаёт базовый слот в ДРУГОМ цикле
// (slot.toDTO без сдвига на cycle брони), из-за чего ключ sat:slotNum:cycle не совпадает и
// бейдж «бронь» не появляется. Поэтому отмечаем забронированными ровно те рассчитанные слоты
// (с контуром), что вернул POST, не перечитывая by-request.
const bookedSet = new Set ( collected . map ( ( item ) = > ` ${ item . satelliteId } : ${ item . slotNum } : ${ item . cycle } ` ) ) ;
const justBooked = slots . filter ( ( slot ) = > bookedSet . has ( slotKey ( slot ) ) ) ;
const parts = await runChunked (
toBook ,
BOOK_CHUNK ,
( part ) = > bookRequestSlots ( selectedRequestId , part ) ,
( done , total ) = > setProgress ( { label : "Бронирование" , done , total } ) ,
) ;
const collected = parts . flat ( ) ;
// Источник истины — ответ POST: BookedSlotDTO несёт bookedSlotId и тот же cycle, что мы
// отправили (= cycle рассчитанного слота). Контур берём из рассчитанного слота по ключу;
// by-request не перечитываем. Так bookedSlots получает и контур, и bookedSlotId.
const idByKey = new Map (
collected . map ( ( item ) = > [ ` ${ item . satelliteId } : ${ item . slotNum } : ${ item . cycle } ` , item . bookedSlotId ] as const ) ,
) ;
const justBooked : BookedSlotView [ ] = slots
. filter ( ( slot ) = > idByKey . has ( slotKey ( slot ) ) )
. map ( ( slot ) = > ( { . . . slot , bookedSlotId : idByKey.get ( slotKey ( slot ) ) ! } ) ) ;
setBookedSlots ( ( prev ) = > {
const merged = new Map ( prev . map ( ( slot ) = > [ slotKey ( slot ) , slot ] ) ) ;
for ( const slot of justBooked ) merged . set ( slotKey ( slot ) , slot ) ;
@@ -404,17 +449,18 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
setError ( caught instanceof Error ? caught . message : "Не удалось забронировать слоты." ) ;
} finally {
setBooking ( false ) ;
setBookProgress ( undefined ) ;
setProgress ( null ) ;
}
} ;
// Выбранные кандидаты под бронь (без уже забронированных — их бронировать незачем).
const collectSelected = ( ) : SlotBookingRequest [ ] = >
slots
. filter ( ( slot ) = > selectedKeys . has ( slotKey ( slot ) ) )
. filter ( ( slot ) = > selectedKeys . has ( slotKey ( slot ) ) && ! bookedKeys . has ( slotKey ( slot ) ) )
. map ( ( slot ) = > ( { satelliteId : slot.satelliteId , slotNum : slot.slotNumber , cycle : slot.cycle } ) ) ;
const book = ( ) = > {
if ( ! selectedRequestId || booking || selectedKeys . size === 0 ) return ;
if ( ! selectedRequestId || opBusy || selectedBookableCount === 0 ) return ;
const toBook = collectSelected ( ) ;
if ( toBook . length === 0 ) return ;
// Крупная бронь — сначала подтверждение, потом чанкованный POST.
@@ -432,23 +478,65 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
} ;
const cancelPendingBook = ( ) = > setPendingBook ( null ) ;
const cancelBooking = async ( ) = > {
if ( ! selectedRequestId || booking || bookedSlots . length === 0 ) return ;
setBooking ( true ) ;
// Общий чанковый движок снятия: бьёт ids на части (request-aware batch), показывает прогресс
// (тот же бар, что у брони), затем убирает снятые из bookedSlots и из выбора. На него опираются
// и «Снять выбранные», и «Снять всё», и ✕ в строке — снятие везде пачками с прогрессом, а не
// одним bulk-запросом (по одному HTTP нельзя показать ход на 7000 слотов). setBusy — флаг кнопки.
const runCancel = async ( ids : number [ ] , setBusy : ( value : boolean ) = > void ) = > {
setBusy ( true ) ;
setError ( undefined ) ;
setNotice ( undefined ) ;
setProgress ( ids . length > BOOK_CHUNK ? { label : "Снятие брони" , done : 0 , total : ids.length } : null ) ;
try {
const cancelled = await cancelRequestBooking ( selectedRequestId ) ;
// Снимаются все брони заявки — очищаем локально (by-request не перечитываем, см. book()).
setBookedSlots ( [ ] ) ;
setNotice ( ` Снято броней: ${ cancelled } . ` ) ;
const counts = await runChunked (
ids ,
BOOK_CHUNK ,
( part ) = > cancelBookedSlots ( selectedRequestId , part ) ,
( done , total ) = > setProgress ( { label : "Снятие брони" , done , total } ) ,
) ;
const removed = counts . reduce ( ( a , b ) = > a + b , 0 ) ;
const idSet = new Set ( ids ) ;
setSelectedKeys ( ( prev ) = > {
const next = new Set ( prev ) ;
for ( const slot of bookedSlots ) if ( idSet . has ( slot . bookedSlotId ) ) next . delete ( slotKey ( slot ) ) ;
return next ;
} ) ;
setBookedSlots ( ( prev ) = > prev . filter ( ( slot ) = > ! idSet . has ( slot . bookedSlotId ) ) ) ;
setNotice ( ` Снято броней: ${ removed } . ` ) ;
} catch ( caught ) {
setError ( caught instanceof Error ? caught . message : "Не удалось снять бронь." ) ;
} finally {
setBooking ( false ) ;
setBusy ( false ) ;
setProgress ( null ) ;
}
} ;
// Снять выбранные брони (по галочкам). Выбор явный — без подтверждения.
const cancelSelected = ( ) = > {
if ( ! selectedRequestId || opBusy || selectedBookedIds . length === 0 ) return ;
void runCancel ( selectedBookedIds , setCancellingSel ) ;
} ;
// Снять одну бронь из строки таблицы — батч из одного id (обратимо ре-бронью, без подтверждения).
const cancelOne = ( bookedSlotId : number ) = > {
if ( ! selectedRequestId || opBusy ) return ;
void runCancel ( [ bookedSlotId ] , setCancellingSel ) ;
} ;
// Снять ВСЕ брони заявки — тоже пачками (request-aware batch) с прогрессом. Необратимо →
// подтверждение. Отдельный флаг cancelling, чтобы «Снятие…» горело именно на этой кнопке.
const performCancelAll = ( ) = > runCancel ( bookedSlots . map ( ( slot ) = > slot . bookedSlotId ) , setCancelling ) ;
const cancelAll = ( ) = > {
if ( ! selectedRequestId || opBusy || bookedSlots . length === 0 ) return ;
setPendingCancel ( true ) ;
} ;
const confirmPendingCancel = ( ) = > {
setPendingCancel ( false ) ;
void performCancelAll ( ) ;
} ;
const cancelPendingCancel = ( ) = > setPendingCancel ( false ) ;
// Отметка слота из таблицы. Shift+клик — выбрать диапазон от якоря (предыдущего клика) до
// текущей строки; направление (выбрать/снять) задаётся новым состоянием текущей строки.
const handleSlotCheck = ( index : number , shiftKey : boolean ) = > {
@@ -471,7 +559,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
anchorIndexRef . current = null ;
} ;
// Выбрать все слоты расчёта (по всем суткам), снять весь выбор.
const selectAllDays = ( ) = > setSelectedKeys ( new Set ( slots . map ( slotKey ) ) ) ;
const selectAllDays = ( ) = > setSelectedKeys ( new Set ( displaySlots . map ( slotKey ) ) ) ;
const clearSelection = ( ) = > {
setSelectedKeys ( new Set ( ) ) ;
anchorIndexRef . current = null ;
@@ -563,8 +651,9 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
} ) ;
}
}
// Брони, которых нет в текущем расчёте (напр. до нажатия «Рассчитать»), — показываем отдельно.
const slotKeysShown = new Set ( slots . map ( slotKey ) ) ;
// Брони вне текущего окна суток (их нет в filteredSlots) — показываем на карте отдельным слоем,
// чтобы оператор видел все брони заявки. Те, что в окне, уже нарисованы циклом выше.
const slotKeysShown = new Set ( filteredSlots . map ( slotKey ) ) ;
for ( const slot of bookedSlots ) {
const key = slotKey ( slot ) ;
if ( slotKeysShown . has ( key ) ) continue ;
@@ -679,7 +768,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
? "Загрузка заявок…"
: undefined
: slots . length === 0 && bookedSlots . length === 0
? "Слоты не рассчитаны — нажмите «Рассчитать слоты »."
? "Слоты не рассчитаны — нажмите «Показать все слоты» или «Подобрать покрытие »."
: undefined ;
// Видимое окно строк виртуализатора и распорки сверху/снизу (держат суммарную высоту списка).
@@ -723,19 +812,11 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
< option value = "DESC" > н и с х о д я щ а я < / option >
< / select >
< / label >
< label className = "tgu-checkbox" title = "Считать площадь покрытия (дольше) ">
< input
type = "checkbox"
checked = { withCoverage }
onChange = { ( event ) = > setWithCoverage ( event . target . checked ) }
/ >
< span > П о к р ы т и е < / span >
< / label >
< button className = "tgu-button tgu-button--primary" type = "button" onClick = { calculate } disabled = { ! canCalculate } >
{ calculating ? "Расчёт…" : "Рассчитать слоты" }
< button className = "tgu-button tgu-button--primary" type = "button" onClick = { calculate } disabled = { ! canCalculate } title = "Показать весь набор слотов-кандидатов для ручного отбора ">
{ calculating ? "Расчёт…" : "Показать все слоты" }
< / button >
< label className = "tgu-field" title = "Стратегия автоформирования покрытия " >
< span > А в т о п о д б о р < / span >
< label className = "tgu-field" title = "Стратегия отбора покрытия для «Подобрать покрытие» " >
< span > С т р а т е г и я п о к р ы т и я < / span >
< select
className = "tgu-select"
value = { strategy }
@@ -746,28 +827,39 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
< option value = "CONTINUOUS" > н е п р е р ы в н ы й < / option >
< / select >
< / label >
< button className = "tgu-button" type = "button" onClick = { autoCover } disabled = { ! canCalculate } title = "Сформировать покрытие автоматическ и и предложить под бронь" >
{ autoFilling ? "Подбор…" : "Подобрать" }
< button className = "tgu-button" type = "button" onClick = { autoCover } disabled = { ! canCalculate } title = "Отобрать достаточное покрытие по стратеги и и предложить под бронь" >
{ autoFilling ? "Подбор…" : "Подобрать покрытие " }
< / button >
< button
className = "tgu-button"
type = "button"
onClick = { book }
disabled = { booking || selectedKeys . size === 0 }
disabled = { opBusy || selectedBookableCount === 0 }
title = "Забронировать выбранные слоты-кандидаты"
>
{ booking
? bookProgress
? ` Бронь… ${ bookProgress } `
: "Бронь…"
: ` Забронировать ${ selectedKeys . size > 0 ? ` ( ${ selectedKeys . size } ) ` : "" } ` }
? "Бронь…"
: ` Забронировать ${ selectedBookableCount > 0 ? ` ( ${ selectedBookableCount } ) ` : "" } ` }
< / button >
< button
className = "tgu-button"
type = "button"
onClick = { cancelBooking }
disabled = { booking || bookedSlots . length === 0 }
onClick = { cancelSelected }
disabled = { opBusy || selectedBookedIds . length === 0 }
title = "Снять бронь с выбранных слотов (пачками)"
>
С н я т ь б р о н ь { bookedSlots . length > 0 ? ` ( ${ bookedSlots . length } ) ` : "" }
{ cancellingSel
? "Снятие…"
: ` Снять выбранные ${ selectedBookedIds . length > 0 ? ` ( ${ selectedBookedIds . length } ) ` : "" } ` }
< / button >
< button
className = "tgu-button"
type = "button"
onClick = { cancelAll }
disabled = { opBusy || bookedSlots . length === 0 }
title = "Снять все брони заявки"
>
{ cancelling ? "Снятие…" : ` Снять всё ${ bookedSlots . length > 0 ? ` ( ${ bookedSlots . length } ) ` : "" } ` }
< / button >
< div className = "tgu-map-toolbar__spacer" style = { { flex : 1 } } / >
{ ( slots . length > 0 || bookedSlots . length > 0 ) && (
@@ -796,8 +888,8 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
className = "tgu-button tgu-button--sm"
type = "button"
onClick = { selectAllDays }
disabled = { slots . length === 0 }
title = "Выбрать все с лоты расчёта ( по всем суткам)"
disabled = { displaySlots . length === 0 }
title = "Выбрать все с троки (кандидаты и брони, по всем суткам)"
>
В с е с у т к и
< / button >
@@ -814,7 +906,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
< div className = "tgu-slots-scroll" ref = { listRef } >
{ filteredSlots . length === 0 ? (
< div className = "tgu-slots-empty" >
{ slots . length > 0 ? "На выбранные сутки слотов нет." : "Слоты не рассчитаны." }
{ displaySlots . length > 0 ? "На выбранные сутки слотов нет." : "Слоты не рассчитаны." }
< / div >
) : (
< table className = "tgu-slots-table" >
@@ -875,7 +967,23 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
< td > { slot . roll . toFixed ( 1 ) } ° < / td >
< td >
{ booked ? (
< span className = "tgu-slots-badge tgu-slots-badge--booked" > б р о н ь < / span >
< span className = "tgu-slots-status-cell" >
< span className = "tgu-slots-badge tgu-slots-badge--booked" > б р о н ь < / span >
< button
type = "button"
className = "tgu-slots-cancel-one"
title = "Снять бронь с этого слота"
aria-label = "Снять бронь с этого слота"
disabled = { opBusy }
onClick = { ( event ) = > {
event . stopPropagation ( ) ;
const id = bookedByKey . get ( key ) ? . bookedSlotId ;
if ( id != null ) cancelOne ( id ) ;
} }
>
✕
< / button >
< / span >
) : (
< span
className = {
@@ -984,6 +1092,22 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
< / div >
< / div >
) }
{ pendingCancel && (
< div className = "tgu-slots-confirm" >
< span > С н я т ь в с е б р о н и з а я в к и ( { bookedSlots . length } ) ? Д е й с т в и е н е о б р а т и м о . < / span >
< div className = "tgu-slots-confirm__actions" >
< button className = "tgu-button tgu-button--primary tgu-button--sm" type = "button" onClick = { confirmPendingCancel } >
С н я т ь в с ё
< / button >
< button className = "tgu-button tgu-button--sm" type = "button" onClick = { cancelPendingCancel } >
О т м е н а
< / button >
< / div >
< / div >
) }
{ progress && (
< ChunkProgress label = { progress . label } done = { progress . done } total = { progress . total } / >
) }
{ hasRequest && (
< button type = "button" className = "tgu-button tgu-slots-reset" onClick = { resetSelection } >
С б р о с