Files
api-copp/web-react/src/api/client.ts
jze9 eac61923f4 feat(web): React-фронтенд вместо Flet + fix restore rmtree на bind-mount
web-react/: SPA Vite+React+TS, nginx раздаёт статику и проксирует /api на api:8000 по локалке. docker-compose: web(Flet)->web-react на :80. transfer_crud.backup_restore: чистим содержимое data/radar вместо rmtree точки монтирования (EBUSY).
2026-06-02 13:56:44 +00:00

284 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Единое место для всех 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<T> = { data: T | null; error: string | null };
// ---------------------------------------------------------------------------
// Низкоуровневые помощники
// ---------------------------------------------------------------------------
function authHeaders(token?: string | null): Record<string, string> {
return token ? { Authorization: `Bearer ${token}` } : {};
}
async function safeJson(resp: Response): Promise<any> {
try {
return await resp.json();
} catch {
return null;
}
}
// GET, возвращает распарсенный JSON или null при любой ошибке (как в api_client.py)
async function getJson<T>(path: string, token?: string | null): Promise<T | null> {
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<Result<string>> {
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<void> {
try {
await fetch(BASE + "/auth/logout", { method: "POST", headers: authHeaders(token) });
} catch {
/* локальный logout произойдёт в любом случае */
}
}
export async function getMe(token: string): Promise<Me | null> {
return getJson<Me>("/users/me", token);
}
const FIELD_NAMES: Record<string, string> = { password: "Пароль", username: "Логин" };
const MSG_MAP: Record<string, string> = {
"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<string | null> {
const payload: Record<string, unknown> = {
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<Organization[]> {
return (await getJson<Organization[]>("/organizations/")) ?? [];
}
export async function getGroups(): Promise<Group[]> {
return (await getJson<Group[]>("/groups/")) ?? [];
}
// ---------------------------------------------------------------------------
// Polls
// ---------------------------------------------------------------------------
export async function getPolls(): Promise<Poll[]> {
return (await getJson<Poll[]>("/polls/")) ?? [];
}
export async function getPoll(pollId: string): Promise<Poll | null> {
return getJson<Poll>(`/polls/${pollId}`);
}
export async function getResponse(responseId: string): Promise<ResponseDetail | null> {
return getJson<ResponseDetail>(`/polls/responses/${responseId}`);
}
export async function getRadarResult(responseId: string): Promise<RadarResult | null> {
return getJson<RadarResult>(`/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<Result<string>> {
const payload: Record<string, unknown> = { 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<UserResponse[]> {
return (await getJson<UserResponse[]>(`/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<Result<Me>> {
const body: Record<string, unknown> = {};
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: "Не удалось подключиться к серверу" };
}
}