v2.9.0: Улучшения экрана списка задач - оптимизация загрузки, toast уведомления, исправление центрирования
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 44s
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 44s
This commit is contained in:
206
play-life-web/src/components/TaskDetail.jsx
Normal file
206
play-life-web/src/components/TaskDetail.jsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import './TaskDetail.css'
|
||||
|
||||
const API_URL = '/api/tasks'
|
||||
|
||||
function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [taskDetail, setTaskDetail] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [selectedSubtasks, setSelectedSubtasks] = useState(new Set())
|
||||
const [progressionValue, setProgressionValue] = useState('')
|
||||
const [isCompleting, setIsCompleting] = useState(false)
|
||||
|
||||
const fetchTaskDetail = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const response = await authFetch(`${API_URL}/${taskId}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки задачи')
|
||||
}
|
||||
const data = await response.json()
|
||||
setTaskDetail(data)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
console.error('Error fetching task detail:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [taskId, authFetch])
|
||||
|
||||
useEffect(() => {
|
||||
if (taskId) {
|
||||
fetchTaskDetail()
|
||||
} else {
|
||||
// Сбрасываем состояние при закрытии модального окна
|
||||
setTaskDetail(null)
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setSelectedSubtasks(new Set())
|
||||
setProgressionValue('')
|
||||
}
|
||||
}, [taskId, fetchTaskDetail])
|
||||
|
||||
const handleSubtaskToggle = (subtaskId) => {
|
||||
setSelectedSubtasks(prev => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(subtaskId)) {
|
||||
newSet.delete(subtaskId)
|
||||
} else {
|
||||
newSet.add(subtaskId)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (!taskDetail) return
|
||||
|
||||
// Валидация: если progression_base != null, то value обязателен
|
||||
if (taskDetail.task.progression_base != null && !progressionValue.trim()) {
|
||||
alert('Поле "Значение" обязательно для задач с прогрессией')
|
||||
return
|
||||
}
|
||||
|
||||
setIsCompleting(true)
|
||||
try {
|
||||
const payload = {
|
||||
children_task_ids: Array.from(selectedSubtasks)
|
||||
}
|
||||
|
||||
if (taskDetail.task.progression_base != null && progressionValue.trim()) {
|
||||
payload.value = parseFloat(progressionValue)
|
||||
if (isNaN(payload.value)) {
|
||||
throw new Error('Неверное значение')
|
||||
}
|
||||
}
|
||||
|
||||
const response = await authFetch(`${API_URL}/${taskId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(errorData.message || 'Ошибка при выполнении задачи')
|
||||
}
|
||||
|
||||
// Показываем уведомление о выполнении
|
||||
if (onTaskCompleted) {
|
||||
onTaskCompleted()
|
||||
}
|
||||
|
||||
// Обновляем список и закрываем модальное окно
|
||||
if (onRefresh) {
|
||||
onRefresh()
|
||||
}
|
||||
if (onClose) {
|
||||
onClose()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error completing task:', err)
|
||||
alert(err.message || 'Ошибка при выполнении задачи')
|
||||
} finally {
|
||||
setIsCompleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!taskId) return null
|
||||
|
||||
const { task, rewards, subtasks } = taskDetail || {}
|
||||
const hasProgression = task?.progression_base != null
|
||||
const canComplete = !hasProgression || (hasProgression && progressionValue.trim())
|
||||
|
||||
return (
|
||||
<div className="task-detail-modal-overlay" onClick={onClose}>
|
||||
<div className="task-detail-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="task-detail-modal-header">
|
||||
<h2 className="task-detail-title">
|
||||
{loading ? 'Загрузка...' : error ? 'Ошибка' : taskDetail ? task.name : 'Задача'}
|
||||
</h2>
|
||||
<button onClick={onClose} className="task-detail-close-button">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="task-detail-modal-content">
|
||||
{loading && (
|
||||
<div className="loading">Загрузка...</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-message">{error}</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && taskDetail && (
|
||||
<>
|
||||
{subtasks && subtasks.length > 0 && (
|
||||
<div className="task-subtasks">
|
||||
{subtasks.map((subtask) => {
|
||||
const subtaskName = subtask.task.name || 'Подзадача'
|
||||
return (
|
||||
<div key={subtask.task.id} className="subtask-item">
|
||||
<label className="subtask-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSubtasks.has(subtask.task.id)}
|
||||
onChange={() => handleSubtaskToggle(subtask.task.id)}
|
||||
className="subtask-checkbox"
|
||||
/>
|
||||
<div className="subtask-content">
|
||||
<div className="subtask-name">{subtaskName}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="task-complete-section">
|
||||
{hasProgression ? (
|
||||
<div className="progression-input-group">
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={progressionValue}
|
||||
onChange={(e) => setProgressionValue(e.target.value)}
|
||||
placeholder={`Значение (~${task.progression_base})`}
|
||||
className="progression-input"
|
||||
/>
|
||||
{progressionValue.trim() && (
|
||||
<button
|
||||
onClick={handleComplete}
|
||||
disabled={isCompleting}
|
||||
className="complete-button"
|
||||
>
|
||||
{isCompleting ? 'Выполнение...' : '✓'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleComplete}
|
||||
disabled={isCompleting || !canComplete}
|
||||
className="complete-button full-width"
|
||||
>
|
||||
{isCompleting ? 'Выполнение...' : 'Выполнить'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskDetail
|
||||
|
||||
Reference in New Issue
Block a user