From 586999469027a5149c01cd3f8c53269f7ef229ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D1=8C=D0=B5=D0=B2?= Date: Mon, 15 Jun 2026 17:22:35 +0300 Subject: [PATCH] =?UTF-8?q?feat(tgu-slots):=20=D0=BF=D0=BE=D0=B4=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=D0=B8=20=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0?= =?UTF-8?q?=20+=20=C2=AB=D0=A1=D0=BD=D1=8F=D1=82=D1=8C=20=D0=B2=D1=81?= =?UTF-8?q?=D1=91=C2=BB=20=D1=81=D0=BD=D0=B8=D0=BC=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B2=D1=81=D1=8E=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Бронь теперь всегда через диалог подтверждения (симметрично «Снять всё»), убран порог в 500 слотов. - «Снять всё» дочитывает брони заявки через by-request и снимает объединение локального и серверного наборов bookedSlotId — гарантированно вся заявка. - Карта: забронированные слоты вне окна суток подчинены фильтрам видимости (статус «бронь»/КА/ветвь) — при выключенном фильтре брони их не видно. --- .../src/features/tgu-slots/TguSlotsTab.tsx | 80 ++++++++++++------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx b/services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx index 71ad41c..b1cb8aa 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx +++ b/services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx @@ -36,8 +36,6 @@ const DAY_MS = 24 * 60 * 60 * 1000; /** Размер чанка при бронировании больших наборов (бэк бронирует слоты по одному). */ const BOOK_CHUNK = 200; -/** Выше этого числа слотов бронь требует подтверждения. */ -const BOOK_CONFIRM_THRESHOLD = 500; /** Оценка высоты строки таблицы (px) для виртуализатора. */ const ROW_HEIGHT = 34; @@ -517,12 +515,8 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) { if (!selectedRequestId || opBusy || selectedBookableCount === 0) return; const toBook = collectSelected(); if (toBook.length === 0) return; - // Крупная бронь — сначала подтверждение, потом чанкованный POST. - if (toBook.length > BOOK_CONFIRM_THRESHOLD) { - setPendingBook(toBook); - return; - } - void performBook(toBook); + // Бронь — всегда через подтверждение (симметрично «Снять всё»), затем чанкованный POST. + setPendingBook(toBook); }; const confirmPendingBook = () => { @@ -579,7 +573,19 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) { // Снять ВСЕ брони заявки — тоже пачками (request-aware batch) с прогрессом. Необратимо → // подтверждение. Отдельный флаг cancelling, чтобы «Снятие…» горело именно на этой кнопке. - const performCancelAll = () => runCancel(bookedSlots.map((slot) => slot.bookedSlotId), setCancelling); + // Источник истины — backend: перечитываем брони by-request и снимаем ОБЪЕДИНЕНИЕ локального + // набора и серверного. Так «Снять всё» убирает вообще все брони заявки, даже если локальный + // bookedSlots разошёлся с базой (bookedSlotId надёжен независимо от cycle-фрейма брони). + const performCancelAll = async () => { + const idSet = new Set(bookedSlots.map((slot) => slot.bookedSlotId)); + try { + const fresh = await fetchBookedSlotsByRequest(selectedRequestId); + for (const slot of fresh) idSet.add(slot.bookedSlotId); + } catch { + // by-request недоступен — снимаем по локальному набору броней. + } + await runCancel(Array.from(idSet), setCancelling); + }; const cancelAll = () => { if (!selectedRequestId || opBusy || bookedSlots.length === 0) return; @@ -730,27 +736,45 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) { } } // Брони вне текущего окна суток (их нет в filteredSlots) — показываем на карте отдельным слоем, - // чтобы оператор видел все брони заявки. Те, что в окне, уже нарисованы циклом выше. - const slotKeysShown = new Set(filteredSlots.map(slotKey)); - for (const slot of bookedSlots) { - const key = slotKey(slot); - if (slotKeysShown.has(key)) continue; - for (const ring of ringsFor(key, slot.contour)) { - if (ring.length < 3) continue; - polygons.push({ - id: `booked:${key}`, - name: `КА ${slot.satelliteId} · слот ${slot.slotNumber} (бронь)`, - kind: "complexPlan", - layer: "complexPlan", - points: ring, - spacecraftId: String(slot.satelliteId), - revolution: slot.revolution, - zIndex: z++ - }); + // чтобы оператор видел все брони заявки. Те, что в окне, уже нарисованы циклом выше. НО слой + // подчиняем фильтрам видимости: выключен статус «бронь» (или скрыт КА/ветвь) — бронь не рисуем, + // иначе фильтр убирал бы бронь из таблицы, но не с карты. + if (statusFilter.has("booked")) { + const slotKeysShown = new Set(filteredSlots.map(slotKey)); + for (const slot of bookedSlots) { + const key = slotKey(slot); + if (slotKeysShown.has(key)) continue; + if (!satFilter.has(slot.satelliteId)) continue; + if (!branchFilter.has(slot.revolutionSign)) continue; + for (const ring of ringsFor(key, slot.contour)) { + if (ring.length < 3) continue; + polygons.push({ + id: `booked:${key}`, + name: `КА ${slot.satelliteId} · слот ${slot.slotNumber} (бронь)`, + kind: "complexPlan", + layer: "complexPlan", + points: ring, + spacecraftId: String(slot.satelliteId), + revolution: slot.revolution, + zIndex: z++ + }); + } } } return { polygons, lines: [], markers: [] }; - }, [hasRequest, requestItems, selectedRequest, slots, filteredSlots, bookedSlots, bookedKeys, ringsFor]); + }, [ + hasRequest, + requestItems, + selectedRequest, + slots, + filteredSlots, + bookedSlots, + bookedKeys, + ringsFor, + statusFilter, + satFilter, + branchFilter + ]); const layers = useMemo( () => ({ @@ -1186,7 +1210,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) { )} {pendingBook && (
- Забронировать {pendingBook.length} слотов? Это крупная бронь. + Забронировать выбранные слоты ({pendingBook.length})?