pcp: время — сквозной naive LocalDateTime как локальное настенное время (без UTC)
Отказ от остаточной UTC-семантики: naive LocalDateTime трактуется как локальное время единой зоны всех сервисов. Backend (границы внешних систем): - VisibilityPayloadParser, SyncSpacecraftFromNsiUseCase: ZoneOffset.UTC → ZoneId.systemDefault() для проекции внешних Instant/ISO-с-Z в naive. - тесты границ перевёрнуты на systemDefault (под Europe/Moscow ждут +3). - чистка UTC-формулировок в DTO-комментариях, мёртвых ссылок на docs/standards/DATETIME_UTC.md. Frontend (pcp-tgu-ops-ui): - utils/utcTime.ts → utils/localTime.ts: parseLocal/formatLocal*/toLocalIso/ toLocalInputValue с локальной семантикой (без дописывания Z и timeZone:UTC). - замена getUTC*/Date.UTC/toISOString-сериализации на локальные аналоги. - убраны пользовательские подписи «UTC» в редакторе/таймлайне. - удалён utcTime.guard.test.ts (форсил обратный UTC-инвариант); тест-фикстуры сделаны зоно-независимыми. tsc + vitest зелёные под TZ=UTC и TZ=Europe/Moscow. Включает также прочие незакоммиченные правки рабочего дерева.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { formatDate, formatDateTime, formatDuration, formatTime, kindColor, kindLabel } from "./editorPresentation";
|
||||
import { statusStyle } from "../tgu-planning/tguStatus";
|
||||
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
|
||||
import { formatLocalDateTime, parseLocal, toLocalIso } from "../../utils/localTime";
|
||||
import { Time24Field } from "../../components/timeFields";
|
||||
import type { RvaZone } from "../../api/ballisticsApi";
|
||||
import type { SurveyRoute } from "../../api/missionPlanningApi";
|
||||
@@ -50,7 +50,7 @@ type ShootPanelProps = {
|
||||
selectedPass?: EditorVisibilityWindow;
|
||||
modeId?: string;
|
||||
modes: EditorMode[];
|
||||
/** Окно строящегося маршрута (UTC, мс). undefined пока не выбран проход. */
|
||||
/** Окно строящегося маршрута (локальное, мс). undefined пока не выбран проход. */
|
||||
routeWindow?: { startMs: number; endMs: number };
|
||||
/** Текущий крен (gamma) маршрута, градусы. */
|
||||
routeRoll?: number;
|
||||
@@ -105,7 +105,7 @@ type DropPanelProps = {
|
||||
onSelectZone: (zone?: RvaZone) => void;
|
||||
/** Название станции по её номеру (из каталога станций). */
|
||||
stationName: (stationId: number) => string;
|
||||
/** Время начала зоны в мс (UTC) — для подписи. */
|
||||
/** Время начала зоны в мс (локальное) — для подписи. */
|
||||
zoneStartMs: (zone: RvaZone) => number;
|
||||
zoneStopMs: (zone: RvaZone) => number;
|
||||
/** Съёмки-кандидаты для выбранной зоны (end < начало зоны). */
|
||||
@@ -699,7 +699,7 @@ function surveyTypeLabel(surveyType: string): string {
|
||||
}
|
||||
|
||||
function formatIso(iso: string): string {
|
||||
return formatUtcDateTime(parseUtc(iso));
|
||||
return formatLocalDateTime(parseLocal(iso));
|
||||
}
|
||||
|
||||
function PanelHead({ title, onClose }: { title: string; onClose: () => void }) {
|
||||
@@ -747,7 +747,7 @@ function TimeStepper({
|
||||
|
||||
/**
|
||||
* Степпер времени окна маршрута с секундной точностью: кнопки ±5 c / ±30 c и ручной
|
||||
* ввод времени `чч:мм:сс` (UTC, без даты — дата берётся из текущего значения).
|
||||
* ввод времени `чч:мм:сс` (локальное, без даты — дата берётся из текущего значения).
|
||||
* Значение зажимается в [min, max].
|
||||
*/
|
||||
function RouteTimeStepper({
|
||||
@@ -766,16 +766,16 @@ function RouteTimeStepper({
|
||||
const clamp = (next: number) => Math.max(min ?? Number.NEGATIVE_INFINITY, Math.min(max ?? Number.POSITIVE_INFINITY, next));
|
||||
const change = (deltaMs: number) => onChange(clamp(value + deltaMs));
|
||||
const onTimeChange = (raw: string) => {
|
||||
// Ввод — только время суток (ЧЧ:ММ:СС, UTC); день оставляем прежним.
|
||||
const datePart = new Date(value).toISOString().slice(0, 10);
|
||||
const ms = parseUtc(`${datePart}T${raw}`);
|
||||
// Ввод — только время суток (ЧЧ:ММ:СС, локальное); день оставляем прежним.
|
||||
const datePart = toLocalIso(value).slice(0, 10);
|
||||
const ms = parseLocal(`${datePart}T${raw}`);
|
||||
if (Number.isFinite(ms)) onChange(clamp(ms));
|
||||
};
|
||||
|
||||
return (
|
||||
<label className="tgu-editor-time-stepper tgu-editor-time-stepper--seconds">
|
||||
<span>{label}</span>
|
||||
<Time24Field withSeconds value={new Date(value).toISOString().slice(11, 19)} onChange={onTimeChange} />
|
||||
<Time24Field withSeconds value={toLocalIso(value).slice(11, 19)} onChange={onTimeChange} />
|
||||
<div className="tgu-editor-time-stepper__steps">
|
||||
<button type="button" onClick={() => change(-30_000)}>−30с</button>
|
||||
<button type="button" onClick={() => change(-5_000)}>−5с</button>
|
||||
|
||||
@@ -85,7 +85,7 @@ export function EditorTimeline({
|
||||
<div className="tgu-editor-timeline" ref={wrapperRef} onClick={onClearSelection}>
|
||||
<div className="tgu-editor-timeline__surface" style={{ width: LABEL_WIDTH + bodyWidth, height: Math.max(220, surfaceHeight) }}>
|
||||
<div className="tgu-editor-timeline__axis-label" style={{ width: LABEL_WIDTH }}>
|
||||
Время (UTC)
|
||||
Время
|
||||
</div>
|
||||
<div className="tgu-editor-timeline__axis" style={{ left: LABEL_WIDTH, width: bodyWidth }}>
|
||||
{days.map((day) => (
|
||||
@@ -206,7 +206,7 @@ export function EditorTimeline({
|
||||
function buildDays(window: EditorTimelineWindow): number[] {
|
||||
const result: number[] = [];
|
||||
const start = new Date(window.fromMs);
|
||||
let dayMs = Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate());
|
||||
let dayMs = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime();
|
||||
while (dayMs <= window.toMs) {
|
||||
result.push(dayMs);
|
||||
dayMs += 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -9,14 +9,14 @@ type MapPassSelectorProps = {
|
||||
onToChange: (idx: number) => void;
|
||||
};
|
||||
|
||||
function utcHM(ms: number): string {
|
||||
function localHM(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
|
||||
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function utcDM(ms: number): string {
|
||||
function localDM(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
return `${String(d.getUTCDate()).padStart(2, "0")}.${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
|
||||
return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function MapPassSelector({ passes, fromIndex, toIndex, onFromChange, onToChange }: MapPassSelectorProps) {
|
||||
@@ -27,7 +27,7 @@ export function MapPassSelector({ passes, fromIndex, toIndex, onFromChange, onTo
|
||||
const fromPass = passes[Math.max(0, Math.min(fromIndex - 1, n - 1))];
|
||||
const toPass = passes[Math.max(0, Math.min(toIndex - 1, n - 1))];
|
||||
|
||||
const multiDay = utcDM(passes[0].startMs) !== utcDM(passes[n - 1].startMs);
|
||||
const multiDay = localDM(passes[0].startMs) !== localDM(passes[n - 1].startMs);
|
||||
const isFullRange = fromIndex === 1 && toIndex === n;
|
||||
const fromRev = fromPass.revolution ?? fromIndex;
|
||||
const toRev = toPass.revolution ?? toIndex;
|
||||
@@ -76,10 +76,9 @@ export function MapPassSelector({ passes, fromIndex, toIndex, onFromChange, onTo
|
||||
<span className="map-pass-selector__title">Витки</span>
|
||||
<span className="map-pass-selector__nums">{fromRev}–{toRev}</span>
|
||||
<span className="map-pass-selector__times">
|
||||
{multiDay ? `${utcDM(fromPass.startMs)} ` : ""}{utcHM(fromPass.startMs)}
|
||||
{multiDay ? `${localDM(fromPass.startMs)} ` : ""}{localHM(fromPass.startMs)}
|
||||
{" – "}
|
||||
{multiDay ? `${utcDM(toPass.startMs)} ` : ""}{utcHM(toPass.startMs)}
|
||||
{" UTC"}
|
||||
{multiDay ? `${localDM(toPass.startMs)} ` : ""}{localHM(toPass.startMs)}
|
||||
</span>
|
||||
{!isFullRange && (
|
||||
<button
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type SurveyRoute
|
||||
} from "../../api/missionPlanningApi";
|
||||
import { abandonDraft, createDraft, promoteDraft } from "../../api/tguPlanApi";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal, toLocalIso } from "../../utils/localTime";
|
||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
|
||||
import { buildStationMarkers } from "../tgu-map-2d/Tgu2DMapLayers";
|
||||
@@ -150,8 +150,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
.map(([revolution, pts], idx) => ({
|
||||
index: idx + 1,
|
||||
revolution,
|
||||
startMs: parseUtc(pts[0].time),
|
||||
endMs: parseUtc(pts[pts.length - 1].time),
|
||||
startMs: parseLocal(pts[0].time),
|
||||
endMs: parseLocal(pts[pts.length - 1].time),
|
||||
}));
|
||||
}, [flightLineData]);
|
||||
const modes: EditorMode[] = DEFAULT_EDITOR_MODES;
|
||||
@@ -177,7 +177,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const [shootPasses, setShootPasses] = useState<EditorVisibilityWindow[]>([]);
|
||||
const [shootPassesLoading, setShootPassesLoading] = useState(false);
|
||||
const [shootPassesError, setShootPassesError] = useState<string>();
|
||||
// Окно строящегося маршрута съёмки: задаётся вручную в правой панели (начало/конец, UTC).
|
||||
// Окно строящегося маршрута съёмки: задаётся вручную в правой панели (начало/конец, локальное).
|
||||
const [routeWindow, setRouteWindow] = useState<{ startMs: number; endMs: number }>();
|
||||
// Крен (gamma) строящегося маршрута, градусы. По умолчанию — из прохода, дальше правится вручную.
|
||||
const [routeRoll, setRouteRoll] = useState<number>();
|
||||
@@ -275,8 +275,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
void fetchRequestsForMap({
|
||||
endFrom: new Date(window.fromMs).toISOString().slice(0, 19),
|
||||
beginTo: new Date(window.toMs).toISOString().slice(0, 19)
|
||||
endFrom: toLocalIso(window.fromMs),
|
||||
beginTo: toLocalIso(window.toMs)
|
||||
}).then((result) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setRequestItems(result.items);
|
||||
@@ -365,7 +365,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
void buildSurveyRoute({
|
||||
satelliteId: numericNorad,
|
||||
// Окно задано явно (начало/конец) — строим вперёд от начала на выбранную длительность.
|
||||
time: new Date(routeWindow.startMs).toISOString().slice(0, 19),
|
||||
time: toLocalIso(routeWindow.startMs),
|
||||
durationSeconds,
|
||||
roll: routeRoll,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
@@ -522,15 +522,15 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const draftId = await ensureDraft();
|
||||
savedRoute = await saveSurveyRoute(draftId, {
|
||||
satelliteId: numericNorad,
|
||||
time: new Date(routeWindow.startMs).toISOString().slice(0, 19),
|
||||
time: toLocalIso(routeWindow.startMs),
|
||||
durationSeconds: routeDurationSeconds,
|
||||
roll: routeRoll,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
revolution: selectedShootPass.revolution ?? 0,
|
||||
anchor: "FORWARD",
|
||||
// Окно выбранного плана — чтобы при автосоздании миссии/плана задать корректный интервал.
|
||||
planStart: new Date(selectedPlan.startMs).toISOString().slice(0, 19),
|
||||
planEnd: new Date(selectedPlan.endMs).toISOString().slice(0, 19)
|
||||
planStart: toLocalIso(selectedPlan.startMs),
|
||||
planEnd: toLocalIso(selectedPlan.endMs)
|
||||
});
|
||||
setSurveyRoute(savedRoute);
|
||||
void refreshDraftModes(draftId);
|
||||
@@ -614,8 +614,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
zoneStop: selectedZone.onStop.time.slice(0, 19),
|
||||
surveyModeIds: [...selectedDropSurveyIds],
|
||||
// Окно выбранного плана — для автосоздания миссии/плана с корректным интервалом.
|
||||
planStart: new Date(selectedPlan.startMs).toISOString().slice(0, 19),
|
||||
planEnd: new Date(selectedPlan.endMs).toISOString().slice(0, 19)
|
||||
planStart: toLocalIso(selectedPlan.startMs),
|
||||
planEnd: toLocalIso(selectedPlan.endMs)
|
||||
});
|
||||
savedDropId = savedDrop.id;
|
||||
// Перечитываем включения DRAFT (источник истины) и ревалидируем.
|
||||
@@ -1051,7 +1051,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
}
|
||||
|
||||
function visibilityWindowFromParam(param: PointVisibilityParam): EditorVisibilityWindow {
|
||||
const timeMs = parseUtc(param.time);
|
||||
const timeMs = parseLocal(param.time);
|
||||
const half = SHOOT_WINDOW_MS / 2;
|
||||
return {
|
||||
startMs: timeMs - half,
|
||||
@@ -1088,11 +1088,12 @@ function buildStations(selectedPlan: TguEditorTabProps["selectedPlan"]): EditorG
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime без зоны от бэка трактуем как UTC (как и поле `time` в ballisticsApi):
|
||||
* зоны ЗРВ приходят без таймзоны, а времена съёмок — с offset; приводим к одной шкале.
|
||||
* LocalDateTime без зоны от бэка трактуем как локальное настенное время (как и поле `time`
|
||||
* в ballisticsApi): зоны ЗРВ приходят без таймзоны, времена съёмок — с offset (его учитываем);
|
||||
* приводим к одной шкале в локальной зоне браузера.
|
||||
*/
|
||||
function localDateTimeMs(s: string): number {
|
||||
return parseUtc(s);
|
||||
return parseLocal(s);
|
||||
}
|
||||
|
||||
function buildVisibilityTracks(
|
||||
@@ -1208,7 +1209,7 @@ function buildEditorMapScene(
|
||||
const startMs = localDateTimeMs(mode.timeStart);
|
||||
const endMs = startMs + mode.duration * 1000;
|
||||
const pts = flightLineData
|
||||
.map((p) => ({ t: parseUtc(p.time), lat: p.lat, lon: p.long }))
|
||||
.map((p) => ({ t: parseLocal(p.time), lat: p.lat, lon: p.long }))
|
||||
.filter((p) => p.t >= startMs && p.t <= endMs)
|
||||
.sort((a, b) => a.t - b.t)
|
||||
.map((p) => ({ lat: p.lat, lon: p.lon }));
|
||||
|
||||
@@ -14,17 +14,17 @@ import {
|
||||
} from "./editorDraft";
|
||||
import type { PlanMode } from "./editorApi";
|
||||
import type { DraftHistory } from "./model/editorTypes";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal } from "../../utils/localTime";
|
||||
|
||||
const plan: TguPlanUi = {
|
||||
planId: "plan-1",
|
||||
spacecraftId: "56756",
|
||||
startTime: "2026-05-29T01:00:00Z",
|
||||
endTime: "2026-05-29T02:00:00Z",
|
||||
startTime: "2026-05-29T01:00:00",
|
||||
endTime: "2026-05-29T02:00:00",
|
||||
kppId: "KPP-1",
|
||||
status: "PLANNED",
|
||||
startMs: parseUtc("2026-05-29T01:00:00Z"),
|
||||
endMs: parseUtc("2026-05-29T02:00:00Z")
|
||||
startMs: parseLocal("2026-05-29T01:00:00"),
|
||||
endMs: parseLocal("2026-05-29T02:00:00")
|
||||
};
|
||||
|
||||
describe("editorDraft", () => {
|
||||
@@ -85,8 +85,8 @@ describe("editorDraft", () => {
|
||||
id: "survey-11",
|
||||
kind: "shoot",
|
||||
label: "Съёмка · виток 4321",
|
||||
startMs: parseUtc("2026-05-29T01:10:00Z"),
|
||||
endMs: parseUtc("2026-05-29T01:10:00Z") + 180_000,
|
||||
startMs: parseLocal("2026-05-29T01:10:00"),
|
||||
endMs: parseLocal("2026-05-29T01:10:00") + 180_000,
|
||||
origin: "plan",
|
||||
planModeId: 11,
|
||||
revolution: 4321,
|
||||
@@ -100,8 +100,8 @@ describe("editorDraft", () => {
|
||||
id: "drop-22",
|
||||
kind: "downlink",
|
||||
label: "Сброс · 102",
|
||||
startMs: parseUtc("2026-05-29T01:40:00Z"),
|
||||
endMs: parseUtc("2026-05-29T01:40:00Z") + 120_000,
|
||||
startMs: parseLocal("2026-05-29T01:40:00"),
|
||||
endMs: parseLocal("2026-05-29T01:40:00") + 120_000,
|
||||
origin: "plan",
|
||||
planModeId: 22,
|
||||
revolution: 4322,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal } from "../../utils/localTime";
|
||||
import type { PlanMode } from "./editorApi";
|
||||
import type {
|
||||
Draft,
|
||||
@@ -64,7 +64,7 @@ export function seedDraft(plan?: TguPlanUi, spacecraftId?: string): Draft {
|
||||
export function planModesToWorks(modes: PlanMode[]): EditorWork[] {
|
||||
return modes.flatMap((mode): EditorWork[] => {
|
||||
if (mode.timeStart == null) return [];
|
||||
const startMs = parseUtc(mode.timeStart);
|
||||
const startMs = parseLocal(mode.timeStart);
|
||||
if (!Number.isFinite(startMs)) return [];
|
||||
const endMs = startMs + Math.max(1, mode.duration) * 1000;
|
||||
|
||||
@@ -272,7 +272,7 @@ function readTime(record: Record<string, unknown>, key: string): number | undefi
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const parsed = parseUtc(value);
|
||||
const parsed = parseLocal(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -24,12 +24,12 @@ export function kindColor(kind: EditorWorkKind): string {
|
||||
|
||||
export function formatTime(ms: number): string {
|
||||
const date = new Date(ms);
|
||||
return `${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
|
||||
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
export function formatDate(ms: number): string {
|
||||
const date = new Date(ms);
|
||||
return `${pad(date.getUTCDate())} ${EDITOR_MONTHS[date.getUTCMonth()]}`;
|
||||
return `${pad(date.getDate())} ${EDITOR_MONTHS[date.getMonth()]}`;
|
||||
}
|
||||
|
||||
export function formatDateTime(ms: number): string {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RequestMapItem } from "../../model/requestTypes";
|
||||
import type { MapPolygon } from "./model/mapTypes";
|
||||
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
|
||||
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
||||
|
||||
export type RequestAtPoint = { polygon: MapPolygon; item: RequestMapItem };
|
||||
|
||||
@@ -96,18 +96,18 @@ export function Tgu2DMapRequestsPanel({ requests, selectedPolygonId, onHover, on
|
||||
}
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
return formatUtcDateTime(parseUtc(value));
|
||||
return formatLocalDateTime(parseLocal(value));
|
||||
}
|
||||
|
||||
function formatDateRange(begin: string, end: string): string {
|
||||
const b = new Date(parseUtc(begin));
|
||||
const e = new Date(parseUtc(end));
|
||||
const b = new Date(parseLocal(begin));
|
||||
const e = new Date(parseLocal(end));
|
||||
const sameDay =
|
||||
b.getUTCFullYear() === e.getUTCFullYear() &&
|
||||
b.getUTCMonth() === e.getUTCMonth() &&
|
||||
b.getUTCDate() === e.getUTCDate();
|
||||
const dateOpts: Intl.DateTimeFormatOptions = { timeZone: "UTC", day: "2-digit", month: "2-digit" };
|
||||
const timeOpts: Intl.DateTimeFormatOptions = { timeZone: "UTC", hour: "2-digit", minute: "2-digit" };
|
||||
b.getFullYear() === e.getFullYear() &&
|
||||
b.getMonth() === e.getMonth() &&
|
||||
b.getDate() === e.getDate();
|
||||
const dateOpts: Intl.DateTimeFormatOptions = { day: "2-digit", month: "2-digit" };
|
||||
const timeOpts: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit" };
|
||||
const beginStr = `${b.toLocaleDateString("ru-RU", dateOpts)} ${b.toLocaleTimeString("ru-RU", timeOpts)}`;
|
||||
const endStr = sameDay
|
||||
? b.toLocaleTimeString("ru-RU", timeOpts)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { filterPlatforms, platformLabel } from "../tgu-planning/tguTimelineMappe
|
||||
import { TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
||||
import type { Tgu2DMapLayerAvailability } from "./Tgu2DMapLayers";
|
||||
import type { RequestsState } from "./Tgu2DMapTab";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal } from "../../utils/localTime";
|
||||
import { DateTime24Field } from "../../components/timeFields";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
@@ -48,7 +48,7 @@ export function Tgu2DMapSidebar({
|
||||
|
||||
const quickToday = () => {
|
||||
const now = new Date();
|
||||
const fromMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||
const fromMs = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||
onRequestsQuickRange(fromMs, fromMs + DAY_MS);
|
||||
};
|
||||
|
||||
@@ -62,8 +62,8 @@ export function Tgu2DMapSidebar({
|
||||
onRequestsQuickRange(fromMs, fromMs + 30 * DAY_MS);
|
||||
};
|
||||
|
||||
const requestsFromMs = parseUtc(requestsState.fromValue);
|
||||
const requestsToMs = parseUtc(requestsState.toValue);
|
||||
const requestsFromMs = parseLocal(requestsState.fromValue);
|
||||
const requestsToMs = parseLocal(requestsState.toValue);
|
||||
const requestsRangeInvalid =
|
||||
!Number.isFinite(requestsFromMs) ||
|
||||
!Number.isFinite(requestsToMs) ||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Tgu2DMapStationPanel } from "./Tgu2DMapStationPanel";
|
||||
import { Tgu2DMapToolbar } from "./Tgu2DMapToolbar";
|
||||
import { Tgu2DMapView } from "./Tgu2DMapView";
|
||||
import { getTgu2DMapIntervalState, TGU_2D_MAP_INTERVAL_WARNING } from "./tgu2DMapInterval";
|
||||
import { parseUtc, toUtcInputValue } from "../../utils/utcTime";
|
||||
import { parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
|
||||
|
||||
type Tgu2DMapTabProps = {
|
||||
range: TimelineRange;
|
||||
@@ -38,8 +38,8 @@ export type RequestsState = {
|
||||
|
||||
function buildInitialRequestsState(range: TimelineRange): RequestsState {
|
||||
return {
|
||||
fromValue: toUtcInputValue(range.fromMs),
|
||||
toValue: toUtcInputValue(range.toMs),
|
||||
fromValue: toLocalInputValue(range.fromMs),
|
||||
toValue: toLocalInputValue(range.toMs),
|
||||
items: [],
|
||||
loading: false,
|
||||
loadedCount: 0,
|
||||
@@ -78,16 +78,16 @@ export function Tgu2DMapTab({
|
||||
const mapInvalidRange = invalidRange || mapInterval.tooLarge;
|
||||
|
||||
const loadRequests = useCallback(async (fromValue: string, toValue: string) => {
|
||||
const fromMs = parseUtc(fromValue);
|
||||
const toMs = parseUtc(toValue);
|
||||
const fromMs = parseLocal(fromValue);
|
||||
const toMs = parseLocal(toValue);
|
||||
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs >= toMs) return;
|
||||
|
||||
setRequestsState((current) => ({ ...current, loading: true, error: undefined, loadedCount: 0 }));
|
||||
try {
|
||||
const result = await fetchRequestsForMap(
|
||||
{
|
||||
endFrom: new Date(fromMs).toISOString().slice(0, 19),
|
||||
beginTo: new Date(toMs).toISOString().slice(0, 19)
|
||||
endFrom: toLocalIso(fromMs),
|
||||
beginTo: toLocalIso(toMs)
|
||||
},
|
||||
(loaded, total) => {
|
||||
setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total }));
|
||||
@@ -124,8 +124,8 @@ export function Tgu2DMapTab({
|
||||
}, []);
|
||||
|
||||
const setRequestsQuickRange = useCallback((fromMs: number, toMs: number) => {
|
||||
const fromValue = toUtcInputValue(fromMs);
|
||||
const toValue = toUtcInputValue(toMs);
|
||||
const fromValue = toLocalInputValue(fromMs);
|
||||
const toValue = toLocalInputValue(toMs);
|
||||
setRequestsState((current) => ({ ...current, fromValue, toValue }));
|
||||
void loadRequests(fromValue, toValue);
|
||||
}, [loadRequests]);
|
||||
@@ -297,7 +297,6 @@ export function Tgu2DMapTab({
|
||||
|
||||
function formatDate(ms: number): string {
|
||||
return new Date(ms).toLocaleString("ru-RU", {
|
||||
timeZone: "UTC",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
hour: "2-digit",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { DEFAULT_TGU_2D_MAP_LAYERS } from "./model/mapLayerTypes";
|
||||
import { getTgu2DMapIntervalState } from "./tgu2DMapInterval";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal } from "../../utils/localTime";
|
||||
|
||||
function plan(startTime: string, endTime: string): TguPlanUi {
|
||||
return {
|
||||
@@ -12,16 +12,16 @@ function plan(startTime: string, endTime: string): TguPlanUi {
|
||||
endTime,
|
||||
kppId: "KPP-1",
|
||||
status: "PLANNED",
|
||||
startMs: parseUtc(startTime),
|
||||
endMs: parseUtc(endTime)
|
||||
startMs: parseLocal(startTime),
|
||||
endMs: parseLocal(endTime)
|
||||
};
|
||||
}
|
||||
|
||||
describe("tgu2DMapInterval", () => {
|
||||
it("marks common map interval longer than 7 days as too large", () => {
|
||||
const state = getTgu2DMapIntervalState({
|
||||
fromMs: parseUtc("2026-05-29T00:00:00"),
|
||||
toMs: parseUtc("2026-06-05T00:00:01")
|
||||
fromMs: parseLocal("2026-05-29T00:00:00"),
|
||||
toMs: parseLocal("2026-06-05T00:00:01")
|
||||
});
|
||||
|
||||
expect(state.tooLarge).toBe(true);
|
||||
@@ -31,8 +31,8 @@ describe("tgu2DMapInterval", () => {
|
||||
const selectedPlan = plan("2026-05-29T01:00:00", "2026-06-07T02:00:00");
|
||||
const state = getTgu2DMapIntervalState(
|
||||
{
|
||||
fromMs: parseUtc("2026-05-29T00:00:00"),
|
||||
toMs: parseUtc("2026-06-10T00:00:00")
|
||||
fromMs: parseLocal("2026-05-29T00:00:00"),
|
||||
toMs: parseLocal("2026-06-10T00:00:00")
|
||||
},
|
||||
selectedPlan
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "../../api/tguPlanApi";
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { TguApiError } from "../../model/tguTypes";
|
||||
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
|
||||
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
||||
import { buildChainPlanRequest, futureZrvOptions, type ZrvOption } from "./createChainWindows";
|
||||
|
||||
type CreateChainPlanDialogProps = {
|
||||
@@ -123,7 +123,7 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on
|
||||
<option value="">Без истории (новая цепочка с нуля)</option>
|
||||
{baseOptions.map((plan) => (
|
||||
<option key={plan.planId} value={plan.planId}>
|
||||
{formatUtcDateTime(plan.startMs)} · {plan.kppId} · {plan.status}
|
||||
{formatLocalDateTime(plan.startMs)} · {plan.kppId} · {plan.status}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -142,7 +142,7 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on
|
||||
<option value="">— выберите —</option>
|
||||
{zrvOptions.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{formatUtcDateTime(option.startMs)} · {option.kppId}
|
||||
{formatLocalDateTime(option.startMs)} · {option.kppId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -156,7 +156,7 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on
|
||||
.filter((option) => !start || option.endMs > start.startMs)
|
||||
.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{formatUtcDateTime(option.endMs)} · {option.kppId}
|
||||
{formatLocalDateTime(option.endMs)} · {option.kppId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -186,7 +186,7 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on
|
||||
) : (
|
||||
result.projectedChain.map((plan) => (
|
||||
<li key={plan.planId}>
|
||||
{formatUtcDateTime(parseUtc(plan.startTime))} – {formatUtcDateTime(parseUtc(plan.endTime))} ·{" "}
|
||||
{formatLocalDateTime(parseLocal(plan.startTime))} – {formatLocalDateTime(parseLocal(plan.endTime))} ·{" "}
|
||||
{plan.kppId}
|
||||
</li>
|
||||
))
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TguPlanDecision } from "../../model/tguTypes";
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { platformLabel } from "./tguTimelineMapper";
|
||||
import { statusStyle } from "./tguStatus";
|
||||
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
|
||||
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
||||
|
||||
// Бэкенд (pcp-tgu-service) ждёт решения по плану только waiting-decision-grace-minutes
|
||||
// минут после его startTime; затем IssueDuePlanWorker.tryAdvancePastUnconfirmedPlan
|
||||
@@ -161,7 +161,7 @@ function Detail({ label, value, mono }: { label: string; value?: string; mono?:
|
||||
}
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
return formatUtcDateTime(parseUtc(value));
|
||||
return formatLocalDateTime(parseLocal(value));
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
|
||||
@@ -27,15 +27,15 @@ import {
|
||||
operationalSpacecraftIds,
|
||||
pausedSpacecraftIds
|
||||
} from "./tguTimelineMapper";
|
||||
import { parseUtc, toUtcInputValue } from "../../utils/utcTime";
|
||||
import { parseLocal, toLocalInputValue } from "../../utils/localTime";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const MINUTE_MS = 60 * 1000;
|
||||
|
||||
export function TguPlanningPage() {
|
||||
const initialRange = useMemo(defaultRange, []);
|
||||
const [fromValue, setFromValue] = useState(toUtcInputValue(initialRange.fromMs));
|
||||
const [toValue, setToValue] = useState(toUtcInputValue(initialRange.toMs));
|
||||
const [fromValue, setFromValue] = useState(toLocalInputValue(initialRange.fromMs));
|
||||
const [toValue, setToValue] = useState(toLocalInputValue(initialRange.toMs));
|
||||
const [appliedRange, setAppliedRange] = useState<TimelineRange>(initialRange);
|
||||
const [rawPlatforms, setRawPlatforms] = useState<TguPlatform[]>([]);
|
||||
const [plans, setPlans] = useState<TguPlanUi[]>([]);
|
||||
@@ -68,8 +68,8 @@ export function TguPlanningPage() {
|
||||
|
||||
const parsedInputRange = useMemo(
|
||||
() => ({
|
||||
fromMs: parseUtc(fromValue),
|
||||
toMs: parseUtc(toValue)
|
||||
fromMs: parseLocal(fromValue),
|
||||
toMs: parseLocal(toValue)
|
||||
}),
|
||||
[fromValue, toValue]
|
||||
);
|
||||
@@ -158,8 +158,8 @@ export function TguPlanningPage() {
|
||||
return {
|
||||
spacecraftId: selectedSpacecraftId,
|
||||
windows: zrvWindows.map((zrv) => ({
|
||||
startMs: parseUtc(zrv.startTime),
|
||||
endMs: parseUtc(zrv.endTime),
|
||||
startMs: parseLocal(zrv.startTime),
|
||||
endMs: parseLocal(zrv.endTime),
|
||||
kppId: zrv.kppId
|
||||
}))
|
||||
};
|
||||
@@ -221,8 +221,8 @@ export function TguPlanningPage() {
|
||||
const quickRange = (hours: number) => {
|
||||
const fromMs = startOfCurrentDayMs();
|
||||
const toMs = fromMs + hours * 60 * 60 * 1000;
|
||||
setFromValue(toUtcInputValue(fromMs));
|
||||
setToValue(toUtcInputValue(toMs));
|
||||
setFromValue(toLocalInputValue(fromMs));
|
||||
setToValue(toLocalInputValue(toMs));
|
||||
setAppliedRange({ fromMs, toMs });
|
||||
setPageError(undefined);
|
||||
setDecisionNotice(undefined);
|
||||
@@ -442,7 +442,7 @@ function defaultRange(): TimelineRange {
|
||||
|
||||
function startOfCurrentDayMs(): number {
|
||||
const now = new Date();
|
||||
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||
}
|
||||
|
||||
function historyDaysForRange(range: TimelineRange): number {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "./tguTimelineLayout";
|
||||
import { platformLabel } from "./tguTimelineMapper";
|
||||
import { statusStyle } from "./tguStatus";
|
||||
import { formatUtcDate, formatUtcDateTime, formatUtcTime, parseUtc } from "../../utils/utcTime";
|
||||
import { formatLocalDate, formatLocalDateTime, formatLocalTime, parseLocal } from "../../utils/localTime";
|
||||
|
||||
export type ZrvWindow = { startMs: number; endMs: number; kppId: string };
|
||||
|
||||
@@ -196,7 +196,7 @@ export function TguTimeline({
|
||||
onSelectZrv(overlapping, Math.max(0, idx));
|
||||
}}
|
||||
>
|
||||
<title>ЗРВ {zrv.kppId} · {formatUtcDateTime(zrv.startMs)} – {formatUtcDateTime(zrv.endMs)}</title>
|
||||
<title>ЗРВ {zrv.kppId} · {formatLocalDateTime(zrv.startMs)} – {formatLocalDateTime(zrv.endMs)}</title>
|
||||
</rect>
|
||||
);
|
||||
})}
|
||||
@@ -268,13 +268,13 @@ function TimelineArrow({ from, to }: { from: TimelinePlanSegment; to: TimelinePl
|
||||
}
|
||||
|
||||
function formatTickDate(ms: number): string {
|
||||
return formatUtcDate(ms);
|
||||
return formatLocalDate(ms);
|
||||
}
|
||||
|
||||
function formatTickTime(ms: number): string {
|
||||
return formatUtcTime(ms);
|
||||
return formatLocalTime(ms);
|
||||
}
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
return formatUtcDateTime(parseUtc(value));
|
||||
return formatLocalDateTime(parseLocal(value));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ZrvWindow } from "./TguTimeline";
|
||||
import { formatUtcDateTime } from "../../utils/utcTime";
|
||||
import { formatLocalDateTime } from "../../utils/localTime";
|
||||
|
||||
type Props = {
|
||||
windows: ZrvWindow[];
|
||||
@@ -35,7 +35,7 @@ export function TguZrvPanel({ windows, selectedIndex, onSelect, onClose }: Props
|
||||
<span className="tgu-map-request-row__body">
|
||||
<span className="tgu-map-request-row__name">КПП {zrv.kppId}</span>
|
||||
<span className="tgu-map-request-row__meta">
|
||||
{formatUtcDateTime(zrv.startMs)} – {formatUtcDateTime(zrv.endMs)}
|
||||
{formatLocalDateTime(zrv.startMs)} – {formatLocalDateTime(zrv.endMs)}
|
||||
{" · "}
|
||||
{formatDuration(zrv.endMs - zrv.startMs)}
|
||||
</span>
|
||||
@@ -52,10 +52,10 @@ export function TguZrvPanel({ windows, selectedIndex, onSelect, onClose }: Props
|
||||
<dd className="mono">{selected.kppId}</dd>
|
||||
|
||||
<dt>Начало</dt>
|
||||
<dd className="mono">{formatUtcDateTime(selected.startMs)}</dd>
|
||||
<dd className="mono">{formatLocalDateTime(selected.startMs)}</dd>
|
||||
|
||||
<dt>Конец</dt>
|
||||
<dd className="mono">{formatUtcDateTime(selected.endMs)}</dd>
|
||||
<dd className="mono">{formatLocalDateTime(selected.endMs)}</dd>
|
||||
|
||||
<dt>Длительность</dt>
|
||||
<dd className="mono">{formatDuration(selected.endMs - selected.startMs)}</dd>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { buildChainPlanRequest, futureZrvOptions, type ZrvOption } from "./createChainWindows";
|
||||
import type { ZrvWindowResponse } from "../../api/tguPlanApi";
|
||||
|
||||
const NOW = Date.UTC(2026, 5, 3, 12, 0, 0);
|
||||
const NOW = new Date(2026, 5, 3, 12, 0, 0).getTime();
|
||||
|
||||
function zrv(startIso: string, endIso: string, kppId = "KPP-1"): ZrvWindowResponse {
|
||||
return { startTime: startIso, endTime: endIso, kppId };
|
||||
@@ -11,19 +11,19 @@ function zrv(startIso: string, endIso: string, kppId = "KPP-1"): ZrvWindowRespon
|
||||
describe("futureZrvOptions", () => {
|
||||
it("keeps only future zones and sorts by start", () => {
|
||||
const windows = [
|
||||
zrv("2026-06-03T15:00:00Z", "2026-06-03T15:10:00Z"),
|
||||
zrv("2026-06-03T11:00:00Z", "2026-06-03T11:10:00Z"), // в прошлом → отбросить
|
||||
zrv("2026-06-03T13:00:00Z", "2026-06-03T13:10:00Z")
|
||||
zrv("2026-06-03T15:00:00", "2026-06-03T15:10:00"),
|
||||
zrv("2026-06-03T11:00:00", "2026-06-03T11:10:00"), // в прошлом → отбросить
|
||||
zrv("2026-06-03T13:00:00", "2026-06-03T13:10:00")
|
||||
];
|
||||
const options = futureZrvOptions(windows, NOW);
|
||||
expect(options.map((o) => o.startMs)).toEqual([
|
||||
Date.UTC(2026, 5, 3, 13, 0, 0),
|
||||
Date.UTC(2026, 5, 3, 15, 0, 0)
|
||||
new Date(2026, 5, 3, 13, 0, 0).getTime(),
|
||||
new Date(2026, 5, 3, 15, 0, 0).getTime()
|
||||
]);
|
||||
});
|
||||
|
||||
it("produces naive UTC (no zone) start/stop strings", () => {
|
||||
const [option] = futureZrvOptions([zrv("2026-06-03T13:00:00Z", "2026-06-03T13:10:00Z")], NOW);
|
||||
it("produces naive (no zone) start/stop strings", () => {
|
||||
const [option] = futureZrvOptions([zrv("2026-06-03T13:00:00", "2026-06-03T13:10:00")], NOW);
|
||||
expect(option.start).toBe("2026-06-03T13:00:00");
|
||||
expect(option.stop).toBe("2026-06-03T13:10:00");
|
||||
expect(option.kppId).toBe("KPP-1");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal, toLocalIso } from "../../utils/localTime";
|
||||
import type { CreateChainPlanRequest, ZrvWindowResponse } from "../../api/tguPlanApi";
|
||||
|
||||
/**
|
||||
* Опция выбора ЗРВ в диалоге «Создать план». Из стартовой зоны берутся начало окна и КПП, из
|
||||
* конечной — конец окна. `start`/`stop` — naive UTC (ISO без зоны), как ждёт `LocalDateTime` бэка;
|
||||
* конечной — конец окна. `start`/`stop` — naive (ISO без зоны, локальное), как ждёт `LocalDateTime` бэка;
|
||||
* `startMs`/`endMs` — для сортировки/сравнения и подсветки на таймлайне.
|
||||
*/
|
||||
export type ZrvOption = {
|
||||
@@ -11,9 +11,9 @@ export type ZrvOption = {
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
/** Naive UTC начала зоны. */
|
||||
/** Naive (локальное) начала зоны. */
|
||||
start: string;
|
||||
/** Naive UTC конца зоны. */
|
||||
/** Naive (локальное) конца зоны. */
|
||||
stop: string;
|
||||
kppId: string;
|
||||
};
|
||||
@@ -22,14 +22,14 @@ export type ZrvOption = {
|
||||
export function futureZrvOptions(windows: ZrvWindowResponse[], nowMs: number): ZrvOption[] {
|
||||
return windows
|
||||
.map((zrv) => {
|
||||
const startMs = parseUtc(zrv.startTime);
|
||||
const endMs = parseUtc(zrv.endTime);
|
||||
const startMs = parseLocal(zrv.startTime);
|
||||
const endMs = parseLocal(zrv.endTime);
|
||||
return {
|
||||
id: `${zrv.kppId}|${zrv.startTime}|${zrv.endTime}`,
|
||||
startMs,
|
||||
endMs,
|
||||
start: toNaiveUtc(startMs),
|
||||
stop: toNaiveUtc(endMs),
|
||||
start: toNaive(startMs),
|
||||
stop: toNaive(endMs),
|
||||
kppId: zrv.kppId
|
||||
};
|
||||
})
|
||||
@@ -60,6 +60,6 @@ export function buildChainPlanRequest(
|
||||
};
|
||||
}
|
||||
|
||||
function toNaiveUtc(ms: number): string {
|
||||
return new Date(ms).toISOString().slice(0, 19);
|
||||
function toNaive(ms: number): string {
|
||||
return toLocalIso(ms);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TguPlanUi, TimelineRow } from "../../model/timelineTypes";
|
||||
import { buildSequentialLinks, buildTimelineLayout, clipPlanToRange, timeToX } from "./tguTimelineLayout";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal } from "../../utils/localTime";
|
||||
|
||||
const range = {
|
||||
fromMs: parseUtc("2026-05-29T00:00:00"),
|
||||
toMs: parseUtc("2026-05-30T00:00:00")
|
||||
fromMs: parseLocal("2026-05-29T00:00:00"),
|
||||
toMs: parseLocal("2026-05-30T00:00:00")
|
||||
};
|
||||
|
||||
function plan(id: string, startTime: string, endTime: string, spacecraftId = "56756"): TguPlanUi {
|
||||
@@ -16,14 +16,14 @@ function plan(id: string, startTime: string, endTime: string, spacecraftId = "56
|
||||
endTime,
|
||||
kppId: "KPP-1",
|
||||
status: "PLANNED",
|
||||
startMs: parseUtc(startTime),
|
||||
endMs: parseUtc(endTime)
|
||||
startMs: parseLocal(startTime),
|
||||
endMs: parseLocal(endTime)
|
||||
};
|
||||
}
|
||||
|
||||
describe("tguTimelineLayout", () => {
|
||||
it("maps time to x inside the selected interval", () => {
|
||||
expect(timeToX(parseUtc("2026-05-29T12:00:00"), range, 1200)).toBe(600);
|
||||
expect(timeToX(parseLocal("2026-05-29T12:00:00"), range, 1200)).toBe(600);
|
||||
});
|
||||
|
||||
it("clips partially visible plans to the selected interval", () => {
|
||||
@@ -31,7 +31,7 @@ describe("tguTimelineLayout", () => {
|
||||
|
||||
expect(clipped).toEqual({
|
||||
startMs: range.fromMs,
|
||||
endMs: parseUtc("2026-05-29T02:00:00")
|
||||
endMs: parseLocal("2026-05-29T02:00:00")
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import type { TguPlatform, TguPlanStatus } from "../../model/tguTypes";
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { isOperationalPlatform, operationalSpacecraftIds, pausedSpacecraftIds } from "./tguTimelineMapper";
|
||||
import { toLocalIso } from "../../utils/localTime";
|
||||
|
||||
function platform(overrides: Partial<TguPlatform>): TguPlatform {
|
||||
return { id: "x", noradId: 1, status: "OPERATIONAL", ...overrides };
|
||||
@@ -11,8 +12,8 @@ function uiPlan(spacecraftId: string, status: TguPlanStatus, startMs: number): T
|
||||
return {
|
||||
planId: `${spacecraftId}-${status}-${startMs}`,
|
||||
spacecraftId,
|
||||
startTime: new Date(startMs).toISOString().slice(0, 19),
|
||||
endTime: new Date(startMs + 3_600_000).toISOString().slice(0, 19),
|
||||
startTime: toLocalIso(startMs),
|
||||
endTime: toLocalIso(startMs + 3_600_000),
|
||||
kppId: "KPP-1",
|
||||
status,
|
||||
startMs,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TguPlan, TguPlatform } from "../../model/tguTypes";
|
||||
import type { TguPlanUi, TguPlatformUi, TimelineRange, TimelineRow } from "../../model/timelineTypes";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal } from "../../utils/localTime";
|
||||
|
||||
const PROBLEM_STATUSES = new Set(["REJECTED", "START_AMBIGUOUS"]);
|
||||
|
||||
@@ -27,8 +27,8 @@ export function mapPlans(plans: TguPlan[], platforms: TguPlatform[]): TguPlanUi[
|
||||
|
||||
return plans
|
||||
.map((plan) => {
|
||||
const startMs = parseUtc(plan.startTime);
|
||||
const endMs = parseUtc(plan.endTime);
|
||||
const startMs = parseLocal(plan.startTime);
|
||||
const endMs = parseLocal(plan.endTime);
|
||||
const platform = platformByNorad.get(plan.spacecraftId);
|
||||
return {
|
||||
...plan,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
RsaParams,
|
||||
RsaResultType
|
||||
} from "../../model/requestTypes";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal } from "../../utils/localTime";
|
||||
import { DateTime24Field } from "../../components/timeFields";
|
||||
|
||||
export type RequestFormSubmit = {
|
||||
@@ -87,8 +87,8 @@ export function RequestCreatePanel({
|
||||
const [rsaResolution, setRsaResolution] = useState("1");
|
||||
|
||||
const importanceNum = Number(importance);
|
||||
const beginMs = parseUtc(begin);
|
||||
const endMs = parseUtc(end);
|
||||
const beginMs = parseLocal(begin);
|
||||
const endMs = parseLocal(end);
|
||||
|
||||
const kppList = parseKpp(kpp);
|
||||
const kppValid = kppList !== null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { RequestAtPoint } from "../tgu-map-2d/Tgu2DMapRequestsPanel";
|
||||
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
|
||||
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
||||
|
||||
type Props = {
|
||||
requests: RequestAtPoint[];
|
||||
@@ -155,7 +155,7 @@ export function RequestDetailsPanel({
|
||||
}
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
return formatUtcDateTime(parseUtc(value));
|
||||
return formatLocalDateTime(parseLocal(value));
|
||||
}
|
||||
|
||||
function surveyTypeLabel(surveyType: string): string {
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 { parseUtc, toUtcInputValue, toUtcIso } from "../../utils/utcTime";
|
||||
import { parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
|
||||
import { DateTime24Field } from "../../components/timeFields";
|
||||
import { RequestCreatePanel, type RequestFormSubmit } from "./RequestCreatePanel";
|
||||
import { RequestDetailsPanel } from "./RequestDetailsPanel";
|
||||
@@ -37,8 +37,8 @@ type PanelMode = "none" | "details" | "create";
|
||||
|
||||
function buildInitialLoadState(range: TimelineRange): RequestsLoadState {
|
||||
return {
|
||||
fromValue: toUtcInputValue(range.fromMs),
|
||||
toValue: toUtcInputValue(range.toMs),
|
||||
fromValue: toLocalInputValue(range.fromMs),
|
||||
toValue: toLocalInputValue(range.toMs),
|
||||
items: [],
|
||||
loading: false,
|
||||
loadedCount: 0,
|
||||
@@ -75,14 +75,14 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
|
||||
}, []);
|
||||
|
||||
const loadRequests = useCallback(async (fromValue: string, toValue: string) => {
|
||||
const fromMs = parseUtc(fromValue);
|
||||
const toMs = parseUtc(toValue);
|
||||
const fromMs = parseLocal(fromValue);
|
||||
const toMs = parseLocal(toValue);
|
||||
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs >= toMs) return;
|
||||
|
||||
setRequestsState((current) => ({ ...current, loading: true, error: undefined, loadedCount: 0 }));
|
||||
try {
|
||||
const result = await fetchRequestsForMap(
|
||||
{ endFrom: new Date(fromMs).toISOString().slice(0, 19), beginTo: new Date(toMs).toISOString().slice(0, 19) },
|
||||
{ endFrom: toLocalIso(fromMs), beginTo: toLocalIso(toMs) },
|
||||
(loaded, total) => setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total }))
|
||||
);
|
||||
setRequestsState((current) => ({
|
||||
@@ -204,8 +204,8 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
|
||||
name: form.name,
|
||||
geometry,
|
||||
importance: form.importance,
|
||||
beginDateTime: toUtcIso(form.beginDateTime),
|
||||
endDateTime: toUtcIso(form.endDateTime),
|
||||
beginDateTime: toLocalIso(parseLocal(form.beginDateTime)),
|
||||
endDateTime: toLocalIso(parseLocal(form.endDateTime)),
|
||||
kpp: form.kpp,
|
||||
highPriorityTransmit: form.highPriorityTransmit,
|
||||
optics: form.optics,
|
||||
@@ -244,8 +244,8 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
|
||||
[closeAll, loadRequests, requestsState.fromValue, requestsState.toValue]
|
||||
);
|
||||
|
||||
const fromMs = parseUtc(requestsState.fromValue);
|
||||
const toMs = parseUtc(requestsState.toValue);
|
||||
const fromMs = parseLocal(requestsState.fromValue);
|
||||
const toMs = parseLocal(requestsState.toValue);
|
||||
const rangeInvalid = !Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs >= toMs;
|
||||
|
||||
const hasPanel = panelMode !== "none";
|
||||
|
||||
Reference in New Issue
Block a user