pcp: создание плана новой цепочки из вкладки «Планы» (база/без истории, окно из двух ЗРВ)

Оператор берёт за основу прошлый план (предыстория) или начинает цепочку с нуля,
набирает окно из двух реальных ЗРВ и создаёт план-кандидат; сразу показывается прогноз
будущей цепочки от него. Рабочим (якорем) план становится после ручной отправки (promote)
и приёмки — отдельного механизма якорения не вводим.

pcp-tgu-service:
- сырые ЗРВ: PlanCalculationService.rawWindows + PlanProjectionService.rawZrvWindows,
  GET /api/plans/{spacecraftId}/zrv (исходные окна снапшота, не cooked-проекция);
- CreateChainPlanUseCase + POST /api/plans/new-chain: опциональная база (клон включений
  в окно) либо пустая миссия без истории; прогноз цепочки через projectAfterWindow;
- ZrvWindowMatcher (старт+КПП одной зоны, конец — другой); assertDraftSlotStillValid
  переведён на него — единая проверка для replace/fork-forward и окна из двух ЗРВ;
- тесты: CreateChainPlanUseCaseTest (8), ZrvWindowMatcherTest (7), правки promote-тестов.

pcp-mission-planing-service:
- POST /api/missions/blank — пустая миссия для сценария «без истории» (+ 2 теста).

pcp-tgu-ops-ui:
- API fetchZrvWindows/createChainPlan; диалог «Создать план» во вкладке «Планы»
  (выбор базы, стартовой и конечной ЗРВ) с показом спрогнозированной цепочки и ручной
  отправкой; подсветка ЗРВ выбранного КА на таймлайне;
- «Целевая ЗРВ» убрана из редактора (он остаётся под replace-правку); удалён ставший
  мёртвым editorForkForward; createChainWindows + тесты.
This commit is contained in:
Дмитрий Соловьев
2026-06-03 16:25:46 +03:00
parent 057452f19d
commit 9c2d8516bc
26 changed files with 1316 additions and 240 deletions
@@ -1,7 +1,5 @@
import type { TguPlanUi, TguPlatformUi } from "../../model/timelineTypes";
import { formatDuration } from "./editorPresentation";
import type { ZrvWindow } from "./editorForkForward";
import { formatUtcDateTime } from "../../utils/utcTime";
type EditorToolbarProps = {
selectedSpacecraftId?: string;
@@ -15,10 +13,6 @@ type EditorToolbarProps = {
dirty: boolean;
applying: boolean;
canceling: boolean;
futureWindows: ZrvWindow[];
targetWindowId?: string;
targetLocked: boolean;
onSelectTargetWindow: (planId: string | undefined) => void;
windowLabel: string;
rightMode: "none" | "inspect" | "shoot" | "downlink" | "mapInfo";
onSetRightMode: (mode: "shoot" | "downlink") => void;
@@ -43,10 +37,6 @@ export function EditorToolbar({
dirty,
applying,
canceling,
futureWindows,
targetWindowId,
targetLocked,
onSelectTargetWindow,
windowLabel,
rightMode,
onSetRightMode,
@@ -93,25 +83,6 @@ export function EditorToolbar({
</button>
</div>
{(futureWindows.length > 0 || targetWindowId) && (
<label className="tgu-editor-target-zrv" title="Целевая ЗРВ для перевыдачи невозвратного плана">
<span className="tgu-editor-target-zrv__label">Целевая ЗРВ</span>
<select
className="tgu-select"
value={targetWindowId ?? ""}
disabled={targetLocked}
onChange={(event) => onSelectTargetWindow(event.target.value || undefined)}
>
<option value="">Текущий слот (правка)</option>
{futureWindows.map((window) => (
<option key={window.planId} value={window.planId}>
{formatUtcDateTime(window.startMs)} · {window.kppId}
</option>
))}
</select>
</label>
)}
<div className="tgu-editor-toolbar__spacer" />
<div className="tgu-editor-window-control" aria-label="Окно редактора">