Незакоммиченная работа: UI заявок/планирования и правки сервисов

- pcp-tgu-ops-ui: вкладка «Заявки», редактор планирования, утилиты uuid
  и e2e-verify скрипты, обновление зависимостей
- pcp-ballistics-service, pcp-mission-planing-service: текущие правки
This commit is contained in:
Дмитрий Соловьев
2026-06-02 11:04:45 +03:00
parent 96f698112b
commit bd59204fcf
13 changed files with 588 additions and 40 deletions
@@ -28,6 +28,11 @@ export function TguPlanningPage() {
const [selectedSpacecraftId, setSelectedSpacecraftId] = useState<string>();
const [selectedPlanId, setSelectedPlanId] = useState<string>();
const [activeTab, setActiveTab] = useState<TguActiveTab>("timeline");
// Вкладки рендерятся в режиме keep-alive: каждую монтируем при первом открытии
// и дальше держим смонтированной (скрытой через display:none), чтобы её
// внутреннее состояние — загруженные заявки, выбор, слои, скролл — не сбрасывалось
// при переключении между вкладками.
const [visitedTabs, setVisitedTabs] = useState<ReadonlySet<TguActiveTab>>(() => new Set(["timeline"]));
const [decisionInFlight, setDecisionInFlight] = useState(false);
const [decisionNotice, setDecisionNotice] = useState<string>();
const [decisionError, setDecisionError] = useState<string>();
@@ -80,6 +85,15 @@ export function TguPlanningPage() {
void loadData(appliedRange);
}, [appliedRange, loadData]);
useEffect(() => {
setVisitedTabs((prev) => (prev.has(activeTab) ? prev : new Set(prev).add(activeTab)));
}, [activeTab]);
// display:contents для активной вкладки сохраняет grid-раскладку shell
// (внутренний <main> остаётся прямым grid-элементом), display:none скрывает остальные.
const tabStyle = (tab: TguActiveTab) =>
({ display: activeTab === tab ? "contents" : "none" }) as const;
const platforms = useMemo(
() => buildPlatformsForInterval(rawPlatforms, plans, appliedRange),
[rawPlatforms, plans, appliedRange]
@@ -152,7 +166,7 @@ export function TguPlanningPage() {
/>
}
>
{activeTab === "timeline" ? (
<div style={tabStyle("timeline")}>
<main className="tgu-workspace">
<TguSidebar
platforms={platforms}
@@ -190,26 +204,38 @@ export function TguPlanningPage() {
onClose={() => setSelectedPlanId(undefined)}
/>
</main>
) : activeTab === "map" ? (
<Tgu2DMapTab
range={appliedRange}
invalidRange={invalidRange}
selectedPlan={selectedPlan}
platforms={platforms}
/>
) : activeTab === "requests" ? (
<TguRequestsTab
range={appliedRange}
invalidRange={invalidRange}
platforms={platforms}
/>
) : (
<TguEditorTab
selectedSpacecraftId={selectedPlan?.spacecraftId ?? selectedSpacecraftId}
selectedPlan={selectedPlan}
appliedRange={appliedRange}
platforms={platforms}
/>
</div>
{visitedTabs.has("map") && (
<div style={tabStyle("map")}>
<Tgu2DMapTab
range={appliedRange}
invalidRange={invalidRange}
selectedPlan={selectedPlan}
platforms={platforms}
/>
</div>
)}
{visitedTabs.has("requests") && (
<div style={tabStyle("requests")}>
<TguRequestsTab
range={appliedRange}
invalidRange={invalidRange}
platforms={platforms}
/>
</div>
)}
{visitedTabs.has("editor") && (
<div style={tabStyle("editor")}>
<TguEditorTab
selectedSpacecraftId={selectedPlan?.spacecraftId ?? selectedSpacecraftId}
selectedPlan={selectedPlan}
appliedRange={appliedRange}
platforms={platforms}
/>
</div>
)}
</TguPlanningLayout>
);
@@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import type { RequestAtPoint } from "../tgu-map-2d/Tgu2DMapRequestsPanel";
type Props = {
@@ -27,6 +28,13 @@ export function RequestDetailsPanel({
? requests[0]
: undefined;
// Второй шаг подтверждения удаления. Сбрасываем при смене выбранной заявки,
// чтобы открытый запрос на удаление не «переезжал» на другую заявку.
const [confirmingDelete, setConfirmingDelete] = useState(false);
useEffect(() => {
setConfirmingDelete(false);
}, [selectedEntry?.item.id]);
return (
<aside className="tgu-map-requests-panel">
<div className="tgu-panel-heading">
@@ -104,20 +112,41 @@ export function RequestDetailsPanel({
{deleteError && <div className="tgu-alert tgu-alert--danger tgu-alert--compact">{deleteError}</div>}
<div className="tgu-requests-form__actions">
<button
className="tgu-button tgu-button--reject"
type="button"
disabled={deleting}
onClick={() => {
if (window.confirm(`Удалить заявку «${selectedEntry.item.name || selectedEntry.item.id}»?`)) {
onDelete(selectedEntry.item.id);
}
}}
>
{deleting ? "Удаление..." : "Удалить заявку"}
</button>
</div>
{confirmingDelete ? (
<div className="tgu-requests-confirm">
<div className="tgu-requests-confirm__title">Удалить заявку?</div>
<div className="tgu-requests-confirm__subtitle">{selectedEntry.item.name || selectedEntry.item.id}</div>
<div className="tgu-requests-form__actions">
<button
className="tgu-button tgu-button--reject"
type="button"
disabled={deleting}
onClick={() => onDelete(selectedEntry.item.id)}
>
{deleting ? "Удаление..." : "Да, удалить"}
</button>
<button
className="tgu-button"
type="button"
disabled={deleting}
onClick={() => setConfirmingDelete(false)}
>
Нет, не удалять
</button>
</div>
</div>
) : (
<div className="tgu-requests-form__actions">
<button
className="tgu-button tgu-button--reject"
type="button"
disabled={deleting}
onClick={() => setConfirmingDelete(true)}
>
Удалить заявку
</button>
</div>
)}
</div>
)}
</aside>
@@ -8,6 +8,7 @@ import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
import type { RequestAtPoint } from "../tgu-map-2d/Tgu2DMapRequestsPanel";
import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
import type { GeoPoint, MapPolygon } from "../tgu-map-2d/model/mapTypes";
import { generateUuid } from "../../utils/uuid";
import { RequestCreatePanel, type RequestFormSubmit } from "./RequestCreatePanel";
import { RequestDetailsPanel } from "./RequestDetailsPanel";
import { polygonToWkt } from "./requestGeometry";
@@ -195,7 +196,7 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
setCreating(true);
setCreateError(undefined);
try {
const id = crypto.randomUUID();
const id = generateUuid();
await createRequest({
id,
name: form.name,