|
|
|
|
@ -622,7 +622,7 @@ export default function TestDetail() {
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div className="test-detail-page"> |
|
|
|
|
<div className="test-detail-page test-detail-page--with-fixed-actions"> |
|
|
|
|
<div className="cabinet-brick cabinet-brick--hero"> |
|
|
|
|
<p className="link-back" style={{ marginTop: 0 }}> |
|
|
|
|
<Link to="/tests">← к списку</Link> |
|
|
|
|
@ -792,22 +792,23 @@ export default function TestDetail() {
|
|
|
|
|
{draftQuestions.map((q, qi) => ( |
|
|
|
|
<div |
|
|
|
|
key={q.key} |
|
|
|
|
className="question-editor-block" |
|
|
|
|
style={{ |
|
|
|
|
borderTop: '1px solid var(--border-subtle, #2a2a2a)', |
|
|
|
|
marginTop: '1rem', |
|
|
|
|
paddingTop: '1rem', |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<div |
|
|
|
|
className="inline-actions" |
|
|
|
|
style={{ marginBottom: 6, flexWrap: 'wrap', alignItems: 'center' }} |
|
|
|
|
> |
|
|
|
|
<label className="form-label" htmlFor={`qtext-${q.key}`} style={{ marginBottom: 0 }}> |
|
|
|
|
<div className="question-editor-block__header"> |
|
|
|
|
<label |
|
|
|
|
className="form-label question-editor-block__title" |
|
|
|
|
htmlFor={`qtext-${q.key}`} |
|
|
|
|
> |
|
|
|
|
Вопрос {qi + 1} |
|
|
|
|
</label> |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
className="btn btn-ghost btn--sm" |
|
|
|
|
className="btn btn-ghost btn--sm question-editor-block__ai-btn" |
|
|
|
|
disabled={aiQBusy != null} |
|
|
|
|
onClick={() => runAiGenerateQuestion(qi)} |
|
|
|
|
> |
|
|
|
|
@ -845,14 +846,18 @@ export default function TestDetail() {
|
|
|
|
|
<p className="muted" style={{ marginBottom: 6, fontSize: 13 }}> |
|
|
|
|
Варианты |
|
|
|
|
</p> |
|
|
|
|
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}> |
|
|
|
|
<ul className="question-options-list" style={{ listStyle: 'none', padding: 0, margin: 0 }}> |
|
|
|
|
{q.options.map((o, oi) => ( |
|
|
|
|
<li |
|
|
|
|
key={o.key} |
|
|
|
|
className="question-option-row" |
|
|
|
|
style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 8 }} |
|
|
|
|
> |
|
|
|
|
<input |
|
|
|
|
type="checkbox" |
|
|
|
|
key={`${o.key}-${q.hasMultipleAnswers ? 'm' : 's'}`} |
|
|
|
|
className="question-option-row__mark" |
|
|
|
|
type={q.hasMultipleAnswers ? 'checkbox' : 'radio'} |
|
|
|
|
name={q.hasMultipleAnswers ? undefined : `q-${q.key}-correct`} |
|
|
|
|
checked={o.isCorrect} |
|
|
|
|
onChange={(e) => { |
|
|
|
|
const v = e.target.checked; |
|
|
|
|
@ -936,7 +941,7 @@ export default function TestDetail() {
|
|
|
|
|
</div> |
|
|
|
|
</AccSection> |
|
|
|
|
|
|
|
|
|
<div className="cabinet-brick" style={{ marginTop: 0 }}> |
|
|
|
|
<div className="cabinet-brick editor-actions-flow" style={{ marginTop: 0 }}> |
|
|
|
|
<div |
|
|
|
|
className="actions-bar" |
|
|
|
|
style={{ marginBottom: draftStatus ? '0.35rem' : 0 }} |
|
|
|
|
@ -949,7 +954,7 @@ export default function TestDetail() {
|
|
|
|
|
</Link> |
|
|
|
|
</div> |
|
|
|
|
{draftStatus && ( |
|
|
|
|
<p className="muted" style={{ marginTop: 0, marginBottom: 0 }}> |
|
|
|
|
<p className="muted editor-actions-status--flow" style={{ marginTop: 0, marginBottom: 0 }}> |
|
|
|
|
{draftStatus} |
|
|
|
|
</p> |
|
|
|
|
)} |
|
|
|
|
@ -999,61 +1004,55 @@ export default function TestDetail() {
|
|
|
|
|
{attemptsErr} |
|
|
|
|
</p> |
|
|
|
|
)} |
|
|
|
|
<div className="surface-card" style={{ padding: 0, overflow: 'auto' }}> |
|
|
|
|
<table className="table-cabinet"> |
|
|
|
|
<thead> |
|
|
|
|
<tr> |
|
|
|
|
<th>Когда</th> |
|
|
|
|
<th>Участник</th> |
|
|
|
|
<th>v</th> |
|
|
|
|
<th>Результат</th> |
|
|
|
|
<th /> |
|
|
|
|
</tr> |
|
|
|
|
</thead> |
|
|
|
|
<tbody> |
|
|
|
|
{attemptsList.map((a) => ( |
|
|
|
|
<tr key={a.id}> |
|
|
|
|
<td className="mono" style={{ whiteSpace: 'nowrap' }}> |
|
|
|
|
{a.completedAt |
|
|
|
|
? fmtDt(a.completedAt) |
|
|
|
|
: a.startedAt |
|
|
|
|
? fmtDt(a.startedAt) |
|
|
|
|
: '—'} |
|
|
|
|
</td> |
|
|
|
|
<td> |
|
|
|
|
{a.attempterName || '—'} |
|
|
|
|
{a.attempterLogin && ( |
|
|
|
|
<span className="code-inline" style={{ fontSize: 11, marginLeft: 6 }}> |
|
|
|
|
{a.attempterLogin} |
|
|
|
|
</span> |
|
|
|
|
)} |
|
|
|
|
</td> |
|
|
|
|
<td>v{a.testVersion}</td> |
|
|
|
|
<td> |
|
|
|
|
{a.status === 'completed' && a.totalQuestions != null ? ( |
|
|
|
|
<> |
|
|
|
|
{a.correctCount}/{a.totalQuestions} |
|
|
|
|
{a.passed ? ' · зачёт' : ' · незачёт'} |
|
|
|
|
</> |
|
|
|
|
) : ( |
|
|
|
|
a.status |
|
|
|
|
)} |
|
|
|
|
</td> |
|
|
|
|
<td> |
|
|
|
|
{a.status === 'completed' && ( |
|
|
|
|
<Link |
|
|
|
|
to={`/tests/${id}/attempts/${a.id}/review`} |
|
|
|
|
className="btn btn-ghost btn--sm" |
|
|
|
|
> |
|
|
|
|
Разбор |
|
|
|
|
</Link> |
|
|
|
|
)} |
|
|
|
|
</td> |
|
|
|
|
</tr> |
|
|
|
|
))} |
|
|
|
|
</tbody> |
|
|
|
|
</table> |
|
|
|
|
</div> |
|
|
|
|
<ul className="attempts-card-list" aria-label="Список прогонов"> |
|
|
|
|
{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 ( |
|
|
|
|
<li key={a.id} className="surface-card attempts-card-list__item"> |
|
|
|
|
<div className="attempts-card-list__row"> |
|
|
|
|
<div className="attempts-card-list__main"> |
|
|
|
|
<p className="muted attempts-card-list__when mono" style={{ margin: 0, fontSize: '0.8rem' }}> |
|
|
|
|
{when} |
|
|
|
|
</p> |
|
|
|
|
<p className="attempts-card-list__who" style={{ margin: '0.35rem 0 0', fontWeight: 600 }}> |
|
|
|
|
{a.attempterName || '—'} |
|
|
|
|
{a.attempterLogin && ( |
|
|
|
|
<span className="code-inline" style={{ fontSize: 11, marginLeft: 6 }}> |
|
|
|
|
{a.attempterLogin} |
|
|
|
|
</span> |
|
|
|
|
)} |
|
|
|
|
</p> |
|
|
|
|
<p className="muted" style={{ margin: '0.25rem 0 0', fontSize: '0.85rem' }}> |
|
|
|
|
v{a.testVersion} ·{' '} |
|
|
|
|
<span className="attempts-card-list__result">{result}</span> |
|
|
|
|
</p> |
|
|
|
|
</div> |
|
|
|
|
{a.status === 'completed' && ( |
|
|
|
|
<Link |
|
|
|
|
to={`/tests/${id}/attempts/${a.id}/review`} |
|
|
|
|
className="btn btn-ghost btn--sm attempts-card-list__action" |
|
|
|
|
> |
|
|
|
|
Разбор |
|
|
|
|
</Link> |
|
|
|
|
)} |
|
|
|
|
</div> |
|
|
|
|
</li> |
|
|
|
|
); |
|
|
|
|
})} |
|
|
|
|
</ul> |
|
|
|
|
</AccSection> |
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
@ -1085,15 +1084,23 @@ export default function TestDetail() {
|
|
|
|
|
</AccSection> |
|
|
|
|
|
|
|
|
|
<AccSection title="Импорт из файла" defaultOpen={false}> |
|
|
|
|
<div className="inline-actions" style={{ marginBottom: '0.5rem' }}> |
|
|
|
|
<div className="import-file-row" style={{ marginBottom: '0.5rem' }}> |
|
|
|
|
<input |
|
|
|
|
id="import-test-file" |
|
|
|
|
className="import-file-input" |
|
|
|
|
type="file" |
|
|
|
|
accept=".pdf,.docx,.txt,.md,application/pdf" |
|
|
|
|
onChange={onImportFile} |
|
|
|
|
disabled={importBusy} |
|
|
|
|
aria-label="Выбрать файл для импорта" |
|
|
|
|
/> |
|
|
|
|
{importBusy && <span className="text-muted">Обработка…</span>} |
|
|
|
|
<label |
|
|
|
|
htmlFor="import-test-file" |
|
|
|
|
className={`btn btn-ghost import-file-label${importBusy ? ' is-busy' : ''}`} |
|
|
|
|
style={importBusy ? { pointerEvents: 'none' } : undefined} |
|
|
|
|
> |
|
|
|
|
{importBusy ? 'Обработка…' : 'Выбрать файл'} |
|
|
|
|
</label> |
|
|
|
|
</div> |
|
|
|
|
{importErr && ( |
|
|
|
|
<p className="error-text" role="alert"> |
|
|
|
|
@ -1241,6 +1248,28 @@ export default function TestDetail() {
|
|
|
|
|
{assignMsg && <p className="text-muted">{assignMsg}</p>} |
|
|
|
|
</AccSection> |
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
<div |
|
|
|
|
className="test-detail-fixed-actions" |
|
|
|
|
role="region" |
|
|
|
|
aria-label="Быстрые действия с черновиком" |
|
|
|
|
> |
|
|
|
|
<div className="test-detail-fixed-actions__inner"> |
|
|
|
|
{draftStatus && ( |
|
|
|
|
<p className="muted test-detail-fixed-actions__status" role="status"> |
|
|
|
|
{draftStatus} |
|
|
|
|
</p> |
|
|
|
|
)} |
|
|
|
|
<div className="actions-bar"> |
|
|
|
|
<button type="button" className="btn btn-primary" onClick={saveDraft}> |
|
|
|
|
Сохранить черновик |
|
|
|
|
</button> |
|
|
|
|
<Link to="/tests" className="btn btn-ghost"> |
|
|
|
|
К списку |
|
|
|
|
</Link> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|