From d9f82601202b00ca1ffdc2618061565ce8ce25a7 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 13:07:09 +0300 Subject: [PATCH] =?UTF-8?q?pcp-tgu:=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA?= =?UTF-8?q?=D0=B0=20=C2=AB=D0=9F=D0=BB=D0=B0=D0=BD=D1=8B=C2=BB=20=E2=80=94?= =?UTF-8?q?=20=D1=87=D0=B5=D1=80=D0=BD=D0=BE=D0=B2=D0=B8=D0=BA=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=B5=20=D0=BF=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20+=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= =?UTF-8?q?=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Кнопка «Создать черновик на основе плана» в деталях плана: создаёт replace-черновик (окно 1:1, все включения клонируются) и открывает редактор. Путь «сделать исправленный план» вместо повторной отправки. Доступна для персистентных правимых баз; DRAFT/PLANNED/LAID_IN исключены (для LAID_IN replace 1:1 запрещён). Если черновик по базе уже есть — открываем его. - Диалог создания плана цепочки: вместо списка будущих планов и «Отправить вручную» — кнопка «Редактировать» (сразу в редактор черновика). - Сайдбар: повторный клик по выбранному КА снимает выбор (показать все КА). --- .../tgu-planning/CreateChainPlanDialog.tsx | 56 +++++-------------- .../features/tgu-planning/TguPlanDetails.tsx | 22 ++++++++ .../features/tgu-planning/TguPlanningPage.tsx | 36 +++++++++++- .../src/features/tgu-planning/TguSidebar.tsx | 2 +- 4 files changed, 72 insertions(+), 44 deletions(-) diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-planning/CreateChainPlanDialog.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-planning/CreateChainPlanDialog.tsx index d6cb9b8..8a1e17e 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-planning/CreateChainPlanDialog.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-planning/CreateChainPlanDialog.tsx @@ -2,12 +2,11 @@ import { useEffect, useMemo, useState } from "react"; import { createChainPlan, fetchZrvWindows, - promoteDraft, type CreateChainPlanResponse } from "../../api/tguPlanApi"; import type { TguPlanUi } from "../../model/timelineTypes"; import { TguApiError } from "../../model/tguTypes"; -import { formatLocalDateTime, parseLocal } from "../../utils/localTime"; +import { formatLocalDateTime } from "../../utils/localTime"; import { buildChainPlanRequest, futureZrvOptions, type ZrvOption } from "./createChainWindows"; type CreateChainPlanDialogProps = { @@ -15,19 +14,18 @@ type CreateChainPlanDialogProps = { /** Планы для выбора базы-предыстории (берём persisted этого КА, кроме черновиков/проекции). */ plans: TguPlanUi[]; onClose: () => void; - /** Вызвать после ручной отправки созданного плана (для перезагрузки таймлайна). */ - onSent: () => void; /** Вызвать после создания черновика — чтобы он сразу появился на таймлайне (режим «Черновики»). */ onCreated?: () => void; + /** Открыть созданный черновик в редакторе (переход на страницу редактирования). */ + onEdit: (draftPlanId: string) => void; }; /** * Диалог «Создать план» (вкладка «Планы»): оператор берёт за основу прошлый план (или начинает - * цепочку без истории), набирает окно из двух реальных ЗРВ и создаёт план-кандидат. Сразу - * показывается прогноз будущей цепочки от него; кандидат становится рабочим после ручной отправки - * («Отправить вручную» = promote) и последующей приёмки. + * цепочку без истории), набирает окно из двух реальных ЗРВ и создаёт черновик плана. После + * создания сразу предлагается перейти к его редактированию. */ -export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, onCreated }: CreateChainPlanDialogProps) { +export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onCreated, onEdit }: CreateChainPlanDialogProps) { const [zrvOptions, setZrvOptions] = useState([]); const [zrvLoading, setZrvLoading] = useState(true); const [zrvError, setZrvError] = useState(); @@ -39,7 +37,6 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on const [creating, setCreating] = useState(false); const [error, setError] = useState(); const [result, setResult] = useState(); - const [sending, setSending] = useState(false); // Базы-предыстории: persisted планы этого КА (без проекции PLANNED и без черновиков). const baseOptions = useMemo( @@ -90,21 +87,6 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on } }; - const send = async () => { - if (!result) return; - setSending(true); - setError(undefined); - try { - await promoteDraft(result.draftPlanId); - onSent(); - onClose(); - } catch (err) { - setError(err instanceof TguApiError ? err.message : "Не удалось отправить план."); - } finally { - setSending(false); - } - }; - return (
@@ -178,33 +160,23 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on ) : ( <>
- План-кандидат создан (черновик {result.draftPlanId}). Прогноз будущей цепочки от него: + Черновик плана создан ({result.draftPlanId}).
-
    - {result.projectedChain.length === 0 ? ( -
  • Цепочка после этого плана пуста (нет дальнейших ЗРВ).
  • - ) : ( - result.projectedChain.map((plan) => ( -
  • - {formatLocalDateTime(parseLocal(plan.startTime))} – {formatLocalDateTime(parseLocal(plan.endTime))} ·{" "} - {plan.kppId} -
  • - )) - )} -
- - {error &&
{error}
}
-
- После отправки план уходит в выдачу; рабочим (якорем цепочки) он станет после приёмки. + Откроется страница редактирования черновика.
)} diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguPlanDetails.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguPlanDetails.tsx index 91b05bd..331e14f 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguPlanDetails.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguPlanDetails.tsx @@ -32,6 +32,8 @@ type TguPlanDetailsProps = { onEditDraft?: (planId: string) => void; onPromoteDraft?: (planId: string) => void; onDeleteDraft?: (planId: string) => void; + /** Создать черновик-копию на основе этого плана (replace, окно 1:1, все включения). */ + onCreateDraftFromPlan?: (planId: string) => void; onClose: () => void; }; @@ -47,6 +49,7 @@ export function TguPlanDetails({ onEditDraft, onPromoteDraft, onDeleteDraft, + onCreateDraftFromPlan, onClose }: TguPlanDetailsProps) { if (!plan) { @@ -64,6 +67,11 @@ export function TguPlanDetails({ const duration = formatDuration(plan.endMs - plan.startMs); // Окно решения закрылось, если с момента старта плана прошло больше grace-периода. const decisionWindowClosed = Date.now() > plan.startMs + DECISION_GRACE_MINUTES * 60_000; + // «Создать черновик на основе плана» (replace, окно 1:1, все включения) доступно для + // персистентных правимых баз. DRAFT уже имеет свои действия; PLANNED — виртуальная проекция, + // не база; LAID_IN — replace 1:1 запрещён (слот зафиксирован, только fork-forward). + const canCreateDraftFromPlan = + plan.status !== "DRAFT" && plan.status !== "PLANNED" && plan.status !== "LAID_IN"; const reject = () => { const reason = window.prompt("Reason для REJECTED", "UI_TEST_REJECTED"); @@ -163,6 +171,20 @@ export function TguPlanDetails({
))} + + {canCreateDraftFromPlan && onCreateDraftFromPlan && ( +
+ +
+ )} ); } diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguPlanningPage.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguPlanningPage.tsx index 75d0b68..4cf3435 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguPlanningPage.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguPlanningPage.tsx @@ -7,6 +7,7 @@ import { Tgu2DMapTab } from "../tgu-map-2d/Tgu2DMapTab"; import { TguRequestsTab } from "../tgu-requests/TguRequestsTab"; import { abandonDraft, + createDraft, fetchDraftPlans, fetchZrvWindows, promoteDraft, @@ -300,6 +301,33 @@ export function TguPlanningPage() { } }; + // Создать черновик-копию на основе существующего плана (replace, окно 1:1, все включения) и + // открыть его в редакторе. Это путь «сделать исправленный план», а не повторная отправка того же. + const handleCreateDraftFromPlan = async (planId: string) => { + // Один открытый черновик на базу: если он уже есть — открываем его, второй не плодим. + const existing = drafts.find((item) => item.basePlanId === planId); + if (existing) { + handleEditDraft(existing.planId); + return; + } + setDraftActionInFlight(true); + setDecisionError(undefined); + setDecisionNotice(undefined); + try { + const { draftPlanId } = await createDraft(planId, { mode: "replace" }); + await loadData(appliedRange); + const base = allPlans.find((item) => item.planId === planId); + setShowDrafts(true); + if (base) setSelectedSpacecraftId(base.spacecraftId); + setSelectedPlanId(draftPlanId); + setActiveTab("editor"); + } catch (error) { + setDecisionError(error instanceof Error ? error.message : "Не удалось создать черновик на основе плана."); + } finally { + setDraftActionInFlight(false); + } + }; + // Удалить черновик (abandon): убирает DRAFT и его клон-миссию. const handleDeleteDraft = async (planId: string) => { setDraftActionInFlight(true); @@ -414,6 +442,7 @@ export function TguPlanningPage() { onEditDraft={handleEditDraft} onPromoteDraft={handlePromoteDraft} onDeleteDraft={handleDeleteDraft} + onCreateDraftFromPlan={handleCreateDraftFromPlan} onClose={() => setSelectedPlanId(undefined)} /> )} @@ -423,11 +452,16 @@ export function TguPlanningPage() { spacecraftId={selectedSpacecraftId} plans={plans} onClose={() => setCreateChainOpen(false)} - onSent={() => void loadData(appliedRange)} onCreated={() => { setShowDrafts(true); void loadData(appliedRange); }} + onEdit={(draftPlanId) => { + setCreateChainOpen(false); + setSelectedPlanId(draftPlanId); + setDecisionNotice(undefined); + setActiveTab("editor"); + }} /> )}
diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguSidebar.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguSidebar.tsx index 213a665..adffc70 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguSidebar.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguSidebar.tsx @@ -53,7 +53,7 @@ export function TguSidebar({ className={`tgu-spacecraft ${selected ? "is-selected" : ""}`} type="button" key={platform.spacecraftId} - onClick={() => onSelectSpacecraft(platform.spacecraftId)} + onClick={() => onSelectSpacecraft(selected ? undefined : platform.spacecraftId)} >