diff --git a/VERSION b/VERSION index 0104088..a6d9ada 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.14.4 +3.14.5 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index b455266..372229c 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -9976,6 +9976,45 @@ func (a *App) createWishlistHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(createdItem) } +// checkWishlistAccess проверяет доступ пользователя к желанию +// Возвращает (hasAccess, itemUserID, boardID, error) +func (a *App) checkWishlistAccess(itemID int, userID int) (bool, int, sql.NullInt64, error) { + var itemUserID int + var boardID sql.NullInt64 + err := a.DB.QueryRow(` + SELECT user_id, board_id + FROM wishlist_items + WHERE id = $1 AND deleted = FALSE + `, itemID).Scan(&itemUserID, &boardID) + + if err == sql.ErrNoRows { + return false, 0, sql.NullInt64{}, err + } + if err != nil { + return false, 0, sql.NullInt64{}, err + } + + // Проверяем доступ: владелец ИЛИ участник доски + hasAccess := itemUserID == userID + if !hasAccess && boardID.Valid { + var ownerID int + err = a.DB.QueryRow(`SELECT owner_id FROM wishlist_boards WHERE id = $1 AND deleted = FALSE`, boardID.Int64).Scan(&ownerID) + if err == nil { + hasAccess = ownerID == userID + if !hasAccess { + var isMember bool + err = a.DB.QueryRow(`SELECT EXISTS(SELECT 1 FROM wishlist_board_members WHERE board_id = $1 AND user_id = $2)`, + int(boardID.Int64), userID).Scan(&isMember) + if err == nil { + hasAccess = isMember + } + } + } + } + + return hasAccess, itemUserID, boardID, nil +} + // getWishlistItemHandler возвращает одно желание func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "OPTIONS" { @@ -9999,44 +10038,27 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) { } // Проверяем доступ к желанию - var itemUserID int - var boardID sql.NullInt64 - err = a.DB.QueryRow(` - SELECT user_id, board_id - FROM wishlist_items - WHERE id = $1 AND deleted = FALSE - `, itemID).Scan(&itemUserID, &boardID) - + hasAccess, itemUserID, boardID, err := a.checkWishlistAccess(itemID, userID) if err == sql.ErrNoRows { + log.Printf("Wishlist item not found: id=%d, userID=%d", itemID, userID) sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound) return } if err != nil { - log.Printf("Error getting wishlist item: %v", err) + log.Printf("Error getting wishlist item (id=%d, userID=%d): %v", itemID, userID, err) sendErrorWithCORS(w, "Error getting wishlist item", http.StatusInternalServerError) return } - - // Проверяем доступ: владелец ИЛИ участник доски - hasAccess := itemUserID == userID - if !hasAccess && boardID.Valid { - var ownerID int - err = a.DB.QueryRow(`SELECT owner_id FROM wishlist_boards WHERE id = $1 AND deleted = FALSE`, boardID.Int64).Scan(&ownerID) - if err == nil { - hasAccess = ownerID == userID - if !hasAccess { - var isMember bool - a.DB.QueryRow(`SELECT EXISTS(SELECT 1 FROM wishlist_board_members WHERE board_id = $1 AND user_id = $2)`, - int(boardID.Int64), userID).Scan(&isMember) - hasAccess = isMember - } - } - } + + log.Printf("Wishlist item found: id=%d, itemUserID=%d, boardID=%v, currentUserID=%d", itemID, itemUserID, boardID, userID) if !hasAccess { + log.Printf("Access denied for wishlist item: id=%d, itemUserID=%d, boardID=%v, currentUserID=%d", itemID, itemUserID, boardID, userID) sendErrorWithCORS(w, "Access denied", http.StatusForbidden) return } + + log.Printf("Access granted for wishlist item: id=%d, itemUserID=%d, boardID=%v, currentUserID=%d", itemID, itemUserID, boardID, userID) // Сохраняем itemUserID для использования в качестве fallback, если conditionUserID NULL itemOwnerID := itemUserID @@ -10262,8 +10284,11 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) { } setCORSHeaders(w) + log.Printf("updateWishlistHandler called: method=%s, path=%s", r.Method, r.URL.Path) + userID, ok := getUserIDFromContext(r) if !ok { + log.Printf("updateWishlistHandler: Unauthorized") sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) return } @@ -10271,26 +10296,34 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) itemID, err := strconv.Atoi(vars["id"]) if err != nil { + log.Printf("updateWishlistHandler: Invalid wishlist ID: %v", err) sendErrorWithCORS(w, "Invalid wishlist ID", http.StatusBadRequest) return } + + log.Printf("updateWishlistHandler: itemID=%d, userID=%d", itemID, userID) - // Проверяем владельца - var ownerID int - err = a.DB.QueryRow(` - SELECT user_id FROM wishlist_items - WHERE id = $1 AND deleted = FALSE - `, itemID).Scan(&ownerID) - if err == sql.ErrNoRows || ownerID != userID { + // Проверяем доступ к желанию + hasAccess, _, _, err := a.checkWishlistAccess(itemID, userID) + if err == sql.ErrNoRows { + log.Printf("updateWishlistHandler: Wishlist item not found: id=%d, userID=%d", itemID, userID) sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound) return } if err != nil { - log.Printf("Error checking wishlist ownership: %v", err) - sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist ownership: %v", err), http.StatusInternalServerError) + log.Printf("updateWishlistHandler: Error getting wishlist item (id=%d, userID=%d): %v", itemID, userID, err) + sendErrorWithCORS(w, "Error getting wishlist item", http.StatusInternalServerError) return } + if !hasAccess { + log.Printf("updateWishlistHandler: Access denied: id=%d, userID=%d", itemID, userID) + sendErrorWithCORS(w, "Access denied", http.StatusForbidden) + return + } + + log.Printf("updateWishlistHandler: Access granted: id=%d, userID=%d", itemID, userID) + var req WishlistRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Printf("Error decoding wishlist request: %v", err) @@ -10311,11 +10344,12 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) { } defer tx.Rollback() + // Обновляем желание (не проверяем user_id в WHERE, так как доступ уже проверен выше) _, err = tx.Exec(` UPDATE wishlist_items SET name = $1, price = $2, link = $3, updated_at = NOW() - WHERE id = $4 AND user_id = $5 - `, strings.TrimSpace(req.Name), req.Price, req.Link, itemID, userID) + WHERE id = $4 + `, strings.TrimSpace(req.Name), req.Price, req.Link, itemID) if err != nil { log.Printf("Error updating wishlist item: %v", err) @@ -10337,27 +10371,189 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) { return } - // Получаем обновлённое желание - items, err := a.getWishlistItemsWithConditions(userID, true) + // Получаем обновлённое желание через getWishlistItemHandler логику + // Используем тот же запрос, что и в getWishlistItemHandler + query := ` + SELECT + wi.id, + wi.name, + wi.price, + wi.image_path, + wi.link, + wi.completed, + wc.id AS condition_id, + wc.display_order, + wc.task_condition_id, + wc.score_condition_id, + wc.user_id AS condition_user_id, + tc.task_id, + t.name AS task_name, + sc.project_id, + p.name AS project_name, + sc.required_points, + sc.start_date + FROM wishlist_items wi + LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id + LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id + LEFT JOIN tasks t ON tc.task_id = t.id + LEFT JOIN score_conditions sc ON wc.score_condition_id = sc.id + LEFT JOIN projects p ON sc.project_id = p.id + WHERE wi.id = $1 + AND wi.deleted = FALSE + ORDER BY wc.display_order, wc.id + ` + + rows, err := a.DB.Query(query, itemID) if err != nil { - log.Printf("Error getting updated wishlist item: %v", err) - sendErrorWithCORS(w, fmt.Sprintf("Error getting updated wishlist item: %v", err), http.StatusInternalServerError) + log.Printf("Error querying updated wishlist item: %v", err) + sendErrorWithCORS(w, "Error getting updated wishlist item", http.StatusInternalServerError) return } + defer rows.Close() + + itemsMap := make(map[int]*WishlistItem) + var itemOwnerID int + for rows.Next() { + var itemID int + var name string + var price sql.NullFloat64 + var imagePath sql.NullString + var link sql.NullString + var completed bool + var conditionID sql.NullInt64 + var displayOrder sql.NullInt64 + var taskConditionID sql.NullInt64 + var scoreConditionID sql.NullInt64 + var conditionUserID sql.NullInt64 + var taskID sql.NullInt64 + var taskName sql.NullString + var projectID sql.NullInt64 + var projectName sql.NullString + var requiredPoints sql.NullFloat64 + var startDate sql.NullTime + + err := rows.Scan( + &itemID, &name, &price, &imagePath, &link, &completed, + &conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID, + &taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate, + ) + if err != nil { + log.Printf("Error scanning updated wishlist item: %v", err) + continue + } + + item, exists := itemsMap[itemID] + if !exists { + // Получаем user_id для этого желания + err = a.DB.QueryRow(`SELECT user_id FROM wishlist_items WHERE id = $1`, itemID).Scan(&itemOwnerID) + if err != nil { + log.Printf("Error getting item owner: %v", err) + continue + } + + item = &WishlistItem{ + ID: itemID, + Name: name, + Completed: completed, + UnlockConditions: []UnlockConditionDisplay{}, + } + if price.Valid { + item.Price = &price.Float64 + } + if imagePath.Valid && imagePath.String != "" { + url := imagePath.String + if !strings.HasPrefix(url, "http") { + url = url + "?t=" + strconv.FormatInt(time.Now().Unix(), 10) + } + item.ImageURL = &url + } + if link.Valid { + item.Link = &link.String + } + itemsMap[itemID] = item + } + + if conditionID.Valid { + condition := UnlockConditionDisplay{ + ID: int(conditionID.Int64), + DisplayOrder: int(displayOrder.Int64), + } + + conditionOwnerID := itemOwnerID + if conditionUserID.Valid { + conditionOwnerID = int(conditionUserID.Int64) + } + + if taskConditionID.Valid { + condition.Type = "task_completion" + if taskName.Valid { + condition.TaskName = &taskName.String + } + if taskID.Valid { + var taskCompleted int + a.DB.QueryRow(`SELECT completed FROM tasks WHERE id = $1 AND user_id = $2`, taskID.Int64, conditionOwnerID).Scan(&taskCompleted) + isCompleted := taskCompleted > 0 + condition.TaskCompleted = &isCompleted + } + } else if scoreConditionID.Valid { + condition.Type = "project_points" + if projectName.Valid { + condition.ProjectName = &projectName.String + } + if requiredPoints.Valid { + condition.RequiredPoints = &requiredPoints.Float64 + } + if startDate.Valid { + dateStr := startDate.Time.Format("2006-01-02") + condition.StartDate = &dateStr + } + if projectID.Valid { + points, _ := a.calculateProjectPointsFromDate(int(projectID.Int64), startDate, conditionOwnerID) + condition.CurrentPoints = &points + } + } + + item.UnlockConditions = append(item.UnlockConditions, condition) + } + } var updatedItem *WishlistItem - for i := range items { - if items[i].ID == itemID { - updatedItem = &items[i] + for _, it := range itemsMap { + if it.ID == itemID { + updatedItem = it break } } if updatedItem == nil { + log.Printf("Updated item not found: id=%d", itemID) sendErrorWithCORS(w, "Updated item not found", http.StatusInternalServerError) return } + // Проверяем разблокировку + updatedItem.Unlocked = true + if len(updatedItem.UnlockConditions) > 0 { + for _, cond := range updatedItem.UnlockConditions { + if cond.Type == "task_completion" { + if cond.TaskCompleted == nil || !*cond.TaskCompleted { + updatedItem.Unlocked = false + break + } + } else if cond.Type == "project_points" { + if cond.CurrentPoints == nil || cond.RequiredPoints == nil || *cond.CurrentPoints < *cond.RequiredPoints { + updatedItem.Unlocked = false + break + } + } + } + } + + unlocked, err := a.checkWishlistUnlock(itemID, userID) + if err == nil { + updatedItem.Unlocked = unlocked + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(updatedItem) } @@ -10384,27 +10580,27 @@ func (a *App) deleteWishlistHandler(w http.ResponseWriter, r *http.Request) { return } - // Проверяем владельца - var ownerID int - err = a.DB.QueryRow(` - SELECT user_id FROM wishlist_items - WHERE id = $1 AND deleted = FALSE - `, itemID).Scan(&ownerID) - if err == sql.ErrNoRows || ownerID != userID { + // Проверяем доступ к желанию + hasAccess, _, _, err := a.checkWishlistAccess(itemID, userID) + if err == sql.ErrNoRows { sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound) return } if err != nil { - log.Printf("Error checking wishlist ownership: %v", err) - sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist ownership: %v", err), http.StatusInternalServerError) + log.Printf("Error checking wishlist access: %v", err) + sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist access: %v", err), http.StatusInternalServerError) + return + } + if !hasAccess { + sendErrorWithCORS(w, "Access denied", http.StatusForbidden) return } _, err = a.DB.Exec(` UPDATE wishlist_items SET deleted = TRUE, updated_at = NOW() - WHERE id = $1 AND user_id = $2 - `, itemID, userID) + WHERE id = $1 + `, itemID) if err != nil { log.Printf("Error deleting wishlist item: %v", err) @@ -10441,19 +10637,19 @@ func (a *App) uploadWishlistImageHandler(w http.ResponseWriter, r *http.Request) return } - // Проверяем владельца - var ownerID int - err = a.DB.QueryRow(` - SELECT user_id FROM wishlist_items - WHERE id = $1 AND deleted = FALSE - `, wishlistID).Scan(&ownerID) - if err == sql.ErrNoRows || ownerID != userID { + // Проверяем доступ к желанию + hasAccess, _, _, err := a.checkWishlistAccess(wishlistID, userID) + if err == sql.ErrNoRows { sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound) return } if err != nil { - log.Printf("Error checking wishlist ownership: %v", err) - sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist ownership: %v", err), http.StatusInternalServerError) + log.Printf("Error checking wishlist access: %v", err) + sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist access: %v", err), http.StatusInternalServerError) + return + } + if !hasAccess { + sendErrorWithCORS(w, "Access denied", http.StatusForbidden) return } @@ -10519,8 +10715,8 @@ func (a *App) uploadWishlistImageHandler(w http.ResponseWriter, r *http.Request) _, err = a.DB.Exec(` UPDATE wishlist_items SET image_path = $1, updated_at = NOW() - WHERE id = $2 AND user_id = $3 - `, imagePath, wishlistID, userID) + WHERE id = $2 + `, imagePath, wishlistID) if err != nil { log.Printf("Error updating database: %v", err) sendErrorWithCORS(w, "Error updating database", http.StatusInternalServerError) @@ -10555,27 +10751,27 @@ func (a *App) completeWishlistHandler(w http.ResponseWriter, r *http.Request) { return } - // Проверяем владельца - var ownerID int - err = a.DB.QueryRow(` - SELECT user_id FROM wishlist_items - WHERE id = $1 AND deleted = FALSE - `, itemID).Scan(&ownerID) - if err == sql.ErrNoRows || ownerID != userID { + // Проверяем доступ к желанию + hasAccess, _, _, err := a.checkWishlistAccess(itemID, userID) + if err == sql.ErrNoRows { sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound) return } if err != nil { - log.Printf("Error checking wishlist ownership: %v", err) - sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist ownership: %v", err), http.StatusInternalServerError) + log.Printf("Error checking wishlist access: %v", err) + sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist access: %v", err), http.StatusInternalServerError) + return + } + if !hasAccess { + sendErrorWithCORS(w, "Access denied", http.StatusForbidden) return } _, err = a.DB.Exec(` UPDATE wishlist_items SET completed = TRUE, updated_at = NOW() - WHERE id = $1 AND user_id = $2 - `, itemID, userID) + WHERE id = $1 + `, itemID) if err != nil { log.Printf("Error completing wishlist item: %v", err) @@ -10685,27 +10881,27 @@ func (a *App) uncompleteWishlistHandler(w http.ResponseWriter, r *http.Request) return } - // Проверяем владельца - var ownerID int - err = a.DB.QueryRow(` - SELECT user_id FROM wishlist_items - WHERE id = $1 AND deleted = FALSE - `, itemID).Scan(&ownerID) - if err == sql.ErrNoRows || ownerID != userID { + // Проверяем доступ к желанию + hasAccess, _, _, err := a.checkWishlistAccess(itemID, userID) + if err == sql.ErrNoRows { sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound) return } if err != nil { - log.Printf("Error checking wishlist ownership: %v", err) - sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist ownership: %v", err), http.StatusInternalServerError) + log.Printf("Error checking wishlist access: %v", err) + sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist access: %v", err), http.StatusInternalServerError) + return + } + if !hasAccess { + sendErrorWithCORS(w, "Access denied", http.StatusForbidden) return } _, err = a.DB.Exec(` UPDATE wishlist_items SET completed = FALSE, updated_at = NOW() - WHERE id = $1 AND user_id = $2 - `, itemID, userID) + WHERE id = $1 + `, itemID) if err != nil { log.Printf("Error uncompleting wishlist item: %v", err) diff --git a/play-life-web/package.json b/play-life-web/package.json index 3c915a8..6c7c0eb 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "3.14.4", + "version": "3.14.5", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/App.jsx b/play-life-web/src/App.jsx index e30a42e..87fb63f 100644 --- a/play-life-web/src/App.jsx +++ b/play-life-web/src/App.jsx @@ -914,6 +914,7 @@ function AppContent() { key={tabParams.wishlistId} onNavigate={handleNavigate} wishlistId={tabParams.wishlistId} + boardId={tabParams.boardId} onRefresh={() => setWishlistRefreshTrigger(prev => prev + 1)} /> diff --git a/play-life-web/src/components/WishlistDetail.jsx b/play-life-web/src/components/WishlistDetail.jsx index 639d89c..e86de53 100644 --- a/play-life-web/src/components/WishlistDetail.jsx +++ b/play-life-web/src/components/WishlistDetail.jsx @@ -8,7 +8,7 @@ import './TaskList.css' const API_URL = '/api/wishlist' -function WishlistDetail({ wishlistId, onNavigate, onRefresh }) { +function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId }) { const { authFetch, user } = useAuth() const [wishlistItem, setWishlistItem] = useState(null) const [loading, setLoading] = useState(true) @@ -51,7 +51,7 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh }) { }, [wishlistId, fetchWishlistDetail]) const handleEdit = () => { - onNavigate?.('wishlist-form', { wishlistId: wishlistId }) + onNavigate?.('wishlist-form', { wishlistId: wishlistId, boardId: boardId }) } const handleComplete = async () => { diff --git a/play-life-web/src/components/WishlistForm.jsx b/play-life-web/src/components/WishlistForm.jsx index ae6a148..7218fb3 100644 --- a/play-life-web/src/components/WishlistForm.jsx +++ b/play-life-web/src/components/WishlistForm.jsx @@ -31,6 +31,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b const [loadingWishlist, setLoadingWishlist] = useState(false) const [fetchingMetadata, setFetchingMetadata] = useState(false) const [restoredFromSession, setRestoredFromSession] = useState(false) // Флаг восстановления из sessionStorage + const [loadedWishlistData, setLoadedWishlistData] = useState(null) // Данные желания для последующего маппинга условий const fileInputRef = useRef(null) // Загрузка задач и проектов @@ -65,13 +66,41 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b return } - if (wishlistId !== undefined && wishlistId !== null && tasks.length > 0 && projects.length > 0) { + if (wishlistId !== undefined && wishlistId !== null) { + // Загружаем желание независимо от наличия задач и проектов loadWishlist() } else if (wishlistId === undefined || wishlistId === null) { // Сбрасываем форму при создании новой задачи resetForm() + setLoadedWishlistData(null) } - }, [wishlistId, tasks, projects, restoredFromSession]) + }, [wishlistId, restoredFromSession]) + + // Обновляем маппинг условий после загрузки задач и проектов + useEffect(() => { + // Если есть загруженные данные желания, но маппинг еще не выполнен, + // обновляем условия с правильным маппингом + if (loadedWishlistData && tasks.length > 0 && projects.length > 0) { + const data = loadedWishlistData + setName(data.name || '') + setPrice(data.price ? String(data.price) : '') + setLink(data.link || '') + setImageUrl(data.image_url || null) + if (data.unlock_conditions) { + setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({ + type: cond.type, + task_id: cond.type === 'task_completion' ? tasks.find(t => t.name === cond.task_name)?.id : null, + project_id: cond.type === 'project_points' ? projects.find(p => p.project_name === cond.project_name)?.project_id : null, + required_points: cond.required_points || null, + start_date: cond.start_date || null, + display_order: idx, + }))) + } else { + setUnlockConditions([]) + } + setLoadedWishlistData(null) // Очищаем после применения + } + }, [tasks, projects, loadedWishlistData]) // Сброс формы при размонтировании компонента или при изменении wishlistId на undefined useEffect(() => { @@ -199,22 +228,36 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b throw new Error('Ошибка загрузки желания') } const data = await response.json() - setName(data.name || '') - setPrice(data.price ? String(data.price) : '') - setLink(data.link || '') - setImageUrl(data.image_url || null) - setImageFile(null) // Сбрасываем imageFile при загрузке существующего желания - if (data.unlock_conditions) { - setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({ - type: cond.type, - task_id: cond.type === 'task_completion' ? tasks.find(t => t.name === cond.task_name)?.id : null, - project_id: cond.type === 'project_points' ? projects.find(p => p.project_name === cond.project_name)?.project_id : null, - required_points: cond.required_points || null, - start_date: cond.start_date || null, - display_order: idx, - }))) + + // Если задачи и проекты уже загружены, применяем данные сразу + // Иначе сохраняем данные для последующего применения + if (tasks.length > 0 && projects.length > 0) { + setName(data.name || '') + setPrice(data.price ? String(data.price) : '') + setLink(data.link || '') + setImageUrl(data.image_url || null) + setImageFile(null) // Сбрасываем imageFile при загрузке существующего желания + if (data.unlock_conditions) { + setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({ + type: cond.type, + task_id: cond.type === 'task_completion' ? tasks.find(t => t.name === cond.task_name)?.id : null, + project_id: cond.type === 'project_points' ? projects.find(p => p.project_name === cond.project_name)?.project_id : null, + required_points: cond.required_points || null, + start_date: cond.start_date || null, + display_order: idx, + }))) + } else { + setUnlockConditions([]) + } } else { - setUnlockConditions([]) + // Сохраняем данные для последующего применения после загрузки задач и проектов + setLoadedWishlistData(data) + // Применяем базовые данные сразу + setName(data.name || '') + setPrice(data.price ? String(data.price) : '') + setLink(data.link || '') + setImageUrl(data.image_url || null) + setImageFile(null) } } catch (err) { setError(err.message)