diff --git a/VERSION b/VERSION index cf79bf9..1de66e5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.10.0 +6.11.0 diff --git a/play-life-web/package.json b/play-life-web/package.json index 0ab6acc..bbba9bb 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "6.10.0", + "version": "6.11.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/App.jsx b/play-life-web/src/App.jsx index 289093e..8bbbc3e 100644 --- a/play-life-web/src/App.jsx +++ b/play-life-web/src/App.jsx @@ -131,9 +131,6 @@ function AppContent() { // Счётчик для сброса формы товара при каждом открытии const [shoppingItemFormKey, setShoppingItemFormKey] = useState(0) - // Модальное окно выбора типа задачи - const [showAddModal, setShowAddModal] = useState(false) - // Ref для функции открытия модала добавления записи в CurrentWeek const currentWeekAddModalRef = useRef(null) @@ -334,9 +331,12 @@ function AppContent() { if (savedTab && validTabs.includes(savedTab) && mainTabs.includes(savedTab)) { setActiveTab(savedTab) setLoadedTabs(prev => ({ ...prev, [savedTab]: true })) + // Сохраняем таб в history state для корректной работы кнопки "назад" + window.history.replaceState({ tab: savedTab }, '', window.location.href) } else { // Если нет сохранённого таба — активируем current по умолчанию setLoadedTabs(prev => ({ ...prev, current: true })) + window.history.replaceState({ tab: 'current' }, '', window.location.href) } // Очищаем URL от параметров таба, если это основной таб if (tabFromUrl && mainTabs.includes(tabFromUrl)) { @@ -847,9 +847,9 @@ function AppContent() { setSelectedProject(null) clearUrl(event.state.tab) } else { - // Если state пустой, используем сохраненный таб из localStorage + // Если state пустой, используем сохраненный таб из localStorage (только основные табы) const savedTab = window.localStorage?.getItem('activeTab') - const validMainTab = savedTab && validTabs.includes(savedTab) ? savedTab : 'current' + const validMainTab = savedTab && mainTabs.includes(savedTab) ? savedTab : 'current' setActiveTab(validMainTab) setTabParams({}) markTabAsLoaded(validMainTab) @@ -911,8 +911,8 @@ function AppContent() { { // Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров - // task-form может иметь taskId (редактирование), wishlistId (создание из желания), returnTo (возврат после создания), или isTest (создание теста) - const isTaskFormWithNoParams = tab === 'task-form' && params.taskId === undefined && params.wishlistId === undefined && params.returnTo === undefined && params.isTest === undefined && params.isPurchase === undefined + // task-form может иметь taskId (редактирование), wishlistId (создание из желания), returnTo (возврат после создания) + const isTaskFormWithNoParams = tab === 'task-form' && params.taskId === undefined && params.wishlistId === undefined && params.returnTo === undefined // Проверяем, что boardId не null и не undefined (null означает "нет доски", но это валидное значение) const hasBoardId = params.boardId !== null && params.boardId !== undefined const isWishlistFormWithNoParams = tab === 'wishlist-form' && params.wishlistId === undefined && params.newTaskId === undefined && !hasBoardId @@ -1025,24 +1025,9 @@ function AppContent() { } } - // Обработчики для кнопки добавления задачи + // Обработчик для кнопки добавления задачи const handleAddClick = () => { - setShowAddModal(true) - } - - const handleAddTask = () => { - setShowAddModal(false) - handleNavigate('task-form', { taskId: undefined, isTest: false }) - } - - const handleAddTest = () => { - setShowAddModal(false) - handleNavigate('task-form', { taskId: undefined, isTest: true }) - } - - const handleAddPurchase = () => { - setShowAddModal(false) - handleNavigate('task-form', { taskId: undefined, isPurchase: true }) + handleNavigate('task-form', { taskId: undefined }) } // Обработчик навигации для компонентов @@ -1308,13 +1293,11 @@ function AppContent() { {loadedTabs['task-form'] && (
- @@ -1779,51 +1762,6 @@ function AppContent() {
)} - {/* Модальное окно выбора типа задачи */} - {showAddModal && ( -
setShowAddModal(false)}> -
e.stopPropagation()}> -
-

Что добавить?

-
-
- - - -
-
-
- )}
) } diff --git a/play-life-web/src/components/TaskForm.css b/play-life-web/src/components/TaskForm.css index 233e0ec..9c4bff8 100644 --- a/play-life-web/src/components/TaskForm.css +++ b/play-life-web/src/components/TaskForm.css @@ -452,6 +452,57 @@ color: #ef4444; } +/* Task type tabs */ +.task-type-tabs-section { + background: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 0.5rem; + padding: 0; + overflow: hidden; +} + +.task-type-tabs { + display: flex; + border-bottom: 1px solid #bae6fd; +} + +.task-type-tab { + flex: 1; + padding: 0.625rem 0; + border: none; + background: transparent; + font-size: 0.875rem; + font-weight: 500; + color: #64748b; + cursor: pointer; + transition: all 0.15s ease; + position: relative; +} + +.task-type-tab:not(:last-child) { + border-right: 1px solid #bae6fd; +} + +.task-type-tab-active { + color: #3498db; + font-weight: 600; + background: rgba(52, 152, 219, 0.08); +} + +.task-type-tab-active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: #3498db; +} + +.task-type-content { + padding: 1rem; +} + /* Test configuration styles */ .test-config-section { background: #f0f9ff; diff --git a/play-life-web/src/components/TaskForm.jsx b/play-life-web/src/components/TaskForm.jsx index 01b77cc..2a96259 100644 --- a/play-life-web/src/components/TaskForm.jsx +++ b/play-life-web/src/components/TaskForm.jsx @@ -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 /> - {/* Subtask-specific fields (regular task) */} - {!isTest && !isPurchase && !wishlistInfo && ( -
- + {/* Task type tabs */} + {!wishlistInfo && ( +
+
+ + + +
+ + {/* Задача */} + {!isTest && !isPurchase && ( +
- {progressionBase && ( + {progressionBase && !isTest && !isPurchase && ( +
+ )} + + {/* Тест */} + {isTest && ( +
+
+
+ + setWordsCount(e.target.value)} + className="form-input" + required + /> +
+
+ + setMaxCards(e.target.value)} + placeholder="Без ограничения" + className="form-input" + /> +
+
+
+ +
+ {availableDictionaries.map(dict => ( + + ))} + {availableDictionaries.length === 0 && ( +
+ Нет доступных словарей. Создайте словарь в разделе "Словари". +
+ )} +
+
+
+ )} + + {/* Закупка */} + {isPurchase && ( +
+
+ +
+ {availableBoards.map(board => ( +
+ + {board.groups.length > 0 && !selectedPurchaseBoards.some(pb => pb.board_id === board.id && pb.group_name === null) && ( +
+ {board.groups.map(group => ( + + ))} +
+ )} +
+ ))} + {availableBoards.length === 0 && ( +
+ Нет доступных досок. Создайте доску в разделе "Товары". +
+ )} +
+
+
+ )}
)} @@ -1090,128 +1239,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
)} - {/* Test-specific fields */} - {isTest && ( -
- -
-
- - setWordsCount(e.target.value)} - className="form-input" - required - /> -
-
- - setMaxCards(e.target.value)} - placeholder="Без ограничения" - className="form-input" - /> -
-
-
- -
- {availableDictionaries.map(dict => ( - - ))} - {availableDictionaries.length === 0 && ( -
- Нет доступных словарей. Создайте словарь в разделе "Словари". -
- )} -
-
-
- )} - - {/* Purchase-specific fields */} - {isPurchase && ( -
- -
- -
- {availableBoards.map(board => ( -
- - {board.groups.length > 0 && !selectedPurchaseBoards.some(pb => pb.board_id === board.id && pb.group_name === null) && ( -
- {board.groups.map(group => ( - - ))} -
- )} -
- ))} - {availableBoards.length === 0 && ( -
- Нет доступных досок. Создайте доску в разделе "Товары". -
- )} -
-
-
- )} - {!wishlistInfo && (
@@ -1338,7 +1365,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa placeholder="Score" className="form-input reward-score-input" /> - {progressionBase && ( + {progressionBase && !isTest && !isPurchase && (