diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx index 1426371..b7cddc8 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx @@ -127,6 +127,9 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, const [passFrom, setPassFrom] = useState(1); const [passTo, setPassTo] = useState(999); const [flightLineData, setFlightLineData] = useState([]); + // Статус загрузки трассы (витков): отличаем «ещё грузится» от «бэк вернул пусто на это окно», + // чтобы при отсутствии данных не было молчаливой пустоты вместо панели витков. + const [flightLineLoading, setFlightLineLoading] = useState(false); // Команда карте центрироваться/подсветить объект выбранного включения (клик по включению). const [mapFocus, setMapFocus] = useState<{ point?: GeoPoint; objectId?: string; token: number }>({ token: 0 }); const draft = history.draft; @@ -277,15 +280,26 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, setPlanModes([]); setPassFrom(1); setPassTo(999); - setFlightLineData([]); }, [appliedRange, selectedPlan, selectedSpacecraftId]); + // Линия трассы (витки) живёт исключительно по своим входам запроса — noradId и mapRange. + // НЕ сбрасываем её в общем reset-эффекте по ссылке selectedPlan: фоновый loadData (после + // решения/создания черновика/обновления интервала) пересобирает объект selectedPlan с теми же + // временами окна, mapRange по значению не меняется, и фетч бы не перезапустился — витки + // молча пропадали бы до реальной смены окна. Привязка к [noradId, mapRange] делает это + // идемпотентным: сброс и перезагрузка происходят вместе и только когда меняется само окно/КА. useEffect(() => { - if (!noradId) return; + if (!noradId) { + setFlightLineData([]); + setFlightLineLoading(false); + return; + } let cancelled = false; + setFlightLineLoading(true); void fetchFlightLine(noradId, mapRange.fromMs, mapRange.toMs) .then((data) => { if (!cancelled) setFlightLineData(data); }) - .catch(() => { if (!cancelled) setFlightLineData([]); }); + .catch(() => { if (!cancelled) setFlightLineData([]); }) + .finally(() => { if (!cancelled) setFlightLineLoading(false); }); return () => { cancelled = true; }; }, [noradId, mapRange]); @@ -464,8 +478,15 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, // Источник истины для включений — DRAFT, если он уже создан, иначе живой план (read-only превью). useEffect(() => { - const missionId = draftPlanId ?? selectedPlan?.planId; - if (!missionId) return; + // Нет выбранного плана (например, открытый черновик удалили) — чистим включения и НЕ тянем + // режимы по устаревшему draftPlanId. Он обнулится следующим коммитом (эффект выше), но в этом + // ещё указывает на удалённый черновик; без гарда fetchPlanModes(удалённый id) вернул бы его + // включения обратно — редактор «не чистился» бы после удаления. + if (!selectedPlan) { + setPlanModes([]); + return; + } + const missionId = draftPlanId ?? selectedPlan.planId; let cancelled = false; void fetchPlanModes(missionId).then((modes) => { if (!cancelled) setPlanModes(modes); @@ -895,10 +916,13 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, ); }; - if (!selectedSpacecraftId && !selectedPlan) { + // Редактор включений работает только при выбранном плане (план == миссия, в которую кладутся + // включения). Без плана — плейсхолдер: иначе после удаления открытого черновика вкладка + // оставалась бы «активной» по выбранному КА (витки, режим съёмки), хотя редактировать нечего. + if (!selectedPlan) { return (
-
Выберите КА или план, чтобы открыть редактор включений.
+
Выберите КА и план, чтобы открыть редактор включений.
); } @@ -969,7 +993,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, >
{rightMode === "shoot" &&
Кликните точку съёмки на карте.
} - {orbitPasses.length > 0 && ( + {orbitPasses.length > 0 ? ( - )} + ) : noradId ? ( +
+ Витки + + {flightLineLoading ? "Загрузка трассы…" : "Нет данных трассы на это окно"} + +
+ ) : null} { - // Один открытый черновик на базу: если он уже есть — открываем его, второй не плодим. + // Один открытый черновик на базу: если он уже есть — выбираем его, второй не плодим. const existing = drafts.find((item) => item.basePlanId === planId); if (existing) { - handleEditDraft(existing.planId); + setShowDrafts(true); + setSelectedSpacecraftId(existing.spacecraftId); + setSelectedPlanId(existing.planId); + setDecisionNotice("Черновик на основе этого плана уже создан — выбран на схеме."); return; } setDraftActionInFlight(true); @@ -320,7 +324,7 @@ export function TguPlanningPage() { setShowDrafts(true); if (base) setSelectedSpacecraftId(base.spacecraftId); setSelectedPlanId(draftPlanId); - setActiveTab("editor"); + setDecisionNotice("Черновик создан и выбран на схеме. Откройте «Редактор», чтобы поправить и отправить его."); } catch (error) { setDecisionError(error instanceof Error ? error.message : "Не удалось создать черновик на основе плана."); } finally { diff --git a/services/pcp-tgu-ops-ui/src/styles/tgu-editor.css b/services/pcp-tgu-ops-ui/src/styles/tgu-editor.css index bf875db..f64d496 100644 --- a/services/pcp-tgu-ops-ui/src/styles/tgu-editor.css +++ b/services/pcp-tgu-ops-ui/src/styles/tgu-editor.css @@ -308,6 +308,20 @@ user-select: none; } +.map-pass-selector--empty { + display: flex; + align-items: baseline; + gap: 0.5rem; + padding: 0.4rem 0.65rem; + width: auto; + max-width: 28rem; +} + +.map-pass-selector__empty-note { + color: var(--text-muted, #98a2ac); + font-size: 0.74rem; +} + .map-pass-selector__header { display: flex; align-items: baseline;