diff --git a/services/pcp-tgu-ui-service/src/features/tgu-editor/TguEditorTab.tsx b/services/pcp-tgu-ui-service/src/features/tgu-editor/TguEditorTab.tsx index b54a82d..59533fa 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-editor/TguEditorTab.tsx +++ b/services/pcp-tgu-ui-service/src/features/tgu-editor/TguEditorTab.tsx @@ -15,7 +15,9 @@ import { validateMission, type SurveyRoute } from "../../api/missionPlanningApi"; -import { abandonDraft, createDraft } from "../../api/tguPlanApi"; +import { abandonDraft, createDraft, fetchDraftPlans } from "../../api/tguPlanApi"; +import { TguApiError } from "../../model/tguTypes"; +import type { TguPlanUi } from "../../model/timelineTypes"; import { parseLocal, toLocalIso } from "../../utils/localTime"; import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView"; import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser"; @@ -627,6 +629,19 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, // Гарантирует наличие черновика: при первой правке клонирует текущий план в DRAFT (replace, тот же // слот) и возвращает его id. Дальше все правки/валидация/promote адресуются именно сюда. Создание // плана на другую ЗРВ / новой цепочки вынесено во вкладку «Планы». + // Ищет уже открытый DRAFT этого КА с тем же окном, что у базового плана (replace 1:1 совпадает + // по окну). Используется при 409 от createDraft, чтобы подхватить осиротевший черновик. + const adoptExistingDraft = useCallback(async (basePlan: TguPlanUi): Promise => { + if (!basePlan.spacecraftId) return undefined; + const drafts = await fetchDraftPlans(basePlan.spacecraftId).catch(() => [] as Awaited>); + const match = drafts.find( + (draft) => + Math.abs(parseLocal(draft.startTime) - basePlan.startMs) < 2000 && + Math.abs(parseLocal(draft.endTime) - basePlan.endMs) < 2000 + ); + return match?.planId; + }, []); + const ensureDraft = useCallback(async (): Promise => { if (draftPlanId) return draftPlanId; if (!selectedPlan) throw new Error("План не выбран — нечего редактировать."); @@ -635,11 +650,32 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, setDraftPlanId(selectedPlan.planId); return selectedPlan.planId; } - const created = await createDraft(selectedPlan.planId, { mode: "replace" }); - createdImplicitlyRef.current = true; - setDraftPlanId(created.draftPlanId); - return created.draftPlanId; - }, [draftPlanId, selectedPlan]); + try { + const created = await createDraft(selectedPlan.planId, { mode: "replace" }); + createdImplicitlyRef.current = true; + setDraftPlanId(created.draftPlanId); + return created.draftPlanId; + } catch (error) { + // 409: по этому КА уже открыт черновик на это же окно (осиротевший после прошлой сессии — + // авто-отмена при уходе best-effort и могла не отработать). Вместо тупиковой ошибки + // «Conflict» подхватываем существующий черновик того же окна и правим его (replace 1:1 — + // окно совпадает с базой). Не нашли подходящий — пробрасываем читаемую ошибку. + if (error instanceof TguApiError && error.status === 409) { + const adopted = await adoptExistingDraft(selectedPlan); + if (adopted) { + createdImplicitlyRef.current = true; + setDraftPlanId(adopted); + return adopted; + } + throw new TguApiError( + "По этому КА уже есть открытый черновик на это окно. Отмените его во вкладке «Планы» (раздел «Черновики») или продолжите редактирование там.", + "http", + 409, + ); + } + throw error; + } + }, [draftPlanId, selectedPlan, adoptExistingDraft]); // Бэкенд-валидация черновика (Ф4): авторитетные конфликты по режимам → карта по workId. // MVP-заглушка возвращает пусто; ошибки валидации не эскалируем в основной поток правок.