6.11.0: Фикс навигации и переключение типов задач
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m8s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m8s
This commit is contained in:
@@ -8,7 +8,7 @@ import './TaskForm.css'
|
||||
const API_URL = '/api/tasks'
|
||||
const PROJECTS_API_URL = '/projects'
|
||||
|
||||
function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = false, isPurchase: isPurchaseFromProps = false, returnTo, returnWishlistId }) {
|
||||
function TaskForm({ onNavigate, taskId, wishlistId, returnTo, returnWishlistId }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [name, setName] = useState('')
|
||||
const [progressionBase, setProgressionBase] = useState('')
|
||||
@@ -30,13 +30,13 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
const [currentWishlistId, setCurrentWishlistId] = useState(null) // Текущий wishlist_id задачи
|
||||
const [rewardPolicy, setRewardPolicy] = useState('personal') // Политика награждения: 'personal' или 'general'
|
||||
// Test-specific state
|
||||
const [isTest, setIsTest] = useState(isTestFromProps)
|
||||
const [isTest, setIsTest] = useState(false)
|
||||
const [wordsCount, setWordsCount] = useState('10')
|
||||
const [maxCards, setMaxCards] = useState('')
|
||||
const [selectedDictionaryIDs, setSelectedDictionaryIDs] = useState([])
|
||||
const [availableDictionaries, setAvailableDictionaries] = useState([])
|
||||
// Purchase-specific state
|
||||
const [isPurchase, setIsPurchase] = useState(isPurchaseFromProps)
|
||||
const [isPurchase, setIsPurchase] = useState(false)
|
||||
const [availableBoards, setAvailableBoards] = useState([])
|
||||
const [selectedPurchaseBoards, setSelectedPurchaseBoards] = useState([])
|
||||
const debounceTimer = useRef(null)
|
||||
@@ -119,12 +119,12 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
setError('')
|
||||
setLoadingTask(false)
|
||||
// Reset test-specific fields
|
||||
setIsTest(isTestFromProps)
|
||||
setIsTest(false)
|
||||
setWordsCount('10')
|
||||
setMaxCards('')
|
||||
setSelectedDictionaryIDs([])
|
||||
// Reset purchase-specific fields
|
||||
setIsPurchase(isPurchaseFromProps)
|
||||
setIsPurchase(false)
|
||||
setSelectedPurchaseBoards([])
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current)
|
||||
@@ -446,19 +446,8 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка подзадач при переключении задачи в режим теста
|
||||
useEffect(() => {
|
||||
if (isTest && subtasks.length > 0) {
|
||||
setSubtasks([])
|
||||
}
|
||||
}, [isTest])
|
||||
|
||||
// Очистка подзадач при переключении задачи в режим закупки
|
||||
useEffect(() => {
|
||||
if (isPurchase && subtasks.length > 0) {
|
||||
setSubtasks([])
|
||||
}
|
||||
}, [isPurchase])
|
||||
// Подзадачи, словари и товары сохраняются в памяти при переключении типа.
|
||||
// При сохранении используются только данные текущего активного типа.
|
||||
|
||||
// Пересчет rewards при изменении reward_message (debounce)
|
||||
useEffect(() => {
|
||||
@@ -745,8 +734,8 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
const payload = {
|
||||
name: name.trim(),
|
||||
reward_message: rewardMessage.trim() || null,
|
||||
// Тесты и задачи с желанием не могут иметь прогрессию
|
||||
progression_base: (isLinkedToWishlist || isTest) ? null : (progressionBase ? parseFloat(progressionBase) : null),
|
||||
// Тесты, закупки и задачи с желанием не могут иметь прогрессию
|
||||
progression_base: (isLinkedToWishlist || isTest || isPurchase) ? null : (progressionBase ? parseFloat(progressionBase) : null),
|
||||
repetition_period: repetitionPeriod,
|
||||
repetition_date: repetitionDate,
|
||||
// При создании: отправляем currentWishlistId если указан (уже число)
|
||||
@@ -763,7 +752,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
position: r.position,
|
||||
project_name: r.project_name.trim(),
|
||||
value: parseFloat(r.value) || 0,
|
||||
use_progression: !!(progressionBase && r.use_progression)
|
||||
use_progression: !!(progressionBase && !isTest && !isPurchase && r.use_progression)
|
||||
})),
|
||||
subtasks: (isTest || isPurchase) ? [] : subtasks.map((st, index) => ({
|
||||
id: st.id || undefined,
|
||||
@@ -774,7 +763,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
position: r.position,
|
||||
project_name: r.project_name.trim(),
|
||||
value: parseFloat(r.value) || 0,
|
||||
use_progression: !!(progressionBase && r.use_progression)
|
||||
use_progression: !!(progressionBase && !isTest && !isPurchase && r.use_progression)
|
||||
}))
|
||||
})),
|
||||
// Test-specific fields
|
||||
@@ -828,12 +817,16 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
console.log('[TaskForm] Saving newTaskId to sessionStorage and going back:', newTaskId)
|
||||
// Сохраняем newTaskId в sessionStorage, чтобы WishlistForm мог его прочитать
|
||||
sessionStorage.setItem('wishlistFormNewTaskId', String(newTaskId))
|
||||
// Используем history.back() чтобы не создавать лишнюю запись в стеке
|
||||
window.history.back()
|
||||
} else {
|
||||
console.log('[TaskForm] No returnTo, going back in history')
|
||||
// Возврат назад по стеку истории (на список задач, желаний и т.д.)
|
||||
window.history.back()
|
||||
// Возвращаемся назад, если есть предыдущая запись
|
||||
const state = window.history.state
|
||||
if ((state && state.previousTab) || window.history.length > 1) {
|
||||
window.history.back()
|
||||
} else {
|
||||
onNavigate('tasks')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setToastMessage({ text: err.message || 'Ошибка при сохранении задачи', type: 'error' })
|
||||
@@ -852,7 +845,17 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
|
||||
const handleCancel = () => {
|
||||
resetForm()
|
||||
window.history.back()
|
||||
// Проверяем, есть ли предыдущая запись в стеке для history.back()
|
||||
const state = window.history.state
|
||||
if (state && state.previousTab) {
|
||||
// Есть предыдущая запись — можно безопасно вернуться
|
||||
window.history.back()
|
||||
} else if (window.history.length > 1) {
|
||||
window.history.back()
|
||||
} else {
|
||||
// Стек пуст — прямой переход
|
||||
onNavigate('tasks')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -920,10 +923,36 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Subtask-specific fields (regular task) */}
|
||||
{!isTest && !isPurchase && !wishlistInfo && (
|
||||
<div className="form-group test-config-section">
|
||||
<label>Настройки задачи</label>
|
||||
{/* Task type tabs */}
|
||||
{!wishlistInfo && (
|
||||
<div className="form-group task-type-tabs-section">
|
||||
<div className="task-type-tabs">
|
||||
<button
|
||||
type="button"
|
||||
className={`task-type-tab ${!isTest && !isPurchase ? 'task-type-tab-active' : ''}`}
|
||||
onClick={() => { setIsTest(false); setIsPurchase(false) }}
|
||||
>
|
||||
Задача
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`task-type-tab ${isTest ? 'task-type-tab-active' : ''}`}
|
||||
onClick={() => { setIsTest(true); setIsPurchase(false) }}
|
||||
>
|
||||
Тест
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`task-type-tab ${isPurchase ? 'task-type-tab-active' : ''}`}
|
||||
onClick={() => { setIsPurchase(true); setIsTest(false) }}
|
||||
>
|
||||
Закупка
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Задача */}
|
||||
{!isTest && !isPurchase && (
|
||||
<div className="task-type-content">
|
||||
<div className="test-field-group" style={{ marginBottom: '1rem' }}>
|
||||
<label htmlFor="progression_base">Прогрессия</label>
|
||||
<input
|
||||
@@ -1028,7 +1057,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
placeholder="Score"
|
||||
className="form-input reward-score-input"
|
||||
/>
|
||||
{progressionBase && (
|
||||
{progressionBase && !isTest && !isPurchase && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
@@ -1059,6 +1088,126 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Тест */}
|
||||
{isTest && (
|
||||
<div className="task-type-content">
|
||||
<div className="test-config-fields">
|
||||
<div className="test-field-group">
|
||||
<label htmlFor="words_count">Количество слов *</label>
|
||||
<input
|
||||
id="words_count"
|
||||
type="number"
|
||||
min="1"
|
||||
value={wordsCount}
|
||||
onChange={(e) => setWordsCount(e.target.value)}
|
||||
className="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="test-field-group">
|
||||
<label htmlFor="max_cards">Макс. карточек</label>
|
||||
<input
|
||||
id="max_cards"
|
||||
type="number"
|
||||
min="1"
|
||||
value={maxCards}
|
||||
onChange={(e) => setMaxCards(e.target.value)}
|
||||
placeholder="Без ограничения"
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="test-dictionaries-section">
|
||||
<label>Словари *</label>
|
||||
<div className="test-dictionaries-list">
|
||||
{availableDictionaries.map(dict => (
|
||||
<label key={dict.id} className="test-dictionary-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDictionaryIDs.includes(dict.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedDictionaryIDs([...selectedDictionaryIDs, dict.id])
|
||||
} else {
|
||||
setSelectedDictionaryIDs(selectedDictionaryIDs.filter(id => id !== dict.id))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="test-dictionary-name">{dict.name}</span>
|
||||
<span className="test-dictionary-count">({dict.wordsCount} слов)</span>
|
||||
</label>
|
||||
))}
|
||||
{availableDictionaries.length === 0 && (
|
||||
<div className="test-no-dictionaries">
|
||||
Нет доступных словарей. Создайте словарь в разделе "Словари".
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Закупка */}
|
||||
{isPurchase && (
|
||||
<div className="task-type-content">
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<label style={{ fontSize: '0.875rem', fontWeight: 500, color: '#374151', marginBottom: '0.5rem', display: 'block' }}>Доски и группы *</label>
|
||||
<div className="test-dictionaries-list">
|
||||
{availableBoards.map(board => (
|
||||
<div key={board.id}>
|
||||
<label className="test-dictionary-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPurchaseBoards.some(pb => pb.board_id === board.id && pb.group_name === null)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedPurchaseBoards(prev => [
|
||||
...prev.filter(pb => pb.board_id !== board.id),
|
||||
{ board_id: board.id, group_name: null }
|
||||
])
|
||||
} else {
|
||||
setSelectedPurchaseBoards(prev => prev.filter(pb => !(pb.board_id === board.id && pb.group_name === null)))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="test-dictionary-name">{board.name}</span>
|
||||
<span className="test-dictionary-count">(вся доска)</span>
|
||||
</label>
|
||||
{board.groups.length > 0 && !selectedPurchaseBoards.some(pb => pb.board_id === board.id && pb.group_name === null) && (
|
||||
<div style={{ paddingLeft: '1.25rem', marginTop: '2px' }}>
|
||||
{board.groups.map(group => (
|
||||
<label key={group || '__ungrouped'} className="test-dictionary-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPurchaseBoards.some(pb => pb.board_id === board.id && pb.group_name === (group || ''))}
|
||||
onChange={(e) => {
|
||||
const groupValue = group || ''
|
||||
if (e.target.checked) {
|
||||
setSelectedPurchaseBoards(prev => [...prev, { board_id: board.id, group_name: groupValue }])
|
||||
} else {
|
||||
setSelectedPurchaseBoards(prev => prev.filter(pb => !(pb.board_id === board.id && pb.group_name === groupValue)))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="test-dictionary-name">{group || 'Остальные'}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{availableBoards.length === 0 && (
|
||||
<div className="test-no-dictionaries">
|
||||
Нет доступных досок. Создайте доску в разделе "Товары".
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1090,128 +1239,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Test-specific fields */}
|
||||
{isTest && (
|
||||
<div className="form-group test-config-section">
|
||||
<label>Настройки теста</label>
|
||||
<div className="test-config-fields">
|
||||
<div className="test-field-group">
|
||||
<label htmlFor="words_count">Количество слов *</label>
|
||||
<input
|
||||
id="words_count"
|
||||
type="number"
|
||||
min="1"
|
||||
value={wordsCount}
|
||||
onChange={(e) => setWordsCount(e.target.value)}
|
||||
className="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="test-field-group">
|
||||
<label htmlFor="max_cards">Макс. карточек</label>
|
||||
<input
|
||||
id="max_cards"
|
||||
type="number"
|
||||
min="1"
|
||||
value={maxCards}
|
||||
onChange={(e) => setMaxCards(e.target.value)}
|
||||
placeholder="Без ограничения"
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="test-dictionaries-section">
|
||||
<label>Словари *</label>
|
||||
<div className="test-dictionaries-list">
|
||||
{availableDictionaries.map(dict => (
|
||||
<label key={dict.id} className="test-dictionary-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDictionaryIDs.includes(dict.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedDictionaryIDs([...selectedDictionaryIDs, dict.id])
|
||||
} else {
|
||||
setSelectedDictionaryIDs(selectedDictionaryIDs.filter(id => id !== dict.id))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="test-dictionary-name">{dict.name}</span>
|
||||
<span className="test-dictionary-count">({dict.wordsCount} слов)</span>
|
||||
</label>
|
||||
))}
|
||||
{availableDictionaries.length === 0 && (
|
||||
<div className="test-no-dictionaries">
|
||||
Нет доступных словарей. Создайте словарь в разделе "Словари".
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Purchase-specific fields */}
|
||||
{isPurchase && (
|
||||
<div className="form-group test-config-section">
|
||||
<label>Настройка закупки</label>
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<label style={{ fontSize: '0.875rem', fontWeight: 500, color: '#374151', marginBottom: '0.5rem', display: 'block' }}>Доски и группы *</label>
|
||||
<div className="test-dictionaries-list">
|
||||
{availableBoards.map(board => (
|
||||
<div key={board.id}>
|
||||
<label className="test-dictionary-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPurchaseBoards.some(pb => pb.board_id === board.id && pb.group_name === null)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
// Добавляем всю доску, убираем отдельные группы этой доски
|
||||
setSelectedPurchaseBoards(prev => [
|
||||
...prev.filter(pb => pb.board_id !== board.id),
|
||||
{ board_id: board.id, group_name: null }
|
||||
])
|
||||
} else {
|
||||
// Убираем доску целиком
|
||||
setSelectedPurchaseBoards(prev => prev.filter(pb => !(pb.board_id === board.id && pb.group_name === null)))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="test-dictionary-name">{board.name}</span>
|
||||
<span className="test-dictionary-count">(вся доска)</span>
|
||||
</label>
|
||||
{board.groups.length > 0 && !selectedPurchaseBoards.some(pb => pb.board_id === board.id && pb.group_name === null) && (
|
||||
<div style={{ paddingLeft: '1.25rem', marginTop: '2px' }}>
|
||||
{board.groups.map(group => (
|
||||
<label key={group || '__ungrouped'} className="test-dictionary-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPurchaseBoards.some(pb => pb.board_id === board.id && pb.group_name === (group || ''))}
|
||||
onChange={(e) => {
|
||||
const groupValue = group || ''
|
||||
if (e.target.checked) {
|
||||
setSelectedPurchaseBoards(prev => [...prev, { board_id: board.id, group_name: groupValue }])
|
||||
} else {
|
||||
setSelectedPurchaseBoards(prev => prev.filter(pb => !(pb.board_id === board.id && pb.group_name === groupValue)))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="test-dictionary-name">{group || 'Остальные'}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{availableBoards.length === 0 && (
|
||||
<div className="test-no-dictionaries">
|
||||
Нет доступных досок. Создайте доску в разделе "Товары".
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!wishlistInfo && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="repetition_period">Повторения</label>
|
||||
@@ -1338,7 +1365,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
placeholder="Score"
|
||||
className="form-input reward-score-input"
|
||||
/>
|
||||
{progressionBase && (
|
||||
{progressionBase && !isTest && !isPurchase && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
|
||||
Reference in New Issue
Block a user