From bd2bc9d583db5fddbc9ae762db946fcbf0efd50b 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: Thu, 4 Jun 2026 21:03:44 +0300 Subject: [PATCH] =?UTF-8?q?pcp-tgu:=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=20=E2=80=94=20=D1=80=D0=B0=D1=81=D1=87=D1=91=D1=82?= =?UTF-8?q?=20=D0=BF=D0=BB=D0=B0=D0=BD=D0=B0=20=D1=81=D1=8A=D1=91=D0=BC?= =?UTF-8?q?=D0=BA=D0=B8+=D1=81=D0=B1=D1=80=D0=BE=D1=81=D0=B0=20=D0=BE?= =?UTF-8?q?=D0=B4=D0=BD=D0=BE=D0=B9=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B5=D0=B9,=20=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BB=D0=B0=D0=BD=D0=B0,=20=D1=83=D0=B4=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вкладка «Редактор»: - Кнопка «Рассчитать план» — запускает surveys/calculate → drops/calculate одной операцией на черновике (ensureDraft), затем перечитывает включения. Источник комплексного плана (snapshot этапа 4) выбирает бэкенд по интервалу. Вызовы идут напрямую в pcp-mission-planing-service (/api/pcp-mission). Ручные маршруты съёмки (source=MANUAL) сохраняются при пересчёте. - Кнопка «Очистить план» — новый эндпоинт DELETE /api/missions/{id}/modes (clearPlan) сносит ВСЕ включения, включая ручные, с бронями и связями. - Удаление маршрута съёмки / сброса в правой панели с подтверждением: WorkInspector.canDelete показывает кнопку удаления и для рассчитанных (read-only) включений, когда есть черновик; удаление через deleteMode. --- .../controller/MissionController.kt | 21 +++++ .../service/MissionPlaningService.kt | 15 ++++ .../src/api/missionPlanningApi.ts | 70 ++++++++++++++++ .../src/features/tgu-editor/EditorPanels.tsx | 11 +++ .../src/features/tgu-editor/EditorToolbar.tsx | 35 +++++++- .../src/features/tgu-editor/TguEditorTab.tsx | 80 +++++++++++++++++-- 6 files changed, 226 insertions(+), 6 deletions(-) diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt index 0edc981..0882499 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt @@ -252,6 +252,27 @@ class MissionController( return ResponseEntity.noContent().build() } + @Operation( + summary = "Полностью очистить план", + description = "Удаляет ВСЕ включения плана миссии: съёмки (включая построенные вручную, source=MANUAL) " + + "и сбросы, вместе с бронями слотов и связями съёмка↔сброс. В отличие от пересчёта плана, " + + "ручные маршруты тоже удаляются. Используется кнопкой «Очистить план» в редакторе." + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "204", description = "План очищен"), + ApiResponse(responseCode = "404", description = "Миссия не найдена") + ] + ) + @DeleteMapping("/{mission_id}/modes") + fun clearPlan( + @Parameter(description = "Идентификатор миссии") + @PathVariable("mission_id") id: UUID + ): ResponseEntity { + missionPlaningService.clearPlan(id) + return ResponseEntity.noContent().build() + } + @Operation( summary = "Проверить миссию-черновик на конфликты", description = "Считает конфликты/наложения включений плана-черновика и возвращает дифф по " + diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt index 607d63b..5b0aeb7 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt @@ -936,6 +936,21 @@ class MissionPlaningService( logger.info("Удалён сброс (drop_mode id={}), снято связей со съёмками={}", dropId, links.size) } + /** + * Полная очистка плана: удаляет ВСЕ включения миссии — съёмки (включая ручные, source=MANUAL) + * и сбросы — вместе с бронями слотов и связями съёмка↔сброс. В отличие от пересчёта плана + * ([loadSlots]/[calculateDrops]), ручные маршруты тоже удаляются. Сбросы удаляются первыми, + * чтобы снять связи до удаления съёмок и не плодить проверку осиротевших сбросов. + */ + @Transactional + fun clearPlan(missionId: UUID) { + missionRepository.findById(missionId).orElseThrow { notFound(missionId) } + val modes = modeRepository.findAllByPlan_Mission_MissionIdOrderByTimeStartAsc(missionId) + modes.filterIsInstance().forEach { deleteDropMode(it) } + modes.filterIsInstance().forEach { deleteSurveyMode(it) } + logger.info("План миссии {} полностью очищен: удалено включений={}", missionId, modes.size) + } + /** * Валидация миссии-черновика на конфликты/наложения включений. * diff --git a/services/pcp-tgu-ops-ui/src/api/missionPlanningApi.ts b/services/pcp-tgu-ops-ui/src/api/missionPlanningApi.ts index 574a2f4..87700f9 100644 --- a/services/pcp-tgu-ops-ui/src/api/missionPlanningApi.ts +++ b/services/pcp-tgu-ops-ui/src/api/missionPlanningApi.ts @@ -138,6 +138,76 @@ export async function saveDropRoute(planId: string, params: SaveDropRouteParams) return JSON.parse(text) as DropMode; } +/** Итог расчёта плана съёмки (ответ /surveys/calculate). Времена — локальные. */ +export type SurveyCalculationResult = { + missionId: string; + completed: boolean; + comPlanSnapshotId: number | null; + surveyModesCount: number; + manualModesCount: number; + complanModesCount: number; + slotsModesCount: number; + mixedModesCount: number; + bookedSlotIdsCount: number; +}; + +/** + * Запуск расчёта плана съёмки для плана-черновика + * (pcp-mission-planing-service, POST /api/missions/{planId}/surveys/calculate). + * Источник комплексного плана (snapshot этапа 4) бэкенд выбирает сам по интервалу плана. + * Пересчёт сохраняет ручные маршруты (source=MANUAL), затирая только пересчитываемые. + */ +export async function calculateSurveys(planId: string): Promise { + const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/surveys/calculate`; + let response: Response; + try { + response = await fetch(url, { method: "POST", headers: { Accept: "application/json" } }); + } catch { + throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network"); + } + const text = await response.text(); + if (!response.ok) { + throw new TguApiError(extractErrorText(text) || "Не удалось рассчитать съёмку.", "http", response.status); + } + return JSON.parse(text) as SurveyCalculationResult; +} + +/** + * Запуск расчёта плана сбросов для плана-черновика по уже рассчитанным/добавленным съёмкам + * (pcp-mission-planing-service, POST /api/missions/{planId}/drops/calculate). Возвращает 204. + */ +export async function calculateDrops(planId: string): Promise { + const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/drops/calculate`; + let response: Response; + try { + response = await fetch(url, { method: "POST", headers: { Accept: "application/json" } }); + } catch { + throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network"); + } + if (!response.ok) { + const text = await response.text(); + throw new TguApiError(extractErrorText(text) || "Не удалось рассчитать сбросы.", "http", response.status); + } +} + +/** + * Полная очистка плана: удаляет все включения (съёмки, включая ручные, и сбросы) плана-черновика + * (pcp-mission-planing-service, DELETE /api/missions/{planId}/modes). Возвращает 204. + */ +export async function clearPlanModes(planId: string): Promise { + const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/modes`; + let response: Response; + try { + response = await fetch(url, { method: "DELETE", headers: { Accept: "application/json" } }); + } catch { + throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network"); + } + if (!response.ok) { + const text = await response.text(); + throw new TguApiError(extractErrorText(text) || "Не удалось очистить план.", "http", response.status); + } +} + /** * Перестроение контура существующего режима съёмки по новым параметрам окна/крена * (pcp-mission-planing-service, PATCH /api/missions/survey-modes/{modeId}). Правка включения diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx index 74769bc..7ccb17c 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx @@ -37,6 +37,8 @@ type WorkInspectorProps = { conflicts: string[]; /** Включение существующего плана — показываем только информацию, без правок. */ readOnly?: boolean; + /** Разрешить удаление даже у read-only включения плана (есть черновик — режим живёт в нём). */ + canDelete?: boolean; onUpdate: (workId: string, patch: EditorWorkPatch) => void; onDelete: (workId: string) => void; onDuplicate: (workId: string) => void; @@ -131,6 +133,7 @@ export function WorkInspector({ surveyModeLabel, conflicts, readOnly, + canDelete, onUpdate, onDelete, onDuplicate, @@ -270,6 +273,14 @@ export function WorkInspector({ )} + {readOnly && canDelete && ( +
+ +
+ )} +