// Единое место для всех HTTP-запросов к бэкенду. // Порт web/api_client.py. Базовый путь /api проксируется nginx (прод) или Vite (dev) на api:8000. const BASE = "/api"; // --------------------------------------------------------------------------- // Типы // --------------------------------------------------------------------------- export interface Me { id: string; first_name?: string; last_name?: string; group_id?: string | null; organization_id?: string | null; } export interface Organization { id: string; name_organization: string; group_id?: string | null; } export interface Group { id: string; name_group: string; } export interface Choice { id: string; text: string; } export interface Question { id: string; text: string; choices: Choice[]; } export interface Poll { id: string; title: string; description?: string | null; questions?: Question[]; } export interface Answer { question_id: string; choice_id: string; text?: string | null; } export interface ResponseDetail { id: string; poll_id: string; submitted_at: string; answers: Answer[]; } export interface UserResponse { id: string; poll_id: string; submitted_at: string; } export interface RadarResult { image_url?: string | null; [k: string]: unknown; } export type Result = { data: T | null; error: string | null }; // --------------------------------------------------------------------------- // Низкоуровневые помощники // --------------------------------------------------------------------------- function authHeaders(token?: string | null): Record { return token ? { Authorization: `Bearer ${token}` } : {}; } async function safeJson(resp: Response): Promise { try { return await resp.json(); } catch { return null; } } // GET, возвращает распарсенный JSON или null при любой ошибке (как в api_client.py) async function getJson(path: string, token?: string | null): Promise { try { const resp = await fetch(BASE + path, { headers: authHeaders(token) }); if (resp.ok) return (await resp.json()) as T; } catch { /* network error -> null */ } return null; } // --------------------------------------------------------------------------- // Auth // --------------------------------------------------------------------------- export async function login(username: string, password: string): Promise> { try { const body = new URLSearchParams({ username, password }); const resp = await fetch(BASE + "/auth/login", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }); if (resp.ok) { const json = await resp.json(); return { data: json.access_token, error: null }; } if (resp.status === 401) return { data: null, error: "Неверный логин или пароль" }; return { data: null, error: `Ошибка сервера: ${resp.status}` }; } catch { return { data: null, error: "Не удалось подключиться к серверу" }; } } export async function logout(token: string): Promise { try { await fetch(BASE + "/auth/logout", { method: "POST", headers: authHeaders(token) }); } catch { /* локальный logout произойдёт в любом случае */ } } export async function getMe(token: string): Promise { return getJson("/users/me", token); } const FIELD_NAMES: Record = { password: "Пароль", username: "Логин" }; const MSG_MAP: Record = { "Password must be at least 8 characters": "Пароль должен быть не менее 8 символов", "Username must be at least 3 characters": "Логин должен быть не менее 3 символов", }; export interface RegisterPayload { username: string; password: string; first_name: string; last_name: string; group_id?: string | null; organization_id?: string | null; } // Возвращает строку с ошибкой или null при успехе (как register в api_client.py) export async function register(p: RegisterPayload): Promise { const payload: Record = { username: p.username, password: p.password, first_name: p.first_name, last_name: p.last_name, }; if (p.group_id) payload.group_id = p.group_id; if (p.organization_id) payload.organization_id = p.organization_id; try { const resp = await fetch(BASE + "/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (resp.status === 201) return null; const json = await safeJson(resp); if (resp.status === 400) return json?.detail ?? "Ошибка регистрации"; if (resp.status === 422) { const errors: any[] = json?.detail ?? []; for (const err of errors) { const raw = err?.ctx?.error || err?.msg || ""; if (raw in MSG_MAP) return MSG_MAP[raw]; const loc: any[] = err?.loc ?? []; const field = FIELD_NAMES[loc[loc.length - 1]] ?? ""; const msg = (err?.msg ?? "Ошибка валидации").replace(/^Value error, /, ""); return `${field}: ${msg}`.replace(/^: |:?\s*$/g, "").trim() || "Проверьте правильность введённых данных"; } return "Проверьте правильность введённых данных"; } return `Ошибка сервера: ${resp.status}`; } catch { return "Не удалось подключиться к серверу"; } } // --------------------------------------------------------------------------- // Organizations & Groups // --------------------------------------------------------------------------- export async function getOrganizations(): Promise { return (await getJson("/organizations/")) ?? []; } export async function getGroups(): Promise { return (await getJson("/groups/")) ?? []; } // --------------------------------------------------------------------------- // Polls // --------------------------------------------------------------------------- export async function getPolls(): Promise { return (await getJson("/polls/")) ?? []; } export async function getPoll(pollId: string): Promise { return getJson(`/polls/${pollId}`); } export async function getResponse(responseId: string): Promise { return getJson(`/polls/responses/${responseId}`); } export async function getRadarResult(responseId: string): Promise { return getJson(`/responses/${responseId}/radar`); } // URL SVG-диаграммы — браузер тянет напрямую с того же origin (через nginx-прокси). export function radarImageUrl(responseId: string): string { return `${BASE}/responses/${responseId}/radar/image`; } export interface SubmitPayload { user_id?: string | null; group_id?: string | null; organization_id?: string | null; } export async function submitResponse( pollId: string, answers: Answer[], extra: SubmitPayload = {}, ): Promise> { const payload: Record = { answers }; if (extra.user_id) payload.user_id = extra.user_id; if (extra.group_id) payload.group_id = extra.group_id; if (extra.organization_id) payload.organization_id = extra.organization_id; try { const resp = await fetch(BASE + `/polls/${pollId}/responses`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (resp.status === 201) { const json = await resp.json(); return { data: json.id, error: null }; } return { data: null, error: `Ошибка сервера: ${resp.status}` }; } catch { return { data: null, error: "Не удалось подключиться к серверу" }; } } export async function getUserResponses(userId: string): Promise { return (await getJson(`/polls/users/${userId}/responses`)) ?? []; } export interface UpdateUserPayload { first_name?: string | null; last_name?: string | null; group_id?: string | null; organization_id?: string | null; } export async function updateUser( userId: string, token: string, payload: UpdateUserPayload, ): Promise> { const body: Record = {}; if (payload.first_name !== undefined) body.first_name = payload.first_name; if (payload.last_name !== undefined) body.last_name = payload.last_name; if (payload.group_id !== undefined) body.group_id = payload.group_id; if (payload.organization_id !== undefined) body.organization_id = payload.organization_id; try { const resp = await fetch(BASE + `/users/${userId}`, { method: "PUT", headers: { "Content-Type": "application/json", ...authHeaders(token) }, body: JSON.stringify(body), }); if (resp.ok) return { data: await resp.json(), error: null }; return { data: null, error: `Ошибка сервера: ${resp.status}` }; } catch { return { data: null, error: "Не удалось подключиться к серверу" }; } }