From 802c966347d8260337d937d9bbef974f259b43be 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: Wed, 10 Jun 2026 16:13:59 +0300 Subject: [PATCH] =?UTF-8?q?fix(tgu-ui):=20=D0=BF=D0=BE=D0=B4=D1=85=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D1=81=D1=83=D1=89?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D0=B9=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=BD=D0=BE=D0=B2=D0=B8=D0=BA=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20?= =?UTF-8?q?=C2=ABConflict=C2=BB=20=D0=BF=D1=80=D0=B8=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8=20=D1=81=D1=8A=D1=91?= =?UTF-8?q?=D0=BC=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При первой правке плана ensureDraft→createDraft(replace) падал 409, если по КА уже есть открытый черновик на это окно (осиротевший с прошлой сессии — авто-отмена best-effort). В UI это показывалось голым «Conflict» (на стенде не подхвачен include-message у tgu). Теперь на 409 от createDraft редактор подхватывает существующий черновик того же КА с тем же окном (adoptExistingDraft через fetchDraftPlans, сверка окна ±2 c) и продолжает в нём; если подходящего нет — читаемое сообщение со ссылкой на вкладку «Планы» вместо «Conflict». --- .../src/features/tgu-editor/TguEditorTab.tsx | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) 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-заглушка возвращает пусто; ошибки валидации не эскалируем в основной поток правок.