pcp-tgu: редактор — расчёт плана съёмки+сброса одной операцией, очистка плана, удаление включений
Вкладка «Редактор»:
- Кнопка «Рассчитать план» — запускает surveys/calculate → drops/calculate
одной операцией на черновике (ensureDraft), затем перечитывает включения.
Источник комплексного плана (snapshot этапа 4) выбирает бэкенд по интервалу.
Вызовы идут напрямую в pcp-mission-planing-service (/api/pcp-mission).
Ручные маршруты съёмки (source=MANUAL) сохраняются при пересчёте.
- Кнопка «Очистить план» — новый эндпоинт DELETE /api/missions/{id}/modes
(clearPlan) сносит ВСЕ включения, включая ручные, с бронями и связями.
- Удаление маршрута съёмки / сброса в правой панели с подтверждением:
WorkInspector.canDelete показывает кнопку удаления и для рассчитанных
(read-only) включений, когда есть черновик; удаление через deleteMode.
This commit is contained in:
@@ -138,6 +138,76 @@ export async function saveDropRoute(planId: string, params: SaveDropRouteParams)
|
||||
return JSON.parse(text) as DropMode;
|
||||
}
|
||||
|
||||
/** Итог расчёта плана съёмки (ответ /surveys/calculate). Времена — локальные. */
|
||||
export type SurveyCalculationResult = {
|
||||
missionId: string;
|
||||
completed: boolean;
|
||||
comPlanSnapshotId: number | null;
|
||||
surveyModesCount: number;
|
||||
manualModesCount: number;
|
||||
complanModesCount: number;
|
||||
slotsModesCount: number;
|
||||
mixedModesCount: number;
|
||||
bookedSlotIdsCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Запуск расчёта плана съёмки для плана-черновика
|
||||
* (pcp-mission-planing-service, POST /api/missions/{planId}/surveys/calculate).
|
||||
* Источник комплексного плана (snapshot этапа 4) бэкенд выбирает сам по интервалу плана.
|
||||
* Пересчёт сохраняет ручные маршруты (source=MANUAL), затирая только пересчитываемые.
|
||||
*/
|
||||
export async function calculateSurveys(planId: string): Promise<SurveyCalculationResult> {
|
||||
const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/surveys/calculate`;
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, { method: "POST", headers: { Accept: "application/json" } });
|
||||
} catch {
|
||||
throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network");
|
||||
}
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new TguApiError(extractErrorText(text) || "Не удалось рассчитать съёмку.", "http", response.status);
|
||||
}
|
||||
return JSON.parse(text) as SurveyCalculationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск расчёта плана сбросов для плана-черновика по уже рассчитанным/добавленным съёмкам
|
||||
* (pcp-mission-planing-service, POST /api/missions/{planId}/drops/calculate). Возвращает 204.
|
||||
*/
|
||||
export async function calculateDrops(planId: string): Promise<void> {
|
||||
const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/drops/calculate`;
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, { method: "POST", headers: { Accept: "application/json" } });
|
||||
} catch {
|
||||
throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network");
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new TguApiError(extractErrorText(text) || "Не удалось рассчитать сбросы.", "http", response.status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Полная очистка плана: удаляет все включения (съёмки, включая ручные, и сбросы) плана-черновика
|
||||
* (pcp-mission-planing-service, DELETE /api/missions/{planId}/modes). Возвращает 204.
|
||||
*/
|
||||
export async function clearPlanModes(planId: string): Promise<void> {
|
||||
const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/modes`;
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, { method: "DELETE", headers: { Accept: "application/json" } });
|
||||
} catch {
|
||||
throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network");
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new TguApiError(extractErrorText(text) || "Не удалось очистить план.", "http", response.status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Перестроение контура существующего режима съёмки по новым параметрам окна/крена
|
||||
* (pcp-mission-planing-service, PATCH /api/missions/survey-modes/{modeId}). Правка включения
|
||||
|
||||
@@ -37,6 +37,8 @@ type WorkInspectorProps = {
|
||||
conflicts: string[];
|
||||
/** Включение существующего плана — показываем только информацию, без правок. */
|
||||
readOnly?: boolean;
|
||||
/** Разрешить удаление даже у read-only включения плана (есть черновик — режим живёт в нём). */
|
||||
canDelete?: boolean;
|
||||
onUpdate: (workId: string, patch: EditorWorkPatch) => void;
|
||||
onDelete: (workId: string) => void;
|
||||
onDuplicate: (workId: string) => void;
|
||||
@@ -131,6 +133,7 @@ export function WorkInspector({
|
||||
surveyModeLabel,
|
||||
conflicts,
|
||||
readOnly,
|
||||
canDelete,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onDuplicate,
|
||||
@@ -270,6 +273,14 @@ export function WorkInspector({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{readOnly && canDelete && (
|
||||
<div className="tgu-editor-panel__actions">
|
||||
<button className="tgu-button tgu-button--reject" type="button" onClick={() => onDelete(work.id)}>
|
||||
{work.kind === "downlink" ? "Удалить сброс" : "Удалить маршрут съёмки"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="tgu-editor-panel__color" style={{ background: color }} aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,11 +6,19 @@ type EditorToolbarProps = {
|
||||
selectedPlan?: TguPlanUi;
|
||||
platforms: TguPlatformUi[];
|
||||
checking: boolean;
|
||||
/** Идёт расчёт/очистка всего плана — блокирует кнопки операций над планом. */
|
||||
planCalcRunning: boolean;
|
||||
/** Почему нельзя рассчитывать/чистить план (заложен/стартовал) — гасит кнопки. */
|
||||
calcBlockedReason?: string;
|
||||
conflictCount: number;
|
||||
windowLabel: string;
|
||||
rightMode: "none" | "inspect" | "shoot" | "downlink" | "mapInfo";
|
||||
rightMode: "none" | "inspect" | "shoot" | "downlink" | "mapInfo" | "mapPick";
|
||||
onSetRightMode: (mode: "shoot" | "downlink") => void;
|
||||
onCheck: () => void;
|
||||
/** Рассчитать план съёмки и сброса одной операцией. */
|
||||
onCalculatePlan: () => void;
|
||||
/** Полностью очистить план (с подтверждением внутри обработчика). */
|
||||
onClearPlan: () => void;
|
||||
onZoom: (factor: number) => void;
|
||||
};
|
||||
|
||||
@@ -19,11 +27,15 @@ export function EditorToolbar({
|
||||
selectedPlan,
|
||||
platforms,
|
||||
checking,
|
||||
planCalcRunning,
|
||||
calcBlockedReason,
|
||||
conflictCount,
|
||||
windowLabel,
|
||||
rightMode,
|
||||
onSetRightMode,
|
||||
onCheck,
|
||||
onCalculatePlan,
|
||||
onClearPlan,
|
||||
onZoom
|
||||
}: EditorToolbarProps) {
|
||||
const platformLabel = selectedSpacecraftId
|
||||
@@ -61,6 +73,27 @@ export function EditorToolbar({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tgu-editor-toolbar__group">
|
||||
<button
|
||||
className="tgu-button tgu-button--primary"
|
||||
type="button"
|
||||
onClick={onCalculatePlan}
|
||||
disabled={!selectedPlan || planCalcRunning || Boolean(calcBlockedReason)}
|
||||
title={calcBlockedReason ?? "Рассчитать план съёмки и сброса по комплексному плану (ручные маршруты сохраняются)"}
|
||||
>
|
||||
{planCalcRunning ? "Расчёт..." : "Рассчитать план"}
|
||||
</button>
|
||||
<button
|
||||
className="tgu-button tgu-button--reject"
|
||||
type="button"
|
||||
onClick={onClearPlan}
|
||||
disabled={!selectedPlan || planCalcRunning || Boolean(calcBlockedReason)}
|
||||
title={calcBlockedReason ?? "Удалить все включения плана, включая построенные вручную"}
|
||||
>
|
||||
Очистить план
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tgu-editor-toolbar__spacer" />
|
||||
|
||||
<div className="tgu-editor-window-control" aria-label="Масштаб окна" title="Колесо мыши над таймлайном — тоже масштаб">
|
||||
|
||||
@@ -7,6 +7,9 @@ import { stationPasses, type OrbitPassInfo, type PassRange } from "../tgu-map-2d
|
||||
import { fetchPointVisibility, fetchSatelliteRva, type PointVisibilityParam, type RvaZone } from "../../api/ballisticsApi";
|
||||
import {
|
||||
buildSurveyRoute,
|
||||
calculateDrops,
|
||||
calculateSurveys,
|
||||
clearPlanModes,
|
||||
deleteMode,
|
||||
saveDropRoute,
|
||||
saveSurveyRoute,
|
||||
@@ -127,6 +130,9 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const [stationDtos, setStationDtos] = useState<StationDto[]>([]);
|
||||
const [serviceConflicts, setServiceConflicts] = useState<EditorConflictMap>({});
|
||||
const [checking, setChecking] = useState(false);
|
||||
// Идёт серверная операция над всем планом (расчёт съёмки+сброса или полная очистка) —
|
||||
// блокирует повторный запуск и кнопки правок, пока не перечитаем включения.
|
||||
const [planCalcRunning, setPlanCalcRunning] = useState(false);
|
||||
const [notice, setNotice] = useState<string>();
|
||||
const [applyError, setApplyError] = useState<string>();
|
||||
const [contextSpacecraftIds, setContextSpacecraftIds] = useState<Set<string>>(new Set());
|
||||
@@ -712,12 +718,71 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
}
|
||||
};
|
||||
|
||||
// Удаление включения: персистнутые режимы (с planModeId) удаляются в БД черновика (Ф3,
|
||||
// с каскадами drop↔survey), затем убираются из локального представления. Несохранённые
|
||||
// (planModeId нет) — только локально.
|
||||
// Запуск расчёта плана съёмки и сброса как одной операции (вкладка «Редактор»). Адресуем
|
||||
// в DRAFT (ленивое создание на первую правку), затем перечитываем включения. Пересчёт на бэке
|
||||
// сохраняет ручные маршруты съёмки (source=MANUAL) — они не затираются. Источник комплексного
|
||||
// плана (snapshot этапа 4) бэкенд выбирает сам по интервалу плана.
|
||||
const runPlanCalculation = async () => {
|
||||
if (!selectedPlan || planCalcRunning) return;
|
||||
setPlanCalcRunning(true);
|
||||
setApplyError(undefined);
|
||||
setNotice(undefined);
|
||||
try {
|
||||
const draftId = await ensureDraft();
|
||||
const surveyResult = await calculateSurveys(draftId);
|
||||
await calculateDrops(draftId);
|
||||
await refreshDraftModes(draftId);
|
||||
setNotice(
|
||||
`Расчёт выполнен: съёмок ${surveyResult.surveyModesCount} (ручных сохранено ${surveyResult.manualModesCount}); сбросы пересчитаны.`
|
||||
);
|
||||
} catch (error) {
|
||||
setApplyError(error instanceof Error ? error.message : "Не удалось рассчитать план.");
|
||||
} finally {
|
||||
setPlanCalcRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Полная очистка плана: сносит ВСЕ включения (съёмки, включая ручные, и сбросы) в БД черновика
|
||||
// и в локальном представлении. Единственный способ убрать ручные маршруты массово (помимо
|
||||
// ручного удаления по одному). Подтверждение — действие необратимо.
|
||||
const clearPlan = async () => {
|
||||
if (!selectedPlan || planCalcRunning) return;
|
||||
// window здесь затенён состоянием окна таймлайна — берём confirm из globalThis.
|
||||
if (!globalThis.confirm("Очистить план полностью? Будут удалены все включения, включая построенные вручную. Действие необратимо.")) {
|
||||
return;
|
||||
}
|
||||
setPlanCalcRunning(true);
|
||||
setApplyError(undefined);
|
||||
setNotice(undefined);
|
||||
try {
|
||||
const draftId = await ensureDraft();
|
||||
await clearPlanModes(draftId);
|
||||
await refreshDraftModes(draftId);
|
||||
changeDraft((current) => ({ ...current, works: [] }));
|
||||
setSelectedWorkId(undefined);
|
||||
setRightMode("none");
|
||||
setNotice("План полностью очищен.");
|
||||
} catch (error) {
|
||||
setApplyError(error instanceof Error ? error.message : "Не удалось очистить план.");
|
||||
} finally {
|
||||
setPlanCalcRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Удаление включения (правая панель, с подтверждением): персистнутые режимы (с planModeId)
|
||||
// удаляются в БД черновика (Ф3, с каскадами drop↔survey), затем убираются из локального
|
||||
// представления. Plan-origin включения адресуются по planModeId только когда черновик уже
|
||||
// существует (draftPlanId): тогда включения принадлежат именно ему. Несохранённые (planModeId
|
||||
// нет) — только локально.
|
||||
const handleDeleteWork = async (workId: string) => {
|
||||
const work = draft.works.find((item) => item.id === workId);
|
||||
if (work?.planModeId != null && draftPlanId) {
|
||||
const work = displayWorks.find((item) => item.id === workId);
|
||||
if (!work) return;
|
||||
const confirmMessage =
|
||||
work.kind === "downlink"
|
||||
? "Удалить включение сброса? Действие необратимо."
|
||||
: "Удалить маршрут съёмки? Действие необратимо.";
|
||||
if (!globalThis.confirm(confirmMessage)) return;
|
||||
if (work.planModeId != null && draftPlanId) {
|
||||
try {
|
||||
await deleteMode(work.planModeId);
|
||||
await refreshDraftModes(draftPlanId);
|
||||
@@ -900,6 +965,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
surveyModeLabel={surveyModeLabel}
|
||||
conflicts={conflicts[work.id] ?? []}
|
||||
readOnly={work.origin === "plan"}
|
||||
canDelete={draftPlanId != null}
|
||||
onUpdate={onChangeWork}
|
||||
onDelete={(workId) => { void handleDeleteWork(workId); }}
|
||||
onDuplicate={(workId) => {
|
||||
@@ -943,6 +1009,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
selectedPlan={selectedPlan}
|
||||
platforms={platforms}
|
||||
checking={checking}
|
||||
planCalcRunning={planCalcRunning}
|
||||
calcBlockedReason={inPlaceEditBlockedReason}
|
||||
conflictCount={conflictCount(conflicts)}
|
||||
windowLabel={formatDuration(window.toMs - window.fromMs)}
|
||||
rightMode={rightMode}
|
||||
@@ -954,6 +1022,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
setNotice(undefined);
|
||||
}}
|
||||
onCheck={runCheck}
|
||||
onCalculatePlan={() => { void runPlanCalculation(); }}
|
||||
onClearPlan={() => { void clearPlan(); }}
|
||||
onZoom={zoom}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user