import type { BookedSlotDTO, BookedSlotView, RequestSlotsParams, SlotBookingRequest, SlotDTO, } from '../model/slotTypes' import { apiFetch } from './apiFetch' // slots-service через gateway: /api/pcp-slots/** → StripPrefix=2 → slots-service /api/slots/** const BASE = '/api/pcp-slots/api/slots' async function readErrorMessage(response: Response, fallback: string): Promise { try { const text = await response.text() if (!text) return `${fallback}: ${response.status}` try { const body = JSON.parse(text) as { error?: string; message?: string } return body.error || body.message || text } catch { return text } } catch { return `${fallback}: ${response.status}` } } /** * Рассчитывает слоты покрытия заявки (slots-service: GET /api/slots/request-cover). * @param params Заявка, интервал и фильтры (ветвь, признак покрытия, КА, угол Солнца, * стратегия автоформирования покрытия). * @param signal Сигнал отмены (AbortController) — прерывает долгий расчёт. * * @returns Список слотов-кандидатов с контурами для карты. При заданной coverageStrategy — * уже отобранный достаточный набор покрытия (преднабор для брони). */ export async function fetchRequestSlots( params: RequestSlotsParams, signal?: AbortSignal, ): Promise { const query = new URLSearchParams() query.set('requestId', params.requestId) query.set('timeStart', params.timeStart) query.set('timeStop', params.timeStop) if (params.revSign) query.set('revSign', params.revSign) if (params.cov != null) query.set('cov', String(params.cov)) if (params.sun != null) query.set('sun', String(params.sun)) if (params.coverageStrategy) query.set('coverageStrategy', params.coverageStrategy) if (params.all != null) query.set('all', String(params.all)) params.satellites?.forEach((id) => query.append('satellites', String(id))) const response = await apiFetch(`${BASE}/request-cover?${query}`, { headers: { Accept: 'application/json' }, signal, }) if (!response.ok) { throw new Error(await readErrorMessage(response, 'Не удалось рассчитать слоты')) } return response.json() as Promise } /** * Бронирует выбранные слоты под заявку (slots-service: POST /api/slots/booking/request). * @param requestId Идентификатор заявки. * @param slots Список слотов (КА, номер слота, цикл). * * @returns Забронированные слоты. */ export async function bookRequestSlots( requestId: string, slots: SlotBookingRequest[], ): Promise { const response = await apiFetch(`${BASE}/booking/request`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ requestId, slots }), }) if (!response.ok) { throw new Error(await readErrorMessage(response, 'Не удалось забронировать слоты')) } return response.json() as Promise } /** * Снимает все брони заявки (slots-service: DELETE /api/slots/booking/request). * @param requestId Идентификатор заявки. * * @returns Сколько броней снято. */ export async function cancelRequestBooking(requestId: string): Promise { const response = await apiFetch( `${BASE}/booking/request?requestId=${encodeURIComponent(requestId)}`, { method: 'DELETE', headers: { Accept: 'application/json' } }, ) if (!response.ok) { throw new Error(await readErrorMessage(response, 'Не удалось снять бронь')) } const value = Number(await response.text()) return Number.isFinite(value) ? value : 0 } /** * Снимает выбранные брони заявки по списку bookedSlotId (slots-service: DELETE * /api/slots/booking/batch). Один запрос = один чанк; крупный набор чанкует вызывающий * (runChunked), как при бронировании. Список id передаётся в query (?ids=1&ids=2), а не в теле — * тело у DELETE через gateway ненадёжно. requestId обязателен: снятие request-aware, чтобы * шаренный между заявками слот не сносился у других. * @param requestId Идентификатор заявки. * @param bookedSlotIds Идентификаторы броней (bookedSlotId) одного чанка. * * @returns Сколько броней снято в этом чанке. */ export async function cancelBookedSlots( requestId: string, bookedSlotIds: number[], ): Promise { if (bookedSlotIds.length === 0) return 0 const query = new URLSearchParams() query.set('requestId', requestId) bookedSlotIds.forEach((id) => query.append('ids', String(id))) const response = await apiFetch(`${BASE}/booking/batch?${query}`, { method: 'DELETE', headers: { Accept: 'application/json' }, }) if (!response.ok) { throw new Error(await readErrorMessage(response, 'Не удалось снять бронь')) } const value = Number(await response.text()) return Number.isFinite(value) ? value : 0 } /** * Загружает уже забронированные слоты заявки (slots-service: GET /api/slots/booking/by-request/{id}). * @param requestId Идентификатор заявки. * * @returns Список забронированных слотов с контурами и bookedSlotId (для снятия по id); * cycle здесь — цикл брони, поэтому slotKey совпадает с ответом POST booking/request. */ export async function fetchBookedSlotsByRequest(requestId: string): Promise { const response = await apiFetch( `${BASE}/booking/by-request/${encodeURIComponent(requestId)}`, { headers: { Accept: 'application/json' } }, ) if (!response.ok) { throw new Error(await readErrorMessage(response, 'Не удалось загрузить брони заявки')) } return response.json() as Promise }