Добавлена связь задач с желаниями
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 58s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 58s
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import TaskDetail from './TaskDetail'
|
||||
import LoadingError from './LoadingError'
|
||||
import Toast from './Toast'
|
||||
import './WishlistDetail.css'
|
||||
import './TaskList.css'
|
||||
|
||||
const API_URL = '/api/wishlist'
|
||||
|
||||
@@ -15,6 +17,7 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh }) {
|
||||
const [isCompleting, setIsCompleting] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [toastMessage, setToastMessage] = useState(null)
|
||||
const [selectedTaskForDetail, setSelectedTaskForDetail] = useState(null)
|
||||
|
||||
const fetchWishlistDetail = useCallback(async () => {
|
||||
try {
|
||||
@@ -134,6 +137,106 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh }) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateTask = () => {
|
||||
if (!wishlistItem || !wishlistItem.unlocked || wishlistItem.completed) return
|
||||
onNavigate?.('task-form', { wishlistId: wishlistId })
|
||||
}
|
||||
|
||||
const handleTaskCheckmarkClick = (e) => {
|
||||
e.stopPropagation()
|
||||
if (wishlistItem?.linked_task) {
|
||||
setSelectedTaskForDetail(wishlistItem.linked_task.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskItemClick = () => {
|
||||
if (wishlistItem?.linked_task) {
|
||||
onNavigate?.('task-form', { taskId: wishlistItem.linked_task.id })
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseDetail = () => {
|
||||
setSelectedTaskForDetail(null)
|
||||
}
|
||||
|
||||
const handleTaskCompleted = () => {
|
||||
setToastMessage({ text: 'Задача выполнена', type: 'success' })
|
||||
// После выполнения задачи желание тоже завершается, перенаправляем на список
|
||||
if (onRefresh) {
|
||||
onRefresh()
|
||||
}
|
||||
if (onNavigate) {
|
||||
onNavigate('wishlist')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUnlinkTask = async (e) => {
|
||||
e.stopPropagation()
|
||||
if (!wishlistItem?.linked_task) return
|
||||
|
||||
try {
|
||||
// Загружаем текущую задачу
|
||||
const taskResponse = await authFetch(`/api/tasks/${wishlistItem.linked_task.id}`)
|
||||
if (!taskResponse.ok) {
|
||||
throw new Error('Ошибка при загрузке задачи')
|
||||
}
|
||||
const taskData = await taskResponse.json()
|
||||
const task = taskData.task
|
||||
|
||||
// Формируем payload для обновления задачи
|
||||
const payload = {
|
||||
name: task.name,
|
||||
reward_message: task.reward_message || null,
|
||||
progression_base: task.progression_base || null,
|
||||
repetition_period: task.repetition_period || null,
|
||||
repetition_date: task.repetition_date || null,
|
||||
wishlist_id: null, // Отвязываем от желания
|
||||
rewards: (task.rewards || []).map(r => ({
|
||||
position: r.position,
|
||||
project_name: r.project_name,
|
||||
value: r.value,
|
||||
use_progression: r.use_progression || false
|
||||
})),
|
||||
subtasks: (task.subtasks || []).map(st => ({
|
||||
id: st.id,
|
||||
name: st.name || null,
|
||||
reward_message: st.reward_message || null,
|
||||
rewards: (st.rewards || []).map(r => ({
|
||||
position: r.position,
|
||||
project_name: r.project_name,
|
||||
value: r.value,
|
||||
use_progression: r.use_progression || false
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
// Обновляем задачу, отвязывая от желания
|
||||
const updateResponse = await authFetch(`/api/tasks/${wishlistItem.linked_task.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (!updateResponse.ok) {
|
||||
const errorData = await updateResponse.json().catch(() => ({}))
|
||||
throw new Error(errorData.message || errorData.error || 'Ошибка при отвязке задачи')
|
||||
}
|
||||
|
||||
setToastMessage({ text: 'Задача отвязана от желания', type: 'success' })
|
||||
// Обновляем данные желания
|
||||
fetchWishlistDetail()
|
||||
if (onRefresh) {
|
||||
onRefresh()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error unlinking task:', err)
|
||||
setToastMessage({ text: err.message || 'Ошибка при отвязке задачи', type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const formatPrice = (price) => {
|
||||
return new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
@@ -298,17 +401,98 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh }) {
|
||||
{/* Условия разблокировки */}
|
||||
{renderUnlockConditions()}
|
||||
|
||||
{/* Кнопка завершения */}
|
||||
{/* Связанная задача или кнопки действий */}
|
||||
{wishlistItem.unlocked && !wishlistItem.completed && (
|
||||
<div className="wishlist-detail-actions">
|
||||
<button
|
||||
onClick={handleComplete}
|
||||
disabled={isCompleting}
|
||||
className="wishlist-detail-complete-button"
|
||||
>
|
||||
{isCompleting ? 'Завершение...' : 'Завершить'}
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
{wishlistItem.linked_task ? (
|
||||
<div className="wishlist-detail-linked-task">
|
||||
<div className="linked-task-label-header">Связанная задача:</div>
|
||||
<div
|
||||
className="task-item"
|
||||
onClick={handleTaskItemClick}
|
||||
>
|
||||
<div className="task-item-content">
|
||||
<div
|
||||
className="task-checkmark"
|
||||
onClick={handleTaskCheckmarkClick}
|
||||
title="Выполнить задачу"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="2" fill="none" className="checkmark-circle" />
|
||||
<path d="M6 10 L9 13 L14 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="checkmark-check" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="task-name-container">
|
||||
<div className="task-name-wrapper">
|
||||
<div className="task-name">
|
||||
{wishlistItem.linked_task.name}
|
||||
</div>
|
||||
{/* Показываем дату только для выполненных задач (next_show_at > сегодня) */}
|
||||
{wishlistItem.linked_task.next_show_at && (() => {
|
||||
const showDate = new Date(wishlistItem.linked_task.next_show_at)
|
||||
// Нормализуем дату: устанавливаем время в 00:00:00 в локальном времени
|
||||
const showDateNormalized = new Date(showDate.getFullYear(), showDate.getMonth(), showDate.getDate())
|
||||
|
||||
const today = new Date()
|
||||
const todayNormalized = new Date(today.getFullYear(), today.getMonth(), today.getDate())
|
||||
|
||||
// Показываем только если дата > сегодня
|
||||
if (showDateNormalized.getTime() <= todayNormalized.getTime()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tomorrowNormalized = new Date(todayNormalized)
|
||||
tomorrowNormalized.setDate(tomorrowNormalized.getDate() + 1)
|
||||
|
||||
let dateText
|
||||
if (showDateNormalized.getTime() === tomorrowNormalized.getTime()) {
|
||||
dateText = 'Завтра'
|
||||
} else {
|
||||
dateText = showDate.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-next-show-date">
|
||||
{dateText}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="task-actions">
|
||||
<button
|
||||
className="task-unlink-button"
|
||||
onClick={handleUnlinkTask}
|
||||
title="Отвязать от желания"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="wishlist-detail-actions">
|
||||
<button
|
||||
onClick={handleComplete}
|
||||
disabled={isCompleting}
|
||||
className="wishlist-detail-complete-button"
|
||||
>
|
||||
{isCompleting ? 'Завершение...' : 'Завершить'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreateTask}
|
||||
className="wishlist-detail-create-task-button"
|
||||
title="Создать задачу"
|
||||
>
|
||||
<svg width="20" height="20" 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>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -320,6 +504,20 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh }) {
|
||||
onClose={() => setToastMessage(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Модальное окно для деталей задачи */}
|
||||
{selectedTaskForDetail && (
|
||||
<TaskDetail
|
||||
taskId={selectedTaskForDetail}
|
||||
onClose={handleCloseDetail}
|
||||
onRefresh={() => {
|
||||
fetchWishlistDetail()
|
||||
if (onRefresh) onRefresh()
|
||||
}}
|
||||
onTaskCompleted={handleTaskCompleted}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user