2D-карта (станции, заявки) и вкладка «Заявки»

Новая вкладка «Заявки»: показ заявок за интервал на 2D-карте,
создание заявки рисованием полигона на карте с формой параметров
(Оптика/РСА) и удаление выбранной заявки (createRequest/deleteRequest,
RequestCreatePanel/RequestDetailsPanel, polygonToWkt). В Tgu2DMapView
добавлен изолированный режим рисования полигона.

2D-карта: панели информации о станции и о заявках, слой станций
(stationsApi), поддержка выбора объектов через спатиальный индекс.
Список слоёв упрощён — трасса/полоса/маркеры КА убраны из переключателя,
по умолчанию включены работы плана, станции и заявки.
This commit is contained in:
Дмитрий Соловьев
2026-06-01 22:17:50 +03:00
parent 715b5108d6
commit 0128c5569c
26 changed files with 1851 additions and 96 deletions
@@ -4,6 +4,7 @@ import type { TguPlanDecision, TguPlatform } from "../../model/tguTypes";
import type { TguActiveTab, TguPlanUi, TimelineRange } from "../../model/timelineTypes";
import { TguEditorTab } from "../tgu-editor/TguEditorTab";
import { Tgu2DMapTab } from "../tgu-map-2d/Tgu2DMapTab";
import { TguRequestsTab } from "../tgu-requests/TguRequestsTab";
import { TguPlanDetails } from "./TguPlanDetails";
import { TguPlanningLayout } from "./TguPlanningLayout";
import { TguSidebar } from "./TguSidebar";
@@ -114,14 +115,6 @@ export function TguPlanningPage() {
setDecisionNotice(undefined);
};
const refresh = () => {
if (invalidRange) {
setPageError("Некорректный интервал: from должен быть меньше to.");
return;
}
void loadData(appliedRange);
};
const handleDecision = async (planId: string, decision: TguPlanDecision, reason?: string) => {
setDecisionInFlight(true);
setDecisionError(undefined);
@@ -156,7 +149,6 @@ export function TguPlanningPage() {
onToChange={setToValue}
onApply={applyRange}
onQuickRange={quickRange}
onRefresh={refresh}
/>
}
>
@@ -205,6 +197,12 @@ export function TguPlanningPage() {
selectedPlan={selectedPlan}
platforms={platforms}
/>
) : activeTab === "requests" ? (
<TguRequestsTab
range={appliedRange}
invalidRange={invalidRange}
platforms={platforms}
/>
) : (
<TguEditorTab
selectedSpacecraftId={selectedPlan?.spacecraftId ?? selectedSpacecraftId}
@@ -1,5 +1,6 @@
import type { TguPlatformUi } from "../../model/timelineTypes";
import { filterPlatforms, platformLabel } from "./tguTimelineMapper";
import { TguLegend } from "./TguLegend";
type TguSidebarProps = {
platforms: TguPlatformUi[];
@@ -69,6 +70,10 @@ export function TguSidebar({
})}
{filtered.length === 0 && <div className="tgu-empty tgu-empty--compact">Нет КА по текущему поиску.</div>}
</div>
<div className="tgu-sidebar__legend">
<TguLegend />
</div>
</aside>
);
}
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useLayoutEffect, useMemo, useRef, useState } from "react";
import type { TimelinePlanSegment, TimelineRange, TimelineRow } from "../../model/timelineTypes";
import {
buildSequentialLinks,
@@ -30,9 +30,11 @@ export function TguTimeline({ rows, range, selectedPlanId, invalidRange, onSelec
const height = Math.max(260, timelineHeight(rows.length));
const nowMs = Date.now();
useEffect(() => {
useLayoutEffect(() => {
if (!containerRef.current) return;
setContainerWidth(Math.floor(containerRef.current.getBoundingClientRect().width));
const observer = new ResizeObserver(([entry]) => {
setContainerWidth(Math.floor(entry.contentRect.width));
});
@@ -55,17 +57,14 @@ export function TguTimeline({ rows, range, selectedPlanId, invalidRange, onSelec
? TIMELINE_LABEL_WIDTH + timeToX(nowMs, range, timelineWidth)
: undefined;
if (invalidRange) {
return <div className="tgu-empty">Некорректный интервал: from должен быть меньше to.</div>;
}
if (rows.length === 0 || segments.length === 0) {
return <div className="tgu-empty">Нет планов в выбранном интервале.</div>;
}
return (
<div className="tgu-timeline" ref={containerRef}>
<svg className="tgu-timeline__svg" viewBox={`0 0 ${fullWidth} ${height}`} role="img" aria-label="Timeline планов ТГУ">
{invalidRange ? (
<div className="tgu-empty">Некорректный интервал: from должен быть меньше to.</div>
) : rows.length === 0 || segments.length === 0 ? (
<div className="tgu-empty">Нет планов в выбранном интервале.</div>
) : (
<svg className="tgu-timeline__svg" viewBox={`0 0 ${fullWidth} ${height}`} style={{ height }} role="img" aria-label="Timeline планов ТГУ">
<defs>
<marker id="tgu-arrow-head" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<path d="M0 0 L8 4 L0 8 z" fill="#8aa2a6" />
@@ -175,6 +174,7 @@ export function TguTimeline({ rows, range, selectedPlanId, invalidRange, onSelec
);
})}
</svg>
)}
</div>
);
}
@@ -1,4 +1,3 @@
import { TguLegend } from "./TguLegend";
import type { TguActiveTab } from "../../model/timelineTypes";
type TguToolbarProps = {
@@ -12,7 +11,6 @@ type TguToolbarProps = {
onToChange: (value: string) => void;
onApply: () => void;
onQuickRange: (hours: number) => void;
onRefresh: () => void;
};
export function TguToolbar({
@@ -25,8 +23,7 @@ export function TguToolbar({
onFromChange,
onToChange,
onApply,
onQuickRange,
onRefresh
onQuickRange
}: TguToolbarProps) {
return (
<header className="tgu-toolbar">
@@ -56,7 +53,7 @@ export function TguToolbar({
aria-selected={activeTab === "map"}
onClick={() => onTabChange("map")}
>
Карта 2D
Карта
</button>
<button
className={activeTab === "editor" ? "is-active" : ""}
@@ -67,6 +64,15 @@ export function TguToolbar({
>
Редактор
</button>
<button
className={activeTab === "requests" ? "is-active" : ""}
type="button"
role="tab"
aria-selected={activeTab === "requests"}
onClick={() => onTabChange("requests")}
>
Заявки
</button>
</div>
<label className="tgu-field">
@@ -90,11 +96,7 @@ export function TguToolbar({
<button className="tgu-button" type="button" onClick={() => onQuickRange(168)} disabled={loading}>
7 суток
</button>
<button className="tgu-button" type="button" onClick={onRefresh} disabled={loading}>
{loading ? "Обновление..." : "Обновить"}
</button>
</div>
<TguLegend />
{error && <div className="tgu-alert tgu-alert--danger">{error}</div>}
</header>
);