// static/js/admin.Tests.jsx function AdminTests({ token }) { const [tests, setTests] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [selectedTestId, setSelectedTestId] = useState(null); const [isEditing, setIsEditing] = useState(false); // Для модалки с примером JSON const [showJsonExample, setShowJsonExample] = useState(false); const [exampleJson, setExampleJson] = useState(""); const makeDefaultTest = () => ({ title: "", description: "", start_date: new Date().toISOString(), end_date: new Date(Date.now() + 86400000 * 30).toISOString(), time_limit_minutes: 30, attempts_allowed: 1, passing_score_percent: 80, shuffle_questions: true, is_active: true, }); const makeEmptyQuestion = (id) => ({ id, text: "", q_type: "single", // single, multi score: 1, options: [ { id: 1, text: "", is_correct: true }, { id: 2, text: "", is_correct: false }, ], }); const [test, setTest] = useState(makeDefaultTest()); const [questions, setQuestions] = useState([makeEmptyQuestion(1)]); const loadTests = async () => { setLoading(true); try { const data = await API.adminGetTests(token); setTests(data); } catch (e) { console.error(e); alert(e.message || "Ошибка загрузки списка тестов"); } finally { setLoading(false); } }; useEffect(() => { loadTests(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const resetForm = () => { setTest(makeDefaultTest()); setQuestions([makeEmptyQuestion(1)]); setSelectedTestId(null); setIsEditing(false); }; const addQuestion = () => { setQuestions(prev => { const nextId = prev.length > 0 ? Math.max(...prev.map(q => q.id)) + 1 : 1; return [...prev, makeEmptyQuestion(nextId)]; }); }; const removeQuestion = (id) => { setQuestions(prev => prev.filter(q => q.id !== id)); }; const moveQuestionUp = (id) => { setQuestions(prev => { const idx = prev.findIndex(q => q.id === id); if (idx <= 0) return prev; const copy = [...prev]; const tmp = copy[idx - 1]; copy[idx - 1] = copy[idx]; copy[idx] = tmp; return copy; }); }; const moveQuestionDown = (id) => { setQuestions(prev => { const idx = prev.findIndex(q => q.id === id); if (idx === -1 || idx === prev.length - 1) return prev; const copy = [...prev]; const tmp = copy[idx + 1]; copy[idx + 1] = copy[idx]; copy[idx] = tmp; return copy; }); }; const updateQuestionField = (id, field, value) => { setQuestions(prev => prev.map(q => q.id === id ? { ...q, [field]: value } : q) ); }; const addOption = (questionId) => { setQuestions(prev => prev.map(q => { if (q.id !== questionId) return q; const nextOptId = q.options.length > 0 ? Math.max(...q.options.map(o => o.id)) + 1 : 1; return { ...q, options: [...q.options, { id: nextOptId, text: "", is_correct: false }] }; }) ); }; const removeOption = (questionId, optId) => { setQuestions(prev => prev.map(q => { if (q.id !== questionId) return q; return { ...q, options: q.options.filter(o => o.id !== optId) }; }) ); }; const updateOptionField = (questionId, optId, field, value) => { setQuestions(prev => prev.map(q => { if (q.id !== questionId) return q; return { ...q, options: q.options.map(o => o.id === optId ? { ...o, [field]: value } : o) }; }) ); }; const setCorrectSingle = (questionId, optId) => { setQuestions(prev => prev.map(q => { if (q.id !== questionId) return q; return { ...q, options: q.options.map(o => ({ ...o, is_correct: o.id === optId })) }; }) ); }; const toggleCorrectMulti = (questionId, optId) => { setQuestions(prev => prev.map(q => { if (q.id !== questionId) return q; return { ...q, options: q.options.map(o => o.id === optId ? { ...o, is_correct: !o.is_correct } : o) }; }) ); }; // Загрузка теста в форму для редактирования const loadTestForEdit = async (id) => { try { const data = await API.adminGetTestById(token, id); setSelectedTestId(id); setIsEditing(true); setTest({ title: data.title, description: data.description, start_date: data.start_date, end_date: data.end_date, time_limit_minutes: data.time_limit_minutes, attempts_allowed: data.attempts_allowed, passing_score_percent: data.passing_score_percent, shuffle_questions: data.shuffle_questions, is_active: data.is_active, }); const mappedQuestions = (data.questions || []).map((q, idx) => ({ id: idx + 1, text: q.text, q_type: q.q_type, score: q.score, options: (q.options || []).map((o, oIdx) => ({ id: oIdx + 1, text: o.text, is_correct: o.is_correct, })) })); setQuestions(mappedQuestions.length ? mappedQuestions : [makeEmptyQuestion(1)]); } catch (e) { console.error(e); alert(e.message || "Ошибка загрузки теста"); } }; const deleteTest = async (id) => { if (!confirm("Удалить этот тест со всеми вопросами и результатами?")) return; try { await API.adminDeleteTest(token, id); if (selectedTestId === id) { resetForm(); } await loadTests(); } catch (e) { console.error(e); alert(e.message || "Ошибка удаления теста"); } }; // Переключение активности теста const toggleActive = async (t) => { try { await API.adminUpdateTestStatus(token, t.id, !t.is_active); await loadTests(); } catch (e) { console.error(e); alert(e.message || "Ошибка обновления статуса теста"); } }; const saveTest = async () => { if (!test.title.trim()) { alert("Введите название теста"); return; } if (questions.length === 0) { alert("Добавьте хотя бы один вопрос"); return; } const payload = { ...test, questions: questions.map((q) => ({ text: q.text, q_type: q.q_type, score: Number(q.score) || 1, options: (q.q_type === "single" || q.q_type === "multi") ? q.options.map(o => ({ text: o.text, is_correct: !!o.is_correct })) : [] })) }; setSaving(true); try { if (selectedTestId) { await API.adminUpdateTest(token, selectedTestId, payload); alert("Тест обновлён"); } else { await API.adminCreateTest(token, payload); alert("Тест сохранён"); } resetForm(); await loadTests(); } catch (e) { console.error(e); alert(e.message || "Ошибка сохранения теста"); } finally { setSaving(false); } }; // ---------- Импорт JSON и подсказка-пример ---------- // убираем все поля-Комментарии (ключи, начинающиеся с "_") const stripComments = (obj) => { if (Array.isArray(obj)) { return obj.map(stripComments); } if (obj && typeof obj === "object") { const res = {}; Object.entries(obj).forEach(([key, value]) => { if (key.startsWith("_")) return; res[key] = stripComments(value); }); return res; } return obj; }; const applyImportedTest = (data) => { if (!data || typeof data !== "object") { alert("Неверная структура JSON"); return; } setTest(prev => ({ ...prev, title: data.title || "", description: data.description || "", start_date: data.start_date || prev.start_date, end_date: data.end_date || prev.end_date, time_limit_minutes: typeof data.time_limit_minutes === "number" ? data.time_limit_minutes : prev.time_limit_minutes, attempts_allowed: typeof data.attempts_allowed === "number" ? data.attempts_allowed : prev.attempts_allowed, passing_score_percent: typeof data.passing_score_percent === "number" ? data.passing_score_percent : prev.passing_score_percent, shuffle_questions: typeof data.shuffle_questions === "boolean" ? data.shuffle_questions : prev.shuffle_questions, is_active: typeof data.is_active === "boolean" ? data.is_active : prev.is_active, })); const importedQuestions = Array.isArray(data.questions) ? data.questions : []; const mappedQuestions = importedQuestions.map((q, idx) => ({ id: idx + 1, text: q.text || "", q_type: q.q_type || "single", score: typeof q.score === "number" ? q.score : 1, options: (Array.isArray(q.options) ? q.options : []).map((o, oIdx) => ({ id: oIdx + 1, text: o.text || "", is_correct: !!o.is_correct, })), })); if (mappedQuestions.length === 0) { alert("В JSON не найдено ни одного вопроса. Будет создан один пустой вопрос."); setQuestions([makeEmptyQuestion(1)]); } else { setQuestions(mappedQuestions); } setSelectedTestId(null); setIsEditing(false); }; const handleJsonFileChange = (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const text = ev.target.result; const raw = JSON.parse(text); const clean = stripComments(raw); applyImportedTest(clean); alert("JSON загружен. Проверьте данные и нажмите «Сохранить тест»."); } catch (err) { console.error(err); alert("Ошибка чтения JSON: " + err.message); } finally { e.target.value = ""; } }; reader.readAsText(file, "utf-8"); }; const openJsonExample = () => { if (exampleJson) { setShowJsonExample(true); return; } fetch("sample_test.json") .then(res => res.text()) .then(text => { setExampleJson(text); setShowJsonExample(true); }) .catch(err => { console.error(err); setExampleJson("// Не удалось загрузить sample_test.json. Проверьте, что файл лежит рядом с index.html."); setShowJsonExample(true); }); }; const copyExampleJson = () => { if (!exampleJson) return; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(exampleJson) .then(() => alert("JSON-шаблон скопирован в буфер обмена")) .catch(() => alert("Не удалось скопировать автоматически. Скопируйте текст вручную.")); } else { alert("Браузер не поддерживает автоматическое копирование. Скопируйте текст вручную."); } }; return (
{/* Список тестов слева */}

Список тестов

{loading ? (
Загрузка...
) : tests.length === 0 ? (
Тестов пока нет
) : (
    {tests.map(t => (
  • {t.is_active ? 'Активен' : 'Неактивен'}
    Время: {t.time_limit_minutes} мин · Попыток: {t.attempts_allowed || '∞'}
  • ))}
)}
{/* Конструктор теста справа */}

{isEditing ? `Редактирование теста #${selectedTestId}` : 'Создание теста'}

{/* Импорт JSON */} {/* Пример JSON */}
{/* Основные настройки теста */}
setTest({ ...test, title: e.target.value })} />