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 && ( +
+ +
+ )} +