Files
dc-observatio/services/pcp-tgu-ops-ui/src/api/tguPlanApi.ts
T
Дмитрий Соловьев 057452f19d pcp-tgu-ops-ui: fork-forward в редакторе — выбор будущей ЗРВ для перевыдачи невозвратного плана
Селектор «Целевая ЗРВ» в EditorToolbar: текущий слот (replace, по умолчанию)
либо будущая ЗРВ из проекции pcp-tgu-service (fetchSpacecraftPlans →
GET /api/plans/{spacecraftId}, фильтр futureZrvWindows: PLANNED строго в
будущем, кроме базы — та же проекция, что валидирует бэк). Выбор окна
переводит обзор редактора и заставляет ensureDraft слать mode=fork-forward
(naive-UTC окно + kpp); селектор блокируется после создания DRAFT.
2026-06-03 14:12:21 +03:00

151 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { TguApiError } from "../model/tguTypes";
/**
* Вызовы версионирования/выдачи плана в pcp-tgu-service напрямую через Vite-прокси
* (`/api/pcp-tgu` → :7011), как и pcp-mission напрямую на :7010. BFF не используется.
*/
const BASE = "/api/pcp-tgu";
/** Режим наполнения черновика; та же строка уходит в clone миссии. */
export type DraftMode = "replace" | "fork-forward";
export type CreateDraftRequest = {
mode: DraftMode;
/** Начало окна целевой ЗРВ (UTC naive, ISO без зоны). Обязательно для fork-forward. */
windowStart?: string;
/** Конец окна целевой ЗРВ. Обязательно для fork-forward. */
windowStop?: string;
/** КПП целевой ЗРВ. Обязательно для fork-forward. */
windowKppId?: string;
};
export type CreateDraftResponse = {
/** Идентификатор созданного черновика (= mission_id клон-миссии). */
draftPlanId: string;
};
export type PromoteDraftResponse = {
processInstanceKey: number;
processDefinitionKey: number;
bpmnProcessId: string;
version: number;
planId: string;
attemptId: string;
};
/** План КА из проекции pcp-tgu-service (persisted будущая проекция). Времена — ISO с `Z`. */
export type TguPlanResponse = {
planId: string;
spacecraftId: string;
startTime: string;
endTime: string;
kppId: string;
status: string;
};
/**
* Цепочка планов одного КА из pcp-tgu-service (GET /api/plans/{spacecraftId}): persisted планы плюс
* будущая проекция (PLANNED). Используется fork-forward-пикером для выбора целевой ЗРВ — бэк сверяет
* выбранное окно ровно с этой же проекцией (assertWindowMatchesProjection).
*/
export async function fetchSpacecraftPlans(spacecraftId: string): Promise<TguPlanResponse[]> {
const url = `${BASE}/api/plans/${encodeURIComponent(spacecraftId)}`;
let response: Response;
try {
response = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
} catch {
throw new TguApiError("Не удалось подключиться к сервису планирования ТГУ.", "network");
}
const text = await response.text();
if (!response.ok) {
throw new TguApiError(extractErrorText(text) || "Не удалось получить планы КА.", "http", response.status);
}
return text.trim() ? (JSON.parse(text) as TguPlanResponse[]) : [];
}
/**
* Создаёт черновик плана (DRAFT) на основе базового плана
* (pcp-tgu-service, POST /api/plans/{basePlanId}/draft). Триггерит клон миссии; возвращает
* draftPlanId, в который адресуются правки (Ф3) и который применяется promote (Ф5).
*/
export async function createDraft(basePlanId: string, request: CreateDraftRequest): Promise<CreateDraftResponse> {
return postJson<CreateDraftResponse>(
`${BASE}/api/plans/${encodeURIComponent(basePlanId)}/draft`,
request,
"Не удалось создать черновик плана."
);
}
/**
* «Применить» черновик: промоутит DRAFT в живую цепочку и выдаёт его
* (pcp-tgu-service, POST /api/plans/{draftPlanId}/promote). Под оптимистичным локом базы и
* гардом точки невозврата: 409 — база уехала / уже идёт выдача; 422 — не DRAFT / слот сместился /
* заморожено; 404 — черновика нет.
*/
export async function promoteDraft(draftPlanId: string): Promise<PromoteDraftResponse> {
return postJson<PromoteDraftResponse>(
`${BASE}/api/plans/${encodeURIComponent(draftPlanId)}/promote`,
undefined,
"Не удалось применить черновик плана."
);
}
/**
* Отказ от черновика (pcp-tgu-service, DELETE /api/plans/{draftPlanId}): удаляет DRAFT и его
* клон-миссию, снимая блокировку «один открытый черновик на базу». 404 — нет; 422 — не DRAFT.
*/
export async function abandonDraft(draftPlanId: string): Promise<void> {
const url = `${BASE}/api/plans/${encodeURIComponent(draftPlanId)}`;
let response: Response;
try {
response = await fetch(url, { method: "DELETE", headers: { Accept: "application/json" } });
} catch {
throw new TguApiError("Не удалось подключиться к сервису планирования ТГУ.", "network");
}
if (!response.ok) {
const text = await response.text();
throw new TguApiError(extractErrorText(text) || "Не удалось отменить черновик плана.", "http", response.status);
}
}
async function postJson<T>(url: string, body: unknown, fallbackMessage: string): Promise<T> {
let response: Response;
try {
response = await fetch(url, {
method: "POST",
headers: body === undefined
? { Accept: "application/json" }
: { Accept: "application/json", "Content-Type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) })
});
} catch {
throw new TguApiError("Не удалось подключиться к сервису планирования ТГУ.", "network");
}
const text = await response.text();
if (!response.ok) {
throw new TguApiError(extractErrorText(text) || fallbackMessage, "http", response.status);
}
if (!text.trim()) {
return undefined as T;
}
return JSON.parse(text) as T;
}
function extractErrorText(body: string): string {
const trimmed = body.trim();
if (!trimmed) return "";
try {
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
for (const field of ["message", "error", "reason", "detail"]) {
const value = parsed[field];
if (typeof value === "string" && value.trim()) {
return value;
}
}
} catch {
return trimmed;
}
return trimmed;
}