/** * Network requests: duties and calendar events. */ import { FETCH_TIMEOUT_MS } from "./constants.js"; import { getInitData } from "./auth.js"; import { state } from "./dom.js"; import { t } from "./i18n.js"; /** * Build fetch options with init data header, Accept-Language and timeout abort. * @param {string} initData - Telegram init data * @returns {{ headers: object, signal: AbortSignal, timeoutId: number }} */ export function buildFetchOptions(initData) { const headers = {}; if (initData) headers["X-Telegram-Init-Data"] = initData; headers["Accept-Language"] = state.lang || "ru"; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); return { headers, signal: controller.signal, timeoutId }; } /** * GET request to API with init data, Accept-Language, timeout. * Caller checks res.ok, res.status, res.json(). * @param {string} path - e.g. "/api/duties" * @param {{ from?: string, to?: string }} params - query params * @returns {Promise} - raw response */ export async function apiGet(path, params = {}) { const base = window.location.origin; const query = new URLSearchParams(params).toString(); const url = query ? `${base}${path}?${query}` : `${base}${path}`; const initData = getInitData(); const opts = buildFetchOptions(initData); try { const res = await fetch(url, { headers: opts.headers, signal: opts.signal }); return res; } finally { clearTimeout(opts.timeoutId); } } /** * Fetch duties for date range. Throws ACCESS_DENIED error on 403. * @param {string} from - YYYY-MM-DD * @param {string} to - YYYY-MM-DD * @returns {Promise} */ export async function fetchDuties(from, to) { try { const res = await apiGet("/api/duties", { from, to }); if (res.status === 403) { let detail = t(state.lang, "access_denied"); try { const body = await res.json(); if (body && body.detail !== undefined) { detail = typeof body.detail === "string" ? body.detail : (body.detail.msg || JSON.stringify(body.detail)); } } catch (parseErr) { /* ignore */ } const err = new Error("ACCESS_DENIED"); err.serverDetail = detail; throw err; } if (!res.ok) throw new Error(t(state.lang, "error_load_failed")); return res.json(); } catch (e) { if (e.name === "AbortError") { throw new Error(t(state.lang, "error_network")); } throw e; } } /** * Fetch calendar events for range. Returns [] on non-200 or error. Does not throw for 403. * @param {string} from - YYYY-MM-DD * @param {string} to - YYYY-MM-DD * @returns {Promise} */ export async function fetchCalendarEvents(from, to) { try { const res = await apiGet("/api/calendar-events", { from, to }); if (!res.ok) return []; return res.json(); } catch (e) { return []; } }