2026-01-04 19:37:59 +03:00
|
|
|
|
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"
|
|
|
|
|
|
>
|
2026-01-06 14:38:16 +03:00
|
|
|
|
✓
|
2026-01-04 19:37:59 +03:00
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleComplete}
|
|
|
|
|
|
disabled={isCompleting || !canComplete}
|
|
|
|
|
|
className="complete-button full-width"
|
|
|
|
|
|
>
|
|
|
|
|
|
{isCompleting ? 'Выполнение...' : 'Выполнить'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default TaskDetail
|
|
|
|
|
|
|