5.0.9: Ожидание бэкенда при завершении теста
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import LoadingError from './LoadingError'
|
||||
import './TestWords.css'
|
||||
@@ -26,11 +26,14 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
const [error, setError] = useState('')
|
||||
const [showPreview, setShowPreview] = useState(false) // Показывать ли экран предпросмотра
|
||||
const [showResults, setShowResults] = useState(false) // Показывать ли экран результатов
|
||||
const [isClosing, setIsClosing] = useState(false) // Ожидание завершения сохранения при нажатии «Закончить»
|
||||
|
||||
const isFinishingRef = useRef(false)
|
||||
const wordStatsRef = useRef({})
|
||||
const processingRef = useRef(false)
|
||||
const cardsShownRef = useRef(0) // Синхронный счётчик для избежания race condition
|
||||
const saveProgressPromiseRef = useRef(null) // Промис сохранения прогресса (null = ещё не запущено)
|
||||
const savePayloadRef = useRef(null) // Данные для сохранения при нажатии «Закончить» (если сохранение ещё не запускали)
|
||||
|
||||
// Функция равномерного распределения слов в пуле с гарантией максимального расстояния между одинаковыми словами
|
||||
// excludeFirstWordId - ID слова, которое не должно быть первым в пуле (только что показанная карточка)
|
||||
@@ -221,6 +224,8 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
setShowResults(false) // Сбрасываем экран результатов при загрузке нового теста
|
||||
isFinishingRef.current = false
|
||||
processingRef.current = false
|
||||
saveProgressPromiseRef.current = null
|
||||
savePayloadRef.current = null
|
||||
setLoading(true)
|
||||
|
||||
const loadWords = async () => {
|
||||
@@ -307,89 +312,80 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
})
|
||||
}
|
||||
|
||||
// Завершение теста
|
||||
const finishTest = async () => {
|
||||
// Завершение теста: показываем результаты сразу, сохранение на бэкенде идёт в фоне (промис в ref для ожидания в handleFinish)
|
||||
const finishTest = () => {
|
||||
if (isFinishingRef.current) return
|
||||
isFinishingRef.current = true
|
||||
|
||||
// Сразу показываем экран результатов, чтобы предотвратить показ новых карточек
|
||||
setShowResults(true)
|
||||
|
||||
// Отправляем статистику на бэкенд
|
||||
try {
|
||||
// Получаем актуальные данные из состояния
|
||||
const currentStats = wordStatsRef.current
|
||||
|
||||
// Отправляем все слова, которые были в тесте, с их текущими значениями
|
||||
// Бэкенд сам обновит только измененные поля
|
||||
const updates = words.map(word => {
|
||||
const stats = currentStats[word.id] || {
|
||||
success: word.success || 0,
|
||||
failure: word.failure || 0,
|
||||
lastSuccessAt: word.last_success_at || null,
|
||||
lastFailureAt: word.last_failure_at || null
|
||||
}
|
||||
return {
|
||||
id: word.id,
|
||||
success: stats.success || 0,
|
||||
failure: stats.failure || 0,
|
||||
last_success_at: stats.lastSuccessAt || null,
|
||||
last_failure_at: stats.lastFailureAt || null
|
||||
}
|
||||
})
|
||||
|
||||
if (updates.length === 0) {
|
||||
console.log('No words to send - empty test')
|
||||
return
|
||||
const currentStats = wordStatsRef.current
|
||||
const updates = words.map(word => {
|
||||
const stats = currentStats[word.id] || {
|
||||
success: word.success || 0,
|
||||
failure: word.failure || 0,
|
||||
lastSuccessAt: word.last_success_at || null,
|
||||
lastFailureAt: word.last_failure_at || null
|
||||
}
|
||||
|
||||
const requestBody = { words: updates }
|
||||
if (configId !== null) {
|
||||
requestBody.config_id = configId
|
||||
return {
|
||||
id: word.id,
|
||||
success: stats.success || 0,
|
||||
failure: stats.failure || 0,
|
||||
last_success_at: stats.lastSuccessAt || null,
|
||||
last_failure_at: stats.lastFailureAt || null
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Sending test progress to backend:', {
|
||||
wordsCount: updates.length,
|
||||
configId: configId,
|
||||
requestBody
|
||||
})
|
||||
|
||||
const response = await authFetch(`${API_URL}/test/progress`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Server responded with status ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const responseData = await response.json().catch(() => ({}))
|
||||
console.log('Test progress saved successfully:', responseData)
|
||||
|
||||
// Если есть taskId, выполняем задачу
|
||||
if (taskId) {
|
||||
try {
|
||||
const completeResponse = await authFetch(`${API_URL}/tasks/${taskId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
|
||||
if (completeResponse.ok) {
|
||||
console.log('Task completed successfully')
|
||||
} else {
|
||||
console.error('Failed to complete task:', await completeResponse.text())
|
||||
}
|
||||
} catch (taskErr) {
|
||||
console.error('Failed to complete task:', taskErr)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save progress:', err)
|
||||
// Можно показать уведомление пользователю, но не блокируем показ результатов
|
||||
if (updates.length === 0) {
|
||||
saveProgressPromiseRef.current = Promise.resolve()
|
||||
savePayloadRef.current = null
|
||||
setShowResults(true)
|
||||
return
|
||||
}
|
||||
|
||||
const requestBody = { words: updates }
|
||||
if (configId !== null) {
|
||||
requestBody.config_id = configId
|
||||
}
|
||||
|
||||
savePayloadRef.current = { requestBody, configId, taskId }
|
||||
|
||||
const doSave = async () => {
|
||||
try {
|
||||
const response = await authFetch(`${API_URL}/test/progress`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Server responded with status ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
await response.json().catch(() => ({}))
|
||||
|
||||
if (taskId) {
|
||||
try {
|
||||
const completeResponse = await authFetch(`${API_URL}/tasks/${taskId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
|
||||
if (!completeResponse.ok) {
|
||||
console.error('Failed to complete task:', await completeResponse.text())
|
||||
}
|
||||
} catch (taskErr) {
|
||||
console.error('Failed to complete task:', taskErr)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save progress:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const promise = doSave()
|
||||
saveProgressPromiseRef.current = promise
|
||||
setShowResults(true)
|
||||
}
|
||||
|
||||
// Берём карточку из пула (getAndDelete) и показываем её
|
||||
@@ -566,8 +562,46 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
showNextCard()
|
||||
}
|
||||
|
||||
const handleFinish = () => {
|
||||
onNavigate?.('tasks')
|
||||
const runSaveFromPayload = useCallback(async (payload) => {
|
||||
const { requestBody, taskId: payloadTaskId } = payload
|
||||
const progressRes = await authFetch(`${API_URL}/test/progress`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
if (!progressRes.ok) {
|
||||
const text = await progressRes.text()
|
||||
throw new Error(`Progress save failed: ${progressRes.status} ${text}`)
|
||||
}
|
||||
if (payloadTaskId) {
|
||||
const completeRes = await authFetch(`${API_URL}/tasks/${payloadTaskId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
if (!completeRes.ok) {
|
||||
const text = await completeRes.text()
|
||||
throw new Error(`Task complete failed: ${completeRes.status} ${text}`)
|
||||
}
|
||||
}
|
||||
}, [authFetch])
|
||||
|
||||
const handleFinish = async () => {
|
||||
if (isClosing) return
|
||||
setIsClosing(true)
|
||||
try {
|
||||
let promise = saveProgressPromiseRef.current
|
||||
if (promise == null && savePayloadRef.current) {
|
||||
promise = runSaveFromPayload(savePayloadRef.current)
|
||||
saveProgressPromiseRef.current = promise
|
||||
}
|
||||
await (promise ?? Promise.resolve())
|
||||
onNavigate?.('tasks')
|
||||
} catch (err) {
|
||||
console.error('Failed to save before close:', err)
|
||||
} finally {
|
||||
setIsClosing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getRandomSide = (word) => {
|
||||
@@ -630,8 +664,12 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
})}
|
||||
</div>
|
||||
<div className="results-actions">
|
||||
<button className="test-finish-button" onClick={handleFinish}>
|
||||
Закончить
|
||||
<button
|
||||
className="test-finish-button"
|
||||
onClick={handleFinish}
|
||||
disabled={isClosing}
|
||||
>
|
||||
{isClosing ? 'Завершение…' : 'Закончить'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user