9c2d8516bc
Оператор берёт за основу прошлый план (предыстория) или начинает цепочку с нуля,
набирает окно из двух реальных ЗРВ и создаёт план-кандидат; сразу показывается прогноз
будущей цепочки от него. Рабочим (якорем) план становится после ручной отправки (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 + тесты.
131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
import type { TguPlanUi, TguPlatformUi } from "../../model/timelineTypes";
|
||
import { formatDuration } from "./editorPresentation";
|
||
|
||
type EditorToolbarProps = {
|
||
selectedSpacecraftId?: string;
|
||
selectedPlan?: TguPlanUi;
|
||
platforms: TguPlatformUi[];
|
||
canUndo: boolean;
|
||
canRedo: boolean;
|
||
checking: boolean;
|
||
conflictCount: number;
|
||
addedCount: number;
|
||
dirty: boolean;
|
||
applying: boolean;
|
||
canceling: boolean;
|
||
windowLabel: string;
|
||
rightMode: "none" | "inspect" | "shoot" | "downlink" | "mapInfo";
|
||
onSetRightMode: (mode: "shoot" | "downlink") => void;
|
||
onUndo: () => void;
|
||
onRedo: () => void;
|
||
onCheck: () => void;
|
||
onApply: () => void;
|
||
onCancel: () => void;
|
||
onZoom: (factor: number) => void;
|
||
onPan: (factor: number) => void;
|
||
};
|
||
|
||
export function EditorToolbar({
|
||
selectedSpacecraftId,
|
||
selectedPlan,
|
||
platforms,
|
||
canUndo,
|
||
canRedo,
|
||
checking,
|
||
conflictCount,
|
||
addedCount,
|
||
dirty,
|
||
applying,
|
||
canceling,
|
||
windowLabel,
|
||
rightMode,
|
||
onSetRightMode,
|
||
onUndo,
|
||
onRedo,
|
||
onCheck,
|
||
onApply,
|
||
onCancel,
|
||
onZoom,
|
||
onPan
|
||
}: EditorToolbarProps) {
|
||
const platformLabel = selectedSpacecraftId
|
||
? platforms.find((platform) => platform.spacecraftId === selectedSpacecraftId)?.name ?? selectedSpacecraftId
|
||
: "КА не выбран";
|
||
|
||
return (
|
||
<div className="tgu-editor-toolbar">
|
||
<div className="tgu-editor-toolbar__identity">
|
||
<span className="tgu-editor-type-dot" aria-hidden="true" />
|
||
<div>
|
||
<div className="tgu-editor-toolbar__title">{platformLabel}</div>
|
||
<div className="tgu-editor-toolbar__meta">
|
||
{selectedPlan ? `${selectedPlan.planId} · ${selectedPlan.status}` : "Выберите план на timeline"}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="tgu-editor-toolbar__group">
|
||
<button
|
||
className={`tgu-button ${rightMode === "shoot" ? "tgu-button--primary" : ""}`}
|
||
type="button"
|
||
onClick={() => onSetRightMode("shoot")}
|
||
disabled={!selectedSpacecraftId}
|
||
>
|
||
+ Съёмка
|
||
</button>
|
||
<button
|
||
className={`tgu-button ${rightMode === "downlink" ? "tgu-button--primary" : ""}`}
|
||
type="button"
|
||
onClick={() => onSetRightMode("downlink")}
|
||
disabled={!selectedSpacecraftId}
|
||
>
|
||
+ Сброс
|
||
</button>
|
||
</div>
|
||
|
||
<div className="tgu-editor-toolbar__spacer" />
|
||
|
||
<div className="tgu-editor-window-control" aria-label="Окно редактора">
|
||
<button type="button" onClick={() => onPan(-0.25)} title="Назад по времени">◀</button>
|
||
<button type="button" onClick={() => onZoom(1.4)} title="Отдалить">−</button>
|
||
<span>{windowLabel || formatDuration(0)}</span>
|
||
<button type="button" onClick={() => onZoom(0.7)} title="Приблизить">+</button>
|
||
<button type="button" onClick={() => onPan(0.25)} title="Вперёд по времени">▶</button>
|
||
</div>
|
||
|
||
<div className="tgu-editor-toolbar__group">
|
||
<button className="tgu-icon-button" type="button" onClick={onUndo} disabled={!canUndo} title="Отменить">
|
||
↶
|
||
</button>
|
||
<button className="tgu-icon-button" type="button" onClick={onRedo} disabled={!canRedo} title="Вернуть">
|
||
↷
|
||
</button>
|
||
</div>
|
||
|
||
<button
|
||
className={`tgu-button ${conflictCount > 0 ? "tgu-editor-button--warning" : ""}`}
|
||
type="button"
|
||
onClick={onCheck}
|
||
disabled={checking || !selectedSpacecraftId}
|
||
>
|
||
{checking ? "Проверка..." : conflictCount > 0 ? `Конфликты · ${conflictCount}` : "Проверить"}
|
||
</button>
|
||
|
||
{dirty && (
|
||
<button
|
||
className="tgu-button tgu-button--reject"
|
||
type="button"
|
||
onClick={onCancel}
|
||
disabled={canceling || applying}
|
||
>
|
||
{canceling ? "Отмена..." : "Отмена"}
|
||
</button>
|
||
)}
|
||
|
||
<button className="tgu-button tgu-button--primary" type="button" onClick={onApply} disabled={!dirty || applying || canceling}>
|
||
{applying ? "Применение..." : `Применить${addedCount > 0 ? ` · +${addedCount}` : ""}`}
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|