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 { const query = new URLSearchParams({ time_start: toLocalIso(fromMs), time_stop: toLocalIso(toMs), }) return fetchJson( `/api/tgu-planning/spacecraft/${encodeURIComponent(noradId)}/flight-line?${query}`, ) } /** * Список платформ (КА) ТГУ. * @returns Платформы планирования ТГУ. */ export async function fetchPlatforms(): Promise { return fetchJson('/api/tgu-planning/platforms') } /** * Планы ТГУ с заданной глубиной истории, опционально по одному КА. * @param historyDays Глубина истории в сутках. * @param spacecraftId Идентификатор КА для фильтра (необязателен — иначе по всем КА). * * @returns Планы ТГУ. */ export async function fetchPlans(historyDays: number, spacecraftId?: string): Promise { 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(path) } /** * Отправляет решение оператора по плану (принять/отклонить и т.п.). * @param planId Идентификатор плана. * @param decision Решение по плану. * @param reason Причина решения (необязательна). * * @returns Сообщение-результат обработки решения. */ export async function sendPlanDecision( planId: string, decision: TguPlanDecision, reason?: string, ): Promise { const query = new URLSearchParams({ decision }) if (reason?.trim()) { query.set('reason', reason.trim()) } return fetchJson( `/api/tgu-planning/plans/${encodeURIComponent(planId)}/decision?${query}`, { method: 'POST' }, true, ) } /** * «Снять с учёта» терминально-негативный план: он перестаёт считаться проблемой и красить * индикатор КА. Статус и цепочку не трогает; обратимо ([restorePlan]). 422 — статус не снимаемый. * @param planId Идентификатор плана. */ export async function dismissPlan(planId: string): Promise { await fetchJson(`/api/tgu-planning/plans/${encodeURIComponent(planId)}/dismiss`, { method: 'POST', }) } /** * «Вернуть на учёт» ранее снятый план. * @param planId Идентификатор плана. */ export async function restorePlan(planId: string): Promise { await fetchJson(`/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 { await fetchJson(`/api/tgu-planning/plans/${encodeURIComponent(planId)}/confirm-layin`, { method: 'POST', }) } async function fetchJson(url: string, init?: RequestInit, decisionRequest = false): Promise { 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 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 }