// static/js/globals.jsx // Достаём хуки из React один раз и делаем их глобальными const { useState, useEffect, createContext, useContext } = React; // Базовый URL API — тот же домен, с которого открыт фронт const API_URL = window.location.origin; // Глобальный контекст авторизации const AuthContext = createContext(null); /** * Централизованный слой работы с API. * * Здесь собраны все вызовы к бэкенду, чтобы: * - не размазывать fetch по компонентам, * - проще было потом менять урлы / заголовки / авторизацию. */ const API = { // --------- АУТЕНТИФИКАЦИЯ / РЕГИСТРАЦИЯ --------- /** * Логин по email/паролю. * Возвращает access_token или кидает Error с текстом. */ async login(email, password) { const formData = new FormData(); formData.append("username", email); formData.append("password", password); const res = await fetch(`${API_URL}/token`, { method: "POST", body: formData, }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка авторизации"); } if (!res.ok) { throw new Error(data.detail || "Ошибка авторизации"); } return data.access_token; }, /** * Регистрация пользователя. * (Сейчас без invite_code — форма регистрации пока не использует коды.) */ async register({ email, password, first_name, last_name, invite_code }) { const res = await fetch(`${API_URL}/register`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ email, first_name, last_name, password, invite_code, // отправляем код приглашения в бэк }), }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка регистрации"); } if (!res.ok) { throw new Error(data.detail || "Ошибка регистрации"); } return data; }, /** * Получение текущего пользователя (/users/me). */ async getCurrentUser(token) { const res = await fetch(`${API_URL}/users/me`, { headers: { Authorization: `Bearer ${token}`, }, }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка загрузки профиля"); } if (!res.ok) { throw new Error(data.detail || "Ошибка загрузки профиля"); } return data; }, /** * Обновление профиля текущего пользователя. * Возвращает обновлённый объект пользователя. */ async updateProfile(token, { email, first_name, last_name }) { const res = await fetch(`${API_URL}/users/me`, { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ email, first_name, last_name, }), }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка сохранения профиля"); } if (!res.ok) { throw new Error(data.detail || "Ошибка сохранения профиля"); } return data; }, /** * Смена пароля текущего пользователя. */ async changePassword(token, { old_password, new_password }) { const res = await fetch(`${API_URL}/users/me/change_password`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ old_password, new_password, }), }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка смены пароля"); } if (!res.ok) { throw new Error(data.detail || "Ошибка смены пароля"); } return data; }, // --------- ПРОФИЛЬ КОМПАНИИ / ОРГАНИЗАЦИИ --------- /** * Получить профиль компании текущего пользователя. * GET /organization/me */ async getOrganization(token) { const res = await fetch(`${API_URL}/organization/me`, { headers: { Authorization: `Bearer ${token}`, }, }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка загрузки профиля компании"); } if (!res.ok) { throw new Error(data.detail || "Ошибка загрузки профиля компании"); } return data; }, /** * Обновить профиль компании. * PUT /organization/me */ async updateOrganization( token, { name, description, contact_email, contact_phone, website, logo_url } ) { const res = await fetch(`${API_URL}/organization/me`, { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ name, description, contact_email, contact_phone, website, logo_url, }), }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка сохранения профиля компании"); } if (!res.ok) { throw new Error(data.detail || "Ошибка сохранения профиля компании"); } return data; }, /** * Сгенерировать новый инвайт-код компании. * POST /organization/me/invite/regenerate */ async regenerateInviteCode(token) { const res = await fetch( `${API_URL}/organization/me/invite/regenerate`, { method: "POST", headers: { Authorization: `Bearer ${token}`, }, } ); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка генерации инвайт-кода"); } if (!res.ok) { throw new Error(data.detail || "Ошибка генерации инвайт-кода"); } return data; }, // --------- ТЕСТЫ ДЛЯ СОТРУДНИКА --------- /** * Список доступных тестов для сотрудника. */ async getAvailableTests(token) { const res = await fetch(`${API_URL}/tests/available`, { headers: { Authorization: `Bearer ${token}`, }, }); if (!res.ok) { throw new Error("Ошибка загрузки доступных тестов"); } return await res.json(); }, /** * Запуск теста для сотрудника. * POST /tests/{test_id}/start * Возвращает структуру теста + attempt_id. */ async userStartTest(token, testId) { const res = await fetch(`${API_URL}/tests/${testId}/start`, { method: "POST", headers: { Authorization: `Bearer ${token}`, }, }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка запуска теста"); } if (!res.ok) { // Попробуем вытащить detail, если он есть throw new Error(data.detail || "Ошибка запуска теста"); } return data; }, /** * Отправка ответа на один вопрос в рамках попытки. * POST /tests/submit_answer?attempt_id=... * * payload: * - question_id * - selected_options: number[] * - text_answer: string * - time_spent: number (секунды) */ async userSubmitAnswer(token, attemptId, payload) { const res = await fetch( `${API_URL}/tests/submit_answer?attempt_id=${attemptId}`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), } ); let data = null; try { data = await res.json(); } catch (e) { // Бэк раньше и так не всегда возвращал тело, поэтому просто // даём общее сообщение, если статус не ок } if (!res.ok) { throw new Error( (data && data.detail) || "Ошибка отправки ответа по тесту" ); } // Здесь обычно можно ничего не возвращать, но вдруг бэк что-то шлёт return data; }, /** * Завершение попытки теста. * POST /tests/{attempt_id}/finish * Возвращает итог результата (score, percent, passed и т.п.). */ async userFinishAttempt(token, attemptId) { const res = await fetch(`${API_URL}/tests/${attemptId}/finish`, { method: "POST", headers: { Authorization: `Bearer ${token}`, }, }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка завершения теста"); } if (!res.ok) { throw new Error(data.detail || "Ошибка завершения теста"); } return data; }, // --------- АДМИН: СОТРУДНИКИ --------- /** * Получить список сотрудников для админа. */ async adminGetUsers(token) { const res = await fetch(`${API_URL}/admin/users`, { headers: { Authorization: `Bearer ${token}`, }, }); if (!res.ok) { throw new Error("Ошибка загрузки списка сотрудников"); } const data = await res.json(); return Array.isArray(data) ? data : []; }, /** * Создать нового сотрудника. */ async adminCreateUser(token, payload) { const res = await fetch(`${API_URL}/admin/users`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка создания сотрудника"); } if (!res.ok) { throw new Error(data.detail || "Ошибка создания сотрудника"); } return data; }, /** * Обновить сотрудника по id. * В payload можно не передавать password, если он не меняется. */ async adminUpdateUser(token, id, payload) { const res = await fetch(`${API_URL}/admin/users/${id}`, { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка обновления сотрудника"); } if (!res.ok) { throw new Error(data.detail || "Ошибка обновления сотрудника"); } return data; }, /** * Удалить сотрудника по id. */ async adminDeleteUser(token, id) { const res = await fetch(`${API_URL}/admin/users/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}`, }, }); if (!res.ok) { let data = null; try { data = await res.json(); } catch (e) { // ignore } const msg = data && data.detail ? data.detail : "Ошибка удаления сотрудника"; throw new Error(msg); } return true; }, // --------- АДМИН: ТЕСТЫ --------- /** * Получить список тестов для админа. */ async adminGetTests(token) { const res = await fetch(`${API_URL}/admin/tests`, { headers: { Authorization: `Bearer ${token}`, }, }); if (!res.ok) { throw new Error("Ошибка загрузки списка тестов"); } const data = await res.json(); return Array.isArray(data) ? data : []; }, /** * Получить тест с вопросами по id. */ async adminGetTestById(token, id) { const res = await fetch(`${API_URL}/admin/tests/${id}`, { headers: { Authorization: `Bearer ${token}`, }, }); if (!res.ok) { throw new Error("Ошибка загрузки теста"); } return await res.json(); }, /** * Создать новый тест. */ async adminCreateTest(token, payload) { const res = await fetch(`${API_URL}/admin/tests`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка создания теста"); } if (!res.ok) { throw new Error(data.detail || "Ошибка создания теста"); } return data; }, /** * Обновить существующий тест. */ async adminUpdateTest(token, id, payload) { const res = await fetch(`${API_URL}/admin/tests/${id}`, { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка обновления теста"); } if (!res.ok) { throw new Error(data.detail || "Ошибка обновления теста"); } return data; }, /** * Удалить тест. */ async adminDeleteTest(token, id) { const res = await fetch(`${API_URL}/admin/tests/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}`, }, }); if (!res.ok) { let data = null; try { data = await res.json(); } catch (e) { // ignore } const msg = data && data.detail ? data.detail : "Ошибка удаления теста"; throw new Error(msg); } return true; }, /** * Обновить статус активности теста. */ async adminUpdateTestStatus(token, id, is_active) { const res = await fetch(`${API_URL}/admin/tests/${id}/status`, { method: "PATCH", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ is_active }), }); if (!res.ok) { let data = null; try { data = await res.json(); } catch (e) { // ignore } const msg = data && data.detail ? data.detail : "Ошибка обновления статуса теста"; throw new Error(msg); } return await res.json(); }, // --------- АДМИН: АНАЛИТИКА --------- /** * Получить детальную аналитику по тесту. * GET /admin/analytics/test/{test_id} */ async adminGetTestAnalytics(token, testId) { const res = await fetch(`${API_URL}/admin/analytics/test/${testId}`, { headers: { Authorization: `Bearer ${token}`, }, }); let data; try { data = await res.json(); } catch (e) { throw new Error("Ошибка при загрузке аналитики"); } if (!res.ok) { throw new Error(data.detail || "Ошибка при загрузке аналитики"); } return data; }, /** * Скачать CSV-отчёт по тесту. * GET /admin/analytics/test/{test_id}/csv * Возвращает Blob. */ async adminDownloadTestCsv(token, testId) { const res = await fetch(`${API_URL}/admin/analytics/test/${testId}/csv`, { headers: { Authorization: `Bearer ${token}`, }, }); if (!res.ok) { throw new Error("Не удалось скачать отчет"); } return await res.blob(); }, };