189 lines
6.1 KiB
TypeScript
189 lines
6.1 KiB
TypeScript
import {
|
|
TguApiError,
|
|
type TguPlan,
|
|
type TguPlanDecision,
|
|
type TguPlanDecisionMessage,
|
|
type TguPlatform,
|
|
} from '../model/tguTypes'
|
|
import { toLocalIso } from '../utils/localTime'
|
|
import { apiFetch } from './apiFetch'
|
|
|
|
export type FlightLinePoint = {
|
|
time: string
|
|
revolution: number
|
|
lat: number
|
|
long: number
|
|
latLeft: number
|
|
longLeft: number
|
|
latInnerLeft: number
|
|
longInnerLeft: number
|
|
latInnerRight: number
|
|
longInnerRight: number
|
|
latRight: number
|
|
longRight: number
|
|
revSign: 'ASC' | 'DESC'
|
|
}
|
|
|
|
/**
|
|
* Трасса полёта КА (flight-line) на интервале для отрисовки на карте/таймлайне.
|
|
* @param noradId NORAD-идентификатор КА.
|
|
* @param fromMs Начало интервала, мс от эпохи.
|
|
* @param toMs Конец интервала, мс от эпохи.
|
|
*
|
|
* @returns Точки трассы полёта на интервале.
|
|
*/
|
|
export async function fetchFlightLine(
|
|
noradId: string,
|
|
fromMs: number,
|
|
toMs: number,
|
|
): Promise<FlightLinePoint[]> {
|
|
const query = new URLSearchParams({
|
|
time_start: toLocalIso(fromMs),
|
|
time_stop: toLocalIso(toMs),
|
|
})
|
|
return fetchJson<FlightLinePoint[]>(
|
|
`/api/tgu-planning/spacecraft/${encodeURIComponent(noradId)}/flight-line?${query}`,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Список платформ (КА) ТГУ.
|
|
* @returns Платформы планирования ТГУ.
|
|
*/
|
|
export async function fetchPlatforms(): Promise<TguPlatform[]> {
|
|
return fetchJson<TguPlatform[]>('/api/tgu-planning/platforms')
|
|
}
|
|
|
|
/**
|
|
* Планы ТГУ с заданной глубиной истории, опционально по одному КА.
|
|
* @param historyDays Глубина истории в сутках.
|
|
* @param spacecraftId Идентификатор КА для фильтра (необязателен — иначе по всем КА).
|
|
*
|
|
* @returns Планы ТГУ.
|
|
*/
|
|
export async function fetchPlans(historyDays: number, spacecraftId?: string): Promise<TguPlan[]> {
|
|
const query = new URLSearchParams({ historyDays: String(historyDays) })
|
|
const path = spacecraftId
|
|
? `/api/tgu-planning/spacecraft/${encodeURIComponent(spacecraftId)}/plans?${query}`
|
|
: `/api/tgu-planning/plans?${query}`
|
|
|
|
return fetchJson<TguPlan[]>(path)
|
|
}
|
|
|
|
/**
|
|
* Отправляет решение оператора по плану (принять/отклонить и т.п.).
|
|
* @param planId Идентификатор плана.
|
|
* @param decision Решение по плану.
|
|
* @param reason Причина решения (необязательна).
|
|
*
|
|
* @returns Сообщение-результат обработки решения.
|
|
*/
|
|
export async function sendPlanDecision(
|
|
planId: string,
|
|
decision: TguPlanDecision,
|
|
reason?: string,
|
|
): Promise<TguPlanDecisionMessage> {
|
|
const query = new URLSearchParams({ decision })
|
|
if (reason?.trim()) {
|
|
query.set('reason', reason.trim())
|
|
}
|
|
|
|
return fetchJson<TguPlanDecisionMessage>(
|
|
`/api/tgu-planning/plans/${encodeURIComponent(planId)}/decision?${query}`,
|
|
{ method: 'POST' },
|
|
true,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* «Снять с учёта» терминально-негативный план: он перестаёт считаться проблемой и красить
|
|
* индикатор КА. Статус и цепочку не трогает; обратимо ([restorePlan]). 422 — статус не снимаемый.
|
|
* @param planId Идентификатор плана.
|
|
*/
|
|
export async function dismissPlan(planId: string): Promise<void> {
|
|
await fetchJson<void>(`/api/tgu-planning/plans/${encodeURIComponent(planId)}/dismiss`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
/**
|
|
* «Вернуть на учёт» ранее снятый план.
|
|
* @param planId Идентификатор плана.
|
|
*/
|
|
export async function restorePlan(planId: string): Promise<void> {
|
|
await fetchJson<void>(`/api/tgu-planning/plans/${encodeURIComponent(planId)}/restore`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
/**
|
|
* ТЕСТ: подтверждает закладку плана (AWAITING_LAYIN → LAID_IN) через тест-контроллер ТГУ.
|
|
* decisionRequest=false — пробрасываем реальный текст ошибки бэка (напр. гард «закладка ±10 мин»).
|
|
* @param planId Идентификатор плана.
|
|
*/
|
|
export async function confirmPlanLayin(planId: string): Promise<void> {
|
|
await fetchJson<void>(`/api/tgu-planning/plans/${encodeURIComponent(planId)}/confirm-layin`, {
|
|
method: 'POST',
|
|
})
|
|
}
|
|
|
|
async function fetchJson<T>(url: string, init?: RequestInit, decisionRequest = false): Promise<T> {
|
|
let response: Response
|
|
try {
|
|
response = await apiFetch(url, {
|
|
headers: { Accept: 'application/json' },
|
|
...init,
|
|
})
|
|
} catch {
|
|
throw new TguApiError('Не удалось подключиться к backend API.', 'network')
|
|
}
|
|
|
|
const text = await response.text()
|
|
if (!response.ok) {
|
|
throw new TguApiError(
|
|
errorMessage(response.status, text, decisionRequest),
|
|
'http',
|
|
response.status,
|
|
)
|
|
}
|
|
|
|
if (!text.trim()) {
|
|
return undefined as T
|
|
}
|
|
|
|
return JSON.parse(text) as T
|
|
}
|
|
|
|
function errorMessage(status: number, body: string, decisionRequest: boolean): string {
|
|
if (decisionRequest && status === 404) {
|
|
return 'План не найден.'
|
|
}
|
|
if (decisionRequest && status === 409) {
|
|
return 'План не является активным планом, ожидающим решения, или для него нет active attempt.'
|
|
}
|
|
if (decisionRequest && status >= 500) {
|
|
return 'Ошибка сервиса при отправке решения.'
|
|
}
|
|
|
|
return extractErrorText(body) || 'Ошибка backend API.'
|
|
}
|
|
|
|
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 ['reason', 'message', 'error', 'detail']) {
|
|
const value = parsed[field]
|
|
if (typeof value === 'string' && value.trim()) {
|
|
return value
|
|
}
|
|
}
|
|
} catch {
|
|
return trimmed
|
|
}
|
|
|
|
return trimmed
|
|
}
|