Добавлена связь задач с желаниями
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 58s

This commit is contained in:
poignatov
2026-01-12 18:58:52 +03:00
parent 9fbe2081ed
commit 72a6a3caf9
15 changed files with 983 additions and 73 deletions

View File

@@ -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>
)
}