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:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "6.10.0",
|
"version": "6.11.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -131,9 +131,6 @@ function AppContent() {
|
|||||||
// Счётчик для сброса формы товара при каждом открытии
|
// Счётчик для сброса формы товара при каждом открытии
|
||||||
const [shoppingItemFormKey, setShoppingItemFormKey] = useState(0)
|
const [shoppingItemFormKey, setShoppingItemFormKey] = useState(0)
|
||||||
|
|
||||||
// Модальное окно выбора типа задачи
|
|
||||||
const [showAddModal, setShowAddModal] = useState(false)
|
|
||||||
|
|
||||||
// Ref для функции открытия модала добавления записи в CurrentWeek
|
// Ref для функции открытия модала добавления записи в CurrentWeek
|
||||||
const currentWeekAddModalRef = useRef(null)
|
const currentWeekAddModalRef = useRef(null)
|
||||||
|
|
||||||
@@ -334,9 +331,12 @@ function AppContent() {
|
|||||||
if (savedTab && validTabs.includes(savedTab) && mainTabs.includes(savedTab)) {
|
if (savedTab && validTabs.includes(savedTab) && mainTabs.includes(savedTab)) {
|
||||||
setActiveTab(savedTab)
|
setActiveTab(savedTab)
|
||||||
setLoadedTabs(prev => ({ ...prev, [savedTab]: true }))
|
setLoadedTabs(prev => ({ ...prev, [savedTab]: true }))
|
||||||
|
// Сохраняем таб в history state для корректной работы кнопки "назад"
|
||||||
|
window.history.replaceState({ tab: savedTab }, '', window.location.href)
|
||||||
} else {
|
} else {
|
||||||
// Если нет сохранённого таба — активируем current по умолчанию
|
// Если нет сохранённого таба — активируем current по умолчанию
|
||||||
setLoadedTabs(prev => ({ ...prev, current: true }))
|
setLoadedTabs(prev => ({ ...prev, current: true }))
|
||||||
|
window.history.replaceState({ tab: 'current' }, '', window.location.href)
|
||||||
}
|
}
|
||||||
// Очищаем URL от параметров таба, если это основной таб
|
// Очищаем URL от параметров таба, если это основной таб
|
||||||
if (tabFromUrl && mainTabs.includes(tabFromUrl)) {
|
if (tabFromUrl && mainTabs.includes(tabFromUrl)) {
|
||||||
@@ -847,9 +847,9 @@ function AppContent() {
|
|||||||
setSelectedProject(null)
|
setSelectedProject(null)
|
||||||
clearUrl(event.state.tab)
|
clearUrl(event.state.tab)
|
||||||
} else {
|
} else {
|
||||||
// Если state пустой, используем сохраненный таб из localStorage
|
// Если state пустой, используем сохраненный таб из localStorage (только основные табы)
|
||||||
const savedTab = window.localStorage?.getItem('activeTab')
|
const savedTab = window.localStorage?.getItem('activeTab')
|
||||||
const validMainTab = savedTab && validTabs.includes(savedTab) ? savedTab : 'current'
|
const validMainTab = savedTab && mainTabs.includes(savedTab) ? savedTab : 'current'
|
||||||
setActiveTab(validMainTab)
|
setActiveTab(validMainTab)
|
||||||
setTabParams({})
|
setTabParams({})
|
||||||
markTabAsLoaded(validMainTab)
|
markTabAsLoaded(validMainTab)
|
||||||
@@ -911,8 +911,8 @@ function AppContent() {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров
|
// Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров
|
||||||
// task-form может иметь taskId (редактирование), wishlistId (создание из желания), returnTo (возврат после создания), или isTest (создание теста)
|
// task-form может иметь taskId (редактирование), wishlistId (создание из желания), returnTo (возврат после создания)
|
||||||
const isTaskFormWithNoParams = tab === 'task-form' && params.taskId === undefined && params.wishlistId === undefined && params.returnTo === undefined && params.isTest === undefined && params.isPurchase === undefined
|
const isTaskFormWithNoParams = tab === 'task-form' && params.taskId === undefined && params.wishlistId === undefined && params.returnTo === undefined
|
||||||
// Проверяем, что boardId не null и не undefined (null означает "нет доски", но это валидное значение)
|
// Проверяем, что boardId не null и не undefined (null означает "нет доски", но это валидное значение)
|
||||||
const hasBoardId = params.boardId !== null && params.boardId !== undefined
|
const hasBoardId = params.boardId !== null && params.boardId !== undefined
|
||||||
const isWishlistFormWithNoParams = tab === 'wishlist-form' && params.wishlistId === undefined && params.newTaskId === undefined && !hasBoardId
|
const isWishlistFormWithNoParams = tab === 'wishlist-form' && params.wishlistId === undefined && params.newTaskId === undefined && !hasBoardId
|
||||||
@@ -1025,24 +1025,9 @@ function AppContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработчики для кнопки добавления задачи
|
// Обработчик для кнопки добавления задачи
|
||||||
const handleAddClick = () => {
|
const handleAddClick = () => {
|
||||||
setShowAddModal(true)
|
handleNavigate('task-form', { taskId: undefined })
|
||||||
}
|
|
||||||
|
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработчик навигации для компонентов
|
// Обработчик навигации для компонентов
|
||||||
@@ -1308,13 +1293,11 @@ function AppContent() {
|
|||||||
{loadedTabs['task-form'] && (
|
{loadedTabs['task-form'] && (
|
||||||
<div className={getTabContainerClasses('task-form')}>
|
<div className={getTabContainerClasses('task-form')}>
|
||||||
<div className={getInnerContainerClasses('task-form')}>
|
<div className={getInnerContainerClasses('task-form')}>
|
||||||
<TaskForm
|
<TaskForm
|
||||||
key={tabParams.taskId || `new-${tabParams.isTest ? 'test' : tabParams.isPurchase ? 'purchase' : 'task'}`}
|
key={tabParams.taskId || 'new-task'}
|
||||||
onNavigate={handleNavigate}
|
onNavigate={handleNavigate}
|
||||||
taskId={tabParams.taskId}
|
taskId={tabParams.taskId}
|
||||||
wishlistId={tabParams.wishlistId}
|
wishlistId={tabParams.wishlistId}
|
||||||
isTest={tabParams.isTest}
|
|
||||||
isPurchase={tabParams.isPurchase}
|
|
||||||
returnTo={tabParams.returnTo}
|
returnTo={tabParams.returnTo}
|
||||||
returnWishlistId={tabParams.returnWishlistId}
|
returnWishlistId={tabParams.returnWishlistId}
|
||||||
/>
|
/>
|
||||||
@@ -1779,51 +1762,6 @@ function AppContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Модальное окно выбора типа задачи */}
|
|
||||||
{showAddModal && (
|
|
||||||
<div className="task-add-modal-overlay" onClick={() => setShowAddModal(false)}>
|
|
||||||
<div className="task-add-modal" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className="task-add-modal-header">
|
|
||||||
<h3>Что добавить?</h3>
|
|
||||||
</div>
|
|
||||||
<div className="task-add-modal-buttons">
|
|
||||||
<button
|
|
||||||
className="task-add-modal-button task-add-modal-button-task"
|
|
||||||
onClick={handleAddTask}
|
|
||||||
>
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M9 11l3 3L22 4"></path>
|
|
||||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
|
||||||
</svg>
|
|
||||||
Задача
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="task-add-modal-button task-add-modal-button-test"
|
|
||||||
onClick={handleAddTest}
|
|
||||||
>
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
|
||||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
|
|
||||||
<path d="M8 7h6"></path>
|
|
||||||
<path d="M8 11h4"></path>
|
|
||||||
</svg>
|
|
||||||
Тест
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="task-add-modal-button task-add-modal-button-purchase"
|
|
||||||
onClick={handleAddPurchase}
|
|
||||||
>
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<circle cx="9" cy="21" r="1"></circle>
|
|
||||||
<circle cx="20" cy="21" r="1"></circle>
|
|
||||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
|
||||||
</svg>
|
|
||||||
Закупка
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -452,6 +452,57 @@
|
|||||||
color: #ef4444;
|
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 configuration styles */
|
||||||
.test-config-section {
|
.test-config-section {
|
||||||
background: #f0f9ff;
|
background: #f0f9ff;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import './TaskForm.css'
|
|||||||
const API_URL = '/api/tasks'
|
const API_URL = '/api/tasks'
|
||||||
const PROJECTS_API_URL = '/projects'
|
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 { authFetch } = useAuth()
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [progressionBase, setProgressionBase] = 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 [currentWishlistId, setCurrentWishlistId] = useState(null) // Текущий wishlist_id задачи
|
||||||
const [rewardPolicy, setRewardPolicy] = useState('personal') // Политика награждения: 'personal' или 'general'
|
const [rewardPolicy, setRewardPolicy] = useState('personal') // Политика награждения: 'personal' или 'general'
|
||||||
// Test-specific state
|
// Test-specific state
|
||||||
const [isTest, setIsTest] = useState(isTestFromProps)
|
const [isTest, setIsTest] = useState(false)
|
||||||
const [wordsCount, setWordsCount] = useState('10')
|
const [wordsCount, setWordsCount] = useState('10')
|
||||||
const [maxCards, setMaxCards] = useState('')
|
const [maxCards, setMaxCards] = useState('')
|
||||||
const [selectedDictionaryIDs, setSelectedDictionaryIDs] = useState([])
|
const [selectedDictionaryIDs, setSelectedDictionaryIDs] = useState([])
|
||||||
const [availableDictionaries, setAvailableDictionaries] = useState([])
|
const [availableDictionaries, setAvailableDictionaries] = useState([])
|
||||||
// Purchase-specific state
|
// Purchase-specific state
|
||||||
const [isPurchase, setIsPurchase] = useState(isPurchaseFromProps)
|
const [isPurchase, setIsPurchase] = useState(false)
|
||||||
const [availableBoards, setAvailableBoards] = useState([])
|
const [availableBoards, setAvailableBoards] = useState([])
|
||||||
const [selectedPurchaseBoards, setSelectedPurchaseBoards] = useState([])
|
const [selectedPurchaseBoards, setSelectedPurchaseBoards] = useState([])
|
||||||
const debounceTimer = useRef(null)
|
const debounceTimer = useRef(null)
|
||||||
@@ -119,12 +119,12 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
setError('')
|
setError('')
|
||||||
setLoadingTask(false)
|
setLoadingTask(false)
|
||||||
// Reset test-specific fields
|
// Reset test-specific fields
|
||||||
setIsTest(isTestFromProps)
|
setIsTest(false)
|
||||||
setWordsCount('10')
|
setWordsCount('10')
|
||||||
setMaxCards('')
|
setMaxCards('')
|
||||||
setSelectedDictionaryIDs([])
|
setSelectedDictionaryIDs([])
|
||||||
// Reset purchase-specific fields
|
// Reset purchase-specific fields
|
||||||
setIsPurchase(isPurchaseFromProps)
|
setIsPurchase(false)
|
||||||
setSelectedPurchaseBoards([])
|
setSelectedPurchaseBoards([])
|
||||||
if (debounceTimer.current) {
|
if (debounceTimer.current) {
|
||||||
clearTimeout(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)
|
// Пересчет rewards при изменении reward_message (debounce)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -745,8 +734,8 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
const payload = {
|
const payload = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
reward_message: rewardMessage.trim() || null,
|
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_period: repetitionPeriod,
|
||||||
repetition_date: repetitionDate,
|
repetition_date: repetitionDate,
|
||||||
// При создании: отправляем currentWishlistId если указан (уже число)
|
// При создании: отправляем currentWishlistId если указан (уже число)
|
||||||
@@ -763,7 +752,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
position: r.position,
|
position: r.position,
|
||||||
project_name: r.project_name.trim(),
|
project_name: r.project_name.trim(),
|
||||||
value: parseFloat(r.value) || 0,
|
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) => ({
|
subtasks: (isTest || isPurchase) ? [] : subtasks.map((st, index) => ({
|
||||||
id: st.id || undefined,
|
id: st.id || undefined,
|
||||||
@@ -774,7 +763,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
position: r.position,
|
position: r.position,
|
||||||
project_name: r.project_name.trim(),
|
project_name: r.project_name.trim(),
|
||||||
value: parseFloat(r.value) || 0,
|
value: parseFloat(r.value) || 0,
|
||||||
use_progression: !!(progressionBase && r.use_progression)
|
use_progression: !!(progressionBase && !isTest && !isPurchase && r.use_progression)
|
||||||
}))
|
}))
|
||||||
})),
|
})),
|
||||||
// Test-specific fields
|
// 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)
|
console.log('[TaskForm] Saving newTaskId to sessionStorage and going back:', newTaskId)
|
||||||
// Сохраняем newTaskId в sessionStorage, чтобы WishlistForm мог его прочитать
|
// Сохраняем newTaskId в sessionStorage, чтобы WishlistForm мог его прочитать
|
||||||
sessionStorage.setItem('wishlistFormNewTaskId', String(newTaskId))
|
sessionStorage.setItem('wishlistFormNewTaskId', String(newTaskId))
|
||||||
// Используем history.back() чтобы не создавать лишнюю запись в стеке
|
|
||||||
window.history.back()
|
window.history.back()
|
||||||
} else {
|
} else {
|
||||||
console.log('[TaskForm] No returnTo, going back in history')
|
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) {
|
} catch (err) {
|
||||||
setToastMessage({ text: err.message || 'Ошибка при сохранении задачи', type: 'error' })
|
setToastMessage({ text: err.message || 'Ошибка при сохранении задачи', type: 'error' })
|
||||||
@@ -852,7 +845,17 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
resetForm()
|
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 () => {
|
const handleDelete = async () => {
|
||||||
@@ -920,10 +923,36 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subtask-specific fields (regular task) */}
|
{/* Task type tabs */}
|
||||||
{!isTest && !isPurchase && !wishlistInfo && (
|
{!wishlistInfo && (
|
||||||
<div className="form-group test-config-section">
|
<div className="form-group task-type-tabs-section">
|
||||||
<label>Настройки задачи</label>
|
<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' }}>
|
<div className="test-field-group" style={{ marginBottom: '1rem' }}>
|
||||||
<label htmlFor="progression_base">Прогрессия</label>
|
<label htmlFor="progression_base">Прогрессия</label>
|
||||||
<input
|
<input
|
||||||
@@ -1028,7 +1057,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
placeholder="Score"
|
placeholder="Score"
|
||||||
className="form-input reward-score-input"
|
className="form-input reward-score-input"
|
||||||
/>
|
/>
|
||||||
{progressionBase && (
|
{progressionBase && !isTest && !isPurchase && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -1059,6 +1088,126 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1090,128 +1239,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
</div>
|
</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 && (
|
{!wishlistInfo && (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="repetition_period">Повторения</label>
|
<label htmlFor="repetition_period">Повторения</label>
|
||||||
@@ -1338,7 +1365,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
placeholder="Score"
|
placeholder="Score"
|
||||||
className="form-input reward-score-input"
|
className="form-input reward-score-input"
|
||||||
/>
|
/>
|
||||||
{progressionBase && (
|
{progressionBase && !isTest && !isPurchase && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|||||||
Reference in New Issue
Block a user