import React, { useState, useEffect, useRef, useMemo } from 'react' import { useAuth } from './auth/AuthContext' import BoardSelector from './BoardSelector' import LoadingError from './LoadingError' import WishlistDetail from './WishlistDetail' import { sortProjectsLikeCurrentWeek } from '../utils/projectUtils' import './Wishlist.css' const API_URL = '/api/wishlist' const BOARDS_CACHE_KEY = 'wishlist_boards_cache' const ITEMS_CACHE_KEY = 'wishlist_items_cache' const SELECTED_BOARD_KEY = 'wishlist_selected_board_id' function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoardId = null, boardDeleted = false }) { const { authFetch } = useAuth() const [boards, setBoards] = useState([]) // Восстанавливаем выбранную доску из localStorage или используем initialBoardId const getInitialBoardId = () => { if (initialBoardId) return initialBoardId return getSavedBoardId() } // Получает сохранённую доску из localStorage const getSavedBoardId = () => { try { const saved = localStorage.getItem(SELECTED_BOARD_KEY) if (saved) { const boardId = parseInt(saved, 10) if (!isNaN(boardId)) return boardId } } catch (err) { console.error('Error loading selected board from cache:', err) } return null } const [selectedBoardId, setSelectedBoardIdState] = useState(getInitialBoardId) const [items, setItems] = useState([]) const [completed, setCompleted] = useState([]) const [completedCount, setCompletedCount] = useState(0) const [loading, setLoading] = useState(true) const [boardsLoading, setBoardsLoading] = useState(true) const [error, setError] = useState('') const [completedExpanded, setCompletedExpanded] = useState(false) const [completedLoading, setCompletedLoading] = useState(false) const [selectedItem, setSelectedItem] = useState(null) const [selectedWishlistForDetail, setSelectedWishlistForDetail] = useState(null) const [currentWeekData, setCurrentWeekData] = useState(null) const fetchingRef = useRef(false) const fetchingCompletedRef = useRef(false) const initialFetchDoneRef = useRef(false) const prevIsActiveRef = useRef(isActive) // Обёртка для setSelectedBoardId с сохранением в localStorage const setSelectedBoardId = (boardId) => { setSelectedBoardIdState(boardId) try { if (boardId) { localStorage.setItem(SELECTED_BOARD_KEY, String(boardId)) } else { localStorage.removeItem(SELECTED_BOARD_KEY) } } catch (err) { console.error('Error saving selected board to cache:', err) } } // Загрузка досок из кэша const loadBoardsFromCache = () => { try { const cached = localStorage.getItem(BOARDS_CACHE_KEY) if (cached) { const data = JSON.parse(cached) setBoards(data.boards || []) // Проверяем, что сохранённая доска существует в списке if (selectedBoardId) { const boardExists = data.boards?.some(b => b.id === selectedBoardId) if (!boardExists && data.boards?.length > 0) { setSelectedBoardId(data.boards[0].id) } } else if (data.boards?.length > 0) { // Пытаемся восстановить из localStorage const savedBoardId = getSavedBoardId() if (savedBoardId && data.boards.some(b => b.id === savedBoardId)) { setSelectedBoardId(savedBoardId) } else { setSelectedBoardId(data.boards[0].id) } } return true } } catch (err) { console.error('Error loading boards from cache:', err) } return false } // Сохранение досок в кэш const saveBoardsToCache = (boardsData) => { try { localStorage.setItem(BOARDS_CACHE_KEY, JSON.stringify({ boards: boardsData, timestamp: Date.now() })) } catch (err) { console.error('Error saving boards to cache:', err) } } // Загрузка желаний из кэша (по board_id) const loadItemsFromCache = (boardId) => { try { const cached = localStorage.getItem(`${ITEMS_CACHE_KEY}_${boardId}`) if (cached) { const data = JSON.parse(cached) setItems(data.items || []) setCompletedCount(data.completedCount || 0) return true } } catch (err) { console.error('Error loading items from cache:', err) } return false } // Сохранение желаний в кэш const saveItemsToCache = (boardId, itemsData, count) => { try { localStorage.setItem(`${ITEMS_CACHE_KEY}_${boardId}`, JSON.stringify({ items: itemsData, completedCount: count, timestamp: Date.now() })) } catch (err) { console.error('Error saving items to cache:', err) } } // Загрузка списка досок const fetchBoards = async () => { try { const response = await authFetch(`${API_URL}/boards`) if (response.ok) { const data = await response.json() setBoards(data || []) saveBoardsToCache(data || []) // Проверяем, что выбранная доска существует в списке if (selectedBoardId) { const boardExists = data?.some(b => b.id === selectedBoardId) if (!boardExists && data?.length > 0) { // Сохранённая доска не существует, выбираем первую setSelectedBoardId(data[0].id) } } else if (data?.length > 0) { // Пытаемся восстановить из localStorage const savedBoardId = getSavedBoardId() if (savedBoardId && data.some(b => b.id === savedBoardId)) { setSelectedBoardId(savedBoardId) } else { setSelectedBoardId(data[0].id) } } } } catch (err) { console.error('Error fetching boards:', err) } finally { setBoardsLoading(false) } } // Загрузка желаний выбранной доски const fetchItems = async () => { if (!selectedBoardId || fetchingRef.current) return fetchingRef.current = true try { const hasDataInState = items.length > 0 || completedCount > 0 if (!hasDataInState) { const cacheLoaded = loadItemsFromCache(selectedBoardId) if (!cacheLoaded) { setLoading(true) } } const response = await authFetch(`${API_URL}/boards/${selectedBoardId}/items`) if (!response.ok) { throw new Error('Ошибка при загрузке желаний') } const data = await response.json() const allItems = [...(data.unlocked || []), ...(data.locked || [])] const count = data.completed_count || 0 setItems(allItems) setCompletedCount(count) saveItemsToCache(selectedBoardId, allItems, count) setError('') } catch (err) { setError(err.message) if (!loadItemsFromCache(selectedBoardId)) { setItems([]) setCompletedCount(0) } } finally { setLoading(false) fetchingRef.current = false } } // Загрузка завершённых для текущей доски const fetchCompleted = async () => { if (fetchingCompletedRef.current || !selectedBoardId) return fetchingCompletedRef.current = true try { setCompletedLoading(true) // Используем новый API для получения завершённых на доске const response = await authFetch(`${API_URL}/boards/${selectedBoardId}/completed`) if (!response.ok) { const errText = await response.text() const msg = errText || 'Ошибка при загрузке завершённых желаний' throw new Error(msg) } const data = await response.json() const completedData = Array.isArray(data) ? data : [] setCompleted(completedData) } catch (err) { console.error('Error fetching completed items:', err) setCompleted([]) } finally { setCompletedLoading(false) fetchingCompletedRef.current = false } } // Загрузка данных текущей недели для сортировки проектов const fetchCurrentWeek = async () => { try { const response = await authFetch('/api/current-week') if (response.ok) { const data = await response.json() // Обрабатываем ответ: приходит массив с одним объектом [{total: ..., projects: [...]}] if (Array.isArray(data) && data.length > 0) { setCurrentWeekData(data[0]) } else if (data && typeof data === 'object') { setCurrentWeekData(data) } } } catch (err) { console.error('Error loading current week data:', err) } } // Первая инициализация useEffect(() => { if (!initialFetchDoneRef.current) { initialFetchDoneRef.current = true // Загружаем доски из кэша const boardsCacheLoaded = loadBoardsFromCache() if (boardsCacheLoaded) { setBoardsLoading(false) } // Загружаем доски с сервера fetchBoards() // Загружаем данные текущей недели для сортировки проектов fetchCurrentWeek() } }, []) // Загружаем желания при смене доски useEffect(() => { if (selectedBoardId) { // Сбрасываем состояние setItems([]) setCompletedCount(0) setCompleted([]) setCompletedExpanded(false) setLoading(true) // Пробуем загрузить из кэша const cacheLoaded = loadItemsFromCache(selectedBoardId) if (cacheLoaded) { setLoading(false) } // Загружаем свежие данные fetchItems() } }, [selectedBoardId]) // Обновление при активации таба useEffect(() => { const wasActive = prevIsActiveRef.current prevIsActiveRef.current = isActive if (!initialFetchDoneRef.current) return if (isActive && !wasActive) { fetchBoards() if (selectedBoardId) { fetchItems() } } }, [isActive]) // Обновление при refreshTrigger useEffect(() => { if (refreshTrigger > 0 && selectedBoardId) { // Очищаем кэш для текущей доски, чтобы загрузить свежие данные try { localStorage.removeItem(`${ITEMS_CACHE_KEY}_${selectedBoardId}`) } catch (err) { console.error('Error clearing cache:', err) } fetchBoards() fetchItems() if (completedExpanded && completedCount > 0) { fetchCompleted() } } }, [refreshTrigger, selectedBoardId]) // Обновление при initialBoardId (когда создана новая доска или переход по ссылке) useEffect(() => { if (initialBoardId && initialBoardId !== selectedBoardId) { // Сбрасываем флаг загрузки, чтобы не блокировать новую загрузку fetchingRef.current = false // Обновляем список досок (чтобы новая доска появилась) fetchBoards().then(() => { // Переключаемся на новую доску после обновления списка // Это вызовет useEffect для selectedBoardId, который загрузит данные setSelectedBoardId(initialBoardId) }) } }, [initialBoardId]) // Обработка удаления доски - выбираем первую доступную useEffect(() => { if (boardDeleted && boards.length > 0) { // Очищаем текущие данные setItems([]) setCompletedCount(0) setCompleted([]) setCompletedExpanded(false) setLoading(true) // Обновляем список досок и выбираем первую fetchBoards().then(() => { // fetchBoards обновит boards, но мы уже в этом useEffect // selectedBoardId обновится автоматически в useEffect ниже }) } }, [boardDeleted]) // Если текущая доска больше не существует в списке - выбираем первую useEffect(() => { if (boards.length > 0 && selectedBoardId) { const boardExists = boards.some(b => b.id === selectedBoardId) if (!boardExists) { setSelectedBoardId(boards[0].id) } } }, [boards, selectedBoardId]) const handleBoardChange = (boardId) => { setSelectedBoardId(boardId) } const handleBoardEdit = () => { const board = boards.find(b => b.id === selectedBoardId) if (board?.is_owner) { onNavigate?.('board-form', { boardId: selectedBoardId }) } else { // Показать подтверждение выхода handleLeaveBoard() } } const handleLeaveBoard = async () => { if (!window.confirm('Отвязаться от этой доски? Вы больше не будете видеть её желания.')) return try { const response = await authFetch(`${API_URL}/boards/${selectedBoardId}/leave`, { method: 'POST' }) if (response.ok) { // Убираем доску из списка const newBoards = boards.filter(b => b.id !== selectedBoardId) setBoards(newBoards) saveBoardsToCache(newBoards) // Выбираем первую доску if (newBoards.length > 0) { setSelectedBoardId(newBoards[0].id) } else { setSelectedBoardId(null) setItems([]) } } } catch (err) { console.error('Error leaving board:', err) } } const handleAddBoard = () => { onNavigate?.('board-form', { boardId: null }) } const handleToggleCompleted = () => { const newExpanded = !completedExpanded setCompletedExpanded(newExpanded) if (newExpanded && completedCount > 0) { fetchCompleted() } } const handleAddClick = () => { // Если selectedBoardId равен null, но есть доски, используем первую доску // Если доски еще не загружены, используем initialBoardId const boardIdToUse = selectedBoardId || (boards.length > 0 ? boards[0].id : initialBoardId) onNavigate?.('wishlist-form', { wishlistId: undefined, boardId: boardIdToUse }) } const handleItemClick = (item) => { setSelectedWishlistForDetail(item.id) } const handleCloseDetail = () => { setSelectedWishlistForDetail(null) } const handleMenuClick = (item, e) => { e.stopPropagation() setSelectedItem(item) } const handleEdit = () => { if (selectedItem) { onNavigate?.('wishlist-form', { wishlistId: selectedItem.id, boardId: selectedBoardId }) setSelectedItem(null) } } const handleDelete = async () => { if (!selectedItem) return try { const response = await authFetch(`${API_URL}/${selectedItem.id}`, { method: 'DELETE', }) if (!response.ok) { throw new Error('Ошибка при удалении') } setSelectedItem(null) await fetchItems() if (completedExpanded) { await fetchCompleted() } } catch (err) { setError(err.message) setSelectedItem(null) } } const handleCopy = async () => { if (!selectedItem) return try { const response = await authFetch(`${API_URL}/${selectedItem.id}/copy`, { method: 'POST', }) if (!response.ok) { const errorText = await response.text().catch(() => '') throw new Error(errorText || 'Ошибка при копировании') } const newItem = await response.json() setSelectedItem(null) // Очищаем кэш для текущей доски, чтобы новое желание появилось в списке if (selectedBoardId) { try { localStorage.removeItem(`${ITEMS_CACHE_KEY}_${selectedBoardId}`) } catch (err) { console.error('Error clearing cache:', err) } } // Обновляем список await fetchItems() // Открываем форму редактирования для нового желания onNavigate?.('wishlist-form', { wishlistId: newItem.id, boardId: selectedBoardId }) } catch (err) { console.error('Error copying wishlist item:', err) setError(err.message || 'Ошибка при копировании') setSelectedItem(null) } } const formatPrice = (price) => { return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(price) } const findFirstUnmetCondition = (item) => { if (!item.unlock_conditions || item.unlock_conditions.length === 0) { return null } for (const condition of item.unlock_conditions) { let isMet = false if (condition.type === 'task_completion') { isMet = condition.task_completed === true } else if (condition.type === 'project_points') { const currentPoints = condition.current_points || 0 const requiredPoints = condition.required_points || 0 isMet = currentPoints >= requiredPoints } if (!isMet) { return condition } } return null } const renderUnlockCondition = (item) => { if (item.completed) return null const condition = findFirstUnmetCondition(item) if (!condition) return null let conditionText = '' if (condition.type === 'task_completion') { conditionText = condition.task_name || 'Задача' } else { const requiredPoints = condition.required_points || 0 const currentPoints = condition.current_points || 0 const remainingPoints = Math.max(0, requiredPoints - currentPoints) const project = condition.project_name || 'Проект' // Показываем оставшиеся баллы в формате "33 в Привлекательность" // Дата начала отсчёта уже учтена в current_points на бэкенде conditionText = `${Math.round(remainingPoints)} в ${project}` } return (