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:
@@ -13,8 +13,7 @@ import {
|
||||
validateMission,
|
||||
type SurveyRoute
|
||||
} from "../../api/missionPlanningApi";
|
||||
import { abandonDraft, createDraft, fetchSpacecraftPlans, promoteDraft } from "../../api/tguPlanApi";
|
||||
import { futureZrvWindows, toCreateDraftRequest, type ZrvWindow } from "./editorForkForward";
|
||||
import { abandonDraft, createDraft, promoteDraft } from "../../api/tguPlanApi";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
|
||||
@@ -122,10 +121,6 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
// Пока null — редактор показывает живой план read-only; после создания все правки и валидация
|
||||
// адресуются в этот DRAFT, а «Применить» делает его promote в живую цепочку.
|
||||
const [draftPlanId, setDraftPlanId] = useState<string>();
|
||||
// Будущие ЗРВ КА (проекция PLANNED) и выбранная цель fork-forward. Пусто/undefined — правка
|
||||
// текущего слота (replace); выбор окна заставляет ensureDraft создать черновик на следующей ЗРВ.
|
||||
const [futureWindows, setFutureWindows] = useState<ZrvWindow[]>([]);
|
||||
const [targetWindowId, setTargetWindowId] = useState<string>();
|
||||
const [passFrom, setPassFrom] = useState(1);
|
||||
const [passTo, setPassTo] = useState(999);
|
||||
const [flightLineData, setFlightLineData] = useState<FlightLinePoint[]>([]);
|
||||
@@ -228,10 +223,6 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
// «Применить» доступно, когда на сервере есть черновик (создаётся на первую сохранённую правку);
|
||||
// локальные несохранённые твики без DRAFT применять нечего.
|
||||
const dirty = Boolean(draftPlanId);
|
||||
const targetWindow = useMemo(
|
||||
() => futureWindows.find((window) => window.planId === targetWindowId),
|
||||
[futureWindows, targetWindowId]
|
||||
);
|
||||
const addedCount = draft.works.filter((work) => work.isNew).length;
|
||||
// Реальный состав плана берём из включений (plan modes), а не из черновика:
|
||||
// черновик сидируется заглушкой, когда у плана нет встроенных works.
|
||||
@@ -256,33 +247,11 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
setNotice(undefined);
|
||||
setApplyError(undefined);
|
||||
setPlanModes([]);
|
||||
setTargetWindowId(undefined);
|
||||
setPassFrom(1);
|
||||
setPassTo(999);
|
||||
setFlightLineData([]);
|
||||
}, [appliedRange, selectedPlan, selectedSpacecraftId]);
|
||||
|
||||
// Будущие ЗРВ-кандидаты для fork-forward: проекция КА из pcp-tgu-service (та же, с которой бэк
|
||||
// сверяет выбранное окно). Грузим при выборе плана; невозвратный план оператор перевыдаёт на одну
|
||||
// из этих ЗРВ. Сбой непрозрачен — список просто остаётся пустым (доступен только replace).
|
||||
useEffect(() => {
|
||||
if (!selectedPlan || !selectedSpacecraftId) {
|
||||
setFutureWindows([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void fetchSpacecraftPlans(selectedSpacecraftId)
|
||||
.then((plans) => {
|
||||
if (!cancelled) setFutureWindows(futureZrvWindows(plans, selectedPlan.planId, Date.now()));
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setFutureWindows([]);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedPlan, selectedSpacecraftId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noradId) return;
|
||||
let cancelled = false;
|
||||
@@ -484,31 +453,16 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
changeDraft((current) => updateWork(current, workId, patch));
|
||||
};
|
||||
|
||||
// Гарантирует наличие черновика: при первой правке клонирует базовый план в DRAFT и возвращает его id.
|
||||
// Без выбранной целевой ЗРВ — replace (тот же слот); с выбранной — fork-forward на следующую ЗРВ
|
||||
// (для невозвратного плана). Дальше все правки/валидация/promote адресуются именно сюда.
|
||||
// Гарантирует наличие черновика: при первой правке клонирует текущий план в DRAFT (replace, тот же
|
||||
// слот) и возвращает его id. Дальше все правки/валидация/promote адресуются именно сюда. Создание
|
||||
// плана на другую ЗРВ / новой цепочки вынесено во вкладку «Планы».
|
||||
const ensureDraft = useCallback(async (): Promise<string> => {
|
||||
if (draftPlanId) return draftPlanId;
|
||||
if (!selectedPlan) throw new Error("План не выбран — нечего редактировать.");
|
||||
const created = await createDraft(selectedPlan.planId, toCreateDraftRequest(targetWindow));
|
||||
const created = await createDraft(selectedPlan.planId, { mode: "replace" });
|
||||
setDraftPlanId(created.draftPlanId);
|
||||
return created.draftPlanId;
|
||||
}, [draftPlanId, selectedPlan, targetWindow]);
|
||||
|
||||
// Выбор целевой ЗРВ для fork-forward (до создания черновика): фиксирует окно и переводит
|
||||
// обзор редактора на него, чтобы оператор строил включения там, где будет жить новый план.
|
||||
const selectTargetWindow = useCallback(
|
||||
(planId: string | undefined) => {
|
||||
setTargetWindowId(planId);
|
||||
const window = planId ? futureWindows.find((item) => item.planId === planId) : undefined;
|
||||
if (window) {
|
||||
setWindow({ fromMs: window.startMs - 3 * HOUR_MS, toMs: window.endMs + 3 * HOUR_MS });
|
||||
} else if (selectedPlan) {
|
||||
setWindow(defaultEditorWindow(selectedPlan, appliedRange));
|
||||
}
|
||||
},
|
||||
[futureWindows, selectedPlan, appliedRange]
|
||||
);
|
||||
}, [draftPlanId, selectedPlan]);
|
||||
|
||||
// Бэкенд-валидация черновика (Ф4): авторитетные конфликты по режимам → карта по workId.
|
||||
// MVP-заглушка возвращает пусто; ошибки валидации не эскалируем в основной поток правок.
|
||||
@@ -828,10 +782,6 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
dirty={dirty}
|
||||
applying={applying}
|
||||
canceling={canceling}
|
||||
futureWindows={futureWindows}
|
||||
targetWindowId={targetWindowId}
|
||||
targetLocked={dirty}
|
||||
onSelectTargetWindow={selectTargetWindow}
|
||||
windowLabel={formatDuration(window.toMs - window.fromMs)}
|
||||
rightMode={rightMode}
|
||||
onSetRightMode={(mode) => {
|
||||
|
||||
Reference in New Issue
Block a user