All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
796 lines
32 KiB
JavaScript
796 lines
32 KiB
JavaScript
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||
import { createPortal } from 'react-dom'
|
||
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'
|
||
|
||
function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId, onClose, previousTab }) {
|
||
const { authFetch, user } = useAuth()
|
||
const [wishlistItem, setWishlistItem] = useState(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [loadingWishlist, setLoadingWishlist] = useState(true)
|
||
const [error, setError] = useState(null)
|
||
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 {
|
||
setLoadingWishlist(true)
|
||
setLoading(true)
|
||
setError(null)
|
||
const response = await authFetch(`${API_URL}/${wishlistId}`)
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка загрузки желания')
|
||
}
|
||
const data = await response.json()
|
||
setWishlistItem(data)
|
||
} catch (err) {
|
||
setError(err.message)
|
||
console.error('Error fetching wishlist detail:', err)
|
||
} finally {
|
||
setLoading(false)
|
||
setLoadingWishlist(false)
|
||
}
|
||
}, [wishlistId, authFetch])
|
||
|
||
useEffect(() => {
|
||
if (wishlistId) {
|
||
fetchWishlistDetail()
|
||
} else {
|
||
setWishlistItem(null)
|
||
setLoading(true)
|
||
setLoadingWishlist(true)
|
||
setError(null)
|
||
}
|
||
}, [wishlistId, fetchWishlistDetail])
|
||
|
||
const handleEdit = () => {
|
||
// Сбрасываем флаг, чтобы handleClose не вызвал history.back()
|
||
// handleTabChange заменит запись модального окна через replaceState
|
||
historyPushedForWishlistRef.current = false
|
||
onClose?.()
|
||
onNavigate?.('wishlist-form', { wishlistId: wishlistId, boardId: boardId })
|
||
}
|
||
|
||
const handleComplete = async () => {
|
||
if (!wishlistItem || !wishlistItem.unlocked) return
|
||
|
||
setIsCompleting(true)
|
||
try {
|
||
const response = await authFetch(`${API_URL}/${wishlistId}/complete`, {
|
||
method: 'POST',
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка при завершении')
|
||
}
|
||
|
||
if (onRefresh) {
|
||
onRefresh()
|
||
}
|
||
if (onNavigate) {
|
||
onNavigate('wishlist')
|
||
}
|
||
onClose?.()
|
||
} catch (err) {
|
||
console.error('Error completing wishlist:', err)
|
||
setToastMessage({ text: err.message || 'Ошибка при завершении', type: 'error' })
|
||
} finally {
|
||
setIsCompleting(false)
|
||
}
|
||
}
|
||
|
||
const handleReject = async () => {
|
||
if (!wishlistItem || !wishlistItem.unlocked) return
|
||
|
||
setIsCompleting(true)
|
||
try {
|
||
const response = await authFetch(`${API_URL}/${wishlistId}/reject`, {
|
||
method: 'POST',
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка при отклонении')
|
||
}
|
||
|
||
if (onRefresh) {
|
||
onRefresh()
|
||
}
|
||
if (onNavigate) {
|
||
onNavigate('wishlist')
|
||
}
|
||
onClose?.()
|
||
} catch (err) {
|
||
console.error('Error rejecting wishlist:', err)
|
||
setToastMessage({ text: err.message || 'Ошибка при отклонении', type: 'error' })
|
||
} finally {
|
||
setIsCompleting(false)
|
||
}
|
||
}
|
||
|
||
const handleUncomplete = async () => {
|
||
if (!wishlistItem || !wishlistItem.completed) return
|
||
|
||
setIsCompleting(true)
|
||
try {
|
||
const response = await authFetch(`${API_URL}/${wishlistId}/uncomplete`, {
|
||
method: 'POST',
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка при возобновлении желания')
|
||
}
|
||
|
||
if (onRefresh) {
|
||
onRefresh()
|
||
}
|
||
fetchWishlistDetail()
|
||
onClose?.()
|
||
} catch (err) {
|
||
console.error('Error uncompleting wishlist:', err)
|
||
setToastMessage({ text: err.message || 'Ошибка при возобновлении желания', type: 'error' })
|
||
} finally {
|
||
setIsCompleting(false)
|
||
}
|
||
}
|
||
|
||
const handleDelete = async () => {
|
||
if (!wishlistItem) return
|
||
|
||
if (!window.confirm('Вы уверены, что хотите удалить это желание?')) {
|
||
return
|
||
}
|
||
|
||
setIsDeleting(true)
|
||
try {
|
||
const response = await authFetch(`${API_URL}/${wishlistId}`, {
|
||
method: 'DELETE',
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка при удалении')
|
||
}
|
||
|
||
if (onRefresh) {
|
||
onRefresh()
|
||
}
|
||
if (onNavigate) {
|
||
onNavigate('wishlist')
|
||
}
|
||
} catch (err) {
|
||
console.error('Error deleting wishlist:', err)
|
||
setToastMessage({ text: err.message || 'Ошибка при удалении', type: 'error' })
|
||
} finally {
|
||
setIsDeleting(false)
|
||
}
|
||
}
|
||
|
||
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) {
|
||
setSelectedTaskForDetail(wishlistItem.linked_task.id)
|
||
}
|
||
}
|
||
|
||
const handleConditionTaskClick = async (condition) => {
|
||
if (condition.type !== 'task_completion' || !condition.task_id) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
// Загружаем информацию о задаче
|
||
const response = await authFetch(`/api/tasks/${condition.task_id}`)
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка при загрузке задачи')
|
||
}
|
||
const taskDetail = await response.json()
|
||
|
||
// Проверяем, является ли задача тестом
|
||
const isTest = taskDetail.task?.config_id != null
|
||
|
||
if (isTest) {
|
||
// Для задач-тестов открываем экран прохождения теста
|
||
if (taskDetail.task.config_id) {
|
||
onNavigate?.('test', {
|
||
configId: taskDetail.task.config_id,
|
||
taskId: taskDetail.task.id,
|
||
maxCards: taskDetail.max_cards
|
||
})
|
||
}
|
||
} else {
|
||
// Для обычных задач открываем модальное окно выполнения
|
||
setSelectedTaskForDetail(condition.task_id)
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to load task details:', err)
|
||
setToastMessage({ text: 'Ошибка при загрузке задачи', type: 'error' })
|
||
}
|
||
}
|
||
|
||
const handleCloseDetail = (skipHistoryBack = false) => {
|
||
// Если skipHistoryBack = true (например, при навигации на форму редактирования),
|
||
// закрываем модальные окна без удаления записей из истории
|
||
// App.jsx сам обработает навигацию и заменит запись task-detail на task-form через replaceState
|
||
// Запись wishlist-detail останется в истории, но экран будет закрыт
|
||
if (skipHistoryBack === true) {
|
||
// Сохраняем флаг перед сбросом
|
||
const hadWishlistHistory = historyPushedForWishlistRef.current
|
||
|
||
// Закрываем модальные окна
|
||
historyPushedForTaskRef.current = false
|
||
setSelectedTaskForDetail(null)
|
||
historyPushedForWishlistRef.current = false
|
||
|
||
// Закрываем экран желания через onClose
|
||
// Навигация на task-form уже происходит в TaskDetail, поэтому не вызываем onNavigate здесь
|
||
// App.jsx обработает навигацию и заменит запись task-detail на task-form через replaceState
|
||
if (hadWishlistHistory && onClose) {
|
||
onClose()
|
||
}
|
||
} else if (historyPushedForTaskRef.current) {
|
||
window.history.back()
|
||
} else {
|
||
historyPushedForTaskRef.current = false
|
||
setSelectedTaskForDetail(null)
|
||
}
|
||
}
|
||
|
||
// Добавляем запись в историю при открытии модальных окон и обрабатываем "назад"
|
||
const historyPushedForWishlistRef = useRef(false)
|
||
const historyPushedForTaskRef = useRef(false)
|
||
const wishlistIdRef = useRef(wishlistId)
|
||
const selectedTaskForDetailRef = useRef(selectedTaskForDetail)
|
||
|
||
// Обновляем refs при изменении значений
|
||
useEffect(() => {
|
||
wishlistIdRef.current = wishlistId
|
||
selectedTaskForDetailRef.current = selectedTaskForDetail
|
||
}, [wishlistId, selectedTaskForDetail])
|
||
|
||
useEffect(() => {
|
||
if (wishlistId && !historyPushedForWishlistRef.current) {
|
||
// Добавляем запись в историю при открытии модального окна WishlistDetail
|
||
window.history.pushState({ modalOpen: true, type: 'wishlist-detail' }, '', window.location.href)
|
||
historyPushedForWishlistRef.current = true
|
||
} else if (!wishlistId) {
|
||
historyPushedForWishlistRef.current = false
|
||
}
|
||
|
||
if (selectedTaskForDetail && !historyPushedForTaskRef.current) {
|
||
// Добавляем запись в историю при открытии вложенного модального окна TaskDetail
|
||
window.history.pushState({ modalOpen: true, type: 'task-detail', nested: true }, '', window.location.href)
|
||
historyPushedForTaskRef.current = true
|
||
} else if (!selectedTaskForDetail) {
|
||
historyPushedForTaskRef.current = false
|
||
}
|
||
|
||
if (!wishlistId && !selectedTaskForDetail) return
|
||
|
||
const handlePopState = (event) => {
|
||
// Проверяем наличие модальных окон в DOM
|
||
const taskDetailModal = document.querySelector('.task-detail-modal-overlay')
|
||
const wishlistDetailModal = document.querySelector('.wishlist-detail-modal-overlay')
|
||
|
||
// Используем refs для получения актуального состояния
|
||
const currentTaskDetail = selectedTaskForDetailRef.current
|
||
const currentWishlistId = wishlistIdRef.current
|
||
|
||
|
||
// Сначала проверяем вложенное модальное окно TaskDetail
|
||
if (currentTaskDetail || taskDetailModal) {
|
||
setSelectedTaskForDetail(null)
|
||
historyPushedForTaskRef.current = false
|
||
// Возвращаем запись для WishlistDetail
|
||
if (currentWishlistId || wishlistDetailModal) {
|
||
window.history.pushState({ modalOpen: true, type: 'wishlist-detail' }, '', window.location.href)
|
||
}
|
||
return
|
||
}
|
||
|
||
// Если открыто модальное окно WishlistDetail, закрываем его
|
||
if (currentWishlistId || wishlistDetailModal) {
|
||
if (onClose) {
|
||
onClose()
|
||
} else {
|
||
// Возвращаемся на предыдущий таб, если он был сохранен, иначе на wishlist
|
||
if (previousTab) {
|
||
if (boardId) {
|
||
onNavigate?.(previousTab, { boardId })
|
||
} else {
|
||
onNavigate?.(previousTab)
|
||
}
|
||
} else if (boardId) {
|
||
onNavigate?.('wishlist', { boardId })
|
||
} else {
|
||
onNavigate?.('wishlist')
|
||
}
|
||
}
|
||
historyPushedForWishlistRef.current = false
|
||
// Следующее нажатие "назад" обработается App.jsx нормально
|
||
return
|
||
}
|
||
}
|
||
|
||
window.addEventListener('popstate', handlePopState)
|
||
return () => {
|
||
window.removeEventListener('popstate', handlePopState)
|
||
}
|
||
}, [wishlistId, selectedTaskForDetail, onClose, onNavigate, previousTab, boardId])
|
||
|
||
const handleClose = () => {
|
||
// Если была добавлена запись в историю, удаляем её через history.back()
|
||
// Обработчик popstate закроет модальное окно
|
||
if (historyPushedForWishlistRef.current) {
|
||
window.history.back()
|
||
} else if (onClose) {
|
||
onClose()
|
||
} else {
|
||
// Возвращаемся на предыдущий таб, если он был сохранен, иначе на wishlist
|
||
if (previousTab) {
|
||
// Сохраняем boardId при возврате на предыдущий таб
|
||
if (boardId) {
|
||
onNavigate?.(previousTab, { boardId })
|
||
} else {
|
||
onNavigate?.(previousTab)
|
||
}
|
||
} else if (boardId) {
|
||
onNavigate?.('wishlist', { boardId })
|
||
} else {
|
||
onNavigate?.('wishlist')
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleTaskCompleted = () => {
|
||
setToastMessage({ text: 'Задача выполнена', type: 'success' })
|
||
// После выполнения задачи желание тоже завершается, перенаправляем на список
|
||
if (onRefresh) {
|
||
onRefresh()
|
||
}
|
||
if (onNavigate) {
|
||
onNavigate('wishlist')
|
||
}
|
||
}
|
||
|
||
const handleDeleteTask = async (e) => {
|
||
e.stopPropagation()
|
||
if (!wishlistItem?.linked_task || wishlistItem?.completed) return
|
||
|
||
if (!window.confirm('Удалить задачу, связанную с желанием?')) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
// Удаляем задачу (помечаем как удалённую)
|
||
const deleteResponse = await authFetch(`/api/tasks/${wishlistItem.linked_task.id}`, {
|
||
method: 'DELETE',
|
||
})
|
||
|
||
if (!deleteResponse.ok) {
|
||
const errorData = await deleteResponse.json().catch(() => ({}))
|
||
throw new Error(errorData.message || errorData.error || 'Ошибка при удалении задачи')
|
||
}
|
||
|
||
setToastMessage({ text: 'Задача удалена', type: 'success' })
|
||
// Обновляем данные желания
|
||
fetchWishlistDetail()
|
||
if (onRefresh) {
|
||
onRefresh()
|
||
}
|
||
} catch (err) {
|
||
console.error('Error deleting task:', err)
|
||
setToastMessage({ text: err.message || 'Ошибка при удалении задачи', type: 'error' })
|
||
}
|
||
}
|
||
|
||
|
||
const formatPrice = (price) => {
|
||
return new Intl.NumberFormat('ru-RU', {
|
||
style: 'currency',
|
||
currency: 'RUB',
|
||
minimumFractionDigits: 0,
|
||
maximumFractionDigits: 0,
|
||
}).format(price)
|
||
}
|
||
|
||
const renderUnlockConditions = () => {
|
||
if (!wishlistItem || !wishlistItem.unlock_conditions || wishlistItem.unlock_conditions.length === 0) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<div className="wishlist-detail-conditions">
|
||
<h3 className="wishlist-detail-section-title">Цели:</h3>
|
||
{wishlistItem.unlock_conditions.map((condition, index) => {
|
||
let conditionText = ''
|
||
let progress = null
|
||
|
||
if (condition.type === 'task_completion') {
|
||
conditionText = condition.task_name || 'Задача'
|
||
const isCompleted = condition.task_completed === true
|
||
progress = {
|
||
type: 'task',
|
||
completed: isCompleted
|
||
}
|
||
} else {
|
||
const requiredPoints = condition.required_points || 0
|
||
const currentPoints = condition.current_points || 0
|
||
const project = condition.project_name || 'Проект'
|
||
let dateText = ''
|
||
if (condition.start_date) {
|
||
const date = new Date(condition.start_date + 'T00:00:00')
|
||
dateText = ` с ${date.toLocaleDateString('ru-RU')}`
|
||
} else {
|
||
dateText = ' за всё время'
|
||
}
|
||
conditionText = `${requiredPoints} в ${project}${dateText}`
|
||
const remaining = Math.max(0, requiredPoints - currentPoints)
|
||
progress = {
|
||
type: 'points',
|
||
current: currentPoints,
|
||
required: requiredPoints,
|
||
remaining: remaining,
|
||
percentage: requiredPoints > 0 ? Math.min(100, (currentPoints / requiredPoints) * 100) : 0
|
||
}
|
||
}
|
||
|
||
// Проверяем каждое условие индивидуально
|
||
let isMet = false
|
||
if (progress?.type === 'task') {
|
||
isMet = progress.completed === true
|
||
} else if (progress?.type === 'points') {
|
||
isMet = progress.current >= progress.required
|
||
}
|
||
|
||
return (
|
||
<div
|
||
key={index}
|
||
className={`wishlist-detail-condition ${isMet ? 'met' : 'not-met'} ${condition.type === 'task_completion' && condition.task_id ? 'clickable' : ''}`}
|
||
onClick={condition.type === 'task_completion' && condition.task_id ? () => handleConditionTaskClick(condition) : undefined}
|
||
style={condition.type === 'task_completion' && condition.task_id ? { cursor: 'pointer' } : {}}
|
||
>
|
||
<div className="condition-header">
|
||
<svg className="condition-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||
{isMet ? (
|
||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||
) : (
|
||
<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>
|
||
<span className="condition-text">{conditionText}</span>
|
||
</div>
|
||
{/* Показываем дату для целей-задач, если next_show_at > сегодня */}
|
||
{condition.type === 'task_completion' && condition.task_next_show_at && (() => {
|
||
const showDate = new Date(condition.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="condition-task-date">
|
||
{dateText}
|
||
</div>
|
||
)
|
||
})()}
|
||
{progress && progress.type === 'points' && !isMet && (
|
||
<div className="condition-progress">
|
||
<div className="progress-bar">
|
||
<div
|
||
className="progress-fill"
|
||
style={{ width: `${progress.percentage}%` }}
|
||
></div>
|
||
</div>
|
||
<div className="progress-text">
|
||
<span>{Math.round(progress.current)} / {Math.round(progress.required)}</span>
|
||
{progress.remaining > 0 && (
|
||
<span className="progress-remaining">
|
||
Осталось: {Math.round(progress.remaining)}
|
||
{condition.weeks_text && ` (${condition.weeks_text})`}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
const modalContent = (
|
||
<div className="wishlist-detail-modal-overlay" onClick={handleClose}>
|
||
<div className="wishlist-detail-modal" onClick={(e) => e.stopPropagation()}>
|
||
<div className="wishlist-detail-modal-header">
|
||
<h2
|
||
className="wishlist-detail-title"
|
||
onClick={wishlistItem ? handleEdit : undefined}
|
||
style={{ cursor: wishlistItem ? 'pointer' : 'default' }}
|
||
>
|
||
{loadingWishlist ? 'Загрузка...' : error ? 'Ошибка' : wishlistItem ? wishlistItem.name : 'Желание'}
|
||
{wishlistItem && (
|
||
<svg
|
||
className="wishlist-detail-edit-icon"
|
||
width="18"
|
||
height="18"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
>
|
||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
||
</svg>
|
||
)}
|
||
</h2>
|
||
<button onClick={handleClose} className="wishlist-detail-close-button">
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div className="wishlist-detail-modal-content">
|
||
{loadingWishlist && (
|
||
<div className="flex justify-center items-center py-8">
|
||
<div className="flex flex-col items-center">
|
||
<div className="w-12 h-12 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mb-4"></div>
|
||
<div className="text-gray-600 font-medium">Загрузка...</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{error && !loadingWishlist && (
|
||
<LoadingError onRetry={fetchWishlistDetail} />
|
||
)}
|
||
|
||
{!loadingWishlist && !error && wishlistItem && (
|
||
<>
|
||
{/* Изображение */}
|
||
{wishlistItem.image_url && (
|
||
<div className="wishlist-detail-image">
|
||
<img src={wishlistItem.image_url} alt={wishlistItem.name} />
|
||
</div>
|
||
)}
|
||
|
||
{/* Цена */}
|
||
{wishlistItem.price && (
|
||
<div className="wishlist-detail-price">
|
||
{formatPrice(wishlistItem.price)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Ссылка */}
|
||
{wishlistItem.link && (() => {
|
||
try {
|
||
const url = new URL(wishlistItem.link)
|
||
const host = url.host.replace(/^www\./, '') // Убираем www. если есть
|
||
return (
|
||
<div className="wishlist-detail-link">
|
||
<a href={wishlistItem.link} target="_blank" rel="noopener noreferrer">
|
||
{host}
|
||
</a>
|
||
</div>
|
||
)
|
||
} catch {
|
||
// Если URL некорректный, показываем оригинальный текст
|
||
return (
|
||
<div className="wishlist-detail-link">
|
||
<a href={wishlistItem.link} target="_blank" rel="noopener noreferrer">
|
||
Открыть ссылку
|
||
</a>
|
||
</div>
|
||
)
|
||
}
|
||
})()}
|
||
|
||
{/* Условия разблокировки */}
|
||
{renderUnlockConditions()}
|
||
|
||
{/* Связанная задача или кнопки действий */}
|
||
{wishlistItem.unlocked && (
|
||
<>
|
||
{wishlistItem.linked_task && wishlistItem.linked_task.user_id === user?.id ? (
|
||
<div className="wishlist-detail-linked-task">
|
||
<div style={{ position: 'relative', display: 'inline-block', width: '100%' }}>
|
||
<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>
|
||
{wishlistItem && !wishlistItem.completed && (
|
||
<div className="task-actions">
|
||
<button
|
||
className="task-delete-button"
|
||
onClick={handleDeleteTask}
|
||
title="Удалить задачу"
|
||
>
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="M3 6h18"></path>
|
||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
|
||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
|
||
<line x1="10" y1="11" x2="10" y2="17"></line>
|
||
<line x1="14" y1="11" x2="14" y2="17"></line>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
{wishlistItem?.tasks_count > 0 && (
|
||
<div className="wishlist-detail-tasks-badge">
|
||
{wishlistItem.tasks_count}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="wishlist-detail-actions">
|
||
{wishlistItem.completed ? (
|
||
<button
|
||
onClick={handleUncomplete}
|
||
disabled={isCompleting}
|
||
className="wishlist-detail-uncomplete-button"
|
||
>
|
||
{isCompleting ? 'Возобновление...' : 'Возобновить'}
|
||
</button>
|
||
) : (
|
||
<>
|
||
<div className="wishlist-detail-action-buttons">
|
||
<button
|
||
onClick={handleComplete}
|
||
disabled={isCompleting}
|
||
className="wishlist-detail-complete-button"
|
||
>
|
||
{isCompleting ? '...' : 'Завершить'}
|
||
</button>
|
||
<button
|
||
onClick={handleReject}
|
||
disabled={isCompleting}
|
||
className="wishlist-detail-reject-button"
|
||
>
|
||
{isCompleting ? '...' : 'Отклонить'}
|
||
</button>
|
||
</div>
|
||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||
<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>
|
||
{wishlistItem?.tasks_count > 0 && (
|
||
<div className="wishlist-detail-tasks-badge">
|
||
{wishlistItem.tasks_count}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{toastMessage && (
|
||
<Toast
|
||
message={toastMessage.text}
|
||
type={toastMessage.type}
|
||
onClose={() => setToastMessage(null)}
|
||
/>
|
||
)}
|
||
|
||
{/* Модальное окно для деталей задачи */}
|
||
{selectedTaskForDetail && (
|
||
<TaskDetail
|
||
taskId={selectedTaskForDetail}
|
||
onClose={handleCloseDetail}
|
||
onRefresh={() => {
|
||
fetchWishlistDetail()
|
||
if (onRefresh) onRefresh()
|
||
}}
|
||
onTaskCompleted={handleTaskCompleted}
|
||
onNavigate={onNavigate}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
return typeof document !== 'undefined'
|
||
? createPortal(modalContent, document.body)
|
||
: modalContent
|
||
}
|
||
|
||
export default WishlistDetail
|
||
|