diff --git a/frontend/src/pages/TestDetail.jsx b/frontend/src/pages/TestDetail.jsx
index b6c0a67..27036c2 100644
--- a/frontend/src/pages/TestDetail.jsx
+++ b/frontend/src/pages/TestDetail.jsx
@@ -34,13 +34,18 @@ function createEmptyQuestion() {
}
/**
- * @param {{ title: string, defaultOpen?: boolean, id?: string, children: import('react').ReactNode }} p
+ * @param {{ title: string, subtitle?: string, defaultOpen?: boolean, id?: string, children: import('react').ReactNode }} p
*/
-function AccSection({ title, defaultOpen, id, children }) {
+function AccSection({ title, subtitle, defaultOpen, id, children }) {
return (
- {title}
+
+
+ {title}
+ {subtitle ? {subtitle} : null}
+
+
{children}
@@ -235,6 +240,16 @@ export default function TestDetail() {
});
}
+ function selectAllVisible() {
+ setAssignSelected((prev) => {
+ const next = new Set(prev);
+ for (const p of assignPeople) {
+ next.add(assignPersonKey(p));
+ }
+ return next;
+ });
+ }
+
async function postAssign() {
const selectedRows = assignPeople.filter((p) =>
assignSelected.has(assignPersonKey(p))
@@ -657,14 +672,13 @@ export default function TestDetail() {
При сохранении будет создана новая версия теста.
)}
- {data && attemptsErr && (
-
- {attemptsErr}
-
- )}
-
+
-
+
-
+
(
-
-
- {versions.map((r) => (
- -
-
-
-
-
- v{r.version}
-
- {r.is_active && (
-
- текущая
+
+
+
Версии
+
+ {versions.map((r) => (
+ -
+
+
+
+
+ v{r.version}
- )}
-
-
- {fmtDt(r.created_at)}
-
-
- Активна: {r.is_active ? 'да' : 'нет'}
-
-
- {!r.is_active && (
-
activateVersion(r.id)}
- >
- Сделать активной
-
- )}
-
-
- ))}
-
-
-
- {attemptsList != null && attemptsList.length > 0 && (
-
- {attemptsErr && (
-
- {attemptsErr}
-
- )}
-
- {attemptsList.map((a) => {
- const when = a.completedAt
- ? fmtDt(a.completedAt)
- : a.startedAt
- ? fmtDt(a.startedAt)
- : '—';
- const result =
- a.status === 'completed' && a.totalQuestions != null ? (
- <>
- {a.correctCount}/{a.totalQuestions}
- {a.passed ? ' · зачёт' : ' · незачёт'}
- >
- ) : (
- a.status
- );
- return (
- -
-
-
-
- {when}
-
-
- {a.attempterName || '—'}
- {a.attempterLogin && (
-
- {a.attempterLogin}
-
- )}
-
-
- v{a.testVersion} ·{' '}
- {result}
-
+ {r.is_active && (
+
+ текущая
+
+ )}
- {a.status === 'completed' && (
-
- Разбор
-
- )}
+
+ {fmtDt(r.created_at)}
+
+
+ Активна: {r.is_active ? 'да' : 'нет'}
+
-
- );
- })}
+ {!r.is_active && (
+ activateVersion(r.id)}
+ >
+ Сделать активной
+
+ )}
+
+
+ ))}
-
- )}
-
-
-
- {test?.chainActive !== false ? (
- setChainVisible(false)}
- >
- Скрыть из списка
-
- ) : (
- setChainVisible(true)}
- >
- Снова показать в списке
-
+
+ {attemptsList !== undefined && (
+
+
Прохождения
+ {attemptsList == null && (
+
+ {attemptsErr || 'Список прогонов сейчас недоступен.'}
+
+ )}
+ {attemptsList && attemptsList.length > 0 && (
+
+ {attemptsList.map((a) => {
+ const when = a.completedAt
+ ? fmtDt(a.completedAt)
+ : a.startedAt
+ ? fmtDt(a.startedAt)
+ : '—';
+ const result =
+ a.status === 'completed' && a.totalQuestions != null ? (
+ <>
+ {a.correctCount}/{a.totalQuestions}
+ {a.passed ? ' · зачёт' : ' · незачёт'}
+ >
+ ) : (
+ a.status
+ );
+ return (
+ -
+
+
+
+ {when}
+
+
+ {a.attempterName || '—'}
+ {a.attempterLogin && (
+
+ {a.attempterLogin}
+
+ )}
+
+
+ v{a.testVersion} ·{' '}
+ {result}
+
+
+ {a.status === 'completed' && (
+
+ Разбор
+
+ )}
+
+
+ );
+ })}
+
+ )}
+ {attemptsList && attemptsList.length === 0 && (
+
+ Пока нет зарегистрированных прогонов.
+
+ )}
+
)}
-
-
-
-
-
- {importBusy ? 'Обработка…' : 'Выбрать файл'}
-
-
- {importErr && (
-
- {importErr}
-
- )}
- {importPreview && (
-
-
- {importPreview.generation?.message}
+
+
+
Видимость
+
+ Скрытые тесты в общем списке не показываются; ссылку на тест по-прежнему можно открыть.
- {importPreview.generation?.textPreview && !importPreview.generation?.available && (
-
- {importPreview.generation.textPreview}
- {importPreview.textLength > 4000 ? '…' : ''}
-
- )}
- {importPreview.generation?.draft && (
-
+
+ {test?.chainActive !== false ? (
setChainVisible(false)}
>
- Применить сгенерированный черновик
+ Скрыть из списка
-
- )}
- {importPreview.extractedText && (
-
+ ) : (
setChainVisible(true)}
>
- Вставить в первый вопрос (до 2000 симв.)
+ Снова показать в списке
-
- )}
+ )}
+
- )}
-
-
- {assignmentUi && data && (
-
- {assignErr && (
-
- {assignErr}
+ {assignmentUi && data && (
+
+
Кому выдать
+
+ Список с учётом поиска и фильтров; можно отметить всех на экране.
- )}
-
- setAssignSearch(e.target.value)}
- aria-label="Поиск сотрудника"
- />
-
-
- {assignLoadBusy && Загрузка…}
-
- {assignPeople.length > 0 ? (
-
- {assignPeople.map((p) => {
- const k = assignPersonKey(p);
- const picked = assignSelected.has(k);
- return (
-
- toggleAssignPerson(p)}
- />
-
- {p.fio}
- {p.webLogin && (
- {p.webLogin}
- )}
-
- {p.departments || '—'}
- {p.clinicUserId
- ? ' · есть учётка в модуле'
- : ' · нет учётки (создадим при назначении)'}
+ {assignErr && (
+
+ {assignErr}
+
+ )}
+
+ setAssignSearch(e.target.value)}
+ aria-label="Поиск сотрудника"
+ />
+
+
+ {assignLoadBusy && Загрузка…}
+
+ {assignPeople.length > 0 && (
+
+
+ Выбрать всех ({assignPeople.length})
+
+
+ )}
+ {assignPeople.length > 0 ? (
+
+ {assignPeople.map((p) => {
+ const k = assignPersonKey(p);
+ const picked = assignSelected.has(k);
+ return (
+
+ toggleAssignPerson(p)}
+ />
+
+ {p.fio}
+ {p.webLogin && {p.webLogin}}
+
+ {p.departments || '—'}
+ {p.clinicUserId
+ ? ' · есть учётка в модуле'
+ : ' · нет учётки (создадим при назначении)'}
+
-
-
- );
- })}
+
+ );
+ })}
+
+ ) : (
+ !assignLoadBusy && (
+
+ Нет подходящих записей. Смените фильтры или поиск.
+
+ )
+ )}
+
+
+ Назначить выбранных
+ {assignSelectedInList.length > 0
+ ? ` (${assignSelectedInList.length})`
+ : ''}
+
- ) : (
- !assignLoadBusy && (
-
- Нет подходящих записей. Смените фильтры или поиск.
-
- )
- )}
-
-
- Назначить выбранных
- {assignSelectedInList.length > 0
- ? ` (${assignSelectedInList.length})`
- : ''}
-
+ {assignMsg &&
{assignMsg}
}
- {assignMsg && {assignMsg}
}
-
- )}
+ )}
+