Добавлена связь задач с желаниями
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,6 +1,6 @@
{
"name": "play-life-web",
"version": "3.9.4",
"version": "3.10.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -626,9 +626,11 @@ function AppContent() {
updateUrl(tab, {}, activeTab)
}
} else {
// Для task-form и wishlist-form явно удаляем параметры, если они undefined
if ((tab === 'task-form' && params.taskId === undefined) ||
(tab === 'wishlist-form' && params.wishlistId === undefined)) {
// Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров
// task-form может иметь taskId (редактирование) или wishlistId (создание из желания)
const isTaskFormWithNoParams = tab === 'task-form' && params.taskId === undefined && params.wishlistId === undefined
const isWishlistFormWithNoParams = tab === 'wishlist-form' && params.wishlistId === undefined
if (isTaskFormWithNoParams || isWishlistFormWithNoParams) {
setTabParams({})
if (isNewTabMain) {
clearUrl()
@@ -865,6 +867,7 @@ function AppContent() {
key={tabParams.taskId || 'new'}
onNavigate={handleNavigate}
taskId={tabParams.taskId}
wishlistId={tabParams.wishlistId}
/>
</div>
)}

View File

@@ -273,3 +273,47 @@
color: #ef4444;
}
.task-wishlist-link {
margin-bottom: 1.5rem;
padding: 0.75rem;
background-color: #f0f9ff;
border-radius: 6px;
border: 1px solid #bae6fd;
}
.task-wishlist-link-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.task-wishlist-link-info svg {
color: #6366f1;
flex-shrink: 0;
}
.task-wishlist-link-label {
font-size: 0.9rem;
color: #374151;
font-weight: 500;
}
.task-wishlist-link-button {
background: none;
border: none;
color: #6366f1;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: all 0.2s;
text-decoration: underline;
margin-left: auto;
}
.task-wishlist-link-button:hover {
background-color: rgba(99, 102, 241, 0.1);
text-decoration: none;
}

View File

@@ -373,7 +373,7 @@ const formatTelegramMessage = (task, rewards, subtasks, selectedSubtasks, progre
return finalMessage
}
function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate }) {
const { authFetch } = useAuth()
const [taskDetail, setTaskDetail] = useState(null)
const [loading, setLoading] = useState(true)
@@ -382,6 +382,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
const [progressionValue, setProgressionValue] = useState('')
const [isCompleting, setIsCompleting] = useState(false)
const [toastMessage, setToastMessage] = useState(null)
const [wishlistInfo, setWishlistInfo] = useState(null)
const fetchTaskDetail = useCallback(async () => {
try {
@@ -393,6 +394,25 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
}
const data = await response.json()
setTaskDetail(data)
// Загружаем информацию о связанном желании, если есть
if (data.task.wishlist_id) {
try {
const wishlistResponse = await authFetch(`/api/wishlist/${data.task.wishlist_id}`)
if (wishlistResponse.ok) {
const wishlistData = await wishlistResponse.json()
setWishlistInfo({
id: wishlistData.id,
name: wishlistData.name,
unlocked: wishlistData.unlocked || false
})
}
} catch (err) {
console.error('Error loading wishlist info:', err)
}
} else {
setWishlistInfo(null)
}
} catch (err) {
setError(err.message)
console.error('Error fetching task detail:', err)
@@ -429,6 +449,12 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
const handleComplete = async (shouldDelete = false) => {
if (!taskDetail) return
// Проверяем, что желание разблокировано (если есть связанное желание)
if (wishlistInfo && !wishlistInfo.unlocked) {
setToastMessage({ text: 'Невозможно выполнить задачу: желание не разблокировано', type: 'error' })
return
}
// Если прогрессия не введена, используем 0 (валидация не требуется)
setIsCompleting(true)
@@ -492,8 +518,8 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
const { task, rewards, subtasks } = taskDetail || {}
const hasProgression = task?.progression_base != null
// Кнопка всегда активна (если прогрессия не введена, используем 0)
const canComplete = true
// Кнопка активна только если желание разблокировано (или задачи нет связанного желания)
const canComplete = !wishlistInfo || wishlistInfo.unlocked
// Определяем, является ли задача одноразовой
// Одноразовая задача: когда оба поля null/undefined (из бэкенда видно, что в этом случае задача помечается как deleted)
@@ -556,6 +582,33 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
{!loading && !error && taskDetail && (
<>
{/* Информация о связанном желании */}
{task.wishlist_id && wishlistInfo && (
<div className="task-wishlist-link">
<div className="task-wishlist-link-info">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 12 20 22 4 22 4 12"></polyline>
<rect x="2" y="7" width="20" height="5"></rect>
<line x1="12" y1="22" x2="12" y2="7"></line>
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path>
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path>
</svg>
<span className="task-wishlist-link-label">Связано с желанием:</span>
<button
onClick={() => {
if (onClose) onClose()
if (onNavigate && wishlistInfo) {
onNavigate('wishlist-detail', { wishlistId: wishlistInfo.id })
}
}}
className="task-wishlist-link-button"
>
{wishlistInfo.name}
</button>
</div>
</div>
)}
{/* Поле ввода прогрессии */}
{hasProgression && (
<div className="progression-section">
@@ -619,13 +672,20 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
onClick={() => handleComplete(false)}
disabled={isCompleting || !canComplete}
className="complete-button"
title={!canComplete && wishlistInfo ? 'Желание не разблокировано' : ''}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ marginRight: '0.5rem' }}>
<path d="M13.5 4L6 11.5L2.5 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
{!canComplete && wishlistInfo ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '0.5rem' }}>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/>
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ marginRight: '0.5rem' }}>
<path d="M13.5 4L6 11.5L2.5 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
{isCompleting ? 'Выполнение...' : 'Выполнить'}
</button>
{!isOneTime && (
{!isOneTime && canComplete && (
<button
onClick={() => handleComplete(true)}
disabled={isCompleting || !canComplete}

View File

@@ -370,3 +370,46 @@
color: #6b7280;
}
.wishlist-link-info {
padding: 0.75rem;
background-color: #f0f9ff;
border-radius: 6px;
border: 1px solid #bae6fd;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.wishlist-link-text {
font-size: 0.9rem;
color: #374151;
flex: 1;
}
.wishlist-link-text strong {
color: #6366f1;
font-weight: 600;
}
.wishlist-unlink-x {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: #9ca3af;
font-size: 1rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
flex-shrink: 0;
}
.wishlist-unlink-x:hover {
background-color: rgba(239, 68, 68, 0.1);
color: #ef4444;
}

View File

@@ -6,7 +6,7 @@ import './TaskForm.css'
const API_URL = '/api/tasks'
const PROJECTS_API_URL = '/projects'
function TaskForm({ onNavigate, taskId }) {
function TaskForm({ onNavigate, taskId, wishlistId }) {
const { authFetch } = useAuth()
const [name, setName] = useState('')
const [progressionBase, setProgressionBase] = useState('')
@@ -22,6 +22,8 @@ function TaskForm({ onNavigate, taskId }) {
const [toastMessage, setToastMessage] = useState(null)
const [loadingTask, setLoadingTask] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [wishlistInfo, setWishlistInfo] = useState(null) // Информация о связанном желании
const [currentWishlistId, setCurrentWishlistId] = useState(null) // Текущий wishlist_id задачи
const debounceTimer = useRef(null)
// Загрузка проектов для автокомплита
@@ -65,8 +67,37 @@ function TaskForm({ onNavigate, taskId }) {
} else {
// Сбрасываем форму при создании новой задачи
resetForm()
if (wishlistId) {
// Преобразуем wishlistId в число
const wishlistIdNum = typeof wishlistId === 'string' ? parseInt(wishlistId, 10) : wishlistId
setCurrentWishlistId(wishlistIdNum)
// Загружаем данные желания здесь, чтобы они не сбросились
const loadWishlistData = async () => {
try {
const response = await authFetch(`/api/wishlist/${wishlistIdNum}`)
if (response.ok) {
const data = await response.json()
setWishlistInfo({ id: data.id, name: data.name })
// Предзаполняем название задачи названием желания
if (data.name) {
setName(data.name)
}
// Предзаполняем сообщение награды
if (data.name) {
setRewardMessage(`Выполнить желание: ${data.name}`)
}
}
} catch (err) {
console.error('Error loading wishlist:', err)
}
}
loadWishlistData()
} else {
setCurrentWishlistId(null)
setWishlistInfo(null)
}
}
}, [taskId])
}, [taskId, wishlistId, authFetch])
const loadTask = async () => {
setLoadingTask(true)
@@ -263,6 +294,28 @@ function TaskForm({ onNavigate, taskId }) {
use_progression: r.use_progression
}))
})))
// Загружаем информацию о связанном желании, если есть
if (data.task.wishlist_id) {
setCurrentWishlistId(data.task.wishlist_id)
try {
const wishlistResponse = await authFetch(`/api/wishlist/${data.task.wishlist_id}`)
if (wishlistResponse.ok) {
const wishlistData = await wishlistResponse.json()
setWishlistInfo({ id: wishlistData.id, name: wishlistData.name })
// Если задача привязана к желанию, очищаем поля повторения и прогрессии
setRepetitionPeriodValue('')
setRepetitionPeriodType('day')
setRepetitionMode('after')
setProgressionBase('')
}
} catch (err) {
console.error('Error loading wishlist info:', err)
}
} else {
setCurrentWishlistId(null)
setWishlistInfo(null)
}
} catch (err) {
setError(err.message)
} finally {
@@ -422,12 +475,31 @@ function TaskForm({ onNavigate, taskId }) {
}
}
// Проверяем, что задача с привязанным желанием не может быть периодической
const isLinkedToWishlist = wishlistInfo !== null || (taskId && currentWishlistId)
if (isLinkedToWishlist && repetitionPeriodValue && repetitionPeriodValue.trim() !== '') {
const value = parseInt(repetitionPeriodValue.trim(), 10)
if (!isNaN(value) && value !== 0) {
setError('Задачи, привязанные к желанию, не могут быть периодическими')
setLoading(false)
return
}
}
// Проверяем, что задача с привязанным желанием не может иметь прогрессию
if (isLinkedToWishlist && progressionBase && progressionBase.trim() !== '') {
setError('Задачи, привязанные к желанию, не могут иметь прогрессию')
setLoading(false)
return
}
try {
// Преобразуем период повторения в строку INTERVAL для PostgreSQL или repetition_date
let repetitionPeriod = null
let repetitionDate = null
if (repetitionPeriodValue && repetitionPeriodValue.trim() !== '') {
// Если задача привязана к желанию, не устанавливаем повторения
if (!isLinkedToWishlist && repetitionPeriodValue && repetitionPeriodValue.trim() !== '') {
const valueStr = repetitionPeriodValue.trim()
const value = parseInt(valueStr, 10)
@@ -482,9 +554,16 @@ function TaskForm({ onNavigate, taskId }) {
const payload = {
name: name.trim(),
reward_message: rewardMessage.trim() || null,
progression_base: progressionBase ? parseFloat(progressionBase) : null,
// Если задача привязана к желанию, не отправляем progression_base
progression_base: isLinkedToWishlist ? null : (progressionBase ? parseFloat(progressionBase) : null),
repetition_period: repetitionPeriod,
repetition_date: repetitionDate,
// При создании: отправляем currentWishlistId если указан (уже число)
// При редактировании: отправляем null только если была привязка (currentWishlistId) и пользователь отвязал (!wishlistInfo)
// Если не было привязки или привязка осталась - не отправляем поле (undefined)
wishlist_id: taskId
? (currentWishlistId && !wishlistInfo ? null : undefined)
: (currentWishlistId || undefined),
rewards: rewards.map(r => ({
position: r.position,
project_name: r.project_name.trim(),
@@ -543,6 +622,13 @@ function TaskForm({ onNavigate, taskId }) {
}
}
const handleUnlinkWishlist = () => {
if (window.confirm('Отвязать задачу от желания?')) {
setCurrentWishlistId(null)
setWishlistInfo(null)
}
}
const handleCancel = () => {
resetForm()
onNavigate?.('tasks')
@@ -608,6 +694,27 @@ function TaskForm({ onNavigate, taskId }) {
/>
</div>
{/* Информация о связанном желании */}
{wishlistInfo && (
<div className="form-group">
<div className="wishlist-link-info">
<span className="wishlist-link-text">
Связана с желанием: <strong>{wishlistInfo.name}</strong>
</span>
{taskId && currentWishlistId && (
<button
type="button"
onClick={handleUnlinkWishlist}
className="wishlist-unlink-x"
title="Отвязать от желания"
>
</button>
)}
</div>
</div>
)}
<div className="form-group">
<label htmlFor="progression_base">Прогрессия</label>
<input
@@ -615,18 +722,24 @@ function TaskForm({ onNavigate, taskId }) {
type="number"
step="any"
value={progressionBase}
onChange={(e) => setProgressionBase(e.target.value)}
onChange={(e) => {
if (!wishlistInfo) {
setProgressionBase(e.target.value)
}
}}
placeholder="Базовое значение"
className="form-input"
disabled={wishlistInfo !== null}
/>
<small style={{ color: '#666', fontSize: '0.9em' }}>
Оставьте пустым, если прогрессия не используется
<small style={{ color: wishlistInfo ? '#e74c3c' : '#666', fontSize: '0.9em' }}>
{wishlistInfo ? 'Задачи, привязанные к желанию, не могут иметь прогрессию' : 'Оставьте пустым, если прогрессия не используется'}
</small>
</div>
<div className="form-group">
<label htmlFor="repetition_period">Повторения</label>
{(() => {
const isLinkedToWishlist = wishlistInfo !== null
const hasValidValue = repetitionPeriodValue && repetitionPeriodValue.trim() !== '' && parseInt(repetitionPeriodValue.trim(), 10) !== 0
const isEachMode = hasValidValue && repetitionMode === 'each'
const isYearType = isEachMode && repetitionPeriodType === 'year'
@@ -634,7 +747,7 @@ function TaskForm({ onNavigate, taskId }) {
return (
<>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{hasValidValue && (
{hasValidValue && !isLinkedToWishlist && (
<select
value={repetitionMode}
onChange={(e) => {
@@ -659,12 +772,17 @@ function TaskForm({ onNavigate, taskId }) {
type={isYearType ? 'text' : 'number'}
min="0"
value={repetitionPeriodValue}
onChange={(e) => setRepetitionPeriodValue(e.target.value)}
onChange={(e) => {
if (!isLinkedToWishlist) {
setRepetitionPeriodValue(e.target.value)
}
}}
placeholder={isYearType ? 'ММ-ДД' : 'Число'}
className="form-input"
style={{ flex: '1' }}
disabled={isLinkedToWishlist}
/>
{hasValidValue && (
{hasValidValue && !isLinkedToWishlist && (
<select
value={repetitionPeriodType}
onChange={(e) => setRepetitionPeriodType(e.target.value)}
@@ -691,7 +809,9 @@ function TaskForm({ onNavigate, taskId }) {
)}
</div>
<small style={{ color: '#666', fontSize: '0.9em' }}>
{isEachMode ? (
{isLinkedToWishlist ? (
<span style={{ color: '#e74c3c' }}>Задачи, привязанные к желанию, не могут быть периодическими</span>
) : isEachMode ? (
repetitionPeriodType === 'week' ? 'Номер дня недели (1-7, где 1 = понедельник)' :
repetitionPeriodType === 'month' ? 'Номер дня месяца (1-31)' :
'Дата в формате ММ-ДД (например, 02-01 для 1 февраля)'

View File

@@ -733,6 +733,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
onClose={handleCloseDetail}
onRefresh={onRefresh}
onTaskCompleted={() => setToast({ message: 'Задача выполнена', type: 'success' })}
onNavigate={onNavigate}
/>
)}

View File

@@ -474,11 +474,6 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
<button className="wishlist-modal-copy" onClick={handleCopy}>
Копировать
</button>
{!selectedItem.completed && selectedItem.unlocked && (
<button className="wishlist-modal-complete" onClick={handleComplete}>
Завершить
</button>
)}
<button className="wishlist-modal-delete" onClick={handleDelete}>
Удалить
</button>

View File

@@ -164,9 +164,10 @@
.wishlist-detail-actions {
display: flex;
flex-direction: column;
flex-direction: row;
gap: 0.75rem;
margin-top: 0.75rem;
align-items: center;
}
.wishlist-detail-edit-button,
@@ -194,6 +195,7 @@
}
.wishlist-detail-complete-button {
flex: 1;
background-color: #27ae60;
color: white;
}
@@ -208,6 +210,86 @@
cursor: not-allowed;
}
.wishlist-detail-create-task-button {
padding: 0.75rem;
background-color: transparent;
color: #27ae60;
border: 2px solid #27ae60;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
min-width: 3rem;
height: 3rem;
}
.wishlist-detail-create-task-button:hover {
background-color: rgba(39, 174, 96, 0.1);
transform: translateY(-1px);
}
.wishlist-detail-linked-task {
margin-top: 0.75rem;
}
.linked-task-label-header {
font-size: 0.9rem;
color: #374151;
font-weight: 500;
margin-bottom: 0.5rem;
}
.wishlist-detail-linked-task .task-item {
margin: 0;
}
.wishlist-detail-linked-task .task-item-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
}
.wishlist-detail-linked-task .task-name-container {
flex: 1;
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
overflow: hidden;
}
.wishlist-detail-linked-task .task-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.wishlist-detail-linked-task .task-unlink-button {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: #9ca3af;
font-size: 1rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
flex-shrink: 0;
}
.wishlist-detail-linked-task .task-unlink-button:hover {
background-color: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.wishlist-detail-uncomplete-button {
background-color: #f39c12;
color: white;

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

View File

@@ -261,7 +261,7 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 1500;
z-index: 1700;
}
.condition-form {

View File

@@ -55,15 +55,23 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) {
loadData()
}, [])
// Загрузка желания при редактировании
// Загрузка желания при редактировании или сброс формы при создании
useEffect(() => {
if (wishlistId !== undefined && wishlistId !== null && tasks.length > 0 && projects.length > 0) {
loadWishlist()
} else if (wishlistId === undefined || wishlistId === null) {
// Сбрасываем форму при создании новой задачи
resetForm()
}
}, [wishlistId, tasks, projects])
// Сброс формы при размонтировании компонента
useEffect(() => {
return () => {
resetForm()
}
}, [])
// Открываем форму редактирования условия, если передан editConditionIndex
useEffect(() => {
if (editConditionIndex !== undefined && editConditionIndex !== null && unlockConditions.length > editConditionIndex) {
@@ -110,6 +118,13 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) {
setImageFile(null)
setUnlockConditions([])
setError('')
setShowCropper(false)
setCrop({ x: 0, y: 0 })
setZoom(1)
setCroppedAreaPixels(null)
setShowConditionForm(false)
setEditingConditionIndex(null)
setToastMessage(null)
}
// Функция для извлечения метаданных из ссылки (по нажатию кнопки)
@@ -381,6 +396,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) {
}
const handleCancel = () => {
resetForm()
onNavigate?.('wishlist')
}