Формат времени в UTC
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { TguApiError } from '../model/tguTypes'
|
||||
import { toLocalIso } from '../utils/localTime'
|
||||
import { toUtcIso } from '../utils/localTime'
|
||||
import { apiFetch } from './apiFetch'
|
||||
|
||||
const BASE = '/api/pcp-ballistics'
|
||||
@@ -67,7 +67,7 @@ export type PointVisibilityParam = {
|
||||
noradId: number
|
||||
/** Идентификатор объекта (цели) из запроса. */
|
||||
objectId: string
|
||||
/** Момент видимости (локальное время, ISO без зоны). */
|
||||
/** Момент видимости (UTC ISO). */
|
||||
time: string
|
||||
/** Номер витка. */
|
||||
revolution: number
|
||||
@@ -161,7 +161,7 @@ export async function fetchPointVisibility(
|
||||
|
||||
/** Целеуказание (момент/угол места/азимут/дальность) границы зоны радиовидимости. */
|
||||
export type RvaTargetPosition = {
|
||||
/** Момент (локальное время, ISO без зоны). */
|
||||
/** Момент (UTC ISO). */
|
||||
time: string
|
||||
/** Угол места, град. */
|
||||
elevation: number
|
||||
@@ -233,13 +233,13 @@ export async function fetchSatelliteRva(
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime без зоны (локальные настенные часы): YYYY-MM-DDTHH:mm:ss.
|
||||
* UTC ISO date-time for API DTO/query params.
|
||||
* @param ms Время в миллисекундах от эпохи.
|
||||
*
|
||||
* @returns Строка LocalDateTime без зоны.
|
||||
* @returns Строка UTC date-time.
|
||||
*/
|
||||
function toLocalDateTime(ms: number): string {
|
||||
return toLocalIso(ms)
|
||||
return toUtcIso(ms)
|
||||
}
|
||||
|
||||
function extractErrorText(body: string): string {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TguApiError } from '../model/tguTypes'
|
||||
import { toLocalIso } from '../utils/localTime'
|
||||
import { toUtcIso } from '../utils/localTime'
|
||||
import { apiFetch } from './apiFetch'
|
||||
|
||||
/**
|
||||
@@ -20,9 +20,9 @@ export const MAX_COMPLEX_PLAN_RANGE_MS = MAX_COMPLEX_PLAN_RANGE_DAYS * 24 * 60 *
|
||||
export type ComplexPlanRunStatus = 'CREATED' | 'RUNNING' | 'COMPLETED' | 'FAILED'
|
||||
|
||||
export type RunComplexPlanParams = {
|
||||
/** Начало интервала расчёта (эпоха-мс, локальное). */
|
||||
/** Начало интервала расчёта (эпоха-мс, абсолютный момент). */
|
||||
fromMs: number
|
||||
/** Конец интервала расчёта (эпоха-мс, локальное). */
|
||||
/** Конец интервала расчёта (эпоха-мс, абсолютный момент). */
|
||||
toMs: number
|
||||
/** NORAD-id КА, по которым считаем (все эксплуатируемые КА вкладки «Планы»). */
|
||||
satelliteIds: number[]
|
||||
@@ -82,7 +82,7 @@ export type ComplexPlanRunSummary = {
|
||||
coveredAreaPercent: number
|
||||
}
|
||||
|
||||
/** Режим съёмки/сброса из результата расчёта (SatelliteModeResponseDTO). Времена — локальные. */
|
||||
/** Режим съёмки/сброса из результата расчёта (SatelliteModeResponseDTO). Времена приходят из API как UTC ISO и отображаются в локальной зоне браузера. */
|
||||
export type ComplexPlanMode = {
|
||||
id: number
|
||||
satelliteId: number
|
||||
@@ -115,8 +115,8 @@ export type ComplexPlanMode = {
|
||||
*/
|
||||
export async function runComplexPlan(params: RunComplexPlanParams): Promise<ComplexPlanRunResult> {
|
||||
const body = {
|
||||
intervalStart: toLocalIso(params.fromMs),
|
||||
intervalEnd: toLocalIso(params.toMs),
|
||||
intervalStart: toUtcIso(params.fromMs),
|
||||
intervalEnd: toUtcIso(params.toMs),
|
||||
satelliteIds: params.satelliteIds,
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export type SurveyRouteAnchor = 'CENTERED' | 'FORWARD'
|
||||
export type BuildSurveyRouteParams = {
|
||||
/** Идентификатор КА (NORAD — тот же, что в obj-view/mpl-point). */
|
||||
satelliteId: number
|
||||
/** Момент видимости (локальное время, ISO без зоны). */
|
||||
/** Момент видимости (UTC ISO). */
|
||||
time: string
|
||||
/** Длительность съёмки, секунды. */
|
||||
durationSeconds: number
|
||||
@@ -22,17 +22,17 @@ export type BuildSurveyRouteParams = {
|
||||
/** Способ построения окна относительно момента видимости. */
|
||||
anchor?: SurveyRouteAnchor
|
||||
/**
|
||||
* Начало интервала плана (локальное время, ISO без зоны). Передаётся при сохранении,
|
||||
* Начало интервала плана (UTC ISO). Передаётся при сохранении,
|
||||
* чтобы при автосоздании миссии/плана задать корректное окно (для пересечений/наложений).
|
||||
*/
|
||||
planStart?: string
|
||||
/** Конец интервала плана (локальное время, ISO без зоны). См. planStart. */
|
||||
/** Конец интервала плана (UTC ISO). См. planStart. */
|
||||
planEnd?: string
|
||||
}
|
||||
|
||||
export type SurveyRoute = {
|
||||
satelliteId: number
|
||||
/** Начало съёмки (локальное время, ISO без зоны). */
|
||||
/** Начало съёмки (UTC ISO). */
|
||||
timeStart: string
|
||||
/** Длительность съёмки, секунды. */
|
||||
duration: number
|
||||
@@ -87,9 +87,9 @@ export type SaveDropRouteParams = {
|
||||
station: string
|
||||
/** Номер витка зоны радиовидимости. */
|
||||
revolution: number
|
||||
/** Начало зоны (локальное время, ISO без зоны). */
|
||||
/** Начало зоны (UTC ISO). */
|
||||
zoneStart: string
|
||||
/** Конец зоны (локальное время, ISO без зоны). */
|
||||
/** Конец зоны (UTC ISO). */
|
||||
zoneStop: string
|
||||
/** Идентификаторы режимов съёмки (survey_modes), сбрасываемых в этой зоне. */
|
||||
surveyModeIds: number[]
|
||||
@@ -168,7 +168,7 @@ export async function saveDropRoute(
|
||||
return JSON.parse(text) as DropMode
|
||||
}
|
||||
|
||||
/** Итог расчёта плана съёмки (ответ /surveys/calculate). Времена — локальные. */
|
||||
/** Итог расчёта плана съёмки (ответ /surveys/calculate). Времена приходят из API как UTC ISO и отображаются UI в локальной зоне браузера. */
|
||||
export type SurveyCalculationResult = {
|
||||
missionId: string
|
||||
completed: boolean
|
||||
|
||||
@@ -12,7 +12,7 @@ export type DraftMode = 'replace' | 'fork-forward'
|
||||
|
||||
export type CreateDraftRequest = {
|
||||
mode: DraftMode
|
||||
/** Начало окна целевой ЗРВ (naive, ISO без зоны, локальное). Обязательно для fork-forward. */
|
||||
/** Начало окна целевой ЗРВ (UTC ISO). Обязательно для fork-forward. */
|
||||
windowStart?: string
|
||||
/** Конец окна целевой ЗРВ. Обязательно для fork-forward. */
|
||||
windowStop?: string
|
||||
@@ -54,7 +54,7 @@ export type ZrvWindowResponse = {
|
||||
/**
|
||||
* Запрос создания плана новой цепочки (вкладка «Планы»). Окно набирается из двух ЗРВ:
|
||||
* `windowStart`/`windowKppId` — старт выбранной стартовой зоны, `windowStop` — конец конечной.
|
||||
* Времена — naive (ISO без зоны, локальное), как ждёт `LocalDateTime` бэка. `basePlanId` — предыстория
|
||||
* Времена отправляются в API как UTC ISO с `Z`. `basePlanId` — предыстория
|
||||
* (история тянется) либо `null` (цепочка без истории, пустой план).
|
||||
*/
|
||||
export type CreateChainPlanRequest = {
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
||||
import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
|
||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||
import { MapObjectInspector } from "../tgu-map-2d/MapObjectInspector";
|
||||
import { formatLocalDateTime, parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
|
||||
import { formatLocalDateTime, parseLocal, toLocalInputValue, toUtcIso } from "../../utils/localTime";
|
||||
import { formatVolume } from "../../utils/dataVolume";
|
||||
|
||||
type TguComplexPlanTabProps = {
|
||||
@@ -173,7 +173,7 @@ export function TguComplexPlanTab({ range, platforms }: TguComplexPlanTabProps)
|
||||
let cancelled = false;
|
||||
setRequestsLoading(true);
|
||||
setRequestsError(undefined);
|
||||
fetchRequestsForMap({ endFrom: toLocalIso(fromMs), beginTo: toLocalIso(toMs) })
|
||||
fetchRequestsForMap({ endFrom: toUtcIso(fromMs), beginTo: toUtcIso(toMs) })
|
||||
.then((result) => {
|
||||
if (!cancelled) setRequestItems(result.items);
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { abandonDraft, createDraft, fetchDraftPlans } from "../../api/tguPlanApi";
|
||||
import { TguApiError } from "../../model/tguTypes";
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { parseLocal, toLocalIso } from "../../utils/localTime";
|
||||
import { parseLocal, toUtcIso } 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";
|
||||
@@ -360,8 +360,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
void fetchRequestsForMap({
|
||||
endFrom: toLocalIso(window.fromMs),
|
||||
beginTo: toLocalIso(window.toMs)
|
||||
endFrom: toUtcIso(window.fromMs),
|
||||
beginTo: toUtcIso(window.toMs)
|
||||
}).then((result) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setRequestItems(result.items);
|
||||
@@ -458,7 +458,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
void buildSurveyRoute({
|
||||
satelliteId: numericNorad,
|
||||
// Окно задано явно (начало/конец) — строим вперёд от начала на выбранную длительность.
|
||||
time: toLocalIso(routeWindow.startMs),
|
||||
time: toUtcIso(routeWindow.startMs),
|
||||
durationSeconds,
|
||||
roll: routeRoll,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
@@ -617,9 +617,9 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
noradId: noradId ? Number(noradId) : 0,
|
||||
stationId: Number.isFinite(stationNum) ? stationNum : 0,
|
||||
revolution: work.revolution ?? 0,
|
||||
onStart: { time: toLocalIso(work.startMs).slice(0, 19), elevation: 0, azimuth: 0, range: 0 },
|
||||
onMaximum: { time: toLocalIso(midMs).slice(0, 19), elevation: 0, azimuth: 0, range: 0 },
|
||||
onStop: { time: toLocalIso(work.endMs).slice(0, 19), elevation: 0, azimuth: 0, range: 0 },
|
||||
onStart: { time: toUtcIso(work.startMs), elevation: 0, azimuth: 0, range: 0 },
|
||||
onMaximum: { time: toUtcIso(midMs), elevation: 0, azimuth: 0, range: 0 },
|
||||
onStop: { time: toUtcIso(work.endMs), elevation: 0, azimuth: 0, range: 0 },
|
||||
duration: Math.max(1, (work.endMs - work.startMs) / 1000)
|
||||
};
|
||||
editDropPrefillRef.current = { zone, surveyIds: work.surveyIds ?? [] };
|
||||
@@ -744,15 +744,15 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
if (editWork) await deleteEditedModeFromDraft(draftId, draftExistedBefore, editWork, "SURVEY");
|
||||
savedRoute = await saveSurveyRoute(draftId, {
|
||||
satelliteId: numericNorad,
|
||||
time: toLocalIso(routeWindow.startMs),
|
||||
time: toUtcIso(routeWindow.startMs),
|
||||
durationSeconds: routeDurationSeconds,
|
||||
roll: routeRoll,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
revolution: selectedShootPass.revolution ?? 0,
|
||||
anchor: "FORWARD",
|
||||
// Окно выбранного плана — чтобы при автосоздании миссии/плана задать корректный интервал.
|
||||
planStart: toLocalIso(selectedPlan.startMs),
|
||||
planEnd: toLocalIso(selectedPlan.endMs)
|
||||
planStart: toUtcIso(selectedPlan.startMs),
|
||||
planEnd: toUtcIso(selectedPlan.endMs)
|
||||
});
|
||||
setSurveyRoute(savedRoute);
|
||||
// Ждём перечитывания planModes ДО снятия editingWorkId ниже: иначе на миг editingModeId
|
||||
@@ -853,8 +853,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
zoneStop: selectedZone.onStop.time.slice(0, 19),
|
||||
surveyModeIds: [...selectedDropSurveyIds],
|
||||
// Окно выбранного плана — для автосоздания миссии/плана с корректным интервалом.
|
||||
planStart: toLocalIso(selectedPlan.startMs),
|
||||
planEnd: toLocalIso(selectedPlan.endMs)
|
||||
planStart: toUtcIso(selectedPlan.startMs),
|
||||
planEnd: toUtcIso(selectedPlan.endMs)
|
||||
});
|
||||
savedDropId = savedDrop.id;
|
||||
savedDropVolumeMb = savedDrop.volumeMb;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Tgu2DMapSidebar } from "./Tgu2DMapSidebar";
|
||||
import { Tgu2DMapToolbar } from "./Tgu2DMapToolbar";
|
||||
import { Tgu2DMapView } from "./Tgu2DMapView";
|
||||
import { getTgu2DMapIntervalState, TGU_2D_MAP_INTERVAL_WARNING } from "./tgu2DMapInterval";
|
||||
import { parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
|
||||
import { parseLocal, toLocalInputValue, toUtcIso } from "../../utils/localTime";
|
||||
|
||||
type Tgu2DMapTabProps = {
|
||||
range: TimelineRange;
|
||||
@@ -82,8 +82,8 @@ export function Tgu2DMapTab({
|
||||
try {
|
||||
const result = await fetchRequestsForMap(
|
||||
{
|
||||
endFrom: toLocalIso(fromMs),
|
||||
beginTo: toLocalIso(toMs)
|
||||
endFrom: toUtcIso(fromMs),
|
||||
beginTo: toUtcIso(toMs)
|
||||
},
|
||||
(loaded, total) => {
|
||||
setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total }));
|
||||
|
||||
@@ -22,10 +22,10 @@ describe("futureZrvOptions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("produces naive (no zone) start/stop strings", () => {
|
||||
it("produces UTC 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.start).toBe(new Date(2026, 5, 3, 13, 0, 0).toISOString());
|
||||
expect(option.stop).toBe(new Date(2026, 5, 3, 13, 10, 0).toISOString());
|
||||
expect(option.kppId).toBe("KPP-1");
|
||||
});
|
||||
});
|
||||
@@ -33,11 +33,11 @@ describe("futureZrvOptions", () => {
|
||||
describe("buildChainPlanRequest", () => {
|
||||
const start: ZrvOption = {
|
||||
id: "a", startMs: Date.UTC(2026, 5, 3, 13, 0, 0), endMs: Date.UTC(2026, 5, 3, 13, 10, 0),
|
||||
start: "2026-06-03T13:00:00", stop: "2026-06-03T13:10:00", kppId: "KPP-1"
|
||||
start: "2026-06-03T13:00:00.000Z", stop: "2026-06-03T13:10:00.000Z", kppId: "KPP-1"
|
||||
};
|
||||
const end: ZrvOption = {
|
||||
id: "b", startMs: Date.UTC(2026, 5, 3, 15, 0, 0), endMs: Date.UTC(2026, 5, 3, 15, 10, 0),
|
||||
start: "2026-06-03T15:00:00", stop: "2026-06-03T15:10:00", kppId: "KPP-2"
|
||||
start: "2026-06-03T15:00:00.000Z", stop: "2026-06-03T15:10:00.000Z", kppId: "KPP-2"
|
||||
};
|
||||
|
||||
it("spans from start zone begin to end zone finish and keeps the start kpp", () => {
|
||||
@@ -45,16 +45,16 @@ describe("buildChainPlanRequest", () => {
|
||||
expect(req).toEqual({
|
||||
spacecraftId: "25544",
|
||||
basePlanId: "base-1",
|
||||
windowStart: "2026-06-03T13:00:00",
|
||||
windowStop: "2026-06-03T15:10:00",
|
||||
windowStart: "2026-06-03T13:00:00.000Z",
|
||||
windowStop: "2026-06-03T15:10:00.000Z",
|
||||
windowKppId: "KPP-1"
|
||||
});
|
||||
});
|
||||
|
||||
it("allows a single-zone window (start == end zone)", () => {
|
||||
const req = buildChainPlanRequest("25544", null, start, start);
|
||||
expect(req.windowStart).toBe("2026-06-03T13:00:00");
|
||||
expect(req.windowStop).toBe("2026-06-03T13:10:00");
|
||||
expect(req.windowStart).toBe("2026-06-03T13:00:00.000Z");
|
||||
expect(req.windowStop).toBe("2026-06-03T13:10:00.000Z");
|
||||
expect(req.basePlanId).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { parseLocal, toLocalIso } from "../../utils/localTime";
|
||||
import { parseLocal, toUtcIso } from "../../utils/localTime";
|
||||
import type { CreateChainPlanRequest, ZrvWindowResponse } from "../../api/tguPlanApi";
|
||||
|
||||
/**
|
||||
* Опция выбора ЗРВ в диалоге «Создать план». Из стартовой зоны берутся начало окна и КПП, из
|
||||
* конечной — конец окна. `start`/`stop` — naive (ISO без зоны, локальное), как ждёт `LocalDateTime` бэка;
|
||||
* конечной — конец окна. `start`/`stop` — UTC ISO строки для API;
|
||||
* `startMs`/`endMs` — для сортировки/сравнения и подсветки на таймлайне.
|
||||
*/
|
||||
export type ZrvOption = {
|
||||
@@ -11,9 +11,9 @@ export type ZrvOption = {
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
/** Naive (локальное) начала зоны. */
|
||||
/** UTC ISO начала зоны. */
|
||||
start: string;
|
||||
/** Naive (локальное) конца зоны. */
|
||||
/** UTC ISO конца зоны. */
|
||||
stop: string;
|
||||
kppId: string;
|
||||
};
|
||||
@@ -28,8 +28,8 @@ export function futureZrvOptions(windows: ZrvWindowResponse[], nowMs: number): Z
|
||||
id: `${zrv.kppId}|${zrv.startTime}|${zrv.endTime}`,
|
||||
startMs,
|
||||
endMs,
|
||||
start: toNaive(startMs),
|
||||
stop: toNaive(endMs),
|
||||
start: toApiUtc(startMs),
|
||||
stop: toApiUtc(endMs),
|
||||
kppId: zrv.kppId
|
||||
};
|
||||
})
|
||||
@@ -60,6 +60,6 @@ export function buildChainPlanRequest(
|
||||
};
|
||||
}
|
||||
|
||||
function toNaive(ms: number): string {
|
||||
return toLocalIso(ms);
|
||||
function toApiUtc(ms: number): string {
|
||||
return toUtcIso(ms);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { RequestAtPoint } from "./RequestDetailsPanel";
|
||||
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 { parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
|
||||
import { parseLocal, toLocalInputValue, toUtcIso } from "../../utils/localTime";
|
||||
import { DateTime24Field } from "../../components/timeFields";
|
||||
import { RequestCreatePanel, type RequestFormSubmit } from "./RequestCreatePanel";
|
||||
import { RequestDetailsPanel } from "./RequestDetailsPanel";
|
||||
@@ -82,7 +82,7 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
|
||||
setRequestsState((current) => ({ ...current, loading: true, error: undefined, loadedCount: 0 }));
|
||||
try {
|
||||
const result = await fetchRequestsForMap(
|
||||
{ endFrom: toLocalIso(fromMs), beginTo: toLocalIso(toMs) },
|
||||
{ endFrom: toUtcIso(fromMs), beginTo: toUtcIso(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: toLocalIso(parseLocal(form.beginDateTime)),
|
||||
endDateTime: toLocalIso(parseLocal(form.endDateTime)),
|
||||
beginDateTime: toUtcIso(parseLocal(form.beginDateTime)),
|
||||
endDateTime: toUtcIso(parseLocal(form.endDateTime)),
|
||||
kpp: form.kpp,
|
||||
highPriorityTransmit: form.highPriorityTransmit,
|
||||
optics: form.optics,
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes";
|
||||
import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
|
||||
import { MapObjectInspector } from "../tgu-map-2d/MapObjectInspector";
|
||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||
import { formatLocalDateTimeSeconds, parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
|
||||
import { formatLocalDateTimeSeconds, parseLocal, toLocalInputValue, toUtcIso } from "../../utils/localTime";
|
||||
import { SlotDaySelector, type SlotDay } from "./SlotDaySelector";
|
||||
import { PassStepper } from "./PassStepper";
|
||||
import { groupPasses } from "./slotPasses";
|
||||
@@ -469,7 +469,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
|
||||
setRequestsLoading(true);
|
||||
setRequestsError(undefined);
|
||||
try {
|
||||
const result = await fetchRequestsForMap({ endFrom: toLocalIso(fromMs), beginTo: toLocalIso(toMs) });
|
||||
const result = await fetchRequestsForMap({ endFrom: toUtcIso(fromMs), beginTo: toUtcIso(toMs) });
|
||||
setRequestItems(result.items);
|
||||
setRequestsTotal(result.totalItems);
|
||||
setRequestsTruncated(result.truncated);
|
||||
@@ -730,8 +730,8 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
|
||||
const result = await fetchRequestSlots(
|
||||
{
|
||||
requestId: selectedRequestId,
|
||||
timeStart: toLocalIso(parsedRange.fromMs),
|
||||
timeStop: toLocalIso(parsedRange.toMs),
|
||||
timeStart: toUtcIso(parsedRange.fromMs),
|
||||
timeStop: toUtcIso(parsedRange.toMs),
|
||||
revSign: revSign || undefined,
|
||||
// cov=true (режим покрытия) ровно когда задана стратегия — это путь «Подобрать
|
||||
// покрытие». «Показать все слоты» идёт без стратегии и получает полный набор кандидатов,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/**
|
||||
* Единый разбор и форматирование времени в локальной зоне.
|
||||
* Единые утилиты времени для UI.
|
||||
*
|
||||
* Бэкенд (Kotlin `LocalDateTime`) отдаёт время как ISO-строку без зоны
|
||||
* (например `"2026-05-29T01:00:00"`) — это настенные часы локальной зоны, в которой
|
||||
* работают все сервисы, без UTC-семантики. Браузер по умолчанию трактует такую naive-строку
|
||||
* как локальное время — это нам и нужно: разбор, показ и ввод идут в зоне браузера, а
|
||||
* сериализация назад отдаёт те же настенные часы naive-строкой без зоны.
|
||||
* Контракт API после UTC-перевода:
|
||||
* - backend/БД/DTO используют UTC;
|
||||
* - API отдаёт ISO-8601 UTC с `Z`;
|
||||
* - UI показывает время в часовом поясе браузера;
|
||||
* - значения из локальных полей ввода перед отправкой в API конвертируются в UTC ISO.
|
||||
*
|
||||
* Строки с явной зоной (`...Z`/офсет, напр. валидность платформ из НСИ) парсятся как
|
||||
* абсолютный момент и показываются в локальной зоне.
|
||||
* Legacy-строки API без зоны всё ещё могут встретиться при неполностью обновлённых сервисах.
|
||||
* В основном UI они обычно приходят из локальных input-полей, поэтому `parseLocal` сохраняет старое
|
||||
* поведение: naive-строка разбирается как локальное время браузера. После полного перевода API
|
||||
* новые ответы должны приходить с `Z` и будут разобраны как абсолютный UTC-момент.
|
||||
*/
|
||||
|
||||
function pad(n: number): string {
|
||||
@@ -60,10 +62,15 @@ export function toLocalInputValue(ms: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* naive `LocalDateTime` без зоны (`YYYY-MM-DDTHH:mm:ss`) из локальных компонент эпохи-мс.
|
||||
* Все бэкенды ожидают `LocalDateTime` без зоны — настенные часы локальной зоны.
|
||||
* Локальная ISO-строка без зоны (`YYYY-MM-DDTHH:mm:ss`) из локальных компонент.
|
||||
* Оставлена для локальных UI-расчётов и полей времени, не использовать для отправки API DTO.
|
||||
*/
|
||||
export function toLocalIso(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
/** UTC ISO-строка для DTO/query-параметров API. */
|
||||
export function toUtcIso(ms: number): string {
|
||||
return new Date(ms).toISOString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user